Luminosity sensor added, APRSIS messages are now forwarded to MQTT

This commit is contained in:
marcel
2025-01-26 20:43:35 +01:00
parent 589f303a16
commit 7e2aa14f2a
8 changed files with 721 additions and 33 deletions

View File

@@ -61,3 +61,19 @@ All notable changes to this project will be documented in this file.
### Fixed
- Sporadic crash when uploading RF packet to APRS-IS when payload (message) is empty. Added extra check to payload.
## [0.1.5] - 2025-01-14
### Added
- Luminocity meter is now available in both APRS weather reports as well as in MQTT messages
## [0.1.6] - 2025-01-26
### Fixed
- Luminosity sensor values are now correct
### Added
- Messages from APRSIS are now checked and processed. If message is for local call and sender is not blacklisted, the message is forwarded to MQTT (same as messages received via air already where)

View File

@@ -193,7 +193,10 @@ class aprs_telemetry_to_mqtt:
# Configuration is done via the file pe1rxf_telemetry.yml
def publish_aprs_messages(self, source, ax_device, payload):
if self.config_file_settings['global']['publish_messages'] == 'YES':
#logger.debug('Check if message is for us.')
#logger.debug(self.config_file_settings['global']['publish_messages'])
if self.config_file_settings['global']['publish_messages'] == True:
#logger.debug('Configured to forward messages.')
mqtt_message = {}
@@ -209,6 +212,7 @@ class aprs_telemetry_to_mqtt:
# substrings: the first in empty, the second with the call of the ax25
# interface and the thirth with the message itself
split_message=payload.split(":")
#logger.debug(split_message)
if len(split_message) == 3:
#Remove spaces from destination call
split_message[1] = split_message[1].replace(" ", "")

50
pe1rxf_aprs.py Normal file → Executable file
View File

