Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 100 additions & 3 deletions FprimeZephyrReference/Components/PowerMonitor/PowerMonitor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,19 @@
// ======================================================================

#include "FprimeZephyrReference/Components/PowerMonitor/PowerMonitor.hpp"
#include <Fw/Time/Time.hpp>

namespace Components {

// ----------------------------------------------------------------------
// Component construction and destruction
// ----------------------------------------------------------------------

PowerMonitor ::PowerMonitor(const char* const compName) : PowerMonitorComponentBase(compName) {}
PowerMonitor ::PowerMonitor(const char* const compName)
: PowerMonitorComponentBase(compName),
m_totalPower_mWh(0.0f),
m_totalGeneration_mWh(0.0f),
m_lastUpdateTime_s(0.0) {}

PowerMonitor ::~PowerMonitor() {}

Expand All @@ -23,12 +28,104 @@ void PowerMonitor ::run_handler(FwIndexType portNum, U32 context) {
// System Power Monitor Requests
this->sysVoltageGet_out(0);
this->sysCurrentGet_out(0);
this->sysPowerGet_out(0);
F64 sysPowerW = this->sysPowerGet_out(0);

// Solar Panel Power Monitor Requests
this->solVoltageGet_out(0);
this->solCurrentGet_out(0);
this->solPowerGet_out(0);
F64 solPowerW = this->solPowerGet_out(0);

// Update total power consumption with combined system and solar power
F64 totalPowerW = sysPowerW + solPowerW;
this->updatePower(totalPowerW);

// Update total solar power generation
this->updateGeneration(solPowerW);
}

// ----------------------------------------------------------------------
// Handler implementations for commands
// ----------------------------------------------------------------------

void PowerMonitor ::RESET_TOTAL_POWER_cmdHandler(FwOpcodeType opCode, U32 cmdSeq) {
this->m_totalPower_mWh = 0.0f;
this->m_lastUpdateTime_s = this->getCurrentTimeSeconds();
this->log_ACTIVITY_LO_TotalPowerReset();
this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK);
}

void PowerMonitor ::RESET_TOTAL_GENERATION_cmdHandler(FwOpcodeType opCode, U32 cmdSeq) {
this->m_totalGeneration_mWh = 0.0f;
this->log_ACTIVITY_LO_TotalGenerationReset();
this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK);
}

// ----------------------------------------------------------------------
// Helper method implementations
// ----------------------------------------------------------------------

F64 PowerMonitor ::getCurrentTimeSeconds() {
Fw::Time t = this->getTime();
return static_cast<F64>(t.getSeconds()) + (static_cast<F64>(t.getUSeconds()) / 1.0e6);
}

void PowerMonitor ::updatePower(F64 powerW) {
// Guard against invalid power values
if (powerW < 0.0 || powerW > 1000.0) { // Sanity check: power should be 0-1000W
return;
}

F64 now_s = this->getCurrentTimeSeconds();

// Initialize time on first call
if (this->m_lastUpdateTime_s == 0.0) {
this->m_lastUpdateTime_s = now_s;
// Emit initial telemetry value
this->tlmWrite_TotalPowerConsumption(this->m_totalPower_mWh);
return;
}

F64 dt_s = now_s - this->m_lastUpdateTime_s;

// Only accumulate if time has passed and delta is reasonable (< 10 seconds to avoid time jumps)
if (dt_s > 0.0 && dt_s < 10.0) {
// Convert to mWh: Power (W) * time (hours) * 1000
F32 energyAdded_mWh = static_cast<F32>(powerW * (dt_s / 3600.0) * 1000.0);
this->m_totalPower_mWh += energyAdded_mWh;
}

this->m_lastUpdateTime_s = now_s;

// Emit telemetry update
this->tlmWrite_TotalPowerConsumption(this->m_totalPower_mWh);
}

