Skip to content

Commit

Permalink
adding backend mqtt syncing
Browse files Browse the repository at this point in the history
  • Loading branch information
EricAndrechek committed Apr 10, 2024
1 parent 1ff59bc commit 59862bc
Show file tree
Hide file tree
Showing 41 changed files with 5,989 additions and 1,498 deletions.
39 changes: 26 additions & 13 deletions ground-station/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ For tracking the balloon once it has landed and triggering the cut-down burn wir
- [1.3.1. SIM Card Hat](#131-sim-card-hat)
- [1.4. Display](#14-display)
- [1.5. GPS](#15-gps)
- [1.6. Power](#16-power)
- [2. Setup](#2-setup)
- [2.1. Install Ubuntu Server](#21-install-ubuntu-server)
- [2.1. Install Operating System](#21-install-operating-system)
- [2.2. Download \& Install Dependencies](#22-download--install-dependencies)
- [2.2.1. Automatic Setup](#221-automatic-setup)
- [2.2.2. Manual Setup](#222-manual-setup)
Expand Down Expand Up @@ -57,14 +58,10 @@ There are several different methods one could use to upload APRS and GPS data to

AT COMMANDS:

AT+CSTT="hologram"
AT*MCGDEFCONT="IP","hologram"
AT+CGDCONT=1,"IP","hologram"


// Facility Lock
AT+CLCK
Set to LTE mode
AT+CNMP=38
AT+COPS Operator Selection
AT+CREG Network Registration

## 1.4. Display

Expand All @@ -74,17 +71,33 @@ While buying a display is not strictly necessary, it can make the initial setup

## 1.5. GPS

This software can optionally integrate the same functionality as the [vehicle-tracker](../vehicle-tracker/). In order to track your receiver vehicle/device without separate vehicle-tracker hardware, you will need a USB-enabled GPS device. Ideally find one that says it supports linux so that you don't run into driver issues.
This software can optionally integrate the same functionality as the [vehicle-tracker](../vehicle-tracker/). In order to track your receiver vehicle/device without separate vehicle-tracker hardware, you will need a USB-enabled GPS device. Ideally find one that says it supports linux so that you don't run into driver issues. Alternatively, if you are using a SIM hat, you can use the GPS on that.

## 1.6. Power

"Le Potato" board lists its power draw as:

- 4W Full Load
- 2.5W Typical Load
- < 1W Idle
- < 0.1 Wake Ready




# 2. Setup

## 2.1. Install Ubuntu Server
## 2.1. Install Operating System

Download Raspberry Pi Imager from [here](https://www.raspberrypi.com/software/).

Download the latest Raspberry Pi OS Full image from [here](https://distro.libre.computer/ci/raspbian/12/) if using Le Potato, or just directly from the imager if using a Raspberry Pi.

Start by installing Ubuntu Server 22.04 (or some other distribution or OS, but Ubuntu Server 22.04 is lightweight and tested)
Plug a microSD of at least 8GB into your computer and open the imager.

Download an Ubuntu Server iso from the internet. [Here's the download for the Libre Computer](https://distro.libre.computer/ci/ubuntu/22.04/)
Note: If you are using a Libre Computer Board, you will need to use the "Custom" option in the imager and select the correct image. The popup prompt asking if you would like to apply custom settings (such as hostname, wifi, etc.) can be skipped, as the Le Potato does not support these settings.

Put the iso into a tool like [Raspberry Pi Imager](https://www.raspberrypi.com/software/) and install it onto a microSD card (for installing on SBCs) or a USB (for desktop/laptop style computers). Note that for single board computers, the faster the microSD the better.
Once the image is written, plug the microSD into the device and boot it up.

## 2.2. Download & Install Dependencies

Expand Down
2 changes: 1 addition & 1 deletion tracking-dashboard/backend/api/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def api_rock7_upload():

# if callsign is not in data, add as serial
if 'callsign' not in decoded_data:
decoded_data['callsign'] = data['serial']
decoded_data['callsign'] = data['serial'][:6]
# if ssid is not in data, add as 0
if 'ssid' not in decoded_data:
decoded_data['ssid'] = 0
Expand Down
13 changes: 10 additions & 3 deletions tracking-dashboard/backend/api/map.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from utils.map import Map
from sql.helpers import get_items_last_n_minutes

from flask import Flask, Blueprint, render_template, request, jsonify, redirect, url_for

Expand All @@ -18,6 +19,12 @@
# - upload just to our endpoint and have the option to push to (and pull from) APRS-IS
# - upload to both our endpoint and APRS-IS and have the option to push to (and pull from) APRS-IS

@map_app.route('/map', methods=['GET'])
def map_api():
return "map api"
@map_app.route('/liveTracks', methods=['GET'])
def liveTracks():
# get param from request
age = request.args.get('age') or 60
# should get all callsigns that have transmitted in the last <age> minutes
# default to 60 minutes
# and return them as a list of dictionaries
data = get_items_last_n_minutes(age)
return jsonify(data)
2 changes: 2 additions & 0 deletions tracking-dashboard/backend/sql/aprs-syncer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# connect to APRS-IS and listen for all callsigns in items database, mirroring their data to our database

36 changes: 36 additions & 0 deletions tracking-dashboard/backend/sql/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,39 @@ def get_last_ip_addition(ip):
print(e)

return time

# get all items in the items table that have been updated in the last n minutes
def get_items_last_n_minutes(n):
time = datetime.utcnow() - datetime.timedelta(minutes=n)
items = []
try:
items = Session.query(Items).filter(Items.last_updated > time).all()
except Exception as e:
print(e)
return items

# check if an id is in the items table, returning the callsign, ssid, and symbol if it is - and updating the last_updated time to now
def check_item_id(id):
item = None
try:
item = Session.query(Items).filter_by(id=id).first()
if item is not None:
item.last_updated = datetime.utcnow()
Session.commit()
except Exception as e:
print(e)
return item


# add a given id with optional callsign, ssid, and symbol to the items table
def add_item_id(id, callsign=None, ssid=None, symbol=None):
item = Items(id=id, callsign=callsign, ssid=ssid, symbol=symbol)
Session.add(item)
try:
Session.commit()
return True
except Exception as e:
print(e)
Session.rollback()
Session.flush()
return False
15 changes: 15 additions & 0 deletions tracking-dashboard/backend/sql/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,18 @@ class Position(Base):

def __repr__(self):
return "<Position(id='%s', message='%s', callsign='%s', ssid='%s', symbol='%s', speed='%s', course='%s', geo='%s', altitude='%s', comment='%s', telemetry='%s')>" % (self.id, self.message, self.callsign, self.ssid, self.symbol, self.speed, self.course, self.geo, self.altitude, self.comment, self.telemetry)

class Items(Base):
__tablename__ = 'items'

# list of all unique callsigns found in the positions table
id = Column(String, primary_key=True)
callsign = Column(String(6), nullable=False, unique=True)
ssid = Column(Integer, nullable=False)
symbol = Column(String(2), nullable=False)
last_updated = Column(DateTime, nullable=False, default=datetime.utcnow)

# TODO: should delete this if the callsign is deleted

def __repr__(self):
return "<Items(id='%s', callsign='%s', symbol='%s', last_updated='%s')>" % (self.id, self.callsign, self.symbol, self.last_updated)
187 changes: 187 additions & 0 deletions tracking-dashboard/backend/sql/mqtt-syncer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
# constant running python script to sync data from mqtt to postgres

import paho.mqtt.client as mqtt
from datetime import datetime
from sql.helpers import check_item_id, add_item_id
from utils.api import Data

# open mqtt connection
client = mqtt.Client()
client.connect("localhost", 1883, 60)

# subscribe to all trackers
client.subscribe("T/#")

message_building = {}

def build_json_message(id):
# should have (at minimum):
# id, name, ssid, sym, lat, lon, alt, course, speed, comment
# optionally also have:
# timestamp, telemetry
message = {}
if id not in message_building:
print("No message_building data for id: ", id)
return None

if "name" in message_building[id]:
message['callsign'] = message_building[id]['name']
else:
message['callsign'] = str(id)[:6]
if "ssid" in message_building[id]:
message['ssid'] = message_building[id]['ssid']
else:
message['ssid'] = 0
if "sym" in message_building[id]:
message['symbol'] = message_building[id]['sym']
else:
message['symbol'] = "/>"
if "lat" in message_building[id]:
message['lat'] = message_building[id]['lat']
else:
message['lat'] = None
if "lon" in message_building[id]:
message['lon'] = message_building[id]['lon']
else:
message['lon'] = None
if "alt" in message_building[id]:
message['alt'] = message_building[id]['alt']
else:
message['alt'] = None
if "cse" in message_building[id]:
message['course'] = message_building[id]['cse']
else:
message['course'] = None
if "spd" in message_building[id]:
message['speed'] = message_building[id]['spd']
else:
message['speed'] = None
if "hh" in message_building[id] and "mm" in message_building[id] and "ss" in message_building[id] and "YY" in message_building[id] and "MM" in message_building[id] and "DD" in message_building[id]:
message['timestamp'] = f"{message_building[id]['YY']}-{message_building[id]['MM']}-{message_building[id]['DD']} {message_building[id]['hh']}:{message_building[id]['mm']}:{message_building[id]['ss']}"
else:
message['timestamp'] = None

# for all other keys, add them to telemetry
telemetry = {}
for key in message_building[id]:
if key not in ['name', 'ssid', 'sym', 'lat', 'lon', 'alt', 'cse', 'spd', 'hh', 'mm', 'ss', 'YY', 'MM', 'DD']:
telemetry[key] = message_building[id][key]

if len(telemetry) > 0:
message['telemetry'] = telemetry

return message


# build a source packet of the type the API likes from a message
def build_source_packet(message):
source = {}
source['timestamp'] = datetime.now().isoformat()
source['callsign'] = "UMSERV"
source['ssid'] = 0
source['ip'] = "127.0.0.1"
source['type'] = 'json'
source['data'] = message

return source


# call add_message on message
def on_message(client, userdata, message):
topic = message.topic
payload = message.payload.decode()
timestamp = datetime.now().isoformat()

# split topic
topics = topic.split("/")
if topics[0] == "T":
# handle new tracker message
# tracker messages are the only data influx to mqtt

if len(topics) != 3:
print("Invalid topic: ", topic)
return

id = topics[1]
key = topics[2]

# check if id is in Items table
# if not, add it
# we know it is in items table if "name" has been assigned
if id in message_building and "name" in message_building[id]:
pass
else:
# now check db
item = check_item_id(id)
if item is None:
# check if in message_building
if id in message_building:
# create item with message_building data
callsign = str(id)[:6]
ssid = 0
symbol = "/>"
if "name" in message_building[id]:
callsign = message_building[id]["name"]
if "ssis" in message_building[id]:
ssid = message_building[id]["ssid"]
if "sym" in message_building[id]:
symbol = message_building[id]["sym"]

add_item_id(id, callsign, ssid, symbol)
else:
# add id to message_building and
message_building[id] = {}
else:
# add item data to message_building
if id not in message_building:
message_building[id] = {}
message_building[id]['name'] = item.callsign
message_building[id]['ssid'] = item.ssid
message_building[id]['sym'] = item.symbol

# periodically check that name, ssid, and symbol haven't been overwritten in the items table
# if they have, update message_building
if key == "ss" and id in message_building and (payload == "00" or payload == "15" or payload == "30" or payload == "45" or payload == "60"):
item = check_item_id(id)
if item is not None:
message_building[id]['name'] = item.callsign
message_building[id]['ssid'] = item.ssid
message_building[id]['sym'] = item.symbol
else:
print("Item not found in items table: ", id)
return


# if key is "ss" (seconds), add message to db
if key == "ss":
# add message to db
msg = build_json_message(id)
if msg is None:
return
src = build_source_packet(msg)
data_obj = Data(src)
message_building[id]['ss'] = timestamp
try:
data_obj.upload()
except Exception as e:
print("Error uploading data: ", e)
return
try:
data_obj.parse()
except Exception as e:
print("Error parsing data: ", e)
return
return
else:
# add key and payload to message_building
message_building[id][key] = payload
else:
print("Unknown topic: ", topic)


client.on_message = on_message

client.loop_forever()

# close mqtt connection
client.disconnect()
Loading

0 comments on commit 59862bc

Please sign in to comment.