├── debian ├── compat ├── source │ ├── format │ └── options ├── python3-skygate.docs ├── modules-load.d │ └── skygate.conf ├── modprobe.d │ └── skygate.conf ├── py3dist-overrides ├── python3-skygate.install ├── rules ├── python3-skygate.postinst ├── changelog ├── control ├── asoundrc.loopback └── copyright ├── skygate ├── __init__.py ├── skygate ├── skygate-gateway ├── ball.png ├── balloon.png ├── chute.png ├── compass.png ├── skycademy.png ├── test_gateway.py ├── test_gps.py ├── radio.py ├── test_car_upload.py ├── test_lora.py ├── toggle_skygate ├── test_lora_upload.py ├── gpsscreen.py ├── skygate_rtty ├── rttyscreen.py ├── misc.py ├── lorascreen.py ├── ssdvscreen.py ├── ssdv.py ├── habitat.py ├── rtty.py ├── gateway.py ├── habscreen.py ├── gps.py ├── lora.py ├── skygate.py └── skygate.glade ├── .gitignore ├── MANIFEST.in ├── setup.py ├── .asoundrc └── README.md /debian/compat: -------------------------------------------------------------------------------- 1 | 9 2 | -------------------------------------------------------------------------------- /skygate/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (native) 2 | -------------------------------------------------------------------------------- /debian/python3-skygate.docs: -------------------------------------------------------------------------------- 1 | README.md 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | skygate.egg-info 2 | __pycache__ 3 | -------------------------------------------------------------------------------- /debian/modules-load.d/skygate.conf: -------------------------------------------------------------------------------- 1 | snd-aloop 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include skygate *.glade *.png 2 | -------------------------------------------------------------------------------- /debian/source/options: -------------------------------------------------------------------------------- 1 | extend-diff-ignore = "^[^/]*[.]egg-info/" 2 | -------------------------------------------------------------------------------- /debian/modprobe.d/skygate.conf: -------------------------------------------------------------------------------- 1 | options snd-aloop pcm_substreams=1 2 | -------------------------------------------------------------------------------- /skygate/skygate: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import skygate.skygate 3 | -------------------------------------------------------------------------------- /skygate/skygate-gateway: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | /usr/bin/python3 skygate.py 4 | -------------------------------------------------------------------------------- /debian/py3dist-overrides: -------------------------------------------------------------------------------- 1 | gpiozero python3-gpiozero 2 | spidev python3-spidev 3 | -------------------------------------------------------------------------------- /skygate/ball.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raspberrypi/skygate/HEAD/skygate/ball.png -------------------------------------------------------------------------------- /skygate/balloon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raspberrypi/skygate/HEAD/skygate/balloon.png -------------------------------------------------------------------------------- /skygate/chute.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raspberrypi/skygate/HEAD/skygate/chute.png -------------------------------------------------------------------------------- /skygate/compass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raspberrypi/skygate/HEAD/skygate/compass.png -------------------------------------------------------------------------------- /skygate/skycademy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raspberrypi/skygate/HEAD/skygate/skycademy.png -------------------------------------------------------------------------------- /debian/python3-skygate.install: -------------------------------------------------------------------------------- 1 | debian/modprobe.d/skygate.conf /lib/modprobe.d 2 | debian/modules-load.d/skygate.conf /usr/lib/modules-load.d 3 | debian/asoundrc.loopback /usr/share/skygate 4 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | #export DH_VERBOSE = 1 3 | 4 | export PYBUILD_NAME=skygate 5 | 6 | %: 7 | dh $@ --with python3 --buildsystem=pybuild 8 | 9 | override_dh_auto_test: 10 | -------------------------------------------------------------------------------- /skygate/test_gateway.py: -------------------------------------------------------------------------------- 1 | from skygate.gateway import * 2 | import time 3 | 4 | print("Creating gateway object ...") 5 | mygateway = gateway() 6 | 7 | mygateway.run() 8 | 9 | while 1: 10 | time.sleep(1) 11 | CarPosition = mygateway.gps.Position() 12 | print (CarPosition) 13 | -------------------------------------------------------------------------------- /skygate/test_gps.py: -------------------------------------------------------------------------------- 1 | from skygate.gps import * 2 | import time 3 | 4 | print("Creating GPS object ...") 5 | mygps = GPS(); 6 | 7 | print("Open GPS ...") 8 | mygps.open() 9 | print("GPS open OK") 10 | 11 | # mygps.GetPositions() 12 | mygps.run() 13 | 14 | while 1: 15 | time.sleep(1) 16 | print (mygps.Position()) 17 | -------------------------------------------------------------------------------- /skygate/radio.py: -------------------------------------------------------------------------------- 1 | class Radio(object): 2 | # Radio - RTTY, LoRa 3 | Emulation = False 4 | ChannelOpen = False 5 | 6 | def __init__(self): 7 | pass 8 | 9 | def open(self): 10 | # Open connection to radio device 11 | # Return True if connected OK, False if not 12 | ChannelOpen = True 13 | 14 | return ChannelOpen 15 | 16 | def build_sentence(self, values): 17 | print ("values = ", values) 18 | return "nothing" -------------------------------------------------------------------------------- /debian/python3-skygate.postinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | case "$1" in 6 | configure) 7 | if [ -z "$2" ]; then 8 | modprobe snd-aloop pcm_substreams=1 9 | fi 10 | ;; 11 | abort-upgrade|abort-remove|abort-deconfigure) 12 | ;; 13 | 14 | *) 15 | echo "postinst called with unknown argument \`$1'" >&2 16 | exit 0 17 | ;; 18 | esac 19 | 20 | #DEBHELPER# 21 | 22 | -------------------------------------------------------------------------------- /skygate/test_car_upload.py: -------------------------------------------------------------------------------- 1 | from skygate.gps import * 2 | from skygate.habitat import * 3 | import time 4 | 5 | print("Creating GPS object ...") 6 | mygps = GPS() 7 | 8 | print("Creating habitat object") 9 | hab = habitat() 10 | 11 | print("Open GPS ...") 12 | mygps.open() 13 | print("GPS open OK") 14 | 15 | mygps.run() 16 | hab.EnableCarUpload = True 17 | 18 | while 1: 19 | time.sleep(1) 20 | CarPosition = mygps.Position() 21 | print (CarPosition) 22 | print(hab.SetCarPosition(CarPosition)) 23 | -------------------------------------------------------------------------------- /skygate/test_lora.py: -------------------------------------------------------------------------------- 1 | from skygate.lora import * 2 | import time 3 | 4 | def woo_got_a_packet(packet): 5 | print(packet) 6 | if packet == None: 7 | print("Failed packet") 8 | elif (packet[0] & 0x80) == 0: 9 | # ASCII 10 | Sentence = ''.join(map(chr,bytes(packet).split(b'\x00')[0])) 11 | print("Sentence=" + Sentence, end='') 12 | else: 13 | print("Packet=", packet) 14 | 15 | mylora = LoRa(1, 434.450, 1) 16 | 17 | mylora.listen_for_packets(woo_got_a_packet) 18 | 19 | while 1: 20 | time.sleep(0.01) 21 | 22 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | skygate (0.3) stretch; urgency=medium 2 | 3 | * Add gir1.2-gtk-3.0 dependency 4 | 5 | -- Serge Schneider Thu, 09 Aug 2018 10:30:34 +0100 6 | 7 | skygate (0.2) stretch; urgency=medium 8 | 9 | * Add alsa-utils dependency 10 | * Delete skygate/lcars.py 11 | * Now uses skygate.ini in ~/.config 12 | 13 | -- Serge Schneider Thu, 01 Feb 2018 13:38:16 +0000 14 | 15 | skygate (0.1) stretch; urgency=medium 16 | 17 | * Initial Release. 18 | 19 | -- Serge Schneider Fri, 10 Nov 2017 18:29:07 +0000 20 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: skygate 2 | Section: hamradio 3 | Priority: optional 4 | Maintainer: Serge Schneider 5 | Build-Depends: debhelper (>= 9), dh-python, python3-all, python3-setuptools 6 | Standards-Version: 3.9.8 7 | Homepage: http://github.com/raspberrypi/skygate 8 | X-Python3-Version: >= 3.2 9 | 10 | Package: python3-skygate 11 | Architecture: all 12 | Depends: ${python3:Depends}, ${misc:Depends}, gir1.2-gtk-3.0, wmctrl, rtl-sdr, dl-fldigi, ssdv, 13 | alsa-utils 14 | Description: HAB Receiver for RTTY and LoRa (Python 3) 15 | . 16 | This package installs the library for Python 3. 17 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='skygate', 5 | version='1.0', 6 | packages=['skygate'], 7 | url='www.daveakerman.com', 8 | license='GPL 2.0', 9 | author='Dave Akerman', 10 | author_email='dave@sccs.co.uk', 11 | description='HAB Receiver for RTTY and LoRa', 12 | scripts=[ 13 | 'skygate/skygate', 14 | 'skygate/skygate_rtty', 15 | 'skygate/toggle_skygate' 16 | ], 17 | install_requires=[ 18 | 'pygobject', 19 | 'gpiozero', 20 | 'setuptools', 21 | 'pyserial', 22 | 'spidev', 23 | ], 24 | include_package_data=True, 25 | ) 26 | 27 | -------------------------------------------------------------------------------- /skygate/toggle_skygate: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | AUTOSTART_DIR="$HOME/.config/autostart" 4 | SG_RTTY_FILE="$AUTOSTART_DIR/skygate_rtty.desktop" 5 | SG_FILE="$AUTOSTART_DIR/skygate.desktop" 6 | 7 | create_autostart_file() 8 | { 9 | CMD="$1" 10 | cat > "$AUTOSTART_DIR/$CMD.desktop" << EOF 11 | [Desktop Entry] 12 | Type=Application 13 | Exec=$CMD 14 | EOF 15 | } 16 | 17 | if [ "$(id -u)" -eq 0 ]; then 18 | echo "$0 should not run as root" 19 | exit 1 20 | fi 21 | 22 | if [ -f "$SG_RTTY_FILE" ] && [ -f "$SG_FILE" ]; then 23 | rm -f "$SG_RTTY_FILE" "$SG_FILE" 24 | echo "Skygate autostart disabled" 25 | else 26 | mkdir -p "$AUTOSTART_DIR" 27 | for cmd in skygate skygate_rtty; do 28 | create_autostart_file "$cmd" 29 | done 30 | echo "Skygate autostart enabled" 31 | fi 32 | -------------------------------------------------------------------------------- /skygate/test_lora_upload.py: -------------------------------------------------------------------------------- 1 | from skygate.lora import * 2 | from habitat import * 3 | import time 4 | 5 | def woo_got_a_packet(packet): 6 | if packet == None: 7 | print("Failed packet") 8 | elif hab.IsSentence(packet[0]): 9 | # ASCII 10 | Sentence = ''.join(map(chr,bytes(packet).split(b'\x00')[0])) 11 | print("Sentence=" + Sentence, end='') 12 | hab.UploadTelemetry('python', Sentence) 13 | elif hab.IsSSDV(packet[0]): 14 | print("SSDV Packet") 15 | hab.UploadSSDV('python', packet) 16 | else: 17 | print("Unknown packet ", packet[0]) 18 | 19 | print("Creating lora object") 20 | mylora = LoRa(1, 434.444, 1) 21 | 22 | mylora.listen_for_packets(woo_got_a_packet) 23 | 24 | print("Creating habitat object") 25 | hab = habitat() 26 | 27 | while 1: 28 | time.sleep(0.01) 29 | 30 | -------------------------------------------------------------------------------- /.asoundrc: -------------------------------------------------------------------------------- 1 | # .asoundrc 2 | pcm.multi { 3 | type route; 4 | slave.pcm { 5 | type multi; 6 | slaves.a.pcm "output"; 7 | slaves.b.pcm "loopin"; 8 | slaves.a.channels 2; 9 | slaves.b.channels 2; 10 | bindings.0.slave a; 11 | bindings.0.channel 0; 12 | bindings.1.slave a; 13 | bindings.1.channel 1; 14 | bindings.2.slave b; 15 | bindings.2.channel 0; 16 | bindings.3.slave b; 17 | bindings.3.channel 1; 18 | } 19 | 20 | ttable.0.0 1; 21 | ttable.1.1 1; 22 | ttable.0.2 1; 23 | ttable.1.3 1; 24 | } 25 | 26 | pcm.!default { 27 | type plug 28 | slave.pcm "multi" 29 | } 30 | 31 | pcm.output { 32 | type hw 33 | card 0 34 | } 35 | 36 | pcm.loopin { 37 | type plug 38 | slave.pcm "hw:Loopback,0,0" 39 | } 40 | 41 | pcm.loopout { 42 | type plug 43 | slave.pcm "hw:Loopback,1,0" 44 | } 45 | -------------------------------------------------------------------------------- /debian/asoundrc.loopback: -------------------------------------------------------------------------------- 1 | pcm.multi { 2 | type route; 3 | slave.pcm { 4 | type multi; 5 | slaves.a.pcm "output"; 6 | slaves.b.pcm "loopin"; 7 | slaves.a.channels 2; 8 | slaves.b.channels 2; 9 | bindings.0.slave a; 10 | bindings.0.channel 0; 11 | bindings.1.slave a; 12 | bindings.1.channel 1; 13 | bindings.2.slave b; 14 | bindings.2.channel 0; 15 | bindings.3.slave b; 16 | bindings.3.channel 1; 17 | } 18 | 19 | ttable.0.0 1; 20 | ttable.1.1 1; 21 | ttable.0.2 1; 22 | ttable.1.3 1; 23 | } 24 | 25 | pcm.!default { 26 | type plug 27 | slave.pcm "multi" 28 | } 29 | 30 | pcm.output { 31 | type hw 32 | card CARD_ID 33 | } 34 | 35 | pcm.loopin { 36 | type plug 37 | slave.pcm "hw:Loopback,0,0" 38 | } 39 | 40 | pcm.loopout { 41 | type plug 42 | slave.pcm "hw:Loopback,1,0" 43 | } 44 | -------------------------------------------------------------------------------- /skygate/gpsscreen.py: -------------------------------------------------------------------------------- 1 | from skygate.misc import * 2 | 3 | class GPSScreen(object): 4 | 5 | def __init__(self, builder): 6 | self.frame = builder.get_object("frameGPS") 7 | self.Log = [] 8 | 9 | self.lblGPSDevice = builder.get_object("lblGPSDevice") 10 | self.lblGPSTime = builder.get_object("lblGPSTime") 11 | self.lblGPSLatitude = builder.get_object("lblGPSLatitude") 12 | self.lblGPSLongitude = builder.get_object("lblGPSLongitude") 13 | self.lblGPSAltitude = builder.get_object("lblGPSAltitude") 14 | self.lblGPSSatellites = builder.get_object("lblGPSSatellites") 15 | 16 | def ShowPortStatus(self, Status): 17 | self.lblGPSDevice.set_text(Status) 18 | 19 | def ShowPosition(self, Position): 20 | self.lblGPSTime.set_text(Position['time']) 21 | self.lblGPSLatitude.set_text("{0:.5f}".format(Position['lat'])) 22 | self.lblGPSLongitude.set_text("{0:.5f}".format(Position['lon'])) 23 | self.lblGPSAltitude.set_text(str(int(Position['alt']))) 24 | self.lblGPSSatellites.set_text(str(Position['sats'])) 25 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: skygate 3 | Source: http://github.com/raspberrypi/skygate 4 | 5 | Files: * 6 | Copyright: 2017 Raspberry Pi (Trading) Ltd. 7 | License: GPL-2.0+ 8 | 9 | License: GPL-2.0+ 10 | This package is free software; you can redistribute it and/or modify 11 | it under the terms of the GNU General Public License as published by 12 | the Free Software Foundation; either version 2 of the License, or 13 | (at your option) any later version. 14 | . 15 | This package is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU General Public License for more details. 19 | . 20 | You should have received a copy of the GNU General Public License 21 | along with this program. If not, see 22 | . 23 | On Debian systems, the complete text of the GNU General 24 | Public License version 2 can be found in "/usr/share/common-licenses/GPL-2". 25 | -------------------------------------------------------------------------------- /skygate/skygate_rtty: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cleanup() 4 | { 5 | if [ ! -d "$TMP_DIR" ]; then 6 | return 7 | fi 8 | jobs -p > "${TMP_DIR}/jobs" 9 | xargs kill < "${TMP_DIR}/jobs" 2> /dev/null 10 | if [ -f "${TMP_DIR}/.asoundrc.bak" ]; then 11 | mv "${TMP_DIR}/.asoundrc.bak" "${HOME}/.asoundrc" 12 | else 13 | rm "${HOME}/.asoundrc" 14 | fi 15 | rm -rf "$TMP_DIR" 16 | wait 17 | } 18 | 19 | CARD_ID="$(aplay -l | sed -n '0,/card \([[:digit:]]*\): ALSA \[bcm2835 ALSA\].*/s//\1/p')" 20 | if [ -z "$CARD_ID" ]; then 21 | echo "Could not determine sound card ID" 22 | exit 1 23 | fi 24 | 25 | TMP_DIR="$(mktemp -d)" 26 | if [ -f "${HOME}/.asoundrc" ]; then 27 | cp "${HOME}/.asoundrc" "${TMP_DIR}/.asoundrc.bak" 28 | fi 29 | trap cleanup 0 1 2 3 6 30 | sed "s/CARD_ID/$CARD_ID/" "/usr/share/skygate/asoundrc.loopback" > "${HOME}/.asoundrc" 31 | 32 | rtl_fm -M usb -f 434.100M -s 192000 -r 8000 - | aplay -f S16_LE -t raw -c 1 & 33 | APLAY_PID="$!" 34 | 35 | sleep 1 36 | if ! ps -p "$APLAY_PID" > /dev/null; then 37 | echo "Could not start rtl_fm" 38 | elif [ "$(pgrep -x -c skygate_rtty 2> /dev/null)" != 1 ]; then 39 | echo "Skygate already running" 40 | else 41 | dl-fldigi --wfall-only 42 | fi 43 | -------------------------------------------------------------------------------- /skygate/rttyscreen.py: -------------------------------------------------------------------------------- 1 | from skygate.misc import * 2 | 3 | class RTTYScreen(object): 4 | 5 | def __init__(self, builder): 6 | self.frame = builder.get_object("frameRTTY") 7 | 8 | self.textRTTY = builder.get_object("textRTTY") 9 | self.lblCurrentRTTY = builder.get_object("lblCurrentRTTY") 10 | self.scrollRTTY = builder.get_object("scrollRTTY") 11 | self.lblRTTYFrequency = builder.get_object("lblRTTYFrequency") 12 | 13 | self.Log = UpdateLog(self.textRTTY, ['', 14 | ' This window will show messages received from the RTTY receiver', 15 | '', 16 | ' If no such messages appear, then check the following:', 17 | '', 18 | ' - that your tracker is configured and running', 19 | ' - that the RTTY frequency (see Settings screen) matches the tracker', 20 | '', 21 | ' and in dl-fldigi (using the button below), check the following:', 22 | '', 23 | ' - that the the thin red cursor lines align with the yellow/red signal', 24 | ' - that the baud rate (50/300) and databits (7/8) match the tracker', 25 | ''], 26 | '', 18) 27 | 28 | # self.PositionDlFldigi() 29 | 30 | def AppendLine(self, Line): 31 | self.Log = UpdateLog(self.textRTTY, self.Log, Line, 18) 32 | 33 | def ShowRTTYFrequency(self, Frequency): 34 | self.lblRTTYFrequency.set_text("{0:.4f}".format(Frequency) + ' MHz') 35 | 36 | def ShowCurrentRTTY(self, CurrentRTTY): 37 | self.lblCurrentRTTY.set_text(CurrentRTTY) 38 | -------------------------------------------------------------------------------- /skygate/misc.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | def BoolToStr(value): 4 | if value: 5 | return '1' 6 | else: 7 | return '0' 8 | 9 | def CalculateDistance(HABLatitude, HABLongitude, CarLatitude, CarLongitude): 10 | HABLatitude = HABLatitude * math.pi / 180 11 | HABLongitude = HABLongitude * math.pi / 180 12 | CarLatitude = CarLatitude * math.pi / 180 13 | CarLongitude = CarLongitude * math.pi / 180 14 | 15 | return 6371000 * math.acos(math.sin(CarLatitude) * math.sin(HABLatitude) + math.cos(CarLatitude) * math.cos(HABLatitude) * math.cos(HABLongitude-CarLongitude)) 16 | 17 | def CalculateDirection(HABLatitude, HABLongitude, CarLatitude, CarLongitude): 18 | HABLatitude = HABLatitude * math.pi / 180 19 | HABLongitude = HABLongitude * math.pi / 180 20 | CarLatitude = CarLatitude * math.pi / 180 21 | CarLongitude = CarLongitude * math.pi / 180 22 | 23 | y = math.sin(HABLongitude - CarLongitude) * math.cos(HABLatitude) 24 | x = math.cos(CarLatitude) * math.sin(HABLatitude) - math.sin(CarLatitude) * math.cos(HABLatitude) * math.cos(HABLongitude - CarLongitude) 25 | 26 | return math.atan2(y, x) * 180 / math.pi 27 | 28 | def PlaceTextInTextBox(TextBox, SomeText): 29 | buffer = TextBox.get_buffer() 30 | start = buffer.get_iter_at_offset(0) 31 | end = buffer.get_iter_at_offset(999) 32 | buffer.delete(start, end) 33 | buffer.insert_at_cursor(SomeText) 34 | 35 | def UpdateLog(TextBox, Log, Line, MaxLines): 36 | Log += [Line.rstrip()] 37 | first = max(0, len(Log)-MaxLines) 38 | Log = Log[first:] 39 | buffer = TextBox.get_buffer() 40 | buffer.set_text("\n".join(Log)) 41 | return Log 42 | -------------------------------------------------------------------------------- /skygate/lorascreen.py: -------------------------------------------------------------------------------- 1 | from skygate.misc import * 2 | 3 | class LoRaScreen(object): 4 | 5 | def __init__(self, builder): 6 | self.frame = builder.get_object("frameLoRa") 7 | self.textLoRa = builder.get_object("textLoRa") 8 | self.scrollLoRa = builder.get_object("scrollLoRa") 9 | self.lblLoRaFrequency = builder.get_object("lblLoRaFrequency") 10 | self.lblLoRaFrequencyError = builder.get_object("lblLoRaFrequencyError") 11 | 12 | # Place description in log window 13 | self.Log = UpdateLog(self.textLoRa, ['', '', 14 | ' This window will show messages received from the LoRa device', 15 | '', 16 | ' If no such messages appear, then check the following:', 17 | '', 18 | ' - that your tracker is configured and running', 19 | ' - that the LoRa frequency (see Settings screen) matches the tracker', 20 | ' - that the LoRa mode (see Settings screen) matches the tracker', 21 | ' - that the tracker and receiver are very close or have aerials attached', 22 | '', 23 | ' If all of the above are OK, try tuning the receiver up/down using the buttons below.', 24 | ' These step the frequency up/down in 1kHz steps.', 25 | '', 26 | ' You may need to tune as far as 5kHz away from the transmitter frequency before it works.', 27 | '', 28 | ' Once you are receiving data, adjust to reduce the displayed error to 1kHz or less', 29 | ''], '', 15) 30 | 31 | def AppendLine(self, Line): 32 | self.Log = UpdateLog(self.textLoRa, self.Log, Line, 15) 33 | 34 | def ShowLoRaFrequencyAndMode(self, LoRaFrequency, LoRaMode): 35 | self.lblLoRaFrequency.set_text("{0:.3f}".format(LoRaFrequency) + ' MHz, Mode ' + str(LoRaMode)) 36 | 37 | def ShowFrequencyError(self, LoRaFrequencyError): 38 | self.lblLoRaFrequencyError.set_text("Err: {0:.1f}".format(LoRaFrequencyError) + ' kHz') 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # skygate - Modular Python/GTK HAB Receiver and Uploader 2 | 3 | HAB receiver software for tracking LoRa and RTTY payloads, and uploading telemetry and image data to Habitat along with the receiver's GPS position. Uses external software (dl-fldigi) to decode RTTY, using audio from a real radio or from an RTL-SDR using rtl_fm program. 4 | 5 | 6 | ## Receiver ## 7 | 8 | 9 | This software is written for Python 3. 10 | 11 | It needs these items to be installed: 12 | 13 | sudo apt-get install wmctrl rtl-sdr dl-fldigi ssdv 14 | 15 | 16 | ## Raspbian Configuration ## 17 | 18 | Enable the following with raspi-config: 19 | 20 | Interfacing Options --> SPI --> Yes 21 | 22 | If you are using a GPS HAT, then you also need to set the serial port: 23 | 24 | Interfacing Options --> Serial --> No (login) --> Yes (hardware) 25 | 26 | ## Loopback Sound Device ## 27 | 28 | Create a new ~/.asoundrc file using the supplied file, with 29 | 30 | cp ~/skygate/.asoundrc ~ 31 | 32 | ## Usage (Autostart) ## 33 | 34 | Edit the following text file: 35 | 36 | ~/.config/lxsession/LXDE-pi/autostart 37 | 38 | and append these 2 lines: 39 | 40 | @/home/pi/skygate/skygate/start_gateway.sh 41 | @/home/pi/skygate/skygate/start_rtty.sh 42 | 43 | 44 | ## Usage (Manual) ## 45 | 46 | Set up the loopback audio device, with 47 | 48 | modprobe snd-aloop pcm_substreams=1 49 | 50 | Start the RTL SDR software with 51 | 52 | rtl_fm -M usb -f 434.100M -s 192000 -r 8000 - | aplay -f S16_LE -t raw -c 1 53 | 54 | 55 | Start dl-fldigi with 56 | 57 | ./src/dl-fldigi --wfall-only 58 | 59 | At startup, enter your receiver callsign. Then, on the audio config page, select Port Audio and the first device. 60 | 61 | Then configure dl-fldigi through its menus (these settings suit pytrack and Pi In The Sky default settings): 62 | 63 | Op Mode --> RTTY Custom --> Set 300 baud, 8 data bits, 2 stop bits, 830Hz shift 64 | 65 | (or, if not using SSDV) 50 baud, 7 data bits, 2 stop bits, 830Hz shift 66 | 67 | 68 | dl-fldigi includes an fldigi bug that causes it to sometimes fail on startup with an error code of 11. Try again if this happens. 69 | 70 | 71 | The receiver program can be started manually from a terminal window in an X session with: 72 | 73 | cd 74 | cd skygate/gateway 75 | python3 skygate.py 76 | 77 | 78 | -------------------------------------------------------------------------------- /skygate/ssdvscreen.py: -------------------------------------------------------------------------------- 1 | import os, time, glob 2 | from skygate.misc import * 3 | from gi.repository import Gtk, GObject, Pango, GdkPixbuf 4 | 5 | class SSDVScreen(object): 6 | 7 | def __init__(self, builder): 8 | self.DisplayedSSDVFileName = '' 9 | self.SSDVModificationDate = 0 10 | 11 | self.frame = builder.get_object("frameSSDV") 12 | 13 | self.imageSSDV = builder.get_object("imageSSDV") 14 | self.boxSSDV = builder.get_object("boxSSDV") 15 | self.lblSSDVInfo = builder.get_object("lblSSDVInfo") 16 | 17 | def ExtractImageInfoFromFileName(self, FileName): 18 | print(FileName) 19 | temp = FileName.split('/') 20 | temp = temp[1].split('.') 21 | fields = temp[0].split('_') 22 | return {'callsign': fields[0], 'imagenumber': fields[1]} 23 | 24 | def GetSSDVFileName(self, SelectedFileIndex=0): 25 | # Get list of jpg files 26 | date_file_list = [] 27 | for file in glob.glob('images/*.jpg'): 28 | stats = os.stat(file) 29 | lastmod_date = time.localtime(stats[8]) 30 | date_file_tuple = lastmod_date, file 31 | date_file_list.append(date_file_tuple) 32 | 33 | if len(date_file_list) == 0: 34 | return '' 35 | 36 | if SelectedFileIndex < 0: 37 | SelectedFileIndex = 0 38 | 39 | if SelectedFileIndex >= len(date_file_list): 40 | SelectedFileIndex = len(date_file_list)-1 41 | 42 | Index = len(date_file_list) - SelectedFileIndex - 1 43 | 44 | selection = sorted(date_file_list)[Index] 45 | 46 | return selection[1] 47 | 48 | def ShowFile(self, SelectedFileIndex, Always): 49 | # 0 means latest file; 1 onwards means 1st file (oldest), etc 50 | FileName = self.GetSSDVFileName(SelectedFileIndex) 51 | if FileName != '': 52 | ModificationDate = time.ctime(os.path.getmtime(FileName)) 53 | if Always or (FileName != self.DisplayedSSDVFileName) or (ModificationDate != self.SSDVModificationDate): 54 | # self.imageSSDV.set_from_file(FileName) 55 | pixbuf = GdkPixbuf.Pixbuf.new_from_file(FileName) 56 | pixbuf = pixbuf.scale_simple(506, 380, GdkPixbuf.InterpType.BILINEAR) 57 | self.imageSSDV.set_from_pixbuf(pixbuf) 58 | 59 | ImageInfo = self.ExtractImageInfoFromFileName(FileName) 60 | self.lblSSDVInfo.set_text('Callsign ' + ImageInfo['callsign'] + ', Image ' + ImageInfo['imagenumber']) 61 | 62 | self.DisplayedSSDVFileName = FileName 63 | self.SSDVModificationDate = ModificationDate 64 | -------------------------------------------------------------------------------- /skygate/ssdv.py: -------------------------------------------------------------------------------- 1 | import os, os.path 2 | import time 3 | import threading 4 | from time import sleep 5 | import glob 6 | 7 | class SSDV(object): 8 | 9 | def __init__(self): 10 | self.SSDVFolder = 'images' 11 | pass 12 | 13 | def decode_callsign(self, CallsignCode): 14 | callsign = '' 15 | 16 | if CallsignCode > 0xF423FFFF: 17 | return callsign 18 | 19 | while CallsignCode > 0: 20 | s = CallsignCode % 40 21 | if s == 0: 22 | callsign = callsign + '-' 23 | elif s < 11: 24 | callsign = callsign + chr(ord('0') + s - 1) 25 | elif s < 14: 26 | callsign = callsign + '-' 27 | else: 28 | callsign = callsign + chr(ord('A') + s - 14) 29 | 30 | CallsignCode = CallsignCode // 40 31 | 32 | return callsign 33 | 34 | def extract_header(self, packet): 35 | CallsignCode = packet[2] 36 | CallsignCode <<= 8 37 | CallsignCode |= packet[3] 38 | CallsignCode <<= 8 39 | CallsignCode |= packet[4] 40 | CallsignCode <<= 8 41 | CallsignCode |= packet[5] 42 | 43 | Callsign = self.decode_callsign(CallsignCode) 44 | 45 | ImageNumber = packet[6] 46 | 47 | PacketNumber = packet[7] * 256 + packet[8] 48 | 49 | return {'callsign': Callsign, 'imagenumber': ImageNumber, 'packetnumber': PacketNumber} 50 | 51 | def write_packet(self, Callsign, ImageNumber, packet): 52 | self.SSDVFolder = 'images' 53 | if not os.path.exists(self.SSDVFolder): 54 | os.makedirs(self.SSDVFolder) 55 | 56 | FileName = self.SSDVFolder + '/' + Callsign + '_' + str(ImageNumber) + '.bin' 57 | 58 | if os.path.isfile(FileName): 59 | # File exists - append or replace? 60 | if (time.time() - os.path.getmtime(FileName)) > 600: 61 | os.remove(FileName) 62 | 63 | file = open(FileName, mode='ab') 64 | file.write(packet) 65 | file.close() 66 | 67 | def __conversion_thread(self): 68 | if not os.path.exists(self.SSDVFolder): 69 | os.makedirs(self.SSDVFolder) 70 | while 1: 71 | self.ConvertSSDVFiles() 72 | sleep(10) 73 | 74 | def ConvertSSDVFile(self, SourceFileName, TargetFileName): 75 | print("Convert " + SourceFileName + " to " + TargetFileName) 76 | os.system("ssdv -d " + SourceFileName + " " + TargetFileName + " 2> /dev/null") 77 | 78 | def ConvertSSDVFiles(self): 79 | # Get list of jpg files 80 | for FileName in glob.glob(self.SSDVFolder + '/*.bin'): 81 | # stats = os.stat(file) 82 | # lastmod_date = time.localtime(stats[8]) 83 | ImageName = os.path.splitext(FileName)[0] + '.jpg' 84 | if not os.path.isfile(ImageName): 85 | # jpg file doesn't exist 86 | self.ConvertSSDVFile(FileName, ImageName) 87 | elif os.path.getmtime(FileName) > os.path.getmtime(ImageName): 88 | # SSDV file younger than jpg file 89 | self.ConvertSSDVFile(FileName, ImageName) 90 | 91 | def StartConversions(self): 92 | t = threading.Thread(target=self.__conversion_thread) 93 | t.daemon = True 94 | t.start() 95 | 96 | -------------------------------------------------------------------------------- /skygate/habitat.py: -------------------------------------------------------------------------------- 1 | import math 2 | import socket 3 | import json 4 | import time 5 | import threading 6 | import http.client 7 | import urllib.parse 8 | import urllib.request 9 | from base64 import b64encode 10 | from hashlib import sha256 11 | from datetime import datetime 12 | 13 | def ConvertTimeForHabitat(GPSTime): 14 | return GPSTime[0:2] + GPSTime[3:5] + GPSTime[6:8] 15 | 16 | 17 | class habitat(object): 18 | """ 19 | """ 20 | PortOpen = False 21 | 22 | def __init__(self, ChaseCarID = 'python', ChaseCarPeriod = 30, ChaseCarEnabled = False): 23 | self.CarPosition = None 24 | self.ChaseCarID = ChaseCarID + '_chase' 25 | self.ChaseCarPeriod = ChaseCarPeriod 26 | self.ChaseCarEnabled = ChaseCarEnabled 27 | 28 | def open(self): 29 | return True 30 | 31 | def run(self): 32 | t = threading.Thread(target=self.__car_thread) 33 | t.daemon = True 34 | t.start() 35 | 36 | def __car_thread(self): 37 | while 1: 38 | if self.CarPosition and self.ChaseCarEnabled and (self.ChaseCarPeriod > 0): 39 | url = 'http://spacenear.us/tracker/track.php' 40 | values = {'vehicle' : self.ChaseCarID, 41 | 'time' : ConvertTimeForHabitat(self.CarPosition['time']), 42 | 'lat' : self.CarPosition['lat'], 43 | 'lon' : self.CarPosition['lon'], 44 | 'alt' : self.CarPosition['alt'], 45 | 'pass' : 'aurora'} 46 | data = urllib.parse.urlencode(values) 47 | data = data.encode('utf-8') # data should be bytes 48 | req = urllib.request.Request(url, data) 49 | with urllib.request.urlopen(req) as response: 50 | the_page = response.read() # content = urllib.request.urlopen(url=url, data=data).read() 51 | # OurStatus['chasecarstatus'] = 3 52 | time.sleep(self.ChaseCarPeriod) 53 | else: 54 | time.sleep(1) 55 | 56 | def IsSentence(self, FirstByte): 57 | return chr(FirstByte) == '$' 58 | 59 | def IsSSDV(self, FirstByte): 60 | return (FirstByte & 0x7F) in [0x66, 0x67, 0x68, 0x69] 61 | 62 | def UploadTelemetry(self, Callsign, Sentence): 63 | sentence_b64 = b64encode(Sentence.encode()) 64 | 65 | date = datetime.utcnow().isoformat("T") + "Z" 66 | 67 | data = {"type": "payload_telemetry", "data": {"_raw": sentence_b64.decode()}, "receivers": {Callsign: {"time_created": date, "time_uploaded": date,},},} 68 | data = json.dumps(data) 69 | 70 | url = "http://habitat.habhub.org/habitat/_design/payload_telemetry/_update/add_listener/%s" % sha256(sentence_b64).hexdigest() 71 | req = urllib.request.Request(url) 72 | req.add_header('Content-Type', 'application/json') 73 | try: 74 | response = urllib.request.urlopen(req, data.encode()) 75 | except Exception as e: 76 | pass 77 | # return (False,"Failed to upload to Habitat: %s" % (str(e))) 78 | 79 | def UploadSSDV(self, Callsign, packet): 80 | encoded_packet = b64encode(packet) 81 | 82 | date = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ") 83 | 84 | data = {"type": "packet", "packet": encoded_packet.decode(), "encoding": "base64", "received": date, "receiver": Callsign} 85 | 86 | data = json.dumps(data) 87 | 88 | url = "http://ssdv.habhub.org/api/v0/packets" 89 | req = urllib.request.Request(url) 90 | req.add_header('Content-Type', 'application/json') 91 | try: 92 | response = urllib.request.urlopen(req, data.encode()) 93 | print("OK") 94 | except Exception as e: 95 | print("Failed") 96 | pass 97 | -------------------------------------------------------------------------------- /skygate/rtty.py: -------------------------------------------------------------------------------- 1 | from skygate.radio import * 2 | import threading 3 | import socket 4 | import time 5 | 6 | 7 | class RTTY(Radio): 8 | def __init__(self, Frequency=434.250, BaudRate=50): 9 | self.CallbackWhenReceived = None 10 | self.CallbackEveryByte = None 11 | self.SentenceCount = 0 12 | self.LatestSentence = None 13 | self.Frequency = Frequency 14 | self.BaudRate = BaudRate 15 | self.SetFrequency(Frequency) 16 | self.SetBaudRate(BaudRate) 17 | self.listening = False 18 | self.CurrentRTTY = '' 19 | 20 | def SetBaudRate(self, BaudRate): 21 | self.BaudRate = BaudRate 22 | # Send to dl-fldigi ? 23 | 24 | def SetFrequency(self, Frequency): 25 | print("RTTY Frequency = ", Frequency) 26 | self.Frequency = Frequency 27 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 28 | s.connect(("localhost", 6020)) 29 | buf = b'\x00' 30 | data = int(Frequency * 1000000) 31 | for i in range(4): 32 | buf = buf + bytes([data & 0xff]) 33 | data = data >> 8 34 | s.send(buf) 35 | s.close() 36 | 37 | def ChecksumOK(self, Line): 38 | return True 39 | 40 | def ProcessdlfldigiLine(self, line): 41 | # Process sentence 42 | # The $ and LF are already present 43 | # Check checksum/CRC and then save and do callback 44 | # $BUZZ,483,10:04:27,51.95022,-2.54435,00190,5*6856 45 | if self.ChecksumOK(line): 46 | if self.listening: 47 | self.SentenceCount += 1 48 | self.LatestSentence = line 49 | if self.CallbackWhenReceived: 50 | self.CallbackWhenReceived(line) 51 | 52 | def Processdlfldigi(self, s): 53 | while 1: 54 | reply = s.recv(1) 55 | if reply: 56 | value = reply[0] 57 | if value == 10: 58 | if self.CurrentRTTY != '': 59 | if self.CurrentRTTY[0] == '$': 60 | self.ProcessdlfldigiLine(self.CurrentRTTY) 61 | self.CurrentRTTY = '' 62 | elif (value >= 32) and (value < 127): 63 | temp = chr(reply[0]) 64 | self.CurrentRTTY = (self.CurrentRTTY + temp)[-256:] 65 | if (temp == '$' and self.CurrentRTTY[0] != '$'): 66 | self.CurrentRTTY = '$' 67 | 68 | if self.CallbackEveryByte: 69 | self.CallbackEveryByte(self.CurrentRTTY) 70 | else: 71 | time.sleep(1) 72 | 73 | def dodlfldigi(self, host, port): 74 | try: 75 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 76 | 77 | s.connect((host, port)) 78 | 79 | print("Connected to dl-fldigi") 80 | 81 | self.Processdlfldigi(s) 82 | 83 | s.close() 84 | except: 85 | print("Failed to connect to dl-fldigi") 86 | time.sleep(5) 87 | 88 | def listen_thread(self): 89 | host = "localhost" 90 | port = 7322 91 | print("listen_thread") 92 | 93 | while True: 94 | self.dodlfldigi(host, port) 95 | 96 | def listen_for_sentences(self, sentence_callback=None, byte_callback=None): 97 | self.CallbackWhenReceived = sentence_callback 98 | self.CallbackEveryByte = byte_callback 99 | 100 | if sentence_callback == None: 101 | # Stop listening 102 | self.listening = False 103 | elif not self.listening: 104 | # Start listening 105 | self.listening = True 106 | 107 | T = threading.Thread(target=self.listen_thread) 108 | T.daemon = True 109 | T.start() 110 | 111 | def CurrentRSSI(self): 112 | return self.__FixRSSI(self.__readRegister(REG_CURRENT_RSSI), 0) 113 | -------------------------------------------------------------------------------- /skygate/gateway.py: -------------------------------------------------------------------------------- 1 | from skygate.gps import * 2 | from skygate.lora import * 3 | from skygate.rtty import * 4 | from skygate.habitat import * 5 | from skygate.ssdv import * 6 | from time import sleep 7 | 8 | class gateway(object): 9 | """ 10 | Combines GPS, LoRa, RTTY with habitat uploads. 11 | Provides callbacks so that user can provide screen updates 12 | """ 13 | 14 | def __init__(self, 15 | CarID='Python', CarPeriod=30, CarEnabled=True, 16 | RadioCallsign='python', 17 | LoRaChannel=1, LoRaFrequency=434.450, LoRaMode=1, EnableLoRaUpload=True, StoreSSDVLocally=True, 18 | RTTYFrequency=434.250, 19 | OnNewGPSPosition=None, 20 | OnNewRTTYData=None, OnNewRTTYSentence=None, 21 | OnNewLoRaSentence=None, OnNewLoRaSSDV=None, OnLoRaFrequencyError=None, 22 | GPSDevice=None): 23 | 24 | self.RadioCallsign = RadioCallsign 25 | self.EnableLoRaUpload = EnableLoRaUpload 26 | self.StoreSSDVLocally = StoreSSDVLocally 27 | self.LatestLoRaSentence = None 28 | self.LatestLoRaPacketHeader = None 29 | self.LoRaFrequencyError = 0 30 | self.LatestRTTYSentence = None 31 | self.OnNewGPSPosition = OnNewGPSPosition 32 | self.OnNewRTTYData = OnNewRTTYData 33 | self.OnNewRTTYSentence = OnNewRTTYSentence 34 | self.OnNewLoRaSentence = OnNewLoRaSentence 35 | self.OnNewLoRaSSDV = OnNewLoRaSSDV 36 | self.OnLoRaFrequencyError = OnLoRaFrequencyError 37 | 38 | self.ssdv = SSDV() 39 | self.ssdv.StartConversions() 40 | 41 | self.gps = GPS(Device=GPSDevice) 42 | self.gps.open() 43 | self.gps.WhenNewPosition = self.__OnNewGPSPosition 44 | 45 | self.habitat = habitat(ChaseCarID=CarID, ChaseCarPeriod=CarPeriod, ChaseCarEnabled=CarEnabled) 46 | self.habitat.open() 47 | 48 | self.lora = LoRa(LoRaChannel, LoRaFrequency, LoRaMode) 49 | self.lora.listen_for_packets(self.__lora_packet) 50 | 51 | self.rtty = RTTY(Frequency=RTTYFrequency) 52 | self.rtty.listen_for_sentences(self.__rtty_sentence, self.__rtty_partial_sentence) 53 | 54 | def __OnNewGPSPosition(self, Position): 55 | self.habitat.CarPosition = Position 56 | if self.OnNewGPSPosition: 57 | self.OnNewGPSPosition(Position) 58 | 59 | def __lora_packet(self, result): 60 | packet = result['packet'] 61 | if packet == None: 62 | print("Failed packet") 63 | else: 64 | self.LoRaFrequencyError = result['freq_error'] 65 | if self.habitat.IsSentence(packet[0]): 66 | self.LatestLoRaSentence = ''.join(map(chr,bytes(packet).split(b'\x00')[0])) 67 | print("LoRa Sentence: " + self.LatestLoRaSentence, end='') 68 | if self.EnableLoRaUpload: 69 | self.habitat.UploadTelemetry(self.RadioCallsign, self.LatestLoRaSentence) 70 | if self.OnNewLoRaSentence: 71 | self.OnNewLoRaSentence(self.LatestLoRaSentence) 72 | elif self.habitat.IsSSDV(packet[0]): 73 | packet = bytearray([0x55] + packet) 74 | header = self.ssdv.extract_header(packet) 75 | print("LoRa SSDV Hdr:", header) 76 | if self.EnableLoRaUpload: 77 | self.habitat.UploadSSDV(self.RadioCallsign, packet) 78 | if self.StoreSSDVLocally: 79 | self.ssdv.write_packet(header['callsign'], header['imagenumber'], packet) 80 | self.LatestLoRaPacketHeader = header 81 | if self.OnNewLoRaSSDV: 82 | self.OnNewLoRaSSDV(header) 83 | else: 84 | print("Unknown packet ", packet[0]) 85 | 86 | if self.OnLoRaFrequencyError: 87 | self.OnLoRaFrequencyError(self.LoRaFrequencyError) 88 | 89 | 90 | 91 | def __rtty_sentence(self, sentence): 92 | self.LatestRTTYSentence = sentence 93 | print("RTTY Sentence: " + sentence) 94 | if self.OnNewRTTYSentence: 95 | self.OnNewRTTYSentence(sentence) 96 | 97 | def __rtty_partial_sentence(self, partial_sentence): 98 | if self.OnNewRTTYData: 99 | self.OnNewRTTYData(partial_sentence) 100 | 101 | 102 | def run(self): 103 | self.gps.run() 104 | self.habitat.run() 105 | 106 | # t = threading.Thread(target=self.__chase_thread) 107 | # t.daemon = True 108 | # t.start() 109 | -------------------------------------------------------------------------------- /skygate/habscreen.py: -------------------------------------------------------------------------------- 1 | import gi 2 | from skygate.misc import * 3 | from datetime import datetime 4 | 5 | class HABScreen(object): 6 | 7 | def __init__(self, builder): 8 | self.frame = builder.get_object("frameHAB") 9 | 10 | self.LatestLoRaValues = None 11 | self.LatestRTTYValues = None 12 | self.PreviousLoRaValues = None 13 | self.PreviousRTTYValues = None 14 | self.GPSPosition = None 15 | self.ShowBalloon = True 16 | 17 | self.MaximumAltitude = 0 18 | self.LastPositionAt = None 19 | 20 | self.btnAuto = builder.get_object("btnHABAuto") 21 | self.btnLoRa = builder.get_object("btnHABLoRa") 22 | self.btnRTTY = builder.get_object("btnHABRTTY") 23 | 24 | self.lblHABPayload = builder.get_object("lblHABPayload") 25 | self.lblHABRate = builder.get_object("lblHABRate") 26 | self.imgHABBalloon = builder.get_object("imgHABBalloon") 27 | self.imgHABChute = builder.get_object("imgHABChute") 28 | 29 | self.lblHABDistance = builder.get_object("lblHABDistance") 30 | self.imgHABBall = builder.get_object("imgHABBall") 31 | self.fixedHABCompass = builder.get_object("fixedHABCompass") 32 | 33 | self.lblHABTime = builder.get_object("lblHABTime") 34 | self.lblHABLatitude = builder.get_object("lblHABLatitude") 35 | self.lblHABLongitude = builder.get_object("lblHABLongitude") 36 | self.lblHABAltitude = builder.get_object("lblHABAltitude") 37 | self.lblHABMaxAltitude = builder.get_object("lblHABMaxAltitude") 38 | 39 | self.lblHABTimeSince = builder.get_object("lblHABTimeSince") 40 | 41 | def ShowTimeSinceData(self): 42 | if self.LastPositionAt: 43 | self.lblHABTimeSince.set_text(str(round((datetime.utcnow() - self.LastPositionAt).total_seconds())) + ' s') 44 | 45 | 46 | def RadioButtonsChanged(self): 47 | self.ShowLatestValues() 48 | 49 | def LatestHABValues(self): 50 | if self.btnAuto.get_active(): 51 | if self.LatestLoRaValues == None: 52 | return None 53 | if self.LatestRTTYValues == None: 54 | return self.LatestLoRaValues 55 | if self.LatestLoRaValues['time'] > self.LatestRTTYValues['time']: 56 | return self.LatestLoRaValues 57 | return self.LatestRTTYValues 58 | elif self.btnLoRa.get_active(): 59 | return self.LatestLoRaValues 60 | else: 61 | return self.LatestRTTYValues 62 | 63 | 64 | def ShowDistanceAndDirection(self, HABPosition, GPSPosition): 65 | if HABPosition and GPSPosition: 66 | # try: 67 | DistanceToHAB = CalculateDistance(HABPosition['lat'], HABPosition['lon'], GPSPosition['lat'], GPSPosition['lon']) 68 | DirectionToHAB = CalculateDirection(HABPosition['lat'], HABPosition['lon'], GPSPosition['lat'], GPSPosition['lon']) 69 | 70 | self.fixedHABCompass.move(self.imgHABBall, 150 + 131 * math.sin(math.radians(DirectionToHAB)), 134 + 131 * math.cos(math.radians(DirectionToHAB))) 71 | self.lblHABDistance.set_text("%.3f" % (DistanceToHAB/1000) + " km") 72 | #finally: 73 | # pass 74 | 75 | def ShowLatestValues(self): 76 | HABPosition = self.LatestHABValues() 77 | if HABPosition: 78 | self.lblHABPayload.set_text(HABPosition['payload']) 79 | self.lblHABRate.set_text("{0:.1f}".format(HABPosition['rate']) + 'm/s') 80 | self.lblHABTime.set_text(HABPosition['time'].strftime('%H:%M:%S')) 81 | self.lblHABLatitude.set_text("{0:.5f}".format(HABPosition['lat'])) 82 | self.lblHABLongitude.set_text("{0:.5f}".format(HABPosition['lon'])) 83 | self.lblHABAltitude.set_text(str(round(HABPosition['alt'])) + 'm') 84 | self.MaximumAltitude = max(self.MaximumAltitude, round(HABPosition['alt'])) 85 | self.lblHABMaxAltitude.set_text(str(self.MaximumAltitude) + 'm') 86 | self.ShowDistanceAndDirection(HABPosition, self.GPSPosition) 87 | if (HABPosition['rate'] >= -1) != self.ShowBalloon: 88 | self.ShowBalloon = not self.ShowBalloon 89 | # self.imgHABBalloon.set_visible(self.ShowBalloon) 90 | # self.imgHABChute.set_visible(not self.ShowBalloon) 91 | else: 92 | self.lblHABPayload.set_text('') 93 | self.lblHABRate.set_text('') 94 | self.lblHABTime.set_text('') 95 | self.lblHABLatitude.set_text('') 96 | self.lblHABLongitude.set_text('') 97 | self.lblHABAltitude.set_text('') 98 | self.fixedHABCompass.move(self.imgHABBall, 150, 134) 99 | self.lblHABDistance.set_text("") 100 | 101 | def CalculateRate(self, Latest, Previous): 102 | if Latest and Previous: 103 | if Latest['time'] > Previous['time']: 104 | return (Latest['alt'] - Previous['alt']) / (Latest['time'] - Previous['time']).seconds 105 | return 0 106 | 107 | def NewLoRaValues(self, LatestLoRaValues): 108 | self.PreviousLoRaValues = self.LatestLoRaValues 109 | LatestLoRaValues['rate'] = self.CalculateRate(self.LatestLoRaValues, self.PreviousLoRaValues) 110 | self.LatestLoRaValues = LatestLoRaValues 111 | self.LastPositionAt = datetime.utcnow() 112 | self.lblHABTimeSince.set_text('0 s') 113 | self.ShowLatestValues() 114 | 115 | def NewRTTYValues(self, LatestRTTYValues): 116 | self.PreviousRTTYValues = self.LatestRTTYValues 117 | LatestRTTYValues['rate'] = self.CalculateRate(self.LatestRTTYValues, self.PreviousRTTYValues) 118 | self.LatestRTTYValues = LatestRTTYValues 119 | self.LastPositionAt = datetime.utcnow() 120 | self.lblHABTimeSince.set_text('0 s') 121 | self.ShowLatestValues() 122 | 123 | def NewGPSPosition(self, GPSPosition): 124 | self.GPSPosition = GPSPosition 125 | self.ShowLatestValues() 126 | 127 | -------------------------------------------------------------------------------- /skygate/gps.py: -------------------------------------------------------------------------------- 1 | import math 2 | import serial 3 | import threading 4 | import time 5 | 6 | GPSPosition = {'time': '00:00:00', 'lat': 0.0, 'lon': 0.0, 'alt': 0, 'sats': 0, 'fixtype': 0} 7 | 8 | def GPSChecksumOK(Line): 9 | Count = len(Line) 10 | 11 | XOR = 0; 12 | 13 | for i in range(1, Count-4): 14 | c = ord(Line[i]) 15 | XOR ^= c 16 | 17 | return (Line[Count-4] == '*') and (Line[Count-3:Count-1] == hex(XOR)[2:4].upper()) 18 | 19 | 20 | def FixPosition(Position): 21 | Position = Position / 100 22 | 23 | MinutesSeconds = math.modf(Position) 24 | 25 | return MinutesSeconds[1] + MinutesSeconds[0] * 5 / 3 26 | 27 | 28 | def SendGPS(ser, Bytes): 29 | # print("SendGPS " + str(len(Bytes)) + " bytes") 30 | ser.write(Bytes) 31 | 32 | 33 | def ProcessLine(self, Line): 34 | global GPSPosition 35 | 36 | if GPSChecksumOK(Line): 37 | if Line[3:6] == "GGA": 38 | 39 | # $GNGGA,213511.00,5157.01416,N,00232.65975,W,1,12,0.64,149.8,M,48.6,M,,*55 40 | Fields = Line.split(',') 41 | 42 | # print(Fields) 43 | 44 | if Fields[1] != '': 45 | GPSPosition['time'] = Fields[1][0:2] + ':' + Fields[1][2:4] + ':' + Fields[1][4:6] 46 | if Fields[2] != '': 47 | GPSPosition['lat'] = FixPosition(float(Fields[2])) 48 | if Fields[3] == 'S': 49 | GPSPosition['lat'] = -GPSPosition['lat'] 50 | GPSPosition['lon'] = FixPosition(float(Fields[4])) 51 | if Fields[5] == 'W': 52 | GPSPosition['lon'] = -GPSPosition['lon'] 53 | GPSPosition['alt'] = float(Fields[9]) 54 | if GPSPosition['fixtype'] != int(Fields[6]): 55 | GPSPosition['fixtype'] = int(Fields[6]) 56 | if GPSPosition['fixtype'] > 0: 57 | if self._WhenLockGained: 58 | self._WhenLockGained() 59 | else: 60 | if self._WhenLockLost: 61 | self._WhenLockLost() 62 | GPSPosition['sats'] = int(Fields[7]) 63 | if self._WhenNewPosition: 64 | self._WhenNewPosition(GPSPosition) 65 | elif Line[3:6] == "RMC": 66 | # print("Disabling RMC") 67 | setRMC = bytearray([0xB5, 0x62, 0x06, 0x01, 0x08, 0x00, 0xF0, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x04, 0x40]) 68 | SendGPS(self.ser, setRMC) 69 | elif Line[3:6] == "GSV": 70 | # print("Disabling GSV") 71 | setGSV = bytearray([0xB5, 0x62, 0x06, 0x01, 0x08, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x03, 0x39]) 72 | SendGPS(self.ser, setGSV) 73 | elif Line[3:6] == "GLL": 74 | # print("Disabling GLL") 75 | setGLL = bytearray([0xB5, 0x62, 0x06, 0x01, 0x08, 0x00, 0xF0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x2B]) 76 | SendGPS(self.ser, setGLL) 77 | elif Line[3:6] == "GSA": 78 | # print("Disabling GSA") 79 | setGSA = bytearray([0xB5, 0x62, 0x06, 0x01, 0x08, 0x00, 0xF0, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x32]) 80 | SendGPS(self.ser, setGSA) 81 | elif Line[3:6] == "VTG": 82 | # print("Disabling VTG") 83 | setVTG = bytearray([0xB5, 0x62, 0x06, 0x01, 0x08, 0x00, 0xF0, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x05, 0x47]) 84 | SendGPS(self.ser, setVTG) 85 | else: 86 | pass 87 | # print("Unknown NMEA sentence " + Line) 88 | else: 89 | pass 90 | # print("Bad checksum") 91 | 92 | 93 | class GPS(object): 94 | """ 95 | Gets position from UBlox GPS receiver, using s/w i2c to GPIO pins 96 | Uses UBX commands; disables any incoming NMEA messages 97 | Puts GPS into flight mode as required 98 | Provides emulated GPS option 99 | Provides callbacks on change of state (e.g. lock attained, lock lost) 100 | """ 101 | PortOpen = False 102 | 103 | def __init__(self, Device='/dev/ttyAMA0'): 104 | self._WhenLockGained = None 105 | self._WhenLockLost = None 106 | self._WhenNewPosition = None 107 | self.IsOpen = False 108 | 109 | # Serial port /dev/ttyAMA0 110 | self.ser = serial.Serial() 111 | self.ser.baudrate = 9600 112 | self.ser.stopbits = 1 113 | self.ser.bytesize = 8 114 | self.ser.port = Device 115 | 116 | def __gps_thread(self): 117 | Line = '' 118 | 119 | while True: 120 | if self.IsOpen: 121 | Byte = self.ser.read(1) 122 | 123 | Character = chr(Byte[0]) 124 | 125 | if Character == '$': 126 | Line = Character 127 | elif len(Line) > 90: 128 | Line = '' 129 | elif (Line != '') and (Character != '\r'): 130 | Line = Line + Character 131 | if Character == '\n': 132 | ProcessLine(self, Line) 133 | 134 | Line = '' 135 | time.sleep(0.1) 136 | else: 137 | time.sleep(1) 138 | 139 | def open(self): 140 | # Open connection to GPS 141 | try: 142 | self.ser.open() 143 | self.IsOpen = True 144 | except: 145 | self.IsOpen = False 146 | 147 | def Position(self): 148 | return GPSPosition 149 | 150 | def SetDevice(self, device): 151 | if device != self.ser.port: 152 | self.ser.close() 153 | self.ser.port = device 154 | try: 155 | self.ser.open() 156 | self.IsOpen = True 157 | except: 158 | self.IsOpen = False 159 | 160 | @property 161 | def WhenLockGained(self): 162 | return self._WhenLockGained 163 | 164 | @WhenLockGained.setter 165 | def WhenLockGained(self, value): 166 | self._WhenLockGained = value 167 | 168 | @property 169 | def WhenLockLost(self): 170 | return self._WhenLockLost 171 | 172 | @WhenLockLost.setter 173 | def WhenLockGained(self, value): 174 | self._WhenLockLost = value 175 | 176 | @property 177 | def WhenNewPosition(self): 178 | return self._WhenNewPosition 179 | 180 | @WhenLockGained.setter 181 | def WhenNewPosition(self, value): 182 | self._WhenNewPosition = value 183 | 184 | def run(self): 185 | t = threading.Thread(target=self.__gps_thread) 186 | t.daemon = True 187 | t.start() 188 | -------------------------------------------------------------------------------- /skygate/lora.py: -------------------------------------------------------------------------------- 1 | from gpiozero import InputDevice 2 | import threading 3 | from skygate.radio import * 4 | import spidev 5 | import time 6 | 7 | REG_FIFO = 0x00 8 | REG_FIFO_ADDR_PTR = 0x0D 9 | REG_FIFO_TX_BASE_AD = 0x0E 10 | REG_FIFO_RX_BASE_AD = 0x0F 11 | REG_RX_NB_BYTES = 0x13 12 | REG_OPMODE = 0x01 13 | REG_FIFO_RX_CURRENT_ADDR = 0x10 14 | REG_IRQ_FLAGS = 0x12 15 | REG_PACKET_SNR = 0x19 16 | REG_PACKET_RSSI = 0x1A 17 | REG_CURRENT_RSSI = 0x1B 18 | REG_DIO_MAPPING_1 = 0x40 19 | REG_DIO_MAPPING_2 = 0x41 20 | REG_MODEM_CONFIG = 0x1D 21 | REG_MODEM_CONFIG2 = 0x1E 22 | REG_MODEM_CONFIG3 = 0x26 23 | REG_PAYLOAD_LENGTH = 0x22 24 | REG_IRQ_FLAGS_MASK = 0x11 25 | REG_HOP_PERIOD = 0x24 26 | REG_FREQ_ERROR = 0x28 27 | REG_DETECT_OPT = 0x31 28 | REG_DETECTION_THRESHOLD = 0x37 29 | 30 | # MODES 31 | RF98_MODE_RX_CONTINUOUS = 0x85 32 | RF98_MODE_TX = 0x83 33 | RF98_MODE_SLEEP = 0x80 34 | RF98_MODE_STANDBY = 0x81 35 | 36 | # Modem Config 1 37 | EXPLICIT_MODE = 0x00 38 | IMPLICIT_MODE = 0x01 39 | 40 | ERROR_CODING_4_5 = 0x02 41 | ERROR_CODING_4_6 = 0x04 42 | ERROR_CODING_4_7 = 0x06 43 | ERROR_CODING_4_8 = 0x08 44 | 45 | BANDWIDTH_7K8 = 0x00 46 | BANDWIDTH_10K4 = 0x10 47 | BANDWIDTH_15K6 = 0x20 48 | BANDWIDTH_20K8 = 0x30 49 | BANDWIDTH_31K25 = 0x40 50 | BANDWIDTH_41K7 = 0x50 51 | BANDWIDTH_62K5 = 0x60 52 | BANDWIDTH_125K = 0x70 53 | BANDWIDTH_250K = 0x80 54 | BANDWIDTH_500K = 0x90 55 | 56 | # Modem Config 2 57 | 58 | SPREADING_6 = 0x60 59 | SPREADING_7 = 0x70 60 | SPREADING_8 = 0x80 61 | SPREADING_9 = 0x90 62 | SPREADING_10 = 0xA0 63 | SPREADING_11 = 0xB0 64 | SPREADING_12 = 0xC0 65 | 66 | CRC_OFF = 0x00 67 | CRC_ON = 0x04 68 | 69 | # POWER AMPLIFIER CONFIG 70 | REG_PA_CONFIG = 0x09 71 | PA_MAX_BOOST = 0x8F 72 | PA_LOW_BOOST = 0x81 73 | PA_MED_BOOST = 0x8A 74 | PA_MAX_UK = 0x88 75 | PA_OFF_BOOST = 0x00 76 | RFO_MIN = 0x00 77 | 78 | # LOW NOISE AMPLIFIER 79 | REG_LNA = 0x0C 80 | LNA_MAX_GAIN = 0x23 # 0010 0011 81 | LNA_OFF_GAIN = 0x00 82 | LNA_LOW_GAIN = 0xC0 # 1100 0000 83 | 84 | class LoRa(Radio): 85 | """ 86 | Radio - LoRa. Single channel - if you want to use more channels then create more objects. 87 | """ 88 | 89 | def __init__(self, Channel=0, Frequency=434.250, Mode=1, DIO0=0, DIO5=0): 90 | self.SentenceCount = 0 91 | self.ImagePacketCount = 0 92 | self.sending = False 93 | self.listening = False 94 | self.CallbackWhenSent = None 95 | self.CallbackWhenReceived = None 96 | self.CurrentBandwidth = 20.8 97 | self.FreqError = 0 98 | 99 | if DIO0 == 0: 100 | if Channel == 1: 101 | DIO0 = 16 102 | else: 103 | DIO0 = 25 104 | 105 | if DIO5 == 0: 106 | if Channel == 1: 107 | DIO5 = 12 108 | else: 109 | DIO5 = 24 110 | 111 | self.Channel = Channel 112 | self.Frequency = Frequency 113 | self.Mode = Mode 114 | self.DIO0 = InputDevice(DIO0) 115 | self.DIO5 = InputDevice(DIO5) 116 | self.currentMode = 0x81; 117 | self.Power = PA_MAX_UK 118 | self.spi = spidev.SpiDev() 119 | self.spi.open(0, Channel) 120 | self.spi.max_speed_hz = 976000 121 | self.__writeRegister(REG_DIO_MAPPING_2, 0x00) 122 | self.__SetLoRaFrequency(Frequency) 123 | self.__SetStandardLoRaParameters(Mode) 124 | 125 | def __readRegister(self, register): 126 | data = [register & 0x7F, 0] 127 | result = self.spi.xfer(data) 128 | return result[1] 129 | 130 | def __writeRegister(self, register, value): 131 | self.spi.xfer([register | 0x80, value]) 132 | 133 | def __setMode(self, newMode): 134 | if newMode != self.currentMode: 135 | if newMode == RF98_MODE_TX: 136 | # TURN LNA OFF FOR TRANSMIT 137 | self.__writeRegister(REG_LNA, LNA_OFF_GAIN) 138 | 139 | # Set 10mW 140 | self.__writeRegister(REG_PA_CONFIG, self.Power) 141 | elif newMode == RF98_MODE_RX_CONTINUOUS: 142 | # PA Off 143 | self.__writeRegister(REG_PA_CONFIG, PA_OFF_BOOST) 144 | 145 | # Max LNA Gain 146 | self.__writeRegister(REG_LNA, LNA_MAX_GAIN) 147 | 148 | self.__writeRegister(REG_OPMODE, newMode) 149 | self.currentMode = newMode 150 | 151 | if newMode != RF98_MODE_SLEEP: 152 | #while not self.DIO5.is_active: 153 | # pass 154 | # time.sleep(0.1) 155 | pass 156 | 157 | def __SetLoRaFrequency(self, Frequency): 158 | self.Frequency = Frequency 159 | self.__setMode(RF98_MODE_STANDBY) 160 | self.__setMode(RF98_MODE_SLEEP) 161 | self.__writeRegister(REG_OPMODE, 0x80); 162 | #self.__setMode(RF98_MODE_SLEEP) 163 | self.__setMode(RF98_MODE_STANDBY) 164 | 165 | FrequencyValue = int((Frequency * 7110656) / 434) 166 | 167 | self.__writeRegister(0x06, (FrequencyValue >> 16) & 0xFF) 168 | self.__writeRegister(0x07, (FrequencyValue >> 8) & 0xFF) 169 | self.__writeRegister(0x08, FrequencyValue & 0xFF) 170 | 171 | def SetLoRaFrequency(self, Frequency): 172 | self.Frequency = Frequency 173 | self.__SetLoRaFrequency(Frequency) 174 | self.__startReceiving() 175 | 176 | def SetLoRaParameters(self, ImplicitOrExplicit, ErrorCoding, Bandwidth, SpreadingFactor, LowDataRateOptimize): 177 | self.__writeRegister(REG_MODEM_CONFIG, ImplicitOrExplicit | ErrorCoding | Bandwidth) 178 | self.__writeRegister(REG_MODEM_CONFIG2, SpreadingFactor | CRC_ON) 179 | self.__writeRegister(REG_MODEM_CONFIG3, 0x04 | (0x08 if LowDataRateOptimize else 0)) 180 | self.__writeRegister(REG_DETECT_OPT, (self.__readRegister(REG_DETECT_OPT) & 0xF8) | (0x05 if (SpreadingFactor == SPREADING_6) else 0x03)) 181 | self.__writeRegister(REG_DETECTION_THRESHOLD, 0x0C if (SpreadingFactor == SPREADING_6) else 0x0A) 182 | 183 | self.PayloadLength = 255 if (ImplicitOrExplicit == IMPLICIT_MODE) else 0 184 | 185 | self.__writeRegister(REG_PAYLOAD_LENGTH, self.PayloadLength) 186 | self.__writeRegister(REG_RX_NB_BYTES, self.PayloadLength) 187 | 188 | def __SetStandardLoRaParameters(self, Mode): 189 | if Mode == 0: 190 | self.SetLoRaParameters(EXPLICIT_MODE, ERROR_CODING_4_8, BANDWIDTH_20K8, SPREADING_11, True) 191 | elif Mode == 1: 192 | self.SetLoRaParameters(IMPLICIT_MODE, ERROR_CODING_4_5, BANDWIDTH_20K8, SPREADING_6, False) 193 | elif Mode == 2: 194 | self.SetLoRaParameters(EXPLICIT_MODE, ERROR_CODING_4_8, BANDWIDTH_62K5, SPREADING_8, False) 195 | 196 | def SetStandardLoRaParameters(self, Mode): 197 | self.Mode = Mode 198 | self.__SetStandardLoRaParameters(Mode) 199 | self.__startReceiving() 200 | 201 | def send_thread(self): 202 | # wait for DIO0 203 | while not self.DIO0.is_active: 204 | time.sleep(0.01) 205 | self.sending = False 206 | if self.CallbackWhenSent: 207 | self.CallbackWhenSent() 208 | 209 | def is_sending(self): 210 | return self.sending 211 | 212 | def send_packet(self, packet, callback=None): 213 | self.CallbackWhenSent = callback 214 | self.sending = True 215 | 216 | self.__setMode(RF98_MODE_STANDBY) 217 | 218 | # map DIO0 to TxDone 219 | self.__writeRegister(REG_DIO_MAPPING_1, 0x40) 220 | 221 | self.__writeRegister(REG_FIFO_TX_BASE_AD, 0x00) 222 | self.__writeRegister(REG_FIFO_ADDR_PTR, 0x00) 223 | 224 | data = [REG_FIFO | 0x80] + packet + [0] 225 | self.spi.xfer(data) 226 | 227 | self.__writeRegister(REG_PAYLOAD_LENGTH, self.PayloadLength if self.PayloadLength else len(packet)) 228 | 229 | self.__setMode(RF98_MODE_TX); 230 | 231 | t = threading.Thread(target=self.send_thread) 232 | t.daemon = True 233 | t.start() 234 | 235 | def send_text(self, sentence, callback=None): 236 | self.send_packet(list(sentence.encode()), callback) 237 | 238 | def __FixRSSI(self, RawRSSI, SNR): 239 | if self.Frequency > 525: 240 | # HF port (band 1) 241 | RSSI = RawRSSI - 157 242 | else: 243 | # LF port (Bands 2/3) 244 | RSSI = RawRSSI - 164 245 | 246 | if SNR < 0: 247 | RSSI += SNR // 4 248 | 249 | return RSSI 250 | 251 | def __PacketSNR(self): 252 | SNR = self.__readRegister(REG_PACKET_SNR) 253 | SNR //= 4; 254 | 255 | return SNR 256 | 257 | 258 | def __PacketRSSI(self): 259 | SNR = self.__PacketSNR() 260 | 261 | return self.__FixRSSI(self.__readRegister(REG_PACKET_RSSI), SNR) 262 | 263 | def __startReceiving(self): 264 | self.__writeRegister(REG_DIO_MAPPING_1, 0x00) # 00 00 00 00 maps DIO0 to RxDone 265 | 266 | self.__writeRegister(REG_PAYLOAD_LENGTH, 255) 267 | self.__writeRegister(REG_RX_NB_BYTES, 255) 268 | 269 | self.__writeRegister(REG_FIFO_RX_BASE_AD, 0 ) 270 | self.__writeRegister(REG_FIFO_ADDR_PTR, 0 ) 271 | 272 | # Setup Receive Continous Mode 273 | self.__setMode(RF98_MODE_RX_CONTINUOUS) 274 | 275 | def __FrequencyError(self): 276 | Temp = self.__readRegister(REG_FREQ_ERROR) & 7 277 | Temp <<= 8 278 | Temp += self.__readRegister(REG_FREQ_ERROR + 1) 279 | Temp <<= 8 280 | Temp += self.__readRegister(REG_FREQ_ERROR + 2) 281 | 282 | if (self.__readRegister(REG_FREQ_ERROR) & 8): 283 | Temp -= 524288 284 | 285 | return -(Temp * (1 << 24) / 32000000.0) * (self.CurrentBandwidth / 500.0) 286 | 287 | def __receiveMessage(self): 288 | Packet = None 289 | 290 | Status = self.__readRegister(REG_IRQ_FLAGS) 291 | # print ("Status=" + str(Status)) 292 | 293 | # clear the rxDone flag 294 | self.__writeRegister(REG_IRQ_FLAGS, 0x40) 295 | 296 | # check for payload crc issues (0x20 is the bit we are looking for 297 | if (Status & 0x20 ) == 0x20: 298 | print("CRC Failure, RSSI " + str(self.__PacketRSSI())) 299 | 300 | # reset the crc flags 301 | self.__writeRegister(REG_IRQ_FLAGS, 0x20) 302 | # self.BadCRCCount++ 303 | else: 304 | currentAddr = self.__readRegister(REG_FIFO_RX_CURRENT_ADDR) 305 | Bytes = self.__readRegister(REG_RX_NB_BYTES) 306 | 307 | self.FreqError = self.__FrequencyError() / 1000 308 | 309 | self.__writeRegister(REG_FIFO_ADDR_PTR, currentAddr) 310 | 311 | Request = [REG_FIFO] + [0] * Bytes 312 | Packet = self.spi.xfer(Request)[1:] 313 | 314 | # if Config.LoRaDevices[Channel].AFC && (fabs( FreqError ) > 0.5) 315 | # { 316 | # if (Config.LoRaDevices[Channel].MaxAFCStep > 0) 317 | # { 318 | # // Limit step to MaxAFCStep 319 | # if (FreqError > Config.LoRaDevices[Channel].MaxAFCStep) 320 | # { 321 | # FreqError = Config.LoRaDevices[Channel].MaxAFCStep; 322 | # } 323 | # else if (FreqError < -Config.LoRaDevices[Channel].MaxAFCStep) 324 | # { 325 | # FreqError = -Config.LoRaDevices[Channel].MaxAFCStep; 326 | # } 327 | # } 328 | # ReTune( Channel, FreqError / 1000 ); 329 | # } 330 | 331 | # Clear all flags 332 | self.__writeRegister(REG_IRQ_FLAGS, 0xFF) 333 | 334 | return {'packet': Packet, 'freq_error': self.FreqError} 335 | 336 | def listen_thread(self): 337 | while self.listening: 338 | # wait for DIO0 339 | while self.listening and not self.DIO0.is_active: 340 | time.sleep(0.01) 341 | 342 | if self.listening: 343 | if self.CallbackWhenReceived: 344 | packet = self.__receiveMessage() 345 | self.CallbackWhenReceived(packet) 346 | 347 | def listen_for_packets(self, callback=None): 348 | self.CallbackWhenReceived = callback 349 | 350 | if callback == None: 351 | # Stop listening 352 | self.listening = False 353 | elif not self.listening: 354 | # Start listening 355 | self.listening = True 356 | 357 | self.__startReceiving() 358 | 359 | T = threading.Thread(target=self.listen_thread) 360 | T.daemon = True 361 | T.start() 362 | 363 | def CurrentRSSI(self): 364 | return self.__FixRSSI(self.__readRegister(REG_CURRENT_RSSI), 0) 365 | -------------------------------------------------------------------------------- /skygate/skygate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import gi 4 | gi.require_version('Gtk', '3.0') 5 | from gi.repository import Gtk, GObject, Pango, Gdk, GLib, GdkPixbuf 6 | from pkg_resources import resource_filename 7 | import skygate.misc 8 | from skygate.gateway import * 9 | from skygate.habscreen import * 10 | from skygate.lorascreen import * 11 | from skygate.rttyscreen import * 12 | from skygate.gpsscreen import * 13 | from skygate.ssdvscreen import * 14 | import configparser 15 | import datetime 16 | import string 17 | 18 | 19 | class SkyGate: 20 | def __init__(self): 21 | self.CurrentWindow = None 22 | self.CurrentParent = None 23 | self.LatestLoRaSentence = None 24 | self.LatestLoRaPacketHeader = None 25 | self.LatestLoRaValues = None 26 | self.LatestRTTYSentence = None 27 | self.LatestRTTYValues = None 28 | self.LatestHABValues = None 29 | self.SelectedSSDVIndex = 0 30 | self.LoRaFrequencyError = 0 31 | self.CurrentGPSPosition = None 32 | self.SettingsEditBox = None 33 | 34 | self.GladeFile=resource_filename('skygate', 35 | 'skygate.glade') 36 | 37 | self.builder = Gtk.Builder() 38 | self.builder.add_from_file(self.GladeFile) 39 | self.builder.connect_signals(self) 40 | 41 | self.windowMain = self.builder.get_object("windowMain") 42 | self.frameMain = self.builder.get_object("frameMain") 43 | self.frameDefault = self.builder.get_object("frameDefault") 44 | 45 | # Stop windows from getting too large and expanding main window 46 | # self.windowMain.set_resizable(False) 47 | 48 | # Selectable screens 49 | self.HABScreen = HABScreen(self.builder) 50 | self.LoRaScreen = LoRaScreen(self.builder) 51 | self.RTTYScreen = RTTYScreen(self.builder) 52 | self.GPSScreen = GPSScreen(self.builder) 53 | self.SSDVScreen = SSDVScreen(self.builder) 54 | self.frameSettings = self.builder.get_object("frameSettings") 55 | 56 | # Show default window 57 | self.SetNewWindow(self.frameDefault) 58 | 59 | # Main screen widgets - upper status bar 60 | # self.lblLoRaPayload = self.builder.get_object("lblLoRaPayload") 61 | # self.lblLoRaTime = self.builder.get_object("lblLoRaTime") 62 | # self.lblLoRaLat = self.builder.get_object("lblLoRaLat") 63 | # self.lblLoRaLon = self.builder.get_object("lblLoRaLon") 64 | # self.lblLoRaAlt = self.builder.get_object("lblLoRaAlt") 65 | 66 | # Main screen widgets - lower status bar 67 | # self.lblTime = self.builder.get_object("lblTime") 68 | # self.lblLat = self.builder.get_object("lblLat") 69 | # self.lblLon = self.builder.get_object("lblLon") 70 | # self.lblAlt = self.builder.get_object("lblAlt") 71 | # self.lblSats = self.builder.get_object("lblSats") 72 | 73 | # Settings screen 74 | # This presently is done as-required 75 | 76 | # Size main window to match the official Pi display, if we can 77 | ScreenInfo = Gdk.Screen.get_default() 78 | ScreenWidth = ScreenInfo.get_width() 79 | ScreenHeight = ScreenInfo.get_height() 80 | # Set to size of official display, or available space, whichever is smaller 81 | self.windowMain.resize(min(ScreenWidth, 800), min(ScreenHeight, 414)) 82 | # If this is the official touchscreen or smaller, position at top-left 83 | if (ScreenWidth <= 800) or (ScreenHeight <= 480): 84 | self.windowMain.move(0,0) 85 | else: 86 | self.windowMain.move(100,100) 87 | 88 | self.windowMain.show_all() 89 | 90 | self.PositionDlFldigi() 91 | 92 | # Read config file 93 | self.ConfigFileName = os.path.join(os.environ['HOME'], '.config/skygate.ini') 94 | self.LoadSettingsFromFile(self.ConfigFileName) 95 | 96 | # Show current settings 97 | self.LoRaScreen.ShowLoRaFrequencyAndMode(self.LoRaFrequency, self.LoRaMode) 98 | self.RTTYScreen.ShowRTTYFrequency(self.RTTYFrequency) 99 | 100 | # Timer for updating UI 101 | GObject.timeout_add_seconds(1, self.ssdv_update_timer) 102 | 103 | # Gateway 104 | self.gateway = gateway(CarID=self.ChaseCarID, CarPeriod=30, CarEnabled=self.ChaseCarEnabled, 105 | RadioCallsign=self.ReceiverCallsign, 106 | LoRaChannel=1, LoRaFrequency=self.LoRaFrequency, LoRaMode=self.LoRaMode, EnableLoRaUpload=self.EnableLoRaUpload, 107 | RTTYFrequency=self.RTTYFrequency, 108 | OnNewGPSPosition=self._NewGPSPosition, 109 | OnNewRTTYData=self._NewRTTYData, OnNewRTTYSentence=self._NewRTTYSentence, 110 | OnNewLoRaSentence=self._NewLoRaSentence, OnNewLoRaSSDV=self._NewLoRaSSDV, OnLoRaFrequencyError=self._LoRaFrequencyError, 111 | GPSDevice=self.GPSDevice) 112 | if self.gateway.gps.IsOpen: 113 | self.GPSScreen.ShowPortStatus("OK") 114 | else: 115 | self.GPSScreen.ShowPortStatus("Failed to open GPS device " + self.GPSDevice) 116 | 117 | self.gateway.run() 118 | 119 | def AdjustLoRaFrequency(self, Delta): 120 | # Adjust and set frequency 121 | self.LoRaFrequency = self.LoRaFrequency + Delta 122 | self.gateway.lora.SetLoRaFrequency(self.LoRaFrequency) 123 | 124 | # Update screens 125 | self.LoRaScreen.ShowLoRaFrequencyAndMode(self.LoRaFrequency, self.LoRaMode) 126 | 127 | def AdjustRTTYFrequency(self, Delta): 128 | # Adjust and set frequency 129 | self.RTTYFrequency = self.RTTYFrequency + Delta 130 | self.gateway.rtty.SetFrequency(self.RTTYFrequency) 131 | 132 | # Update screens 133 | self.RTTYScreen.ShowRTTYFrequency(self.RTTYFrequency) 134 | 135 | # Main window signals 136 | def onDeleteWindow(self, *args): 137 | self.ShowDlFldigi(False) 138 | Gtk.main_quit(*args) 139 | 140 | def on_windowMain_check_resize(self, window): 141 | pass 142 | 143 | # Main window button signals 144 | def on_buttonHAB_clicked(self, button): 145 | self.SetNewWindow(self.HABScreen.frame) 146 | 147 | def on_buttonLoRa_clicked(self, button): 148 | self.SetNewWindow(self.LoRaScreen.frame) 149 | 150 | def on_buttonRTTY_clicked(self, button): 151 | self.SetNewWindow(self.RTTYScreen.frame) 152 | 153 | def on_buttonGPS_clicked(self, button): 154 | self.SetNewWindow(self.GPSScreen.frame) 155 | 156 | def on_buttonSSDV_clicked(self, button): 157 | self.SetNewWindow(self.SSDVScreen.frame) 158 | self.SelectedSSDVIndex = 0 159 | self.SSDVScreen.ShowFile(self.SelectedSSDVIndex, True) 160 | 161 | def on_buttonSettings_clicked(self, button): 162 | self.PopulateSettingsScreen() 163 | self.SetKeyboardCase(False) 164 | self.SetNewWindow(self.frameSettings) 165 | 166 | def on_AutoScroll(self, *args): 167 | ScrolledWindow = args[0] 168 | adj = ScrolledWindow.get_vadjustment() 169 | adj.set_value(adj.get_upper() - adj.get_page_size()) 170 | 171 | # HAB window signals 172 | def on_btnHABAuto_toggled(self, button): 173 | self.HABScreen.RadioButtonsChanged() 174 | 175 | # LoRa window signals 176 | def on_btnLoRaDown_clicked(self, button): 177 | self.AdjustLoRaFrequency(-0.001) 178 | 179 | def on_btnLoRaUp_clicked(self, button): 180 | self.AdjustLoRaFrequency(0.001) 181 | 182 | # RTTY window signals 183 | def on_btnRTTYDown_clicked(self, button): 184 | self.AdjustRTTYFrequency(-0.0005) 185 | 186 | def on_btnRTTYUp_clicked(self, button): 187 | self.AdjustRTTYFrequency(0.0005) 188 | 189 | def on_btnRTTYdlfldigi_clicked(self, button): 190 | self.PositionDlFldigi() 191 | self.ShowDlFldigi(True) 192 | 193 | def on_textRTTY_button_press_event(self, thing1, thing2): 194 | self.ShowDlFldigi(False) 195 | # GPS window signals 196 | 197 | # (none) 198 | 199 | # SSDV window signals 200 | def on_btnSSDVPrevious_clicked(self, button): 201 | self.SelectedSSDVIndex += 1 202 | self.SSDVScreen.ShowFile(self.SelectedSSDVIndex, True) 203 | 204 | def on_btnSSDVNext_clicked(self, button): 205 | if self.SelectedSSDVIndex > 0: 206 | self.SelectedSSDVIndex -= 1 207 | self.SSDVScreen.ShowFile(self.SelectedSSDVIndex, True) 208 | 209 | # Settings window signals 210 | def on_btnSettingsSave_clicked(self, button): 211 | self.LoadFromSettingsScreen() 212 | self.ApplySettings() 213 | self.SaveSettingsToFile(self.ConfigFileName) 214 | 215 | def on_btnSettingsCancel_clicked(self, button): 216 | self.PopulateSettingsScreen() 217 | 218 | def on_textSettings_focus(self, textbox, event): 219 | self.SettingsEditBox = textbox 220 | 221 | def on_button1_clicked(self, button): 222 | if self.SettingsEditBox: 223 | if button.get_label() == '<-': 224 | self.SettingsEditBox.set_text(self.SettingsEditBox.get_text()[:-1]) 225 | else: 226 | self.SettingsEditBox.set_text(self.SettingsEditBox.get_text() + button.get_label()) 227 | 228 | def on_btnShift_clicked(self, button): 229 | ToUpper = self.builder.get_object("buttonQ").get_label() == 'q' 230 | self.SetKeyboardCase(ToUpper) 231 | 232 | # Gtk UI updaters 233 | def _UpdateGPSPosition(self): 234 | Position = self.CurrentGPSPosition 235 | 236 | # GPS screen 237 | self.GPSScreen.ShowPosition(Position) 238 | 239 | # HAB screen 240 | self.HABScreen.NewGPSPosition(Position) 241 | 242 | return False # So we don't get called again, until there's a new GPS position 243 | 244 | def PositionDlFldigi(self): 245 | os.system('wmctrl -r "dl-fldigi - waterfall-only mode" -e 0,' + str(self.windowMain.get_position()[0]+0) + ',' + str(self.windowMain.get_position()[1]+150) + ',700,173') 246 | 247 | def ShowDlFldigi(self, Show): 248 | os.system('wmctrl -r "dl-fldigi - waterfall-only mode" -b add,' + ('above' if Show else 'below')) 249 | if Show: 250 | os.system('wmctrl -a "dl-fldigi - waterfall-only mode"') 251 | 252 | def _UpdateCurrentRTTY(self): 253 | self.RTTYScreen.ShowCurrentRTTY(self.CurrentRTTY[-80:]) 254 | return False 255 | 256 | def _UpdateRTTYSentence(self): 257 | # Update HAB screen 258 | self.HABScreen.NewRTTYValues(self.LatestRTTYValues) 259 | 260 | # Update RTTY screen 261 | self.RTTYScreen.AppendLine(str(self.LatestRTTYSentence + '\n')) 262 | 263 | return False 264 | 265 | def _UpdateLoRaSentence(self): 266 | # Update HAB screen 267 | self.HABScreen.NewLoRaValues(self.LatestLoRaValues) 268 | 269 | # Update LoRa screen 270 | self.LoRaScreen.AppendLine(str(self.LatestLoRaSentence)) 271 | 272 | return False 273 | 274 | def _UpdateLoRaSSDV(self): 275 | # Update LoRa screen 276 | self.LoRaScreen.AppendLine('SSDV packet, payload ID: ' + self.LatestLoRaPacketHeader['callsign'] + ', image #: ' + str(self.LatestLoRaPacketHeader['imagenumber']) + ', packet #: ' + str(self.LatestLoRaPacketHeader['packetnumber'])) 277 | 278 | return False 279 | 280 | def _UpdateLoRaFrequencyError(self): 281 | # LoRa Frequency Error 282 | self.LoRaScreen.ShowFrequencyError(self.LoRaFrequencyError) 283 | 284 | return False 285 | 286 | # Callbacks 287 | 288 | def _NewGPSPosition(self, Position): 289 | self.CurrentGPSPosition = Position 290 | GLib.idle_add(self._UpdateGPSPosition) 291 | 292 | def _NewRTTYData(self, CurrentRTTY): 293 | self.CurrentRTTY = CurrentRTTY 294 | GLib.idle_add(self._UpdateCurrentRTTY) 295 | 296 | def _NewRTTYSentence(self, Sentence): 297 | self.LatestRTTYSentence = Sentence 298 | self.LatestRTTYValues = self.DecodeSentence(Sentence) 299 | if self.LatestRTTYValues: 300 | self.LatestHABValues = self.LatestRTTYValues 301 | GLib.idle_add(self._UpdateRTTYSentence) 302 | 303 | def _NewLoRaSentence(self, Sentence): 304 | self.LatestLoRaSentence = Sentence 305 | self.LatestLoRaValues = self.DecodeSentence(Sentence) 306 | 307 | if self.LatestLoRaValues: 308 | self.LatestHABValues = self.LatestLoRaValues 309 | GLib.idle_add(self._UpdateLoRaSentence) 310 | 311 | def _NewLoRaSSDV(self, header): 312 | self.LatestLoRaPacketHeader = header 313 | GLib.idle_add(self._UpdateLoRaSSDV) 314 | 315 | def _LoRaFrequencyError(self, FrequencyError): 316 | self.LoRaFrequencyError = FrequencyError 317 | GLib.idle_add(self._UpdateLoRaFrequencyError) 318 | 319 | # Functions 320 | 321 | def SetNewWindow(self, SomeWindow): 322 | # Hide dl-fldigi in case it's been brought up on top of us 323 | self.ShowDlFldigi(False) 324 | 325 | # Send existing screen back to mummy 326 | if self.CurrentWindow: 327 | self.CurrentWindow.reparent(self.CurrentParent) 328 | 329 | # Swap back to initial screen if user taps the same button again 330 | if SomeWindow == self.CurrentWindow: 331 | SomeWindow = self.frameDefault 332 | 333 | # Get parent so we can return window after use 334 | self.CurrentParent = SomeWindow.get_parent() 335 | self.CurrentWindow = SomeWindow 336 | 337 | # Load window as requested 338 | self.CurrentWindow.reparent(self.frameMain) 339 | 340 | def LoadSettingsFromFile(self, FileName): 341 | # Open config file 342 | config = configparser.RawConfigParser() 343 | config.read(FileName) 344 | 345 | self.ReceiverCallsign = config.get('Receiver', 'Callsign', fallback='CHANGE_ME') 346 | 347 | self.LoRaFrequency = float(config.get('LoRa', 'Frequency', fallback='434.450')) 348 | self.LoRaMode = int(config.get('LoRa', 'Mode', fallback='1')) 349 | self.EnableLoRaUpload = config.getboolean('LoRa', 'EnableUpload', fallback=False) 350 | 351 | self.RTTYFrequency = float(config.get('RTTY', 'Frequency', fallback='434.250')) 352 | 353 | self.ChaseCarID = config.get('ChaseCar', 'ID', fallback='CHANGE_ME') 354 | self.ChaseCarPeriod = int(config.get('ChaseCar', 'Period', fallback='30')) 355 | self.ChaseCarEnabled = config.getboolean('ChaseCar', 'EnableUpload', fallback=False) 356 | 357 | self.GPSDevice = config.get('GPS', 'Device', fallback='/dev/ttyAMA0') 358 | 359 | def ApplySettings(self): 360 | # LoRa 361 | self.gateway.lora.SetLoRaFrequency(self.LoRaFrequency) 362 | self.gateway.lora.SetStandardLoRaParameters(self.LoRaMode) 363 | self.LoRaScreen.ShowLoRaFrequencyAndMode(self.LoRaFrequency, self.LoRaMode) 364 | self.gateway.EnableLoRaUpload = self.EnableLoRaUpload 365 | 366 | # RTTY 367 | self.gateway.rtty.SetFrequency(self.RTTYFrequency) 368 | self.RTTYScreen.ShowRTTYFrequency(self.RTTYFrequency) 369 | 370 | # Car 371 | self.gateway.habitat.ChaseCarEnabled = self.ChaseCarEnabled 372 | 373 | # GPS 374 | self.gateway.gps.SetDevice(self.GPSDevice) 375 | if self.gateway.gps.IsOpen: 376 | self.GPSScreen.ShowPortStatus("OK") 377 | else: 378 | self.GPSScreen.ShowPortStatus("Failed to open GPS device " + self.GPSDevice) 379 | 380 | def SaveSettingsToFile(self, FileName): 381 | config = configparser.RawConfigParser() 382 | config.read(FileName) 383 | 384 | if not config.has_section('Receiver'): 385 | config.add_section('Receiver') 386 | config.set('Receiver', 'Callsign', self.ReceiverCallsign) 387 | 388 | if not config.has_section('LoRa'): 389 | config.add_section('LoRa') 390 | config.set('LoRa', 'Frequency', self.LoRaFrequency) 391 | config.set('LoRa', 'Mode', self.LoRaMode) 392 | config.set('LoRa', 'EnableUpload', BoolToStr(self.EnableLoRaUpload)) 393 | 394 | if not config.has_section('RTTY'): 395 | config.add_section('RTTY') 396 | config.set('RTTY', 'Frequency', self.RTTYFrequency) 397 | 398 | if not config.has_section('ChaseCar'): 399 | config.add_section('ChaseCar') 400 | config.set('ChaseCar', 'ID', self.ChaseCarID) 401 | config.set('ChaseCar', 'Period', self.ChaseCarPeriod) 402 | config.set('ChaseCar', 'EnableUpload', BoolToStr(self.ChaseCarEnabled)) 403 | 404 | if not config.has_section('GPS'): 405 | config.add_section('GPS') 406 | # if self.GPSDevice: 407 | config.set('GPS', 'Device', self.GPSDevice) 408 | 409 | with open(FileName, 'wt') as configfile: 410 | config.write(configfile) 411 | 412 | def PopulateSettingsScreen(self): 413 | self.builder.get_object("textSettingsReceiverCallsign").set_text(self.ReceiverCallsign) 414 | self.builder.get_object("chkEnableLoRaUpload").set_active(self.EnableLoRaUpload) 415 | 416 | self.builder.get_object("textSettingsLoRaFrequency").set_text("{0:.3f}".format(self.LoRaFrequency)) 417 | self.builder.get_object("cmbSettingsLoRaMode").set_active(self.LoRaMode) 418 | 419 | self.builder.get_object("textSettingsRTTYFrequency").set_text("{0:.4f}".format(self.RTTYFrequency)) 420 | 421 | self.builder.get_object("textSettingsChaseCarID").set_text(self.ChaseCarID) 422 | self.builder.get_object("textSettingsChaseCarPeriod").set_text(str(self.ChaseCarPeriod)) 423 | self.builder.get_object("chkEnableChaseCarUpload").set_active(self.ChaseCarEnabled) 424 | 425 | self.builder.get_object("textSettingsGPSDevice").set_text(self.GPSDevice) 426 | 427 | def LoadFromSettingsScreen(self): 428 | self.ReceiverCallsign = self.builder.get_object("textSettingsReceiverCallsign").get_text() 429 | self.EnableLoRaUpload = self.builder.get_object("chkEnableLoRaUpload").get_active() 430 | 431 | self.LoRaFrequency = float(self.builder.get_object("textSettingsLoRaFrequency").get_text()) 432 | self.LoRaMode = self.builder.get_object("cmbSettingsLoRaMode").get_active() 433 | 434 | self.RTTYFrequency = float(self.builder.get_object("textSettingsRTTYFrequency").get_text()) 435 | 436 | self.ChaseCarID = self.builder.get_object("textSettingsChaseCarID").get_text() 437 | self.ChaseCarPeriod = int(self.builder.get_object("textSettingsChaseCarPeriod").get_text()) 438 | self.ChaseCarEnabled = self.builder.get_object("chkEnableChaseCarUpload").get_active() 439 | 440 | self.GPSDevice = self.builder.get_object("textSettingsGPSDevice").get_text() 441 | 442 | def SetKeyboardCase(self, ToUpper): 443 | for character in string.ascii_uppercase: 444 | button = self.builder.get_object("button" + character) 445 | button.set_label(character if ToUpper else character.lower()) 446 | 447 | def DecodeSentence(self, sentence): 448 | # $BUZZ,483,10:04:27,51.95022,-2.54435,00190,5*6856 449 | try: 450 | list = sentence.split(",") 451 | 452 | payload = list[0].split("$") 453 | payload = payload[len(payload)-1] 454 | 455 | TimeStamp = datetime.datetime.strptime(list[2], '%H:%M:%S') 456 | 457 | return {'payload': payload, 'time': TimeStamp, 'lat': float(list[3]), 'lon': float(list[4]), 'alt': float(list[5])} 458 | except: 459 | return None 460 | 461 | def ssdv_update_timer(self, *args): 462 | # Only update the image on the SSDV window if it's being displayed 463 | if self.CurrentWindow == self.SSDVScreen.frame: 464 | self.SSDVScreen.ShowFile(self.SelectedSSDVIndex, False) 465 | 466 | self.HABScreen.ShowTimeSinceData() 467 | 468 | return True # Run again 469 | 470 | 471 | hwg = SkyGate() 472 | Gtk.main() 473 | 474 | -------------------------------------------------------------------------------- /skygate/skygate.glade: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | False 7 | True 8 | True 9 | 10 | 11 | True 12 | False 13 | True 14 | 15 | 16 | True 17 | False 18 | vertical 19 | True 20 | 21 | 22 | 23 | 24 | 25 | 100 26 | 80 27 | True 28 | False 29 | 8 30 | skycademy.png 31 | 32 | 33 | False 34 | True 35 | 1 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | False 44 | True 45 | 0 46 | 47 | 48 | 49 | 50 | True 51 | False 52 | True 53 | True 54 | 55 | 56 | True 57 | False 58 | end 59 | 32 60 | True 61 | The <b>HAB</b> screen shows the current payload position 62 | plus distance and direction from you to it 63 | True 64 | right 65 | 66 | 67 | 0 68 | 0 69 | 70 | 71 | 72 | 73 | True 74 | False 75 | end 76 | 32 77 | True 78 | The <b>SSDV</b> (Slow Scan Digital Video) screen shows 79 | the payload photographs as they are received 80 | True 81 | right 82 | 83 | 84 | 0 85 | 1 86 | 87 | 88 | 89 | 90 | True 91 | False 92 | end 93 | 32 94 | True 95 | The <b>LoRa</b> screen allows you to tune in to the 96 | LoRa (LOng RAnge modem) transmissions 97 | and to see the raw data 98 | True 99 | right 100 | 101 | 102 | 0 103 | 2 104 | 105 | 106 | 107 | 108 | True 109 | False 110 | end 111 | 32 112 | True 113 | The <b>RTTY</b> screen allows you to tune in to the 114 | RTTY (Radio TeleTYpe) transmissions 115 | and to see the raw data 116 | True 117 | right 118 | 119 | 120 | 0 121 | 3 122 | 123 | 124 | 125 | 126 | True 127 | False 128 | end 129 | 32 130 | True 131 | The <b>GPS</b> screen shows your current location 132 | True 133 | right 134 | 135 | 136 | 0 137 | 4 138 | 139 | 140 | 141 | 142 | True 143 | False 144 | end 145 | 32 146 | True 147 | The <b>Settings</b> screen is where you can change 148 | items such as your callsign for the live map 149 | True 150 | right 151 | 152 | 153 | 0 154 | 5 155 | 156 | 157 | 158 | 159 | 12 160 | True 161 | False 162 | end 163 | 1 164 | 165 | 166 | 1 167 | 0 168 | 169 | 170 | 171 | 172 | 12 173 | True 174 | False 175 | end 176 | 1 177 | 178 | 179 | 1 180 | 1 181 | 182 | 183 | 184 | 185 | 12 186 | True 187 | False 188 | end 189 | 1 190 | 191 | 192 | 1 193 | 2 194 | 195 | 196 | 197 | 198 | 12 199 | True 200 | False 201 | end 202 | 1 203 | 204 | 205 | 1 206 | 3 207 | 208 | 209 | 210 | 211 | 12 212 | True 213 | False 214 | end 215 | 1 216 | 217 | 218 | 1 219 | 4 220 | 221 | 222 | 223 | 224 | 12 225 | True 226 | False 227 | end 228 | 1 229 | 230 | 231 | 1 232 | 5 233 | 234 | 235 | 236 | 237 | True 238 | True 239 | end 240 | 1 241 | 242 | 243 | 244 | 245 | 246 | 247 | False 248 | 249 | 250 | True 251 | False 252 | 253 | 254 | True 255 | False 256 | end 257 | 24 258 | True 259 | GPS Connection Status: 260 | 261 | 262 | 0 263 | 0 264 | 265 | 266 | 267 | 268 | True 269 | False 270 | 16 271 | True 272 | 273 | 274 | 275 | 276 | 277 | 1 278 | 0 279 | 280 | 281 | 282 | 283 | True 284 | False 285 | end 286 | 24 287 | True 288 | Number of Satellites: 289 | 290 | 291 | 0 292 | 10 293 | 294 | 295 | 296 | 297 | True 298 | False 299 | end 300 | True 301 | Altitude: 302 | 303 | 304 | 0 305 | 8 306 | 307 | 308 | 309 | 310 | True 311 | False 312 | end 313 | True 314 | Longitude: 315 | 316 | 317 | 0 318 | 6 319 | 320 | 321 | 322 | 323 | True 324 | False 325 | end 326 | True 327 | Latitude: 328 | 329 | 330 | 0 331 | 4 332 | 333 | 334 | 335 | 336 | True 337 | False 338 | end 339 | True 340 | Time (UTC): 341 | 342 | 343 | 0 344 | 2 345 | 346 | 347 | 348 | 349 | True 350 | False 351 | 352 | 353 | 0 354 | 1 355 | 356 | 357 | 358 | 359 | True 360 | False 361 | 362 | 363 | 0 364 | 3 365 | 366 | 367 | 368 | 369 | True 370 | False 371 | 372 | 373 | 0 374 | 5 375 | 376 | 377 | 378 | 379 | True 380 | False 381 | 382 | 383 | 0 384 | 7 385 | 386 | 387 | 388 | 389 | True 390 | False 391 | 392 | 393 | 0 394 | 9 395 | 396 | 397 | 398 | 399 | True 400 | False 401 | 402 | 403 | 1 404 | 9 405 | 406 | 407 | 408 | 409 | True 410 | False 411 | 412 | 413 | 414 | 415 | 416 | 1 417 | 10 418 | 419 | 420 | 421 | 422 | True 423 | False 424 | 425 | 426 | 427 | 428 | 429 | 1 430 | 4 431 | 432 | 433 | 434 | 435 | True 436 | False 437 | 438 | 439 | 440 | 441 | 442 | 1 443 | 8 444 | 445 | 446 | 447 | 448 | True 449 | False 450 | 451 | 452 | 453 | 454 | 455 | 1 456 | 6 457 | 458 | 459 | 460 | 461 | True 462 | False 463 | 464 | 465 | 466 | 467 | 468 | 1 469 | 2 470 | 471 | 472 | 473 | 474 | True 475 | False 476 | 477 | 478 | 1 479 | 1 480 | 481 | 482 | 483 | 484 | True 485 | False 486 | 487 | 488 | 1 489 | 3 490 | 491 | 492 | 493 | 494 | True 495 | False 496 | 497 | 498 | 1 499 | 5 500 | 501 | 502 | 503 | 504 | True 505 | False 506 | 507 | 508 | 1 509 | 7 510 | 511 | 512 | 513 | 514 | 515 | 516 | False 517 | 518 | 519 | True 520 | False 521 | 522 | 523 | True 524 | False 525 | vertical 526 | 527 | 528 | True 529 | False 530 | center 531 | end 532 | 4 533 | Time (UTC): 534 | True 535 | 536 | 537 | False 538 | True 539 | 0 540 | 541 | 542 | 543 | 544 | True 545 | False 546 | start 547 | 4 548 | 549 | 550 | 551 | 552 | 553 | True 554 | True 555 | 1 556 | 557 | 558 | 559 | 560 | True 561 | False 562 | center 563 | end 564 | Latitude: 565 | True 566 | 567 | 568 | False 569 | True 570 | 2 571 | 572 | 573 | 574 | 575 | True 576 | False 577 | start 578 | 4 579 | 580 | 581 | 582 | 583 | 584 | True 585 | True 586 | 3 587 | 588 | 589 | 590 | 591 | True 592 | False 593 | center 594 | end 595 | Longitude: 596 | True 597 | 598 | 599 | False 600 | True 601 | 4 602 | 603 | 604 | 605 | 606 | True 607 | False 608 | start 609 | 4 610 | 611 | 612 | 613 | 614 | 615 | True 616 | True 617 | 5 618 | 619 | 620 | 621 | 622 | True 623 | False 624 | center 625 | end 626 | Altitude: 627 | True 628 | 629 | 630 | False 631 | True 632 | 6 633 | 634 | 635 | 636 | 637 | 200 638 | True 639 | False 640 | start 641 | 4 642 | 643 | 644 | 645 | 646 | 647 | True 648 | True 649 | 7 650 | 651 | 652 | 653 | 654 | True 655 | False 656 | center 657 | end 658 | Maximum Altitude: 659 | True 660 | 661 | 662 | False 663 | True 664 | 8 665 | 666 | 667 | 668 | 669 | 200 670 | True 671 | False 672 | start 673 | 4 674 | 675 | 676 | 677 | 678 | 679 | True 680 | True 681 | 9 682 | 683 | 684 | 685 | 686 | False 687 | True 688 | 0 689 | 690 | 691 | 692 | 693 | True 694 | False 695 | vertical 696 | 697 | 698 | True 699 | False 700 | center 701 | 4 702 | Direction: 703 | True 704 | 705 | 706 | False 707 | True 708 | 0 709 | 710 | 711 | 712 | 713 | 300 714 | 300 715 | True 716 | False 717 | 8 718 | 719 | 720 | True 721 | False 722 | 16 723 | compass.png 724 | 725 | 726 | 727 | 728 | True 729 | False 730 | ball.png 731 | 732 | 733 | 150 734 | 134 735 | 736 | 737 | 738 | 739 | False 740 | True 741 | 1 742 | 743 | 744 | 745 | 746 | True 747 | False 748 | center 749 | 16 750 | Distance: 751 | True 752 | 753 | 754 | False 755 | True 756 | 2 757 | 758 | 759 | 760 | 761 | True 762 | False 763 | start 764 | 765 | 766 | 767 | 768 | 769 | True 770 | True 771 | 3 772 | 773 | 774 | 775 | 776 | False 777 | True 778 | 1 779 | 780 | 781 | 782 | 783 | True 784 | False 785 | vertical 786 | 787 | 788 | True 789 | False 790 | center 791 | 4 792 | Payload: 793 | True 794 | 795 | 796 | False 797 | True 798 | 0 799 | 800 | 801 | 802 | 803 | True 804 | False 805 | start 806 | 807 | 808 | 809 | 810 | 811 | True 812 | True 813 | 1 814 | 815 | 816 | 817 | 818 | True 819 | False 820 | 32 821 | 8 822 | balloon.png 823 | 824 | 825 | True 826 | True 827 | 2 828 | 829 | 830 | 831 | 832 | False 833 | 32 834 | 8 835 | chute.png 836 | 837 | 838 | True 839 | True 840 | 3 841 | 842 | 843 | 844 | 845 | True 846 | False 847 | center 848 | 6 849 | Vertical Rate: 850 | True 851 | 852 | 853 | False 854 | True 855 | 4 856 | 857 | 858 | 859 | 860 | True 861 | False 862 | start 863 | 0.029999999999999999 864 | 865 | 866 | 867 | 868 | 869 | True 870 | True 871 | 5 872 | 873 | 874 | 875 | 876 | True 877 | False 878 | center 879 | 6 880 | From Channel: 881 | True 882 | 883 | 884 | False 885 | True 886 | 6 887 | 888 | 889 | 890 | 891 | Auto 892 | True 893 | True 894 | False 895 | 0 896 | True 897 | True 898 | 899 | 900 | 901 | False 902 | True 903 | 7 904 | 905 | 906 | 907 | 908 | LoRa 909 | True 910 | True 911 | False 912 | 0 913 | True 914 | True 915 | btnHABAuto 916 | 917 | 918 | 919 | False 920 | True 921 | 8 922 | 923 | 924 | 925 | 926 | RTTY 927 | True 928 | True 929 | False 930 | 16 931 | 0 932 | True 933 | True 934 | btnHABAuto 935 | 936 | 937 | 938 | False 939 | True 940 | 9 941 | 942 | 943 | 944 | 945 | True 946 | False 947 | center 948 | 6 949 | Time since position: 950 | True 951 | 952 | 953 | False 954 | True 955 | 10 956 | 957 | 958 | 959 | 960 | True 961 | False 962 | start 963 | 964 | 965 | 966 | 967 | 968 | True 969 | True 970 | 11 971 | 972 | 973 | 974 | 975 | False 976 | True 977 | 2 978 | 979 | 980 | 981 | 982 | 983 | 984 | False 985 | 986 | 987 | True 988 | False 989 | vertical 990 | 991 | 992 | True 993 | False 994 | 8 995 | 8 996 | Raw Log from the LoRa Radio Channel 997 | 998 | 999 | False 1000 | True 1001 | 0 1002 | 1003 | 1004 | 1005 | 1006 | True 1007 | True 1008 | in 1009 | 1010 | 1011 | 100 1012 | 80 1013 | True 1014 | True 1015 | False 1016 | 1017 | 1018 | 1019 | 1020 | 1021 | True 1022 | True 1023 | 1 1024 | 1025 | 1026 | 1027 | 1028 | True 1029 | False 1030 | 4 1031 | 4 1032 | 1033 | 1034 | True 1035 | False 1036 | 1037 | 1038 | True 1039 | True 1040 | 0 1041 | 1042 | 1043 | 1044 | 1045 | True 1046 | False 1047 | 1048 | 1049 | True 1050 | True 1051 | 1 1052 | 1053 | 1054 | 1055 | 1056 | Frequency 1kHz Up 1057 | True 1058 | True 1059 | True 1060 | end 1061 | immediate 1062 | 1063 | 1064 | 1065 | False 1066 | True 1067 | 2 1068 | 1069 | 1070 | 1071 | 1072 | Frequency 1kHz Down 1073 | True 1074 | True 1075 | True 1076 | end 1077 | 4 1078 | 0.43000000715255737 1079 | 1080 | 1081 | 1082 | False 1083 | True 1084 | 3 1085 | 1086 | 1087 | 1088 | 1089 | False 1090 | True 1091 | 2 1092 | 1093 | 1094 | 1095 | 1096 | 1097 | 1098 | False 1099 | SkyGate 1100 | True 1101 | 1102 | 1103 | 1104 | 1105 | True 1106 | False 1107 | 1108 | 1109 | True 1110 | False 1111 | True 1112 | True 1113 | 1114 | 1115 | True 1116 | False 1117 | baseline 1118 | True 1119 | False 1120 | 0 1121 | none 1122 | 1123 | 1124 | 1125 | 1126 | 1127 | 1128 | 1129 | 1130 | 0 1131 | 0 1132 | 1133 | 1134 | 1135 | 1136 | True 1137 | False 1138 | True 1139 | True 1140 | True 1141 | 1142 | 1143 | RTTY 1144 | True 1145 | True 1146 | True 1147 | 1148 | 1149 | 1150 | 0 1151 | 3 1152 | 1153 | 1154 | 1155 | 1156 | GPS 1157 | True 1158 | True 1159 | True 1160 | 1161 | 1162 | 1163 | 0 1164 | 4 1165 | 1166 | 1167 | 1168 | 1169 | SSDV 1170 | True 1171 | True 1172 | True 1173 | 1174 | 1175 | 1176 | 0 1177 | 1 1178 | 1179 | 1180 | 1181 | 1182 | Settings 1183 | True 1184 | True 1185 | True 1186 | 1187 | 1188 | 1189 | 0 1190 | 5 1191 | 1192 | 1193 | 1194 | 1195 | LoRa 1196 | True 1197 | True 1198 | True 1199 | 1200 | 1201 | 1202 | 0 1203 | 2 1204 | 1205 | 1206 | 1207 | 1208 | HAB 1209 | True 1210 | True 1211 | True 1212 | 1213 | 1214 | 1215 | 0 1216 | 0 1217 | 1218 | 1219 | 1220 | 1221 | 1 1222 | 0 1223 | 1224 | 1225 | 1226 | 1227 | 0 1228 | 1 1229 | 1230 | 1231 | 1232 | 1233 | 1234 | 1235 | 1236 | 1237 | 1238 | False 1239 | 1240 | 1241 | True 1242 | False 1243 | vertical 1244 | 1245 | 1246 | True 1247 | False 1248 | RTTY Log 1249 | 1250 | 1251 | False 1252 | True 1253 | 0 1254 | 1255 | 1256 | 1257 | 1258 | True 1259 | True 1260 | in 1261 | 1262 | 1263 | 100 1264 | 80 1265 | True 1266 | True 1267 | False 1268 | False 1269 | False 1270 | 1271 | 1272 | 1273 | 1274 | 1275 | 1276 | True 1277 | True 1278 | 1 1279 | 1280 | 1281 | 1282 | 1283 | True 1284 | False 1285 | 8 1286 | 1287 | 1288 | False 1289 | True 1290 | 2 1291 | 1292 | 1293 | 1294 | 1295 | True 1296 | False 1297 | 1298 | 1299 | True 1300 | False 1301 | 1302 | 1303 | True 1304 | True 1305 | 0 1306 | 1307 | 1308 | 1309 | 1310 | Frequency 500Hz Up 1311 | True 1312 | True 1313 | True 1314 | end 1315 | immediate 1316 | 1317 | 1318 | 1319 | False 1320 | True 1321 | 1 1322 | 1323 | 1324 | 1325 | 1326 | Frequency 500Hz Down 1327 | True 1328 | True 1329 | True 1330 | end 1331 | 0.43000000715255737 1332 | 1333 | 1334 | 1335 | False 1336 | True 1337 | 2 1338 | 1339 | 1340 | 1341 | 1342 | Open dl-fldigi 1343 | True 1344 | True 1345 | True 1346 | 1347 | 1348 | 1349 | False 1350 | True 1351 | 3 1352 | 1353 | 1354 | 1355 | 1356 | False 1357 | True 1358 | 3 1359 | 1360 | 1361 | 1362 | 1363 | 1364 | 1365 | False 1366 | 1367 | 1368 | True 1369 | False 1370 | vertical 1371 | 1372 | 1373 | True 1374 | False 1375 | gtk-missing-image 1376 | 1377 | 1378 | True 1379 | True 1380 | 0 1381 | 1382 | 1383 | 1384 | 1385 | True 1386 | False 1387 | 1388 | 1389 | Previous Image 1390 | True 1391 | True 1392 | True 1393 | 1394 | 1395 | 1396 | False 1397 | True 1398 | 0 1399 | 1400 | 1401 | 1402 | 1403 | True 1404 | False 1405 | 1406 | 1407 | True 1408 | True 1409 | 1 1410 | 1411 | 1412 | 1413 | 1414 | Next Image 1415 | True 1416 | True 1417 | True 1418 | 1419 | 1420 | 1421 | False 1422 | True 1423 | 2 1424 | 1425 | 1426 | 1427 | 1428 | False 1429 | True 1430 | 1 1431 | 1432 | 1433 | 1434 | 1435 | 1436 | 1437 | 400 1438 | False 1439 | baseline 1440 | start 1441 | 1442 | 1443 | True 1444 | False 1445 | vertical 1446 | 13 1447 | 1448 | 1449 | True 1450 | False 1451 | 7 1452 | 7 1453 | 8 1454 | True 1455 | 1456 | 1457 | True 1458 | False 1459 | end 1460 | Receiver Callsign: 1461 | 1462 | 1463 | 0 1464 | 0 1465 | 1466 | 1467 | 1468 | 1469 | True 1470 | True 1471 | 12 1472 | 1473 | 1474 | 1475 | 1 1476 | 0 1477 | 1478 | 1479 | 1480 | 1481 | True 1482 | False 1483 | end 1484 | Chase Car ID: 1485 | 1486 | 1487 | 2 1488 | 0 1489 | 1490 | 1491 | 1492 | 1493 | True 1494 | True 1495 | True 1496 | 1497 | 1498 | 1499 | 3 1500 | 0 1501 | 1502 | 1503 | 1504 | 1505 | True 1506 | False 1507 | end 1508 | LoRa Upload: 1509 | 1510 | 1511 | 0 1512 | 1 1513 | 1514 | 1515 | 1516 | 1517 | Enable 1518 | True 1519 | True 1520 | False 1521 | 0 1522 | True 1523 | 1524 | 1525 | 1 1526 | 1 1527 | 1528 | 1529 | 1530 | 1531 | True 1532 | False 1533 | end 1534 | Car Upload Period (s) 1535 | 1536 | 1537 | 2 1538 | 1 1539 | 1540 | 1541 | 1542 | 1543 | True 1544 | True 1545 | 3 1546 | digits 1547 | 1548 | 1549 | 1550 | 3 1551 | 1 1552 | 1553 | 1554 | 1555 | 1556 | True 1557 | False 1558 | end 1559 | LoRa Frequency: 1560 | 1561 | 1562 | 0 1563 | 2 1564 | 1565 | 1566 | 1567 | 1568 | True 1569 | True 1570 | 8 1571 | 1572 | 1573 | 1574 | 1575 | 1 1576 | 2 1577 | 1578 | 1579 | 1580 | 1581 | True 1582 | False 1583 | end 1584 | Chase Car Upload: 1585 | 1586 | 1587 | 2 1588 | 2 1589 | 1590 | 1591 | 1592 | 1593 | Enable 1594 | True 1595 | True 1596 | False 1597 | 0 1598 | True 1599 | 1600 | 1601 | 3 1602 | 2 1603 | 1604 | 1605 | 1606 | 1607 | True 1608 | False 1609 | end 1610 | LoRa Mode: 1611 | 1612 | 1613 | 0 1614 | 3 1615 | 1616 | 1617 | 1618 | 1619 | True 1620 | False 1621 | end 1622 | RTTY Frequency: 1623 | 1624 | 1625 | 0 1626 | 4 1627 | 1628 | 1629 | 1630 | 1631 | True 1632 | False 1633 | 1 1634 | 1635 | 0 - Slow 1636 | 1 - Fast 1637 | 1638 | 1639 | 1640 | 1 1641 | 3 1642 | 1643 | 1644 | 1645 | 1646 | True 1647 | True 1648 | 8 1649 | 1650 | 1651 | 1652 | 1653 | 1 1654 | 4 1655 | 1656 | 1657 | 1658 | 1659 | True 1660 | False 1661 | end 1662 | GPS Device 1663 | 1664 | 1665 | 2 1666 | 3 1667 | 1668 | 1669 | 1670 | 1671 | True 1672 | True 1673 | 12 1674 | 1675 | 1676 | 1677 | 1678 | 3 1679 | 3 1680 | 1681 | 1682 | 1683 | 1684 | Undo Changes 1685 | True 1686 | True 1687 | True 1688 | 4 1689 | 4 1690 | 4 1691 | 4 1692 | 1693 | 1694 | 1695 | 3 1696 | 4 1697 | 1698 | 1699 | 1700 | 1701 | Save 1702 | True 1703 | True 1704 | True 1705 | 4 1706 | 4 1707 | 4 1708 | 4 1709 | 1710 | 1711 | 1712 | 2 1713 | 4 1714 | 1715 | 1716 | 1717 | 1718 | True 1719 | True 1720 | 0 1721 | 1722 | 1723 | 1724 | 1725 | True 1726 | False 1727 | 1728 | 1729 | Shift 1730 | True 1731 | True 1732 | True 1733 | 4 1734 | 4 1735 | 1 1736 | 1737 | 1738 | 1739 | False 1740 | True 1741 | 0 1742 | 1743 | 1744 | 1745 | 1746 | True 1747 | False 1748 | 4 1749 | 2 1750 | True 1751 | True 1752 | 1753 | 1754 | Q 1755 | True 1756 | True 1757 | True 1758 | 1759 | 1760 | 1761 | 0 1762 | 1 1763 | 1764 | 1765 | 1766 | 1767 | A 1768 | True 1769 | True 1770 | True 1771 | 1772 | 1773 | 1774 | 0 1775 | 2 1776 | 1777 | 1778 | 1779 | 1780 | Z 1781 | True 1782 | True 1783 | True 1784 | 1785 | 1786 | 1787 | 0 1788 | 3 1789 | 1790 | 1791 | 1792 | 1793 | 1 1794 | True 1795 | True 1796 | True 1797 | 0.44999998807907104 1798 | 1799 | 1800 | 1801 | 0 1802 | 0 1803 | 1804 | 1805 | 1806 | 1807 | W 1808 | True 1809 | True 1810 | True 1811 | 0.44999998807907104 1812 | 1813 | 1814 | 1815 | 1 1816 | 1 1817 | 1818 | 1819 | 1820 | 1821 | S 1822 | True 1823 | True 1824 | True 1825 | 1826 | 1827 | 1828 | 1 1829 | 2 1830 | 1831 | 1832 | 1833 | 1834 | X 1835 | True 1836 | True 1837 | True 1838 | 0.44999998807907104 1839 | 1840 | 1841 | 1842 | 1 1843 | 3 1844 | 1845 | 1846 | 1847 | 1848 | D 1849 | True 1850 | True 1851 | True 1852 | 1853 | 1854 | 1855 | 2 1856 | 2 1857 | 1858 | 1859 | 1860 | 1861 | E 1862 | True 1863 | True 1864 | True 1865 | 1866 | 1867 | 1868 | 2 1869 | 1 1870 | 1871 | 1872 | 1873 | 1874 | C 1875 | True 1876 | True 1877 | True 1878 | 1879 | 1880 | 1881 | 2 1882 | 3 1883 | 1884 | 1885 | 1886 | 1887 | R 1888 | True 1889 | True 1890 | True 1891 | 1892 | 1893 | 1894 | 3 1895 | 1 1896 | 1897 | 1898 | 1899 | 1900 | 2 1901 | True 1902 | True 1903 | True 1904 | 1905 | 1906 | 1907 | 1 1908 | 0 1909 | 1910 | 1911 | 1912 | 1913 | 3 1914 | True 1915 | True 1916 | True 1917 | 1918 | 1919 | 1920 | 2 1921 | 0 1922 | 1923 | 1924 | 1925 | 1926 | T 1927 | True 1928 | True 1929 | True 1930 | 1931 | 1932 | 1933 | 4 1934 | 1 1935 | 1936 | 1937 | 1938 | 1939 | Y 1940 | True 1941 | True 1942 | True 1943 | 1944 | 1945 | 1946 | 5 1947 | 1 1948 | 1949 | 1950 | 1951 | 1952 | U 1953 | True 1954 | True 1955 | True 1956 | 1957 | 1958 | 1959 | 6 1960 | 1 1961 | 1962 | 1963 | 1964 | 1965 | I 1966 | True 1967 | True 1968 | True 1969 | 1970 | 1971 | 1972 | 7 1973 | 1 1974 | 1975 | 1976 | 1977 | 1978 | O 1979 | True 1980 | True 1981 | True 1982 | 1983 | 1984 | 1985 | 8 1986 | 1 1987 | 1988 | 1989 | 1990 | 1991 | P 1992 | True 1993 | True 1994 | True 1995 | 1996 | 1997 | 1998 | 9 1999 | 1 2000 | 2001 | 2002 | 2003 | 2004 | F 2005 | True 2006 | True 2007 | True 2008 | 2009 | 2010 | 2011 | 3 2012 | 2 2013 | 2014 | 2015 | 2016 | 2017 | G 2018 | True 2019 | True 2020 | True 2021 | 2022 | 2023 | 2024 | 4 2025 | 2 2026 | 2027 | 2028 | 2029 | 2030 | H 2031 | True 2032 | True 2033 | True 2034 | 2035 | 2036 | 2037 | 5 2038 | 2 2039 | 2040 | 2041 | 2042 | 2043 | J 2044 | True 2045 | True 2046 | True 2047 | 2048 | 2049 | 2050 | 6 2051 | 2 2052 | 2053 | 2054 | 2055 | 2056 | K 2057 | True 2058 | True 2059 | True 2060 | 2061 | 2062 | 2063 | 7 2064 | 2 2065 | 2066 | 2067 | 2068 | 2069 | L 2070 | True 2071 | True 2072 | True 2073 | 2074 | 2075 | 2076 | 8 2077 | 2 2078 | 2079 | 2080 | 2081 | 2082 | <- 2083 | True 2084 | True 2085 | True 2086 | 2087 | 2088 | 2089 | 9 2090 | 2 2091 | 2092 | 2093 | 2094 | 2095 | V 2096 | True 2097 | True 2098 | True 2099 | 2100 | 2101 | 2102 | 3 2103 | 3 2104 | 2105 | 2106 | 2107 | 2108 | N 2109 | True 2110 | True 2111 | True 2112 | 2113 | 2114 | 2115 | 5 2116 | 3 2117 | 2118 | 2119 | 2120 | 2121 | B 2122 | True 2123 | True 2124 | True 2125 | 2126 | 2127 | 2128 | 4 2129 | 3 2130 | 2131 | 2132 | 2133 | 2134 | M 2135 | True 2136 | True 2137 | True 2138 | 2139 | 2140 | 2141 | 6 2142 | 3 2143 | 2144 | 2145 | 2146 | 2147 | - 2148 | True 2149 | True 2150 | True 2151 | 2152 | 2153 | 2154 | 7 2155 | 3 2156 | 2157 | 2158 | 2159 | 2160 | . 2161 | True 2162 | True 2163 | True 2164 | 2165 | 2166 | 2167 | 8 2168 | 3 2169 | 2170 | 2171 | 2172 | 2173 | / 2174 | True 2175 | True 2176 | True 2177 | 2178 | 2179 | 2180 | 9 2181 | 3 2182 | 2183 | 2184 | 2185 | 2186 | 4 2187 | True 2188 | True 2189 | True 2190 | 2191 | 2192 | 2193 | 3 2194 | 0 2195 | 2196 | 2197 | 2198 | 2199 | 5 2200 | True 2201 | True 2202 | True 2203 | 2204 | 2205 | 2206 | 4 2207 | 0 2208 | 2209 | 2210 | 2211 | 2212 | 6 2213 | True 2214 | True 2215 | True 2216 | 2217 | 2218 | 2219 | 5 2220 | 0 2221 | 2222 | 2223 | 2224 | 2225 | 7 2226 | True 2227 | True 2228 | True 2229 | 2230 | 2231 | 2232 | 6 2233 | 0 2234 | 2235 | 2236 | 2237 | 2238 | 8 2239 | True 2240 | True 2241 | True 2242 | 2243 | 2244 | 2245 | 7 2246 | 0 2247 | 2248 | 2249 | 2250 | 2251 | 9 2252 | True 2253 | True 2254 | True 2255 | 2256 | 2257 | 2258 | 8 2259 | 0 2260 | 2261 | 2262 | 2263 | 2264 | 0 2265 | True 2266 | True 2267 | True 2268 | 2269 | 2270 | 2271 | 9 2272 | 0 2273 | 2274 | 2275 | 2276 | 2277 | True 2278 | True 2279 | 1 2280 | 2281 | 2282 | 2283 | 2284 | False 2285 | True 2286 | 1 2287 | 2288 | 2289 | 2290 | 2291 | 2292 | 2293 | --------------------------------------------------------------------------------