Working iGate
This commit is contained in:
12
CHANGELOG.md
12
CHANGELOG.md
@@ -31,10 +31,20 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- connected to APRS-IS feed
|
- connected to APRS-IS feed (seperate thread)
|
||||||
|
|
||||||
## [0.1.1] - 2024-02-17
|
## [0.1.1] - 2024-02-17
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- iGate functionality
|
- 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.
|
||||||
|
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
|
|
||||||
|
|
@@ -123,6 +123,8 @@ def parsePacket(string):
|
|||||||
# Split the address and payload separated by APRS PID
|
# Split the address and payload separated by APRS PID
|
||||||
buffer = string.split(b'\x03\xf0')
|
buffer = string.split(b'\x03\xf0')
|
||||||
address = buffer[0]
|
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
|
# Check if the first byte indicates it is a data packet
|
||||||
if address[0] == 0:
|
if address[0] == 0:
|
||||||
@@ -207,7 +209,7 @@ def process_aprsis(packet):
|
|||||||
if rflog_file == 0:
|
if rflog_file == 0:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
string = timestamp + ' ' + 'APRSIS ' + ' R ' + str(packet, 'utf-8') + '\n'
|
string = timestamp + ' ' + 'APRSIS ' + ' R ' + str(packet, 'latin-1') + '\n'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(rflog_file, "a") as logfile:
|
with open(rflog_file, "a") as logfile:
|
||||||
@@ -236,6 +238,17 @@ def send_aprsis(srcCall, dest, digi, msg):
|
|||||||
if ('NOGATE' in digi):
|
if ('NOGATE' in digi):
|
||||||
print(">>> NOGATE, not igated")
|
print(">>> NOGATE, not igated")
|
||||||
return 1
|
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
|
message = srcCall + '>' + dest + ',' + digi + ':' + msg
|
||||||
|
|
||||||
@@ -352,12 +365,25 @@ def send_telemetry():
|
|||||||
|
|
||||||
send_ax25('PE1RXF-3', 'PE1RXF-13', "APZMDM", 0, message)
|
send_ax25('PE1RXF-3', 'PE1RXF-13', "APZMDM", 0, message)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
def check_heater(weather_station):
|
def check_heater(weather_station):
|
||||||
# Check if heater is off, if so, turn it on
|
# Check if heater is off, if so, turn it on
|
||||||
if weather_station.wx_data['Status bits'] & 0x4 == 0:
|
if weather_station.wx_data['Status bits'] & 0x4 == 0:
|
||||||
weather_station.enable_heater()
|
weather_station.enable_heater()
|
||||||
print("Heater was off, turned it back on gain.")
|
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():
|
def run():
|
||||||
|
|
||||||
global WxData
|
global WxData
|
||||||
@@ -396,14 +422,14 @@ def run():
|
|||||||
else:
|
else:
|
||||||
print("Got response from the weather station. Weather information is available.")
|
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.
|
# NOTE: Is now done periodically. So when the weather station is unplugged and plugged back in, the heater will be enabled again.
|
||||||
try:
|
#try:
|
||||||
weather_station.enable_heater()
|
# weather_station.enable_heater()
|
||||||
except:
|
#except:
|
||||||
print("No response from the weather station via the ModBus, even after several retries. Disabling the weather station.")
|
# 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!
|
# sys.exit(1) # Make program work without weather station!
|
||||||
else:
|
#else:
|
||||||
print("Enabled the heater function on the weather station.")
|
# print("Enabled the heater function on the weather station.")
|
||||||
|
|
||||||
###
|
###
|
||||||
# Schedule all periodic transmissions (use a little randomness)
|
# Schedule all periodic transmissions (use a little randomness)
|
||||||
@@ -428,12 +454,16 @@ def run():
|
|||||||
print("Scheduled telemetry transmission.")
|
print("Scheduled telemetry transmission.")
|
||||||
schedule.every(10).minutes.do(send_telemetry)
|
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.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)
|
||||||
|
|
||||||
# Connect to incoming APRS-IS feed
|
# Connect to incoming APRS-IS feed
|
||||||
# by default `raw` is False, then each line is ran through aprslib.parse()
|
# by default `raw` is False, then each line is ran through aprslib.parse()
|
||||||
|
|
||||||
# Set filter on incomming feed
|
# Set filter on incomming feed
|
||||||
APRSIS.set_filter(Configuration.config_file_settings['aprsis']['filter'])
|
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
|
||||||
@@ -445,22 +475,34 @@ def run():
|
|||||||
while (1):
|
while (1):
|
||||||
|
|
||||||
#print ("Reading registers of weather station.")
|
#print ("Reading registers of weather station.")
|
||||||
if weather_station.get_weather_data() == 0:
|
#if weather_station.get_weather_data() == 0:
|
||||||
print ('No response from ModBus, even after 5 retries. Keep trying.')
|
# print ('No response from ModBus, even after 5 retries. Keep trying.')
|
||||||
else:
|
#else:
|
||||||
WxData = weather_station.wx_data
|
# WxData = weather_station.wx_data
|
||||||
|
|
||||||
|
# Scheduler
|
||||||
schedule.run_pending()
|
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)
|
receive = receive_ax25(rx_socket)
|
||||||
for port in range(len(axdevice)):
|
for port in range(len(axdevice)):
|
||||||
if receive[0][1] == axdevice[port]:
|
if receive[0][1] == axdevice[port]:
|
||||||
#print(receive)
|
#print(receive)
|
||||||
source, destination, digipeaters, payload = parsePacket(receive[1])
|
source, destination, digipeaters, payload = parsePacket(receive[1])
|
||||||
#print(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:
|
try:
|
||||||
payload = payload.decode()
|
payload = payload.decode('latin-1')
|
||||||
except:
|
except:
|
||||||
payload = 'NOT VALID'
|
payload = 'NOT VALID'
|
||||||
|
|
||||||
|
@@ -25,7 +25,7 @@ global:
|
|||||||
log-rf: /home/marcel/test/pe1rxf_aprs-rf.log # Log RF traffic to file (0=no logging)
|
log-rf: /home/marcel/test/pe1rxf_aprs-rf.log # Log RF traffic to file (0=no logging)
|
||||||
|
|
||||||
modbus:
|
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.2:1.0-port0 # USB port to which RS-485 dongle is connected
|
||||||
address: 14 # ModBus address of weather station
|
address: 14 # ModBus address of weather station
|
||||||
|
|
||||||
# APRS-IS section
|
# APRS-IS section
|
||||||
@@ -61,28 +61,28 @@ weather:
|
|||||||
beacon:
|
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: 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)
|
||||||
call: PE1RXF-1 # Call from which transmissions are made (can be a different call from the call assigned to the AX.25 port)
|
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)
|
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)
|
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
|
message: APRS RX iGATE 144.800MHz # The beacon text
|
||||||
interval: 30 # Beacon interval in minutes
|
interval: 30 # Beacon interval in minutes
|
||||||
- port: ax1
|
- port: ax1
|
||||||
call: PE1RXF-3
|
call: PE1RXF-3
|
||||||
destination: APRX29
|
destination: APZMDM
|
||||||
digi_path: WIDE2-1
|
digi_path: WIDE2-1
|
||||||
position: "!5302.78NL00707.91E&"
|
position: "!5302.78NL00707.91E&"
|
||||||
message: LoRa APRS RX iGATE 433.775MHz
|
message: LoRa APRS RX iGATE 433.775MHz
|
||||||
interval: 30
|
interval: 30
|
||||||
- port: aprsis
|
- port: aprsis
|
||||||
call: PE1RXF-1
|
call: PE1RXF-1
|
||||||
destination: APRX29
|
destination: APZMDM
|
||||||
digi_path: 0
|
digi_path: 0
|
||||||
position: "!5302.78NR00707.91E&"
|
position: "!5302.78NR00707.91E&"
|
||||||
message: APRS RX iGATE 144.800MHz
|
message: APRS RX iGATE 144.800MHz
|
||||||
interval: 10
|
interval: 10
|
||||||
- port: aprsis
|
- port: aprsis
|
||||||
call: PE1RXF-3
|
call: PE1RXF-3
|
||||||
destination: APRX29
|
destination: APZMDM
|
||||||
digi_path: 0
|
digi_path: 0
|
||||||
position: "!5302.78NL00707.91E&"
|
position: "!5302.78NL00707.91E&"
|
||||||
message: LoRa APRS RX iGATE 433.775MHz
|
message: LoRa APRS RX iGATE 433.775MHz
|
||||||
|
@@ -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 &
|
|
||||||
|
|
Reference in New Issue
Block a user