aprs-mqtt-bridge added

master
marcel 1 year ago
parent 964787c1bc
commit 73a922c437
  1. 1
      aprs_utils/aprs-mqtt-bridge
  2. 19
      aprs_utils/aprs-mqtt-bridge/CHANGELOG.md
  3. 73
      aprs_utils/aprs-mqtt-bridge/README.md
  4. 495
      aprs_utils/aprs-mqtt-bridge/aprs-mqtt-bridge.py
  5. 130
      aprs_utils/aprs-mqtt-bridge/aprs-mqtt-bridge.yml
  6. 115
      aprs_utils/aprs-mqtt-bridge/python-ax25/README.md
  7. 65
      aprs_utils/aprs-mqtt-bridge/python-ax25/examples/readAPRS.py
  8. 49
      aprs_utils/aprs-mqtt-bridge/python-ax25/examples/sendAPRS.py
  9. 17
      aprs_utils/aprs-mqtt-bridge/python-ax25/install.sh
  10. 352
      aprs_utils/aprs-mqtt-bridge/python-ax25/pythonax25module.c
  11. 10
      aprs_utils/aprs-mqtt-bridge/python-ax25/setup.py

@ -1 +0,0 @@
Subproject commit 1ae761d989bad2c2f0aba6b1ee625ba54e7c6610

@ -0,0 +1,19 @@
# Changelog
All notable changes to this project will be documented in this file.
Added : for new features.
Changed : for changes in existing functionality.
Deprecated: for soon-to-be removed features.
Removed : for now removed features.
Fixed : for any bug fixes.
Security : in case of vulnerabilities.
## [1.0.0] - 2023-01-13
First working version.
## [1.0.1] - 2023-01-14
Changed: aprs_status (published on the MQTT broker) now returns actual state of transmision (sending, retrying, send or failed) instead if just 'ready' and 'busy'.
## [1.1.0] - 2023-07-08
Added: APRS nodes can now be polled to get there actual status. An extra section in the YAML configuration file is added for this functionality.