@@ -8,7 +8,7 @@
#
# 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
# Copyright (C) 2023-2025 M.T. Konstapel https://meezenest.nl/mees
#
# This file is part of weather_station
#
@@ -213,6 +213,33 @@ def process_aprsis(packet):
string = timestamp + ' ' + 'APRSIS ' + ' R ' + str(packet, 'latin-1') + '\n'
# Check if frame is a message to us. If so send it to mqtt
aprsis_source, aprsis_destination, aprsis_digipeaters, aprsis_payload = parsePacket(packet)
print(packet)
# Convert byte string to normal string
try:
aprsis_payload = aprsis_payload.decode('latin-1')
except:
aprsis_payload = 'NOT VALID'
print("Packet Received by APRSIS"])
print("Source Address = %s"%aprsis_source)
print("Destination Address = %s"%aprsis_destination)
print("Digipeaters =")
print(aprsis_digipeaters)
print("Payload = %s"%aprsis_payload)
print("")
#aprs_frame = axaddress[0] + '>' + aprsis_destination + ',' + ','.join(aprsis_digipeaters) + ':' + aprsis_payload
#print (aprs_frame)
if aprsis_payload == 'NOT VALID' or aprsis_payload == 0:
print (">>> Packet not valid, ignored.")
else:
# Check if APRS frame is a message to us
mqtt_connection.publish_aprs_messages(aprsis_source, 'aprsis', aprsis_payload)
# write APRSIS string to log file
try:
with open(rflog_file, "a") as logfile:
logfile.write(string)
@@ -316,22 +343,28 @@ def send_aprs_weather_report(weather_settings):
wind_direction = int(WxData['Wind direction'])
wind_speed = int(2.2369 * WxData['Wind speed'])
wind_gust = int(2.2369 * WxData['Wind gust'])
#rain_lasthour = int(3.93700787 * WxData['Rain last hour'])
rain_lasthour = 0
#rain_24hour = int(3.93700787 * WxData['Rain last 24 hours'])
rain_24hour = 0
rain_lasthour = int(3.93700787 * WxData['Rain last hour'])
#rain_lasthour = 0
rain_24hour = int(3.93700787 * WxData['Rain last 24 hours'])
#rain_24hour = 0
temperature = int(WxData['Temperature'] * 1.8 + 32)
humidity = int(WxData['Humidity'])
if (humidity == 100):
humidity = 0;
pressure =int(10 * WxData['Pressure'])
luminosity = int(0.0079 * WxData['Luminosity']) # W/m2: various sources give 0.0079 or 0.0083 as an approxmation for sunlight
if (luminosity <= 999):
APRS_Lum = 'L'
else:
APRS_Lum = 'l'
luminosity = luminosity-1000
# Get date and time
timestamp = time.strftime("%d%H%M", gmtime())
# Construct APRS weather report
aprs_position = weather_settings['position']
aprs_wx_report = '@' + timestamp + 'z' + weather_settings['position'] + "{:03d}".format(wind_direction) + '/' + "{:03d}".format(wind_speed) + 'g' + "{:03d}".format(wind_gust) + 't' + "{:03d}".format(temperature) + 'r' + "{:03d}".format(rain_lasthour) + 'p' + "{:03d}".format(rain_24hour) + 'h' + "{:02d}".format(humidity) + 'b' + "{:05d}".format(pressure)
aprs_wx_report = '@' + timestamp + 'z' + weather_settings['position'] + "{:03d}".format(wind_direction) + '/' + "{:03d}".format(wind_speed) + 'g' + "{:03d}".format(wind_gust) + 't' + "{:03d}".format(temperature) + 'r' + "{:03d}".format(rain_lasthour) + 'p' + "{:03d}".format(rain_24hour) + 'h' + "{:02d}".format(humidity) + 'b' + "{:05d}".format(pressure) + APRS_Lum + "{:03d}".format(luminosity)
if weather_settings['port'] == 'aprsis':
send_aprsis(weather_settings['call'], weather_settings['destination'], weather_settings['digi_path'], aprs_wx_report)
@@ -377,7 +410,7 @@ def send_telemetry():
def publish_weather_data(mqtt_client):
payload = ':' + 'PE1RXF-3 ' + ':' + str(WxData['Wind direction']) + ',' + str(WxData['Wind speed']) + ',' + str(WxData['Wind gust']) + ',' + str(WxData['Rain last hour']) + ',' + str(WxData['Rain last 24 hours']) + ',' + str(WxData['Temperature']) + ',' + str(WxData['Humidity']) + ',' + str(WxData['Pressure']) + ',' + str(WxData['Temp backup']) + ',' + str(WxData['Status bits'])
payload = ':' + 'PE1RXF-3 ' + ':' + str(WxData['Wind direction']) + ',' + str(WxData['Wind speed']) + ',' + str(WxData['Wind gust']) + ',' + str(WxData['Rain last hour']) + ',' + str(WxData['Rain last 24 hours']) + ',' + str(WxData['Temperature']) + ',' + str(WxData['Humidity']) + ',' + str(WxData['Pressure']) + ',' + str(WxData['Temp backup']) + ',' + str(WxData['Status bits']) + ',' + str(WxData['Luminosity'])
telemetry=mqtt_client.publish_telemetry_message("PE1RXF-13", "ax1", "PE1RXF-3", payload)
@@ -417,6 +450,7 @@ def run():
WxData['Temperature'] = 0.0
WxData['Humidity'] = 0.0
WxData['Pressure'] = 0.0
WxData['Luminosity'] = 0.0
WxData['Temp backup'] = 0.0
WxData['Status bits'] = 0.0
@@ -576,6 +610,6 @@ def run():
if __name__ == '__main__':
sys.stdout = sys.stderr = open('/home/marcel/pe1rxf_aprs_debug.log', 'w')
#sys.stdout = sys.stderr = open('/home/marcel/pe1rxf_aprs_debug.log', 'w')
run()

View File

