Files
weather_station_MK2/software/test_software/weather_station_rs485_client.py
2025-08-12 10:15:01 +02:00

346 lines
14 KiB
Python

""""
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 <https://www.gnu.org/licenses/>.
"""
import json
import time
import sys
import logging
import minimalmodbus
import datetime
import subprocess
import os
from pathlib import Path
from modbus_control import ModBusController
from config_reader import config_reader
from modbus_definition_file_reader import definition_file_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
def start_mqtt(config):
version = '3' # or '5'
mqtt_transport = 'tcp' # or 'websockets'
if version == '5':
client = mqtt.Client(CallbackAPIVersion.VERSION2, client_id="myPy",
transport=mqtt_transport,
protocol=mqtt.MQTTv5)
if version == '3':
client = mqtt.Client(CallbackAPIVersion.VERSION2, client_id="myPy",
transport=mqtt_transport,
protocol=mqtt.MQTTv311,
clean_session=True)
#client.username_pw_set("user", "password")
import mqtt_callbacks
client.on_message = mqtt_callbacks.on_message;
client.on_connect = mqtt_callbacks.on_connect;
#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['mqtt-server'],
port=config['mqtt-port'],
clean_start=mqtt.MQTT_CLEAN_START_FIRST_ONLY,
properties=properties,
keepalive=60);
elif version == '3':
client.connect(config['mqtt-server'], port=config['mqtt-port'], keepalive=60);
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"
definition_file = "modbus_registers.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'])
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'])
# Start MQTT section
mqtt_connected = False
while not mqtt_connected:
try:
mqtt_client = start_mqtt(configuration.config_file_settings['global'])
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
# End MQTT section
# Start ModBus section
# 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 = []
for index, entry in enumerate(configuration.config_file_settings['modbus_servers']):
modbus_addresses.append(entry['address'])
logging.info("Using device: " + entry['description'] + " on ModBus address: " + str(entry['address']))
# Open serial port and try to connect to all the ModBus devices from the configuration file
try:
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")
# End ModBus section
return configuration, controller, modbus_addresses, modbus_registers, mqtt_client
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)
# 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 = []
mqtt_full_topic = []
#logging.debug(modbus_registers)
# Match actual device on ModBus with definition in modbus_registers.yaml
for index1, entry1 in enumerate(modbus_registers['devices']):
if entry1['device_type'] == data['Type']:
# Format serial number for unique MQTT ID
mqtt_type_topic = hex(data['ID'][0])[2:].zfill(4) + hex(data['ID'][1])[2:].zfill(4) + hex(data['ID'][2])[2:].zfill(4) + hex(data['ID'][3])[2:].zfill(4)
mqtt_top_topic = configuration.config_file_settings['global']['mqtt-root-topic'] + "/" + mqtt_type_topic + "/"
message_topic = "id"
mqtt_message = "mees_electronics_" + mqtt_type_topic
mqtt_full_topic = mqtt_top_topic + message_topic
MqttClient.publish(mqtt_full_topic, mqtt_message, 2, properties = properties);
logging.debug("Published message to MQTT broker: " + mqtt_full_topic + " = " + mqtt_message)
message_topic = "type"
mqtt_message = str(data['Type'])
mqtt_full_topic = mqtt_top_topic + message_topic
MqttClient.publish(mqtt_full_topic, mqtt_message, 2, properties = properties);
logging.debug("Published message to MQTT broker: " + mqtt_full_topic + " = " + mqtt_message)
message_topic = "type_string"
mqtt_message = data['TypeString']
mqtt_full_topic = mqtt_top_topic + message_topic
MqttClient.publish(mqtt_full_topic, mqtt_message, 2, properties = properties);
logging.debug("Published message to MQTT broker: " + mqtt_full_topic + " = " + mqtt_message)
# Go through every input register and match the unit and description with the value
for index2, entry2 in enumerate(data['InputRegisters']):
# Scale values
entry2 = entry2/ (10^entry1['input_register_names'][index2][2])
message_topic = entry1['input_register_names'][index2][0]
mqtt_message = str(round(entry2,1))
mqtt_full_topic = mqtt_top_topic + message_topic
MqttClient.publish(mqtt_full_topic, mqtt_message, 2, properties = properties);
logging.debug("Published message to MQTT broker: " + mqtt_full_topic + " = " + mqtt_message)
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.error("Try to reconnect to ModBus dongle")
try:
controller = ModBusController(configuration.config_file_settings['modbus']['port'], 0)
Retrying = False
except:
logging.error("Serial port " + configuration.config_file_settings['modbus']['port'] + " not available. Retry in 10 seconds.")
time.sleep(10)
logging.info("Reconnected to ModBus dongle.")
Configuration, Controller, ModbusAddresses, ModbusRegisters, MqttClient = 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(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
for index, current_modbus_device in enumerate(ModbusAddresses):
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:
ModBusData['Type'] = Controller[index].get_type()
ModBusData['TypeString'] = Controller[index].get_type_string()
ModBusData['InputRegisters'] = Controller[index].read_all_input_registers()
# Keep the watchdog from resetting the ModBusdevice
#Controller[index].toggle_watchdog()
NoError = True
except minimalmodbus.NoResponseError:
# Handle communication timeout
logging.warning("No response from the instrument on ModBus address: " + str(current_modbus_device))
NoError = False
except minimalmodbus.InvalidResponseError:
# Handle invalid Modbus responses
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:
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, ModbusRegisters.definition_file_data, MqttClient)