@ -0,0 +1,73 @@
# APRS to MQTT bridge
The APRS to MQTT bridge can relay commands from an MQTT broker to the APRS nodes via the Linux AX.25 stack. For now, only commands which response with a defined acknowledge (commands 10 and higher) are supported.
This program is a utility for the APRS telemetry system used by PE1RXF. The telemetry is embedded in an APRS message which can travel over the existing APRS network. For more information about this open protocol visit this link: https://www.meezenest.nl/mees-elektronica/projects/aprs_telemetry/APRS_protocol_nodes_PE1RXF.pdf
## Configuration
The program is configured via a YAML file. The global section defines the MQTT broker and some APRS transmit settings. The topics define the MQTT topics on which a client can publish a request. The full path of the topic is 'topic_root/topic_name'. Call, server and port define the AX.25 settings where 'call' is the call of the APRS node the message is send to, 'server' is the call of the APRS server which sends the message (typically this is the call assigned to the ax25 port) and 'port' is the Linux AX.25 port on which the radio is connected.
Lets say we have an APRS node which can switch several power rails. (https://www.meezenest.nl/mees-elektronica/RPi-pico-LoRa-APRS.html). We connected a 5GHz HamNet dish to output 2 of this APRS node. The node is assigned the call 'PE1RXF-6'. Assume this node can be reached via the radio connected to AX.25 port ax2 on our server. The node switches the output to high when it receives command '33{33'. We want the bridge to send this command when it receives payload 'ON' via MQTT on topic 'hamnet_aprs_nodes/ubiquity_dish_ptmp_workshop'. With the below example configuration, we can achieve just that. And by sending MQTT payload 'OFF', the APRS node switches the ouptut off again.
Multiple topics for the same or another APRS node can be defined as shown in the example.
```
# Global settings apply to all other entries
global:
broker: pe1rxf.ampr.org # 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
transmit_rate: 20 # Number of seconds between each transmision
retry: 3 # Try this often before giving up
destination: APRX29 # Destination or program ID
digi_path: WIDE2-1 # Digi path of APRS messages
poll_rate: 300 # Number of second between polling
#beacon_program: /usr/sbin/beacon # The external AX.25 beacon program => obsolete
# Poll the status af these clients (IMPORTANT: the calls must also be defined in the topics section)
poll:
- call: PE1RXF-5
- call: PE1RXF-6
- call: PE1RXF-8
topics:
# MQTT topic: 5GHz dish at workshop (must be unique name)
- name: ubiquity_dish_ptmp_workshop
call: PE1RXF-6 # Call of node to which commands below are send
server: PE1RXF-3 # Call of APRS server sending the commands
port: ax2 # Name of AX.25 port to use
command:
- payload: 'ON' # This is the payload we have to receive
cmd: 33{33 # This command is send to the node
response: ack33 # This response is expected from the node
- payload: 'OFF'
cmd: 32{32
response: ack32
# Server at tiny house
- name: server_tiny_house
call: PE1RXF-7
server: PE1RXF-3
port: ax2
command:
- payload: 'ON'
cmd: 31{31
response: ack31
- payload: 'OFF'
cmd: 30{30
response: ack30
```
The configuration entry 'global:retry' sets the amount of APRS message retries we attempt before giving up. The configuration entry 'global:transmit_rate' sets the time between retries.
When a node is added to the poll: call: entry its status will be polled on a regular base. The poll rate is set in the global entry. This is the poll interval between every transmision. When three nodes are defined, a node is polled every 3*poll_rate seconds.
## Requirements
- Python3
- Python AX.25 Module for Python3 (https://github.com/ha5di/pyax25)
- pathlib
- yaml
- paho-mqtt
- Linux AX.25 stack

@ -0,0 +1,495 @@
#!/usr/bin/python3
"""
A bridge between APRS messaging and MQTT, designed to control my lora_aprs_node_pico
(C)2022-2023 M.T. Konstapel https://meezenest.nl/mees
This file is part of aprs-mqtt-bridge.
aprs-mqtt-bridge 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.
aprs-mqtt-bridge 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 aprs-mqtt-bridge. If not, see <https://www.gnu.org/licenses/>.
"""
import sys
import random
import time
import os
from pathlib import Path
import yaml
from yaml.loader import SafeLoader
from paho.mqtt import client as mqtt_client
import pythonax25
configuration_file = "aprs-mqtt-bridge.yml"
# This is where we keep our settings
class mqtt_settings:
#broker
#topic_root
#port
#client_id
#transmit_rate
#retry
#topics
#poll
state = 'ready'
aprs_state = 'idle'
pass
mqtt = mqtt_settings()
axport = []
axdevice = []
axaddress = []
class aprs_status:
nr_of_ports = 0
busy = 0
wait_for_ack = 0
call_of_wait_for_ack = 0
time_out_timer = 0
retry_counter = 0
selected_port = 0
request_to_send = 0
pass
aprs = aprs_status()
def parsePacket(string):
# Split the address and payload separated by APRS PID
buffer = string.split(b'\x03\xf0')
address = buffer[0]
# 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:]
payload = buffer[1]
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):
addressSize = 7
# Check if the networked address string is valid
if (len(packetAddress) % 7) == 0:
# 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
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 bind_ax25():
# Check if there's any active AX25 port
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:
exit(0)
# 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 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:
res = pythonax25.datagram_tx(tx_socket, dest, msg)
else:
res = pythonax25.datagram_tx_digi(tx_socket, dest, digi, msg)
#print(res)
pythonax25.close_socket(tx_socket)
def connect_mqtt():
def on_connect(client, userdata, flags, rc):
if rc == 0:
print("Connected to MQTT Broker!")
else:
print("Failed to connect, return code %d\n", rc)
# Set Connecting Client ID
client = mqtt_client.Client(mqtt.client_id)
#client.username_pw_set(username, password)
client.on_connect = on_connect
client.connect(mqtt.broker, mqtt.port)
return client
def publish(client, topic, message):
result = client.publish(topic, message)
status = result[0]
if status == 0:
print(f"Send `{message}` to topic `{topic}`")
else:
print(f"Failed to send message to topic {topic}")
def subscribe(client: mqtt_client, topic):
def on_message(client, userdata, message):
received_payload = message.payload.decode()
received_topic = Path(message.topic).name
print(f"Received `{received_payload}` from `{message.topic}` topic")
# Find corresponding topic in configuration-file and send this to the next function
for topics in mqtt.topics:
if received_topic == topics['name']:
#print ('Found topic in list!')
#print (topics)
process_message(topics, received_payload)
break
# print(topic['name'])
# print(topic['command'])
client.subscribe(topic)
client.on_message = on_message
def read_config():
try:
with open(configuration_file) as f:
cfg = yaml.load(f, Loader=SafeLoader)
mqtt.topics = cfg['topics']
mqtt.poll = cfg['poll']
#print(mqtt.topics)
#for topic in mqtt.topics:
# print(topic['name'])
# print(topic['command'])
except:
print ("Configuration file ./" + configuration_file + " not found.")
sys.exit(1)
try:
mqtt.broker = cfg['global']['broker']
except:
print ("Error in configuration file: no broker defined.")
sys.exit(1)
try:
mqtt.port = cfg['global']['port']
except:
print ("Error in configuration file: no port defined.")
sys.exit(1)
try:
mqtt.topic_root = cfg['global']['topic_root']
except:
print ("Error in configuration file: no topic defined.")
sys.exit(1)
try:
mqtt.transmit_rate = cfg['global']['transmit_rate']
except:
print ("Error in configuration file: no transmit_rate defined.")
sys.exit(1)
try:
mqtt.retry = cfg['global']['retry']
except:
print ("Error in configuration file: no retry defined.")
sys.exit(1)
try:
mqtt.destination = cfg['global']['destination']
except:
print ("Error in configuration file: no retry defined.")
sys.exit(1)
try:
mqtt.poll_rate = cfg['global']['poll_rate']
except:
print ("Error in configuration file: no poll_rate defined.")
sys.exit(1)
mqtt.client_id = f'{mqtt.topic_root}-{random.randint(0, 1000)}'
print (mqtt.broker)
print (mqtt.topic_root)
print (mqtt.port)
print (mqtt.client_id)
# Loop through all topics and activate them
def add_subscribtions_from_configfile(client):
for topics in mqtt.topics:
current_topic = mqtt.topic_root + '/' + topics['name']
subscribe(client,current_topic)
print('Topic ' + topics['name'] + ' added')
# Loop through poll list and activate then
def poll_clients(count=[0]):
# How many clients do we have to poll?
nr_of_clients = len(mqtt.poll)
current_call = mqtt.poll[count[0]]['call']
print ('Polling ' + current_call)
# Now we have to figure out the other paramters of the client. These are defined in the topics section of the configuration file
for topics in mqtt.topics:
if current_call == topics['call']:
current_port = topics['port']
source_call = topics['server']
#print ('Server: ' + source_call)
#print ('AX.25 port: ' + current_port)
break
# Find call of ax25 port
for position in range(len(axdevice)):
if axdevice[position] == current_port:
port_call = axaddress[position]
#print (port_call)
message = ':' + topics['call'].ljust(9) + ':' + '06'
#print (port_call + ' ' + source_call + ' ' + mqtt.destination + ' ' + message)
send_ax25(port_call, source_call, mqtt.destination, 0, message)
# Every time this functtion is called it moves up one call and wraps around
count[0] += 1
if count[0] >= nr_of_clients:
count[0] = 0
# Decode response of clients to sensible MQTT messages
def process_polling(call, data, mqtt_client):
print ('Received status: ' + call + " " + data)
if len(data) != 5:
return
for cnt in range(len(data)):
if data[-1*cnt] != '0' and data[-1*cnt] != '1':
print('Invalid response')
return
# Now we have to figure out the other paramters of the client. These are defined in the topics section of the configuration file
for topics in mqtt.topics:
if call == topics['call']:
current_name = topics['name']
current_poll_bit = topics['poll_bit']
print ('MQTT topic: ' + current_name)
#print (current_poll_bit)
inverted_poll_bit = -1*current_poll_bit
status_bit = data[inverted_poll_bit]
if status_bit == '1':
print ('ON')
state = 'ON'
else:
print('OFF')
state = 'OFF'
topic = mqtt.topic_root + '/' + current_name + '/state'
publish(mqtt_client,topic,state)
def process_message(data, payload):
#print ('Payload: '+ payload)
#print (data['call'])
#print (data['port'])
if aprs.busy == 0:
# find payload in configuration file
for commands in data['command']:
if payload == commands['payload']:
aprs.time_out_timer = time.time() # Start timeout timer
aprs.busy = 1
aprs.selected_port = data['port']
# Find call of ax25 port
for position in range(len(axdevice)):
if axdevice[position] == aprs.selected_port:
aprs.port_call = axaddress[position]
aprs.source_call = data['server']
aprs.wait_for_ack = commands['response']
aprs.call_of_wait_for_ack = data['call']
aprs.message = ':' + data['call'].ljust(9) + ':' + commands['cmd']
arguments = '-d \"APRX29\" -s ' + data['port'] + ' \"' + aprs.message + '\"'
beacon_program = "/usr/sbin/beacon"
aprs.beacon_program_with_arguments = beacon_program + " " + arguments
#os.system(aprs.beacon_program_with_arguments)
print ('APRS message ' + aprs.message + ' send to ' + aprs.call_of_wait_for_ack + '.')
mqtt.state = 'busy'
mqtt.aprs_state = 'sending message'
aprs.request_to_send = 1;
else:
mqtt.state = 'busy'
def run():
read_config()
rx_socket = bind_ax25()
client = connect_mqtt()
add_subscribtions_from_configfile(client)
#topic = mqtt.topic_root + '/set'
#subscribe(client,topic)
client.loop_start()
# Send ready to MQTT broker to indicate we are meaning business
mqtt.aprs_state = 'ready'
#topic = mqtt.topic_root + '/aprs_status'
#publish(client,topic,'ready')
mqtt.state = 'busy'
aprs.time_out_timer = time.time()
aprs.poll_timer = time.time()
while True:
if aprs.request_to_send == 1:
send_ax25(aprs.port_call, aprs.source_call, mqtt.destination, 0, aprs.message)
aprs.time_out_timer = time.time() # Restart timeout timer
#print(aprs.selected_port)
#print(aprs.message)
aprs.request_to_send = 0
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])
# bug UnicodeDecodeError: 'utf-8' codec can't decode byte 0xf8 in position 47: invalid start byte
try:
payload = payload.decode()
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("")
# Test if received packet is from a polled station
for poll in mqtt.poll:
if poll['call'] == source:
#print ('Received packet from polled station: ' + payload)
# 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:
if split_message[1].replace(" ", "") == axaddress[port]:
if len(split_message[2]) == 5:
#print ('Received status: ' + source + " " + split_message[2])
process_polling(source, split_message[2], client)
# Waiting for acknowledge...
if aprs.wait_for_ack != 0:
if source == aprs.call_of_wait_for_ack:
# split payload at colon. If it is a valid acknowledge, we should get three
# substrings: the first in empty, the second with the call of the ax25
# interface and the thirth with the acknowledge
split_message=payload.split(":")
if len(split_message) == 3:
if split_message[1].replace(" ", "") == axaddress[port]:
if split_message[2] == aprs.wait_for_ack:
print ('Received acknowledge ' + aprs.wait_for_ack + ' from ' + aprs.call_of_wait_for_ack + ".")
aprs.time_out_timer = time.time() # Restart timeout timer
aprs.wait_for_ack = 0
aprs.busy = 0
aprs.retry_counter = 0
mqtt.aprs_state = 'message sent'
#topic = mqtt.topic_root + '/aprs_status'
#publish(client,topic,mqtt.aprs_state)
mqtt.state = 'busy'
# Time out waiting for acknowledge
if aprs.wait_for_ack != 0:
if time.time() - aprs.time_out_timer > mqtt.transmit_rate:
aprs.retry_counter = aprs.retry_counter + 1
if aprs.retry_counter < mqtt.retry:
# Try again
aprs.time_out_timer = time.time() # Restart timeout timer
aprs.request_to_send = 1;
#os.system(aprs.beacon_program_with_arguments)
print ('Retry: APRS ' + aprs.message + ' message send to ' + aprs.call_of_wait_for_ack + '.')
mqtt.aprs_state = 'sending message (retry ' + str(aprs.retry_counter) + ')'
mqtt.state = 'busy'
else:
# Give up
print ('No acknowledge received from ' + aprs.call_of_wait_for_ack + '. Giving up.')
aprs.time_out_timer = time.time() # Restart timeout timer
aprs.wait_for_ack = 0
aprs.busy = 0
aprs.retry_counter = 0
mqtt.aprs_state = 'sending message failed'
#topic = mqtt.topic_root + '/aprs_status'
#publish(client,topic,mqtt.aprs_state)
mqtt.state = 'busy'
# If APRS system is transmitting, retrying and still waiting for acknowledge, keep on waiting and send an MQTT update
if mqtt.state == 'busy':
topic = mqtt.topic_root + '/aprs_status'
publish(client,topic,mqtt.aprs_state)
mqtt.state = 'ready'
if time.time() - aprs.poll_timer > mqtt.poll_rate:
poll_clients()
# Reset poll timer
aprs.poll_timer = time.time()
#print('Poll clients')
if __name__ == '__main__':
#sys.stdout = sys.stderr = open('debug.log', 'w')
run()