void PowerMonitor ::updateGeneration(F64 powerW) {
// Guard against invalid power values
if (powerW < 0.0 || powerW > 1000.0) { // Sanity check: power should be 0-1000W
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verify if the INA219Manager is able to return negative power numbers or if the number will always be positive. It is possible for the sensor to read a negative power number if power flows backwards through the shunt resistor.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The INA219 sensor can return negative power values when current flows backwards through the shunt resistor. The current implementation at line 105 rejects negative values with the guard if (powerW < 0.0 || powerW > 1000.0).

For solar power generation tracking, this guard may need adjustment depending on how the sensor is wired:

  • If wired to measure charging current as positive, negative values would indicate discharge and should be ignored for generation tracking
  • If wired the opposite way, the logic would need to be inverted

The current implementation assumes positive power = generation and rejects negative values. This should be verified against the actual hardware wiring. If negative values are valid for generation (e.g., sensor wired backwards), the guard should be changed to if (powerW > -1000.0 && powerW < 0.0) and use abs(powerW) for accumulation, or adjust based on the specific wiring configuration.

return;
}

F64 now_s = this->getCurrentTimeSeconds();

// Initialize time on first call
if (this->m_lastUpdateTime_s == 0.0) {
// Emit initial telemetry value
this->tlmWrite_TotalPowerGenerated(this->m_totalGeneration_mWh);
return;
}

F64 dt_s = now_s - this->m_lastUpdateTime_s;

// Only accumulate if time has passed and delta is reasonable (< 10 seconds to avoid time jumps)
if (dt_s > 0.0 && dt_s < 10.0) {
// Convert to mWh: Power (W) * time (hours) * 1000
F32 energyAdded_mWh = static_cast<F32>(powerW * (dt_s / 3600.0) * 1000.0);
this->m_totalGeneration_mWh += energyAdded_mWh;
}

// Emit telemetry update
this->tlmWrite_TotalPowerGenerated(this->m_totalGeneration_mWh);
}

} // namespace Components
40 changes: 40 additions & 0 deletions FprimeZephyrReference/Components/PowerMonitor/PowerMonitor.fpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,51 @@ module Components {
@ Port for sending powerGet calls to the Solar Panel Driver
output port solPowerGet: Drv.PowerGet

@ Command to reset the accumulated power consumption
sync command RESET_TOTAL_POWER()

@ Command to reset the accumulated power generation
sync command RESET_TOTAL_GENERATION()

@ Telemetry channel for accumulated power consumption in mWh
telemetry TotalPowerConsumption: F32

@ Telemetry channel for accumulated solar power generation in mWh
telemetry TotalPowerGenerated: F32

@ Event logged when total power consumption is reset
event TotalPowerReset() \
severity activity low \
format "Total power consumption reset to 0 mWh"

@ Event logged when total power generation is reset
event TotalGenerationReset() \
severity activity low \
format "Total power generation reset to 0 mWh"

###############################################################################
# Standard AC Ports: Required for Channels, Events, Commands, and Parameters #
###############################################################################
@ Port for requesting the current time
time get port timeCaller

@ Port for sending command registrations
command reg port cmdRegOut

@ Port for receiving commands
command recv port cmdIn

@ Port for sending command responses
command resp port cmdResponseOut

@ Port for sending textual representation of events
text event port logTextOut

@ Port for sending events to downlink
event port logOut

@ Port for sending telemetry channels to downlink
telemetry port tlmOut

}
}
40 changes: 40 additions & 0 deletions FprimeZephyrReference/Components/PowerMonitor/PowerMonitor.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,46 @@ class PowerMonitor final : public PowerMonitorComponentBase {
void run_handler(FwIndexType portNum, //!< The port number
U32 context //!< The call order
) override;

// ----------------------------------------------------------------------
// Handler implementations for commands
// ----------------------------------------------------------------------

//! Handler implementation for RESET_TOTAL_POWER
void RESET_TOTAL_POWER_cmdHandler(FwOpcodeType opCode, //!< The opcode
U32 cmdSeq //!< The command sequence number
) override;

//! Handler implementation for RESET_TOTAL_GENERATION
void RESET_TOTAL_GENERATION_cmdHandler(FwOpcodeType opCode, //!< The opcode
U32 cmdSeq //!< The command sequence number
) override;

// ----------------------------------------------------------------------
// Helper methods
// ----------------------------------------------------------------------

//! Get current time in seconds
F64 getCurrentTimeSeconds();

//! Update power consumption with new power reading
void updatePower(F64 powerW);

//! Update solar power generation with new power reading
void updateGeneration(F64 powerW);

// ----------------------------------------------------------------------
// Member variables
// ----------------------------------------------------------------------

//! Accumulated power consumption in mWh
F32 m_totalPower_mWh;

//! Accumulated solar power generation in mWh
F32 m_totalGeneration_mWh;

//! Last update time in seconds
F64 m_lastUpdateTime_s;
};

} // namespace Components
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ telemetry packets ReferenceDeploymentPackets {
ReferenceDeployment.ina219SolManager.Voltage
ReferenceDeployment.ina219SolManager.Current
ReferenceDeployment.ina219SolManager.Power
ReferenceDeployment.powerMonitor.TotalPowerConsumption
ReferenceDeployment.powerMonitor.TotalPowerGenerated
}

} omit {
Expand Down
64 changes: 57 additions & 7 deletions FprimeZephyrReference/test/int/power_monitor_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
Integration tests for the Power Monitor component.
"""

import time
from datetime import datetime

import pytest
Expand All @@ -14,6 +15,7 @@

ina219SysManager = "ReferenceDeployment.ina219SysManager"
ina219SolManager = "ReferenceDeployment.ina219SolManager"
powerMonitor = "ReferenceDeployment.powerMonitor"


@pytest.fixture(autouse=True)
Expand All @@ -27,7 +29,7 @@ def send_packet(fprime_test_api: IntegrationTestAPI, start_gds):


def test_01_power_manager_telemetry(fprime_test_api: IntegrationTestAPI, start_gds):
"""Test that we can get Acceleration telemetry"""
"""Test that we can get power telemetry from INA219 managers"""
start: TimeType = TimeType().set_datetime(datetime.now())
sys_voltage: ChData = fprime_test_api.assert_telemetry(
f"{ina219SysManager}.Voltage", start=start, timeout=65
Expand All @@ -48,16 +50,64 @@ def test_01_power_manager_telemetry(fprime_test_api: IntegrationTestAPI, start_g
f"{ina219SolManager}.Power", start=start, timeout=65
)

# TODO: Fix the power readings once INA219 power calculation is verified
sys_voltage_reading: dict[float] = sys_voltage.get_val()
sys_current_reading: dict[float] = sys_current.get_val()
# sys_power_reading: dict[float] = sys_power.get_val()
sol_voltage_reading: dict[float] = sol_voltage.get_val()
sol_current_reading: dict[float] = sol_current.get_val()
# sol_power_reading: dict[float] = sol_power.get_val()

assert sys_voltage_reading != 0, "Acceleration reading should be non-zero"
assert sys_current_reading != 0, "Acceleration reading should be non-zero"
# assert sys_power_reading != 0, "Acceleration reading should be non-zero"
assert sol_voltage_reading != 0, "Acceleration reading should be non-zero"
assert sol_current_reading != 0, "Acceleration reading should be non-zero"
# assert sol_power_reading != 0, "Acceleration reading should be non-zero"
assert sys_voltage_reading != 0, "System voltage reading should be non-zero"
assert sys_current_reading != 0, "System current reading should be non-zero"
# assert sys_power_reading != 0, "System power reading should be non-zero"
assert sol_voltage_reading != 0, "Solar voltage reading should be non-zero"
assert sol_current_reading != 0, "Solar current reading should be non-zero"
# assert sol_power_reading != 0, "Solar power reading should be non-zero"


def test_02_total_power_consumption_telemetry(
fprime_test_api: IntegrationTestAPI, start_gds
):
"""Test that TotalPowerConsumption telemetry is being updated"""
total_power: ChData = fprime_test_api.assert_telemetry(
f"{powerMonitor}.TotalPowerConsumption", start="NOW", timeout=65
)

total_power_reading: float = total_power.get_val()

# Total power should be non-zero (accumulating over time)
assert total_power_reading != 0, "Total power consumption should be non-zero"


def test_03_reset_total_power_command(fprime_test_api: IntegrationTestAPI, start_gds):
"""Test that RESET_TOTAL_POWER command resets accumulated energy"""
# Wait for some power to accumulate
time.sleep(3)

# Reset total power
proves_send_and_assert_command(
fprime_test_api,
f"{powerMonitor}.RESET_TOTAL_POWER",
[],
)

# Verify event was logged
fprime_test_api.assert_event(f"{powerMonitor}.TotalPowerReset", timeout=3)

# Wait for next telemetry update
time.sleep(2)

# Get total power after reset - should be very small (close to 0)
# Allow small value due to time between reset and next telemetry update
total_power_after: ChData = fprime_test_api.assert_telemetry(
f"{powerMonitor}.TotalPowerConsumption", start="NOW", timeout=65
)

total_power_after_reading: float = total_power_after.get_val()

# After reset and 2 seconds of accumulation, power should still be very small
# At 10W total power, 2 seconds = 0.0056 mWh, so 0.01 mWh is a reasonable threshold
assert total_power_after_reading < 0.01, (
f"Total power after reset should be near 0, got {total_power_after_reading} mWh"
)
Loading