Ver código fonte

ref: rename battery to cell. remove some placeholers. adapt mqtt subscription

Silas Gruen 10 meses atrás
pai
commit
c9210ea76f

+ 6 - 6
battery_measure_ctrl/config/config.yaml

@@ -5,6 +5,8 @@ mqtt:
   topic_prefix: "cells_inserted"
   client_id: "battery_measure_ctrl"
   keepalive: 60
+  username: "robot"
+  password: "robot"
 
 i2c:
   debug: true
@@ -26,12 +28,10 @@ measurement:
   rest_time_minutes: 30
 
 devices:
-  - id: "device_1"
-    i2c_address: 0x48
-    slots: 4
-  - id: "device_2"
-    i2c_address: 0x49
-    slots: 4
+  - i2c_address: 0x40
+    temp_sensors: [0x48, 0x49, 0x4A]
+  - i2c_address: 0x41
+    temp_sensors: [0x4B, 0x4C, 0x4D]
 
 logging:
   level: "INFO"

+ 0 - 33
battery_measure_ctrl/src/controllers/device_controller.py

@@ -1,33 +0,0 @@
-import paho.mqtt.client as mqtt
-from src.models.device import Device
-
-class DeviceController:
-    def __init__(self, mqtt_broker, devices_config):
-        self.devices = [Device(config) for config in devices_config]
-        self.mqtt_client = mqtt.Client()
-        self.mqtt_broker = mqtt_broker
-        self.setup_mqtt()
-
-    def setup_mqtt(self):
-        self.mqtt_client.on_connect = self.on_connect
-        self.mqtt_client.on_message = self.on_message
-        self.mqtt_client.connect(self.mqtt_broker)
-        self.mqtt_client.loop_start()
-
-    def on_connect(self, client, userdata, flags, rc):
-        print("Connected to MQTT broker with result code " + str(rc))
-        for device in self.devices:
-            client.subscribe(f"cells_inserted/{device.device_id}")
-
-    def on_message(self, client, userdata, msg):
-        payload = json.loads(msg.payload)
-        slot = payload['slot']
-        cell_id = payload['cell_id']
-        self.handle_cell_insertion(msg.topic, slot, cell_id)
-
-    def handle_cell_insertion(self, topic, slot, cell_id):
-        device_id = topic.split('/')[-1]
-        device = next((d for d in self.devices if d.device_id == device_id), None)
-        if device:
-            device.insert_cell(slot, cell_id)
-            print(f"Cell {cell_id} inserted in device {device_id} at slot {slot}.")

+ 42 - 7
battery_measure_ctrl/src/controllers/measurement_controller.py

@@ -2,24 +2,59 @@ import asyncio
 import logging
 from typing import Dict, List
 from datetime import datetime
-from models.battery import Battery
+import json
+from models.cell import Cell
 from models.device import Device
 from services.i2c_service import I2CService
 from services.http_service import HTTPService
-from utils.health_calculator import calculate_health
+from services.mqtt_service import MQTTService
 
 logger = logging.getLogger(__name__)
 
 class MeasurementController:
     """Controls the measurement process for multiple devices and slots."""
     
-    def __init__(self, config: dict, i2c_service: I2CService, http_service: HTTPService):
+    def __init__(self, config: dict, i2c_service: I2CService, http_service: HTTPService, mqtt_service: MQTTService):
         self.config = config
         self.i2c_service = i2c_service
         self.http_service = http_service
+        self.mqtt_service = mqtt_service
+
         self.active_measurements: Dict[str, Dict[int, asyncio.Task]] = {}
+
+        devices_config = config['devices']
+        self.devices = {id: Device(id, config) for id, config in enumerate(devices_config)}
         
