parent
964787c1bc
commit
73a922c437
11 changed files with 1325 additions and 1 deletions
@ -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…
Reference in new issue