-
Notifications
You must be signed in to change notification settings - Fork 19
New State of Health #235
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
base: main
Are you sure you want to change the base?
New State of Health #235
Changes from 11 commits
0b47e2d
ac8c555
3a15ef7
d5811af
874923a
581230c
a5973f3
fc99a88
020033d
89af0bc
91c75e5
2e101fc
00e7ccd
6f912d3
44aeddb
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,174 @@ | ||
from collections import OrderedDict | ||
|
||
import microcontroller | ||
|
||
from pysquared.logger import Logger | ||
from pysquared.protos.imu import IMUProto | ||
from pysquared.protos.power_monitor import PowerMonitorProto | ||
from pysquared.protos.radio import RadioProto | ||
|
||
try: | ||
from typing import Any, OrderedDict | ||
|
||
from .nvm.counter import Counter | ||
from .nvm.flag import Flag | ||
|
||
except Exception: | ||
pass | ||
|
||
|
||
class StateOfHealth: | ||
def __init__( | ||
self, | ||
logger: Logger, | ||
battery_power_monitor: PowerMonitorProto, | ||
solar_power_monitor: PowerMonitorProto, | ||
radio_manager: RadioProto, | ||
imu_manager: IMUProto, | ||
boot_count: Flag, | ||
JamesDCowley marked this conversation as resolved.
Show resolved
Hide resolved
|
||
burned_flag: Flag, | ||
brownout_flag: Flag, | ||
fsk_flag: Flag, | ||
) -> None: | ||
self.logger: Logger = logger | ||
self.battery_power_monitor: PowerMonitorProto = battery_power_monitor | ||
self.solar_power_monitor: PowerMonitorProto = solar_power_monitor | ||
self.radio_manager: RadioProto = radio_manager | ||
self.imu_manager: IMUProto = imu_manager | ||
self.boot_count: Counter = boot_count | ||
self.burned_flag: Flag = burned_flag | ||
self.brownout_flag: Flag = brownout_flag | ||
self.fsk_flag: Flag = fsk_flag | ||
|
||
self.state: OrderedDict[str, Any] = OrderedDict( | ||
[ | ||
# init all values in ordered dict to None | ||
("system_voltage", None), | ||
("system_current", None), | ||
("solar_voltage", None), | ||
("solar_current", None), | ||
("battery_voltage", None), | ||
("radio_temperature", None), | ||
("radio_modulation", None), | ||
("microcontroller_temperature", None), | ||
("internal_temperature", None), | ||
("error_count", None), | ||
("boot_count", None), | ||
("burned_flag", None), | ||
("brownout_flag", None), | ||
("fsk_flag", None), | ||
] | ||
) | ||
|
||
def update(self): | ||
""" | ||
Update the state of health | ||
""" | ||
try: | ||
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. Looking over the calls in this try/except I think that none of these calls have the possibility of raising an exception. What are you seeing? |
||
self.state["system_voltage"] = self.system_voltage() | ||
self.state["system_current"] = self.current_draw() | ||
self.state["solar_voltage"] = self.solar_voltage() | ||
self.state["solar_current"] = self.charge_current() | ||
self.state["battery_voltage"] = self.battery_voltage() | ||
self.state["radio_temperature"] = self.radio_manager.get_temperature() | ||
self.state["radio_modulation"] = self.radio_manager.get_modulation() | ||
self.state["microcontroller_temperature"] = microcontroller.cpu.temperature | ||
self.state["internal_temperature"] = self.imu_manager.get_temperature() | ||
self.state["error_count"] = self.logger.get_error_count() | ||
self.state["boot_count"] = self.boot_count.get() | ||
self.state["burned_flag"] = self.burned_flag.get() | ||
self.state["brownout_flag"] = self.brownout_flag.get() | ||
self.state["fsk_flag"] = self.fsk_flag.get() | ||
|
||
except Exception as e: | ||
self.logger.error("Couldn't acquire data for state of health", err=e) | ||
|
||
self.logger.info("State of Health", state=self.state) | ||
|
||
def system_voltage(self) -> float | None: | ||
""" | ||
Get the system voltage | ||
|
||
:return: The system voltage in volts | ||
:rtype: float | None | ||
|
||
""" | ||
if self.battery_power_monitor is not None: | ||
voltage: float = 0 | ||
try: | ||
for _ in range(50): | ||
voltage += ( | ||
self.battery_power_monitor.get_bus_voltage() | ||
+ self.battery_power_monitor.get_shunt_voltage() | ||
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. Where does this idea for adding these two voltages together come from? 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. This was based on the legacy implementation that we took from Max Holliday's PyCubed! Good flag on this Nate, because looking at it again I realized that the implementation of This code should actually be changed so the # INA219 measure bus voltage on the load side. So PSU voltage = bus_voltage + shunt_voltage
print("Voltage (VIN+) : {:6.3f} V".format(bus_voltage + shunt_voltage))
|
||
) | ||
return voltage / 50 | ||
except Exception as e: | ||
self.logger.error("Couldn't acquire system voltage", err=e) | ||
|
||
def battery_voltage(self) -> float | None: | ||
""" | ||
Get the battery voltage | ||
|
||
:return: The battery voltage in volts | ||
:rtype: float | None | ||
|
||
""" | ||
if self.battery_power_monitor is not None: | ||
voltage: float = 0 | ||
try: | ||
for _ in range(50): | ||
voltage += self.battery_power_monitor.get_bus_voltage() | ||
return voltage / 50 + 0.2 # volts and correction factor | ||
nateinaction marked this conversation as resolved.
Show resolved
Hide resolved
|
||
except Exception as e: | ||
self.logger.error("Couldn't acquire battery voltage", err=e) | ||
|
||
def current_draw(self) -> float | None: | ||
""" | ||
Get the current draw | ||
|
||
:return: The current draw in amps | ||
:rtype: float | None | ||
|
||
""" | ||
if self.battery_power_monitor is not None: | ||
current: float = 0 | ||
try: | ||
for _ in range(50): | ||
current += self.battery_power_monitor.get_current() | ||
return current / 50 | ||
except Exception as e: | ||
self.logger.error("Couldn't acquire current draw", err=e) | ||
|
||
def charge_current(self) -> float | None: | ||
""" | ||
Get the charge current | ||
|
||
:return: The charge current in amps | ||
:rtype: float | None | ||
|
||
""" | ||
if self.solar_power_monitor is not None: | ||
current: float = 0 | ||
try: | ||
for _ in range(50): | ||
current += self.solar_power_monitor.get_current() | ||
return current / 50 | ||
except Exception as e: | ||
self.logger.error("Couldn't acquire charge current", err=e) | ||
|
||
def solar_voltage(self) -> float | None: | ||
""" | ||
Get the solar voltage | ||
|
||
:return: The solar voltage in volts | ||
:rtype: float | None | ||
|
||
""" | ||
if self.solar_power_monitor is not None: | ||
voltage: float = 0 | ||
try: | ||
for _ in range(50): | ||
voltage += self.solar_power_monitor.get_bus_voltage() | ||
return voltage / 50 | ||
except Exception as e: | ||
self.logger.error("Couldn't acquire solar voltage", err=e) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
from unittest.mock import MagicMock | ||
|
||
import microcontroller | ||
import pytest | ||
|
||
from mocks.circuitpython.byte_array import ByteArray | ||
from pysquared.logger import Logger | ||
from pysquared.nvm.counter import Counter | ||
from pysquared.nvm.flag import Flag | ||
from pysquared.protos.imu import IMUProto | ||
from pysquared.protos.power_monitor import PowerMonitorProto | ||
from pysquared.protos.radio import RadioProto | ||
from pysquared.state_of_health import StateOfHealth | ||
|
||
|
||
@pytest.fixture | ||
def state_of_health(): | ||
# Mock dependencies | ||
logger = MagicMock(spec=Logger) | ||
battery_power_monitor = MagicMock(spec=PowerMonitorProto) | ||
solar_power_monitor = MagicMock(spec=PowerMonitorProto) | ||
radio_manager = MagicMock(spec=RadioProto) | ||
imu_manager = MagicMock(spec=IMUProto) | ||
radio_manager.get_temperature = MagicMock(return_value=25.0) | ||
imu_manager.get_temperature = MagicMock(return_value=30.0) | ||
boot_count = Counter(index=0, datastore=ByteArray(size=8)) | ||
burned_flag = Flag(index=0, bit_index=1, datastore=ByteArray(size=8)) | ||
brownout_flag = Flag(index=0, bit_index=2, datastore=ByteArray(size=8)) | ||
fsk_flag = Flag(index=0, bit_index=3, datastore=ByteArray(size=8)) | ||
microcontroller.cpu = MagicMock() | ||
|
||
# Create StateOfHealth instance | ||
return StateOfHealth( | ||
logger=logger, | ||
battery_power_monitor=battery_power_monitor, | ||
solar_power_monitor=solar_power_monitor, | ||
radio_manager=radio_manager, | ||
imu_manager=imu_manager, | ||
boot_count=boot_count, | ||
burned_flag=burned_flag, | ||
brownout_flag=brownout_flag, | ||
fsk_flag=fsk_flag, | ||
) | ||
|
||
|
||
def test_system_voltage(state_of_health): | ||
state_of_health.battery_power_monitor.get_bus_voltage.return_value = 3.7 | ||
state_of_health.battery_power_monitor.get_shunt_voltage.return_value = 0.1 | ||
|
||
result = state_of_health.system_voltage() | ||
|
||
assert result == pytest.approx(3.8, rel=1e-2) | ||
assert state_of_health.battery_power_monitor.get_bus_voltage.call_count == 50 | ||
assert state_of_health.battery_power_monitor.get_shunt_voltage.call_count == 50 | ||
|
||
|
||
def test_battery_voltage(state_of_health): | ||
state_of_health.battery_power_monitor.get_bus_voltage.return_value = 3.7 | ||
|
||
result = state_of_health.battery_voltage() | ||
|
||
assert result == pytest.approx(3.9, rel=1e-2) | ||
assert state_of_health.battery_power_monitor.get_bus_voltage.call_count == 50 | ||
|
||
|
||
def test_current_draw(state_of_health): | ||
state_of_health.battery_power_monitor.get_current.return_value = 1.5 | ||
|
||
result = state_of_health.current_draw() | ||
|
||
assert result == pytest.approx(1.5, rel=1e-2) | ||
assert state_of_health.battery_power_monitor.get_current.call_count == 50 | ||
|
||
|
||
def test_charge_current(state_of_health): | ||
state_of_health.solar_power_monitor.get_current.return_value = 2.0 | ||
|
||
result = state_of_health.charge_current() | ||
|
||
assert result == pytest.approx(2.0, rel=1e-2) | ||
assert state_of_health.solar_power_monitor.get_current.call_count == 50 | ||
|
||
|
||
def test_solar_voltage(state_of_health): | ||
state_of_health.solar_power_monitor.get_bus_voltage.return_value = 5.0 | ||
|
||
result = state_of_health.solar_voltage() | ||
|
||
assert result == pytest.approx(5.0, rel=1e-2) | ||
assert state_of_health.solar_power_monitor.get_bus_voltage.call_count == 50 | ||
|
||
|
||
def test_update(state_of_health): | ||
# Mock return values for all dependencies | ||
state_of_health.system_voltage = MagicMock(return_value=3.8) | ||
state_of_health.current_draw = MagicMock(return_value=1.5) | ||
state_of_health.solar_voltage = MagicMock(return_value=5.0) | ||
state_of_health.charge_current = MagicMock(return_value=2.0) | ||
state_of_health.battery_voltage = MagicMock(return_value=3.9) | ||
state_of_health.radio_manager.get_temperature.return_value = 25.0 | ||
state_of_health.radio_manager.get_modulation.return_value = "FSK" | ||
state_of_health.imu_manager.get_temperature.return_value = 30.0 | ||
state_of_health.logger.get_error_count.return_value = 0 | ||
microcontroller.cpu.temperature = 45.0 | ||
|
||
state_of_health.update() | ||
|
||
assert state_of_health.state["system_voltage"] == 3.8 | ||
assert state_of_health.state["system_current"] == 1.5 | ||
assert state_of_health.state["solar_voltage"] == 5.0 | ||
assert state_of_health.state["solar_current"] == 2.0 | ||
assert state_of_health.state["battery_voltage"] == 3.9 | ||
assert state_of_health.state["radio_temperature"] == 25.0 | ||
assert state_of_health.state["radio_modulation"] == "FSK" | ||
assert state_of_health.state["internal_temperature"] == 30.0 | ||
assert state_of_health.state["microcontroller_temperature"] == 45.0 | ||
assert state_of_health.state["error_count"] == 0 | ||
assert state_of_health.state["boot_count"] == 0 | ||
assert state_of_health.state["burned_flag"] is False | ||
assert state_of_health.state["brownout_flag"] is False | ||
assert state_of_health.state["fsk_flag"] is False |
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.
I like that you're removing things from
functions.py
. Thank you!