diff --git a/CHANGELOG.md b/CHANGELOG.md
index c37350f..057d8f3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,3 +11,7 @@ All notable changes to this project will be documented in this file.
## [1.0.0] - 2023-12-08
First working version.
+
+## [1.1.0] - 2023-12-18
+Added: AX.25 support. Telemetry is now received directly from the AX.25 stack.
+Removed: Telemetry from CSV file is removed in favour of the AX.25 stack
diff --git a/README.md b/README.md
index ffdfed6..9a8bca9 100644
--- a/README.md
+++ b/README.md
@@ -4,15 +4,19 @@ The APRS telemetry to MQTT bridge can relay PE1RXF telemetry to an MQTT broker.
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
+An APRS node (can be anything that can send standard APRS messages) sends telemetry data to an APRS digipeater (Linux computer with AX.25 stack and suitable transceiver). This Python program filters out any valid telemetry (as defined in a YAML file) and forwards it to the specified MQTT broker. From there the possibilities are endless.
+
+![Impression](./impression.png "impression")
+Overview of possible setup
-This first version reads the telemetry files as generated by APRS server software. This software can be found here: https://git.meezenest.nl/marcel/pe1rxf-aprs-server
+![Impression 2](./impression2.png "impression 2")
+Example of an APRS node sending data to Grafana
-It would be nice when future versions would use the ax.25 stack instead of the APRS server software (like the utility "aprs-mqtt-bridge" already does). This would make it more universal. But for now, it works just fine!
+## Configuration
The program is configured via a YAML file. The global section defines the MQTT broker to which to publish the data.
-The topic section specifies the telemetry files to read. The descriptions of the fields are defined here as well.
+The topic section specifies the telemetry nodes to listen to. The descriptions of the fields are defined here as well.
```
# Global settings apply to all other entries
@@ -24,9 +28,14 @@ global:
topics:
# MQTT topic: each telemtry node has its own name (sub root) and must be unique
- name: solar_generator
- telemetry_file: /home/marcel/ham/aprs_utils/aprs_log/latest_telemetry_PE1RXF-9.dat
- # Defines the names of the values in the telemetry_file. Also defines the number of entries in this file.
- # So make sure the number of descriptions match the number of values in the telemetry_file!
+ # 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
@@ -35,15 +44,22 @@ topics:
```
+## Usage
+
+The program can only be run as root, due to the contrains of pyax25. Add aprs_telemetry_to_mqtt.py to /etc/sudoers by editing it via visudo so it can start with sudo at boot time without the need to enter a password. Example of line to add:
+
+```
+
+user ALL = (root) NOPASSWD: /home/user/ham/aprs_utils/aprs_telemetry_to_mqtt/aprs_telemetry_to_mqtt.py
+
+```
+
## Requirements
- Python3
- pathlib
- yaml
- paho-mqtt
-
-Future versions probably will also need:
-
-- Python AX.25 Module for Python3 (https://github.com/ha5di/pyax25)
-- Linux AX.25 stack
+- Python AX.25 Module for Python3 (https://github.com/ha5di/pyax25 - also supplied with the source of this program)
+- Linux AX.25 stack enabled
diff --git a/aprs_telemetry_to_mqtt.py b/aprs_telemetry_to_mqtt.py
index 63a1c99..1042400 100755
--- a/aprs_telemetry_to_mqtt.py
+++ b/aprs_telemetry_to_mqtt.py
@@ -1,23 +1,24 @@
#!/usr/bin/python3
"""
- A bridge between APRS messaging and MQTT, designed to control my lora_aprs_node_pico
+ A bridge between PE1RXF APRS telemetry messaging and MQTT.
+ It uses pythonax25 (https://github.com/josefmtd/python-ax25)
- (C)2022-2023 M.T. Konstapel https://meezenest.nl/mees
+ (C)2023 M.T. Konstapel https://meezenest.nl/mees
- This file is part of aprs-mqtt-bridge.
+ This file is part of aprs_telemetry_to_mqtt.
- aprs-mqtt-bridge is free software: you can redistribute it and/or modify
+ aprs_telemetry_to_mqtt 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,
+ aprs_telemetry_to_mqtt 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 .
+ along with aprs_telemetry_to_mqtt. If not, see .
"""
import sys
import random
@@ -29,6 +30,22 @@ from yaml.loader import SafeLoader
from paho.mqtt import client as mqtt_client
import csv
+import pythonax25
+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()
+
configuration_file = "aprs_telemetry_to_mqtt.yml"
# This is where we keep our settings
@@ -40,9 +57,6 @@ class mqtt_settings:
#transmit_rate
#retry
#topics
- #poll
- state = 'ready'
- aprs_state = 'idle'
pass
mqtt = mqtt_settings()
@@ -114,12 +128,7 @@ def read_config():
except:
print ("Error in configuration file: no topic 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)
@@ -156,9 +165,179 @@ def send_telemetry_to_broker(client):
publish(client,current_topic,row[index])
#print(current_topic + '=' + row[index])
+# AX.25 stuff
+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 process_message(source, ax_port, payload, mqtt_client):
+ #print(source)
+ #print(axdevice[ax_port])
+ #print(payload)
+ #print(axaddress[ax_port])
+
+ values=0
+
+ for topics in mqtt.topics:
+
+ # Check source call
+ if source == topics['call']:
+
+ # Check ax_port
+ if topics['ax_port'] == 'all' or topics['ax_port'] == axdevice[ax_port]:
+ #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(" ", "") == axaddress[ax_port]:
+ 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 is_float(field):
+ return 0
+
+ # Check if number of teleemtry 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 = mqtt.topic_root + '/' + topics['name'] + '/' + descr
+ publish(mqtt_client,current_topic,values[index])
+ #print('Publish ' + current_topic + '=' + values[index])
+
+
+ return values
+
+# End AX.25 stuff
+
+# Test if string is a number
+def is_float(v):
+ try:
+ f=float(v)
+ except:
+ return False
+ return True
+
def run():
read_config()
+
+ rx_socket = bind_ax25()
+
client = connect_mqtt()
@@ -169,16 +348,27 @@ def run():
client.loop_start()
while True:
-
- #topic = mqtt.topic_root + '/test'
- #publish(client,topic,mqtt.aprs_state)
-
- send_telemetry_to_broker(client)
-
- time.sleep(mqtt.poll_rate) # Sleep (time defined in yml file)
+ 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("")
+ telemetry=process_message(source, port, payload, client)
if __name__ == '__main__':
diff --git a/aprs_telemetry_to_mqtt.yml b/aprs_telemetry_to_mqtt.yml
index b56cb03..753cb73 100644
--- a/aprs_telemetry_to_mqtt.yml
+++ b/aprs_telemetry_to_mqtt.yml
@@ -1,3 +1,8 @@
+# Configuration file 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: pe1rxf.ampr.org # The MQTT broker we are going to use
@@ -7,9 +12,14 @@ global:
topics:
# MQTT topic: each telemtry node has its own name (sub root) and must be unique
- name: solar_generator
- telemetry_file: /home/marcel/ham/aprs_utils/aprs_log/latest_telemetry_PE1RXF-9.dat
- # Defines the names of the values in the telemetry_file. Also defines the number of entries in this file.
- # So make sure the number of descriptions match the number of values in the telemetry_file!
+ # 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
diff --git a/impression.odg b/impression.odg
new file mode 100644
index 0000000..c81cd89
Binary files /dev/null and b/impression.odg differ
diff --git a/impression.png b/impression.png
new file mode 100644
index 0000000..c520353
Binary files /dev/null and b/impression.png differ
diff --git a/impression.svg b/impression.svg
new file mode 100644
index 0000000..7ab07c5
--- /dev/null
+++ b/impression.svg
@@ -0,0 +1,2053 @@
+
+
+
\ No newline at end of file
diff --git a/impression2.png b/impression2.png
new file mode 100644
index 0000000..627fde5
Binary files /dev/null and b/impression2.png differ
diff --git a/python-ax25/README.md b/python-ax25/README.md
new file mode 100644
index 0000000..f39cfea
--- /dev/null
+++ b/python-ax25/README.md
@@ -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
diff --git a/python-ax25/examples/readAPRS.py b/python-ax25/examples/readAPRS.py
new file mode 100755
index 0000000..3239997
--- /dev/null
+++ b/python-ax25/examples/readAPRS.py
@@ -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()
diff --git a/python-ax25/examples/sendAPRS.py b/python-ax25/examples/sendAPRS.py
new file mode 100755
index 0000000..914c4b4
--- /dev/null
+++ b/python-ax25/examples/sendAPRS.py
@@ -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()
diff --git a/python-ax25/install.sh b/python-ax25/install.sh
new file mode 100755
index 0000000..2cb70fe
--- /dev/null
+++ b/python-ax25/install.sh
@@ -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"
diff --git a/python-ax25/pythonax25module.c b/python-ax25/pythonax25module.c
new file mode 100644
index 0000000..acc77ad
--- /dev/null
+++ b/python-ax25/pythonax25module.c
@@ -0,0 +1,352 @@
+#define PY_SSIZE_T_CLEAN
+#include
+
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+// #include
+
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+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;
+}
diff --git a/python-ax25/setup.py b/python-ax25/setup.py
new file mode 100755
index 0000000..ed0ffe3
--- /dev/null
+++ b/python-ax25/setup.py
@@ -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])