@@ -22,7 +22,7 @@
# Global settings
global:
log-rf: /home/marcel/test/pe1rxf_aprs-rf.log # Log RF traffic to file (0=no logging)
log-rf: /home/marcel/ham/weather_station/pe1rxf_aprs-rf.log # Log RF traffic to file (0=no logging)
modbus:
port: /dev/serial/by-path/platform-3f980000.usb-usb-0:1.3:1.0-port0 # USB port to which RS-485 dongle is connected
@@ -41,32 +41,32 @@ weather:
- 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: WIDE2-2 # Digipeater path for weather reports (0 = no path)
digi_path: WIDE2-1 # Digipeater path for weather reports (0 = no path)
position: 5302.76N/00707.85E_ # The position string for the weather station
interval: 10 # Time between weather report transmissions (0 = disable)
interval: 30 # Time between weather report transmissions (0 = disable)
- port: ax1
call: PE1RXF-13
destination: APZMDM
digi_path: WIDE2-2
digi_path: WIDE2-1
position: 5302.76N/00707.85E_
interval: 30
- port: aprsis
call: PE1RXF-13
destination: APZMDM
digi_path: 0
position: 5302.76N/00707.85E_
interval: 10
# - port: aprsis
# call: PE1RXF-13
# destination: APZMDM
# digi_path: 0
# position: 5302.76N/00707.85E_
# interval: 10
# APRS beacon section
beacon:
- port: ax0 # The AX.25 port on which to transmit (use aprsis for beaconing to the internet via APRS-IS, set to 0 if you want to use the call assigned to the port in /etc/ax25/axports)
- port: ax1 # The AX.25 port on which to transmit (use aprsis for beaconing to the internet via APRS-IS, set to 0 if you want to use the call assigned to the port in /etc/ax25/axports)
call: PE1RXF-1 # 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 # Specifie the digipeater path (best practise is to use WIDE2-1, WIDE2-2 or set to 0 for no path)
position: "!5302.78NR00707.91E&" # The position string for the beacon (better to put this string between parentheses)
message: APRS RX iGATE 144.800MHz # The beacon text
interval: 30 # Beacon interval in minutes
- port: ax1
- port: ax0
call: PE1RXF-3
destination: APZMDM
digi_path: WIDE2-1
@@ -87,3 +87,10 @@ beacon:
position: "!5302.78NL00707.91E&"
message: LoRa APRS RX iGATE 433.775MHz
interval: 10
- port: aprsis
call: PE1RXF
destination: APJ8CL
digi_path: 0
position: "=5302.78N/00707.90EG"
message: JS8 to APRS RX iGate 7.078MHz
interval: 10

9
pe1rxf_aprs_logrotate.sh Executable file
View File

@@ -0,0 +1,9 @@
#!/bin/bash
# Rotate the RF logs of pe1rxf_aprs program. If called every week, three weeks worth of logs are kept. Older logs are overwritten
cd /home/marcel/ham/weather_station
cp pe1rxf_aprs-rf.log.2 pe1rxf_aprs-rf.log.3
cp pe1rxf_aprs-rf.log.1 pe1rxf_aprs-rf.log.2
cp pe1rxf_aprs-rf.log pe1rxf_aprs-rf.log.1
/usr/bin/truncate -s 0 pe1rxf_aprs-rf.log

625
pe1rxf_aprs_test.py Executable file
View File

