aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMatthias P. Braendli <matthias.braendli@mpb.li>2023-07-13 11:23:58 +0200
committerMatthias P. Braendli <matthias.braendli@mpb.li>2023-07-13 11:23:58 +0200
commit09e294e240921cc8af73677a273abcbaf447b8de (patch)
treefd7b242cfa96beb6b0a06864bbfb2f6fae114379
parent6f9e0d4ed4036fe3b0fe0ff85e02611cf8672ca2 (diff)
downloadlora-aprs-hb9egm-09e294e240921cc8af73677a273abcbaf447b8de.tar.gz
lora-aprs-hb9egm-09e294e240921cc8af73677a273abcbaf447b8de.tar.bz2
lora-aprs-hb9egm-09e294e240921cc8af73677a273abcbaf447b8de.zip
Add raspi igate script
-rw-r--r--README.md15
-rwxr-xr-xpython/igate.py120
2 files changed, 135 insertions, 0 deletions
diff --git a/README.md b/README.md
index 9219919..260cb0b 100644
--- a/README.md
+++ b/README.md
@@ -9,3 +9,18 @@ Try to follow https://aprs434.github.io/
Contains STM32F103, u-blox NEO-M8N, RFM98W 433MHz LoRa Module and AFT09MS007NT1 PA:
![3d view](tracker-kicad/20230709-tracker-kicad-3d.png)
+
+## i-gate
+
+`python/igate.py` contains an APRS-IS i-gate that uses a RFM98W on a Raspberry Pi.
+It only forwards position reports.
+
+### Installation
+
+Configuration: Modify the CALLSIGN and PASSCODE inside `igate.py`
+
+ cd python
+ python3 -m venv venv
+ venv/bin/pip install -r requirements.txt
+ ./igate.py
+
diff --git a/python/igate.py b/python/igate.py
new file mode 100755
index 0000000..9466304
--- /dev/null
+++ b/python/igate.py
@@ -0,0 +1,120 @@
+#!venv/bin/python
+#
+# This script is a very simple APRS-IS LoRa igate that
+# runs on a raspberry pi and expects a RFM9x on the SPI bus.
+#
+# Also, there are two status LEDs on D23 and D24 that count
+# the incoming messages.
+#
+# It is a send-only i-gate that uses the HTTP POST mechanism described
+# on http://aprs-is.net/SendOnlyPorts.aspx
+#
+import datetime
+import socket
+import struct
+import time
+
+import busio
+from digitalio import DigitalInOut, Direction, Pull
+import board
+# Import the RFM9x radio module.
+import adafruit_rfm9x
+
+# For APRS-IS
+APRS_IS_SERVER = "france.aprs2.net"
+APRS_IS_PORT = 8080
+#APRS_IS_PORT = 14580
+CALLSIGN = "HB9EGM-10"
+PASSCODE = "22681"
+REPORTING_DISTANCE = "10"
+
+def decode_encoded_position_report(packet):
+ """Returns a string with decoded latitude and longitude, or raises ValueError"""
+ ix = packet.index(b":!/")
+ gps_packet = packet[ix+3:]
+ encoded_latitude = gps_packet[:4]
+ encoded_longitude = gps_packet[4:8]
+
+ (y1, y2, y3, y4) = struct.unpack("<bbbb", encoded_latitude)
+
+ decodedLatitude = 90.0 - ((((y1-33) * pow(91,3)) + ((y2-33) * pow(91,2)) + ((y3-33) * 91) + y4-33) / 380926.0);
+
+ (x1, x2, x3, x4) = struct.unpack("<bbbb", encoded_longitude)
+
+ decodedLongitude = -180.0 + ((((x1-33) * pow(91,3)) + ((x2-33) * pow(91,2)) + ((x3-33) * 91) + x4-33) / 190463.0);
+ return f"{decodedLatitude}N / {decodedLongitude}E"
+
+def send_frame_to_aprs_is(frame):
+ headers = { "Accept": "text/plain", "Content-Type": "application/octet-stream" }
+
+ aprsauth = f"user {CALLSIGN} pass {PASSCODE} vers HB9EGM_lorapy 1.0 filter t/m/{CALLSIGN}/{REPORTING_DISTANCE}\n";
+
+ # Prefix igate path
+ path, _, message = frame[3:].partition(b":")
+ new_frame = path + b",qAO," + CALLSIGN.encode() + b":" + message + b"\n"
+ data = aprsauth.encode() + new_frame
+
+ r = requests.post(f"http://{APRS_IS_SERVER}:{APRS_IS_PORT}/", data=data, headers=headers)
+ print(f"{r.status_code} {r.content.decode()}")
+
+
+led_yellow = DigitalInOut(board.D23)
+led_yellow.direction = Direction.OUTPUT
+led_green = DigitalInOut(board.D24)
+led_green.direction = Direction.OUTPUT
+
+led_counter = 0
+
+def update_leds():
+ led_counter = (led_counter + 1) % 4
+
+ led_green.value = (led_counter & 0x1) != 0
+ led_yellow.value = (led_counter & 0x2) != 0
+
+update_leds()
+
+cs = DigitalInOut(board.CE1)
+reset = DigitalInOut(board.D25)
+spi = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO)
+
+baudrate = 5000000
+
+# see https://docs.circuitpython.org/projects/rfm9x/en/latest/
+try:
+ rfm9x = adafruit_rfm9x.RFM9x(spi, cs, reset, 433.775, baudrate=baudrate, preamble_length=8)
+ rfm9x.signal_bandwidth = 125000
+ rfm9x.coding_rate = 5
+ #rfm9x.tx_power = 23 # default 13
+
+ rfm9x.spreading_factor = 12
+
+ # Section 4.1.1.6 - Set low datarate automatically based on BW and SF
+ symbolDuration = 1000 / ( rfm9x.signal_bandwidth / (1 << rfm9x.spreading_factor) )
+ if symbolDuration > 16:
+ rfm9x.low_datarate_optimize = 1
+ else:
+ rfm9x.low_datarate_optimize = 0
+
+ print("Waiting for packets...")
+ while True:
+ packet = rfm9x.receive(timeout=4, with_header=True)
+ if packet is not None:
+ packet = bytes(packet) # otherwise it's a bytearray
+
+ print("{}: {}".format(datetime.datetime.now(), packet))
+ #print(" {}".format(" ".join(hex(x) for x in packet)))
+
+ if b":!/" in packet:
+ print(decode_encoded_position_report(packet))
+ try:
+ send_frame_to_aprs_is(packet)
+ except Exception as ex:
+ print("Error sending to APRS-IS")
+ print(ex)
+
+ print(" RSSI: {}".format(rfm9x.last_rssi))
+ print()
+
+ update_leds()
+except RuntimeError as error:
+ print(f'RFM9x Error: {error}')