AX.25 support added

master v1.1.0
marcel 5 months ago
parent 8c7895ea92
commit 447603cf62
  1. 4
      CHANGELOG.md
  2. 40
      README.md
  3. 234
      aprs_telemetry_to_mqtt.py
  4. 16
      aprs_telemetry_to_mqtt.yml
  5. BIN
      impression.odg
  6. BIN
      impression.png
  7. 2053
      impression.svg
  8. BIN
      impression2.png
  9. 115
      python-ax25/README.md
  10. 65
      python-ax25/examples/readAPRS.py
  11. 49
      python-ax25/examples/sendAPRS.py
  12. 17
      python-ax25/install.sh
  13. 352
      python-ax25/pythonax25module.c
  14. 10
      python-ax25/setup.py

@ -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

@ -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

@ -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 <https://www.gnu.org/licenses/>.
along with aprs_telemetry_to_mqtt. If not, see <https://www.gnu.org/licenses/>.
"""
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__':

@ -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

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

@ -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