├── .gitignore ├── README.md ├── debian ├── .gitignore ├── changelog ├── compat ├── control ├── copyright ├── py3dist-overrides ├── python3-pytrack.docs ├── python3-pytrack.postinst ├── pytrack-gps.install ├── rules └── source │ ├── format │ └── options ├── docs ├── camera.md ├── cgps.md ├── led.md ├── lora.md ├── rtty.md ├── telemetry.md └── tracker.md ├── gps ├── Makefile ├── gps ├── gps.c ├── gps.h ├── server.c ├── server.h ├── ublox.c └── ublox.h ├── pytrack ├── __init__.py ├── camera.py ├── cgps.py ├── led.py ├── lora.py ├── pytrack ├── pytrack.ini ├── rtty.py ├── telemetry.py ├── temperature.py ├── test_camera.py ├── test_cgps.py ├── test_led.py ├── test_lora.py ├── test_rtty.py ├── test_tracker.py └── tracker.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *.egg-info/ 3 | build/ 4 | .pybuild/ 5 | gps/pytrack-gps 6 | *.pyc 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pytrack - Modular Python HAB tracker 2 | 3 | HAB tracker software for the Pi In The Sky board and LoRa expansion board. 4 | 5 | 6 | The pytrack package and its Python dependencies, can be installed on a Raspberry Pi with: 7 | 8 | ```bash 9 | sudo apt update 10 | sudo apt install python3-pytrack 11 | ``` 12 | 13 | ## Raspbian Configuration 14 | 15 | Enable the following with raspi-config: 16 | 17 | Interfacing Options --> Camera --> Yes 18 | Interfacing Options --> SPI --> Yes 19 | Interfacing Options --> Serial --> No (enable login) --> Yes (enable hardware) 20 | Interfacing Options --> 1-Wire --> Yes 21 | 22 | ### Additional step for Raspberry Pi 3 23 | In order to use the PITs board with the Raspberry Pi 3 you will need to carry out some additional steps to disable the bluetooth (which conflicts with PITs) 24 | 25 | - Edit your */boot/config.txt* by typing `sudo nano /boot/config.txt` 26 | - Add `dtoverlay=pi3-disable-bt` to the very bottom of the file to disable bluetooth. 27 | - Press `Ctrl + x` then `Enter` to save and exit. 28 | - Finally type `sudo systemctl disable hciuart` followed by `sudo reboot` 29 | 30 | ## Usage 31 | 32 | Before running the tracker, it is necessary to start the pigpio daemon, with: 33 | 34 | sudo pigpiod 35 | 36 | You can then create you own tracker program in python using the pytrack module, here's a simple example: 37 | 38 | ``` 39 | from pytrack.tracker import * 40 | from time import sleep 41 | 42 | # Creates a tracker object to control the PITs board 43 | mytracker = Tracker() 44 | 45 | # Set rtty payload name, transmission details image frequency 46 | mytracker.set_rtty(payload_id='name1', frequency=434.250, baud_rate=300) 47 | mytracker.add_rtty_camera_schedule('images/RTTY', period=60) 48 | 49 | # Set Lora payload name, transmission details and image frequency 50 | mytracker.set_lora(payload_id='name2', channel=0, frequency=434.150, mode=1) 51 | mytracker.add_lora_camera_schedule('images/LORA', period=60) 52 | 53 | # Set how frequently to capture and store an image at Maximum resolution 54 | mytracker.add_full_camera_schedule('images/FULL', period=60) 55 | 56 | # Start the tracker 57 | mytracker.start() 58 | 59 | while True: 60 | sleep(1) 61 | ``` 62 | To run the tracker type `python3 your_file_name.py` 63 | 64 | ## Auto Startup 65 | 66 | Add the following lines to the file **/etc/rc.local**, before the **exit 0** line: 67 | 68 | ```bash 69 | pigpiod 70 | su pi -c "python3 /home/pi/your_file_name.py" & 71 | ``` 72 | 73 | ## Documentation 74 | 75 | You can find more information about the component libraries and their usage in the [Documentation](docs/) 76 | -------------------------------------------------------------------------------- /debian/.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.debhelper 3 | *.substvars 4 | python3-pytrack/ 5 | pytrack-gps/ 6 | debhelper-build-stamp 7 | files 8 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | pytrack (0.3) stretch; urgency=medium 2 | 3 | * Fixed imports in tracker.py 4 | 5 | -- Serge Schneider Tue, 27 Mar 2018 14:58:00 +0100 6 | 7 | pytrack (0.2) stretch; urgency=medium 8 | 9 | * Updated documentation 10 | 11 | -- Serge Schneider Mon, 26 Mar 2018 08:59:26 +0100 12 | 13 | pytrack (0.1) stretch; urgency=medium 14 | 15 | * Initial Release. 16 | 17 | -- Serge Schneider Fri, 10 Nov 2017 18:29:07 +0000 18 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 9 2 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: pytrack 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/pytrack 8 | X-Python3-Version: >= 3.2 9 | 10 | Package: python3-pytrack 11 | Architecture: all 12 | Depends: ${python3:Depends}, ${misc:Depends}, ssdv, pytrack-gps, pigpio 13 | Description: HAB Receiver for RTTY and LoRa (Python 3) 14 | . 15 | This package installs the library for Python 3. 16 | 17 | Package: pytrack-gps 18 | Architecture: any 19 | Depends: ${misc:Depends}, ${shlibs:Depends}, wiringpi 20 | Description: Presents the Ublox 6/7/8 GPS receiver as a network socket 21 | Provides a network socket to comminicate with Ublox GPS recievers through 22 | bitbanged I2C 23 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: pytrack 3 | Source: http://github.com/raspberrypi/pytrack 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 | -------------------------------------------------------------------------------- /debian/py3dist-overrides: -------------------------------------------------------------------------------- 1 | picamera python3-picamera 2 | pigpio python3-pigpio 3 | -------------------------------------------------------------------------------- /debian/python3-pytrack.docs: -------------------------------------------------------------------------------- 1 | README.md 2 | docs/telemetry.md 3 | docs/rtty.md 4 | docs/led.md 5 | docs/cgps.md 6 | docs/lora.md 7 | docs/tracker.md 8 | docs/camera.md 9 | -------------------------------------------------------------------------------- /debian/python3-pytrack.postinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | case "$1" in 6 | configure) 7 | ;; 8 | abort-upgrade|abort-remove|abort-deconfigure) 9 | ;; 10 | 11 | *) 12 | echo "postinst called with unknown argument \`$1'" >&2 13 | exit 0 14 | ;; 15 | esac 16 | 17 | #DEBHELPER# 18 | 19 | -------------------------------------------------------------------------------- /debian/pytrack-gps.install: -------------------------------------------------------------------------------- 1 | gps/pytrack-gps /usr/bin/ 2 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | #export DH_VERBOSE = 1 3 | 4 | export PYBUILD_NAME=pytrack 5 | 6 | %: 7 | dh $@ --with python3 --buildsystem=pybuild 8 | 9 | override_dh_auto_test: 10 | 11 | override_dh_clean: 12 | cd gps && make clean 13 | dh_clean 14 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (native) 2 | -------------------------------------------------------------------------------- /debian/source/options: -------------------------------------------------------------------------------- 1 | extend-diff-ignore = "^[^/]*[.]egg-info/" 2 | -------------------------------------------------------------------------------- /docs/camera.md: -------------------------------------------------------------------------------- 1 | # Python camera Module - camera.py 2 | 3 | This library takes care of taking camera photographs, choosing the "best" for transmission, and converting the JPG files to SSDV format for transmission. 4 | 5 | It works from a schedule list which dictate how often photos should be taken, to what resolution, and where they should be stored. Images are taken and converted in a thread so as not to delay the user functions. 6 | 7 | Normally there should be one schedule per radio channel, plus an extra one for full-sized images that are not sent over radio. 8 | 9 | ## Sample Usage 10 | 11 | ```python 12 | from pytrack import SSDVCamera 13 | 14 | MyCam = SSDVCamera() 15 | 16 | MyCam.add_schedule('RTTY', 'PYSKY', 'images/RTTY', Period=30, Width=320, Height=240) 17 | MyCam.add_schedule('LoRa0', 'PYSKY2', 'images/LoRa0', Period=30, Width=640, Height=480) 18 | MyCam.add_schedule('SD', '', 'images/FULL, Period=60, Width=3280, Height=2464) 19 | 20 | MyCam.take_photos() 21 | 22 | Packet = MyCam.get_next_ssdv_packet('RTTY') 23 | ``` 24 | 25 | ## Reference 26 | 27 | ### Object Creation 28 | 29 | SSDVCamera() 30 | 31 | ### Functions 32 | 33 | ```python 34 | add_schedule(Channel, Callsign, TargetFolder, Period, Width, Height, VFlip=False, HFlip=False) 35 | ``` 36 | 37 | where 38 | 39 | - Channel is a unique name for this entry, and is used to retrieve/convert photographs later 40 | - Callsign is used for radio channels, and should be the same as used by telemetry on that channel (it is embedded into SSDV packets) 41 | - TargetFolder is where the JPG files should be saved. It will be created if necessary. Each channel should have its own target folder. 42 | - Period is the time in seconds between photographs. This should be much less than the time taken to transmit an image, so that there are several images to choose from when transmitting. Depending on the combination of schedules, and how long each photograph takes, it may not always (or ever) be possible to maintain the specified periods for all channels. 43 | - Width and Height are self-evident. Take care not to create photographs that take a long time to send. If Width or Height are zero then the full camera resolution (as determined by checking the camera model - Omnivision or Sony) is used. 44 | - VFlip and HFlip can be used to correct images if the camera is not physically oriented correctly. 45 | 46 | 47 | ```python 48 | clear_schedule() 49 | ``` 50 | 51 | Clears the schedule. 52 | 53 | ```python 54 | take_photos(callback=None) 55 | ``` 56 | 57 | Begins execution of the schedule. If the callback is specified, then this is called instead of taking a photo directly. The callback is called with the following parameters: 58 | 59 | filename - name of image file to create 60 | 61 | width - desired image width in pixels (can be ignored) 62 | 63 | height - desired image height in pixels (can be ignored) 64 | 65 | The callback is expected to take a photograph, using whatever method it likes, and with whatever manipulation it likes, creating the file specified by 'filename'. 66 | 67 | ```python 68 | get_next_ssdv_packet() 69 | ``` 70 | 71 | Retrieves the next SSDV packet for a particular channel. If there is none available (i.e. no photograph has been taken and converted yet for this channel) then **None** is returned. Returned packets contain a complete (256-byte) SSDV packet. 72 | 73 | ### Dependencies 74 | 75 | - Needs the Pi Camera to be enabled 76 | - Needs SSDV to be installed 77 | 78 | ## Behind The Scenes 79 | 80 | The library includes "best image selection", by looking for the largest available JPG file. This works better than you might expect, largely eliminating poor images from the radio transmissions. 81 | 82 | The process begins with the schedules as described above. So, for example, RTTY images might be stored in the images/RTTY folder. At some point (see below), the image files are checked and the largest one is selected for conversion to SSDV format. The conversion is done by calling the SSDV program, resulting in an SSDV file ready for transmission. All the JPG files in that folder (i.e. the selected one and the rejected ones) are then moved to a dated folder images/RTTY/, leaving the original folder ready for subsequent images. 83 | 84 | This conversion process is triggered when there are fewer than 10 image packets remaining for transmission. So, on startup, this will happen immediately after the first image is taken, and then again shortly before that first image has been completely sent. 85 | -------------------------------------------------------------------------------- /docs/cgps.md: -------------------------------------------------------------------------------- 1 | # Python GPS Module - cgps.py 2 | 3 | This library provides access to the PITS+ / PITS Zero GPS module. 4 | 5 | ## Basic Usage 6 | 7 | ```python 8 | from pytrack import GPS 9 | from time import sleep 10 | 11 | mygps = GPS() 12 | 13 | while 1: 14 | time.sleep(1) 15 | print ("Position: ", mygps.Position()) 16 | ``` 17 | 18 | When the GPS object is created, it starts to care of all the GPS communications. The current time and position can then be retrieved as required, however note that depending on the position of the GPS aerial, it can take some time before this information is available. 19 | 20 | The time and position are returned as a dict, for example 21 | 22 | ```python 23 | {'lat': 51.95014, 'alt': 171, 'sats': 12, 'lon': -2.54445, 'time': '10:42:35', 'fix': 2} 24 | ``` 25 | 26 | ## Callbacks 27 | 28 | Rather than poll the library (as in the above example), it's neater to provide callback functions so that the library informs us when a new position has arrived. 29 | 30 | ```python 31 | from cgps import * 32 | import time 33 | 34 | def NewPosition(Position): 35 | print("Callback: ", Position) 36 | 37 | def LockChanged(GotLock): 38 | print("Lock " + ("GAINED" if GotLock else "LOST")) 39 | 40 | print("Creating GPS object ...") 41 | mygps = GPS(when_new_position=NewPosition, when_lock_changed=LockChanged) 42 | 43 | print("loop ...") 44 | while 1: 45 | time.sleep(1) 46 | ``` 47 | 48 | ## Reference 49 | 50 | ### Object Creation 51 | 52 | ```python 53 | GPS(WhenNewPosition=None, WhenLockChanged=None) 54 | ``` 55 | 56 | WhenNewPosition and WhenLockChanged are callbacks (see below). 57 | 58 | ### Functions 59 | 60 | 61 | position() 62 | 63 | returns the current GPS position as a dictionary, containing the latest GPS data ('time', 'lat', 'lon', alt', 'sats', 'fix'). These values can be access individually using the properties below (see the descriptions for return types etc.). 64 | 65 | ### Properties 66 | 67 | time 68 | 69 | returns the current time, as a string, format 'hh:mm:ss' 70 | 71 | lat 72 | 73 | returns the current latitude as a floating point number in decimal degrees 74 | 75 | lon 76 | 77 | returns the current longitude as a floating point number in decimal degrees 78 | 79 | alt 80 | 81 | returns the current altitude floating point number in metres 82 | 83 | sats 84 | 85 | returns the current number of satellites used in the position fix. At least 3 are needed for a 2D (horizontal) fix, and at least 4 for a 3D fix 86 | 87 | fix 88 | 89 | returns the fix status (0 is no fix, 1 or more is a fix) 90 | 91 | 92 | ### Callbacks 93 | 94 | ```python 95 | when_new_position(Position) 96 | ``` 97 | 98 | This is called when a new position is received from the GPS (one per second). "Position" is a dict: 99 | 100 | {'lat': 51.95014, 'alt': 171, 'sats': 12, 'lon': -2.54445, 'time': '10:42:35', 'fix': 2} 101 | 102 | Note that all fields are zeroes until the GPS gains a lock. 103 | 104 | when_lock_changed(have_lock) 105 | 106 | This is called when the GPS lock status changes. "have_lock" is True or False 107 | 108 | ### Dependencies 109 | 110 | - Needs the external **gps** program be present in ../gps 111 | - Requires that the Python module **psutil** be installed 112 | 113 | ## Behind the scenes 114 | 115 | This is a wrapper from the separate C GPS program, providing basic GPS functions intended for use in a HAB (High Altitude Balloon) tracker. It starts the program automatically as required. 116 | 117 | That program works with a the UBlox GPS module on a PITS+ or PITS Zero board, using a software i2c implementation. We cannot use the Pi I2C ports because of a hardware limitation in the BCM processor, and we cannot use the Pi serial port if (as is likely) that is in use for RTTY transmissions. It handles the required "flight mode" of the UBlox module, so that it works at altitudes of up to 50km. 118 | 119 | The GPS program is a socket server, sending JSON position data at a rate of 1 position per second, using port 6005. Currently it accepts one client at a time only. 120 | 121 | ## Testing 122 | 123 | Run the supplied test program: 124 | 125 | python3 test_cgps.py 126 | -------------------------------------------------------------------------------- /docs/led.md: -------------------------------------------------------------------------------- 1 | # Python LED Module - led.py 2 | 3 | This library provides access to the PITS+ / PITS Zero LEDS. 4 | 5 | ## Sample Usage 6 | 7 | ```python 8 | from pytrack import PITS_LED 9 | from signal import pause 10 | 11 | print("Creating LED object ...") 12 | status_leds = PITS_LED(); 13 | 14 | status_leds.fail() 15 | pause() 16 | ``` 17 | 18 | This causes the LEDs to both flash. 19 | 20 | 21 | ## Reference 22 | 23 | ### Object Creation 24 | 25 | ```python 26 | leds = PITS_LED(when_new_position, when_lock_changed) 27 | ``` 28 | 29 | when_new_position and when_lock_changed are callbacks (see below). 30 | 31 | ### Functions 32 | 33 | ```python 34 | fail() 35 | ``` 36 | 37 | Blinks both LEDs at 5Hz; used to indicate a catastrophic failure (e.g. tracker software cannot start) 38 | 39 | ```python 40 | gps_lock_status(have_lock) 41 | ``` 42 | Used to indicate if we have GPS lock or not. 43 | 44 | GPS lock is indicated by the green "OK" LED flashing at 2Hz, and the red "Warn" LED off. 45 | 46 | No GPS lock is indicated by the red "Warn" LED flashing at 2Hz, and the green "OK" LED off. 47 | 48 | ## Testing 49 | 50 | Run the supplied test program to flash both LEDs at 5Hz: 51 | 52 | ```python 53 | python3 test_leds.py 54 | ``` 55 | -------------------------------------------------------------------------------- /docs/lora.md: -------------------------------------------------------------------------------- 1 | # Python LoRa Module - lora.py 2 | 3 | This library provides access to one LoRa (Long Range Radio) module on the PITS Zero board or the add-on board for the PITS+ board. 4 | 5 | ## Basic Usage 6 | 7 | from pytrack import LoRa 8 | import time 9 | 10 | print("Create LoRa object") 11 | mylora = LoRa(Channel=0, Frequency=434.450, Mode=1) 12 | 13 | print("Send message") 14 | mylora.send_text("$$Hello World\n") 15 | 16 | while mylora.is_sending(): 17 | time.sleep(0.01) 18 | print("DONE") 19 | 20 | The LoRa object is **non-blocking**. The calling code can either poll to find out when the transmission has completed, or can be notified via a callback. 21 | 22 | ## Reference 23 | 24 | ### Object Creation 25 | 26 | LoRa(Channel=0, Frequency=434.450, Mode=1, DIO0=0) 27 | 28 | **Channel** is the SPI channel that the LoRa device is connected to (0 is for devices connected to the Pi CE0 pin, and 1 for the CE1 pin). For the Uputronics board there are 2 positions labelled CE0 and CE1, so use 0 or 1 depending on which device you want to use; for other boards check their documentation. 29 | 30 | The **frequency** is in MHz and should be selected carefully: 31 | 32 | - If you are using RTTY also, it should be at least 25kHz (0.025MHz) away from the RTTY frequency 33 | - It should be different to that used by any other HAB flights that are airborne at the same time and within 400 miles (600km) 34 | - It should be legal in your country (for the UK see [https://www.ofcom.org.uk/__data/assets/pdf_file/0028/84970/ir_2030-june2014.pdf](https://www.ofcom.org.uk/__data/assets/pdf_file/0028/84970/ir_2030-june2014.pdf "IR2030")) 35 | 36 | **Mode** should be either **0** (best if you are not sending image data over LoRa) or **1** (best if you are). 37 | 38 | When setting up your receiver, use matching settings. 39 | 40 | **DIO0** is the BCM pin that the DIO0 pin of the LoRa device connects to. Setting this to 0 (default) causes the LoRa code to choose a value that matches the Uputronics LoRa HAT; for other boards you should set the value explicitly (again, check the board's documentation). 41 | 42 | 43 | ### Primary Functions 44 | 45 | These are identical those of the RTTY module. 46 | 47 | send_packet(packet, callback=None) 48 | 49 | Sends a binary packet **packet** which should be a **bytes object**. Normally this would be a 256-byte SSDV packet (see the camera.py module). 50 | 51 | **callback**, if used, is called when the packet has been completely set and the RTTY object is ready to accept more data to transmit. 52 | 53 | send_text(sentence, callback=None) 54 | 55 | Sends a text string **sentence**. Normally this would be a UKHAS-compatible HAB telemetry sentence but it can be anything. See the telemetry.py module for how to create compliant telemetry sentences. 56 | 57 | **callback** is as for **send_packet()** 58 | 59 | is_sending() 60 | 61 | returns **True** if the RTTY module is busy sending data, or **False** if not. 62 | 63 | ### Secondary Functions 64 | 65 | These are not needed for most purposes, but may be useful for some users. 66 | 67 | SetLoRaFrequency(Frequency) 68 | 69 | Sets the frequency in MHz. 70 | 71 | SetStandardLoRaParameters(self, Mode) 72 | 73 | Sets the various LoRa parameters to one of the following standard combinations: 74 | 75 | - 0: EXPLICIT_MODE, ERROR_CODING_4_8, BANDWIDTH_20K8, SPREADING_11, LDO On 76 | - 1: IMPLICIT_MODE, ERROR_CODING_4_5, BANDWIDTH_20K8, SPREADING_6, LDO Off 77 | - 2: EXPLICIT_MODE, ERROR_CODING_4_8, BANDWIDTH_62K5, SPREADING_8, LOD Off 78 | 79 | these have the following common usages: 80 | 81 | - 0: Slow mode for sending telemetry with the best range 82 | - 1: Fast mode for sending images and telemetry at the highest rate 83 | - 2: Intermittent mode for sending brief messages periodically 84 | 85 | SetLoRaParameters(ImplicitOrExplicit, ErrorCoding, Bandwidth, SpreadingFactor, LowDataRateOptimize) 86 | 87 | Sets the LoRa parameters to any combination supported by the device. See the LoRa technical manual for details. 88 | 89 | ### Dependencies 90 | 91 | - Needs **SPI** to be enabled 92 | 93 | ## Testing 94 | 95 | Run the supplied test program: 96 | 97 | python3 test_lora.py 98 | -------------------------------------------------------------------------------- /docs/rtty.md: -------------------------------------------------------------------------------- 1 | # Python RTTY Module - rtty.py 2 | 3 | This library provides access to the PITS+ / PITS Zero RTTY (Radio TeleTYpe) module. 4 | 5 | ## Basic Usage 6 | 7 | from pytrack import RTTY 8 | from time import sleep 9 | 10 | print("Create RTTY object") 11 | rtty = RTTY() 12 | 13 | print("Send RTTY Sentence") 14 | rtty.send_text("$$PYSKY,1,10:42:56,51.95023,-2.54445,145,8,21.6*EB9C\n") 15 | 16 | while rtty.is_sending(): 17 | sleep(0.1) 18 | print("FINISHED") 19 | 20 | The RTTY object is **non-blocking**, which means that the calling code does not have to wait for slow operations (i.e. sending an RTTY message) to complete. This is vital, since otherwise the tracker program would not be able to do other things (e.g. send a LoRa packet, or take a photograph) whilst an RTTY transmission (which can take 20 seconds or more) to complete. The calling code can either poll to find out when the transmission has completed, or can be notified via a callback. 21 | 22 | ## Reference 23 | 24 | ### Object Creation 25 | 26 | RTTY(frequency=434.250, baudrate=50) 27 | 28 | The **frequency** is in MHz and should be selected carefully: 29 | 30 | - If you are using LoRa also, it should be at least 25kHz (0.025MHz) away from the LoRa frequency 31 | - It should be different to that used by any other HAB flights that are airborne at the same time and within 400 miles (600km) 32 | - It should be legal in your country (for the UK see [https://www.ofcom.org.uk/__data/assets/pdf_file/0028/84970/ir_2030-june2014.pdf](https://www.ofcom.org.uk/__data/assets/pdf_file/0028/84970/ir_2030-june2014.pdf "IR2030")) 33 | 34 | The **baudrate** should be either **50** (best if you are not sending image data over RTTY) or **300** (best if you are). 35 | 36 | When setting up your receiver, use the following settings: 37 | 38 | - 50 or 300 baud 39 | - 7 data bits (if using 50 baud) or 8 (300 baud) 40 | - no parity 41 | - 2 stop bits 42 | 43 | ### Functions 44 | 45 | send_packet(packet, callback=None) 46 | 47 | Sends a binary packet **packet** which should be a **bytes object**. Normally this would be a 256-byte SSDV packet (see the camera.py module). 48 | 49 | **callback**, if used, is called when the packet has been completely set and the RTTY object is ready to accept more data to transmit. 50 | 51 | send_text(sentence, callback=None) 52 | 53 | Sends a text string **sentence**. Normally this would be a UKHAS-compatible HAB telemetry sentence but it can be anything. See the telemetry.py module for how to create compliant telemetry sentences. 54 | 55 | **callback** is as for **send_packet()** 56 | 57 | is_sending() 58 | 59 | returns **True** if the RTTY module is busy sending data, or **False** if not. 60 | 61 | ### Dependencies 62 | 63 | - Needs **pigpio** to be installed 64 | - Needs the standard serial port **/dev/ttyAMA0** to be present and available 65 | - On a Pi 3B, or any other future Pi with the Bluetooth module, needs an appropriate devtree module installed to remap the main serial port to the GPIO pins. 66 | 67 | ## Behind the scenes 68 | 69 | RTTY is essentially a radio version of serial (e.g. RS232) communications. On a Pi the easiest way of generating RTTY is to send data to the serial port, and have that control the frequency of a suitable radio transmitter. PITS uses the MTX2 radio transmitter which accepts an input voltage to control (modulate) the radio frequency. The serial port and MTX2 are connected together via a simple resistor network which means that the change of frequency is approx 830Hz. 70 | 71 | ## Testing 72 | 73 | Run the supplied test program: 74 | 75 | python3 test_rtty.py 76 | -------------------------------------------------------------------------------- /docs/telemetry.md: -------------------------------------------------------------------------------- 1 | # Python LoRa telemetry - telemetry.py 2 | 3 | This file contains some functions useful for building UKHAS-compatible sentences. See [https://ukhas.org.uk/communication:protocol](https://ukhas.org.uk/communication:protocol "UKHAS Communications Protocol") for more information 4 | 5 | ## Basic Usage 6 | 7 | ```Python 8 | from pytrack import build_sentence 9 | 10 | sentence = build_sentence(values) 11 | ``` 12 | 13 | where **values** is a **list** containing all fields to be combined into a sentence. At a minimum this should have, at the start of the list and in this sequence, the following: 14 | 15 | 1. Payload ID (unique to this payload, and different between RTTY and LoRa) 16 | 2. Count (a counter from 1 upwards) 17 | 3. Time (current UTC (GMT) time) 18 | 4. Latitude (latitude in decimal degrees) 19 | 5. Longitude (longitude in decimal degrees) 20 | 6. Altitude (altitude in metres) 21 | 22 | Subsequent fields are optional. 23 | 24 | The resulting sentence will be of this form: 25 | 26 | $$payload_id,count,time,latitude,longitude,altitude*CRC\n 27 | 28 | where CRC is the CRC16_CCITT code for all characters in the string after the $$ and before the *, and "\n" is linefeed. 29 | 30 | ## Getting Payload Onto The Map 31 | 32 | See [http://www.pi-in-the-sky.com/index.php?id=getting-on-the-map](http://www.pi-in-the-sky.com/index.php?id=getting-on-the-map "this guide") 33 | -------------------------------------------------------------------------------- /docs/tracker.md: -------------------------------------------------------------------------------- 1 | # Python Tracker Module - tracker.py 2 | 3 | This library uses the other modules (GPS, RTTY etc.) to build a complete tracker. 4 | 5 | ## Sample Usage (from pytrack.py) 6 | 7 | ```python 8 | from pytrack import Tracker 9 | from time import sleep 10 | 11 | mytracker = Tracker() 12 | 13 | mytracker.set_rtty(payload_id='PIP1', frequency=434.100, baud_rate=300, image_packet_ratio=4) 14 | mytracker.add_rtty_camera_schedule('images/RTTY', period=60, width=320, height=240) 15 | 16 | mytracker.set_lora(payload_id='PIP2', channel=0, frequency=434.150, mode=1, image_packet_ratio=6) 17 | mytracker.add_lora_camera_schedule('images/LORA', period=60, width=640, height=480) 18 | 19 | mytracker.add_full_camera_schedule('images/FULL', period=60, width=0, height=0) 20 | 21 | mytracker.start() 22 | 23 | while True: 24 | sleep(1) 25 | ``` 26 | ## Reference 27 | 28 | ### Object Creation 29 | 30 | ```python 31 | Tracker() 32 | ``` 33 | 34 | ### Functions 35 | 36 | ```python 37 | set_rtty(payload_id='PIP1', frequency=434.100, baud_rate=300, image_packet_ratio=4)` 38 | ``` 39 | 40 | This sets the RTTY payload ID, radio frequency, baud rate (use 50 for telemetry only, 300 (faster) if you want to include image data), and ratio of image packets to telemetry packets. 41 | 42 | If you don't want RTTY transmissions, just don't call this function. 43 | 44 | Note that the RTTY stream will only include image packets if you add a camera schedule (see add_rtty_camera_schedule) 45 | 46 | ```python 47 | set_lora(payload_id='PIP2', channel=0, frequency=434.150, mode=1, DIO0=0, image_packet_ratio=6) 48 | ``` 49 | 50 | This sets the LoRa payload ID, radio frequency, mode (use 0 for telemetry-only; 1 (which is faster) if you want to include images), and ratio of image packets to telemetry packets. 51 | 52 | If you don't want LoRa transmissions, just don't call this function. 53 | 54 | For details of the parameters, see the documentation in [lora.md](lora.md) 55 | 56 | Note that the LoRa stream will only include image packets if you add a camera schedule (see add_lora_camera_schedule) 57 | 58 | ```python 59 | add_rtty_camera_schedule('images/RTTY', period=60, width=320, height=240) 60 | ``` 61 | 62 | Adds an RTTY camera schedule. For this example, an image of size 320x240 pixels will be taken every 60 seconds and the resulting file saved in the images/RTTY folder. 63 | 64 | Similar functions exist for the LoRa channel and for full-sized images: 65 | 66 | ```python 67 | add_lora_camera_schedule('images/LORA', period=60, width=640, height=480) 68 | 69 | add_full_camera_schedule('images/FULL', period=60, width=0, height=0) 70 | ``` 71 | 72 | Note that specifying width and height of zero results in the camera taking full-sized images (exact resolution depends on the camera model), and that this is the default if those parameters are not included in the call. 73 | 74 | ```python 75 | start() 76 | ``` 77 | 78 | Starts the tracker. Specifically, it: 79 | 80 | 1. Loads the LED module to control the LEDs according to GPS status 81 | 2. Loads the temperature module to periodically measure temperature on the PITS board 82 | 3. Loads the GPS module to get GPS positions 83 | 4. Loads the camera module (if at least one camera schedule was added) to follow those schedules 84 | 5. Creates a thread to send telemetry and camera image packets to RTTY and/or LoRa radios 85 | 86 | ```python 87 | set_sentence_callback(extra_telemetry) 88 | ``` 89 | 90 | This specifies a function to be called whenever a telemetry sentence is built. That function should return a string containing a comma-separated list of fields to append to the telemetry sentence. e.g.: 91 | 92 | ```python 93 | def extra_telemetry(): 94 | extra_value1 = 123.4 95 | extra_value2 = 42 96 | return "{:.1f}".format(extra_value1) + ',' + "{:.0f}".format(extra_value2) 97 | ``` 98 | 99 | There is also a callback for images: 100 | 101 | ```python 102 | set_image_callback(take_photo) 103 | ``` 104 | 105 | The callback function is called whenever an image is required. **If you specify this callback, then it's up to you to provide code to take the photograph**. Here's an example: 106 | 107 | ```python 108 | def take_photo(filename, width, height, gps): 109 | with picamera.PiCamera() as camera: 110 | camera.resolution = (width, height) 111 | camera.start_preview() 112 | time.sleep(2) 113 | camera.capture(filename) 114 | camera.stop_preview() 115 | ``` 116 | 117 | Use the gps object if you want to add a telemetry overlay, or use different image sizes at different altitudes, for example. 118 | -------------------------------------------------------------------------------- /gps/Makefile: -------------------------------------------------------------------------------- 1 | SRC=$(wildcard *.c) 2 | HED=$(wildcard *.h) 3 | OBJ=$(SRC:.c=.o) # replaces the .c from SRC with .o 4 | EXE=pytrack-gps 5 | 6 | INDOPT= -bap -bl -blf -bli0 -brs -cbi0 -cdw -cs -ci4 -cli4 -i4 -ip0 -nbc -nce -lp -npcs -nut -pmt -psl -prs -ts4 7 | 8 | CC=gcc 9 | CFLAGS=-Wall -O3 -g #-std=c99 10 | LDFLAGS= -lm -lwiringPi -lpthread 11 | RM=rm 12 | 13 | %.o: %.c *.h # combined w/ next line will compile recently changed .c files 14 | $(CC) $(CFLAGS) -o $@ -c $< 15 | 16 | .PHONY : all # .PHONY ignores files named all 17 | all: $(EXE) # all is dependent on $(EXE) to be complete 18 | 19 | $(EXE): $(OBJ) # $(EXE) is dependent on all of the files in $(OBJ) to exist 20 | $(CC) $(OBJ) $(LDFLAGS) -o $@ 21 | 22 | .PHONY : clean # .PHONY ignores files named clean 23 | clean: 24 | -$(RM) $(OBJ) 25 | 26 | tidy: 27 | indent $(INDOPT) $(SRC) $(HED) 28 | rm *~ 29 | -------------------------------------------------------------------------------- /gps/gps: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raspberrypi/pytrack/e39f33452d8354664c80d97fcff4e69318933ab8/gps/gps -------------------------------------------------------------------------------- /gps/gps.c: -------------------------------------------------------------------------------- 1 | /* ========================================================================== */ 2 | /* gps.c */ 3 | /* */ 4 | /* GPS program using software i2c for ublox */ 5 | /* */ 6 | /* ========================================================================== */ 7 | 8 | #define _GNU_SOURCE 1 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include "gps.h" 26 | #include "ublox.h" 27 | #include "server.h" 28 | 29 | 30 | int main(void) 31 | { 32 | pthread_t GPSThread, ServerThread; 33 | struct TGPS GPS; 34 | 35 | memset((void *)&GPS, 0, sizeof(GPS)); 36 | 37 | if (pthread_create(&GPSThread, NULL, GPSLoop, &GPS)) 38 | { 39 | fprintf(stderr, "Error creating GPS thread\n"); 40 | return 1; 41 | } 42 | 43 | if (pthread_create(&ServerThread, NULL, ServerLoop, &GPS)) 44 | { 45 | fprintf(stderr, "Error creating Server thread\n"); 46 | return 1; 47 | } 48 | 49 | while (1) 50 | { 51 | sleep(1); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /gps/gps.h: -------------------------------------------------------------------------------- 1 | struct TGPS 2 | { 3 | // long Time; // Time as read from GPS, as an integer but 12:13:14 is 121314 4 | long SecondsInDay; // Time in seconds since midnight 5 | int Hours, Minutes, Seconds; 6 | float Longitude, Latitude; 7 | int32_t Altitude, MaximumAltitude; 8 | unsigned int Satellites; 9 | int FixType; 10 | // int Speed; 11 | // int Direction; 12 | // int FlightMode; 13 | // int PowerMode; 14 | // int Lock; 15 | }; 16 | -------------------------------------------------------------------------------- /gps/server.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include // Standard input/output definitions 6 | #include // String function definitions 7 | #include // File control definitions 8 | #include // UNIX standard function definitions 9 | #include // Error number definitions 10 | #include // POSIX terminal control definitions 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | #include "server.h" 22 | #include "gps.h" 23 | 24 | void error(char *msg) 25 | { 26 | perror(msg); 27 | exit(1); 28 | } 29 | 30 | int SendJSON(int connfd, struct TGPS *GPS) 31 | { 32 | int OK; 33 | char sendBuff[1025]; 34 | 35 | OK = 1; 36 | 37 | memset(sendBuff, '0', sizeof(sendBuff )); 38 | 39 | sprintf(sendBuff, "{\"time\":\"%02d:%02d:%02d\",\"lat\":%.5lf,\"lon\":%.5lf,\"alt\":%d,\"sats\":%d,\"fix\":%d}\r\n", 40 | GPS->Hours, GPS->Minutes, GPS->Seconds, 41 | GPS->Latitude, GPS->Longitude, 42 | GPS->Altitude, 43 | GPS->Satellites, 44 | GPS->FixType); 45 | 46 | if (send(connfd, sendBuff, strlen(sendBuff), MSG_NOSIGNAL ) <= 0) 47 | { 48 | printf( "Disconnected from client\n" ); 49 | OK = 0; 50 | } 51 | 52 | return OK; 53 | } 54 | 55 | void ProcessSocket(int sock, struct TGPS *GPS) 56 | { 57 | int OK=1; 58 | 59 | while (OK) 60 | { 61 | OK = SendJSON(sock, GPS); 62 | delay(1000); 63 | } 64 | } 65 | 66 | /* 67 | sockfd = socket(AF_INET, SOCK_STREAM, 0); 68 | memset(&serv_addr, '0', sizeof(serv_addr)); 69 | 70 | serv_addr.sin_family = AF_INET; 71 | serv_addr.sin_addr.s_addr = htonl( INADDR_ANY ); 72 | serv_addr.sin_port = htons(Port); 73 | 74 | printf("Listening on JSON port %d\n", Port); 75 | 76 | if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &(int){1}, sizeof(int)) < 0) 77 | { 78 | printf("setsockopt(SO_REUSEADDR) failed" ); 79 | } 80 | 81 | if (bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) 82 | { 83 | printf("Server failed errno %d\n", errno ); 84 | exit( -1 ); 85 | } 86 | 87 | listen(sockfd, 10); 88 | 89 | fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFL) & ~O_NONBLOCK); // Blocking mode so we wait for a connection 90 | 91 | connfd = accept(sockfd, ( struct sockaddr * ) NULL, NULL); // Wait for connection 92 | 93 | printf("Connected to client\n"); 94 | Connected = 1; 95 | 96 | // fcntl(connfd, F_SETFL, fcntl(sockfd, F_GETFL) | O_NONBLOCK); // Non-blocking, so we don't block on receiving any commands from client 97 | 98 | while (Connected) 99 | { 100 | if (SendJSON(connfd, GPS)) 101 | { 102 | Connected = 0; 103 | } 104 | else 105 | { 106 | delay(1000); 107 | } 108 | } 109 | 110 | printf("Close connection\n"); 111 | close(connfd); 112 | */ 113 | 114 | void *ServerLoop(void *some_void_ptr) 115 | { 116 | struct TGPS *GPS; 117 | int portno; 118 | int sockfd, newsockfd; 119 | struct sockaddr_in serv_addr, cli_addr; 120 | 121 | GPS = (struct TGPS *)some_void_ptr; 122 | 123 | while (1) 124 | { 125 | // int pid; 126 | unsigned int clilen; 127 | 128 | sockfd = socket(AF_INET, SOCK_STREAM, 0); 129 | if (sockfd < 0) 130 | error("ERROR opening socket"); 131 | 132 | bzero((char *) &serv_addr, sizeof(serv_addr)); 133 | 134 | portno = 6005; 135 | serv_addr.sin_family = AF_INET; 136 | serv_addr.sin_addr.s_addr = INADDR_ANY; 137 | serv_addr.sin_port = htons(portno); 138 | 139 | if (bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) 140 | { 141 | error("ERROR on binding"); 142 | } 143 | 144 | listen(sockfd, 10); 145 | printf("Listening on JSON port %d\n", portno); 146 | 147 | clilen = sizeof(cli_addr); 148 | 149 | // while (1) 150 | { 151 | newsockfd = accept(sockfd, (struct sockaddr *)&cli_addr, &clilen); 152 | 153 | if (newsockfd < 0) 154 | error("ERROR on accept"); 155 | // pid = fork(); 156 | // if (pid < 0) 157 | // error("ERROR on fork"); 158 | // if (pid == 0) 159 | // { 160 | printf("sockfd=%d, newsockfd=%d\n", sockfd, newsockfd); 161 | close(sockfd); 162 | ProcessSocket(newsockfd, GPS); 163 | // exit(0); 164 | // } 165 | // else 166 | // { 167 | // close(newsockfd); 168 | // } 169 | } 170 | } 171 | 172 | return NULL; 173 | } 174 | -------------------------------------------------------------------------------- /gps/server.h: -------------------------------------------------------------------------------- 1 | void *ServerLoop(void *some_void_ptr); 2 | -------------------------------------------------------------------------------- /gps/ublox.c: -------------------------------------------------------------------------------- 1 | // /* ========================================================================== */ 2 | /* ublox.c */ 3 | /* */ 4 | /* i2c bit-banging code for ublox on Pi A/A+/B/B+ */ 5 | /* */ 6 | /* Description */ 7 | /* */ 8 | /* 12/10/14: Modified for the UBlox Max8 on the B+ board */ 9 | /* 19/12/14: Rewritten to use wiringPi library */ 10 | /* */ 11 | /* */ 12 | /* ========================================================================== */ 13 | 14 | #define _GNU_SOURCE 1 15 | 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include "gps.h" 31 | #include "ublox.h" 32 | 33 | struct gps_info { 34 | uint8_t address; // 7 bit address 35 | uint8_t sda; // pin used for sda coresponds to gpio 36 | uint8_t scl; // clock 37 | uint32_t clock_delay; // proportional to bus speed 38 | uint32_t timeout; 39 | int Failed; 40 | }; 41 | 42 | 43 | // ***************************************************************************** 44 | // open bus, sets structure and initialises GPIO 45 | // The scl and sda line are set to be always 0 (low) output, when a high is 46 | // required they are set to be an input. 47 | // ***************************************************************************** 48 | int OpenGPSPort(struct gps_info *bb, 49 | uint8_t adr, // 7 bit address 50 | uint8_t data, // GPIO pin for data 51 | uint8_t clock, // GPIO pin for clock 52 | uint32_t delay, // clock delay us 53 | uint32_t timeout) // clock stretch & timeout 54 | { 55 | bb->Failed = 0; 56 | 57 | bb->address = adr; 58 | bb->sda = data; 59 | bb->scl = clock; 60 | bb->clock_delay = delay; 61 | bb->timeout = timeout; 62 | 63 | // also they should be set low, input - output determines level 64 | pinMode(bb->sda, INPUT); 65 | pinMode(bb->scl, INPUT); 66 | 67 | digitalWrite(bb->sda, LOW); 68 | digitalWrite(bb->scl, LOW); 69 | 70 | pullUpDnControl(bb->sda, PUD_UP); 71 | pullUpDnControl(bb->scl, PUD_UP); 72 | 73 | printf("Opened I2C GPS Port\n"); 74 | 75 | return bb->Failed; 76 | } 77 | 78 | void BitDelay(uint32_t delay) 79 | { 80 | struct timespec sleeper, dummy ; 81 | 82 | sleeper.tv_sec = 0; 83 | sleeper.tv_nsec = delay; 84 | nanosleep (&sleeper, &dummy) ; 85 | } 86 | 87 | void CloseGPSPort(struct gps_info *bb) 88 | { 89 | int i; 90 | 91 | pinMode(bb->sda, INPUT); 92 | digitalWrite(bb->scl, LOW); 93 | 94 | for (i=0; i<16; i++) 95 | { 96 | pinMode(bb->scl, OUTPUT); 97 | BitDelay(bb->clock_delay); 98 | pinMode(bb->scl, INPUT); 99 | BitDelay(bb->clock_delay); 100 | } 101 | } 102 | 103 | 104 | // ***************************************************************************** 105 | // clock with stretch - bit level 106 | // puts clock line high and checks that it does go high. When bit level 107 | // stretching is used the clock needs checking at each transition 108 | // ***************************************************************************** 109 | void I2CClockHigh(struct gps_info *bb) 110 | { 111 | uint32_t to = bb->timeout; 112 | 113 | pinMode(bb->scl, INPUT); 114 | 115 | // check that it is high 116 | while (!digitalRead(bb->scl)) 117 | { 118 | BitDelay(1000); 119 | if(!to--) 120 | { 121 | fprintf(stderr, "gps_info: Clock line held by slave\n"); 122 | bb->Failed = 1; 123 | return; 124 | } 125 | } 126 | } 127 | 128 | void I2CClockLow(struct gps_info *bb) 129 | { 130 | pinMode(bb->scl, OUTPUT); 131 | } 132 | 133 | void I2CDataLow(struct gps_info *bb) 134 | { 135 | pinMode(bb->sda, OUTPUT); 136 | } 137 | 138 | void I2CDataHigh(struct gps_info *bb) 139 | { 140 | pinMode(bb->sda, INPUT); 141 | } 142 | 143 | 144 | // ***************************************************************************** 145 | // Returns 1 if bus is free, i.e. both sda and scl high 146 | // ***************************************************************************** 147 | int BusIsFree(struct gps_info *bb) 148 | { 149 | return digitalRead(bb->sda) && digitalRead(bb->scl); 150 | 151 | } 152 | 153 | // ***************************************************************************** 154 | // Start condition 155 | // This is when sda is pulled low when clock is high. This also puls the clock 156 | // low ready to send or receive data so both sda and scl end up low after this. 157 | // ***************************************************************************** 158 | void I2CStart(struct gps_info *bb) 159 | { 160 | uint32_t to = bb->timeout; 161 | // bus must be free for start condition 162 | while(to-- && !BusIsFree(bb)) 163 | { 164 | BitDelay(1000); 165 | } 166 | 167 | if (!BusIsFree(bb)) 168 | { 169 | fprintf(stderr, "gps_info: Cannot set start condition\n"); 170 | bb->Failed = 1; 171 | return; 172 | } 173 | 174 | // start condition is when data linegoes low when clock is high 175 | I2CDataLow(bb); 176 | BitDelay((bb->clock_delay)/2); 177 | I2CClockLow(bb); 178 | BitDelay(bb->clock_delay); 179 | } 180 | 181 | 182 | // ***************************************************************************** 183 | // stop condition 184 | // when the clock is high, sda goes from low to high 185 | // ***************************************************************************** 186 | void I2CStop(struct gps_info *bb) 187 | { 188 | I2CDataLow(bb); 189 | 190 | BitDelay(bb->clock_delay); 191 | 192 | I2CClockHigh(bb); // clock will be low from read/write, put high 193 | 194 | BitDelay(bb->clock_delay); 195 | 196 | I2CDataHigh(bb); 197 | } 198 | 199 | // ***************************************************************************** 200 | // sends a byte to the bus, this is an 8 bit unit so could be address or data 201 | // msb first 202 | // returns 1 for NACK and 0 for ACK (0 is good) 203 | // ***************************************************************************** 204 | int I2CSend(struct gps_info *bb, uint8_t value) 205 | { 206 | uint32_t rv; 207 | uint8_t j, mask=0x80; 208 | 209 | // clock is already low from start condition 210 | for(j=0;j<8;j++) 211 | { 212 | BitDelay(bb->clock_delay); 213 | if (value & mask) 214 | { 215 | I2CDataHigh(bb); 216 | } 217 | else 218 | { 219 | I2CDataLow(bb); 220 | } 221 | // clock out data 222 | I2CClockHigh(bb); // clock it out 223 | BitDelay(bb->clock_delay); 224 | I2CClockLow(bb); // back to low so data can change 225 | mask>>= 1; // next bit along 226 | } 227 | // release bus for slave ack or nack 228 | I2CDataHigh(bb); 229 | BitDelay(bb->clock_delay); 230 | I2CClockHigh(bb); // and clock high tels slave to NACK/ACK 231 | BitDelay(bb->clock_delay); // delay for slave to act 232 | rv = digitalRead(bb->sda); // get ACK, NACK from slave 233 | 234 | I2CClockLow(bb); 235 | BitDelay(bb->clock_delay); 236 | return rv; 237 | } 238 | 239 | // ***************************************************************************** 240 | // receive 1 char from bus 241 | // Input 242 | // send: 1=nack, (last byte) 0 = ack (get another) 243 | // ***************************************************************************** 244 | uint8_t I2CRead(struct gps_info *bb, uint8_t ack) 245 | { 246 | uint8_t j, data=0; 247 | 248 | for (j=0;j<8;j++) 249 | { 250 | data<<= 1; // shift in 251 | BitDelay(bb->clock_delay); 252 | I2CClockHigh(bb); // set clock high to get data 253 | BitDelay(bb->clock_delay); // delay for slave 254 | 255 | if (digitalRead(bb->sda)) data++; // get data 256 | 257 | I2CClockLow(bb); 258 | } 259 | 260 | // clock has been left low at this point 261 | // send ack or nack 262 | BitDelay(bb->clock_delay); 263 | 264 | if (ack) 265 | { 266 | I2CDataHigh(bb); 267 | } 268 | else 269 | { 270 | I2CDataLow(bb); 271 | } 272 | 273 | BitDelay(bb->clock_delay); 274 | I2CClockHigh(bb); // clock it in 275 | BitDelay(bb->clock_delay); 276 | I2CClockLow(bb); 277 | I2CDataHigh(bb); 278 | 279 | return data; 280 | } 281 | 282 | // ***************************************************************************** 283 | // writes buffer 284 | // ***************************************************************************** 285 | void I2Cputs(struct gps_info *bb, uint8_t *s, uint32_t len) 286 | { 287 | I2CStart(bb); 288 | I2CSend(bb, bb->address * 2); // address 289 | while(len) { 290 | I2CSend(bb, *(s++)); 291 | len--; 292 | } 293 | I2CStop(bb); // stop 294 | } 295 | 296 | // ***************************************************************************** 297 | // read one byte from GPS 298 | // ***************************************************************************** 299 | uint8_t GPSGetc(struct gps_info *bb) 300 | { 301 | uint8_t Character; 302 | 303 | Character = 0xFF; 304 | 305 | I2CStart(bb); 306 | I2CSend(bb, (bb->address * 2)+1); // address 307 | Character = I2CRead(bb, 1); 308 | I2CStop(bb); // stop 309 | 310 | return Character; 311 | } 312 | 313 | char Hex(unsigned char Character) 314 | { 315 | char HexTable[] = "0123456789ABCDEF"; 316 | 317 | return HexTable[Character & 15]; 318 | } 319 | 320 | int GPSChecksumOK(char *Buffer, int Count) 321 | { 322 | unsigned char XOR, i, c; 323 | 324 | XOR = 0; 325 | for (i = 1; i < (Count-4); i++) 326 | { 327 | c = Buffer[i]; 328 | XOR ^= c; 329 | } 330 | 331 | return (Buffer[Count-4] == '*') && (Buffer[Count-3] == Hex(XOR >> 4)) && (Buffer[Count-2] == Hex(XOR & 15)); 332 | } 333 | 334 | void FixUBXChecksum(unsigned char *Message, int Length) 335 | { 336 | int i; 337 | unsigned char CK_A, CK_B; 338 | 339 | CK_A = 0; 340 | CK_B = 0; 341 | 342 | for (i=2; i<(Length-2); i++) 343 | { 344 | CK_A = CK_A + Message[i]; 345 | CK_B = CK_B + CK_A; 346 | } 347 | 348 | Message[Length-2] = CK_A; 349 | Message[Length-1] = CK_B; 350 | } 351 | 352 | 353 | void SendUBX(struct gps_info *bb, unsigned char *MSG, int len) 354 | { 355 | I2Cputs(bb, MSG, len); 356 | } 357 | 358 | void SetFlightMode(struct gps_info *bb) 359 | { 360 | // Send navigation configuration command 361 | unsigned char setNav[] = {0xB5, 0x62, 0x06, 0x24, 0x24, 0x00, 0xFF, 0xFF, 0x06, 0x03, 0x00, 0x00, 0x00, 0x00, 0x10, 0x27, 0x00, 0x00, 0x05, 0x00, 0xFA, 0x00, 0xFA, 0x00, 0x64, 0x00, 0x2C, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0xDC}; 362 | SendUBX(bb, setNav, sizeof(setNav)); 363 | printf ("Setting flight mode\n"); 364 | } 365 | 366 | void SetPowerMode(struct gps_info *bb, int SavePower) 367 | { 368 | unsigned char setPSM[] = {0xB5, 0x62, 0x06, 0x11, 0x02, 0x00, 0x08, 0x01, 0x22, 0x92 }; 369 | 370 | setPSM[7] = SavePower ? 1 : 0; 371 | 372 | printf ("Setting power-saving %s\n", SavePower ? "ON" : "OFF"); 373 | 374 | FixUBXChecksum(setPSM, sizeof(setPSM)); 375 | 376 | SendUBX(bb, setPSM, sizeof(setPSM)); 377 | } 378 | 379 | void setGPS_GNSS(struct gps_info *bb) 380 | { 381 | // Sets CFG-GNSS to disable everything other than GPS GNSS 382 | // solution. Failure to do this means GPS power saving 383 | // doesn't work. Not needed for MAX7, needed for MAX8's 384 | unsigned char setgnss[] = { 385 | 0xB5, 0x62, 0x06, 0x3E, 0x2C, 0x00, 0x00, 0x00, 386 | 0x20, 0x05, 0x00, 0x08, 0x10, 0x00, 0x01, 0x00, 387 | 0x01, 0x01, 0x01, 0x01, 0x03, 0x00, 0x00, 0x00, 388 | 0x01, 0x01, 0x03, 0x08, 0x10, 0x00, 0x00, 0x00, 389 | 0x01, 0x01, 0x05, 0x00, 0x03, 0x00, 0x00, 0x00, 390 | 0x01, 0x01, 0x06, 0x08, 0x0E, 0x00, 0x00, 0x00, 391 | 0x01, 0x01, 0xFC, 0x11 }; 392 | 393 | printf ("Disabling GNSS\n"); 394 | 395 | SendUBX(bb, setgnss, sizeof(setgnss)); 396 | } 397 | 398 | void setGPS_DynamicModel6(struct gps_info *bb) 399 | { 400 | uint8_t setdm6[] = { 401 | 0xB5, 0x62, 0x06, 0x24, 0x24, 0x00, 0xFF, 0xFF, 0x06, 402 | 0x03, 0x00, 0x00, 0x00, 0x00, 0x10, 0x27, 0x00, 0x00, 403 | 0x05, 0x00, 0xFA, 0x00, 0xFA, 0x00, 0x64, 0x00, 0x2C, 404 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 405 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0xDC }; 406 | 407 | printf ("Setting dynamic model 6\n"); 408 | 409 | SendUBX(bb, setdm6, sizeof(setdm6)); 410 | } 411 | 412 | float FixPosition(float Position) 413 | { 414 | float Minutes, Seconds; 415 | 416 | Position = Position / 100; 417 | 418 | Minutes = trunc(Position); 419 | Seconds = fmod(Position, 1); 420 | 421 | return Minutes + Seconds * 5 / 3; 422 | } 423 | 424 | time_t day_seconds() 425 | { 426 | time_t t1, t2; 427 | struct tm tms; 428 | time(&t1); 429 | localtime_r(&t1, &tms); 430 | tms.tm_hour = 0; 431 | tms.tm_min = 0; 432 | tms.tm_sec = 0; 433 | t2 = mktime(&tms); 434 | return t1 - t2; 435 | } 436 | 437 | void ProcessLine(struct gps_info *bb, struct TGPS *GPS, char *Buffer, int Count) 438 | { 439 | // static int SystemTimeHasBeenSet=0; 440 | 441 | float utc_time, latitude, longitude, hdop, altitude; 442 | int lock, satellites; 443 | char ns, ew, units; // *date, *ptr, speedstring[16], *course, restofline[80], timestring[16], active 444 | 445 | if (GPSChecksumOK(Buffer, Count)) 446 | { 447 | satellites = 0; 448 | 449 | if (strncmp(Buffer+3, "GGA", 3) == 0) 450 | { 451 | if (sscanf(Buffer+7, "%f,%f,%c,%f,%c,%d,%d,%f,%f,%c", &utc_time, &latitude, &ns, &longitude, &ew, &lock, &satellites, &hdop, &altitude, &units) >= 1) 452 | { 453 | // $GPGGA,124943.00,5157.01557,N,00232.66381,W,1,09,1.01,149.3,M,48.6,M,,*42 454 | if (satellites >= 4) 455 | { 456 | unsigned long utc_seconds; 457 | utc_seconds = utc_time; 458 | GPS->Hours = utc_seconds / 10000; 459 | GPS->Minutes = (utc_seconds / 100) % 100; 460 | GPS->Seconds = utc_seconds % 100; 461 | // GPS->SecondsInDay = GPS->Hours * 3600 + GPS->Minutes * 60 + GPS->Seconds; 462 | // printf("\nGGA: %ld seconds offset\n\n", GPS->SecondsInDay - day_seconds()); 463 | GPS->Latitude = FixPosition(latitude); 464 | if (ns == 'S') GPS->Latitude = -GPS->Latitude; 465 | GPS->Longitude = FixPosition(longitude); 466 | if (ew == 'W') GPS->Longitude = -GPS->Longitude; 467 | 468 | // if (GPS->Altitude <= 0) 469 | // { 470 | // GPS->AscentRate = 0; 471 | // } 472 | // else 473 | // { 474 | // GPS->AscentRate = GPS->AscentRate * 0.7 + ((int32_t)altitude - GPS->Altitude) * 0.3; 475 | // } 476 | // printf("Altitude=%ld, AscentRate = %.1lf\n", GPS->Altitude, GPS->AscentRate); 477 | GPS->Altitude = altitude; 478 | // if (GPS->Altitude > GPS->MaximumAltitude) GPS->MaximumAltitude = GPS->Altitude; 479 | } 480 | GPS->FixType = lock; 481 | GPS->Satellites = satellites; 482 | } 483 | // if (Config.EnableGPSLogging) 484 | // { 485 | // WriteLog("gps.txt", Buffer); 486 | // } 487 | } 488 | else if (strncmp(Buffer+3, "RMC", 3) == 0) 489 | { 490 | // speedstring[0] = '\0'; 491 | // if (sscanf(Buffer+7, "%[^,],%c,%f,%c,%f,%c,%[^,],%s", timestring, &active, &latitude, &ns, &longitude, &ew, speedstring, restofline) >= 7) 492 | // { 493 | // $GPRMC,124943.00,A,5157.01557,N,00232.66381,W,0.039,,200314,,,A*6C 494 | 495 | // ptr = restofline; 496 | 497 | // course = strsep(&ptr, ","); 498 | 499 | // date = strsep(&ptr, ","); 500 | 501 | // GPS->Speed = (int)atof(speedstring); 502 | // GPS->Direction = (int)atof(course); 503 | 504 | // if ((atof(timestring) > 0) && !SystemTimeHasBeenSet) 505 | // { 506 | // struct tm tm; 507 | // char timedatestring[32]; 508 | // time_t t; 509 | 510 | // Now create a tm structure from our date and time 511 | // memset(&tm, 0, sizeof(struct tm)); 512 | // sprintf(timedatestring, "%c%c-%c%c-20%c%c %c%c:%c%c:%c%c", 513 | // date[0], date[1], date[2], date[3], date[4], date[5], 514 | // timestring[0], timestring[1], timestring[2], timestring[3], timestring[4], timestring[5]); 515 | // strptime(timedatestring, "%d-%m-%Y %H:%M:%S", &tm); 516 | 517 | // t = mktime(&tm); 518 | // if (stime(&t) == -1) 519 | // { 520 | // printf("Failed to set system time\n"); 521 | // } 522 | // else 523 | // { 524 | // printf("System time set from GPS time\n"); 525 | // SystemTimeHasBeenSet = 1; 526 | // } 527 | // } 528 | // } 529 | 530 | // if (Config.EnableGPSLogging) 531 | // { 532 | // WriteLog("gps.txt", Buffer); 533 | // } 534 | } 535 | else if (strncmp(Buffer+3, "GSV", 3) == 0) 536 | { 537 | // Disable GSV 538 | printf("Disabling GSV\r\n"); 539 | unsigned char setGSV[] = { 0xB5, 0x62, 0x06, 0x01, 0x08, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x03, 0x39 }; 540 | SendUBX(bb, setGSV, sizeof(setGSV)); 541 | } 542 | else if (strncmp(Buffer+3, "GLL", 3) == 0) 543 | { 544 | // Disable GLL 545 | printf("Disabling GLL\r\n"); 546 | unsigned char setGLL[] = { 0xB5, 0x62, 0x06, 0x01, 0x08, 0x00, 0xF0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x2B }; 547 | SendUBX(bb, setGLL, sizeof(setGLL)); 548 | } 549 | else if (strncmp(Buffer+3, "GSA", 3) == 0) 550 | { 551 | // Disable GSA 552 | printf("Disabling GSA\r\n"); 553 | unsigned char setGSA[] = { 0xB5, 0x62, 0x06, 0x01, 0x08, 0x00, 0xF0, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x32 }; 554 | SendUBX(bb, setGSA, sizeof(setGSA)); 555 | } 556 | else if (strncmp(Buffer+3, "VTG", 3) == 0) 557 | { 558 | // Disable VTG 559 | printf("Disabling VTG\r\n"); 560 | unsigned char setVTG[] = {0xB5, 0x62, 0x06, 0x01, 0x08, 0x00, 0xF0, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x05, 0x47}; 561 | SendUBX(bb, setVTG, sizeof(setVTG)); 562 | } 563 | else 564 | { 565 | printf("Unknown NMEA sentence: %s\n", Buffer); 566 | } 567 | } 568 | else 569 | { 570 | printf("Bad checksum\r\n"); 571 | } 572 | } 573 | 574 | 575 | void *GPSLoop(void *some_void_ptr) 576 | { 577 | char Line[100]; 578 | int Length; 579 | struct gps_info bb; 580 | int SentenceCount, SDA, SCL, Power_Saving; 581 | struct TGPS *GPS; 582 | 583 | GPS = (struct TGPS *)some_void_ptr; 584 | 585 | Length = 0; 586 | SentenceCount = 0; 587 | Power_Saving = 0; 588 | 589 | if (wiringPiSetup() == -1) 590 | { 591 | printf("Cannot initialise WiringPi\n"); 592 | exit (1); 593 | } 594 | 595 | // pinMode(2, OUTPUT); 596 | // digitalWrite(2, LOW); 597 | 598 | while (1) 599 | { 600 | unsigned char Character; 601 | 602 | // printf ("SDA/SCL = %d/%d\n", Config.SDA, Config.SCL); 603 | 604 | SDA = 2; // 5; // 2; 605 | SCL = 3; // 6; // 3; 606 | 607 | if (OpenGPSPort(&bb, 0x42, SDA, SCL, 2000, 100)) // struct, i2c address, SDA, SCL, ns clock delay, timeout ms 608 | { 609 | printf("Failed to open GPS\n"); 610 | bb.Failed = 1; 611 | } 612 | 613 | while (!bb.Failed) 614 | { 615 | Character = GPSGetc(&bb); 616 | // if (Character == 0xFF) printf("."); else printf("%c", Character); 617 | 618 | if (Character == 0xFF) 619 | { 620 | delay(100); 621 | } 622 | else if (Character == '$') 623 | { 624 | Line[0] = Character; 625 | Length = 1; 626 | } 627 | else if (Length > 90) 628 | { 629 | Length = 0; 630 | } 631 | else if ((Length > 0) && (Character != '\r')) 632 | { 633 | Line[Length++] = Character; 634 | if (Character == '\n') 635 | { 636 | Line[Length] = '\0'; 637 | ProcessLine(&bb, GPS, Line, Length); 638 | printf("%s", Line); 639 | 640 | if (++SentenceCount > 100) SentenceCount = 0; 641 | 642 | if ((SentenceCount == 10) && Power_Saving) 643 | { 644 | setGPS_GNSS(&bb); 645 | } 646 | else if ((SentenceCount == 20) && Power_Saving) 647 | { 648 | setGPS_DynamicModel6(&bb); 649 | } 650 | else if (SentenceCount == 30) 651 | { 652 | SetPowerMode(&bb, Power_Saving && (GPS->Satellites > 4)); 653 | } 654 | else if (SentenceCount == 40) 655 | { 656 | SetFlightMode(&bb); 657 | } 658 | 659 | Length = 0; 660 | delay(100); 661 | } 662 | } 663 | } 664 | 665 | CloseGPSPort(&bb); 666 | } 667 | } 668 | -------------------------------------------------------------------------------- /gps/ublox.h: -------------------------------------------------------------------------------- 1 | void *GPSLoop(void *some_void_ptr); 2 | -------------------------------------------------------------------------------- /pytrack/__init__.py: -------------------------------------------------------------------------------- 1 | from .lora import LoRa 2 | from .camera import SSDVCamera 3 | from .rtty import RTTY 4 | from .led import PITS_LED 5 | from .cgps import GPS 6 | from .telemetry import build_sentence 7 | from .temperature import Temperature 8 | from .tracker import Tracker 9 | -------------------------------------------------------------------------------- /pytrack/camera.py: -------------------------------------------------------------------------------- 1 | import picamera 2 | import threading 3 | import time 4 | import os 5 | import fnmatch 6 | 7 | def SelectBestImage(TargetFolder): 8 | Result = '' 9 | Largest = 0 10 | for item in os.listdir(TargetFolder): 11 | extension = os.path.splitext(item)[1] 12 | if extension == '.jpg': 13 | itemsize = os.path.getsize(TargetFolder + item) 14 | if itemsize > Largest: 15 | Result = item 16 | Largest = itemsize 17 | 18 | return Result 19 | 20 | def ConvertToSSDV(TargetFolder, FileName, Callsign, ImageNumber, SSDVFileName): 21 | print('ssdv -e -c ' + Callsign + ' -i ' + str(ImageNumber) + ' ' + TargetFolder + FileName + ' ' + TargetFolder + SSDVFileName) 22 | os.system('ssdv -e -c ' + Callsign + ' -i ' + str(ImageNumber) + ' ' + TargetFolder + FileName + ' ' + TargetFolder + SSDVFileName) 23 | 24 | def MoveFiles(Folder, SubFolder, Extension): 25 | if not os.path.exists(Folder + SubFolder): 26 | os.makedirs(Folder + SubFolder) 27 | for item in os.listdir(Folder): 28 | if os.path.splitext(item)[1] == Extension: 29 | os.rename(Folder + item, Folder + SubFolder + '/' + item) 30 | 31 | class SSDVCamera(object): 32 | """ 33 | Simple Pi camera library that uses the picamera library and the SSDV encoder 34 | """ 35 | 36 | def __init__(self): 37 | # self.camera = picamera.PiCamera() 38 | self.Schedule = [] 39 | self.ImageCallback = None 40 | 41 | def __find_item_for_channel(self, Channel): 42 | for item in self.Schedule: 43 | if item['Channel'] == Channel: 44 | return item 45 | return None 46 | 47 | def __get_next_ssdv_file(self, item): 48 | ssdv_filename = item['TargetFolder'] + item['SSDVFileName'] 49 | next_filename = item['TargetFolder'] + item['NextSSDVFileName'] 50 | if os.path.isfile(next_filename): 51 | if os.path.isfile(ssdv_filename): 52 | os.remove(ssdv_filename) 53 | os.rename(next_filename, ssdv_filename) 54 | return ssdv_filename 55 | 56 | return None 57 | 58 | def __photo_thread(self): 59 | while True: 60 | for item in self.Schedule: 61 | # Take photo if needed 62 | if time.monotonic() > item['LastTime']: 63 | item['LastTime'] = time.monotonic() + item['Period'] 64 | filename = item['TargetFolder'] + time.strftime("%H_%M_%S", time.gmtime()) + '.jpg' 65 | if self.ImageCallback: 66 | self.ImageCallback(filename, item['Width'], item['Height']) 67 | if not os.path.isfile(filename): 68 | print("User image callback did not produce the file " + filename) 69 | else: 70 | print("Taking image " + filename) 71 | with picamera.PiCamera() as camera: 72 | camera.resolution = (item['Width'], item['Height']) 73 | camera.start_preview() 74 | time.sleep(2) 75 | camera.capture(filename) 76 | camera.stop_preview() 77 | 78 | # Choose and convert yet? 79 | if item['Callsign'] != '': 80 | # Is a radio channel 81 | if item['PacketIndex'] >= (item['PacketCount'] - 10): 82 | # SSDV file alread exists ? 83 | if not os.path.isfile(item['TargetFolder'] + item['NextSSDVFileName']): 84 | # At least one jpg file waiting for us? 85 | if len(fnmatch.filter(os.listdir(item['TargetFolder']), '*.jpg')) > 0: 86 | # Select file to convert 87 | FileName = SelectBestImage(item['TargetFolder']) 88 | 89 | if FileName != None: 90 | # Convert it 91 | item['ImageNumber'] += 1 92 | ConvertToSSDV(item['TargetFolder'], FileName, item['Callsign'], item['ImageNumber'], item['NextSSDVFileName']) 93 | 94 | # Move the jpg files so we don't use them again 95 | MoveFiles(item['TargetFolder'], time.strftime("%Y_%m_%d", time.gmtime()), '.jpg') 96 | 97 | time.sleep(1) 98 | 99 | def clear_schedule(self): 100 | """Clears the schedule.""" 101 | self.Schedule = [] 102 | 103 | def add_schedule(self, Channel, Callsign, TargetFolder, Period, Width, Height, VFlip=False, HFlip=False): 104 | """ 105 | Adds a schedule for a specific "channel", and normally you would set a schedule for each radio channel (RTTY and LoRa) and also one for full-sized images that are not transmitted. 106 | - Channel is a unique name for this entry, and is used to retrieve/convert photographs later 107 | - Callsign is used for radio channels, and should be the same as used by telemetry on that channel (it is embedded into SSDV packets) 108 | - TargetFolder is where the JPG files should be saved. It will be created if necessary. Each channel should have its own target folder. 109 | - Period is the time in seconds between photographs. This should be much less than the time taken to transmit an image, so that there are several images to choose from when transmitting. Depending on the combination of schedules, and how long each photograph takes, it may not always (or ever) be possible to maintain the specified periods for all channels. 110 | - Width and Height are self-evident. Take care not to create photographs that take a long time to send. If Width or Height are zero then the full camera resolution (as determined by checking the camera model - Omnivision or Sony) is used. 111 | - VFlip and HFlip can be used to correct images if the camera is not physically oriented correctly. 112 | """ 113 | TargetFolder = os.path.join(TargetFolder, '') 114 | 115 | if not os.path.exists(TargetFolder): 116 | os.makedirs(TargetFolder) 117 | 118 | # Check width/height. 0,0 means use full camera resolution 119 | if (Width == 0) or (Height == 0): 120 | try: 121 | with picamera.PiCamera() as camera: 122 | NewCamera = self.camera.revision == 'imx219' 123 | except: 124 | NewCamera = False 125 | if NewCamera: 126 | Width = 3280 127 | Height = 2464 128 | else: 129 | Width = 2592 130 | Height = 1944 131 | 132 | 133 | self.Schedule.append({'Channel': Channel, 134 | 'Callsign': Callsign, 135 | 'TargetFolder': TargetFolder, 136 | 'Period': Period, 137 | 'Width': Width, 138 | 'Height': Height, 139 | 'VFlip': VFlip, 140 | 'HFlip': HFlip, 141 | 'LastTime': 0, 142 | 'ImageNumber': 0, 143 | 'PacketIndex': 0, 144 | 'PacketCount': 0, 145 | 'SSDVFileName': 'ssdv.bin', 146 | 'NextSSDVFileName': '_ext.bin', 147 | 'File': None}) 148 | # print("schedule is: ", self.Schedule) 149 | 150 | def take_photos(self, callback=None): 151 | """ 152 | Begins execution of the schedule, in a thread. If the callback is specified, then this is called instead of taking a photo directly. The callback is called with the following parameters: 153 | 154 | filename - name of image file to create 155 | width - desired image width in pixels (can be ignored) 156 | height - desired image height in pixels (can be ignored) 157 | 158 | The callback is expected to take a photograph, using whatever method it likes, and with whatever manipulation it likes, creating the file specified by 'filename'. 159 | """ 160 | self.ImageCallback = callback 161 | 162 | t = threading.Thread(target=self.__photo_thread) 163 | t.daemon = True 164 | t.start() 165 | 166 | def get_next_ssdv_packet(self, Channel): 167 | """ 168 | Retrieves the next SSDV packet for a particular channel. 169 | If there is no image available (i.e. no photograph has been taken and converted yet for this channel) then None is returned. 170 | Returned packets contain a complete (256-byte) SSDV packet. 171 | """ 172 | Result = None 173 | 174 | item = self.__find_item_for_channel(Channel) 175 | if item != None: 176 | # Open file if we're not reading a file already 177 | if item['File'] == None: 178 | # Get next file to read, if there is one 179 | filename = self.__get_next_ssdv_file(item) 180 | if filename != None: 181 | item['PacketIndex'] = 0 182 | item['PacketCount'] = os.path.getsize(filename) / 256 183 | item['File'] = open(filename, mode='rb') 184 | 185 | # Read from file 186 | if item['File'] != None: 187 | Result = item['File'].read(256) 188 | item['PacketIndex'] += 1 189 | if item['PacketIndex'] >= item['PacketCount']: 190 | # Close file if we're at the end 191 | item['PacketIndex'] = 0 192 | item['PacketCount'] = 0 193 | item['File'].close() 194 | item['File'] = None 195 | 196 | return Result 197 | 198 | 199 | -------------------------------------------------------------------------------- /pytrack/cgps.py: -------------------------------------------------------------------------------- 1 | import math 2 | import socket 3 | import json 4 | import threading 5 | import psutil 6 | from os import system 7 | from time import sleep 8 | 9 | class GPSPosition(object): 10 | def __init__(self, when_new_position=None, when_lock_changed=None): 11 | self.GPSPosition = None 12 | 13 | @property 14 | def time(self): 15 | return self.GPSPosition['time'] 16 | 17 | @property 18 | def lat(self): 19 | return self.GPSPosition['lat'] 20 | 21 | @property 22 | def lon(self): 23 | return self.GPSPosition['lon'] 24 | 25 | @property 26 | def alt(self): 27 | return self.GPSPosition['alt'] 28 | 29 | @property 30 | def sats(self): 31 | return self.GPSPosition['sats'] 32 | 33 | @property 34 | def fix(self): 35 | return self.GPSPosition['fix'] 36 | 37 | class GPS(object): 38 | """ 39 | Gets position from UBlox GPS receiver, using external program for s/w i2c to GPIO pins 40 | Provides callbacks on change of state (e.g. lock attained, lock lost, new position received) 41 | """ 42 | PortOpen = False 43 | 44 | def __init__(self, when_new_position=None, when_lock_changed=None): 45 | self._WhenLockChanged = when_lock_changed 46 | self._WhenNewPosition = when_new_position 47 | self._GotLock = False 48 | self._GPSPosition = {'time': '00:00:00', 'lat': 0.0, 'lon': 0.0, 'alt': 0, 'sats': 0, 'fix': 0} 49 | self._GPSPositionObject = GPSPosition() 50 | 51 | # Start thread to talk to GPS program 52 | t = threading.Thread(target=self.__gps_thread) 53 | t.daemon = True 54 | t.start() 55 | 56 | def __process_gps(self, s): 57 | while 1: 58 | reply = s.recv(4096) 59 | if reply: 60 | inputstring = reply.split(b'\n') 61 | for line in inputstring: 62 | if line: 63 | temp = line.decode('utf-8') 64 | j = json.loads(temp) 65 | self._GPSPosition = j 66 | if self._WhenNewPosition: 67 | self._WhenNewPosition(self._GPSPosition) 68 | GotLock = self._GPSPosition['fix'] >= 1 69 | if GotLock != self._GotLock: 70 | self._GotLock = GotLock 71 | if self._WhenLockChanged: 72 | self._WhenLockChanged(GotLock) 73 | else: 74 | sleep(1) 75 | 76 | s.close() 77 | 78 | def _ServerRunning(self): 79 | return "gps" in [psutil.Process(i).name() for i in psutil.pids()] 80 | 81 | def _StartServer(self): 82 | system("pytrack-gps > /dev/null &") 83 | sleep(1) 84 | 85 | def __doGPS(self, host, port): 86 | try: 87 | # Connect socket to GPS server 88 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 89 | s.connect((host, port)) 90 | self.__process_gps(s) 91 | s.close() 92 | except: 93 | # Start GPS server if it's not running 94 | if not self._ServerRunning(): 95 | self._StartServer() 96 | 97 | def __gps_thread(self): 98 | host = '127.0.0.1' 99 | port = 6005 100 | 101 | while 1: 102 | self.__doGPS(host, port) 103 | 104 | 105 | def position(self): 106 | """returns the current GPS position as a dictionary, containing the latest GPS data ('time', 'lat', 'lon', alt', 'sats', 'fix'). 107 | These values can be access individually using the properties below (see the descriptions for return types etc.). 108 | """ 109 | self._GPSPositionObject.GPSPosition = self._GPSPosition 110 | return self._GPSPositionObject 111 | 112 | 113 | @property 114 | def time(self): 115 | """Returns latest GPS time (UTC)""" 116 | return self._GPSPosition['time'] 117 | 118 | @property 119 | def lat(self): 120 | """Returns latest GPS latitude""" 121 | return self._GPSPosition['lat'] 122 | 123 | @property 124 | def lon(self): 125 | """Returns latest GPS longitude""" 126 | return self._GPSPosition['lon'] 127 | 128 | @property 129 | def alt(self): 130 | """Returns latest GPS altitude""" 131 | return self._GPSPosition['alt'] 132 | 133 | @property 134 | def sats(self): 135 | """Returns latest GPS satellite count. Needs at least 4 satellites for a 3D position""" 136 | return self._GPSPosition['sats'] 137 | 138 | @property 139 | def fix(self): 140 | """Returns a number >=1 for a fix, or 0 for no fix""" 141 | return self._GPSPosition['fix'] 142 | 143 | -------------------------------------------------------------------------------- /pytrack/led.py: -------------------------------------------------------------------------------- 1 | from gpiozero import LED 2 | from time import sleep 3 | 4 | class PITS_LED(object): 5 | """ Provides control over the OK and Warn LEDs on the PITS+ / PITS Zero boards 6 | These LEDs are on BCM pins 26 (OK) and 19 (Warn) for the above boards 7 | Earlier boards (for the model A/B) had different allocations but are not currently supported 8 | 9 | To use, just create a PITS_LED object, then call one of the following functions to show 10 | the current tracker status: 11 | 12 | fail() - shows that software cannot operate normally (e.g. misconfigured). OK/Warn flashing 13 | GPS_LockStatus() - Shows if we're waiting on a GPS lock (OK off; Warn flashing) or have a 3D lock (OK flashing; Warn off) 14 | """ 15 | 16 | def __init__(self): 17 | self._LED_OK = LED(26) 18 | self._LED_Warn = LED(19) 19 | 20 | def fail(self): 21 | """ shows that software cannot operate normally (e.g. misconfigured). OK/Warn flashing """ 22 | self._LED_OK.blink(0.2,0.2) 23 | sleep(0.2) 24 | self._LED_Warn.blink(0.2,0.2) 25 | 26 | def gps_lock_status(self, have_lock): 27 | if have_lock: 28 | # 3D gps lock achieved. OK flashing; Warn off """ 29 | self._LED_OK.blink(0.5,0.5) 30 | self._LED_Warn.off() 31 | else: 32 | # waiting on a GPS lock. OK off; Warn flashing """ 33 | self._LED_OK.off() 34 | self._LED_Warn.blink(0.5,0.5) 35 | 36 | -------------------------------------------------------------------------------- /pytrack/lora.py: -------------------------------------------------------------------------------- 1 | from gpiozero import InputDevice 2 | import threading 3 | import spidev 4 | import time 5 | 6 | REG_FIFO = 0x00 7 | REG_FIFO_ADDR_PTR = 0x0D 8 | REG_FIFO_TX_BASE_AD = 0x0E 9 | REG_FIFO_RX_BASE_AD = 0x0F 10 | REG_RX_NB_BYTES = 0x13 11 | REG_OPMODE = 0x01 12 | REG_FIFO_RX_CURRENT_ADDR = 0x10 13 | REG_IRQ_FLAGS = 0x12 14 | REG_PACKET_SNR = 0x19 15 | REG_PACKET_RSSI = 0x1A 16 | REG_CURRENT_RSSI = 0x1B 17 | REG_DIO_MAPPING_1 = 0x40 18 | REG_DIO_MAPPING_2 = 0x41 19 | REG_MODEM_CONFIG = 0x1D 20 | REG_MODEM_CONFIG2 = 0x1E 21 | REG_MODEM_CONFIG3 = 0x26 22 | REG_PAYLOAD_LENGTH = 0x22 23 | REG_IRQ_FLAGS_MASK = 0x11 24 | REG_HOP_PERIOD = 0x24 25 | REG_FREQ_ERROR = 0x28 26 | REG_DETECT_OPT = 0x31 27 | REG_DETECTION_THRESHOLD = 0x37 28 | 29 | # MODES 30 | RF98_MODE_RX_CONTINUOUS = 0x85 31 | RF98_MODE_TX = 0x83 32 | RF98_MODE_SLEEP = 0x80 33 | RF98_MODE_STANDBY = 0x81 34 | 35 | # Modem Config 1 36 | EXPLICIT_MODE = 0x00 37 | IMPLICIT_MODE = 0x01 38 | 39 | ERROR_CODING_4_5 = 0x02 40 | ERROR_CODING_4_6 = 0x04 41 | ERROR_CODING_4_7 = 0x06 42 | ERROR_CODING_4_8 = 0x08 43 | 44 | BANDWIDTH_7K8 = 0x00 45 | BANDWIDTH_10K4 = 0x10 46 | BANDWIDTH_15K6 = 0x20 47 | BANDWIDTH_20K8 = 0x30 48 | BANDWIDTH_31K25 = 0x40 49 | BANDWIDTH_41K7 = 0x50 50 | BANDWIDTH_62K5 = 0x60 51 | BANDWIDTH_125K = 0x70 52 | BANDWIDTH_250K = 0x80 53 | BANDWIDTH_500K = 0x90 54 | 55 | # Modem Config 2 56 | 57 | SPREADING_6 = 0x60 58 | SPREADING_7 = 0x70 59 | SPREADING_8 = 0x80 60 | SPREADING_9 = 0x90 61 | SPREADING_10 = 0xA0 62 | SPREADING_11 = 0xB0 63 | SPREADING_12 = 0xC0 64 | 65 | CRC_OFF = 0x00 66 | CRC_ON = 0x04 67 | 68 | # POWER AMPLIFIER CONFIG 69 | REG_PA_CONFIG = 0x09 70 | PA_MAX_BOOST = 0x8F 71 | PA_LOW_BOOST = 0x81 72 | PA_MED_BOOST = 0x8A 73 | PA_MAX_UK = 0x88 74 | PA_OFF_BOOST = 0x00 75 | RFO_MIN = 0x00 76 | 77 | # LOW NOISE AMPLIFIER 78 | REG_LNA = 0x0C 79 | LNA_MAX_GAIN = 0x23 # 0010 0011 80 | LNA_OFF_GAIN = 0x00 81 | LNA_LOW_GAIN = 0xC0 # 1100 0000 82 | 83 | class LoRa(object): 84 | def __init__(self, Channel=0, Frequency=434.250, Mode=1, DIO0=0): 85 | """ 86 | This library provides access to one LoRa (Long Range Radio) module on the PITS Zero board or the add-on board for the PITS+ board. 87 | The LoRa object is **non-blocking**. The calling code can either poll to find out when the transmission has completed, or can be notified via a callback. 88 | 89 | Channel should match the number of the position occupied by the LoRa module (as labelled on the LoRa board). 90 | 91 | The frequency is in MHz and should be selected carefully: 92 | - If you are using RTTY also, it should be at least 25kHz (0.025MHz) away from the RTTY frequency 93 | - It should be different to that used by any other HAB flights that are airborne at the same time and within 400 miles (600km) 94 | - It should be legal in your country (for the UK see [https://www.ofcom.org.uk/__data/assets/pdf_file/0028/84970/ir_2030-june2014.pdf](https://www.ofcom.org.uk/__data/assets/pdf_file/0028/84970/ir_2030-june2014.pdf "IR2030")) 95 | 96 | Mode should be either 0 (best if you are not sending image data over LoRa) or 1 (best if you are). 97 | 98 | When setting up your receiver, use matching settings. 99 | """ 100 | self.SentenceCount = 0 101 | self.ImagePacketCount = 0 102 | self.sending = False 103 | self.CallbackWhenSent = None 104 | 105 | # Set DIO0 (used for TxDone) automatically if it hasn't already been set. The values below are for the Uputronics boards; for anything else DIO0 should be passed explicitly. 106 | if DIO0 == 0: 107 | if Channel == 1: 108 | DIO0 = 16 109 | else: 110 | DIO0 = 25 111 | 112 | 113 | self.Channel = Channel 114 | self.Frequency = Frequency 115 | self.DIO0 = InputDevice(DIO0) 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 | def SetLoRaFrequency(self, Frequency): 152 | """Sets the frequency in MHz.""" 153 | self.__setMode(RF98_MODE_STANDBY) 154 | self.__setMode(RF98_MODE_SLEEP) 155 | self.__writeRegister(REG_OPMODE, 0x80); 156 | #self.__setMode(RF98_MODE_SLEEP) 157 | self.__setMode(RF98_MODE_STANDBY) 158 | 159 | FrequencyValue = int((Frequency * 7110656) / 434) 160 | 161 | self.__writeRegister(0x06, (FrequencyValue >> 16) & 0xFF) 162 | self.__writeRegister(0x07, (FrequencyValue >> 8) & 0xFF) 163 | self.__writeRegister(0x08, FrequencyValue & 0xFF) 164 | 165 | def SetLoRaParameters(self, ImplicitOrExplicit, ErrorCoding, Bandwidth, SpreadingFactor, LowDataRateOptimize): 166 | self.__writeRegister(REG_MODEM_CONFIG, ImplicitOrExplicit | ErrorCoding | Bandwidth) 167 | self.__writeRegister(REG_MODEM_CONFIG2, SpreadingFactor | CRC_ON) 168 | self.__writeRegister(REG_MODEM_CONFIG3, 0x04 | (0x08 if LowDataRateOptimize else 0)) 169 | self.__writeRegister(REG_DETECT_OPT, (self.__readRegister(REG_DETECT_OPT) & 0xF8) | (0x05 if (SpreadingFactor == SPREADING_6) else 0x03)) 170 | self.__writeRegister(REG_DETECTION_THRESHOLD, 0x0C if (SpreadingFactor == SPREADING_6) else 0x0A) 171 | 172 | self.PayloadLength = 255 if (ImplicitOrExplicit == IMPLICIT_MODE) else 0 173 | 174 | self.__writeRegister(REG_PAYLOAD_LENGTH, self.PayloadLength) 175 | self.__writeRegister(REG_RX_NB_BYTES, self.PayloadLength) 176 | 177 | def SetStandardLoRaParameters(self, Mode): 178 | """ 179 | Sets the various LoRa parameters to one of the following standard combinations: 180 | - 0: EXPLICIT_MODE, ERROR_CODING_4_8, BANDWIDTH_20K8, SPREADING_11, LDO On 181 | - 1: IMPLICIT_MODE, ERROR_CODING_4_5, BANDWIDTH_20K8, SPREADING_6, LDO Off 182 | - 2: EXPLICIT_MODE, ERROR_CODING_4_8, BANDWIDTH_62K5, SPREADING_8, LOD Off 183 | """ 184 | if Mode == 0: 185 | self.SetLoRaParameters(EXPLICIT_MODE, ERROR_CODING_4_8, BANDWIDTH_20K8, SPREADING_11, True) 186 | elif Mode == 1: 187 | self.SetLoRaParameters(IMPLICIT_MODE, ERROR_CODING_4_5, BANDWIDTH_20K8, SPREADING_6, False) 188 | elif Mode == 2: 189 | self.SetLoRaParameters(EXPLICIT_MODE, ERROR_CODING_4_8, BANDWIDTH_62K5, SPREADING_8, False) 190 | 191 | def _send_thread(self): 192 | # wait for TxSent (packet sent flag) on DIO0 193 | while not self.DIO0.is_active: 194 | time.sleep(0.01) 195 | 196 | # Reset TxSent thus resetting DIO0 197 | self.__writeRegister(REG_IRQ_FLAGS, 0x08); 198 | 199 | self.sending = False 200 | 201 | # Callback if set 202 | if self.CallbackWhenSent: 203 | self.CallbackWhenSent() 204 | 205 | def is_sending(self): 206 | """Returns True if LoRa module is still sending the latest packet""" 207 | return self.sending 208 | 209 | def send_packet(self, packet, callback=None): 210 | """ 211 | Sends a binary packet which should be a bytes object. Normally this would be a 256-byte SSDV packet (see the camera.py module). 212 | 213 | The callback, if used, is called when the packet has been completely set and the RTTY object is ready to accept more data to transmit. 214 | """ 215 | self.CallbackWhenSent = callback 216 | self.sending = True 217 | 218 | self.__setMode(RF98_MODE_STANDBY) 219 | 220 | # map DIO0 to TxDone 221 | self.__writeRegister(REG_DIO_MAPPING_1, 0x40) 222 | 223 | self.__writeRegister(REG_FIFO_TX_BASE_AD, 0x00) 224 | self.__writeRegister(REG_FIFO_ADDR_PTR, 0x00) 225 | 226 | data = [REG_FIFO | 0x80] + list(packet) + [0] 227 | self.spi.xfer(data) 228 | 229 | self.__writeRegister(REG_PAYLOAD_LENGTH, self.PayloadLength if self.PayloadLength else len(packet)) 230 | 231 | self.__setMode(RF98_MODE_TX); 232 | 233 | t = threading.Thread(target=self._send_thread) 234 | t.daemon = True 235 | t.start() 236 | 237 | def send_text(self, sentence, callback=None): 238 | """ 239 | Sends a text string sentence. Normally this would be a UKHAS-compatible HAB telemetry sentence but it can be anything. See the telemetry.py module for how to create compliant telemetry sentences. 240 | 241 | callback is as for send_packet() 242 | """ 243 | self.send_packet(sentence.encode(), callback) 244 | -------------------------------------------------------------------------------- /pytrack/pytrack: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytrack.pytrack 4 | 5 | -------------------------------------------------------------------------------- /pytrack/pytrack.ini: -------------------------------------------------------------------------------- 1 | [RTTY] 2 | ID = PYSKY 3 | Frequency = 434.250 4 | BaudRate = 50 5 | Camera = 0 6 | 7 | [LoRa] 8 | Channel = 0 9 | ID = PYSKY2 10 | Frequency = 434.450 11 | Mode = 1 12 | 13 | [General] 14 | Camera = 1 15 | ImagePacketsPerSentence = 6 16 | -------------------------------------------------------------------------------- /pytrack/rtty.py: -------------------------------------------------------------------------------- 1 | from gpiozero import OutputDevice 2 | import serial 3 | import threading 4 | import pigpio 5 | import time 6 | 7 | class RTTY(object): 8 | """ 9 | Radio - RTTY 10 | """ 11 | 12 | def __init__(self, frequency=434.250, baudrate=50): 13 | """ 14 | The frequency is in MHz and should be selected carefully: 15 | 16 | - If you are using LoRa also, it should be at least 25kHz (0.025MHz) away from the LoRa frequency 17 | - It should be different to that used by any other HAB flights that are airborne at the same time and within 400 miles (600km) 18 | - It should be legal in your country (for the UK see [https://www.ofcom.org.uk/__data/assets/pdf_file/0028/84970/ir_2030-june2014.pdf](https://www.ofcom.org.uk/__data/assets/pdf_file/0028/84970/ir_2030-june2014.pdf "IR2030")) 19 | 20 | The baudrate should be either 50 (best if you are not sending image data over RTTY) or 300 (best if you are). 21 | 22 | When setting up your receiver, use the following settings: 23 | 24 | - 50 or 300 baud 25 | - 7 data bits (if using 50 baud) or 8 (300 baud) 26 | - no parity 27 | - 2 stop bits 28 | """ 29 | self.SentenceCount = 0 30 | self.ImagePacketCount = 0 31 | self.sending = False 32 | self.CallbackWhenSent = None 33 | 34 | self._set_frequency(frequency) 35 | self._set_frequency(frequency) # In case MTX2 wasn't in ready mode when we started 36 | 37 | self.ntx2 = OutputDevice(17) 38 | self.ntx2.off() 39 | 40 | self.ser = serial.Serial() 41 | self.ser.baudrate = baudrate 42 | self.ser.stopbits = 2 43 | if baudrate < 300: 44 | self.ser.bytesize = 7 45 | else: 46 | self.ser.bytesize = 8 47 | self.ser.port = '/dev/ttyAMA0' 48 | 49 | def _set_frequency(self, Frequency): 50 | pio = pigpio.pi() 51 | if not pio.connected: 52 | print() 53 | print("*** Please start the PIGPIO daemon by typing the following command at the shell prompt:") 54 | print() 55 | print(" sudo pigpiod") 56 | print() 57 | quit() 58 | 59 | _mtx2comp = (Frequency+0.0015)/6.5 60 | 61 | _mtx2int = int(_mtx2comp) 62 | _mtx2fractional = int(((_mtx2comp-_mtx2int)+1) * 524288) 63 | 64 | _mtx2command = "@PRG_" + "%0.2X" % (_mtx2int-1) + "%0.6X" % _mtx2fractional + "\r" 65 | 66 | pio.set_mode(17, pigpio.OUTPUT) 67 | 68 | pio.wave_add_new() 69 | 70 | pio.wave_add_serial(17, 9600, _mtx2command, 0, 8, 2) 71 | 72 | wave_id = pio.wave_create() 73 | 74 | if wave_id >= 0: 75 | pio.wave_send_once(wave_id) 76 | 77 | while pio.wave_tx_busy(): 78 | time.sleep(0.1) 79 | 80 | pio.stop() 81 | 82 | def _send_thread(self): 83 | self.ser.close() 84 | self.sending = False 85 | if self.CallbackWhenSent: 86 | self.CallbackWhenSent() 87 | 88 | def is_sending(self): 89 | return self.sending 90 | 91 | def send_packet(self, packet, callback=None): 92 | """ 93 | Sends a binary packet packet which should be a bytes object. Normally this would be a 256-byte SSDV packet (see the camera.py module). 94 | 95 | callback, if used, is called when the packet has been completely set and the RTTY object is ready to accept more data to transmit. 96 | """ 97 | self.CallbackWhenSent = callback 98 | self.ntx2.on() 99 | try: 100 | self.ser.open() 101 | self.sending = True 102 | self.ser.write(packet) 103 | t = threading.Thread(target=self._send_thread) 104 | t.daemon = True 105 | t.start() 106 | except: 107 | raise RuntimeError('Failed to open RTTY serial port\nCheck that port is present and has been enabled') 108 | 109 | def send_text(self, sentence, callback=None): 110 | """ 111 | Sends a text string sentence. Normally this would be a UKHAS-compatible HAB telemetry sentence but it can be anything. See the telemetry.py module for how to create compliant telemetry sentences. 112 | 113 | callback is as for send_packet() 114 | """ 115 | self.send_packet(sentence.encode(), callback) 116 | -------------------------------------------------------------------------------- /pytrack/telemetry.py: -------------------------------------------------------------------------------- 1 | import crcmod 2 | 3 | def crc16_ccitt(data): 4 | """Returns 16-bit CCITT CRC as used by UKHAS""" 5 | crc16 = crcmod.predefined.mkCrcFun('crc-ccitt-false') 6 | return hex(crc16(data))[2:].upper().zfill(4) 7 | 8 | def build_sentence(values): 9 | """ 10 | Builds a UKHAS sentence from a list of fields. 11 | 12 | values is a list containing all fields to be combined into a sentence. At a minimum this should have, at the start of the list and in this sequence, the following: 13 | 14 | 1. Payload ID (unique to this payload, and different between RTTY and LoRa) 15 | 2. Count (a counter from 1 upwards) 16 | 3. Time (current UTC (GMT) time) 17 | 4. Latitude (latitude in decimal degrees) 18 | 5. Longitude (longitude in decimal degrees) 19 | 6. Altitude (altitude in metres) 20 | 21 | Subsequent fields are optional. 22 | 23 | The resulting sentence will be of this form: 24 | 25 | $$payload_id,count,time,latitude,longitude,altitude*CRC\n 26 | 27 | where CRC is the CRC16_CCITT code for all characters in the string after the $$ and before the *, and "\n" is linefeed. 28 | """ 29 | temp = ','.join(map(str, values)) 30 | sentence = '$$' + temp + '*' + crc16_ccitt(temp.encode()) + '\n' 31 | return sentence 32 | -------------------------------------------------------------------------------- /pytrack/temperature.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | import threading 4 | 5 | class Temperature(object): 6 | """ 7 | Reads temperature 8 | """ 9 | 10 | def __init__(self): 11 | """Gets the current board temperature from a DS18B20 device on the PITS board """ 12 | self.Temperatures = [0] 13 | pass 14 | 15 | def _get_temperatures(self): 16 | """Returns current temperature""" 17 | Folder = "/sys/bus/w1/devices" 18 | Folders = os.listdir(Folder) 19 | 20 | for Item in Folders: 21 | if len(Item) > 3: 22 | if (Item[0] != 'w') and (Item[2] == '-'): 23 | Filename = Folder + '/' + Item + '/w1_slave' 24 | with open(Filename) as f: 25 | content = f.readlines() 26 | # Second line has temperature 27 | self.Temperatures[0] = int(content[1].split('=')[1]) / 1000.0 28 | 29 | def __temperature_thread(self): 30 | while True: 31 | self._get_temperatures() 32 | time.sleep(10) 33 | 34 | 35 | def run(self): 36 | """ 37 | Uses a thread to read the current DS18B20 temperature every few seconds, available as Temperatures[0] 38 | 39 | Worth changing at some point to handle an extra DS18B20, so users can measure external temperature 40 | """ 41 | t = threading.Thread(target=self.__temperature_thread) 42 | t.daemon = True 43 | t.start() 44 | -------------------------------------------------------------------------------- /pytrack/test_camera.py: -------------------------------------------------------------------------------- 1 | from pytrack import SSDVCamera 2 | from time import sleep 3 | 4 | camera = SSDVCamera() 5 | camera.take_photos() 6 | 7 | while True: 8 | sleep(1) 9 | print("FINISHED") 10 | -------------------------------------------------------------------------------- /pytrack/test_cgps.py: -------------------------------------------------------------------------------- 1 | from pytrack import GPS 2 | import time 3 | 4 | def NewPosition(Position): 5 | print("Callback: ", Position) 6 | 7 | def LockChanged(GotLock): 8 | print("Lock " + ("GAINED" if GotLock else "LOST")) 9 | 10 | print("Creating GPS object ...") 11 | mygps = GPS(when_new_position=NewPosition, when_lock_changed=LockChanged) 12 | 13 | print("loop ...") 14 | while 1: 15 | time.sleep(1) 16 | position = mygps.position() 17 | print ("Posn: ", position.time, position.lat, position.lon, position.alt, position.fix) 18 | -------------------------------------------------------------------------------- /pytrack/test_led.py: -------------------------------------------------------------------------------- 1 | from pytrack import PITS_LED 2 | from signal import pause 3 | 4 | print("Creating LED object ...") 5 | status_leds = PITS_LED(); 6 | 7 | status_leds.fail() 8 | pause() 9 | -------------------------------------------------------------------------------- /pytrack/test_lora.py: -------------------------------------------------------------------------------- 1 | from pytrack import LoRa 2 | import time 3 | 4 | print("Create LoRa object") 5 | mylora = LoRa(Channel=0, Frequency=434.450, Mode=1) 6 | 7 | print("Send message") 8 | mylora.send_text("$$Hello World\n") 9 | 10 | while mylora.is_sending(): 11 | time.sleep(0.01) 12 | print("DONE") 13 | -------------------------------------------------------------------------------- /pytrack/test_rtty.py: -------------------------------------------------------------------------------- 1 | from pytrack import RTTY 2 | from time import sleep 3 | 4 | global SentOK 5 | SentOK = False 6 | 7 | print("Create RTTY object") 8 | rtty = RTTY() 9 | 10 | print("Send RTTY Sentence") 11 | rtty.send_text("$$PYSKY,1,10:42:56,51.95023,-2.54445,145,8,21.6*EB9C\n") 12 | 13 | while rtty.is_sending(): 14 | sleep(0.1) 15 | print("FINISHED") 16 | -------------------------------------------------------------------------------- /pytrack/test_tracker.py: -------------------------------------------------------------------------------- 1 | from pytrack.tracker import * 2 | from time import sleep 3 | 4 | def extra_telemetry(): 5 | # sample code to add one telemetry field 6 | extra_value = 123.4 7 | return "{:.1f}".format(extra_value) 8 | 9 | def take_photo(filename, width, height, gps): 10 | # sample code to take a photo 11 | # Use the gps object if you want to add a telemetry overlay, or use different image sizes at different altitudes, for example 12 | with picamera.PiCamera() as camera: 13 | camera.resolution = (width, height) 14 | camera.start_preview() 15 | time.sleep(2) 16 | camera.capture(filename) 17 | camera.stop_preview() 18 | 19 | mytracker = Tracker() 20 | 21 | mytracker.set_rtty(payload_id='PIP1', frequency=434.100, baud_rate=300, image_packet_ratio=4) 22 | mytracker.add_rtty_camera_schedule('images/RTTY', period=60, width=320, height=240) 23 | 24 | mytracker.set_lora(payload_id='PIP2', channel=0, frequency=434.150, mode=1, image_packet_ratio=6) 25 | mytracker.add_lora_camera_schedule('images/LORA', period=60, width=640, height=480) 26 | 27 | mytracker.add_full_camera_schedule('images/FULL', period=60, width=1024, height=768) 28 | 29 | # mytracker.set_sentence_callback(extra_telemetry) 30 | 31 | # mytracker.set_image_callback(take_photo) 32 | 33 | mytracker.start() 34 | 35 | while True: 36 | sleep(1) 37 | -------------------------------------------------------------------------------- /pytrack/tracker.py: -------------------------------------------------------------------------------- 1 | from .rtty import * 2 | from .lora import * 3 | from .led import * 4 | from .temperature import * 5 | from .cgps import * 6 | from .camera import * 7 | from .telemetry import * 8 | 9 | from time import sleep 10 | import threading 11 | import configparser 12 | 13 | class Tracker(object): 14 | # HAB Radio/GPS Tracker 15 | 16 | def __init__(self): 17 | """ 18 | This library uses the other modules (CGPS, RTTY etc.) to build a complete tracker. 19 | """ 20 | self.camera = None 21 | self.lora = None 22 | self.rtty = None 23 | self.SentenceCallback = None 24 | self.ImageCallback = None 25 | 26 | def _TransmitIfFree(self, Channel, PayloadID, ChannelName, ImagePacketsPerSentence): 27 | if not Channel.is_sending(): 28 | # Do we need to send an image packet or sentence ? 29 | # print("ImagePacketCount = ", Channel.ImagePacketCount, ImagePacketsPerSentence) 30 | if (Channel.ImagePacketCount < ImagePacketsPerSentence) and self.camera: 31 | Packet = self.camera.get_next_ssdv_packet(ChannelName) 32 | else: 33 | Packet = None 34 | 35 | if Packet == None: 36 | print("Sending telemetry sentence for " + PayloadID) 37 | 38 | Channel.ImagePacketCount = 0 39 | 40 | # Get temperature 41 | InternalTemperature = self.temperature.Temperatures[0] 42 | 43 | # Get GPS position 44 | position = self.gps.position() 45 | 46 | # Build sentence 47 | Channel.SentenceCount += 1 48 | 49 | fieldlist = [PayloadID, 50 | Channel.SentenceCount, 51 | position.time, 52 | "{:.5f}".format(position.lat), 53 | "{:.5f}".format(position.lon), 54 | int(position.alt), 55 | position.sats, 56 | "{:.1f}".format(InternalTemperature)] 57 | 58 | if self.SentenceCallback: 59 | fieldlist.append(self.SentenceCallback()) 60 | 61 | sentence = build_sentence(fieldlist) 62 | print(sentence, end="") 63 | 64 | # Send sentence 65 | Channel.send_text(sentence) 66 | else: 67 | Channel.ImagePacketCount += 1 68 | print("Sending SSDV packet for " + PayloadID) 69 | Channel.send_packet(Packet[1:]) 70 | 71 | def set_rtty(self, payload_id='CHANGEME', frequency=434.200, baud_rate=50, image_packet_ratio=4): 72 | """ 73 | This sets the RTTY payload ID, radio frequency, baud rate (use 50 for telemetry only, 300 (faster) if you want to include image data), and ratio of image packets to telemetry packets. 74 | 75 | If you don't want RTTY transmissions, just don't call this function. 76 | """ 77 | self.RTTYPayloadID = payload_id 78 | self.RTTYFrequency = frequency 79 | self.RTTYBaudRate = baud_rate 80 | self.RTTYImagePacketsPerSentence = image_packet_ratio 81 | 82 | self.rtty = RTTY(self.RTTYFrequency, self.RTTYBaudRate) 83 | 84 | def set_lora(self, payload_id='CHANGEME', channel=0, frequency=424.250, mode=1, DIO0=0, camera=False, image_packet_ratio=6): 85 | """ 86 | 87 | This sets the LoRa payload ID, radio frequency, mode (use 0 for telemetry-only; 1 (which is faster) if you want to include images), and ratio of image packets to telemetry packets. 88 | 89 | If you don't want LoRa transmissions, just don't call this function. 90 | 91 | Note that the LoRa stream will only include image packets if you add a camera schedule (see add_rtty_camera_schedule) 92 | """ 93 | self.LoRaPayloadID = payload_id 94 | self.LoRaChannel = channel 95 | self.LoRaFrequency = frequency 96 | self.LoRaMode = mode 97 | self.LORAImagePacketsPerSentence = image_packet_ratio 98 | 99 | self.lora = LoRa(Channel=self.LoRaChannel, Frequency=self.LoRaFrequency, Mode=self.LoRaMode, DIO0=DIO0) 100 | 101 | def add_rtty_camera_schedule(self, path='images/RTTY', period=60, width=320, height=240): 102 | """ 103 | Adds an RTTY camera schedule. The default parameters are for an image of size 320x240 pixels every 60 seconds and the resulting file saved in the images/RTTY folder. 104 | """ 105 | if not self.camera: 106 | self.camera = SSDVCamera() 107 | if self.RTTYBaudRate >= 300: 108 | print("Enable camera for RTTY") 109 | self.camera.add_schedule('RTTY', self.RTTYPayloadID, path, period, width, height) 110 | else: 111 | print("RTTY camera schedule not added - baud rate too low (300 minimum needed") 112 | 113 | def add_lora_camera_schedule(self, path='images/LORA', period=60, width=640, height=480): 114 | """ 115 | Adds a LoRa camera schedule. The default parameters are for an image of size 640x480 pixels every 60 seconds and the resulting file saved in the images/LORA folder. 116 | """ 117 | if not self.camera: 118 | self.camera = SSDVCamera() 119 | if self.LoRaMode == 1: 120 | print("Enable camera for LoRa") 121 | self.camera.add_schedule('LoRa0', self.LoRaPayloadID, path, period, width, height) 122 | else: 123 | print("LoRa camera schedule not added - LoRa mode needs to be set to 1 not 0") 124 | 125 | def add_full_camera_schedule(self, path='images/FULL', period=60, width=0, height=0): 126 | """ 127 | Adds a camera schedule for full-sized images. The default parameters are for an image of full sensor resolution, every 60 seconds and the resulting file saved in the images/FULL folder. 128 | """ 129 | if not self.camera: 130 | self.camera = SSDVCamera() 131 | self.camera.add_schedule('FULL', '', path, period, width, height) 132 | 133 | def set_sentence_callback(self, callback): 134 | """ 135 | This specifies a function to be called whenever a telemetry sentence is built. That function should return a string containing a comma-separated list of fields to append to the telemetry sentence. 136 | """ 137 | self.SentenceCallback = callback 138 | 139 | def set_image_callback(self, callback): 140 | """ 141 | The callback function is called whenever an image is required. **If you specify this callback, then it's up to you to provide code to take the photograph (see tracker.md for an example)**. 142 | """ 143 | self.ImageCallback = callback 144 | 145 | def __ImageCallback(self, filename, width, height): 146 | self.ImageCallback(filename, width, height, self.gps) 147 | 148 | def __transmit_thread(self): 149 | while True: 150 | if self.rtty: 151 | self._TransmitIfFree(self.rtty, self.RTTYPayloadID, 'RTTY', self.RTTYImagePacketsPerSentence) 152 | if self.lora: 153 | self._TransmitIfFree(self.lora, self.LoRaPayloadID, 'LoRa0', self.LORAImagePacketsPerSentence) 154 | sleep(0.01) 155 | 156 | def start(self): 157 | """ 158 | Starts the tracker. 159 | """ 160 | LEDs = PITS_LED() 161 | 162 | self.temperature = Temperature() 163 | self.temperature.run() 164 | 165 | self.gps = GPS(when_lock_changed=LEDs.gps_lock_status) 166 | 167 | if self.camera: 168 | if self.ImageCallback: 169 | self.camera.take_photos(self.__ImageCallback) 170 | else: 171 | self.camera.take_photos(None) 172 | 173 | t = threading.Thread(target=self.__transmit_thread) 174 | t.daemon = True 175 | t.start() 176 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # from distutils.core import setup 2 | # from setuptools import setup, find_packages 3 | import subprocess 4 | from setuptools import setup 5 | from setuptools.command.install import install 6 | 7 | class CustomInstall(install): 8 | def run(self): 9 | subprocess.check_call('make', cwd='./gps/', shell=True) 10 | super().run() 11 | 12 | setup( 13 | name='pytrack', 14 | version='1.0', 15 | packages=['pytrack'], 16 | url='www.daveakerman.com', 17 | license='GPL 2.0', 18 | author='Dave Akerman', 19 | author_email='dave@sccs.co.uk', 20 | description='HAB Tracker for RTTY and LoRa', 21 | scripts=[ 22 | 'pytrack/pytrack', 23 | ], 24 | install_requires=['psutil','pyserial','pigpio','picamera','crcmod', 25 | 'gpiozero'], 26 | cmdclass={'install': CustomInstall} 27 | ) 28 | 29 | --------------------------------------------------------------------------------