Compare commits

...

8 Commits

12 changed files with 4589 additions and 32 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

@@ -29,6 +29,8 @@
# Changes by PE1RXF # Changes by PE1RXF
# #
# 2022-01-23: - in encode_address() added correct handling of has_been_repeated flag '*' # 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 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 # If it's the final address in the header, set the low bit to 1
# Ignoring command/response for simple example # Ignoring command/response for simple example
def encode_address(s, final): 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: if b"-" not in s:
s = s + b"-0" # default to SSID 0 s = s + b"-0" # default to SSID 0
call, ssid = s.split(b'-') call, ssid = s.split(b'-')
@@ -50,18 +61,16 @@ def encode_address(s, final):
call = call + b" "*(6 - len(call)) # pad with spaces call = call + b" "*(6 - len(call)) # pad with spaces
encoded_call = [x << 1 for x in call[0:6]] encoded_call = [x << 1 for x in call[0:6]]
encoded_ssid = 0b00000000 # SSID should now be one or two postions long and contain a number (idealy between 0 and 15).
# 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 len(ssid) == 1 and ssid[0] > 47 and ssid[0] < 58:
if ssid[-1] == 42: encoded_ssid |= (int(ssid) << 1) | 0b01100000 | (0b00000001 if final else 0)
# print("Message has been repeated") elif len(ssid) == 2 and ssid[0] > 47 and ssid[0] < 58 and ssid[1] > 47 and ssid[1] < 58:
ssid = ssid[:-1] encoded_ssid |= (int(ssid) << 1) | 0b01100000 | (0b00000001 if final else 0)
encoded_ssid |= 0b10000000 else:
return None
encoded_ssid |= (int(ssid) << 1) | 0b01100000 | (0b00000001 if final else 0)
return encoded_call + [encoded_ssid] return encoded_call + [encoded_ssid]
def decode_address(data, cursor): def decode_address(data, cursor):
(a1, a2, a3, a4, a5, a6, a7) = struct.unpack("<BBBBBBB", data[cursor:cursor + 7]) (a1, a2, a3, a4, a5, a6, a7) = struct.unpack("<BBBBBBB", data[cursor:cursor + 7])
hrr = a7 >> 5 hrr = a7 >> 5
@@ -74,22 +83,56 @@ def decode_address(data, cursor):
call = addr call = addr
return (call, hrr, ext) 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): 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: if not b":" in frame:
return None return None
# Split the frame in a path field and a data field
path = frame.split(b":")[0] 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] src_addr = path.split(b">")[0]
digis = path[path.find(b">") + 1:].split(b",") digis = path[path.find(b">") + 1:].split(b",")
# destination address # 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 # 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 # digipeaters
for digi in digis: for digi in digis:
final_addr = digis.index(digi) == len(digis) - 1 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 # control field
packet += [0x03] # This is an UI frame packet += [0x03] # This is an UI frame
# protocol ID # protocol ID
@@ -148,6 +191,9 @@ def decode_kiss(frame):
# print("RPT: ", rpt_addr) # print("RPT: ", rpt_addr)
pos += 7 pos += 7
result += b"," + rpt_addr.strip() result += b"," + rpt_addr.strip()
# Add repeater flag if set
if (rpt_hrr & 0x04) == 0x04:
result += b"*"
result += b":" result += b":"

View File

@@ -44,6 +44,9 @@ class LoraAprsKissTnc(LoRa):
super(LoraAprsKissTnc, self).__init__(verbose) super(LoraAprsKissTnc, self).__init__(verbose)
self.queue = queue self.queue = queue
if appendSignalReport == 'False':
appendSignalReport = False
self.appendSignalReport = appendSignalReport self.appendSignalReport = appendSignalReport
self.set_mode(MODE.SLEEP) self.set_mode(MODE.SLEEP)
@@ -51,12 +54,64 @@ class LoraAprsKissTnc(LoRa):
self.set_freq(frequency) self.set_freq(frequency)
self.set_preamble(preamble) self.set_preamble(preamble)
self.set_spreading_factor(spreadingFactor) 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_bw(bandwidth)
self.set_low_data_rate_optim(True) self.set_low_data_rate_optim(True)
self.set_coding_rate(codingrate)
self.set_ocp_trim(100) 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)
self.set_pa_config(paSelect, MaxoutputPower, outputPower) # 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_max_payload_length(255)
self.set_dio_mapping([0] * 6) self.set_dio_mapping([0] * 6)
self.server = server self.server = server
@@ -77,7 +132,8 @@ class LoraAprsKissTnc(LoRa):
if self.aprs_data_type(data) == self.DATA_TYPE_THIRD_PARTY: if self.aprs_data_type(data) == self.DATA_TYPE_THIRD_PARTY:
# remove third party thing # remove third party thing
data = data[data.find(self.DATA_TYPE_THIRD_PARTY) + 1:] 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)) print("LoRa TX: " + repr(data))
self.transmit(data) self.transmit(data)
except QueueEmpty: except QueueEmpty:

View File

@@ -1,14 +1,34 @@
# Raspberry Pi LoRa KISS TNC # 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. 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 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.
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)
## Software ## 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. 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 ### 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 from AXUDPServer import AXUDPServer
import config import config
from LoraAprsKissTnc import LoraAprsKissTnc 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) # TX KISS frames go here (Digipeater -> TNC)
kissQueue = Queue() kissQueue = Queue()
@@ -29,13 +48,14 @@ kissQueue = Queue()
if config.USE_AXUDP: if config.USE_AXUDP:
server = AXUDPServer(kissQueue, config.AXUDP_LOCAL_IP, config.AXUDP_LOCAL_PORT, config.AXUDP_REMOTE_IP, config.AXUDP_REMOTE_PORT) server = AXUDPServer(kissQueue, config.AXUDP_LOCAL_IP, config.AXUDP_LOCAL_PORT, config.AXUDP_REMOTE_IP, config.AXUDP_REMOTE_PORT)
else: else:
server = KissServer(kissQueue, config.TCP_HOST, config.TCP_PORT) server = KissServer(kissQueue, config_TCP_HOST, config_TCP_PORT_AX25)
server.setDaemon(True) server.setDaemon(True)
server.start() server.start()
# LoRa transceiver instance # 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 # this call loops forever inside
lora.startListening() lora.startListening()

View File

@@ -60,7 +60,7 @@ class KissServer(Thread):
encoded_data = KissHelper.encode_kiss(data) encoded_data = KissHelper.encode_kiss(data)
except Exception as e: except Exception as e:
print("KISS encoding went wrong (exception while parsing)") print("KISS encoding went wrong (exception while parsing)")
# traceback.print_tb(e.__traceback__) traceback.print_tb(e.__traceback__)
encoded_data = None encoded_data = None
if 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 ## KISS Settings
# Where to listen? # Where to listen?
# TCP_HOST can be "localhost", "0.0.0.0" or a specific interface address # 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

View File

@@ -37,7 +37,8 @@ class BOARD:
DIO1 = 23 # RaspPi GPIO 23 DIO1 = 23 # RaspPi GPIO 23
DIO2 = 24 # RaspPi GPIO 24 DIO2 = 24 # RaspPi GPIO 24
DIO3 = 25 # RaspPi GPIO 25 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 SWITCH = 4 # RaspPi GPIO 4 connects to a switch
# The spi object is kept here # The spi object is kept here

View File

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