/************************************************************************** * * Copyright (c) 2020-2022 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 ROPump.c * * @author (last) Bill Bracken * @date (last) 25-Aug-2022 * * @author (original) Sean * @date (original) 04-Apr-2020 * ***************************************************************************/ #include #include "etpwm.h" #include "mibspi.h" #include "ConcentratePumps.h" #include "FlowSensors.h" #include "FPGA.h" #include "InternalADC.h" #include "NVDataMgmt.h" #include "MessageSupport.h" #include "OperationModes.h" #include "PersistentAlarm.h" #include "PIControllers.h" #include "Pressures.h" #include "ROPump.h" #include "SafetyShutdown.h" #include "SystemCommMessages.h" #include "TaskGeneral.h" #include "TaskPriority.h" #include "Timers.h" #include "Valves.h" /** * @addtogroup ROPump * @{ */ // ********** private definitions ********** #define RO_PUMP_DATA_PUB_INTERVAL ( MS_PER_SECOND / TASK_PRIORITY_INTERVAL ) ///< Interval (ms/task time) at which the RO Pump data is published on the CAN bus. #define MAX_RO_PUMP_DUTY_CYCLE 0.99F ///< Max duty cycle. #define MIN_RO_PUMP_DUTY_CYCLE 0.0F ///< Min duty cycle. #define ROP_CONTROL_INTERVAL ( MS_PER_SECOND / TASK_GENERAL_INTERVAL ) ///< Interval (ms/task time) at which the RO pump is controlled. #define ROP_RAMP_UP_CONTROL_INTERVAL ( 500 / TASK_GENERAL_INTERVAL ) ///< Interval (ms/task time) at which the RO pump is controlled. #define ROP_RAMP_UP_P_COEFFICIENT 0.22F ///< P term for RO pump ramp up to flow control. #define ROP_FLOW_CONTROL_P_COEFFICIENT 0.4F ///< P term for RO pump flow control. #define ROP_FLOW_CONTROL_I_COEFFICIENT 0.3F ///< I term for RO pump flow control. #define ROP_MAX_PRESSURE_P_COEFFICIENT 0.01F ///< P term for RO pump max pressure control. #define ROP_MAX_PRESSURE_I_COEFFICIENT 0.01F ///< I term for RO pump max pressure control. #define ROP_FLOW_TARGET_TOLERANCE 0.03F ///< Tolerance in between the target flow rate and the actual flow rate in percentage. #define ROP_RAMP_DOWN_DUTY_CYCLE_RATIO 0.03F ///< Pump ramp down duty cycle ratio when the pressure higher than max defined. #define FLOW_SENSOR_ZERO_READING 0xFFFF ///< Flow sensor reading indicates zero flow (or flow lower than can be detected by sensor). #define FLOW_SAMPLES_TO_AVERAGE ( 250 / TASK_PRIORITY_INTERVAL ) ///< Averaging flow data over 250 ms intervals. #define FLOW_AVERAGE_MULTIPLIER ( 1.0F / (F32)FLOW_SAMPLES_TO_AVERAGE ) ///< Optimization - multiplying is faster than dividing. // The ADC to LPM factor is calculated using the following steps: // 0.1 to 2 LPM range // 110000 pulses/liter // For 2 LPM => 2LPM x 110000 pulses/liter * 1 edges/pulse * 1 min/60 seconds = 3666.66 counts/sec => 272.72 microseconds => for 1 LPM = 136.36 counts #define RO_FLOW_ADC_TO_LPM_FACTOR 272.72F ///< Conversion factor from ADC counts to LPM (liters/min) for RO flow rate (multiply this by inverse of FPGA reading). #define ROP_FLOW_TO_PWM_SLOPE 0.1F ///< Slope of flow to PWM line equation. #define ROP_FLOW_TO_PWM_INTERCEPT 0.0F ///< Intercept of flow to PWM line equation. /// Initial conversion factor from target flow rate to PWM duty cycle estimate. #define ROP_FLOW_TO_PWM_DC(flow) ( ROP_FLOW_TO_PWM_SLOPE * flow + ROP_FLOW_TO_PWM_INTERCEPT ) #define MAX_ALLOWED_FLOW_DEVIATION 0.1F ///< Max allowed deviation from target flow. #define FLOW_OUT_OF_RANGE_PERSISTENT_INTERVAL ( 12 * MS_PER_SECOND ) ///< Flow out of range time out in counts. #define MAX_PRESSURE_TARGET_TOLERANCE 5 ///< Pressure tolerance from maximum set pressure by user in psi. #define MAX_ALLOWED_PRESSURE_PSI 130 ///< Maximum allowed pressure that the RO pump can go to. #define MIN_ALLOWED_PRESSURE_PSI 10 ///< Minimum allowed pressure that the RO pump can go to. #define MAX_ALLOWED_MEASURED_PRESSURE_PSI 135 ///< Maximum allowed pressure that the sensor measures. RO pump shut off pressure is 140psi. #define MAX_PRESSURE_OUT_OF_RANGE_PERSISTENT_INTERVAL MS_PER_SECOND ///< Maximum allowed time that the pressure can be very high. #define MAX_ALLOWED_RAMP_UP_TIME ( 10 * MS_PER_SECOND ) ///< Maximum allowed ramp up time to a flow rate in ms. #define ROP_PSI_TO_PWM_DC(p) ( 0.2F + ( (F32)((p) - 100) * 0.01F ) ) ///< Conversion factor from target PSI to PWM duty cycle estimate. #define SAFETY_SHUTDOWN_TIMEOUT ( 3 * MS_PER_SECOND ) ///< RO pump safety shutdown activation timeout in ms. #define ROP_FEEDBACK_0_PCT_DUTY_CYCLE_VOLTAGE 2.51F ///< RO pump 0% duty cycle feedback voltage. #define ROP_DUTY_CYCLE_OUT_OF_RANGE_TOLERANCE 0.05F ///< RO pump duty cycle out of range tolerance. #define DATA_PUBLISH_COUNTER_START_COUNT 50 ///< Data publish counter start count. /// Enumeration of RO pump states. typedef enum ROPump_States { RO_PUMP_OFF_STATE = 0, ///< RO pump off state. RO_PUMP_RAMP_UP_TO_TARGET_FLOW_STATE, ///< RO pump ramp up to target flow rate state. RO_PUMP_CONTROL_TO_TARGET_FLOW_STATE, ///< RO pump control to target flow state. RO_PUMP_CONTROL_TO_MAX_PRESSURE_STATE, ///< RO pump control to max pressure state. RO_PUMP_OPEN_LOOP_STATE, ///< RO pump open loop state. NUM_OF_RO_PUMP_STATES ///< Number of RO pump states. } RO_PUMP_STATE_T; /// Enumeration of RO pump self-test states. typedef enum ROPump_Self_Test_States { RO_PUMP_SELF_TEST_STATE_START = 0, ///< RO pump self-test start state. RO_PUMP_TEST_STATE_IN_PROGRESS, ///< RO pump self-tests in progress state. RO_PUMP_TEST_STATE_COMPLETE, ///< RO pump self-tests completed state. NUM_OF_RO_PUMP_SELF_TEST_STATES ///< Number of RO pump self-test states. } RO_PUMP_SELF_TEST_STATE_T; // ********** private data ********** static RO_PUMP_STATE_T roPumpState; ///< Current state of RO pump controller state machine. static U32 roPumpDataPublicationTimerCounter; ///< Used to schedule RO pump data publication to CAN bus. static BOOL isROPumpOn; ///< RO pump is currently running. static F32 roPumpPWMDutyCyclePct; ///< Initial RO pump PWM duty cycle. static F32 roPumpDutyCyclePctSet; ///< Currently set RO pump PWM duty cycle. static F32 roPumpFeedbackDutyCyclePct; ///< RO pump feedback duty cycle in percent. static PUMP_CONTROL_MODE_T roPumpControlMode; ///< Requested RO pump control mode. static F32 pendingROPumpCmdMaxPressure; ///< Delayed (pending) RO pump max pressure (in PSI) setting. static F32 pendingROPumpCmdTargetFlow; ///< Delayed (pending) RO pump target flow rate (in mL/min) setting. static U32 pendingROPumpCmdCountDown; ///< Delayed (pending) RO pump command count down timer (in task intervals). static F32 targetROPumpFlowRateLPM; ///< Target RO flow rate (in L/min). static F32 targetROPumpMaxPressure; ///< Target RO max allowed pressure (in PSI). static OVERRIDE_U32_T roPumpDataPublishInterval = { RO_PUMP_DATA_PUB_INTERVAL, RO_PUMP_DATA_PUB_INTERVAL, 0, 0 }; ///< Interval (in ms) at which to publish RO flow data to CAN bus. static OVERRIDE_F32_T measuredROFlowRateLPM = { 0.0, 0.0, 0.0, 0 }; ///< Measured RO flow rate (in L/min). static U32 roControlTimerCounter; ///< Determines when to perform control on RO pump. static F32 roPumpOpenLoopTargetDutyCycle; ///< Target RO pump open loop PWM. static S32 measuredFlowReadingsSum; ///< Raw flow reading sums for averaging. static U32 flowFilterCounter; ///< Flow filtering counter. static DG_FLOW_SENSORS_CAL_RECORD_T flowSensorsCalRecord; ///< Flow sensors calibration record. static OVERRIDE_F32_T measuredROFlowRateWithConcPumpsLPM = { 0.0, 0.0, 0.0, 0 }; ///< Measure RO flow rate with concentrate pumps (L/min). static DG_RO_PUMP_CAL_RECORD_T roPumpCalRecord; ///< RO pump calibration record. static F32 roVolumeL; ///< RO water generated in liters. // ********** private function prototypes ********** static RO_PUMP_STATE_T handleROPumpOffState( void ); static RO_PUMP_STATE_T handleROPumpRampUpToTargetFlowState( void ); static RO_PUMP_STATE_T handleROPumpControlToTargetFlowState( void ); static RO_PUMP_STATE_T handleROPumpControlToMaxPressureState( void ); static RO_PUMP_STATE_T handleROPumpOpenLoopState( void ); static void setROPumpTargetDutyCycle( F32 duty ); static void setROPumpControlSignalDutyCycle( F32 dutyCycle ); static void stopROPump( void ); static void publishROPumpData( void ); /*********************************************************************//** * @brief * The initROPump function initializes the RO Pump module. * @details Inputs: roControlTimerCounter,roPumpOpenLoopTargetDutyCycle, * roPumpFlowRateRunningSum, roPumpPressureRunningSum, measuredFlowReadingsSum, * flowFilterCounter, flowVerificationCounter, roPumpState, roPumpControlMode * roPumpDataPublicationTimerCounter, rawFlowLP, targetROPumpFlowRateLPM, * roPumpPWMDutyCyclePct, roPumpDutyCyclePctSet, pendingROPumpCmdMaxPressure, * pendingROPumpCmdTargetFlow, pendingROPumpCmdCountDown, targetROPumpMaxPressure * @details Outputs: none * @return none *************************************************************************/ void initROPump( void ) { stopROPump(); // Initialize RO pump PI controller to flow initializePIController( PI_CONTROLLER_ID_RO_PUMP_FLOW, MIN_RO_PUMP_DUTY_CYCLE, ROP_FLOW_CONTROL_P_COEFFICIENT, ROP_FLOW_CONTROL_I_COEFFICIENT, MIN_RO_PUMP_DUTY_CYCLE, MAX_RO_PUMP_DUTY_CYCLE ); // Initialize RO pump PI controller t maximum pressure initializePIController( PI_CONTROLLER_ID_RO_PUMP_MAX_PRES, MIN_RO_PUMP_DUTY_CYCLE, ROP_MAX_PRESSURE_P_COEFFICIENT, ROP_MAX_PRESSURE_I_COEFFICIENT, MIN_RO_PUMP_DUTY_CYCLE, MAX_RO_PUMP_DUTY_CYCLE ); // Initialize the persistent alarm for flow out of upper and lower range initPersistentAlarm( ALARM_ID_FLOW_RATE_OUT_OF_UPPER_RANGE, FLOW_OUT_OF_RANGE_PERSISTENT_INTERVAL, FLOW_OUT_OF_RANGE_PERSISTENT_INTERVAL ); initPersistentAlarm( ALARM_ID_FLOW_RATE_OUT_OF_LOWER_RANGE, FLOW_OUT_OF_RANGE_PERSISTENT_INTERVAL, FLOW_OUT_OF_RANGE_PERSISTENT_INTERVAL ); // Initialize the persistent alarm for max allowed pressure out of range initPersistentAlarm( ALARM_ID_RO_PUMP_PRESSURE_OUT_OF_RANGE, MAX_PRESSURE_OUT_OF_RANGE_PERSISTENT_INTERVAL, MAX_PRESSURE_OUT_OF_RANGE_PERSISTENT_INTERVAL ); // Initialize the persistent alarm for not turning off the pump initPersistentAlarm( ALARM_ID_RO_PUMP_DUTY_CYCLE_OUT_OF_RANGE, SAFETY_SHUTDOWN_TIMEOUT, SAFETY_SHUTDOWN_TIMEOUT ); // Initialize the variables roControlTimerCounter = 0; roPumpOpenLoopTargetDutyCycle = 0; measuredFlowReadingsSum = 0; flowFilterCounter = 0; roPumpDataPublicationTimerCounter = DATA_PUBLISH_COUNTER_START_COUNT; roPumpState = RO_PUMP_OFF_STATE; roPumpControlMode = NUM_OF_PUMP_CONTROL_MODES; isROPumpOn = FALSE; roPumpFeedbackDutyCyclePct = 0.0F; roVolumeL = 0.0F; targetROPumpFlowRateLPM = 0.0F; roPumpPWMDutyCyclePct = 0.0F; roPumpDutyCyclePctSet = 0.0F; pendingROPumpCmdMaxPressure = 0.0F; pendingROPumpCmdTargetFlow = 0.0F; pendingROPumpCmdCountDown = 0; targetROPumpMaxPressure = 0.0F; } /*********************************************************************//** * @brief * The setROPumpTargetFlowRate function sets a new target flow rate for the * RO pump. * @details Inputs: targetROPumpPressure, targetROPumpFlowRate, * roPumpControlMode, rampUp2FlowTimeoutCounter * @details Outputs: targetROPumpPressure, targetROPumpFlowRate, * roPumpControlMode, rampUp2FlowTimeoutCounter * @param roFlowRate which is target RO flow rate * @param maxPressure which is the maximum allowed pressure that the RO pump * can reach * @return TRUE if new target flow rate is set successfully, FALSE if not *************************************************************************/ BOOL setROPumpTargetFlowRateLPM( F32 roFlowRate, U32 maxPressure ) { BOOL result = FALSE; // First of all, the flow rate must be in range if ( ( roFlowRate <= MAX_RO_FLOWRATE_LPM ) && ( roFlowRate >= MIN_RO_FLOWRATE_LPM ) ) { // Then the max pressure that we are allowed to reach must be in range if ( ( maxPressure >= MIN_ALLOWED_PRESSURE_PSI ) && ( maxPressure <= MAX_ALLOWED_PRESSURE_PSI ) ) { // For now maximum allowed pressure is inserted into the target pressure override // if the target flow rate exceeded the max pressure, it will set the maximum pressure targetROPumpMaxPressure = maxPressure; targetROPumpFlowRateLPM = roFlowRate; roPumpControlMode = PUMP_CONTROL_MODE_CLOSED_LOOP; roPumpState = RO_PUMP_RAMP_UP_TO_TARGET_FLOW_STATE; // Get the initial guess of the duty cycle roPumpPWMDutyCyclePct = ROP_FLOW_TO_PWM_DC( roFlowRate ); roControlTimerCounter = 0; isROPumpOn = TRUE; result = TRUE; } // Requested max pressure is out of range else { SET_ALARM_WITH_2_U32_DATA( ALARM_ID_DG_SOFTWARE_FAULT, SW_FAULT_ID_RO_PUMP_INVALID_PRESSURE_SELECTED, maxPressure ) } } // Requested flow rate is out of range else { SET_ALARM_WITH_2_F32_DATA( ALARM_ID_DG_SOFTWARE_FAULT, SW_FAULT_ID_RO_PUMP_INVALID_FLOW_RATE_SET, roFlowRate ) } return result; } /*********************************************************************//** * @brief * The setROPumpTargetFlowRateDelayed function sets a new target flow rate for the * RO pump to be set after given delay. * @details Inputs: none * @details Outputs: pendingROPumpCmdMaxPressure, pendingROPumpCmdTargetFlow, * pendingROPumpCmdCountDown * @param roFlowRate which is target RO flow rate * @param maxPressure which is the maximum allowed pressure that the RO pump * can reach * @param delayMs delay duration (in ms) before RO pump started * @return TRUE if new target flow rate is set successfully, FALSE if not *************************************************************************/ BOOL setROPumpTargetFlowRateDelayed( F32 roFlowRate, U32 maxPressure, U32 delayMs ) { BOOL result = FALSE; // First of all, the flow rate must be in range if ( ( roFlowRate <= MAX_RO_FLOWRATE_LPM ) && ( roFlowRate >= MIN_RO_FLOWRATE_LPM ) ) { // Then the max pressure that we are allowed to reach must be in range if ( ( maxPressure >= MIN_ALLOWED_PRESSURE_PSI ) && ( maxPressure <= MAX_ALLOWED_PRESSURE_PSI ) ) { pendingROPumpCmdMaxPressure = (F32)maxPressure; pendingROPumpCmdTargetFlow = roFlowRate; pendingROPumpCmdCountDown = delayMs / TASK_GENERAL_INTERVAL; result = TRUE; } // Requested max pressure is out of range else { SET_ALARM_WITH_2_U32_DATA( ALARM_ID_DG_SOFTWARE_FAULT, SW_FAULT_ID_RO_PUMP_INVALID_PRESSURE_SELECTED, maxPressure ) } } // Requested flow rate is out of range else { SET_ALARM_WITH_2_F32_DATA( ALARM_ID_DG_SOFTWARE_FAULT, SW_FAULT_ID_RO_PUMP_INVALID_FLOW_RATE_SET, roFlowRate ) } return result; } /*********************************************************************//** * @brief * The signalROPumpHardStop function stops the RO pump immediately and * resets all the variables associated with the RO pump run. * @details Inputs: targetROPumpFlowRate, roPumpState, roPumpPWMDutyCyclePct, * roPumpOpenLoopTargetDutyCycle, roControlTimerCounter, flowVerificationCounter, * flowVerificationCounter, isROPumpOn, rampUp2FlowTimeoutCounter * @details Outputs: targetROPumpFlowRate, roPumpState, roPumpPWMDutyCyclePct, * roPumpOpenLoopTargetDutyCycle, roControlTimerCounter, * flowVerificationCounter, isROPumpOn, rampUp2FlowTimeoutCounter * @return none *************************************************************************/ void signalROPumpHardStop( void ) { stopROPump(); targetROPumpFlowRateLPM = 0; roPumpState = RO_PUMP_OFF_STATE; roPumpPWMDutyCyclePct = 0.0; roPumpOpenLoopTargetDutyCycle = 0.0; roControlTimerCounter = 0; isROPumpOn = FALSE; targetROPumpMaxPressure = 0; pendingROPumpCmdMaxPressure = 0.0; pendingROPumpCmdTargetFlow = 0.0; pendingROPumpCmdCountDown = 0; resetPIController( PI_CONTROLLER_ID_RO_PUMP_FLOW, MIN_RO_PUMP_DUTY_CYCLE ); } /*********************************************************************//** * @brief * The execROPumpMonitor function executes the RO pump monitor. The RO flow * sensor is read, filtered, converted to L/min and calibrated. * @details Inputs: measuredFlowReadingsSum, flowFilterCounter, * measuredROFlowRateLPM, measuredROFlowRateLPM, roPumpState, * flowOutOfRangeCounter, roPumpControlMode * @details Outputs: measuredFlowReadingsSum, flowFilterCounter, * measuredROFlowRateLPM, measuredROFlowRateLPM * @return none *************************************************************************/ void execROPumpMonitor( void ) { U32 roFlowReading = (U32)getFPGAROPumpFlowRate(); F32 roFeedbackVoltage = getIntADCVoltageConverted( INT_ADC_RO_PUMP_FEEDBACK_DUTY_CYCLE ); // Read the pressure at the sensor. The pump cannot be more that the maximum allowed pressure // to make sure the hardware (especially the ROF) is not damaged. If it is the case, we need to stop immediately F32 actualPressure = getMeasuredDGPressure( PRESSURE_SENSOR_RO_PUMP_OUTLET ); BOOL isPressureMax = ( actualPressure >= MAX_ALLOWED_MEASURED_PRESSURE_PSI ? TRUE : FALSE ); BOOL isDutyCylceOutOfRange = FALSE; // Update sum for flow average calculation measuredFlowReadingsSum += (S32)roFlowReading; checkPersistentAlarm( ALARM_ID_RO_PUMP_PRESSURE_OUT_OF_RANGE, isPressureMax, actualPressure, MAX_ALLOWED_MEASURED_PRESSURE_PSI ); // Check if a new calibration is available if ( TRUE == isNewCalibrationRecordAvailable() ) { getNVRecord2Driver( GET_CAL_FLOW_SENSORS, (U08*)&flowSensorsCalRecord, sizeof( DG_FLOW_SENSORS_CAL_RECORD_T ), NUM_OF_CAL_DATA_FLOW_SENSORS, ALARM_ID_DG_RO_FLOW_SENSOR_INVALID_CAL_RECORD ); } // Read flow at the control set if ( ++flowFilterCounter >= FLOW_SAMPLES_TO_AVERAGE ) { F32 flow = RO_FLOW_ADC_TO_LPM_FACTOR / ( (F32)measuredFlowReadingsSum * FLOW_AVERAGE_MULTIPLIER ); flow = pow(flow, 4) * flowSensorsCalRecord.flowSensors[ CAL_DATA_RO_PUMP_FLOW_SENSOR ].fourthOrderCoeff + pow(flow, 3) * flowSensorsCalRecord.flowSensors[ CAL_DATA_RO_PUMP_FLOW_SENSOR ].thirdOrderCoeff + pow(flow, 2) * flowSensorsCalRecord.flowSensors[ CAL_DATA_RO_PUMP_FLOW_SENSOR ].secondOrderCoeff + flow * flowSensorsCalRecord.flowSensors[ CAL_DATA_RO_PUMP_FLOW_SENSOR ].gain + flowSensorsCalRecord.flowSensors[ CAL_DATA_RO_PUMP_FLOW_SENSOR ].offset; measuredROFlowRateWithConcPumpsLPM.data = flow; measuredROFlowRateLPM.data = flow - ( getMeasuredPumpSpeed( CONCENTRATEPUMPS_CP1_ACID ) / ML_PER_LITER ) - ( getMeasuredPumpSpeed( CONCENTRATEPUMPS_CP2_BICARB ) / ML_PER_LITER ); // If the flow is less than a certain value, FPGA will return 0xFFFF meaning that // the flow is 0. if ( FLOW_SENSOR_ZERO_READING == roFlowReading ) { measuredROFlowRateLPM.data = 0.0; } measuredFlowReadingsSum = 0; flowFilterCounter = 0; } if ( ( measuredROFlowRateLPM.data > NEARLY_ZERO ) && ( (U32)VALVE_STATE_CLOSED == getValveState( (U32)VBF ) ) ) { roVolumeL += ( measuredROFlowRateLPM.data * SEC_PER_MIN ); } #ifndef _RELEASE_ if ( getSoftwareConfigStatus( SW_CONFIG_DISABLE_RO_PUMP_MONITOR ) != SW_CONFIG_ENABLE_VALUE ) #endif { // To monitor the flow, the control mode must be in closed loop mode and the pump should be control to flow state // If the pump is controlled to the maximum pressure, the flow might be different from the target flow for more than 10% // but the pump is not able to achieve the flow. if ( ( PUMP_CONTROL_MODE_CLOSED_LOOP == roPumpControlMode ) && ( RO_PUMP_CONTROL_TO_TARGET_FLOW_STATE == roPumpState ) ) { F32 currentFlow = getMeasuredROFlowRateLPM(); F32 targetFlow = getTargetROPumpFlowRateLPM(); // The flow cannot be out of range for than 10% of the target flow BOOL isFlowOutOfRange = ( fabs( 1.0F - ( currentFlow / targetFlow ) ) > MAX_ALLOWED_FLOW_DEVIATION ? TRUE : FALSE ); // Figure out whether flow is out of range from which side BOOL isFlowOutOfUpperRange = ( isFlowOutOfRange && ( currentFlow > targetFlow ) ? TRUE : FALSE ); BOOL isFlowOutOfLowerRange = ( isFlowOutOfRange && ( currentFlow < targetFlow ) ? TRUE : FALSE ); checkPersistentAlarm( ALARM_ID_FLOW_RATE_OUT_OF_UPPER_RANGE, isFlowOutOfUpperRange, currentFlow, targetFlow ); checkPersistentAlarm( ALARM_ID_FLOW_RATE_OUT_OF_LOWER_RANGE, isFlowOutOfLowerRange, currentFlow, targetFlow ); } #ifndef _RELEASE_ if ( getSoftwareConfigStatus( SW_CONFIG_ENABLE_V3_SYSTEM ) != SW_CONFIG_ENABLE_VALUE ) #endif { // The feedback voltage is on the 0V line so when the duty cycle is 0, the feedback is 2.5V // The duty cycle is calculated by getting the 1 - (ratio of feedback / to the voltage at 0 percent duty cycle). roPumpFeedbackDutyCyclePct = 1.0F - ( roFeedbackVoltage / ROP_FEEDBACK_0_PCT_DUTY_CYCLE_VOLTAGE ); isDutyCylceOutOfRange = ( fabs( roPumpFeedbackDutyCyclePct - roPumpDutyCyclePctSet ) > ROP_DUTY_CYCLE_OUT_OF_RANGE_TOLERANCE ? TRUE : FALSE ); checkPersistentAlarm( ALARM_ID_RO_PUMP_DUTY_CYCLE_OUT_OF_RANGE, isDutyCylceOutOfRange, roPumpFeedbackDutyCyclePct, roPumpDutyCyclePctSet ); // Check if it the alarm has timed out and if the pump is supposed to be off but it is still on, activate the safety shutdown if ( ( TRUE == isAlarmActive( ALARM_ID_RO_PUMP_DUTY_CYCLE_OUT_OF_RANGE ) ) && ( FALSE == isROPumpOn ) ) { activateSafetyShutdown(); } } } // Publish RO pump data on interval publishROPumpData(); } /*********************************************************************//** * @brief * The execROPumpController function executes the RO pump controller. * @details Inputs: roPumpState * @details Outputs: roPumpState * @return none *************************************************************************/ void execROPumpController( void ) { // Handle pending delayed RO pump command if ( pendingROPumpCmdCountDown > 0 ) { pendingROPumpCmdCountDown--; if ( 0 == pendingROPumpCmdCountDown ) { targetROPumpMaxPressure = pendingROPumpCmdMaxPressure; targetROPumpFlowRateLPM = pendingROPumpCmdTargetFlow; pendingROPumpCmdMaxPressure = 0.0; pendingROPumpCmdTargetFlow = 0.0; roPumpControlMode = PUMP_CONTROL_MODE_CLOSED_LOOP; roPumpState = RO_PUMP_RAMP_UP_TO_TARGET_FLOW_STATE; // Get the initial guess of the duty cycle roPumpPWMDutyCyclePct = ROP_FLOW_TO_PWM_DC( targetROPumpFlowRateLPM ); roControlTimerCounter = 0; isROPumpOn = TRUE; } } // Execute RO pump control state machine switch ( roPumpState ) { case RO_PUMP_OFF_STATE: roPumpState = handleROPumpOffState(); break; case RO_PUMP_RAMP_UP_TO_TARGET_FLOW_STATE: roPumpState = handleROPumpRampUpToTargetFlowState(); break; case RO_PUMP_CONTROL_TO_TARGET_FLOW_STATE: roPumpState = handleROPumpControlToTargetFlowState(); break; case RO_PUMP_CONTROL_TO_MAX_PRESSURE_STATE: roPumpState = handleROPumpControlToMaxPressureState(); break; case RO_PUMP_OPEN_LOOP_STATE: roPumpState = handleROPumpOpenLoopState(); break; default: SET_ALARM_WITH_2_U32_DATA( ALARM_ID_DG_SOFTWARE_FAULT, SW_FAULT_ID_RO_PUMP_INVALID_EXEC_STATE, roPumpState ) roPumpState = RO_PUMP_OFF_STATE; break; } } /*********************************************************************//** * @brief * The execROPumpSelfTest function executes the RO pump's self-test. * @details Inputs: none * @details Outputs: none * @return PressuresSelfTestResult (SELF_TEST_STATUS_T) *************************************************************************/ SELF_TEST_STATUS_T execROPumpSelfTest( void ) { SELF_TEST_STATUS_T result = SELF_TEST_STATUS_IN_PROGRESS; BOOL calStatus = FALSE; calStatus |= getNVRecord2Driver( GET_CAL_FLOW_SENSORS, (U08*)&flowSensorsCalRecord, sizeof( DG_FLOW_SENSORS_CAL_RECORD_T ), NUM_OF_CAL_DATA_FLOW_SENSORS, ALARM_ID_DG_RO_FLOW_SENSOR_INVALID_CAL_RECORD ); calStatus |= getNVRecord2Driver( GET_CAL_RO_PUMP_RECORD, (U08*)&roPumpCalRecord, sizeof( DG_RO_PUMP_CAL_RECORD_T ), NUM_OF_CAL_DATA_FLOW_SENSORS, ALARM_ID_NO_ALARM ); if ( TRUE == calStatus ) { result = SELF_TEST_STATUS_PASSED; } else { result = SELF_TEST_STATUS_FAILED; } return result; } /*********************************************************************//** * @brief * The isROPumpRunning function returns the on/off status of RO pump. * @details Inputs: isROPumpOn * @details Outputs: none * @return isROPumpOn the boolean flag that is TRUE if the pump is on and * FALSE if it is off *************************************************************************/ BOOL isROPumpRunning( void ) { return isROPumpOn; } /*********************************************************************//** * @brief * The getTargetROPumpFlowRateLPM function gets the current target RO pump * flow rate. * @details Inputs: targetROPumpFlowRateLPM * @details Outputs: none * @return the current target RO flow rate (in L/min). *************************************************************************/ F32 getTargetROPumpFlowRateLPM( void ) { return targetROPumpFlowRateLPM; } /*********************************************************************//** * @brief * The getMeasuredROFlowRateLPM function gets the measured RO pump flow rate. * @details Inputs: measuredROFlowRateLPM * @details Outputs: measuredROFlowRateLPM * @return the current RO pump flow rate (in L/min). *************************************************************************/ F32 getMeasuredROFlowRateLPM( void ) { return getF32OverrideValue( &measuredROFlowRateLPM ); } /*********************************************************************//** * @brief * The getMeasuredROFlowRateWithConcPumpsLPM function gets the measured RO * pump flow rate with the concentrate pumps. * @details Inputs: measuredROFlowRateWithConcPumpsLPM * @details Outputs: measuredROFlowRateWithConcPumpsLPM * @return the current RO pump flow rate with the concentrate pumps (in L/min). *************************************************************************/ F32 getMeasuredROFlowRateWithConcPumpsLPM( void ) { return getF32OverrideValue( &measuredROFlowRateWithConcPumpsLPM ); } /*********************************************************************//** * @brief * The getTargetROPumpPressure function gets the current target RO pump * pressure. * @details Inputs: targetROPumpPressure * @details Outputs: targetROPumpPressure * @return the current target RO targetROPumpPressure in psi. *************************************************************************/ F32 getTargetROPumpPressure( void ) { return targetROPumpMaxPressure; } /*********************************************************************//** * @brief * The getROGeneratedVolumeL function returns the RO generated volume in liters. * @details Inputs: none * @details Outputs: none * @return the RO generated volume in liters *************************************************************************/ F32 getROGeneratedVolumeL( void ) { return roVolumeL; } /*********************************************************************//** * @brief * The resetROGenerateVolumeL function resets the RO generated volume in liters. * @details Inputs: none * @details Outputs: roVolumeL * @return none *************************************************************************/ void resetROGenerateVolumeL( void ) { roVolumeL = 0.0; } /*********************************************************************//** * @brief * The handleROPumpOffState function handles the RO pump off state of the * controller state machine. * @details Inputs: roPumpControlMode, roPumpPWMDutyCyclePctSet, * roPumpOpenLoopTargetDutyCycle * @details Outputs: roPumpPWMDutyCyclePct, setupROPumpControl, isROPumpOn * @return next state of the controller state machine *************************************************************************/ static RO_PUMP_STATE_T handleROPumpOffState( void ) { RO_PUMP_STATE_T state = RO_PUMP_OFF_STATE; // If there is a flow, transition to the PI controller to get the corresponding pressure of that flow if ( getTargetROPumpFlowRateLPM() > 0.0F && roPumpControlMode == PUMP_CONTROL_MODE_CLOSED_LOOP ) { // Set pump to on isROPumpOn = TRUE; roPumpDutyCyclePctSet = ROP_FLOW_TO_PWM_DC( getTargetROPumpFlowRateLPM() ); setROPumpControlSignalDutyCycle( roPumpDutyCyclePctSet ); state = RO_PUMP_RAMP_UP_TO_TARGET_FLOW_STATE; } // If the target duty cycle is greater than zero (minimum is 10%) and the mode has been set to open // loop, set the duty cycle if ( roPumpOpenLoopTargetDutyCycle > 0.0F && roPumpControlMode == PUMP_CONTROL_MODE_OPEN_LOOP ) { setROPumpControlSignalDutyCycle( roPumpOpenLoopTargetDutyCycle ); isROPumpOn = TRUE; state = RO_PUMP_OPEN_LOOP_STATE; } return state; } /*********************************************************************//** * @brief * The handleROPumpRampUpToTargetFlowState function handles the RO pump * ramp up to flow state of the controller state machine. * @details Inputs: roControlTimerCounter, roPumpPWMDutyCyclePctSet, * rampUp2FlowTimeoutCounter * @details Outputs: roControlTimerCounter, roPumpPWMDutyCyclePctSet, * rampUp2FlowTimeoutCounter * @return next state of the controller state machine *************************************************************************/ static RO_PUMP_STATE_T handleROPumpRampUpToTargetFlowState( void ) { RO_PUMP_STATE_T state = RO_PUMP_RAMP_UP_TO_TARGET_FLOW_STATE; // Get the current pressure from the sensor F32 actualPressure = getMeasuredDGPressure( PRESSURE_SENSOR_RO_PUMP_OUTLET ); F32 targetFlowRate = getTargetROPumpFlowRateLPM(); F32 actualFlowRate = getMeasuredFlowRateLPM( RO_FLOW_SENSOR ); F32 flowRateDeviation = fabs( targetFlowRate - actualFlowRate ) / targetFlowRate; BOOL isFlowOutOfRange = flowRateDeviation > ROP_FLOW_TARGET_TOLERANCE; F32 targetPressure = getTargetROPumpPressure(); if ( ++roControlTimerCounter >= ROP_RAMP_UP_CONTROL_INTERVAL ) { // If the actual pressure is greater than the target pressure or it is within the tolerance of the maximum pressure, move to set // to target pressure straight. At the beginning the maximum pressure is set in the targetROPumpPressure override variable. // If the flow rate was reached without reaching to maximum pressure, the pressure that was set to targetROPumpPressure override will // be reset to the corresponding pressure of the target flow rate. if ( ( actualPressure > targetPressure ) || ( ( targetPressure - actualPressure ) < MAX_PRESSURE_TARGET_TOLERANCE ) ) { resetPIController( PI_CONTROLLER_ID_RO_PUMP_MAX_PRES, roPumpDutyCyclePctSet ); state = RO_PUMP_CONTROL_TO_MAX_PRESSURE_STATE; } // If the actual flow is still far from target flow, update the duty cycle using the I controller and stay in this state else if ( TRUE == isFlowOutOfRange ) { roPumpDutyCyclePctSet += ( targetFlowRate - actualFlowRate ) * ROP_RAMP_UP_P_COEFFICIENT; // If the duty cycle is out of the define range for the RO pump, set the boundaries roPumpDutyCyclePctSet = roPumpDutyCyclePctSet < MIN_RO_PUMP_DUTY_CYCLE ? MIN_RO_PUMP_DUTY_CYCLE : roPumpDutyCyclePctSet; roPumpDutyCyclePctSet = roPumpDutyCyclePctSet > MAX_RO_PUMP_DUTY_CYCLE ? MAX_RO_PUMP_DUTY_CYCLE : roPumpDutyCyclePctSet; setROPumpControlSignalDutyCycle( roPumpDutyCyclePctSet ); } // Reached to the target flow go to the next state else { resetPIController( PI_CONTROLLER_ID_RO_PUMP_FLOW, roPumpDutyCyclePctSet ); state = RO_PUMP_CONTROL_TO_TARGET_FLOW_STATE; } roControlTimerCounter = 0; } return state; } /*********************************************************************//** * @brief * The handleROPumpControlToTargetFlowState function handles the control to * target flow state of the RO pump controller state machine. * @details Inputs: roPumpPWMDutyCyclePctSet, roControlTimerCounter * @details Outputs: roPumpPWMDutyCyclePctSet, roControlTimerCounter * @return next state of the controller state machine *************************************************************************/ static RO_PUMP_STATE_T handleROPumpControlToTargetFlowState( void ) { RO_PUMP_STATE_T state = RO_PUMP_CONTROL_TO_TARGET_FLOW_STATE; // Control at set interval if ( ++roControlTimerCounter >= ROP_CONTROL_INTERVAL && roPumpControlMode == PUMP_CONTROL_MODE_CLOSED_LOOP ) { // Get the pressure to use it for setting the control F32 actualPressure = getMeasuredDGPressure( PRESSURE_SENSOR_RO_PUMP_OUTLET ); if ( actualPressure >= targetROPumpMaxPressure ) { resetPIController( PI_CONTROLLER_ID_RO_PUMP_MAX_PRES, roPumpDutyCyclePctSet ); state = RO_PUMP_CONTROL_TO_MAX_PRESSURE_STATE; } else { roPumpDutyCyclePctSet = runPIController( PI_CONTROLLER_ID_RO_PUMP_FLOW, getTargetROPumpFlowRateLPM(), getMeasuredFlowRateLPM( RO_FLOW_SENSOR ) ); } setROPumpControlSignalDutyCycle( roPumpDutyCyclePctSet ); roControlTimerCounter = 0; } return state; } /*********************************************************************//** * @brief * The handleROPumpControlToMaxPressureState function handles the control * to max pressure state of the RO pump controller state machine. * @details Inputs: roPumpPWMDutyCyclePctSet, roControlTimerCounter * @details Outputs: roPumpPWMDutyCyclePctSet, roControlTimerCounter * @return next state of the controller state machine *************************************************************************/ static RO_PUMP_STATE_T handleROPumpControlToMaxPressureState( void ) { RO_PUMP_STATE_T state = RO_PUMP_CONTROL_TO_MAX_PRESSURE_STATE; // Get the pressure to use it for setting the control F32 actualPressure = getMeasuredDGPressure( PRESSURE_SENSOR_RO_PUMP_OUTLET ); // Control at set interval if ( ++roControlTimerCounter >= ROP_CONTROL_INTERVAL && roPumpControlMode == PUMP_CONTROL_MODE_CLOSED_LOOP ) { if ( actualPressure > targetROPumpMaxPressure ) { roPumpDutyCyclePctSet -= ROP_RAMP_DOWN_DUTY_CYCLE_RATIO; resetPIController( PI_CONTROLLER_ID_RO_PUMP_MAX_PRES, roPumpDutyCyclePctSet ); } else { roPumpDutyCyclePctSet = runPIController( PI_CONTROLLER_ID_RO_PUMP_MAX_PRES, getTargetROPumpFlowRateLPM(), getMeasuredFlowRateLPM( RO_FLOW_SENSOR ) ); } setROPumpControlSignalDutyCycle( roPumpDutyCyclePctSet ); roControlTimerCounter = 0; } // Check if the actual pressure is out of tolerance from max pressure. // If it is out, go back to ramp up state and to try to reach to target flow again if ( ( targetROPumpMaxPressure - actualPressure ) > MAX_PRESSURE_TARGET_TOLERANCE ) { resetPIController( PI_CONTROLLER_ID_RO_PUMP_FLOW, roPumpDutyCyclePctSet ); state = RO_PUMP_CONTROL_TO_TARGET_FLOW_STATE; roControlTimerCounter = 0; } return state; } /*********************************************************************//** * @brief * The handleROPumpOpenLoopState function handles the open loop state of * the RO pump controller state machine. * @details Inputs: none * @details Outputs: none * @return next state of the controller state machine *************************************************************************/ static RO_PUMP_STATE_T handleROPumpOpenLoopState( void ) { return RO_PUMP_OPEN_LOOP_STATE; } /*********************************************************************//** * @brief * The setROPumpTargetDutyCycle function sets the duty cycle that the * pump should run. * @details Inputs: roPumpOpenLoopTargetDutyCycle, roPumpPWMDutyCyclePct, * roPumpPWMDutyCyclePctSet, roPumpControlMode * @details Outputs: roPumpOpenLoopTargetDutyCycle, roPumpPWMDutyCyclePct, * roPumpPWMDutyCyclePctSet, roPumpControlMode * @param: duty which is the duty cycle * @return none *************************************************************************/ static void setROPumpTargetDutyCycle( F32 duty ) { roPumpOpenLoopTargetDutyCycle = duty; roPumpPWMDutyCyclePct = roPumpOpenLoopTargetDutyCycle; roPumpDutyCyclePctSet = roPumpPWMDutyCyclePct; roPumpControlMode = PUMP_CONTROL_MODE_OPEN_LOOP; // Set the new duty cycle of the pump setROPumpControlSignalDutyCycle( roPumpOpenLoopTargetDutyCycle ); } /*********************************************************************//** * @brief * The setROPumpControlSignalDutyCycle function sets the duty cycle for the * RO pump to a given duty cycle. * @details Inputs: none * @details Outputs: none * @param dutyCycle which is the duty cycle * @return none *************************************************************************/ static void setROPumpControlSignalDutyCycle( F32 dutyCycle ) { etpwmSetCmpB( etpwmREG2, (U32)( (S32)( ( dutyCycle * (F32)(etpwmREG2->TBPRD) ) + FLOAT_TO_INT_ROUNDUP_OFFSET ) ) ); } /*********************************************************************//** * @brief * The stopROPump function sets the RO pump duty cycle to zero. * @details Inputs: isROPumpOn, roPumpPWMDutyCyclePctSet * @details Outputs: isROPumpOn, roPumpPWMDutyCyclePctSet * @return none *************************************************************************/ static void stopROPump( void ) { isROPumpOn = FALSE; roPumpDutyCyclePctSet = 0.0; etpwmSetCmpB( etpwmREG2, 0 ); } /*********************************************************************//** * @brief * The publishROPumpData function publishes RO pump data at the set interval. * @details Inputs: roPumpDataPublicationTimerCounter * @details Outputs: roPumpDataPublicationTimerCounter * @return none *************************************************************************/ static void publishROPumpData( void ) { // publish RO pump data on interval if ( ++roPumpDataPublicationTimerCounter >= getU32OverrideValue( &roPumpDataPublishInterval ) ) { RO_PUMP_DATA_T pumpData; pumpData.roPumpTgtFlowRateLM = getTargetROPumpFlowRateLPM(); pumpData.roPumpTgtPressure = getTargetROPumpPressure(); pumpData.measROFlowRate = getMeasuredFlowRateLPM( RO_FLOW_SENSOR ); //getMeasuredROFlowRateLPM(); pumpData.roPumpDutyCycle = roPumpDutyCyclePctSet * FRACTION_TO_PERCENT_FACTOR; pumpData.roPumpState = (U32)roPumpState; pumpData.roPumpFBDutyCycle = roPumpFeedbackDutyCyclePct * FRACTION_TO_PERCENT_FACTOR; pumpData.roPumpMeasFlowWithConcPumps = getMeasuredROFlowRateWithConcPumpsLPM(); broadcastData( MSG_ID_RO_PUMP_DATA, COMM_BUFFER_OUT_CAN_DG_BROADCAST, (U08*)&pumpData, sizeof( RO_PUMP_DATA_T ) ); roPumpDataPublicationTimerCounter = 0; } } /************************************************************************* * TEST SUPPORT FUNCTIONS *************************************************************************/ /*********************************************************************//** * @brief * The testSetROPumpDataPublishIntervalOverride function overrides the * RO pump data publish interval. * @details Inputs: roPumpDataPublishInterval * @details Outputs: roPumpDataPublishInterval * @param: value : override RO pump data publish interval with (in ms) * @return TRUE if override successful, FALSE if not *************************************************************************/ BOOL testSetROPumpDataPublishIntervalOverride( U32 value ) { BOOL result = FALSE; if ( TRUE == isTestingActivated() ) { U32 intvl = value / TASK_PRIORITY_INTERVAL; roPumpDataPublishInterval.ovData = intvl; roPumpDataPublishInterval.override = OVERRIDE_KEY; result = TRUE; } return result; } /*********************************************************************//** * @brief * The testResetROPumpDataPublishIntervalOverride function resets the * override of the RO pump data publish interval. * @details Inputs: roPumpDataPublishInterval * @details Outputs: roPumpDataPublishInterval * @return TRUE if override reset successful, FALSE if not *************************************************************************/ BOOL testResetROPumpDataPublishIntervalOverride( void ) { BOOL result = FALSE; if ( TRUE == isTestingActivated() ) { roPumpDataPublishInterval.override = OVERRIDE_RESET; roPumpDataPublishInterval.ovData = roPumpDataPublishInterval.ovInitData; result = TRUE; } return result; } /*********************************************************************//** * @brief * The testSetTargetROPumpFlow function overrides the target flow rate. * @details Inputs: none * @details Outputs: none * @param value which is override target RO flow rate (in L/min) * @return TRUE if override successful, FALSE if not *************************************************************************/ BOOL testSetTargetROPumpFlow( F32 flow ) { BOOL result = FALSE; if ( TRUE == isTestingActivated() ) { // The flow rate and pressure must be in range if ( ( flow <= MAX_RO_FLOWRATE_LPM ) && ( flow >= MIN_RO_FLOWRATE_LPM ) ) { result = setROPumpTargetFlowRateLPM( flow, MAX_ALLOWED_PRESSURE_PSI ); } // If the flow is less than a very small number, it means 0 LPM flow rate has been requested // and this is coming from Dialin so the user requested the pump to stop if ( fabs( flow ) < NEARLY_ZERO ) { signalROPumpHardStop(); result = TRUE; } } return result; } /*********************************************************************//** * @brief * The testSetTargetROPumpPressureOverride function overrides the target * RO pressure. * @details Inputs: targetROPumpPressure * @details Outputs: targetROPumpPressure * @param value override target RO pressure (in psi) * @return TRUE if override successful, FALSE if not *************************************************************************/ BOOL testSetTargetROPumpPressure( U32 value ) { BOOL result = FALSE; if ( TRUE == isTestingActivated() ) { // Make sure the requested pressure is in range if ( value >= MIN_ALLOWED_PRESSURE_PSI && value <= MAX_ALLOWED_PRESSURE_PSI ) { targetROPumpMaxPressure = value; result = TRUE; } } return result; } /*********************************************************************//** * @brief * The testSetMeasuredROFlowRateOverride function overrides the measured * RO flow rate. * @details Inputs: measuredROFlowRateLPM * @details Outputs: measuredROFlowRateLPM * @param: value : override measured RO pump motor speed (in L/min) * @return TRUE if override successful, FALSE if not *************************************************************************/ BOOL testSetMeasuredROFlowRateOverride( F32 value ) { BOOL result = FALSE; if ( TRUE == isTestingActivated() ) { measuredROFlowRateLPM.ovInitData = measuredROFlowRateLPM.data; measuredROFlowRateLPM.ovData = value; measuredROFlowRateLPM.override = OVERRIDE_KEY; result = TRUE; } return result; } /*********************************************************************//** * @brief * The testResetMeasuredROFlowRateOverride function resets the override * of the measured RO flow rate. * @details Inputs: measuredROFlowRateLPM * @details Outputs: measuredROFlowRateLPM * @return TRUE if override successful, FALSE if not *************************************************************************/ BOOL testResetMeasuredROFlowRateOverride( void ) { BOOL result = FALSE; if ( TRUE == isTestingActivated() ) { measuredROFlowRateLPM.data = measuredROFlowRateLPM.ovInitData; measuredROFlowRateLPM.override = OVERRIDE_RESET; measuredROFlowRateLPM.ovInitData = 0.0; measuredROFlowRateLPM.ovData = 0.0; result = TRUE; } return result; } /*********************************************************************//** * @brief * The testSetTargetDutyCycleOverride function overrides the target duty * cycle of the RO pump. * @details Inputs: none * @details Outputs: none * @param value which is the duty cycle to be set * @return TRUE if override successful, FALSE if not *************************************************************************/ BOOL testSetTargetDutyCycle( F32 value ) { BOOL result = FALSE; if ( TRUE == isTestingActivated() ) { // Check if duty cycle is within range if ( value >= MIN_RO_PUMP_DUTY_CYCLE && value <= MAX_RO_PUMP_DUTY_CYCLE ) { setROPumpTargetDutyCycle( value ); result = TRUE; } } return result; } /**@}*/