@@ -0,0 +1,625 @@
#!/usr/bin/python3
'''
# 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-2025 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 <https://www.gnu.org/licenses/>.
'''
import sys
import time
from time import gmtime, strftime
import pythonax25
import yaml
from yaml.loader import SafeLoader
import schedule
import aprslib
import logging
from threading import Thread
from weather_station_modbus import WeatherStation
from config_reader import config_reader
from aprs_telemetry_to_mqtt import aprs_telemetry_to_mqtt
main_config_file = "pe1rxf_aprs.yml"
telemetry_config_file = "pe1rxf_telemetry.yml"
rflog_file = ""
# Make Weather data global so scheduled task can use it
WxData = {}
APRSIS = []
aprsis_data = ['', '',False] # Global variable for storing received APRSIS frames. Shared between threads. [0]=from_call, [1]=payload, [2]=token (set by thread, reset by main loop)
axport = []
axdevice = []
axaddress = []
class aprs_status:
nr_of_ports = 0
pass
aprs = aprs_status()
# AX.25 stuff
def setup_ax25():
# Check if there's any active AX25 port
print("\nAvailable AX.25 ports:")
current_port = 0;
port_nr = pythonax25.config_load_ports()
aprs.nr_of_ports = port_nr
if port_nr > 0:
# Get the device name of the first port
axport.append(pythonax25.config_get_first_port())
axdevice.append(pythonax25.config_get_device(axport[current_port]))
axaddress.append(pythonax25.config_get_address(axport[current_port]))
print (axport[current_port], axdevice[current_port], axaddress[current_port])
current_port = current_port + 1
while port_nr - current_port > 0:
axport.append(pythonax25.config_get_next_port(axport[current_port-1]))
axdevice.append(pythonax25.config_get_device(axport[current_port]))
axaddress.append(pythonax25.config_get_address(axport[current_port]))
print (axport[current_port], axdevice[current_port], axaddress[current_port])
current_port = current_port + 1
else:
print("No AX.25 ports found.")
sys.exit()
# Initiate a PF_PACKET socket (RX)
rx_socket = pythonax25.packet_socket()
return rx_socket
def setup_old__ax25():
# Check if there's any active AX25 port
print("\nAvailable AX.25 ports:")
current_port = 0;
port_nr = pythonax25.config_load_ports()
aprs.nr_of_ports = port_nr
if port_nr > 0:
# Get the device name of the first port
axport.append(pythonax25.config_get_first_port())
axdevice.append(pythonax25.config_get_device(axport[current_port]))
axaddress.append(pythonax25.config_get_address(axport[current_port]))
print (axport[current_port], axdevice[current_port], axaddress[current_port])
current_port = current_port + 1
while port_nr - current_port > 0:
axport.append(pythonax25.config_get_next_port(axport[current_port-1]))
axdevice.append(pythonax25.config_get_device(axport[current_port]))
axaddress.append(pythonax25.config_get_address(axport[current_port]))
print (axport[current_port], axdevice[current_port], axaddress[current_port])
current_port = current_port + 1
else:
print("No AX.25 ports found.")
sys.exit()
# Initiate a PF_PACKET socket (RX)
rx_socket = pythonax25.packet_socket()
return rx_socket
def receive_ax25(rx_socket):
# Blocking receive packet, 10 ms timeout
receive = pythonax25.packet_rx(rx_socket,10)
return receive
def parsePacket(string):
# Split the address and payload separated by APRS PID
buffer = string.split(b'\x03\xf0')
address = buffer[0]
#Define empty list in case packet does not has digipeaters in path
digipeaters = []
# Check if the first byte indicates it is a data packet
if address[0] == 0:
# Cut the first byte and feed it to the address parser
listAddress = getAllAddress(address[1:])
if listAddress != 0:
# Get the source, destination, and digipeaters from the address list
source = listAddress[1]
destination = listAddress[0]
digipeaters = listAddress[2:]
# Occasionally a bad packet is received causng the program to crash with an "IndexError: list index out of range". Fix: check if index IS out of range before copying it to payload
if len(buffer) > 1:
payload = buffer[1]
else:
payload = 'NOT VALID'
else:
# If there was an error decoding the address we return save values which will be ignored by the rest of the program
source = 'NOCALL'
destination = 'NOCALL'
digipeaters = 'NOCALL'
payload = 'NOT VALID'
else:
# If there was an error decoding the address we return save values which will be ignored by the rest of the program
source = 'NOCALL'
destination = 'NOCALL'
digipeaters = 'NOCALL'
payload = 'NOT VALID'
#raise Exception('Not a data packet')
return (source, destination, digipeaters, payload)
def getAllAddress(packetAddress):
allAddress = []
addressSize = 7
# Check if the networked address string is valid
if (len(packetAddress) % 7) == 0:
for i in range(0, len(packetAddress), addressSize):
address = ""
# First extract CALL
for pos in range(6):
address = address + ( chr(packetAddress[i+pos]>>1) )
# Remove spaces
address= address.rstrip()
# Than extract SSID
ssid = packetAddress[i+6] & 0b00011110
ssid = ssid >> 1
if ssid != 0:
address = address + '-' + str(ssid)
# Has been repeated flag set? Check only for digipeaters, not source and destination
if i != 0 and i != 7:
if packetAddress[i+6] & 0b10000000:
address = address + '*'
#print (address)
allAddress.append(address)
# Create a list of all address in ASCII form
#try:
# allAddress = [pythonax25.network_to_ascii(packetAddress[i:i+addressSize])
# for i in range(0, len(packetAddress), addressSize)]
#except:
# allAddress = 0
#print (allAddress)
return allAddress
else:
# Received a non valid address. Fill return value with NULL so we don't crash
allAddress = 0
return allAddress
#raise Exception('Error: Address is not a multiple of 7')
def process_aprsis(packet):
global aprsis_data
# Setup MQTT connection
mqtt_connection2 = aprs_telemetry_to_mqtt(telemetry_config_file)
mqtt_connection2.read_settings()
#print("Received APRSIS")
timestamp = time.strftime("%Y-%m-%d %H:%M:%S", gmtime())
if rflog_file == 0:
return 0
string = timestamp + ' ' + 'APRSIS ' + ' R ' + str(packet, 'latin-1') + '\n'
# Check if frame is a message to us. If so send it to mqtt
# Split packet at first occurance of ':', which seperates the header from the payload
aprsis_payload = str(packet, 'latin-1').split(':', 1)
# We should have two substrings
if len(aprsis_payload) == 2:
# extract from_call
aprsis_data[0] = aprsis_payload[0].split('>',1)
aprsis_data[1] = aprsis_payload[1]
aprsis_data[2] = True
#print ("In thread:")
#print (aprsis_data[0])
#print (aprsis_data[1])
# write APRSIS string to log file
try:
with open(rflog_file, "a") as logfile:
logfile.write(string)
except:
return 0
else:
return 1
def send_aprsis(srcCall, dest, digi, msg):
# Bug, once the program crashed when accessing msg[0] because of "error IndexError: string index out of range". This should never happen, because the function parsePacket checks for this.
# But id did happen, don't know why. But to prevent is from happening again, we check it here as well:
try:
tmp = msg[0]
except:
print("Empty APRS message, nothing to do.")
return 1
try:
APRSIS.connect()
except:
print("Could not connect to APRS-IS network.")
else:
if digi == 0 or digi == "":
message = srcCall + '>' + dest + ':' + msg
else:
# Drop those packets we shouldn't send to APRS-IS
if (',TCP' in digi): # drop packets sourced from internet
print(">>> Internet packet not igated: " + packet)
return 1
if ('RFONLY' in digi):
print(">>> RFONLY, not igated.")
return 1
if ('NOGATE' in digi):
print(">>> NOGATE, not igated")
return 1
if msg[0] == '}':
if ('TCPIP' in msg) or ('TCPXX' in msg):
print(">>> Third party packet, not igated")
return 1
# TODO: strip third party header and send to igate
else:
print(">>> Third party packet, not igated")
return 1
if msg[0] == '?':
print(">>> Query, not igated")
return 1
message = srcCall + '>' + dest + ',' + digi + ':' + msg
# send a single status message
try:
APRSIS.sendall(message)
#print(message)
except:
print("Failed to send message to APRS-IS network.")
else:
log_ax25("APRSIS ", srcCall, dest, digi, msg, 'T')
def send_ax25(portCall, srcCall, dest, digi, msg):
# Initiate a datagram socket (TX)
tx_socket = pythonax25.datagram_socket()
res = pythonax25.datagram_bind(tx_socket, srcCall, portCall)
#print(res)
if digi == 0 or digi == "":
res = pythonax25.datagram_tx(tx_socket, dest, msg)
#print(timestamp + ' ' + portCall + ' T ' + srcCall + '>' + dest + ':' + msg)
else:
res = pythonax25.datagram_tx_digi(tx_socket, dest, digi, msg)
#print(timestamp + ' ' + portCall + ' T ' + srcCall + '>' + dest + ',' + digi + ':' + msg)
#print(res)
pythonax25.close_socket(tx_socket)
log_ax25(portCall, srcCall, dest, digi, msg, 'T')
# If logging is enabled in configuration file (global:log-rf), append APRS frame to log file
def log_ax25(portCall, srcCall, dest, digi, msg, mode):
timestamp = time.strftime("%Y-%m-%d %H:%M:%S", gmtime())
if rflog_file == 0:
return 0
if digi == 0 or digi == "":
string = timestamp + ' ' + portCall.ljust(9) + ' ' + mode + ' ' + srcCall + '>' + dest + ':' + msg + '\n'
else:
string = timestamp + ' ' + portCall.ljust(9) + ' ' + mode + ' ' + srcCall + '>' + dest + ',' + digi + ':' + msg + '\n'
try:
with open(rflog_file, "a") as logfile:
logfile.write(string)
except:
return 0
else:
return 1
def send_aprs_weather_report(weather_settings):
global WxData
# Convert sensible SI values to freedom units for APRS weather report
wind_direction = int(WxData['Wind direction'])
wind_speed = int(2.2369 * WxData['Wind speed'])
wind_gust = int(2.2369 * WxData['Wind gust'])
rain_lasthour = int(3.93700787 * WxData['Rain last hour'])
#rain_lasthour = 0
rain_24hour = int(3.93700787 * WxData['Rain last 24 hours'])
#rain_24hour = 0
temperature = int(WxData['Temperature'] * 1.8 + 32)
humidity = int(WxData['Humidity'])
if (humidity == 100):
humidity = 0;
pressure =int(10 * WxData['Pressure'])
luminosity = int(0.0079 * WxData['Luminosity']) # W/m2: various sources give 0.0079 or 0.0083 as an approxmation for sunlight
if (luminosity <= 999):
APRS_Lum = 'L'
else:
APRS_Lum = 'l'
luminosity = luminosity-1000
# Get date and time
timestamp = time.strftime("%d%H%M", gmtime())
# Construct APRS weather report
aprs_position = weather_settings['position']
aprs_wx_report = '@' + timestamp + 'z' + weather_settings['position'] + "{:03d}".format(wind_direction) + '/' + "{:03d}".format(wind_speed) + 'g' + "{:03d}".format(wind_gust) + 't' + "{:03d}".format(temperature) + 'r' + "{:03d}".format(rain_lasthour) + 'p' + "{:03d}".format(rain_24hour) + 'h' + "{:02d}".format(humidity) + 'b' + "{:05d}".format(pressure) + APRS_Lum + "{:03d}".format(luminosity)
if weather_settings['port'] == 'aprsis':
send_aprsis(weather_settings['call'], weather_settings['destination'], weather_settings['digi_path'], aprs_wx_report)
else:
# Send it
# Find call of ax25 port
port_call = 0
for position in range(len(axdevice)):
if axdevice[position] == weather_settings['port']:
port_call = axaddress[position]
if (weather_settings['call'] == 0):
src_call = port_call
else:
src_call = weather_settings['call']
send_ax25(port_call, src_call, weather_settings['destination'], weather_settings['digi_path'], aprs_wx_report)
def send_aprs_beacon(beacon_settings):
beacon_message = beacon_settings['position'] + beacon_settings['message']
if beacon_settings['port'] == 'aprsis':
send_aprsis(beacon_settings['call'], beacon_settings['destination'], beacon_settings['digi_path'], beacon_message)
else:
# Send it
# Find call of ax25 port
port_call = 0
for position in range(len(axdevice)):
if axdevice[position] == beacon_settings['port']:
port_call = axaddress[position]
if (beacon_settings['call'] == 0):
src_call = port_call
else:
src_call = beacon_settings['call']
send_ax25(port_call, src_call, beacon_settings['destination'], beacon_settings['digi_path'], beacon_message)
def send_telemetry():
message = ':' + 'PE1RXF-3 ' + ':' + str(WxData['Wind direction']) + ',' + str(WxData['Wind speed']) + ',' + str(WxData['Wind gust']) + ',' + str(WxData['Rain last hour']) + ',' + str(WxData['Rain last 24 hours']) + ',' + str(WxData['Temperature']) + ',' + str(WxData['Humidity']) + ',' + str(WxData['Pressure']) + ',' + str(WxData['Temp backup']) + ',' + str(WxData['Status bits'])
send_ax25('PE1RXF-3', 'PE1RXF-13', "APZMDM", 0, message)
def publish_weather_data(mqtt_client):
payload = ':' + 'PE1RXF-3 ' + ':' + str(WxData['Wind direction']) + ',' + str(WxData['Wind speed']) + ',' + str(WxData['Wind gust']) + ',' + str(WxData['Rain last hour']) + ',' + str(WxData['Rain last 24 hours']) + ',' + str(WxData['Temperature']) + ',' + str(WxData['Humidity']) + ',' + str(WxData['Pressure']) + ',' + str(WxData['Temp backup']) + ',' + str(WxData['Status bits']) + ',' + str(WxData['Luminosity'])
telemetry=mqtt_client.publish_telemetry_message("PE1RXF-13", "ax1", "PE1RXF-3", payload)
return 1
def read_weather_station(weather_station):
global WxData
#print ("Reading registers of weather station.")
if weather_station.get_weather_data() == 0:
print ('No response from ModBus, even after 5 retries. Keep trying.')
else:
WxData = weather_station.wx_data
print (WxData)
def check_heater(weather_station):
# Check if heater is off, if so, turn it on
if weather_station.wx_data['Status bits'] & 0x4 == 0:
weather_station.enable_heater()
print("Heater was off, turned it back on gain.")
def check_thread_alive(thr):
thr.join(timeout=0.0)
# returns True if the thread is still running and False, otherwise
return thr.is_alive()
def run():
global WxData
# Fill WxData with some sensible data to start:
WxData['ID'] = 0.0
WxData['Wind direction'] = 0.0
WxData['Wind speed'] = 0.0
WxData['Wind gust'] = 0.0
WxData['Rain last hour'] = 0.0
WxData['Rain last 24 hours'] = 0.0
WxData['Temperature'] = 0.0
WxData['Humidity'] = 0.0
WxData['Pressure'] = 0.0
WxData['Luminosity'] = 0.0
WxData['Temp backup'] = 0.0
WxData['Status bits'] = 0.0
# Debug logging for aprslib
logging.basicConfig(level=logging.DEBUG) # level=10
loop_counter=0
# Read the main configuration file (pe1rxf_aprs.yml)
Configuration = config_reader(main_config_file, telemetry_config_file)
if Configuration.read_settings() == 0:
sys.exit()
global rflog_file
rflog_file = Configuration.config_file_settings['global']['log-rf']
print ("Write APRS frames to: " + rflog_file)
print ("Read configuration files.")
# Setup MQTT connection
mqtt_connection = aprs_telemetry_to_mqtt(telemetry_config_file)
mqtt_connection.read_settings()
rx_socket = setup_ax25()
# a valid passcode for the callsign is required in order to send
print("Connecting to APRS-IS server")
global APRSIS
APRSIS = aprslib.IS(Configuration.config_file_settings['aprsis']['call'], passwd=Configuration.config_file_settings['aprsis']['passcode'], port=Configuration.config_file_settings['aprsis']['port'])
APRSIS.connect()
print("Trying to connect to the weather station via the RS-485 dongle...")
try:
weather_station = WeatherStation(Configuration.config_file_settings['modbus']['port'], Configuration.config_file_settings['modbus']['address'])
except:
print("No response from the weather station via the ModBus, even after several retries. Disabling the weather station.")
sys.exit(1) # Make program work without weather station
else:
print("Got response from the weather station. Weather information is available.")
# NOTE: Is now done periodically. So when the weather station is unplugged and plugged back in, the heater will be enabled again.
#try:
# weather_station.enable_heater()
#except:
# print("No response from the weather station via the ModBus, even after several retries. Disabling the weather station.")
# sys.exit(1) # Make program work without weather station!
#else:
# print("Enabled the heater function on the weather station.")
###
# Schedule all periodic transmissions (use a little randomness)
###
# Schedule all weather report transmisions
for entry in Configuration.config_file_settings['weather']:
interval = entry['interval']
if interval != 0:
print("Scheduled WX report transmission")
interval = interval * 60 # from minutes to seconds
schedule.every(interval - 59).to(interval + 59).seconds.do(send_aprs_weather_report, entry)
# Schedule all beacon transmisions
for entry in Configuration.config_file_settings['beacon']:
interval = entry['interval']
if interval != 0:
print("Scheduled beacon transmission.")
interval = interval * 60 # from minutes to seconds
schedule.every(interval - 59).to(interval + 59).seconds.do(send_aprs_beacon, entry)
# Schedule check if heater is still on
# So when the weather station is unplugged and plugged back in, the heater will be enabled again.
schedule.every(10).minutes.do(check_heater, weather_station)
# Schedule readout of weather station
print("Scheduled readout of weather station.")
schedule.every(1).minutes.do(read_weather_station, weather_station)
# Schedule telemetry transmision
#print("Scheduled telemetry transmission.")
#schedule.every(10).minutes.do(send_telemetry)
print("Schedule mqtt weather publisher.")
interval = mqtt_connection.config_file_settings['global']['weather_report_interval']
if interval != 0:
schedule.every(interval).minutes.do(publish_weather_data, mqtt_connection)
# Connect to incoming APRS-IS feed
# by default `raw` is False, then each line is ran through aprslib.parse()
# Set filter on incomming feed
APRSIS.set_filter(Configuration.config_file_settings['aprsis']['filter'])
# This is a blocking call, should run as seperate thread. Data from aprsis is stored in aprsid_data. [0]=from_call, [1]=payload, [2]=token (set by thread, reset by main loop)
# create a thread
global aprsis_data
thread = Thread(target=APRSIS.consumer, args=(process_aprsis, True, True, True))
# run the thread
thread.start()
while (1):
#print ("Reading registers of weather station.")
#if weather_station.get_weather_data() == 0:
# print ('No response from ModBus, even after 5 retries. Keep trying.')
#else:
# WxData = weather_station.wx_data
# Scheduler
schedule.run_pending()
# Check if APRS-IS thread is still running, if not restart it
if check_thread_alive(thread) == False:
# Set filter on incomming feed
APRSIS.set_filter(Configuration.config_file_settings['aprsis']['filter'])
# This is a blocking call, should run as seperate thread
# create a thread
thread = Thread(target=APRSIS.consumer, args=(process_aprsis, True, True, True))
# run the thread
thread.start()
# Listen on all ax25 ports and send to APRS-IS
receive = receive_ax25(rx_socket)
for port in range(len(axdevice)):
if receive[0][1] == axdevice[port]:
#print(receive)
source, destination, digipeaters, payload = parsePacket(receive[1])
#print(receive[1])
# Convert byte string to normal string
try:
payload = payload.decode('latin-1')
except:
payload = 'NOT VALID'
#print("Packet Received by = %s"%axaddress[0])
#print("Source Address = %s"%source)
#print("Destination Address = %s"%destination)
#print("Digipeaters =")
#print(digipeaters)
#print("Payload = %s"%payload)
#print("")
#aprs_frame = axaddress[0] + '>' + destination + ',' + ','.join(digipeaters) + ':' + payload
#print (aprs_frame)
if payload == 'NOT VALID' or payload == 0:
print (">>> Packet not valid, ignored.")
else:
log_ax25(axaddress[port], source, destination, ','.join(digipeaters), payload, 'R')
digipeaters.append('qAR')
digipeaters.append(Configuration.config_file_settings['aprsis']['call'])
send_aprsis(source, destination, ','.join(digipeaters), payload)
# Check if APRS frame is PE1RXF telemetry
telemetry=mqtt_connection.publish_telemetry_message(source, axdevice[port], axaddress[port], payload)
# Check if APRS frame is a message to us
mqtt_connection.publish_aprs_messages(source, axdevice[port], payload)
# If APRSIS read thread receives message for us, send it to MQTT
if aprsis_data[2] == True:
aprsis_data[2] = False
print ("In loop:")
print (aprsis_data[0])
print (aprsis_data[1])
mqtt_connection.publish_aprs_messages(aprsis_data[0][0], 'APRSIS', aprsis_data[1])
#time.sleep(1) # Short sleep
if __name__ == '__main__':
#sys.stdout = sys.stderr = open('/home/marcel/pe1rxf_aprs_debug.log', 'w')
run()

