From 9e21f6a011ebab5f896e16ef5770a8ce7e55c61d Mon Sep 17 00:00:00 2001 From: marcel Date: Thu, 14 Aug 2025 11:45:47 +0200 Subject: [PATCH] ModBus software works, MQTT bridge software added. --- .../README.md | 0 software/mqqt_to_pe1rxf_telemetry/config.yaml | 37 +++ .../mqqt_to_pe1rxf_telemetry/config_reader.py | 90 ++++++++ .../modbus_control.py | 0 .../modbus_definition_file_reader.py | 0 .../modbus_registers.yaml | 0 .../mqtt_callbacks.py | 10 + .../mqtt_to_pe1rxf_telemetry.py | 215 ++++++++++++++++++ software/rs485_client_to_mqtt/README.md | 33 +++ .../config.yaml} | 13 -- .../config_reader.py | 17 -- .../rs485_client_to_mqtt/modbus_control.py | 107 +++++++++ .../modbus_definition_file_reader.py | 56 +++++ .../modbus_registers.yaml | 30 +++ .../mqtt_callbacks.py | 0 .../rs485_client_to_mqtt.py} | 62 +---- software/test_software/example.log | 15 -- .../test_software/weather_station_data.log | 2 - 18 files changed, 587 insertions(+), 100 deletions(-) rename software/{test_software => mqqt_to_pe1rxf_telemetry}/README.md (100%) create mode 100644 software/mqqt_to_pe1rxf_telemetry/config.yaml create mode 100644 software/mqqt_to_pe1rxf_telemetry/config_reader.py rename software/{test_software => mqqt_to_pe1rxf_telemetry}/modbus_control.py (100%) rename software/{test_software => mqqt_to_pe1rxf_telemetry}/modbus_definition_file_reader.py (100%) rename software/{test_software => mqqt_to_pe1rxf_telemetry}/modbus_registers.yaml (100%) create mode 100644 software/mqqt_to_pe1rxf_telemetry/mqtt_callbacks.py create mode 100644 software/mqqt_to_pe1rxf_telemetry/mqtt_to_pe1rxf_telemetry.py create mode 100644 software/rs485_client_to_mqtt/README.md rename software/{test_software/config.yml => rs485_client_to_mqtt/config.yaml} (54%) rename software/{test_software => rs485_client_to_mqtt}/config_reader.py (84%) create mode 100644 software/rs485_client_to_mqtt/modbus_control.py create mode 100644 software/rs485_client_to_mqtt/modbus_definition_file_reader.py create mode 100644 software/rs485_client_to_mqtt/modbus_registers.yaml rename software/{test_software => rs485_client_to_mqtt}/mqtt_callbacks.py (100%) rename software/{test_software/weather_station_rs485_client.py => rs485_client_to_mqtt/rs485_client_to_mqtt.py} (86%) delete mode 100644 software/test_software/example.log delete mode 100644 software/test_software/weather_station_data.log diff --git a/software/test_software/README.md b/software/mqqt_to_pe1rxf_telemetry/README.md similarity index 100% rename from software/test_software/README.md rename to software/mqqt_to_pe1rxf_telemetry/README.md diff --git a/software/mqqt_to_pe1rxf_telemetry/config.yaml b/software/mqqt_to_pe1rxf_telemetry/config.yaml new file mode 100644 index 0000000..c4400c0 --- /dev/null +++ b/software/mqqt_to_pe1rxf_telemetry/config.yaml @@ -0,0 +1,37 @@ +# This is the configuration file for the Mees Electronics weather station MK2 + +# Global settings +global: + program-log: 0 # All program output will be written to this file (0 = do not log to file) + #program-log: /home/marcel/rs458.log # All program output will be written to this file (0 = do not log to file) + mqtt-server: mqtt.meezenest.nl + mqtt-port: 1883 + +# APRS settings +aprs: + - port: ax1 # Linux AX.25 port to which APRS weather report is sent + from_call: PE1RXF-13 # Call from which transmissions are made (can be a different call from the call assigned to the AX.25 port) + to_call: PE1RXF-1 # Call of the receiver + destination: APZMDM # APRS destination + digipath: 0 # Digipeater path for weather reports (0 = no path) + interval: 30 +# - port: ax1 # Linux AX.25 port to which APRS weather report is sent +# call: PE1RXF-13 # Call from which transmissions are made (can be a different call from the call assigned to the AX.25 port) +# destination: APZMDM # APRS destination +# digipath: WIDE2-1 # Digipeater path for weather reports (0 = no path) +# interval: 30 + +# Define the MQTT data to transmit over AX25. The order of the items is the order the data is combined to the telemetry string. +mqtt: + subscribe: + - mees_electronics/4d45000000000002/wind_direction + - mees_electronics/4d45000000000002/wind_speed + - mees_electronics/4d45000000000002/wind_gust + - mees_electronics/4d45000000000002/rain_last_hour + - mees_electronics/4d45000000000002/rain_last_24 hours + - mees_electronics/4d45000000000002/temperature + - mees_electronics/4d45000000000002/humidity + - mees_electronics/4d45000000000002/barometric_pressure + - mees_electronics/4d45000000000002/temperature_pressure_sensor + - mees_electronics/4d45000000000002/status_bits + - mees_electronics/4d45000000000002/luminosity diff --git a/software/mqqt_to_pe1rxf_telemetry/config_reader.py b/software/mqqt_to_pe1rxf_telemetry/config_reader.py new file mode 100644 index 0000000..6f0a1e3 --- /dev/null +++ b/software/mqqt_to_pe1rxf_telemetry/config_reader.py @@ -0,0 +1,90 @@ +"""" +ModBus configuration file reader routines +Copyright (C) 2023-2025 M.T. Konstapel + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" +import yaml +from yaml.loader import SafeLoader + +class config_reader: + + # initiate class: define name configuration files + def __init__(self, config_file): + self.config_file = config_file + + def read_settings(self): + + if self.read_config_file() == 0: + return 0 + + if self.test_global_settings() == 0: + return 0 + + if self.test_aprs_settings() == 0: + return 0 + + if self.test_mqtt_settings() == 0: + return 0 + + + return 1 + + def read_config_file (self): + try: + with open(self.config_file) as f: + self.config_file_settings = yaml.load(f, Loader=SafeLoader) + except: + print ("Configuration file ./" + self.config_file + " not found or syntax error in file.") + return 0 + else: + return 1 + + # Test if all settings are pressent + def test_global_settings(self): + # Test is all expected settings are present + try: + tmp = self.config_file_settings['global']['program-log'] + tmp = self.config_file_settings['global']['mqtt-server'] + tmp = self.config_file_settings['global']['mqtt-port'] + except: + print ("Error in the global section of the configuration file.") + return 0 + else: + return 1 + + def test_aprs_settings(self): + for entry in self.config_file_settings['aprs']: + try: + tmp = entry['port'] + tmp = entry['from_call'] + tmp = entry['to_call'] + tmp = entry['destination'] + tmp = entry['digipath'] + tmp = entry['interval'] + except: + print ("Error in the aprs section of the configuration file.") + return 0 + else: + return 1 + + def test_mqtt_settings(self): + # Test is all expected settings are present + try: + tmp = self.config_file_settings['mqtt']['subscribe'] + except: + print ("Error in the mqtt section of the configuration file.") + return 0 + else: + return 1 diff --git a/software/test_software/modbus_control.py b/software/mqqt_to_pe1rxf_telemetry/modbus_control.py similarity index 100% rename from software/test_software/modbus_control.py rename to software/mqqt_to_pe1rxf_telemetry/modbus_control.py diff --git a/software/test_software/modbus_definition_file_reader.py b/software/mqqt_to_pe1rxf_telemetry/modbus_definition_file_reader.py similarity index 100% rename from software/test_software/modbus_definition_file_reader.py rename to software/mqqt_to_pe1rxf_telemetry/modbus_definition_file_reader.py diff --git a/software/test_software/modbus_registers.yaml b/software/mqqt_to_pe1rxf_telemetry/modbus_registers.yaml similarity index 100% rename from software/test_software/modbus_registers.yaml rename to software/mqqt_to_pe1rxf_telemetry/modbus_registers.yaml diff --git a/software/mqqt_to_pe1rxf_telemetry/mqtt_callbacks.py b/software/mqqt_to_pe1rxf_telemetry/mqtt_callbacks.py new file mode 100644 index 0000000..b45ba8f --- /dev/null +++ b/software/mqqt_to_pe1rxf_telemetry/mqtt_callbacks.py @@ -0,0 +1,10 @@ +import logging + + + +def on_message(client, userdata, message, properties=None): + logging.info( + f"Received message {message.payload} on topic '{message.topic}' with QoS {message.qos}" + ) +def on_subscribe(client, userdata, mid, qos, properties=None): + logging.info(f"Subscribed with QoS {qos}") diff --git a/software/mqqt_to_pe1rxf_telemetry/mqtt_to_pe1rxf_telemetry.py b/software/mqqt_to_pe1rxf_telemetry/mqtt_to_pe1rxf_telemetry.py new file mode 100644 index 0000000..09da443 --- /dev/null +++ b/software/mqqt_to_pe1rxf_telemetry/mqtt_to_pe1rxf_telemetry.py @@ -0,0 +1,215 @@ +"""" +MQTT to pe1rxf telemetry for Mees Electronics sensors. +Subscribes to MQTT broker entries, combines the values and sends +the data as pe1rxf telemetry over an AX.25 port. + +Copyright (C) 2025 M.T. Konstapel + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" +import json +import time +import sys +import logging +import datetime +import subprocess +import os +import random + +from pathlib import Path +from config_reader import config_reader + +import paho.mqtt.client as mqtt +from paho.mqtt.client import CallbackAPIVersion +from paho.mqtt.properties import Properties +from paho.mqtt.packettypes import PacketTypes +properties=Properties(PacketTypes.PUBLISH) +properties.MessageExpiryInterval=30 # in seconds +#import ssl + +class MqttHandler: + def __init__(self, config): + self.config = config + # Define list with length equal to the number of subscriptions defined in the config file + self.number_of_mqtt_subscriptions = len(config['mqtt']['subscribe']) + self.aprs_telemetry_data = ['0.0']*self.number_of_mqtt_subscriptions + + def process_message(self, client, userdata, message): + + # Loop through all mqtt:subscribe: entries in config.yaml and see which mqtt message we have. Put in in the correct position of the APRS telemetry string + for index, entry in enumerate(self.config['mqtt']['subscribe']): + if entry == message.topic: + self.aprs_telemetry_data[index] = message.payload + + logging.info(f"Received message {message.payload} on topic '{message.topic}' with QoS {message.qos}") + + def on_connect(self, client, userdata, flags, reason_code, properties=None): + logging.info('Connected to MQTT broker.') + + # Format the pe1rxf telemetry string + def get_aprs_telemetry_string(self): + data_string = [] + for entry in self.aprs_telemetry_data: + data_string.append(entry.decode('utf-8')) + data_string = ','.join(data_string) + + return data_string + + +def start_mqtt(config): + + version = '3' # or '5' + mqtt_transport = 'tcp' # or 'websockets' + client_id = f'mees_electronics-mqtt-{random.randint(0, 1000)}' + + if version == '5': + client = mqtt.Client(CallbackAPIVersion.VERSION2, client_id=client_id, + transport=mqtt_transport, + protocol=mqtt.MQTTv5) + if version == '3': + client = mqtt.Client(CallbackAPIVersion.VERSION2, client_id=client_id, + transport=mqtt_transport, + protocol=mqtt.MQTTv311, + clean_session=True) + #client.username_pw_set("user", "password") + + mqtt_handler = MqttHandler(config) + client.on_connect = mqtt_handler.on_connect; + client.on_message = mqtt_handler.process_message + #client.on_message = mqtt_callbacks.on_message; + #client.on_publish = mqtt_callbacks.on_publish; + #client.on_subscribe = mqtt_callbacks.on_subscribe; + + if version == '5': + from paho.mqtt.properties import Properties + from paho.mqtt.packettypes import PacketTypes + properties=Properties(PacketTypes.CONNECT) + properties.SessionExpiryInterval=30*60 # in seconds + client.connect(config['global']['mqtt-server'], + port=config['global']['mqtt-port'], + clean_start=mqtt.MQTT_CLEAN_START_FIRST_ONLY, + properties=properties, + keepalive=60); + + elif version == '3': + client.connect(config['global']['mqtt-server'], port=config['global']['mqtt-port'], keepalive=60); + + client.loop_start(); + + return client, mqtt_handler + + +def setup(): + config_file = "config.yaml" + + # Read the configuration file + configuration = config_reader(config_file) + + if configuration.read_settings() == 0: + sys.exit() + + print("Succesfully read the configuration from file: " + config_file) + + # show values from config file + if configuration.config_file_settings['global']['program-log'] == 0: + print("Program output will not be logged to a file.") + else: + print("Program output will be logged to file: " + configuration.config_file_settings['global']['program-log']) + + # Enable the logging module. Log to file if enabled in configuration file, otherwise log to stand i/o (probably the screen) + logging.basicConfig( + level=logging.DEBUG, + format="{asctime} - {levelname} - {message}", + style="{", + datefmt="%Y-%m-%d %H:%M", + filename=configuration.config_file_settings['global']['program-log'], + ) + + logging.info("Connecting to MQTT broker: " + configuration.config_file_settings['global']['mqtt-server']) + + # Start MQTT section + mqtt_connected = False + while not mqtt_connected: + try: + mqtt_client, mqtt_handler = start_mqtt(configuration.config_file_settings) + mqtt_connected = True + except: + logging.error("Could not connect to MQTT broker. Retry until success (of until CTRL-C is pressed).") + time.sleep(3) # Sleep for 3 seconds + + subscribe_to_mqtt(configuration.config_file_settings, mqtt_client) + + # End MQTT section + + return configuration, mqtt_client, mqtt_handler + +def subscribe_to_mqtt(configuration, client): + + for index, entry in enumerate(configuration['mqtt']['subscribe']): + logging.debug("Subscribed to MQTT topic: " + str(entry) ) + client.subscribe(topic=str(entry)); + +# It is not possible to send multiple beacons in a short time. Call this function not faster than every 3 seconds or so. +def send_data_to_aprs(weather_data, configuration): + + to_call = configuration['to_call'].ljust(9)[:9] + + # Define the Bash script and its arguments as a list + script = "/usr/sbin/beacon" + + if configuration['digipath'] == 0: + arguments = ["-c", f"{configuration['from_call']}", "-d", f"{configuration['destination']}", "-s", f"{configuration['port']}", f":{to_call}:{weather_data}"] + else: + #arguments = ["-d", f"{configuration['destination']} {configuration['digipath']}", "-s", f"{configuration['port']}", f":{to_call}:{weather_data}"] + arguments = ["-c", f"{configuration['from_call']}", "-d", f"{configuration['destination']} {configuration['digipath']}", "-s", f"{configuration['port']}", f":{to_call}:{weather_data}"] + + # Combine the script and its arguments into a single list + command = [script] + arguments + + # Run the script + logging.debug(command) + try: + result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + # Check the result + if result.returncode == 0: + logging.info("Send data to APRS radio.") + else: + logging.error("Failed to send data to APRS radio.") + logging.error(f"Reason: {result.stderr}") + except Exception as e: + logging.error("Failed to send data to APRS radio.") + logging.error(f"Command returned: {e}") + + +Configuration, MqttClient, mqtt_handler = setup() +LoopCounter = 0 + +while (1): + time.sleep(3) # Sleep for 3 seconds + + # Send APRS telemetry every 10 cycles = every 10 minutes + LoopCounter = LoopCounter + 1 + if LoopCounter >= 1: + + # Send data to LoRa radio via external program (/usr/sbin/beacon). Make sure we use all radios defined in the configuration file. + for entry in Configuration.config_file_settings['aprs']: + send_data_to_aprs(mqtt_handler.get_aprs_telemetry_string(), entry) + logging.debug(mqtt_handler.get_aprs_telemetry_string()) + # We cannot send multiple APRS messages in a short period of time, so we wait 3 deconds between messages. + time.sleep(3) + + LoopCounter = 0 + + + diff --git a/software/rs485_client_to_mqtt/README.md b/software/rs485_client_to_mqtt/README.md new file mode 100644 index 0000000..7905777 --- /dev/null +++ b/software/rs485_client_to_mqtt/README.md @@ -0,0 +1,33 @@ +# RS485 ModBus client to MQTT bridge + +Scans the configured ModBus for Mees Electronics sensors and publishes the sensor data to the configured MQTT broker. + +The Mees Electronics sensors are almost plug-and-play. You just have to set the sensor to a unique address and add this address to the config.yaml file. The description entry in the YAML file is only there for convenience. + +## Configuration + +Edit config.yaml. + +The file modbus_registers.yaml contains the Mees Electronics register definitions of the various sensors. The newest definition file can be downloaded from the git repository. + +## Requirements + +- Python3 +- minimalmodbus +- json +- time +- sys +- logging +- os +- pathlib +- paho.mqtt.client +- pyyaml + +## License + +Copyright (C) 2025 M.T. Konstapel (https://meezenest.nl/mees) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. diff --git a/software/test_software/config.yml b/software/rs485_client_to_mqtt/config.yaml similarity index 54% rename from software/test_software/config.yml rename to software/rs485_client_to_mqtt/config.yaml index 93b241b..a633b85 100644 --- a/software/test_software/config.yml +++ b/software/rs485_client_to_mqtt/config.yaml @@ -1,6 +1,5 @@ # This is the configuration file for the Mees Electronics weather station MK2 - # Global settings global: program-log: 0 # All program output will be written to this file (0 = do not log to file) @@ -21,15 +20,3 @@ modbus_servers: description: Dual temperature sensor - address: 1 description: Dummy - -aprs: - - port: ax0 # Linux AX.25 port to which APRS weather report is sent - call: PE1RXF-13 # Call from which transmissions are made (can be a different call from the call assigned to the AX.25 port) - destination: APZMDM # APRS destination - digi_path: 0 # Digipeater path for weather reports (0 = no path) - interval: 30 - - port: ax1 # Linux AX.25 port to which APRS weather report is sent - call: PE1RXF-13 # Call from which transmissions are made (can be a different call from the call assigned to the AX.25 port) - destination: APZMDM # APRS destination - digi_path: WIDE2-1 # Digipeater path for weather reports (0 = no path) - interval: 30 diff --git a/software/test_software/config_reader.py b/software/rs485_client_to_mqtt/config_reader.py similarity index 84% rename from software/test_software/config_reader.py rename to software/rs485_client_to_mqtt/config_reader.py index a745daa..2c38e1b 100644 --- a/software/test_software/config_reader.py +++ b/software/rs485_client_to_mqtt/config_reader.py @@ -38,9 +38,6 @@ class config_reader: if self.test_modbus_servers_settings() == 0: return 0 - if self.aprs_settings() == 0: - return 0 - return 1 def read_config_file (self): @@ -89,17 +86,3 @@ class config_reader: return 0 else: return 1 - - def aprs_settings(self): - for entry in self.config_file_settings['aprs']: - try: - tmp = tmp = entry['port'] - tmp = entry['call'] - tmp = entry['destination'] - tmp = entry['digi_path'] - tmp = entry['interval'] - except: - print ("Error in the aprs section of the configuration file.") - return 0 - else: - return 1 diff --git a/software/rs485_client_to_mqtt/modbus_control.py b/software/rs485_client_to_mqtt/modbus_control.py new file mode 100644 index 0000000..9236d92 --- /dev/null +++ b/software/rs485_client_to_mqtt/modbus_control.py @@ -0,0 +1,107 @@ +"""" +ModBus control routines +Copyright (C) 2023-2025 M.T. Konstapel + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +import minimalmodbus +import serial + +class ModBusController(minimalmodbus.Instrument): + """Instrument class for Epever Charge Controllers. + + Args: + * portname (str): port name + * slaveaddress (int): slave address in the range 1 to 247 + + """ + + def __init__(self, portname, slaveaddress): + minimalmodbus.Instrument.__init__(self, portname, slaveaddress) + self.serial.baudrate = 9600 + self.serial.bytesize = 8 + self.serial.parity = serial.PARITY_NONE + self.serial.stopbits = 1 + self.serial.timeout = 0.3 + self.mode = minimalmodbus.MODE_RTU + self.clear_buffers_before_each_transaction = True + self.close_port_after_each_call = False + + self.watchdog_toggle_bit = 1 + + # Read the device ID + def get_id(self): + device_id = [None]*4 + device_id[0] = self.read_register(1000, 0, 4, False) + device_id[1] = self.read_register(1001, 0, 4, False) + device_id[2] = self.read_register(1002, 0, 4, False) + device_id[3] = self.read_register(1003, 0, 4, False) + + return device_id + + # Read the device type + def get_type(self): + device_type = self.read_register(1004, 0, 4, False) + + return device_type + + # Get the 40 character long description string from ModBus device + def get_type_string(self): + register_offset = 1006 # ModBus register for text string starts at 1006 + device_type_register = [None]*20 # ModBus register for text string is 20 integers long + device_type_string = "" # Store the decoded string here + + for index, entry in enumerate(device_type_register): + # Read Type string registers + device_type_register[index] = self.read_register(index + register_offset, 0, 4, False) + # Convert to ASCII string + device_type_string += chr((device_type_register[index] & 0xFF00) >> 8) + device_type_string += chr(device_type_register[index] & 0xFF) + + return device_type_string.strip() + + # Ask the device how many input registers it has + def get_number_of_input_registers(self): + device_number_of_input_registers = self.read_register(1026, 0, 4, False) + + return device_number_of_input_registers + + # Read all the input registers + def read_all_input_registers(self): + + number_of_input_registers = self.get_number_of_input_registers() + input_register = [None]*number_of_input_registers + + for index in range(number_of_input_registers): + input_register[index] = self.read_register(index, 0, 4, False) + + return input_register + + def set_watchdog(self): + self.write_bit(0, 1, 5) + + def reset_watchdog(self): + self.write_bit(0, 0, 5) + + # Toggle the devices watchdog + def toggle_watchdog(self): + if self.watchdog_toggle_bit: + self.set_watchdog() + self.watchdog_toggle_bit = 0 + else: + self.reset_watchdog() + self.watchdog_toggle_bit = 1 + + diff --git a/software/rs485_client_to_mqtt/modbus_definition_file_reader.py b/software/rs485_client_to_mqtt/modbus_definition_file_reader.py new file mode 100644 index 0000000..12c5b08 --- /dev/null +++ b/software/rs485_client_to_mqtt/modbus_definition_file_reader.py @@ -0,0 +1,56 @@ +"""" +ModBus definition file reader routines +Copyright (C) 2025 M.T. Konstapel + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" +import yaml +from yaml.loader import SafeLoader + +class definition_file_reader: + + # initiate class: define name configuration files + def __init__(self, definition_file): + self.definition_file = definition_file + + def read_settings(self): + + if self.read_definition_file() == 0: + return 0 + + if self.test_definition_file() == 0: + return 0 + + return 1 + + def read_definition_file (self): + try: + with open(self.definition_file) as f: + self.definition_file_data = yaml.load(f, Loader=SafeLoader) + except: + print ("Definition file ./" + self.definition_file + " not found or syntax error in file.") + return 0 + else: + return 1 + + # Test if all settings are pressent + def test_definition_file(self): + # Test is all expected settings are present + try: + tmp = self.definition_file_data['devices'] + except: + print ("Error in the ModBus definition file.") + return 0 + else: + return 1 diff --git a/software/rs485_client_to_mqtt/modbus_registers.yaml b/software/rs485_client_to_mqtt/modbus_registers.yaml new file mode 100644 index 0000000..df8830e --- /dev/null +++ b/software/rs485_client_to_mqtt/modbus_registers.yaml @@ -0,0 +1,30 @@ +# This file defines the Mees Electronincs ModBus device registers + +devices: + - device_type: 1 + input_registers: 6 # The number of available input registers, starting from offset 40 + input_register_names: # Description, unit + - [Temperature A, °C] + - [Temperature B , °C] + - [Minimum temperature A, °C] + - [Minimum temperature B, °C] + - [Maximum temperature A, °C] + - [Maximum temperature B, °C] + - device_type: 2 + input_registers: 15 # The number of available input registers, starting from offset 40 + input_register_names: # Description, unit, scaling 0 = as is, 1 = decimal one position to the left, 2 = decimal two positions to the left, enz. + - [weater_station_id, '', 0] + - [wind_direction, °, 1] + - [wind_speed, 'km/h', 2] + - [wind_gust, 'km/h', 2] + - [temperature, °C, 2] + - [rain_last_hour, 'l/m2', 2] + - [rain_last_24 hours, 'l/m2', 2] + - [rain_since_midnight, 'l/m2', 2] + - [humidity, '%', 2] + - [barometric_pressure, hPa, 1] + - [luminosity, 'W/m2', 0] + - [snow_fall, NA, 0] + - [raw_rainfall_counter, mm, 0] + - [temperature_pressure_sensor, °C, 2] + - [status_bits, '', 0] diff --git a/software/test_software/mqtt_callbacks.py b/software/rs485_client_to_mqtt/mqtt_callbacks.py similarity index 100% rename from software/test_software/mqtt_callbacks.py rename to software/rs485_client_to_mqtt/mqtt_callbacks.py diff --git a/software/test_software/weather_station_rs485_client.py b/software/rs485_client_to_mqtt/rs485_client_to_mqtt.py similarity index 86% rename from software/test_software/weather_station_rs485_client.py rename to software/rs485_client_to_mqtt/rs485_client_to_mqtt.py index 5bd9901..cb8a90b 100644 --- a/software/test_software/weather_station_rs485_client.py +++ b/software/rs485_client_to_mqtt/rs485_client_to_mqtt.py @@ -1,5 +1,7 @@ """" -ModBus test routines +ModBus client program. Scans the RS-485 ModBus for Mees Electronics +sensors and publishes the sensor data to an MQTT broker. + Copyright (C) 2025 M.T. Konstapel This program is free software: you can redistribute it and/or modify @@ -21,8 +23,8 @@ import sys import logging import minimalmodbus import datetime -import subprocess import os +import random from pathlib import Path from modbus_control import ModBusController @@ -41,13 +43,14 @@ def start_mqtt(config): version = '3' # or '5' mqtt_transport = 'tcp' # or 'websockets' + client_id = f'mees_electronics-mqtt-{random.randint(0, 1000)}' if version == '5': - client = mqtt.Client(CallbackAPIVersion.VERSION2, client_id="myPy", + client = mqtt.Client(CallbackAPIVersion.VERSION2, client_id=client_id, transport=mqtt_transport, protocol=mqtt.MQTTv5) if version == '3': - client = mqtt.Client(CallbackAPIVersion.VERSION2, client_id="myPy", + client = mqtt.Client(CallbackAPIVersion.VERSION2, client_id=client_id, transport=mqtt_transport, protocol=mqtt.MQTTv311, clean_session=True) @@ -76,16 +79,11 @@ def start_mqtt(config): client.loop_start(); -# Debug code -# while 1: -# client.publish(mqtt_topic,'Cedalo Mosquitto is awesome',2,properties=properties); -# time.sleep(1) - return client def setup(): - config_file = "config.yml" + config_file = "config.yaml" definition_file = "modbus_registers.yaml" # Read the configuration file @@ -177,35 +175,6 @@ def data_logger(data, configuration): except: logging.warning("Could not write to file: " + new_filename) -# It is not possible to send multiple beacons in a short time. Call this function not faster than every 3 seconds or so. -def send_data_to_aprs(weather_data, configuration): - - # Define the Bash script and its arguments as a list - script = "/usr/sbin/beacon" - - if configuration['digi_path'] == 0: - arguments = ["-d", f"{configuration['destination']}", "-s", "ax1", ":PE1RXF-3 :test2"] - else: - arguments = ["-d", f"{configuration['destination']} {configuration['digi_path']}", "-s", "ax1", ":PE1RXF-3 :test3"] - - # Combine the script and its arguments into a single list - command = [script] + arguments - - # Run the script - logging.debug(command) - try: - result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - # Check the result - if result.returncode == 0: - logging.info("Send data to APRS radio.") - else: - logging.error("Failed to send data to APRS radio.") - logging.error(f"Reason: {result.stderr}") - except Exception as e: - logging.error("Failed to send data to APRS radio.") - logging.error(f"Command returned: {e}") - - def send_data_to_mqtt(data, configuration, modbus_registers, mqtt_client): mqtt_top_topic = [] @@ -271,19 +240,6 @@ LoopCounter = 0 while (1): time.sleep(3) # Sleep for 3 seconds - # Send APRS telemetry every 10 cycles = every 10 minutes - ''' - LoopCounter = LoopCounter + 1 - if LoopCounter >= 1: - - # Send data to LoRa radio via external program (/usr/sbin/beacon). Make sure we use all radios defined in the configuration file. - for entry in Configuration.config_file_settings['aprs']: - send_data_to_aprs(1, entry) - # We cannot send multiple APRS messages in a short period of time, so we wait 3 deconds between messages. - time.sleep(3) - - LoopCounter = 0 - ''' ModBusData={} # Loop through all configured ModBus devices and try to read the sensor data @@ -299,7 +255,7 @@ while (1): ModBusData['InputRegisters'] = Controller[index].read_all_input_registers() # Keep the watchdog from resetting the ModBusdevice - #Controller[index].toggle_watchdog() + Controller[index].toggle_watchdog() NoError = True except minimalmodbus.NoResponseError: diff --git a/software/test_software/example.log b/software/test_software/example.log deleted file mode 100644 index 24c8463..0000000 --- a/software/test_software/example.log +++ /dev/null @@ -1,15 +0,0 @@ -2025-08-01 16:23 - INFO - Succesfully read the configuration from file: config.yml -2025-08-01 16:23 - INFO - Program output will be logged to file: /home/marcel/rs458.log -2025-08-01 16:23 - INFO - Sensor data will be logged to file: /home/marcel/weather_station_data.log -2025-08-01 16:23 - INFO - Using RS485 port : /dev/ttyUSB0 -2025-08-01 16:23 - INFO - Succesfully read the ModBus register definitions from file: modbus_registers.yaml -2025-08-01 16:23 - INFO - Using device: Dual temperature sensor on ModBus address: 14 -2025-08-01 16:23 - INFO - Using device: Dummy on ModBus address: 1 -2025-08-01 16:23 - WARNING - No response from the instrument on ModBus address: 1 -2025-08-01 16:24 - INFO - Succesfully read the configuration from file: config.yml -2025-08-01 16:24 - INFO - Program output will be logged to file: /home/marcel/rs458.log -2025-08-01 16:24 - INFO - Sensor data will be logged to file: /home/marcel/weather_station_data.log -2025-08-01 16:24 - INFO - Using RS485 port : /dev/ttyUSB0 -2025-08-01 16:24 - INFO - Succesfully read the ModBus register definitions from file: modbus_registers.yaml -2025-08-01 16:24 - INFO - Using device: Dual temperature sensor on ModBus address: 14 -2025-08-01 16:24 - INFO - Using device: Dummy on ModBus address: 1 diff --git a/software/test_software/weather_station_data.log b/software/test_software/weather_station_data.log deleted file mode 100644 index 0315f6d..0000000 --- a/software/test_software/weather_station_data.log +++ /dev/null @@ -1,2 +0,0 @@ -{"DateTime": "2025-08-01T16:55:39", "ID": [19781, 0, 0, 1], "Type": 1, "TypeString": "Dual temperature sensor", "InputRegisters": [20, 21, 22, 24, 23, 25]} -{"DateTime": "2025-08-01T16:55:43", "ID": [19781, 0, 0, 1], "Type": 1, "TypeString": "Dual temperature sensor", "InputRegisters": [20, 21, 22, 24, 23, 25]}