@ -0,0 +1,130 @@
# Global settings apply to all other entries
global:
broker: pe1rxf.ampr.org # 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
transmit_rate: 20 # Number of seconds between each transmision
retry: 3 # Try this often before giving up
destination: APRX29 # Destination or program ID
digi_path: WIDE2-1 # Digi path of APRS messages
poll_rate: 300 # Number of second between polling
#beacon_program: /usr/sbin/beacon # The external AX.25 beacon program => obsolete
# Poll the status af these clients (IMPORTANT: the calls must also be defined in the topics section)
poll:
- call: PE1RXF-5
- call: PE1RXF-6
- call: PE1RXF-8
topics:
# MQTT topic: 5GHz dish at workshop (must be unique name)
- name: ubiquity_dish_ptmp_workshop
call: PE1RXF-6 # Call of node to which commands below are send
server: PE1RXF-3 # Call of APRS server sending the commands
port: ax2 # Name of AX.25 port to use
poll_bit: 2 # Bit in status response of client (1=b00001, 2=b00010, 3=b00100, 4=b01000, 5=b10000)
command:
- payload: 'ON' # This is the payload we have to receive
cmd: 33{33 # This command is send to the node
response: ack33 # This response is expected from the node
- payload: 'OFF'
cmd: 32{32
response: ack32
# Server at workshop
- name: server_workshop
call: PE1RXF-6
server: PE1RXF-3
port: ax2
poll_bit: 1
command:
- payload: 'ON'
cmd: 31{31
response: ack31
- payload: 'OFF'
cmd: 30{30
response: ack30
# Server at tiny house
- name: server_tiny_house
call: PE1RXF-5
server: PE1RXF-3
port: ax2
poll_bit: 3
command:
- payload: 'ON'
cmd: 35{35
response: ack35
- payload: 'OFF'
cmd: 34{34
response: ack34
# MQTT topic: 5GHz dish at tiny house
- name: ubiquity_dish_ptp_tiny_house
call: PE1RXF-5
server: PE1RXF-3
port: ax2
poll_bit: 1
command:
- payload: 'ON'
cmd: 31{31
response: ack31
- payload: 'OFF'
cmd: 30{30
response: ack30
# MQTT topic: 5GHz dish in orchard
- name: ubiquity_dish_ptmp_orchard
call: PE1RXF-8
server: PE1RXF-3
port: ax2
poll_bit: 1
command:
- payload: 'ON'
cmd: 31{31
response: ack31
- payload: 'OFF'
cmd: 30{30
response: ack30
# MQTT topic: 5GHz dish in vegetable garden
- name: ubiquity_dish_ptp_vegetable_garden
call: PE1RXF-8
server: PE1RXF-3
port: ax2
poll_bit: 2
command:
- payload: 'ON'
cmd: 33{33
response: ack33
- payload: 'OFF'
cmd: 32{32
response: ack32
# MQTT topic: QRP-lab QDX transceiver at tiny house
- name: qdx_transceiver_tiny_house
call: PE1RXF-5
server: PE1RXF-3
port: ax2
poll_bit: 2
command:
- payload: 'ON'
cmd: 33{33
response: ack33
- payload: 'OFF'
cmd: 32{32
response: ack32
# MQTT topic: Switched 12V output at tiny house
- name: switched_12v_tiny_house
call: PE1RXF-5
server: PE1RXF-3
port: ax2
poll_bit: 4
command:
- payload: 'ON'
cmd: 37{37
response: ack37
- payload: 'OFF'
cmd: 36{36
response: ack36

@ -0,0 +1,115 @@
# python-ax25
Python AX.25 Module for Python3
## Introduction
This is a python module designed for Python3 to access AX.25 features. This module is a C extension that can access the AX.25 interface from the Linux kernel.
This C extension is inspired from pyax25 https://github.com/ha5di/pyax25
## Installing the Module
Clone the Git repository
```
$ git clone https://github.com/josefmtd/python-ax25
```
Install the module by running the install script inside the python-ax25 directory
```
$ cd python-ax25
# ./install.sh
```
## Module Functions
Before using any of the functions, make sure to load all the available ports using `config_load_ports()`
```
pythonax25.config_load_ports()
Returns = number of available ports (int)
```
To get the names of available ports, use the `config_get_first_port` and `config_get_next_port`
```
pythonax25.config_get_first_port()
Returns = name of first port (unicode string)
pythonax25.config_get_next_port(portname)
Returns = name of port after 'portname' (unicode string)
```
To retrieve further information for each available port, use these functions:
1. `config_get_port_name(device)`
2. `config_get_address(portname)`
3. `config_get_device(portname)`
4. `config_get_window(portname)`
5. `config_get_packet_length(portname)`
6. `config_get_baudrate(portname)`
7. `config_get_description(portname)`
To change the callsign from ASCII to network format and vice versa, use the functions `ascii_to_network(callsignascii)` and `network_to_ascii(callsignnetwork)`
```
pythonax25.ascii_to_network(callsignascii)
Returns = callsign in network format (byte literal string)
pythonax25.network_to_ascii(callsignnetwork)
Returns = callsign in ascii format (unicode string)
```
For receiving AX.25 packets, the packet socket is mostly used in C programs. Start a socket by using `packet_socket()` and begin receiving by using `packet_rx(fd, timeout)`
```
pythonax25.packet_socket()
Returns = file descriptor (int)
pythonax25.packet_rx(fd, timeout)
Returns = Protocol and Address (tuple of int and string) and packet (byte-literal string)
```
For sending APRS messages, the datagram socket is used. Socket is started by using `datagram_socket()`, bound to a port by using `datagram_bind(fd, srccall, portcall)` and send packets via `datagram_tx(fd, destcall, message)` or `datagram_tx(fd, destcall, digicall, message)`
```
pythonax25.datagram_socket()
Returns = file descriptor (int)
pythonax25.datagram_bind(fd, srccall, destcall)
Returns = result of bind (int)
pythonax25.datagram_tx(fd, destcall, message)
Returns = result of transmission (int)
pythonax25.datagram_tx_digi(fd, destcall, digicall, message)
Returns = result of transmission (int)
```
Closing socket is done by using `close_socket(fd)`
```
pythonax25.close_socket(fd)
Returns = result of close (int)
```
2020 - Josef Matondang

@ -0,0 +1,65 @@
#!/usr/bin/python3
import pythonax25
def parsePacket(string):
# Split the address and payload separated by APRS PID
buffer = string.split(b'\x03\xf0')
address = buffer[0]
# Check if the first byte indicates it is a data packet
if address[0] is 0:
# Cut the first byte and feed it to the address parser
listAddress = getAllAddress(address[1:])
# Get the source, destination, and digipeaters from the address list
source = listAddress[1]
destination = listAddress[0]
digipeaters = listAddress[2:]
else:
raise Exception('Not a data packet')
payload = buffer[1]
return (source, destination, digipeaters, payload)
def getAllAddress(packetAddress):
addressSize = 7
# Check if the networked address string is valid
if (len(packetAddress) % 7) is 0:
# Create a list of all address in ASCII form
allAddress = [pythonax25.network_to_ascii(packetAddress[i:i+addressSize])
for i in range(0, len(packetAddress), addressSize)]
return allAddress
else:
raise Exception('Error: Address is not a multiple of 7')
def main():
# Check if there's any active AX25 port
if pythonax25.config_load_ports() > 0:
# Get the device name of the first port
axport = pythonax25.config_get_first_port()
axdevice = pythonax25.config_get_device(axport)
axaddress = pythonax25.config_get_address(axport)
else:
exit(0)
# Initiate a PF_PACKET socket
socket = pythonax25.packet_socket()
while True:
# Blocking receive packet, 10 ms timeout
receive = pythonax25.packet_rx(socket,10)
if receive[0][1] == axdevice:
print(receive)
source, destination, digipeaters, payload = parsePacket(receive[1])
print("Packet Received by = %s"%axaddress)
print("Source Address = %s"%source)
print("Destination Address = %s"%destination)
print("Digipeaters =")
print(digipeaters)
print("Payload = %s"%payload)
print("")
else:
continue
main()

@ -0,0 +1,49 @@
#!/usr/bin/python3
import pythonax25
import time
def main():
# Check if there's any active AX25 port
if pythonax25.config_load_ports() > 0:
# Get the device name of the first port
axport = pythonax25.config_get_first_port()
axdevice = pythonax25.config_get_device(axport)
axaddress = pythonax25.config_get_address(axport)
else:
exit(0)
# Initiate a datagram socket
socket = pythonax25.datagram_socket()
srcCall = 'YD0ABH-13'
portCall = axaddress
res = pythonax25.datagram_bind(socket, srcCall, portCall)
print(res)
dest = 'APZINA'
digi = 'WIDE2-2'
msg = '!0611.08S/10649.35E$ INARad LoRa APRS#CO2=500'
res = pythonax25.datagram_tx_digi(socket, dest, digi, msg)
print(res)
time.sleep(1)
msg = 'T#001,034,034,034,034,000,11111111'
res = pythonax25.datagram_tx_digi(socket, dest, digi, msg)
print(res)
time.sleep(1)
msg = '_07190749c045s055g055t076r001h45b10101'
res = pythonax25.datagram_tx_digi(socket, dest, digi, msg)
print(res)
pythonax25.close_socket(socket)
return res
if __name__ == '__main__':
main()

@ -0,0 +1,17 @@
#!/bin/bash
DIR=`dirname $0`
# Update the system
#/usr/bin/apt update
#/usr/bin/apt -y upgrade
# Install the dependencies
#/usr/bin/apt -y install libax25 libax25-dev ax25-apps ax25-tools python3-dev
# Install the Python module
${DIR}/setup.py build
${DIR}/setup.py install
# Remove the build
/bin/rm -rf "${DIR}/build"

@ -0,0 +1,352 @@
#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/ioctl.h>
#include <netdb.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <syslog.h>
#include <signal.h>
#include <string.h>
#include <time.h>
#include <poll.h>
// #include <curses.h>
#include <sys/socket.h>
#include <net/if.h>
#include <net/ethernet.h>
#include <netax25/ax25.h>
#include <netax25/axconfig.h>
#include <netax25/axlib.h>
static PyObject * config_load_ports(PyObject* self, PyObject* args) {
int activePort;
activePort = ax25_config_load_ports();
return PyLong_FromLong(activePort);
}
static PyObject * config_get_first_port(PyObject* self, PyObject* args) {
char *portName;
portName = ax25_config_get_next(NULL);
return Py_BuildValue("s", portName);
}
static PyObject * config_get_next_port(PyObject* self, PyObject* args) {
char *portName;
char *nextPort;
PyArg_ParseTuple(args, "s", &portName);
nextPort = ax25_config_get_next(portName);
return Py_BuildValue("s", nextPort);
}
static PyObject * config_get_port_name(PyObject* self, PyObject* args) {
char *device;
char *portName;
PyArg_ParseTuple(args, "s", &device);
portName = ax25_config_get_name(device);
return Py_BuildValue("s", portName);
}
static PyObject * config_get_address(PyObject* self, PyObject* args) {
char *portName;
char *address;
PyArg_ParseTuple(args, "s", &portName);
address = ax25_config_get_addr(portName);
return Py_BuildValue("s", address);
}
static PyObject * config_get_device(PyObject* self, PyObject* args) {
char *device;
char *portName;
PyArg_ParseTuple(args, "s", &portName);
device = ax25_config_get_dev(portName);
return Py_BuildValue("s", device);
}
static PyObject * config_get_window(PyObject* self, PyObject* args) {
int window;
char *portName;
PyArg_ParseTuple(args, "s", &portName);
window = ax25_config_get_window(portName);
return PyLong_FromLong(window);
}
static PyObject * config_get_packet_length(PyObject* self, PyObject* args) {
int packetLength;
char *portName;
PyArg_ParseTuple(args, "s", &portName);
packetLength = ax25_config_get_paclen(portName);
return PyLong_FromLong(packetLength);
}
static PyObject * config_get_baudrate(PyObject* self, PyObject* args) {
int baudRate;
char *portName;
PyArg_ParseTuple(args, "s", &portName);
baudRate = ax25_config_get_baud(portName);
return Py_BuildValue("i", baudRate);
}
static PyObject * config_get_description(PyObject* self, PyObject* args) {
char *description;
char *portName;
PyArg_ParseTuple(args, "s", &portName);
description = ax25_config_get_desc(portName);
return Py_BuildValue("s", description);
}
static PyObject * aton_entry(PyObject* self, PyObject* args) {
char *callsignNetwork = null_ax25_address.ax25_call;
char *callsignString;
int result;
PyArg_ParseTuple(args, "s", &callsignString);
result = ax25_aton_entry(callsignString, callsignNetwork);
return Py_BuildValue("iy", result, callsignNetwork);
}
static PyObject * ntoa(PyObject* self, PyObject* args) {
static PyObject * callsignPython;
char *callsignNetwork;
char *callsignString;
ax25_address *callsign = &null_ax25_address;
if (!PyArg_ParseTuple(args, "y", &callsignNetwork))
fprintf(stderr, "ERROR: CANNOT ASSIGN\n");
strncpy(callsign->ax25_call, callsignNetwork, 7);
callsignString = ax25_ntoa(callsign);
callsignPython = Py_BuildValue("s", callsignString);
return callsignPython;
}
static PyObject * datagram_socket(PyObject* self, PyObject* args) {
int fileDescriptor;
fileDescriptor = socket(AF_AX25, SOCK_DGRAM, 0);
return PyLong_FromLong(fileDescriptor);
}
static PyObject * datagram_bind(PyObject* self, PyObject* args) {
struct full_sockaddr_ax25 src;
char *portcall, *srccall;
int len, sock, result;
PyArg_ParseTuple(args, "iss", &sock, &srccall, &portcall);
char * addr = malloc(sizeof(char*) * (strlen(srccall) + strlen(portcall) + 2));
sprintf(addr, "%s %s", srccall, portcall);
len = ax25_aton(addr, &src);
free(addr);
// Binding the socket to source
if (bind(sock, (struct sockaddr *)&src, len) == -1) {
result = 1;
}
else {
result = 0;
}
return PyLong_FromLong(result);
}
static PyObject * datagram_tx_digi(PyObject* self, PyObject* args) {
struct full_sockaddr_ax25 dest;
char *destcall = NULL, *digicall = NULL;
char *message;
int dlen, sock, result;
PyArg_ParseTuple(args, "isss", &sock, &destcall, &digicall, &message);
char * addr = malloc(sizeof(char*) * (strlen(destcall) + strlen(digicall) + 2));
sprintf(addr, "%s %s", destcall, digicall);
dlen = ax25_aton(addr, &dest);
free(addr);
// Send a datagram packet to socket
if (sendto(sock, message, strlen(message), 0, (struct sockaddr *)&dest, dlen) == -1) {
result = 1;
}
result = 0;
return PyLong_FromLong(result);
}
static PyObject * datagram_tx(PyObject* self, PyObject* args) {
struct full_sockaddr_ax25 dest;
char *destcall = NULL;
char *message;
int dlen, sock, result;
PyArg_ParseTuple(args, "iss", &sock, &destcall, &message);
dlen = ax25_aton(destcall, &dest);
// Send a datagram packet to socket
if (sendto(sock, message, strlen(message), 0, (struct sockaddr *)&dest, dlen) == -1) {
result = 1;
}
result = 0;
return PyLong_FromLong(result);
}
// Using PF_PACKET Socket
static PyObject * packet_socket(PyObject* self, PyObject* args) {
int fileDescriptor;
fileDescriptor = socket(PF_PACKET, SOCK_PACKET, htons(ETH_P_AX25));
return PyLong_FromLong(fileDescriptor);
}
// Close a socket
static PyObject * close_socket(PyObject* self, PyObject* args) {
int fileDescriptor;
int result;
PyArg_ParseTuple(args, "i", &fileDescriptor);
result = close(fileDescriptor);
return PyLong_FromLong(result);
}
static PyObject * packet_tx(PyObject* self, PyObject* args) {
int fileDescriptor;
int result;
int length;
char *buffer;
char *destination;
struct sockaddr socketAddress;
int addressSize = sizeof(socketAddress);
unsigned char newBuffer[1000];
int bufferLength;
int i;
int k;
unsigned char charBuffer;
PyArg_ParseTuple(args, "isis", &fileDescriptor, &buffer, &length, &destination);
bufferLength = strlen(buffer);
i = 0;
k = 0;
while ( i < bufferLength ) {
charBuffer = (buffer[i++] & 0x0f) << 4;
charBuffer = charBuffer | (buffer[i++] & 0x0f);
newBuffer[k++] = charBuffer;
}
strcpy(socketAddress.sa_data, destination);
socketAddress.sa_family = AF_AX25;
result = sendto(fileDescriptor, newBuffer, k, 0, &socketAddress, addressSize);
return Py_BuildValue("i", result);
}
static PyObject * packet_rx(PyObject* self, PyObject* args) {
int fileDescriptor;
int result;
int addressSize;
int packetSize;
int timeout;
struct sockaddr socketAddress;
struct pollfd pollFileDescriptor;
unsigned char receiveBuffer[1024];
PyArg_ParseTuple(args, "ii", &fileDescriptor, &timeout);
// Poll the socket for an available data
pollFileDescriptor.fd = fileDescriptor;
pollFileDescriptor.events = POLLRDNORM;
result = poll(&pollFileDescriptor, 1, timeout);
// Read all packet received
packetSize = 0;
socketAddress.sa_family = AF_UNSPEC;
strcpy(socketAddress.sa_data, "");
if (result == 1) {
addressSize = sizeof(socketAddress);
packetSize = recvfrom(fileDescriptor, receiveBuffer, sizeof(receiveBuffer),
0, &socketAddress, (socklen_t*)&addressSize);
}
return Py_BuildValue("(is)y#", socketAddress.sa_family, socketAddress.sa_data,
receiveBuffer, packetSize);
}
static PyObject *PythonAx25Error;
//////////////////////////////////////////
// Define methods
//////////////////////////////////////////
static PyMethodDef python_ax25_functions[] = {
{"config_load_ports", config_load_ports, METH_VARARGS, ""},
{"config_get_first_port", config_get_first_port, METH_VARARGS, ""},
{"config_get_next_port", config_get_next_port, METH_VARARGS, ""},
{"config_get_port_name", config_get_port_name, METH_VARARGS, ""},
{"config_get_address", config_get_address, METH_VARARGS, ""},
{"config_get_device", config_get_device, METH_VARARGS, ""},
{"config_get_window", config_get_window, METH_VARARGS, ""},
{"config_get_packet_length", config_get_packet_length, METH_VARARGS, ""},
{"config_get_baudrate", config_get_baudrate, METH_VARARGS, ""},
{"config_get_description", config_get_description, METH_VARARGS, ""},
{"network_to_ascii", ntoa, METH_VARARGS, ""},
{"ascii_to_network", aton_entry, METH_VARARGS, ""},
{"datagram_socket", datagram_socket, METH_VARARGS, ""},
{"datagram_bind", datagram_bind, METH_VARARGS, ""},
{"datagram_tx_digi", datagram_tx_digi, METH_VARARGS, ""},
{"datagram_tx", datagram_tx, METH_VARARGS, ""},
{"packet_socket", packet_socket, METH_VARARGS, ""},
{"packet_rx", packet_rx, METH_VARARGS, ""},
{"packet_tx", packet_tx, METH_VARARGS, ""},
{"close_socket", close_socket, METH_VARARGS, ""},
{NULL, NULL, 0, NULL}
};
// Initialize module
static struct PyModuleDef moduledef = {
PyModuleDef_HEAD_INIT,
"pythonax25",
"This is a python module for ax.25",
-1,
python_ax25_functions,
NULL,
NULL,
NULL,
NULL,
};
PyMODINIT_FUNC
PyInit_pythonax25(void) {
PyObject * m;
m = PyModule_Create(&moduledef);
if (m == NULL)
return NULL;
PythonAx25Error = PyErr_NewException("pythonax25.error", NULL, NULL);
Py_INCREF(PythonAx25Error);
PyModule_AddObject(m, "error", PythonAx25Error);
return m;
}

@ -0,0 +1,10 @@
#!/usr/bin/python3
from distutils.core import setup, Extension
module1 = Extension('pythonax25', libraries = ['ax25', 'ax25io'], sources = ['pythonax25module.c'])
setup (name = 'pythonax25',
version = '1.0',
description = 'CPython extension for LINUX ax.25 stack',
ext_modules = [module1])
Loading…
Cancel
Save