View File

@@ -57,5 +57,6 @@ topics:
- pressure
- temperature_backup
- status_bits
- luminosity

View File

@@ -61,51 +61,42 @@ class WeatherStation(minimalmodbus.Instrument):
#Address range 0x3000
def get_id(self):
"""PV array rated voltage"""
return self.retriable_read_register(0, 0, 4)
def get_wind_direction(self):
"""PV array rated current"""
return self.retriable_read_register(1, 1, 4)
def get_wind_speedl(self):
"""PV array rated power (low 16 bits)"""
return self.retriable_read_register(2, 2, 4)
def get_wind_gust(self):
"""PV array rated power (high 16 bits)"""
return self.retriable_read_register(3, 2, 4)
def get_temperature(self):
"""Rated Battery's voltage"""
return self.retriable_read_register(4, 2, 4, True)
def get_rain(self):
"""Rated charging current to battery"""
return self.retriable_read_register(5, 2, 4)
def get_rain_last24(self):
"""Rated charging power to battery (low 16 bits)"""
return self.retriable_read_register(6, 2, 4)
def get_rain_since_midnight(self):
"""Charging equipment rated output power (high 16 bits)"""
return self.retriable_read_register(7, 0, 4)
def get_humidity(self):
"""Charging mode: 0x0001 = PWM"""
return self.retriable_read_register(8, 2, 4)
def get_pressure(self):
"""Charging mode: 0x0001 = PWM"""
return self.retriable_read_register(9, 1, 4)
def get_luminosity(self):
return self.retriable_read_register(10, 0, 4)
def get_temperature_backup(self):
"""Charging mode: 0x0001 = PWM"""
return self.retriable_read_register(13, 2, 4,True)
def get_status_bits(self):
"""Charging mode: 0x0001 = PWM"""
return self.retriable_read_register(14, 0, 4)
def enable_heater(self):
@@ -126,6 +117,7 @@ class WeatherStation(minimalmodbus.Instrument):
self.wx_data['Temperature'] = self.get_temperature()
self.wx_data['Humidity'] = self.get_humidity()
self.wx_data['Pressure'] = self.get_pressure()
self.wx_data['Luminosity'] = self.get_luminosity()
self.wx_data['Temp backup'] = self.get_temperature_backup()
self.wx_data['Status bits'] = self.get_status_bits()