-    async def start_measurement(self, device_id: str, slot: int, cell_id: int):
+        self.topic_prefix = self.config['mqtt']['topic_prefix']
+        for device_id in self.devices.keys():
+            self.setup_mqtt_subscription(device_id)
+
+    def setup_mqtt_subscription(self, device_id):
+        """Setup MQTT subscriptions for each device."""
+        topic = f"{self.topic_prefix}/{device_id}"
+        self.mqtt_service.add_message_handler(topic, lambda client, userdata, msg, dev_id=device_id: self._handle_cell_insertion(client, userdata, msg, dev_id))
+
+    def _handle_cell_insertion(self, client, userdata, message: str, device_id: int):
+        """Handle MQTT message for cell insertion."""
+        try:
+            data = json.loads(message.payload)
+            slot = data.get('slot')
+            cell_id = data.get('cell_id')
+            
+            if slot is None or cell_id is None:
+                logger.error(f"Invalid message format: {message.payload}")
+                return
+
+            # Create and schedule the measurement task
+            asyncio.create_task(self.start_measurement(device_id, slot, cell_id))
+            logger.info(f"Initiated measurement for device {device_id}, slot {slot}, cell {cell_id}")
+            
+        except json.JSONDecodeError:
+            logger.error(f"Invalid JSON in MQTT message: {message}")
+        except Exception as e:
+            logger.error(f"Error handling cell insertion: {str(e)}")
+
+    async def start_measurement(self, device_id: int, slot: int, cell_id: int):
         """Start measurement cycle for a specific slot."""
         if device_id not in self.active_measurements:
             self.active_measurements[device_id] = {}
@@ -35,10 +70,10 @@ class MeasurementController:
         self.active_measurements[device_id][slot] = task
         
     async def _measure_cycle(self, device_id: str, slot: int, cell_id: int):
-        """Execute measurement cycles for a battery."""
+        """Execute measurement cycles for a Cell."""
         try:
-            # Get battery info from HTTP service
-            battery_info = await self.http_service.get_cell_info(cell_id)
+            # Get Cell info from HTTP service
+            cell_info = await self.http_service.get_cell_info(cell_id)
             
             measurements = []
             cycles = self.config['measurement']['cycles']

+ 5 - 9
battery_measure_ctrl/src/main.py

@@ -35,17 +35,13 @@ async def main():
 
     # Initialize services
     i2c_service = I2CService(config)
-    http_service = HTTPService(config)
-    measurement_controller = MeasurementController(config, i2c_service, http_service)
-
-    # Setup MQTT client with callback
     mqtt_service = MQTTService(config)
-    
-    async def on_cell_inserted(device_id: str, slot: int, cell_id: int):
-        await measurement_controller.start_measurement(device_id, slot, cell_id)
+    http_service = HTTPService(config)
+    MeasurementController(config, i2c_service, http_service, mqtt_service)
 
-    mqtt_service.set_callback(on_cell_inserted)
-    await mqtt_service.connect()
+    mqtt_service.connect()
+    mqtt_service.subscribe_defined()
+    mqtt_service.loop_start()
 
     try:
         # Keep running until interrupted

+ 0 - 22
battery_measure_ctrl/src/models/battery.py

@@ -1,22 +0,0 @@
-class Battery:
-    def __init__(self, voltage, capacity):
-        self.voltage = voltage
-        self.capacity = capacity
-
-    def estimate_health(self, current_measurements):
-        """
-        Estimate the state of health of the battery based on current measurements.
-        
-        Parameters:
-        current_measurements (list): A list of current measurements during the cycles.
-
-        Returns:
-        float: Estimated state of health as a percentage.
-        """
-        # Basic health estimation logic (placeholder)
-        if not current_measurements:
-            return 0.0
-        
-        avg_current = sum(current_measurements) / len(current_measurements)
-        health = max(0, min(100, (self.capacity - avg_current) / self.capacity * 100))
-        return health

+ 24 - 0
battery_measure_ctrl/src/models/cell.py

