Compare commits

...

10 Commits

16 changed files with 4654 additions and 35 deletions

46
CHANGELOG.md Normal file
View File

@@ -0,0 +1,46 @@
# 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.
## [0.0.1] - 2022-01-31
### Added
- Lora and TCP settings are now configurable via configuration file RPi-LoRa-KISS-TNC.ini
### Changed
- Better encoding of AX.25 frames: less/no crashes due to corrupted incomming frames
### Deprecated
- Configuration via config.py
### Fixed
- LoRa APRS header (<\xff\x01) was not added to the payload, due to an indentation fault. Long live Python!
## [0.0.2] - 2022-02-08
### Added
- Support for 20dBm output power. Temporarily hard coded in source, but in next version it should be configurable via the configuration file.
## [0.0.3] - 2022-02-10
### Changed
- 20dBm output power can now be selected from configuration file instead of being hard coded in program.
- Enabled CRC in header of LoRa frames. Now frames are crc-checked at the receiving side.
### Removed
- Config.py as configuration is now completely removed. Configuration is now done completely via RPi-LoRa-KISS-TNC.ini
## [0.0.4] - 2024-01-18
Should now be possible to use it as a real digipeater with aprx software
### Fixed
- Added '*' to LoRa APRS TX string when the 'has-been-repeated' flag of the repeater(s) in the AX.25 frame is set.
- Proper handling of 'has-been-repeated' asterix in LoRa RX frames, also when no SSID is received

View File

@@ -9,17 +9,72 @@ sudo apt install python3 python3-rpi.gpio python3-spidev aprx screen git python3
Clone this repository.
## Configuration
## Configuration of APRX
Edit /etc/aprx.conf according to example in aprx/aprx.conf.lora-aprs
```
In /etc/aprx.conf:
<interface>
tcp-device 127.0.0.1 10001 KISS
callsign NOCALL-4 # callsign defaults to $mycall
tx-ok true # transmitter enable defaults to false
# #telem-to-is true # set to 'false' to disable
</interface>
```
## Start the LoRa KISS TNC
## Start the LoRa KISS TNC and aprx server instance
```
python3 Start_lora-tnc.py &
sudo aprx
```
## Stop the server's
# Alternative method using the AX.25 stack
This method is more complicated, but also more versitile as not all programs can communicate to a KISS device over TCP.
## Install needed packages
```
sudo killall aprx python3
sudo apt install python3 python3-rpi.gpio python3-spidev aprx screen git python3-pil python3-smbus
```
## Checkout the code
Clone this repository.
## Configuration of APRX
```
In /etc/aprx.conf
<interface>
ax25-device $mycall
tx-ok true # transmitter enable defaults to false
# #telem-to-is true # set to 'false' to disable
</interface>
```
## Install and configure AX.25 stack
```
sudo apt install socat libax25 ax25-apps ax25-tools
sudo nano /etc/ax25/axports
add:
ax0 NOCALL-3 9600 255 2 430.775 MHz LoRa
```
## Start the LoRa KISS TNC
```
python3 Start_lora-tnc.py &
```
## Redirect KISS over TCP to AX.25 device
```
sudo socat PTY,raw,echo=0,link=/tmp/kisstnc TCP4:127.0.0.1:10001
sudo kissattach /tmp/kisstnc ax0
```

View File

