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]}