@@ -0,0 +1,24 @@
+import logging
+
+logger = logging.getLogger(__name__)
+class Cell():
+    
+    def __init__(self, id: int, min_voltage: float, max_voltage: float, nom_capacity: float, estimated_health: float=-1.0):
+        self.id = id
+        self.min_voltage = min_voltage
+        self.max_voltage = max_voltage
+        self.nom_capacity = nom_capacity
+        self.estimated_health = estimated_health # -1.0 indicates unknown health
+        self.measurements = []
+
+    def estimate_health(self):
+        """
+        Estimate and update the state of health of the cell based on current measurements.
+        """
+        # TODO [SG]: Placeholder for health estimation logic
+        if not self.measurements:
+            logger.warning("No measurements available for health estimation.")
+            return
+        
+        avg_current = sum(self.measurements) / float(len(self.measurements))
+        self.health = max(0.0, min(100.0, (self.nom_capacity - avg_current) / self.nom_capacity * 100.0))

+ 27 - 6
battery_measure_ctrl/src/models/device.py

@@ -1,14 +1,17 @@
-class Device:
-    def __init__(self, device_id, slots):
+from .cell import Cell
+
+class Device():
+    def __init__(self, id: int, config: dict):
         """
         Initializes a Device instance.
 
         :param device_id: Unique identifier for the device.
         :param slots: Number of slots available in the device.
         """