@@ -29,6 +29,8 @@
# Changes by PE1RXF
#
# 2022-01-23: - in encode_address() added correct handling of has_been_repeated flag '*'
# 2022-01-28: - in encode_kiss() and encode_address(): better exeption handling for corrupted or mal-formatted APRS frames
# 2024-01-18: - in decode_kiss() add '*' to call when 'has-been-repeated' flag is set for reapeters
#
import struct
@@ -43,6 +45,15 @@ KISS_TFESC = 0xDD # If after an escape, means there was an 0xDB in the source m
# If it's the final address in the header, set the low bit to 1
# Ignoring command/response for simple example
def encode_address(s, final):
encoded_ssid = 0b00000000
# First we check if the call has the 'has_been_repeated' asterix at the ended.
# If so, set the 'has_been_repeated' flag and remove the asterix
if s[-1] == 42:
s = s[:-1]
encoded_ssid |= 0b10000000
if b"-" not in s:
s = s + b"-0" # default to SSID 0
call, ssid = s.split(b'-')
@@ -50,18 +61,16 @@ def encode_address(s, final):
call = call + b" "*(6 - len(call)) # pad with spaces
encoded_call = [x << 1 for x in call[0:6]]
encoded_ssid = 0b00000000
# If ssid ends with *, the message has been repeated, so we have to set the 'has_been_repeated' flag and remove the * from the ssid
if ssid[-1] == 42:
# print("Message has been repeated")
ssid = ssid[:-1]
encoded_ssid |= 0b10000000
encoded_ssid |= (int(ssid) << 1) | 0b01100000 | (0b00000001 if final else 0)
# SSID should now be one or two postions long and contain a number (idealy between 0 and 15).
if len(ssid) == 1 and ssid[0] > 47 and ssid[0] < 58:
encoded_ssid |= (int(ssid) << 1) | 0b01100000 | (0b00000001 if final else 0)
elif len(ssid) == 2 and ssid[0] > 47 and ssid[0] < 58 and ssid[1] > 47 and ssid[1] < 58:
encoded_ssid |= (int(ssid) << 1) | 0b01100000 | (0b00000001 if final else 0)
else:
return None
return encoded_call + [encoded_ssid]
def decode_address(data, cursor):
(a1, a2, a3, a4, a5, a6, a7) = struct.unpack("<BBBBBBB", data[cursor:cursor + 7])
hrr = a7 >> 5
@@ -74,22 +83,56 @@ def decode_address(data, cursor):
call = addr
return (call, hrr, ext)
########################################################################
# Encode string from LoRa radio to AX.25 over KISS
#
# We must make no assumptions as the incomming frame could be carbage.
# So make sure we think of everthing in order to prevent crashes.
#
# The original code from Thomas Kottek did a good job encoding propper APRS frames.
# But when the frames where not what they should be, the program could crash.
#
########################################################################
def encode_kiss(frame):
# Ugly frame disassembling
# First check: do we have a semi column (seperator path field and data field)
# Note that we could still be wrong: for example when the field seperator is corrupted and we now find a semi column from, lets say, an internet address in the data field...
if not b":" in frame:
return None
# Split the frame in a path field and a data field
path = frame.split(b":")[0]
data_field = frame[frame.find(b":") + 1:]
# The source address is always followed by a greather than sign, so lets see if its there.
# There is always a change that there is another greather than sign because the frame could be corrupted...
if not b">" in path:
return None
# Split the path into a source address and a digi-path array (because digis should be seperated by commas, but again, corruption....)
src_addr = path.split(b">")[0]
digis = path[path.find(b">") + 1:].split(b",")
# destination address
packet = encode_address(digis.pop(0).upper(), False)
return_value = encode_address(digis.pop(0).upper(), False)
if return_value is None:
return None
packet = return_value
# source address
packet += encode_address(path.split(b">")[0].upper(), len(digis) == 0)
return_value = encode_address(src_addr.upper(), len(digis) == 0)
if return_value is None:
return None
packet += return_value
# digipeaters
for digi in digis:
final_addr = digis.index(digi) == len(digis) - 1
packet += encode_address(digi.upper(), final_addr)
return_value = encode_address(digi.upper(), final_addr)
if return_value is None:
return None
packet += return_value
# control field
packet += [0x03] # This is an UI frame
# protocol ID
@@ -148,6 +191,9 @@ def decode_kiss(frame):
# print("RPT: ", rpt_addr)
pos += 7
result += b"," + rpt_addr.strip()
# Add repeater flag if set
if (rpt_hrr & 0x04) == 0x04:
result += b"*"
result += b":"

