diff --git a/software/test_software/__pycache__/config_reader.cpython-36.pyc b/software/test_software/__pycache__/config_reader.cpython-36.pyc
index 8f8bc85..f828223 100644
Binary files a/software/test_software/__pycache__/config_reader.cpython-36.pyc and b/software/test_software/__pycache__/config_reader.cpython-36.pyc differ
diff --git a/software/test_software/__pycache__/modbus_control.cpython-36.pyc b/software/test_software/__pycache__/modbus_control.cpython-36.pyc
index 56f41ee..808abbe 100644
Binary files a/software/test_software/__pycache__/modbus_control.cpython-36.pyc and b/software/test_software/__pycache__/modbus_control.cpython-36.pyc differ
diff --git a/software/test_software/__pycache__/modbus_definition_file_reader.cpython-36.pyc b/software/test_software/__pycache__/modbus_definition_file_reader.cpython-36.pyc
new file mode 100644
index 0000000..f3318e0
Binary files /dev/null and b/software/test_software/__pycache__/modbus_definition_file_reader.cpython-36.pyc differ
diff --git a/software/test_software/config.yml b/software/test_software/config.yml
index ac85978..860d9be 100644
--- a/software/test_software/config.yml
+++ b/software/test_software/config.yml
@@ -3,7 +3,10 @@
# Global settings
global:
- log-rf: /home/marcel/ham/weather_station/pe1rxf_aprs-rf.log # Log RF traffic to file (0=no logging)
+ 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)
+ data-log: /home/marcel/weather_station_data.log # All sensor data will be written to this file (0 = do not log to file). Every day the current date is added to the end of the filename, so every dat a new file is created.
+ mqtt-server: mqtt.meezenest.nl
# ModBus hardware settings
modbus:
diff --git a/software/test_software/config_reader.py b/software/test_software/config_reader.py
index b515a7b..4c48283 100644
--- a/software/test_software/config_reader.py
+++ b/software/test_software/config_reader.py
@@ -1,30 +1,20 @@
-'''
-# A basic APRS iGate and APRS weather station with additional (optional) PE1RXF telemetry support
-#
-# This program reads the registers of the PE1RXF weather station via ModBus RTU and sends it as
-# an APRS WX report over APRS. Additionally, it sends beacons and forwards received APRS messages
-# to the APRS-IS network. All configurable via a YAML file called pe1rxf_aprs.yml.
-#
-# This program also has a PE1RXF APRS telemetry to MQTT bridge, which is configurable via pe1rxf_telemetry.yml
-#
-# Copyright (C) 2023, 2024 M.T. Konstapel https://meezenest.nl/mees
-#
-# This file is part of weather_station
-#
-# weather_station 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.
-#
-# weather_station 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 weather_station. If not, see .
-'''
+""""
+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
@@ -64,7 +54,9 @@ class config_reader:
def test_global_settings(self):
# Test is all expected settings are present
try:
- tmp = self.config_file_settings['global']['log-rf']
+ tmp = self.config_file_settings['global']['program-log']
+ tmp = self.config_file_settings['global']['data-log']
+ tmp = self.config_file_settings['global']['mqtt-server']
except:
print ("Error in the global section of the configuration file.")
return 0
diff --git a/software/test_software/example.log b/software/test_software/example.log
new file mode 100644
index 0000000..24c8463
--- /dev/null
+++ b/software/test_software/example.log
@@ -0,0 +1,15 @@
+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/modbus_control.py b/software/test_software/modbus_control.py
index 4533326..47e6e36 100644
--- a/software/test_software/modbus_control.py
+++ b/software/test_software/modbus_control.py
@@ -1,6 +1,6 @@
""""
ModBus control routines
-Copyright (C) 2023 M.T. Konstapel
+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
@@ -49,6 +49,42 @@ class ModBusController(minimalmodbus.Instrument):
return device_id
+ def get_type(self):
+ device_type = self.read_register(4, 0, 4, False)
+
+ return device_type
+
+ # Get the 60 character long description string from ModBus device
+ def get_type_string(self):
+ register_offset = 6 # ModBus register for text string starts at 6
+ device_type_register = [None]*30 # ModBus register for text string is 30 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()
+
+ def get_number_of_input_registers(self):
+ device_number_of_input_registers = self.read_register(36, 0, 4, False)
+
+ return device_number_of_input_registers
+
+ def read_all_input_registers(self):
+
+ number_of_input_registers = self.get_number_of_input_registers()
+ offset = 40
+ input_register = [None]*number_of_input_registers
+
+ for index in range(number_of_input_registers):
+ input_register[index] = self.read_register(index + offset, 0, 4, False)
+
+ return input_register
+
def enable_heater(self):
self.write_bit(0, 1, 5)
diff --git a/software/test_software/modbus_definition_file_reader.py b/software/test_software/modbus_definition_file_reader.py
new file mode 100644
index 0000000..12c5b08
--- /dev/null
+++ b/software/test_software/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/test_software/modbus_registers.yaml b/software/test_software/modbus_registers.yaml
new file mode 100644
index 0000000..2eb5402
--- /dev/null
+++ b/software/test_software/modbus_registers.yaml
@@ -0,0 +1,12 @@
+# This file defines the Mees Electronincs ModBus device registers
+
+devices:
+ 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]
diff --git a/software/test_software/weather_station_data.log b/software/test_software/weather_station_data.log
new file mode 100644
index 0000000..0315f6d
--- /dev/null
+++ b/software/test_software/weather_station_data.log
@@ -0,0 +1,2 @@
+{"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]}
diff --git a/software/test_software/weather_station_rs485_client.py b/software/test_software/weather_station_rs485_client.py
index 6aee2d4..dbd1d0c 100644
--- a/software/test_software/weather_station_rs485_client.py
+++ b/software/test_software/weather_station_rs485_client.py
@@ -1,44 +1,35 @@
-# Copyright 2025 Marcel Konstapel
-# https://www.meezenest.nl/mees
-#
-# Testprogram for weather_station_MK2 sensors
-#
-# V0.1 2025-07-22
-# - First working program
-# V0.1.1 2025-07-28
-# - Added auto detect, reconnect and other fail safe features
-#
-# 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 .
-#
+""""
+ModBus test 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 json
import time
import sys
import logging
import minimalmodbus
+import datetime
+from pathlib import Path
from modbus_control import ModBusController
from config_reader import config_reader
+from modbus_definition_file_reader import definition_file_reader
def setup():
config_file = "config.yml"
-
- logging.basicConfig(
- level=logging.DEBUG,
- format="{asctime} - {levelname} - {message}",
- style="{",
- datefmt="%Y-%m-%d %H:%M",
- )
+ definition_file = "modbus_registers.yaml"
# Read the configuration file
configuration = config_reader(config_file)
@@ -46,8 +37,38 @@ def setup():
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'])
+
+ if configuration.config_file_settings['global']['data-log'] == 0:
+ print("Sensor data will not be logged to a file.")
+ else:
+ print("Sensor data will be logged to file: " + configuration.config_file_settings['global']['data-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("Using RS485 port : " + configuration.config_file_settings['modbus']['port'])
+ logging.info("Publishing sensor data on MQTT broker: " + configuration.config_file_settings['global']['mqtt-server'])
+
+ # Read the ModBus definition file
+ modbus_registers = definition_file_reader(definition_file)
+
+ if modbus_registers.read_settings() == 0:
+ sys.exit()
+
+ logging.info("Succesfully read the ModBus register definitions from file: " + definition_file)
controller = []
modbus_addresses = []
@@ -57,20 +78,42 @@ def setup():
# Open serial port and try to connect to all the ModBus devices from the configuration file
try:
- #controller = ModBusController(configuration.config_file_settings['modbus']['port'], modbus_addresses[0])
controller.append(ModBusController(configuration.config_file_settings['modbus']['port'], modbus_addresses[index]))
except:
logging.error("Could not open serial port " + configuration.config_file_settings['modbus']['port'])
sys.exit("Exiting")
- return configuration, controller, modbus_addresses
+ return configuration, controller, modbus_addresses, modbus_registers
+
+def data_logger(data, configuration):
+
+ if configuration.config_file_settings['global']['data-log'] != 0:
+
+ # Add date to file name, so every day a new file is created
+ filename = Path(configuration.config_file_settings['global']['data-log'])
+ new_filename = '_'.join([filename.stem, datetime.datetime.today().strftime('%Y-%m-%d')])
+ new_filename = ''.join([new_filename, filename.suffix])
+ new_filename = '/'.join([str(filename.parent), new_filename])
+ logging.debug(new_filename)
+
+ try:
+ f = open(new_filename, 'a')
+ json.dump(data, f)
+ f.write("\n")
+ f.close()
+ except:
+ logging.warning("Could not write to file: " + new_filename)
+
+def send_data_to_mqtt(data, configuration):
+ logging.debug("Send data to MQTT broker.")
+
# Lost serial port, try to reconnect. We can try to connect to any ModBus address, even if it does not exists. We are only trying to reconnect to the RS485 serial/USB dongle.
def ReconnectModBus(configuration):
Retrying = True
while (Retrying):
- logging.warning("Try to reconnect to ModBus dongle")
+ logging.error("Try to reconnect to ModBus dongle")
try:
controller = ModBusController(configuration.config_file_settings['modbus']['port'], 0)
Retrying = False
@@ -78,44 +121,66 @@ def ReconnectModBus(configuration):
logging.error("Serial port " + configuration.config_file_settings['modbus']['port'] + " not available. Retry in 10 seconds.")
time.sleep(10)
- logging.warning("Reconnected to ModBus dongle.")
+ logging.info("Reconnected to ModBus dongle.")
#print("Enable heater function")
#controller.enable_heater()
-Configuration, Controller, ModbusAddresses = setup()
+Configuration, Controller, ModbusAddresses, ModbusRegisters = setup()
while (1):
time.sleep(3) # Sleep for 3 seconds
ModBusData={}
- # Loop through all configured ModBus devices
+ # Loop through all configured ModBus devices and try to read the sensor data
for index, current_modbus_device in enumerate(ModbusAddresses):
- logging.info("Reading device on ModBus address: " + str(current_modbus_device))
+ logging.debug("Reading device on ModBus address: " + str(current_modbus_device))
try:
+ ModBusData['DateTime'] = datetime.datetime.now().replace(microsecond=0).isoformat()
ModBusData['ID'] = Controller[index].get_id()
if ModBusData['ID'][0] == 0x4D45:
- logging.info("Found Mees Electronics sensor on ModBus address " + str(current_modbus_device))
- logging.info("Serial number: " + hex(ModBusData['ID'][1]) + " " + hex(ModBusData['ID'][2]) + " " + hex(ModBusData['ID'][3]))
- logging.info (json.dumps(ModBusData, indent=1, sort_keys=False))
+ ModBusData['Type'] = Controller[index].get_type()
+ ModBusData['TypeString'] = Controller[index].get_type_string()
+ ModBusData['InputRegisters'] = Controller[index].read_all_input_registers()
+ NoError = True
except minimalmodbus.NoResponseError:
# Handle communication timeout
- logging.error("No response from the instrument")
+ logging.warning("No response from the instrument on ModBus address: " + str(current_modbus_device))
+ NoError = False
except minimalmodbus.InvalidResponseError:
# Handle invalid Modbus responses
- logging.error("Invalid response from the instrument")
+ logging.warning("Invalid response from the instrument on ModBus address: " + str(current_modbus_device))
+ NoError = False
except minimalmodbus.SlaveReportedException as e:
# Handle errors reported by the slave device
logging.error(f"The instrument reported an error: {e}")
+ NoError = False
except minimalmodbus.ModbusException as e:
# Handle all other Modbus-related errors
logging.error(f"Modbus error: {e}")
+ NoError = False
except Exception as e:
# Handle unexpected errors, probably I/O error in USB/serial
logging.error(f"Unexpected error: {e}" + ", serial port " + Configuration.config_file_settings['modbus']['port'] + " not available while reading device on ModBus address " + str(ModbusAddresses[index]) + ". Try to reconnect.")
Controller[index].serial.close()
ReconnectModBus(Configuration)
+ NoError = False
+ if NoError == True:
+ try:
+ DeviceType = ModbusRegisters.definition_file_data['devices'][ModBusData['Type']]
+ logging.debug("Found Mees Electronics sensor on ModBus address " + str(current_modbus_device))
+ logging.debug("Serial number: " + hex(ModBusData['ID'][1]) + " " + hex(ModBusData['ID'][2]) + " " + hex(ModBusData['ID'][3]))
+ logging.debug("Device type: " + str(ModBusData['Type']) + " (" + ModBusData['TypeString'] + ")")
+ logging.debug (ModBusData['InputRegisters'])
+ logging.debug (json.dumps(ModBusData, indent=1, sort_keys=False))
+ except:
+ logging.warning("Modbus device type " + str(ModBusData['Type']) + " not found in register definition file. Ignoring sensor data.")
+ # Log sensor data to file if enabled in configuration file
+ data_logger(ModBusData, Configuration)
+
+ # Send sensor data to MQTT broker
+ send_data_to_mqtt(ModBusData, Configuration)