-        self.device_id = device_id
-        self.slots = slots
-        self.slot_data = {slot: None for slot in range(slots)}
+        self.id = id
+        self.i2c_address = config['i2c_address']
+        self.temp_sensors = config['temp_sensors']
+        self.slots = {Slot(id, adress) for id, adress in enumerate(self.temp_sensors)}
 
     def read_i2c_data(self):
         """
@@ -33,4 +36,22 @@ class Device:
         """
         Returns the current state of all slots in the device.
         """
-        return self.slot_data
+        return self.slot_data
+    
+class Slot():
+    def __init__(self, id: int, sensor_address: int):
+        self.id = id
+        self.cell = None
+        self.sensor_adress = sensor_address
+
+    def insert_cell(self, cell: Cell):
+        self.cell = cell
+
+    def remove_cell(self):
+        self.cell = None
+
+    def is_empty(self):
+        return self.cell is None
+
+    def get_cell(self):
+        return self.cell

+ 45 - 19
battery_measure_ctrl/src/services/mqtt_service.py

@@ -1,42 +1,68 @@
+import logging
 import paho.mqtt.client as mqtt
 import json
 from typing import Dict, Callable
 
+logger = logging.getLogger(__name__)
+
 class MQTTService:
     def __init__(self, config: dict):
         self.config = config
-        self.debug = config['mqtt'].get('debug', False)
+        # self.debug = config['mqtt'].get('debug', False)
         self.broker_address = config['mqtt']['broker_address']
-        self.subscribe_topics = config['mqtt'].get('subscribe_topics', [])
-        self.publish_topics = config['mqtt'].get('publish_topics', [])
-        self.client = mqtt.Client()
+        self.username = config['mqtt'].get('username')
+        self.password = config['mqtt'].get('password')
+        self.publish_topics = []
         self.message_handlers: Dict[str, Callable] = {}
+        
+        # Initialize MQTT client with credentials
+        self.client = mqtt.Client()
+        if self.username and self.password:
+            self.client.username_pw_set(self.username, self.password)
+        self.client.on_connect = self.on_connect
 
     def connect(self):
-        self.client.connect(self.broker_address)
+        try:
+            self.client.connect(self.broker_address)
+        except Exception as e:
+            logger.error(f"Failed to connect to MQTT broker: {str(e)}")
+            raise
 
-    def on_message(self, client, userdata, message):
-        topic = message.topic
-        payload = json.loads(message.payload.decode())
-        
-        if topic in self.message_handlers:
-            self.message_handlers[topic](payload)
+    def disconnect(self):
+        self.client.disconnect()
+
+    def on_connect(self, client, userdata, flags, rc):
+        if rc == 0:
+            logger.info("Successfully connected to MQTT broker")
+            self.subscribe_defined()
+        elif rc == 1:
+            logger.error("Connection refused - incorrect protocol version")
+        elif rc == 2:
+            logger.error("Connection refused - invalid client identifier")
+        elif rc == 3:
+            logger.error("Connection refused - server unavailable")
+        elif rc == 4:
+            logger.error("Connection refused - bad username or password")
+        elif rc == 5:
+            logger.error("Connection refused - not authorised")
         else:
-            print(f"Received message on topic {topic}: {payload}")
+            logger.error(f"Connection failed with code {rc}")
 
     def add_message_handler(self, topic: str, handler: Callable):
         self.message_handlers[topic] = handler
 
-    def subscribe(self):
-        for topic in self.subscribe_topics:
+    def subscribe_defined(self):
+        for topic, callback in self.message_handlers.items():
+            logger.debug(f"Subscribing to MQTT topic: {topic}")
             self.client.subscribe(topic)
-        self.client.on_message = self.on_message
+            self.client.message_callback_add(topic, callback)
 
     async def publish(self, topic: str, message: str):
-        if self.debug:
-            print(f"Debug MQTT publish to {topic}: {message}")
-            return
+        # if self.debug:
+        #     logger.info(f"Debug MQTT publish to {topic}: {message}")
+        #     return
         if topic in self.publish_topics:
+            logger.info(f"MQTT publish to {topic}: {message}")
             self.client.publish(topic, json.dumps(message))
         else:
             raise ValueError(f"Topic {topic} not in configured publish topics")
@@ -45,4 +71,4 @@ class MQTTService:
         self.client.loop_start()
 
     def loop_stop(self):
-        self.client.loop_stop()
+        self.client.loop_stop()

+ 0 - 1
battery_measure_ctrl/src/utils/__init__.py

@@ -1 +0,0 @@
-# This file is intentionally left blank.

+ 0 - 28
battery_measure_ctrl/src/utils/health_calculator.py

@@ -1,28 +0,0 @@
-def calculate_health(current_measurements, voltage_measurements, temperature_measurements):
-    """
-    Estimate the state of health (SoH) of a battery based on current, voltage, and temperature measurements.
-
-    Parameters:
-    - current_measurements: List of current measurements (in Amperes).
-    - voltage_measurements: List of voltage measurements (in Volts).
-    - temperature_measurements: List of temperature measurements (in Celsius).
-
-    Returns:
-    - health: Estimated state of health as a percentage.
-    """
-    # Basic checks
-    if not current_measurements or not voltage_measurements or not temperature_measurements:
-        return 0.0
-
-    # Calculate average values
-    avg_current = sum(current_measurements) / len(current_measurements)
-    avg_voltage = sum(voltage_measurements) / len(voltage_measurements)
-    avg_temperature = sum(temperature_measurements) / len(temperature_measurements)
-
-    # Simple health estimation logic (placeholder)
-    health = 100 - (avg_current * 0.1) - (avg_voltage * 0.05) + (avg_temperature * 0.1)
-
-    # Ensure health is within 0 to 100
-    health = max(0, min(100, health))
-
-    return health

+ 0 - 25
battery_measure_ctrl/tests/test_health_calculator.py

@@ -1,25 +0,0 @@
-import unittest
-from src.utils.health_calculator import calculate_health
-
-class TestHealthCalculator(unittest.TestCase):
-
-    def test_calculate_health(self):
-        # Test case for a healthy battery
-        current_measurements = [0.5, 0.5, 0.5]  # Example current measurements
-        expected_health = 100  # Expected health percentage
-        self.assertEqual(calculate_health(current_measurements), expected_health)
-
-    def test_calculate_health_low_capacity(self):
-        # Test case for a battery with low capacity
-        current_measurements = [0.1, 0.1, 0.1]  # Example current measurements
-        expected_health = 20  # Expected health percentage
-        self.assertEqual(calculate_health(current_measurements), expected_health)
-
-    def test_calculate_health_empty_measurements(self):
-        # Test case for empty measurements
-        current_measurements = []
-        expected_health = 0  # Expected health percentage
-        self.assertEqual(calculate_health(current_measurements), expected_health)
-
-if __name__ == '__main__':
-    unittest.main()