View File

@@ -38,12 +38,15 @@ class LoraAprsKissTnc(LoRa):
# init has LoRa APRS default config settings - might be initialized different when creating object with parameters
def __init__(self, queue, server, frequency=433.775, preamble=8, spreadingFactor=12, bandwidth=BW.BW125,
codingrate=CODING_RATE.CR4_5, appendSignalReport = True, paSelect = 1, outputPower = 15, verbose=False):
codingrate=CODING_RATE.CR4_5, appendSignalReport = True, paSelect = 1, MaxoutputPower = 15, outputPower = 15, verbose=False):
# Init SX127x
BOARD.setup()
super(LoraAprsKissTnc, self).__init__(verbose)
self.queue = queue
if appendSignalReport == 'False':
appendSignalReport = False
self.appendSignalReport = appendSignalReport
self.set_mode(MODE.SLEEP)
@@ -51,12 +54,64 @@ class LoraAprsKissTnc(LoRa):
self.set_freq(frequency)
self.set_preamble(preamble)
self.set_spreading_factor(spreadingFactor)
if bandwidth == 'BW7_8':
bandwidth = BW.BW7_8
elif bandwidth == 'BW10_4':
bandwidth = BW.BW10_4
elif bandwidth == 'BW15_6':
bandwidth = BW.BW15_6
elif bandwidth == 'BW20_8':
bandwidth = BW.BW20_8
elif bandwidth == 'BW31_25':
bandwidth = BW.BW31_25
elif bandwidth == 'BW41_7':
bandwidth = BW.BW41_7
elif bandwidth == 'BW62_5':
bandwidth = BW.BW62_5
elif bandwidth == 'BW125':
bandwidth = BW.BW125
elif bandwidth == 'BW250':
bandwidth = BW.BW250
elif bandwidth == 'BW500':
bandwidth = BW.BW500
else:
bandwidth = BW.BW125
self.set_bw(bandwidth)
self.set_low_data_rate_optim(True)
self.set_coding_rate(codingrate)
self.set_ocp_trim(100)
self.set_pa_config(paSelect, outputPower)
if codingrate == 'CR4_5':
codingrate = CODING_RATE.CR4_5
elif codingrate == 'CR4_6':
codingrate = CODING_RATE.CR4_6
elif codingrate == 'CR4_7':
codingrate = CODING_RATE.CR4_7
elif codingrate == 'CR4_8':
codingrate = CODING_RATE.CR4_8
else:
codingrate = CODING_RATE.CR4_5
self.set_coding_rate(codingrate)
if outputPower == 20:
# Current limiter 180mA for +20dBm
self.set_ocp_trim(180)
# Set PA to +20dBm
self.set_pa_config(1, 15, 15)
self.set_pa_dac(1)
#print("+20dBm")
else:
# Current limiter 100mA for 17dBm max
self.set_ocp_trim(100)
self.set_pa_config(paSelect, MaxoutputPower, outputPower)
#print("max. +17dBm")
# CRC on
self.set_rx_crc(1)
self.set_max_payload_length(255)
self.set_dio_mapping([0] * 6)
self.server = server
@@ -77,7 +132,8 @@ class LoraAprsKissTnc(LoRa):
if self.aprs_data_type(data) == self.DATA_TYPE_THIRD_PARTY:
# remove third party thing
data = data[data.find(self.DATA_TYPE_THIRD_PARTY) + 1:]
data = self.LORA_APRS_HEADER + data
# Add LoRa-APRS header (original, this was indented one position further, only executed when above if-statement was true. Think it should be executed at all times.
data = self.LORA_APRS_HEADER + data
print("LoRa TX: " + repr(data))
self.transmit(data)
except QueueEmpty:

View File

