Compare commits
6 Commits
8d485233bd
...
master
Author | SHA1 | Date | |
---|---|---|---|
![]() |
adecfc923c | ||
![]() |
7e2aa14f2a | ||
![]() |
589f303a16 | ||
![]() |
88e486936b | ||
![]() |
6a0b201507 | ||
![]() |
e55915d2c7 |
41
CHANGELOG.md
41
CHANGELOG.md
@@ -31,10 +31,49 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
### Added
|
||||
|
||||
- connected to APRS-IS feed
|
||||
- connected to APRS-IS feed (seperate thread)
|
||||
|
||||
## [0.1.1] - 2024-02-17
|
||||
|
||||
### Added
|
||||
|
||||
- iGate functionality
|
||||
|
||||
## [0.1.2] - 2024-02-20
|
||||
|
||||
### Fixed
|
||||
|
||||
- Convert aprs payload byte stings to character strings used utf-8, which gave errors. It now uses latin-1.
|
||||
|
||||
### Changed
|
||||
|
||||
- Reading weather station is now a scheduled task.
|
||||
|
||||
## [0.1.3] - 2024-03-14
|
||||
|
||||
### Added
|
||||
|
||||
- PE1RXF telemetry to MQTT bridge
|
||||
- Forwarding of APRS messages to MQTT
|
||||
|
||||
## [0.1.4] - 2024-03-28
|
||||
|
||||
### 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)
|
||||
|
@@ -2,6 +2,10 @@
|
||||
|
||||
A basic Linux APRS iGate and APRS weather station with additional (optional) PE1RXF telemetry support.
|
||||
|
||||
TODO:
|
||||
|
||||
- change heater algoritm back to 5 min on/15 min off, but start at 92% in stead of 96%. If the cooling period is less than 15 minutes, the temperture readings are periodically off by 1-1.5 degrees.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python3
|
||||
@@ -10,6 +14,7 @@ A basic Linux APRS iGate and APRS weather station with additional (optional) PE1
|
||||
- gps3
|
||||
- schedule
|
||||
- aprslib
|
||||
- paho.mqtt
|
||||
|
||||
## License
|
||||
|
||||
|
239
aprs_telemetry_to_mqtt.py
Executable file
239
aprs_telemetry_to_mqtt.py
Executable file
@@ -0,0 +1,239 @@
|
||||
#!/usr/bin/python3
|
||||
''''
|
||||
# A bridge between PE1RXF APRS telemetry messaging and MQTT.
|
||||
# It uses pythonax25 (https://github.com/josefmtd/python-ax25)
|
||||
#
|
||||
# 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 <https://www.gnu.org/licenses/>.
|
||||
'''
|
||||
import sys
|
||||
import random
|
||||
import time
|
||||
from time import gmtime, strftime
|
||||
#import os
|
||||
#from pathlib import Path
|
||||
import yaml
|
||||
from yaml.loader import SafeLoader
|
||||
#from paho.mqtt import client as mqtt_client
|
||||
import paho.mqtt.publish as publish
|
||||
import logging
|
||||
import json
|
||||
#import csv
|
||||
|
||||
logger = logging.getLogger("aprs_telemetry_to_mqtt")
|
||||
|
||||
class aprs_telemetry_to_mqtt:
|
||||
|
||||
# initiate class: define name configuration files
|
||||
def __init__(self, telemetry_config_file):
|
||||
self.config_file = telemetry_config_file
|
||||
logger.info("Initializing telemetry to mqtt bridge.")
|
||||
|
||||
def read_settings(self):
|
||||
if self.read_config_file() == 0:
|
||||
return 0
|
||||
|
||||
if self.test_telemetry_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 ("Telemetry configuration file ./" + self.config_file + " not found or syntax error in file.")
|
||||
return 0
|
||||
else:
|
||||
return 1
|
||||
|
||||
def test_telemetry_settings(self):
|
||||
try:
|
||||
tmp = self.config_file_settings['global']['broker']
|
||||
tmp = self.config_file_settings['global']['port']
|
||||
tmp = self.config_file_settings['global']['topic_root']
|
||||
tmp = self.config_file_settings['global']['publish_messages']
|
||||
tmp = self.config_file_settings['global']['call']
|
||||
tmp = self.config_file_settings['global']['weather_report_interval']
|
||||
tmp = self.config_file_settings['global']['blacklist']
|
||||
tmp = self.config_file_settings['topics']
|
||||
except:
|
||||
print ("Error in the telemetry configuration file.")
|
||||
return 0
|
||||
else:
|
||||
#print (self.config_file_settings['global']['topic_root'])
|
||||
tmp = self.config_file_settings['global']['topic_root']
|
||||
#mqtt_client_id = f'{self.config_file_settings['global']['topic_root']}-{random.randint(0, 1000)}'
|
||||
self.mqtt_client_id = f'{tmp}-{random.randint(0, 1000)}'
|
||||
return 1
|
||||
|
||||
# Publish a single message to the MQTT broker
|
||||
def publish(self, topic, message):
|
||||
try:
|
||||
#publish.single(topic, message, hostname=self.config_file_settings['global']['broker'])
|
||||
publish.single(topic, message, hostname=self.config_file_settings['global']['broker'], port=self.config_file_settings['global']['port'], client_id=self.mqtt_client_id)
|
||||
except:
|
||||
logger.debug("Failed to connect to MQTT broker.")
|
||||
else:
|
||||
logger.debug('Published: ' + topic + '=' + message)
|
||||
|
||||
# Checks if payload (APRS message) contains valid PE1RXF telemetry data.
|
||||
# If so, it sends the formatted data to the MQTT broker.
|
||||
# Configuration is done via the file pe1rxf_telemetry.yml
|
||||
def publish_telemetry_message(self, source, ax_device, ax_address, payload):
|
||||
|
||||
values=0
|
||||
|
||||
for topics in self.config_file_settings['topics']:
|
||||
|
||||
# Check source call
|
||||
if source == topics['call']:
|
||||
|
||||
# Check ax_port
|
||||
if topics['ax_port'] == 'all' or topics['ax_port'] == ax_device:
|
||||
#print('Call found in configuration file')
|
||||
|
||||
# split payload at colon. If it is a valid reply, we should get three
|
||||
# substrings: the first in empty, the second with the call of the ax25
|
||||
# interface and the thirth with the status of the outputs
|
||||
split_message=payload.split(":")
|
||||
if len(split_message) == 3:
|
||||
#Remove spaces from destination call and test if message is for the server
|
||||
if split_message[1].replace(" ", "") == ax_address:
|
||||
print ('Received from: ' + source + ' Telemetry: ' + split_message[2])
|
||||
|
||||
# The telemetry is available in split_message[2], but we have to check if it contains any valid data
|
||||
# Try to split into seperate values (values should be seperated by a comma)
|
||||
values=split_message[2].split(",")
|
||||
# Test al values: should be numbers and nothing else
|
||||
for field in values:
|
||||
if not self.is_float(field):
|
||||
return 0
|
||||
# One anoying thing of the PE1RXF telemetry standard is that there is also a message containing the status of the output bits.
|
||||
# These messages are interpreted as valid telemetry data by this program. The message is send after a '06' command. This program
|
||||
# does not request this message, but another program might. So we have to filter these messages output
|
||||
if len(values[0]) == 5:
|
||||
allowed = '0' + '1'
|
||||
# Removes from the original string all the characters that are allowed, leaving us with a set containing either a) nothing, or b) the #offending characters from the string:'
|
||||
if not set(values[0]) - set(allowed):
|
||||
print ("Probably digital status bits. Ignore.")
|
||||
return 0
|
||||
|
||||
# Check if number of telemtry values and number of descriptions in yml file are the same. If not make then the same by appending to the shorted list.
|
||||
nr_of_values = len(values)
|
||||
nr_of_descriptions = len(topics['description'])
|
||||
|
||||
if nr_of_values > nr_of_descriptions:
|
||||
items_to_add = nr_of_values - nr_of_descriptions
|
||||
for x in range(items_to_add):
|
||||
topics['description'].append('NotDefined')
|
||||
print('Added ' + str(items_to_add) + ' to descriptions')
|
||||
elif nr_of_values < nr_of_descriptions:
|
||||
items_to_add = nr_of_descriptions - nr_of_values
|
||||
for x in range(items_to_add):
|
||||
values.append('0.0')
|
||||
print('Added ' + str(items_to_add) + ' to values')
|
||||
else:
|
||||
print('values and description are of equal length: good!')
|
||||
|
||||
# Loop through descriptions and send values from telemtry file along with it
|
||||
''''
|
||||
for index, descr in enumerate(topics['description'], start=0):
|
||||
current_topic = self.config_file_settings['global']['topic_root'] + '/' + topics['name'] + '/' + descr
|
||||
#self.publish(client,current_topic,values[index])
|
||||
try:
|
||||
publish.single(current_topic, values[index], hostname=self.config_file_settings['global']['broker'])
|
||||
except:
|
||||
print("Failed to connect to MQTT broker.")
|
||||
else:
|
||||
print('Published: ' + current_topic + '=' + values[index])
|
||||
'''
|
||||
# Loop through descriptions and send values from telemtry file along with it
|
||||
publish_list = []
|
||||
for index, descr in enumerate(topics['description'], start=0):
|
||||
current_topic = self.config_file_settings['global']['topic_root'] + '/' + topics['name'] + '/' + descr
|
||||
|
||||
publish_list.append({"topic": current_topic, "payload": values[index]})
|
||||
|
||||
try:
|
||||
#publish.multiple(publish_list, hostname=self.config_file_settings['global']['broker'])
|
||||
publish.multiple(publish_list, hostname=self.config_file_settings['global']['broker'], port=self.config_file_settings['global']['port'], client_id=self.mqtt_client_id)
|
||||
except:
|
||||
logger.debug("Failed to connect to MQTT broker.")
|
||||
else:
|
||||
logger.debug('Published telemetry to mqtt broker.')
|
||||
logger.debug(publish_list)
|
||||
|
||||
|
||||
return values
|
||||
|
||||
# Checks if payload (APRS message) contains valid message to us.
|
||||
# If so, it sends the formatted data to the MQTT broker.
|
||||
# Configuration is done via the file pe1rxf_telemetry.yml
|
||||
def publish_aprs_messages(self, source, ax_device, payload):
|
||||
|
||||
#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 = {}
|
||||
|
||||
# Loop through blacklist and check if source is not in it
|
||||
for call in self.config_file_settings['global']['blacklist']:
|
||||
if call == source:
|
||||
print ("Call " + call + " blacklisted. Message not send to mqtt broker.")
|
||||
return 0
|
||||
|
||||
# If we come to here the sender is not in the blacklist
|
||||
# But is could well be an ordinary aprs packet, so let's check if it is a message.'
|
||||
# Split payload at colon. If it is a valid message, we should get three
|
||||
# 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(" ", "")
|
||||
# check if call defined in the configuration file is part of the destination call. This way we can also catch all the prefixes
|
||||
if self.config_file_settings['global']['call'] in split_message[1]:
|
||||
print ('Received from: ' + source + ' payload: ' + split_message[2])
|
||||
mqtt_message['from'] = source
|
||||
mqtt_message['to'] = split_message[1]
|
||||
mqtt_message['port'] = ax_device
|
||||
mqtt_message['time'] = time.strftime("%Y-%m-%d %H:%M:%S", gmtime())
|
||||
mqtt_message['message'] = split_message[2]
|
||||
|
||||
message = json.dumps(mqtt_message)
|
||||
topic = self.config_file_settings['global']['topic_root'] + '/aprs_message'
|
||||
self.publish(topic, message)
|
||||
|
||||
# Test if string is a number
|
||||
def is_float(self, v):
|
||||
try:
|
||||
f=float(v)
|
||||
except:
|
||||
return False
|
||||
|
||||
return True
|
36
ax25.py
36
ax25.py
@@ -1,36 +0,0 @@
|
||||
'''
|
||||
# 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 <https://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import yaml
|
||||
from yaml.loader import SafeLoader
|
||||
|
||||
class config_reader:
|
||||
|
||||
# initiate class: define name configuration files
|
||||
def __init__(self, main_config_file, telemetry_config_file):
|
||||
return 0
|
||||
|
203
pe1rxf_aprs.py
Normal file → Executable file
203
pe1rxf_aprs.py
Normal file → Executable file
@@ -1,3 +1,4 @@
|
||||
#!/usr/bin/python3
|
||||
'''
|
||||
# A basic APRS iGate and APRS weather station with additional (optional) PE1RXF telemetry support
|
||||
#
|
||||
@@ -7,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
|
||||
#
|
||||
@@ -37,13 +38,15 @@ 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 = []
|
||||
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 = []
|
||||
@@ -123,6 +126,8 @@ 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:
|
||||
@@ -201,14 +206,38 @@ def getAllAddress(packetAddress):
|
||||
|
||||
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, 'utf-8') + '\n'
|
||||
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)
|
||||
@@ -218,6 +247,14 @@ def process_aprsis(packet):
|
||||
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:
|
||||
@@ -236,6 +273,17 @@ def send_aprsis(srcCall, dest, digi, msg):
|
||||
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
|
||||
|
||||
@@ -293,22 +341,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)
|
||||
@@ -352,16 +406,52 @@ def send_telemetry():
|
||||
|
||||
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
|
||||
|
||||
@@ -379,6 +469,10 @@ def run():
|
||||
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
|
||||
@@ -396,14 +490,14 @@ def run():
|
||||
else:
|
||||
print("Got response from the weather station. Weather information is available.")
|
||||
|
||||
# NOTE: Should be 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.")
|
||||
# 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)
|
||||
@@ -424,20 +518,30 @@ def run():
|
||||
interval = interval * 60 # from minutes to seconds
|
||||
schedule.every(interval - 59).to(interval + 59).seconds.do(send_aprs_beacon, entry)
|
||||
|
||||
# Schedule telemetry transmision
|
||||
print("Scheduled telemetry transmission.")
|
||||
schedule.every(10).minutes.do(send_telemetry)
|
||||
|
||||
# ScheduleL check if heater is still on
|
||||
# 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
|
||||
# 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()
|
||||
@@ -445,22 +549,34 @@ def run():
|
||||
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
|
||||
#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])
|
||||
# bug UnicodeDecodeError: 'utf-8' codec can't decode byte 0xf8 in position 47: invalid start byte
|
||||
# Convert byte string to normal string
|
||||
try:
|
||||
payload = payload.decode()
|
||||
payload = payload.decode('latin-1')
|
||||
except:
|
||||
payload = 'NOT VALID'
|
||||
|
||||
@@ -474,16 +590,35 @@ def run():
|
||||
|
||||
#aprs_frame = axaddress[0] + '>' + destination + ',' + ','.join(digipeaters) + ':' + payload
|
||||
#print (aprs_frame)
|
||||
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)
|
||||
#telemetry=process_message(source, port, payload, client)
|
||||
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')
|
||||
|
||||
|
@@ -7,25 +7,25 @@
|
||||
#
|
||||
|
||||
# This section (ax25) is being depricated
|
||||
ax25:
|
||||
call: PE1RXF-13 # Call from which transmissions are made
|
||||
destination: APZMDM # APRS destination
|
||||
#ax25:
|
||||
# call: PE1RXF-13 # Call from which transmissions are made
|
||||
# destination: APZMDM # APRS destination
|
||||
|
||||
telemetry_port: ax1 # Linux AX.25 port to which telemetry is sent
|
||||
telemetry_digi_path: 0 # Digipeater path for telemetry messages (0 = no path)
|
||||
telemetry_interval: 5 # Time between telemetry transmissions
|
||||
telemetry_server: PE1RXF-3 # PE1RXF telemetry server call
|
||||
# telemetry_port: ax1 # Linux AX.25 port to which telemetry is sent
|
||||
# telemetry_digi_path: 0 # Digipeater path for telemetry messages (0 = no path)
|
||||
# telemetry_interval: 5 # Time between telemetry transmissions
|
||||
# telemetry_server: PE1RXF-3 # PE1RXF telemetry server call
|
||||
|
||||
weather_report_port: ax0 # Linux AX.25 port to which telemetry is sent
|
||||
weather_report_digi_path: WIDE2-2 # Digipeater path for weather reports (0 = no path)
|
||||
weather_report_interval: 10 # Time between weather report transmissions
|
||||
# weather_report_port: ax0 # Linux AX.25 port to which telemetry is sent
|
||||
# weather_report_digi_path: WIDE2-2 # Digipeater path for weather reports (0 = no path)
|
||||
# weather_report_interval: 10 # Time between weather report transmissions
|
||||
|
||||
# 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.1:1.0-port0 # USB port to which RS-485 dongle is connected
|
||||
port: /dev/serial/by-path/platform-3f980000.usb-usb-0:1.3:1.0-port0 # USB port to which RS-485 dongle is connected
|
||||
address: 14 # ModBus address of weather station
|
||||
|
||||
# APRS-IS section
|
||||
@@ -41,15 +41,15 @@ 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: 10
|
||||
interval: 30
|
||||
- port: aprsis
|
||||
call: PE1RXF-13
|
||||
destination: APZMDM
|
||||
@@ -59,31 +59,38 @@ weather:
|
||||
|
||||
# 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: APRX29 # APRS destination
|
||||
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: APRX29
|
||||
destination: APZMDM
|
||||
digi_path: WIDE2-1
|
||||
position: "!5302.78NL00707.91E&"
|
||||
message: LoRa APRS RX iGATE 433.775MHz
|
||||
interval: 30
|
||||
- port: aprsis
|
||||
call: PE1RXF-1
|
||||
destination: APRX29
|
||||
destination: APZMDM
|
||||
digi_path: 0
|
||||
position: "!5302.78NR00707.91E&"
|
||||
message: APRS RX iGATE 144.800MHz
|
||||
interval: 10
|
||||
- port: aprsis
|
||||
call: PE1RXF-3
|
||||
destination: APRX29
|
||||
destination: APZMDM
|
||||
digi_path: 0
|
||||
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
9
pe1rxf_aprs_logrotate.sh
Executable 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
625
pe1rxf_aprs_test.py
Executable 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()
|
@@ -1,5 +1,62 @@
|
||||
# The settings for the PE1RXF telemetry server are configured in this file
|
||||
#
|
||||
# NOTE: At the start, the program randomizes the starting time of every individual periodic transmission,
|
||||
# so even if all intervals are equal, the transmissions are not at the same time, but rather spread over time.
|
||||
# The settings for the PE1RXF APRS telemetry to MQTT bridge.
|
||||
# The program does some syntax checking, but not extensively. Be aware that the program may crash when there is an error in this file!
|
||||
#
|
||||
# Add APRS nodes under topics. More than one can be defined.
|
||||
|
||||
# Global settings apply to all other entries
|
||||
global:
|
||||
broker: mqtt.meezenest.nl # The MQTT broker we are going to use
|
||||
port: 1883 # The tcp port of the MQTT broker
|
||||
topic_root: hamnet_aprs_nodes # MQTT topic root
|
||||
weather_report_interval: 1 # Publish weather report from weather station on ModBus to MQTT broker every x minutes (0=do not publish)
|
||||
|
||||
publish_messages: YES # The program can forward APRS messages addressed to us to the MQTT broker. If YES: publish APRS messages addressed to call to mqtt broker (/topic_root/aprs_message/).
|
||||
# If NO: do not publish to mqtt broker
|
||||
call: PE1RXF # Call used for APRS message publishing to mqtt (if no sufix is given, messages for all sufixes will be forwarded to the mqtt broker)
|
||||
blacklist: # APRS messages from these calls are not published on the mqtt broker (for examle, place the calls from telemetry nodes here. These messages are processed via the 'topic' entry.
|
||||
- PE1RXF-13 # This way the messages are not also published as plain messages to the mqtt broker.
|
||||
- PE1RXF-3
|
||||
- PE1RXF-5
|
||||
- PE1RXF-6
|
||||
- PE1RXF-8
|
||||
- PE1RXF-9
|
||||
topics:
|
||||
# MQTT topic: each telemtry node has its own name (sub root) and must be unique
|
||||
- name: solar_generator
|
||||
# telemetry_file is obsolete. Use call instead.
|
||||
#telemetry_file: /home/marcel/ham/aprs_utils/aprs_log/latest_telemetry_PE1RXF-9.dat
|
||||
# Call of the telemetry node
|
||||
call: PE1RXF-9
|
||||
# AX.25 port to listen on (all for all ports)
|
||||
ax_port: all
|
||||
# Defines the names of the values in the telemetry data. These names are used to publish to the MQTT broker.
|
||||
# Make sure the number of descriptions match the number of values in the telemetry data!
|
||||
description:
|
||||
- soc
|
||||
- voltage
|
||||
- power
|
||||
- temperature
|
||||
- name: wx_workshop
|
||||
call: PE1RXF-6
|
||||
ax_port: all
|
||||
description:
|
||||
- temperature
|
||||
- humidity
|
||||
# Definition of the build in weather station telemetry. Set interval in global/weather_report_interval
|
||||
- name: weather_station
|
||||
call: PE1RXF-13
|
||||
ax_port: all
|
||||
description:
|
||||
- wind_direction
|
||||
- wind_speed
|
||||
- wind_gust
|
||||
- rain_lasthour
|
||||
- rain_24hours
|
||||
- temperature
|
||||
- humidity
|
||||
- pressure
|
||||
- temperature_backup
|
||||
- status_bits
|
||||
- luminosity
|
||||
|
||||
|
||||
|
@@ -1,5 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Start weather_station software
|
||||
/usr/bin/python3 /home/marcel/ham/weather_station/weather_station_rs485_client.py -c /home/marcel/ham/weather_station/config.yml &
|
||||
|
@@ -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()
|
||||
|
||||
|
Reference in New Issue
Block a user