@@ -1,14 +1,34 @@
# Raspberry Pi LoRa KISS TNC
This project adds LoRa APRS to the Raspberry Pi. It is fully integrated in the Linux AX.25 stack for maximum flexibility.
This project was originally started by Tom Kottek (https://github.com/tomelec/RPi-LoRa-KISS-TNC). Because the program had some problems dealing with digipeated frames (it crashed when receiving a ssid with the 'has_been_digipeated' flag -*- set), I took on the task of fixing the code for my personal use.
## Hardware
I also designed my own (open source) hardware for it: a board holding a Raspberry Pi Zero 2 W, an SX1278 LoRa transceiver and a power supply with on/off button to safely switch on and off the system. The design files can be found on my website: [RPi LoRa_shield](https://meezenest.nl/mees/RPi_LoRa_shield.html)
The software is stable and can handle repeated frames (incomming and outgoing) so it can be used with the aprx software to make a digipeater.
## Software
The software controls the LoRa transceiver connected to the Raspberry´s SPI bus and emulates a KISS TNC over TCP. That makes it possible to use existing software like APRX. It is also possible to attach the KISS interface to the AX.25 stack via socat/kissattach.
Assuming you installed and configured the AX.25 stack, start the LoRa KISS TNC with these commands:
#!/bin/bash
# Start TNC software
/usr/bin/python3 ~/RPi-LoRa-KISS-TNC/Start_lora-tnc.py &
sleep 10
# Attach TCP socket to virtual tty device located at /tmp/lorakisstnc
sudo /usr/bin/socat PTY,raw,echo=0,link=/tmp/lorakisstnc TCP4:127.0.0.1:10001 &
sleep 3
# Attach virtual tty to AX.25 stack (port ax2)
sudo /usr/sbin/kissattach /tmp/lorakisstnc ax2 &
## Hardware
[![How to connect LoRa module to Raspberry Pi](./images/RPi_LoRa_shield.svg)](./images/RPi_LoRa_shield.svg)
I also designed my own (open source) hardware for it: a board holding a Raspberry Pi Zero 2 W, an SX1278 LoRa transceiver and a power supply with on/off button to safely switch on and off the system. The design files can be found on my website: [RPi LoRa_shield](https://meezenest.nl/mees/RPi_LoRa_shield.html)
### To Do
* A lot
* Add raw TCP KISS socket for true AX.25 over KISS

45
RPi-LoRa-KISS-TNC.ini Normal file
View File

@@ -0,0 +1,45 @@
[LoRaSettings]
# Settings for LoRa module
frequency=433.775
preamble=8
spreadingFactor=12
# Bandwidth:
# BW7_8
# BW10_4
# BW15_6
# BW20_8
# BW31_25
# BW41_7
# BW62_5
# BW125
# BW250
# BW500
bandwidth=BW125
# Coding Rate:
# CR4_5
# CR4_6
# CR4_7
# CR4_8
codingrate=CR4_5
appendSignalReport=False
# paSelect only tested at 1
paSelect=1
# MaxoutputPower only tested at 15
MaxoutputPower = 15
# 0 ... 15 => +2 ... +17dBm
# 20 = +20dBm
outputPower = 20
[KISS]
# Settings for KISS
TCP_HOST=0.0.0.0
TCP_PORT_AX25=10001
TCP_PORT_RAW =10002
[AXUDP]
# settings for AXUDP
AXUDP_REMOTE_IP=192.168.0.185
AXUDP_REMOTE_PORT=20000
AXUDP_LOCAL_IP=0.0.0.0
AXUDP_LOCAL_PORT=20000
USE_AXUDP=False

View File

@@ -21,6 +21,25 @@ from TCPServer import KissServer
from AXUDPServer import AXUDPServer
import config
from LoraAprsKissTnc import LoraAprsKissTnc
import configparser
# Read configuration file #
parser = configparser.ConfigParser()
parser.read('/home/marcel/ham/RPi-LoRa-KISS-TNC/RPi-LoRa-KISS-TNC.ini')
config_frequency = float(parser.get('LoRaSettings', 'frequency'))
config_preamble = int(parser.get('LoRaSettings', 'preamble'))
config_spreadingFactor = int(parser.get('LoRaSettings', 'spreadingFactor'))
config_bandwidth = parser.get('LoRaSettings', 'bandwidth')
config_codingrate = parser.get('LoRaSettings', 'codingrate')
config_appendSignalReport = parser.get('LoRaSettings', 'appendSignalReport')
config_paSelect = int(parser.get('LoRaSettings', 'paSelect'))
config_MaxoutputPower = int(parser.get('LoRaSettings', 'MaxoutputPower'))
config_outputPower = int(parser.get('LoRaSettings', 'outputPower'))
config_TCP_HOST = parser.get('KISS', 'TCP_HOST')
config_TCP_PORT_AX25 = int(parser.get('KISS', 'TCP_PORT_AX25'))
config_TCP_PORT_RAW = int(parser.get('KISS', 'TCP_PORT_RAW'))
# TX KISS frames go here (Digipeater -> TNC)
kissQueue = Queue()
@@ -29,13 +48,14 @@ kissQueue = Queue()
if config.USE_AXUDP:
server = AXUDPServer(kissQueue, config.AXUDP_LOCAL_IP, config.AXUDP_LOCAL_PORT, config.AXUDP_REMOTE_IP, config.AXUDP_REMOTE_PORT)
else:
server = KissServer(kissQueue, config.TCP_HOST, config.TCP_PORT)
server = KissServer(kissQueue, config_TCP_HOST, config_TCP_PORT_AX25)
server.setDaemon(True)
server.start()
# LoRa transceiver instance
lora = LoraAprsKissTnc(kissQueue, server, verbose=False, appendSignalReport = config.APPEND_SIGNAL_REPORT)
lora = LoraAprsKissTnc(kissQueue, server, frequency=config_frequency, preamble=config_preamble, spreadingFactor=config_spreadingFactor, bandwidth=config_bandwidth,
codingrate=config_codingrate, appendSignalReport=config_appendSignalReport, paSelect = config_paSelect, MaxoutputPower = config_MaxoutputPower, outputPower = config_outputPower,verbose=True)
# this call loops forever inside
lora.startListening()

View File

@@ -60,7 +60,7 @@ class KissServer(Thread):
encoded_data = KissHelper.encode_kiss(data)
except Exception as e:
print("KISS encoding went wrong (exception while parsing)")
# traceback.print_tb(e.__traceback__)
traceback.print_tb(e.__traceback__)
encoded_data = None
if encoded_data != None:

View File

@@ -1,3 +1,8 @@
######
# THIS CONFIGURATION FILE IS NOW OBSOLETE!
# CONFIGURATION IS DONE VIA RPi-LoRa-KISS-TNC.ini
#####
## KISS Settings
# Where to listen?
# TCP_HOST can be "localhost", "0.0.0.0" or a specific interface address

4318
images/RPi_LoRa_shield.svg Normal file

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -37,7 +37,8 @@ class BOARD:
DIO1 = 23 # RaspPi GPIO 23
DIO2 = 24 # RaspPi GPIO 24
DIO3 = 25 # RaspPi GPIO 25
LED = 18 # RaspPi GPIO 18 connects to the LED on the proto shield
LED = 13 # RaspPi GPIO 18 connects to the LED on the proto shield
# Made it GPIO 13, as pin 18 is in use by Direwolf (M. Konstapel 2022-01-27)
SWITCH = 4 # RaspPi GPIO 4 connects to a switch
# The spi object is kept here

7
start_all.sh Executable file
View File

@@ -0,0 +1,7 @@
#!/bin/bash
/usr/bin/python3 /home/marcel/ham/RPi-LoRa-KISS-TNC/Start_lora-tnc.py &
sleep 10
sudo /usr/bin/socat PTY,raw,echo=0,link=/tmp/lorakisstnc TCP4:127.0.0.1:10001 &
sleep 3
sudo /usr/sbin/kissattach /tmp/lorakisstnc ax2 &