diff --git a/champaign_county_map.html b/champaign_county_map.html new file mode 100644 index 00000000..ed2ce607 --- /dev/null +++ b/champaign_county_map.html @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + \ No newline at end of file diff --git a/ground/.gitignore b/ground/.gitignore index 89cc49cb..c78f84fe 100644 --- a/ground/.gitignore +++ b/ground/.gitignore @@ -3,3 +3,4 @@ .vscode/c_cpp_properties.json .vscode/launch.json .vscode/ipch +*.pyc diff --git a/ground/README.md b/ground/README.md new file mode 100644 index 00000000..712c3bec --- /dev/null +++ b/ground/README.md @@ -0,0 +1,176 @@ +# GSS v1.1 +This is the primary source of information for all things related to **GSS 1.1**, the telemetry system used for Spaceshot's 2024 launch of *Kairos II* at FAR 51025! + +## Contributors + +- Nicholas Phillips +- Gautam Dayal +- Patrick Marschoun +- Peter Giannetos +- Aaditya Voruganti +- Michael Karpov + +System architecture: + +Changes from **GSS 1.0**: Unlike GSS 1.0, the new system uses a single laptop to combine streams from our antenna array and send out the data to a network, with all other data consumers subscribing to a stream over [MQTT](https://mqtt.org/). Additionally, this architecture allows us to implement other systems into our ecosystem which use telemetry data but would otherwise slow down our telemetry computers. + +![System Architecture](https://i.ibb.co/YtWs14w/Screenshot-2024-04-16-190354.png) + +# Telemetry Recievers +This section refers to the Feather Reciever software which is stored at `./src/feather`. + +This repository contains the software for the ground station hardware (LoRa Feather module) used by the Spaceshot Telemetry Team during the 2023-24 season. + +## Description + +The ground station software is responsible for receiving telemetry data from the rocket, processing commands sent from the ground station GUI, and interfacing with the LoRa Feather module. It includes functionalities such as setting frequency, sending commands to the rocket, and parsing telemetry data. + +## Features + +- Receive telemetry data from the rocket +- Send commands to the rocket +- Interface with the ground station GUI through serial communication +- Parse incoming commands from the ground station GUI +- Set frequency for communication with the rocket +- Decode and process telemetry data packets +- Handle errors and retries in command transmission + +## Dependencies + +- [RH_RF95 library](https://github.com/PaulStoffregen/RadioHead) - for LoRa communication +- [SPI library](https://github.com/PaulStoffregen/SPI) - for SPI communication +- [SerialParser library](link-to-serial-parser-library) - for parsing serial input + +## Usage + +1. Connect the LoRa Feather module to the ground station hardware. +2. Upload the appropriate version of the code to the ground station hardware using the Arduino IDE or compatible software. + - **For Ground Station**: Upload the code with `IS_GROUND` defined. + - **For Drone**: Upload the code with `IS_DRONE` defined. +3. Open the serial monitor to view output messages and interact with the ground station GUI. +4. Follow the commands and instructions provided by the ground station GUI to control the rocket and receive telemetry data. + +## Configuration + +- Adjust default frequencies (`RF95_FREQ`, `SUSTAINER_FREQ`, `BOOSTER_FREQ`, `GROUND_FREQ`) as needed for your application. +- `RF95_FREQ` is used only when `IS_GROUND` is active +- `SUSTAINER_FREQ`, `BOOSTER_FREQ`, `GROUND_FREQ` is used only when `IS_DRONE` is active + +## Flashing Instructions + +### Ground Station + +It is used for the typical feathers on the ground and will be able to receive any code either directly from the rocket or from the drone relay. + +1. Connect the ground station hardware to your computer. +2. Open the Arduino IDE or compatible software. +3. Load the code with `IS_GROUND` defined. +4. Compile and upload the code to the ground station hardware. + +### Drone + +This code is used by feathers to relay information via a drone from the rocket to ground, this is for better connectivity when the rocket is further away from us or is covered by earth elements. + +1. Connect the drone hardware to your computer. +2. Open the Arduino IDE or compatible software. +3. Load the code with `IS_DRONE` defined. +4. Compile and upload the code to the drone hardware. + + + + + + + +To upload to the reciever you must navigate to the `ground` directory and use the `Platformio` vscode extension and upload using either the `feather` or `drone` build environments. +Alternatively use the `pio` command like so: + +```bash +$ pio run -t upload -e +``` + +# Ground Station Combiner +This section refers to the Ground Station Combiner software which is stored at `./gss_combiner`. +## Installation and Operation +First, begin by cloning the repository to your computer (or opening it) and opening the `gss_combiner` folder: + +```bash +$ git clone https://github.com/ISSUIUC/MIDAS-Software.git +$ cd ./MIDAS-Software/ground/gss_combiner +``` + +To install dependencies for the combiner, you can run: +```bash +$ pip install -r requirements.txt +``` + +*(MK) TODO: Add requirements.txt file.* + +Then, you can run the combiner using **Python**: + +```bash +$ python ./main.py +``` + +`` refers to a set of command line arguments that can be passed to `main.py`, listed below: + +`--booster `: Pass in a comma-separated list of COM ports to receive telemetry from, and transmit to the `Booster` data topics. + +`--sustainer `: Pass in a comma-separated list of COM ports to receive telemetry from, and transmit to the `Sustainer` data topics. + +`--relay `: Pass in a comma-separated list of COM ports to receive telemetry from, specifically for the `Drone Relay` system. + +`--local (alias -l)`: Use `localhost` as the MQTT target. Useful for debugging. + +`--no-log (alias -n)`: Do not generate logs for this run. + +`--verbose (alias -v)`: Print out all combiner actions. This may slow down the combiner due to print volume. + +`--no-vis (alias -nv)`: Disable the visualization for system health (Also disabled with `--verbose`). + +`--config (alias -c)`: Load an argument configuration from the `config.ini` file + +`--no-rf`: Skip overriding RF frequencies for the feather reciever. + +`--help (alias -h)`: Display a set of these options. + + +Not including sustainer / booster sources will throw a warning, but will still run the system, allowing you to check the connectivity for the backend MQTT broker. + +### For Kairos II Summer Launch 2024: +The `config.ini` file will be updated to include all necessary configuration within the `launch` config. As such you will only need to edit the COM ports present in the file, and you will be able to execute the system with the command + +```bash +$ python ./main.py -c launch +``` + + +Additionally, for this launch we have adopted the following lookup scheme for determining the stage callsign: + +| Callsign bit value (highest bit of `fsm_callsign_satcount`) | Callsign | +| ----------------------------------------------------------- | -------- | +| 0 | KD9ZPM | +| 1 | KD9ZMJ | + + +## MQTT Streams +This system uses [MQTT](https://mqtt.org/) as the primary data transfer method to other sections of the telemetry system. This is accomplished using multiple data streams. While technically unsecured, `data` streams are intended to be read-only (and only written to by this service), while `control` streams are intended to allow control of the system. As of writing (4/16), the current accepted data streams for GSS 1.1 are the following: + +`FlightData-All` (`data`): Subscribe to receive all telemetry packets (Both booster and sustainer) + +`FlightData-Sustainer` (`data`): Subscribe to receive sustainer telemetry packets + +`FlightData-Booster` (`data`): Subscribe to receive booster telemetry packets + +`Control-Sustainer` (`control`): Publish to edit Sustainer telemetry system functionality. + +`Control-Booster` (`control`): Publish to edit Booster telemetry system functionality. + +`Common` (`control / data`): Data published by auxiliary services or non-critical systems. + + +## Null-Modem Emulation +It is possible to run full tests of this telemetry system without access to the telemetry hardware. Installing a null-modem emulator such as [this one](https://com0com.sourceforge.net/) will allow you to emulate COM ports on your device and run the `test/test.py` script. + +While this is the software we use for internal testing of this system, we cannot guarantee your results regarding installation and software safety. + diff --git a/ground/gss_combiner/.gitignore b/ground/gss_combiner/.gitignore new file mode 100644 index 00000000..44075982 --- /dev/null +++ b/ground/gss_combiner/.gitignore @@ -0,0 +1 @@ +/outputs/ \ No newline at end of file diff --git a/ground/gss_combiner/config.ini b/ground/gss_combiner/config.ini new file mode 100644 index 00000000..a9b82dae --- /dev/null +++ b/ground/gss_combiner/config.ini @@ -0,0 +1,13 @@ +[config:rf] +rfBooster = 425.15 +rfSustainer = 426.15 +rfRelay = 420 + +[launch] +args = --sustainer COM1,COM2 --booster COM18 --relay COM20 --ip 192.168.0.69 + +[test] +args = --sustainer COM1,COM2 --booster COM18,COM20 --no-log --local --no-rf + +[limited] +args = --sustainer COM13 --booster COM23 --no-log --local \ No newline at end of file diff --git a/ground/gss_combiner/main.py b/ground/gss_combiner/main.py new file mode 100644 index 00000000..9cd33bcc --- /dev/null +++ b/ground/gss_combiner/main.py @@ -0,0 +1,325 @@ +# GSS Combiner. ISS Spaceshot Avionics 2024 +# A pub-sub architecture telemetry system to allow for rapid development of telemetry-consuming systems +# ----------------------------------------------------------------------------------------------------- +# Peter Giannetos (2025) +# Aidan Costello (2026) +# Aaditya Voruganti (2026) +# Zyun Lam (2027) +# Michael Karpov (2027) + +# USAGE: +# py ./main.py [options] +# +# --booster [source1],[source2],[etc..] -> Selects which COM ports should be interpreted as a data stream from the booster stage +# --sustainer [source1],[source2],[etc..] -> Selects which COM ports should be interpreted as a data stream from the sustainer stage +# --relay [source1],[source2],[etc..] -> Selects which COM ports should be interpreted as a data stream from the telemetry relay +# --local (or -l) -> Streams all data to 'localhost' for testing. (Same as --ip localhost) +# --no-log (or -n) -> Will not log data to logfiles for this run +# --verbose (or -v) -> Prints all telemetry events to console +# --no-vis (or -nv) -> Shows a visual display of all systems +# --ip [IP] (or -i [IP]) -> Connects to a specific IP. (Overrides --local) +# --help (or -h) -> Prints this menu +# --config [config] (or -c [config]) -> Uses an argument config defined in config.ini. Added on top of existing params. +# --no-rf -> Does not overwrite feather frequencies on startup + +import sys +import threading +import datetime +import json +import configparser + +import util.mqtt as mqtt +import util.combiner as combiner +import util.logger as logger +import util.print_util + + +uri_target = "" +cfg = configparser.ConfigParser() +cfg.read("./config.ini") + +def assert_alive(threads: list[threading.Thread]): + """Used in the main loop of the combiner to ensure that all threads stay alive.""" + for thread in threads: + assert thread.is_alive() + +def threads_ready(threads: list[mqtt.TelemetryThread]): + """Used to ensure telemetry threads are ready""" + for thread in threads: + if not thread.ready(): + return False + return True + +def parse_params(arguments): + """Parse parameters passed into this script and return them as a flat tuple.""" + + def is_tl_arg(argument): + """Helper function for determining whether a cli argument is a top-level argument or not. + Returns true if the argument is a top-level argument.""" + return argument.startswith("--") or argument.startswith("-") + + num_params = len(arguments) + arg_ptr = 1 # Skip ./main.py arg + + booster_sources = [] + sustainer_sources = [] + relay_sources = [] + is_local = False + should_log = True + + has_booster = False + has_sustainer = False + has_relay = False + is_verbose = False + is_visual = True + + use_ip = None + use_config = None + overwrite_rf = True + + while (arg_ptr < num_params): + arg = arguments[arg_ptr] + if(not is_tl_arg(arg)): + raise ValueError("Invalid argument: " + str(arg)) + + # Handle booster/sustainer logic + if (arg == "--booster"): + if (has_booster): + raise ValueError("Argument '--booster' is unique (You have defined booster sources twice.)") + has_booster = True + + next_arg = arguments[arg_ptr + 1] + if(is_tl_arg(next_arg)): + raise ValueError("You must pass values to argument " + str(arg) + f" (got {next_arg})") + booster_sources = next_arg.split(',') + arg_ptr += 2 + continue + + if (arg == "--sustainer"): + if (has_sustainer): + raise ValueError("Argument '--sustainer' is unique (You have defined sustainer sources twice.)") + has_sustainer = True + + next_arg = arguments[arg_ptr + 1] + if(is_tl_arg(next_arg)): + raise ValueError("You must pass values to argument " + str(arg) + f" (got {next_arg})") + sustainer_sources = next_arg.split(',') + arg_ptr += 2 + continue + + if (arg == "--relay"): + if (has_relay): + raise ValueError("Argument '--relay' is unique (You have defined relay sources twice.)") + has_relay = True + + next_arg = arguments[arg_ptr + 1] + if(is_tl_arg(next_arg)): + raise ValueError("You must pass values to argument " + str(arg) + f" (got {next_arg})") + relay_sources = next_arg.split(',') + arg_ptr += 2 + continue + + # Handle misc arguments + if (arg == "--ip" or arg == "-i"): + next_arg = arguments[arg_ptr + 1] + if(is_tl_arg(next_arg)): + raise ValueError("You must pass values to argument " + str(arg) + f" (got {next_arg})") + use_ip = next_arg + arg_ptr += 2 + continue + + if (arg == "--config" or arg == "-c"): + next_arg = arguments[arg_ptr + 1] + if(is_tl_arg(next_arg)): + raise ValueError("You must pass values to argument " + str(arg) + f" (got {next_arg})") + use_config = next_arg + arg_ptr += 2 + continue + + if (arg == "--local" or arg == "-l"): + is_local = True + + if (arg == "--no-log" or arg == "-n"): + should_log = False + + if (arg == "--no-rf"): + overwrite_rf = False + + if (arg == "--help" or arg == "-h"): + print(util.print_util.HELP_OUTPUT) + exit(0) + + if (arg == "--verbose" or arg == "-v"): + is_verbose = True + + if (arg == "--no-vis" or arg == "-nv"): + is_visual = False + + arg_ptr += 1 + + + + return booster_sources, sustainer_sources, relay_sources, is_local, should_log, is_verbose, is_visual, use_ip, overwrite_rf, use_config + +if __name__ == "__main__": + SCRIPT_START_TIME = datetime.datetime.now().timestamp() + threads = [] + + booster_sources, sustainer_sources, relay_sources, is_local, should_log, is_verbose, is_visual, ip_override, overwrite_rf, use_config = parse_params(sys.argv) + + if(use_config is not None): + # If a config is specified, re-interpret parameters once using the config. + try: + CFG_ARGS = cfg[use_config]['args'].split(" ") + except: + print("Argument config does not exist!") + exit(1) + + print("Using config ", use_config) + print("Using command", " ".join(sys.argv + CFG_ARGS) + "\n") + booster_sources, sustainer_sources, relay_sources, is_local, should_log, is_verbose, is_visual, ip_override, overwrite_rf, use_config = parse_params(sys.argv + CFG_ARGS) + + # Ensure that the user is notified if they do not specify any data sources. + if len(booster_sources)==0 and len(sustainer_sources)==0 and len(relay_sources)==0: + print("\n\x1b[1m\x1b[33mWARNING: No sources have been selected! You will not read data!\x1b[0m\n") + + if is_local: + uri_target = "localhost" + + if ip_override is not None: + uri_target = ip_override + + log = logger.Logger(logger.LoggerOptions(should_log, is_verbose)) + + print("Using sustainer sources: ", sustainer_sources) + print("Using booster sources: ", booster_sources) + print("Using relay sources: ", relay_sources, "\n\n") + + telem_threads_booster = [] + telem_threads_sustainer = [] + telem_threads_relay = [] + + # Initialize primary MQTT thread + broadcast_thread = mqtt.MQTTThread(uri_target, log.create_stream(logger.LoggerType.MQTT, "main", "mqtt")) + + # Initialize telemetry combiners + combiner_sustainer = combiner.TelemetryCombiner("Sustainer", log.create_stream(logger.LoggerType.COMBINER, "Sustainer", "sustainer_comb"), filter=combiner.TelemetryCombiner.FilterOptions(allow_sustainer=True)) + combiner_booster = combiner.TelemetryCombiner("Booster", log.create_stream(logger.LoggerType.COMBINER, "Booster", "booster_comb"), filter=combiner.TelemetryCombiner.FilterOptions(allow_booster=True)) + + # Set up MQTT and control streams + combiner_sustainer.add_mqtt(broadcast_thread) + combiner_booster.add_mqtt(broadcast_thread) + broadcast_thread.subscribe_control(combiner_booster) + broadcast_thread.subscribe_control(combiner_sustainer) + + # Determine RF frequencies from config + FREQ_SUSTAINER = cfg['config:rf']['rfSustainer'] + FREQ_BOOSTER = cfg['config:rf']['rfBooster'] + FREQ_RELAY = cfg['config:rf']['rfRelay'] + + # IMPORTANT! -------------------------------------------------------------------------------------------------------------------------------------- + # When decoding packets for telemetry purposes, we encode callsign in a 'callsign bit' which determines which callsign is being used for that stage + # When this bit is set, the callsign is interpreted as KD9ZMJ + # When this bit is NOT set, the callsign is interpreted as KD9ZPM + # ------------------------------------------------------------------------------------------------------------------------------------------------- + + if overwrite_rf: + print("Waiting for telemetry thread RF initialization... (This may take a bit)") + else: + print("Skipping Frequency override") + + # Set up booster telemetry threads + for port in booster_sources: + new_thread = mqtt.TelemetryThread(port, uri_target, "FlightData-All", log.create_stream(logger.LoggerType.TELEM, port, "booster_telem")) + new_thread.add_combiner(combiner_booster) + if overwrite_rf: + new_thread.write_frequency(FREQ_BOOSTER) + telem_threads_booster.append(new_thread) + + # Set up sustainer telemetry threads + for port in sustainer_sources: + new_thread = mqtt.TelemetryThread(port, uri_target, "FlightData-All", log.create_stream(logger.LoggerType.TELEM, port, "sustainer_telem")) + new_thread.add_combiner(combiner_sustainer) + if overwrite_rf: + new_thread.write_frequency(FREQ_SUSTAINER) + telem_threads_sustainer.append(new_thread) + + # Set up telemetry threads for drone relay + for port in relay_sources: + new_thread = mqtt.TelemetryThread(port, uri_target, "FlightData-All", log.create_stream(logger.LoggerType.TELEM, port, "relay_telem")) + new_thread.add_combiner(combiner_booster) + new_thread.add_combiner(combiner_sustainer) + if overwrite_rf: + new_thread.write_frequency(FREQ_RELAY) + telem_threads_relay.append(new_thread) + + + # Start all threads + for thd in telem_threads_booster: + thd.start() + + for thd in telem_threads_sustainer: + thd.start() + + for thd in telem_threads_relay: + thd.start() + + broadcast_thread.start() + threads = [broadcast_thread] + telem_threads_booster + telem_threads_sustainer + assert_alive(threads) # Ensure all threads initialized successfully + + # Set up visualization variables + print_delay = 0.5 + last_print_db = datetime.datetime.now().timestamp() + print_delay + + # Wait for telem threads to be ready + init_time_warned = False + while True: + if threads_ready(telem_threads_booster + telem_threads_relay + telem_threads_sustainer): + break + + # Inform user of possible errors if init takes too long.. + time_delta = datetime.datetime.now().timestamp() - SCRIPT_START_TIME + if time_delta > 20 and not init_time_warned: + print("\x1b[33mWARNING: RF initialization is taking longer than expected... Make sure that the \x1b[36mconfig.ini\x1b[33m RF configuration is within Feather range.\x1b[0m") + init_time_warned = True + + + print("\n\n\nTelemetry system initialized successfully!\n\n") + + # Print visualization legend + if (not is_verbose and is_visual): + logger.print_legend(uri_target) + + + while True: + # Main loop: + # Ensures threads stay alive and displays visualization of system health + + assert_alive(threads) + + if (is_verbose or not is_visual): + continue + + # Only print occasionally to not flood standard print + if(datetime.datetime.now().timestamp() - last_print_db > 0): + last_print_db = datetime.datetime.now().timestamp() + print_delay + + # Print status + status_text = "" + raw_data = {} + for ls_name, log_stream in log.streams().items(): + log_stream: logger.LoggerStream = log_stream + status_text += logger.format_stat_string(ls_name, log_stream) + meta_cat, data = log_stream.serialize() + + if not (meta_cat in raw_data): + raw_data[meta_cat] = {} + + raw_data[meta_cat][log_stream.get_name()] = data + + print(f"Status: {status_text}", end="\r") + + # Send status + send_data = {"source": "gss_combiner", "action": "none", "time": datetime.datetime.now().timestamp(), "data": raw_data} + broadcast_thread.publish_common(json.dumps(send_data)) \ No newline at end of file diff --git a/ground/gss_combiner/test/telem_packet.json b/ground/gss_combiner/test/telem_packet.json new file mode 100644 index 00000000..e23a3bab --- /dev/null +++ b/ground/gss_combiner/test/telem_packet.json @@ -0,0 +1,19 @@ +{ + "type": "data", + "value": { + "barometer_altitude": 0, + "altitude": 0, + "latitude": 0, + "longitude": 0, + "highG_ax": 0.31641, + "highG_ay": 0.09766, + "highG_az": 1.02734, + "battery_voltage": 1, + "FSM_state": 0, + "tilt_angle": 0, + "frequency": 400, + "RSSI": 0, + "sat_count": 0, + "is_sustainer": 0 + } +} \ No newline at end of file diff --git a/ground/gss_combiner/test/test.py b/ground/gss_combiner/test/test.py new file mode 100644 index 00000000..b47b72ef --- /dev/null +++ b/ground/gss_combiner/test/test.py @@ -0,0 +1,165 @@ +import json +import serial +import time +import random +import math +import paho.mqtt.publish as publish +import datetime + +# com0com setup: +# COM1 <-> COM16 +# COM2 <-> COM17 +# COM18 <-> COM19 +# COM20 <-> COM21 + +# print("Sleep 1s") +# time.sleep(1) +# publish.single("Control-Sustainer", "payload", hostname="localhost") +# print("sent") + + +# exit(0) +com_in = ["COM16"] +# com_in = ["COM16", "COM17", "COM19", "C"] + +s_time = time.time() + +class Sim(): + def __init__(self) -> None: + self.__alt = 0 + self.__vel = 0 + self.__acc = 0 + self.__time = 0 + + self.__launchdelay = 5 + self.__burntime1 = 5 + self.__burntime2 = 12 + self.__burnaccel1 = 120 + self.__burnaccel2 = 70 + self.__delay = 2 + self.__s = datetime.datetime.now().timestamp() + self.__start = datetime.datetime.now().timestamp() + + def getaccel(self, fltime): + + if(fltime < self.__launchdelay): + # before launch + return 0 + + if(fltime < self.__launchdelay + self.__burntime1): + return self.__burnaccel1 + + if(fltime < self.__launchdelay + self.__burntime1 + self.__delay): + return -9.8 + + if(fltime < self.__launchdelay + self.__burntime1 + self.__delay + self.__burntime2): + return self.__burnaccel2 + + return -9.8 + + def step(self): + dt = datetime.datetime.now().timestamp() - self.__s + self.__s = datetime.datetime.now().timestamp() + fltime = datetime.datetime.now().timestamp() - self.__start + + accel = self.getaccel(fltime) + + print(accel) + self.__vel += accel * dt + self.__alt ++ self.__vel * dt + + + + + + def get_alt(self): + return self.__alt + + def get_accel(self): + return self.__acc + + def get_vel(self): + return self.__vel + + +def get_packet(sim: Sim): + delta_s = time.time() - s_time - 1 + # edits packet based on elapsed time to simulate ascent!! + packet = json.load(open("./telem_packet.json")) + packet['value']['barometer_altitude'] = sim.get_alt() + packet['value']['altitude'] = sim.get_alt() + packet['value']['latitude'] = (math.sin(delta_s / 5) * 80) + packet['value']['longitude'] = (math.sin(delta_s / 3) * 180) + packet['value']['highG_ax'] = sim.get_accel() + packet['value']['highG_ay'] = 0.5 + packet['value']['highG_az'] = 0.2 + packet['value']['battery_voltage'] = math.fabs(math.sin(delta_s/5) * 14) + packet['value']['tilt_angle'] = math.fabs(math.sin(delta_s/3) * 180) + packet['value']['FSM_state'] = math.floor(math.fabs(math.sin(delta_s/3) * 12)) + packet['value']['RSSI'] = (math.sin(delta_s/3) * 75) - 75 + packet['value']['sat_count'] = math.floor(math.fabs(math.sin(delta_s/3) * 12)) + + + + # print(packet['BNO_YAW']) + return packet + + +ports = [serial.Serial(c, write_timeout=3) for c in com_in] + +def enc(dict): + json_s = json.dumps(dict) + "\n" + return json_s.encode("ascii") + +print("wait 1s") +time.sleep(1) +print("Start sending:") + +i = 0 +s = Sim() + +while True: + + + for port in ports: + write_in = "" + + while port.in_waiting: + write_in += port.read_all().decode() + + if(len(write_in) > 0): + print("READ:", port.name, write_in) + pkts = write_in.split("\n") + + for pk in pkts: + if pk.startswith("FREQ:"): + frq = float(pk.split(":")[1]) + print("Handling freq test... delay 4 seconds then writeback") + time.sleep(4) + print("Switching to freq ", frq) + port.write((json.dumps({'type': 'freq_success', 'frequency': frq}) + "\n").encode()) + print("Written", json.dumps({'type': 'freq_success', 'frequency': frq})) + print("Freq changed. continuing") + + + + + + + packet = enc(get_packet(s)) + p = ports[i] + p.write(packet) + print(f"Sent packet to port {com_in[i]}") + i = (i + 1) % len(ports) + time.sleep(0.3) + + s.step() + + # time.sleep(0.1) + # p = ports[i] + # p.write(packet) + # print(f"Sent packet to port {com_in[i]}") + # i = (i + 1) % len(ports) + + + pass \ No newline at end of file diff --git a/ground/gss_combiner/util/combiner.py b/ground/gss_combiner/util/combiner.py new file mode 100644 index 00000000..c610fe72 --- /dev/null +++ b/ground/gss_combiner/util/combiner.py @@ -0,0 +1,165 @@ +# Telemetry Combiner +# ISS Spaceshot Avionics 2024 + +from datetime import datetime, timezone +from collections import deque +import copy + +import util.mqtt as mqtt +import util.logger + +class TelemetryCombiner(): + """A class that combines multiple data streams from an antenna array into a single coherent stream to be interpreted by ISS telemetry systems + + Allows for filtering packets based on rocket stages as well as fine control over the handling of duplicate packets""" + + class FilterOptions(): + """A helper filter class to define which packets are allowed to be sent to a `TelemetryCombiner`""" + def __init__(self, allow_booster=False, allow_sustainer=False) -> None: + self.__allow_booster = allow_booster + self.__allow_sustainer = allow_sustainer + + def test(self, packet) -> bool: + """Returns whether or not this packet should be sent to this `TelemetryCombiner`""" + if (packet['value']['is_sustainer'] and self.__allow_sustainer): + return True + + if (not packet['value']['is_sustainer'] and self.__allow_booster): + return True + + if(packet['value']['battery_voltage'] >= 15.75): + return False + + if(packet['value']['battery_voltage'] <= 0.1): + return False + + return False + + class DuplicateDatapoints(): + """Helper class to handle duplicate handling in `TelemetryCombiner`""" + class DP(): + """A single duplicate packet list checker""" + def __init__(self, packets) -> None: + self.__packets = packets + self.__ts = datetime.now().timestamp() + + def get_ts(self) -> float: + """Return the timestamp this packet list was added""" + return self.__ts + + def check(self, packet) -> bool: + """Returns whether or not this packet should be allowed to be added to the `TelemetryCombiner` (Tests for duplicates)""" + return True + try: + for pkt in self.__packets: + incoming = copy.copy(packet['value']) + existing = copy.copy(pkt['value']) + incoming['RSSI'] = 0 # This will compare everything but RSSI, filter out identical packets with different RSSIs. + existing['RSSI'] = 0 + + # print(incoming, existing) + if incoming == existing: + return False + return True + except Exception as e: + print(e) + print("Unable to detect duplicates with packet.") + return True + + + def __init__(self, timeout: float) -> None: + self.__duplicates = [] + self.__timeout = timeout + + def insert(self, pkt_list): + """Add a packet list to the `DuplicateDatapoints` checker""" + self.__duplicates.append(TelemetryCombiner.DuplicateDatapoints.DP(pkt_list)) + + def clear_old(self): + """Removes packet lists whose `timeout` has expired.""" + new_list = [] + for dp in self.__duplicates: + if (datetime.now().timestamp() + self.__timeout > dp.get_ts()): + new_list.append(dp) + self.__duplicates = new_list + + def check(self, packet) -> bool: + """Returns whether or not this packet should be allowed to be added to the `TelemetryCombiner` (Tests for duplicates)""" + self.clear_old() + for dp in self.__duplicates: + if not dp.check(packet): + return False + return True + + + + # splitter_list is a list of TelemetryCombiners acting as relay recievers. + def __init__(self, stage, log_stream: util.logger.LoggerStream, filter=FilterOptions()): + self.__log = log_stream + self.__stage = stage + self.__ts_latest = datetime.now(timezone.utc).timestamp() + self.__filter = filter + self.__packets_in = deque() + self.__mqtt_threads = [] + self.__duplicate = TelemetryCombiner.DuplicateDatapoints(2) + + def enqueue_packet(self, packet): + """Add a packet to this telemetry combiner to be checked and sent""" + self.__packets_in.append(packet) + filter = self.filter() + self.__duplicate.insert(filter) + for mqtt_src in self.__mqtt_threads: + mqtt_src.publish(filter, self.get_mqtt_data_topic()) + + def add_mqtt(self, mqtt_thread: mqtt.MQTTThread): + """Add an MQTT data sink to this combiner""" + self.__mqtt_threads.append(mqtt_thread) + + def empty(self) -> bool: + """Returns whether or not this combiner is empty (has no waiting packets)""" + return len(self.__packets_in) == 0 + + def get_mqtt_data_topic(self) -> str: + """Return the data topic for the MQTT stream for this combiner""" + return "FlightData-" + self.__stage + + def get_mqtt_control_topic(self) -> str: + """Return the control topic for the MQTT stream for this combiner""" + return "Control-" + self.__stage + + def clear(self): + """Clear the packet queue of this combiner""" + self.__packets_in.clear() + + def filter(self): + """Returns a filtered list of packets that should be sent to the system from this combiner""" + seen_timestamps = set() + packet_release = [] + cur_latest = self.__ts_latest + self.__log.set_waiting(len(self.__packets_in)) + queue = copy.copy(self.__packets_in) + self.__packets_in = deque() + for packet in queue: + + # Check if packet passes filter + self.__log.console_log("Packet states: flt: (" + str(self.__filter.test(packet)) + ") dup: (" + str(self.__duplicate.check(packet)) + ")") + if not (packet['unix'] in seen_timestamps) and self.__filter.test(packet) and self.__duplicate.check(packet): + # Do use this packet + + if self.__ts_latest > packet['unix']: + self.__log.console_log(f"Released packet out of order! {self.__ts_latest} > {packet['unix']}") + + if (packet['unix'] > cur_latest): + cur_latest = packet['unix'] + + seen_timestamps.add(packet['unix']) + packet_release.append(packet) + + self.__log.success() + self.__log.waiting_delta(-1) + + + self.__ts_latest = cur_latest + self.__log.file_log(str(packet_release)) + return packet_release + diff --git a/ground/gss_combiner/util/logger.py b/ground/gss_combiner/util/logger.py new file mode 100644 index 00000000..fccd3258 --- /dev/null +++ b/ground/gss_combiner/util/logger.py @@ -0,0 +1,154 @@ +# GSS v1.1 logger utility +# Spaceshot Avionics Software 2024 + +from enum import Enum +import datetime +from pathlib import Path + +class LoggerOptions(): + def __init__(self, should_log, is_verbose) -> None: + self.should_log = should_log + self.is_verbose = is_verbose + +class LoggerType(str, Enum): + """Enum for possible log types""" + TELEM = "TELE" + COMBINER = "COMB" + MQTT = "MQTT" + +class LoggerStream(): + """Class to encapsulate logging at different levels for the GSS combiner system""" + def __init__(self, options: LoggerOptions, type: LoggerType, name: str, meta_category:str) -> None: + self.__title = type.value + " " + name + self.__filename = "./outputs/" + str(int(datetime.datetime.now().timestamp())) + "_" + type.value + name + "_raw_output.txt" + Path("./outputs/").mkdir(parents=True, exist_ok=True) + self.__lstype = type + self.__meta_cat = meta_category + self.__opts = options + self.__file = None + self.__failures = 0 + self.__success = 0 + self.__waiting = 0 + + self.__last_result = datetime.datetime.now().timestamp() + + if self.__opts.should_log and type != LoggerType.MQTT: + self.__file = open(self.__filename, "w") + + def serialize(self) -> dict: + """Turn this stream's vitals into an easy-to-send format""" + return self.__meta_cat, {"success": self.__success, "fail": self.__failures, "waiting": self.__waiting, "last": self.__last_result} + + + + def get_name(self) -> str: + return self.__title + + def __print(self, line: str): + """internal function to print data""" + print(f"[{self.__title}] {line}") + + def __log(self, line: str): + """internal function to log to file""" + self.__file.write(str(line)) + + def get_last_result_time(self): + """Get the last time this logger has had a `success` or `fail`.""" + return self.__last_result + + def console_log(self, line: str): + """Log to a console depending on verbosity level""" + if (self.__opts.is_verbose): + self.__print(line) + + def console_log_always(self, line: str): + """Log to a console depending on verbosity level""" + self.__print(line) + + def file_log(self, data: str): + """Log to a file depending on whether logging is enabled or not.""" + if (self.__opts.should_log): + self.__log(data) + + def success(self): + """Flag a successful transaction (useful for system health)""" + self.__success += 1 + self.__last_result = datetime.datetime.now().timestamp() + + def fail(self): + """Flag a failed transaction (useful for system health)""" + self.__failures += 1 + self.__last_result = datetime.datetime.now().timestamp() + + def set_waiting(self, number: int): + """Set the amount of waiting operations for this system (useful for system health)""" + self.__waiting = number + + def waiting_delta(self, num): + """Change the amount of waiting operations for this system (useful for system health)""" + self.__waiting += num + + def get_successes(self): + return self.__success + + def get_failures(self): + return self.__failures + + def get_waiting(self): + return self.__waiting + +class Logger(): + """Semi-singleton class for all logger streams in GSS 1.1""" + def __init__(self, options: LoggerOptions) -> None: + self.__options = options + self.__streams = {} + + def create_stream(self, stream_type: LoggerType, stream_name: str, stream_meta: str) -> LoggerStream: + stream = LoggerStream(self.__options, stream_type, stream_name, stream_meta) + self.__streams[stream_type + stream_name] = stream + return stream + + def streams(self): + return self.__streams + +def format_stat_string(name, logstream: LoggerStream): + """Format a system's status string depending on the contents of the provided `logstream`""" + successes = logstream.get_successes() + failures = logstream.get_failures() + waiting = logstream.get_waiting() + delta_last = datetime.datetime.now().timestamp() - logstream.get_last_result_time() + total = successes + failures + pct = 0 + if(total != 0): + pct = successes / total + + col = "\x1b[32m" + + if(pct < 0.8): + col = "\x1b[33m" + + if(pct < 0.5): + col = "\x1b[31m" + + col_waiting = "\x1b[32m" + if(waiting > 1): + col_waiting = "\x1b[33m" + if(waiting > 5): + col_waiting = "\x1b[31m" + + col_sysname = "\x1b[90m" + if successes > 0: + if delta_last < 1.5: + col_sysname = "\x1b[32m" + elif delta_last < 3: + col_sysname = "\x1b[33m" + else: + col_sysname = "\x1b[31m" + + return f"\x1b[1m{col_sysname}{name}\x1b[0m ({col}{(pct*100):.0f}%\x1b[0m : {col_waiting}{waiting}\x1b[0m) " + +def print_legend(uri_target): + """Print the legend for the system health display""" + print("Listening on MQTT URI: \x1b[34mmqtt://" + uri_target + ":1883\x1b[0m") + print("LEGEND: System \x1b[1m\x1b[90mInactive \x1b[32mNominal \x1b[33mDelayed \x1b[31mDisconnected\x1b[0m (Success % : Operations Waiting)") + print() \ No newline at end of file diff --git a/ground/gss_combiner/util/mappy.py b/ground/gss_combiner/util/mappy.py new file mode 100644 index 00000000..1e171ea0 --- /dev/null +++ b/ground/gss_combiner/util/mappy.py @@ -0,0 +1,56 @@ +import paho.mqtt.client as mqtt +import folium +import json +import threading +import time + +mymap = folium.Map(location=[0, 0], zoom_start=2) +map_lock = threading.Lock() +client = None + +def on_message(client, userdata, message): + data = json.loads(message.payload.decode()) + print("Received MQTT message:", data) + + lat = data['value']['gps_lat'] + lon = data['value']['gps_long'] + + print(lat, lon) + + if lat is not None and lon is not None: + with map_lock: + folium.Marker(location=[lat, lon]).add_to(mymap) + + +def mqtt_thread(): + global client + broker_address = "10.195.167.19" + topic = "FlightData-Sustainer" + client = mqtt.Client() + client.on_message = on_message + client.connect(broker_address) + client.subscribe(topic) + client.loop_forever() + + +def save_map_thread(): + while True: + time.sleep(5) + with map_lock: + mymap.save("mqtt_map.html") + + +if __name__ == "__main__": + mqtt_thread = threading.Thread(target=mqtt_thread) + mqtt_thread.start() + + save_map_thread = threading.Thread(target=save_map_thread) + save_map_thread.start() + + try: + mqtt_thread.join() + save_map_thread.join() + except KeyboardInterrupt: + if client: + client.disconnect() + diff --git a/ground/gss_combiner/util/mqtt.py b/ground/gss_combiner/util/mqtt.py new file mode 100644 index 00000000..b68fd9e8 --- /dev/null +++ b/ground/gss_combiner/util/mqtt.py @@ -0,0 +1,244 @@ +from datetime import datetime, timezone +from sys import getsizeof +import threading +import json +import traceback +import copy +import io + +import serial # PySerial +import paho.mqtt.client as mqtt + +import util.logger + +class TelemetryThread(threading.Thread): + """A thread class handling all communications between COM ports to which telemetry devices are connected.""" + def __init__(self, com_port, mqtt_uri, all_data_topic, log_stream: util.logger.LoggerStream) -> None: + super(TelemetryThread, self).__init__(daemon=True) + self.__log = log_stream + self.__topic = all_data_topic + self.__log.console_log(f"Opening {com_port}") + self.__comport: serial.Serial = serial.Serial(com_port, baudrate=4800, write_timeout=1) + self.__uri = mqtt_uri + self.__comport.reset_input_buffer() + self.__mqttclient = None + self.__log.console_log(f"Telemetry thread created.") + self.__combiners = [] + + self.__rf_set = True + self.__rf_freq = 0 + self.__last_rf_command_sent = datetime.now().timestamp() + self.__rf_command_period = 3 + + + def process_packet(self, packet_json): + """Append metadata to a packet to conform to GSS v1.1 packet structure""" + # Incoming packets are of form {"type": "data", value: { ... }} + time = datetime.now(timezone.utc) + + # Append timestamps :) + return {'value': packet_json['value'], 'type': packet_json['type'], 'utc': str(time), 'unix': datetime.timestamp(time), 'src': self.__comport.name} + + def write_frequency(self, frequency:float): + """Send a FREQ command to the associated telemetry device to change frequencies, then wait for a response.""" + self.__log.console_log("Writing frequency command (FREQ:" + str(frequency) + ")") + try: + self.__send_comport("FREQ:" + str(frequency) + "\n") + self.__rf_freq = frequency + self.__rf_set = False + self.__last_rf_command_sent = datetime.now().timestamp() + except Exception as e: + print("Unable to send FREQ command: ", e, "Continuing with no FREQ change.") + + def __send_comport(self, msg: str): + """Send arbirtary data to the associated COM port""" + self.__comport.write(msg.encode()) + + def __read_comport(self): + """Read all data from the associated COM port""" + if self.__comport.in_waiting: + p_full = "" + while self.__comport.in_waiting: + data = self.__comport.read_all() + p_full += bytes.decode(data, encoding="ascii") + + + return p_full.split("\n") + else: + return [] + + def add_combiner(self, combiner): + """Add a combiner data sink for this telemetry thread""" + self.__combiners.append(combiner) + + def ready(self) -> bool: + # On startup, tells the main process if this thread is ready + return self.__rf_set + + def run(self) -> None: + # Initialize MQTT + self.__mqttclient = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2) + self.__log.console_log(f"Connecting to MQTT @ {self.__uri}") + self.__mqttclient.connect(self.__uri) + self.__log.console_log(f"Telemetry thread ready.") + + while True: + # Wrap all in a try-except to catch errors. + + try: + + if not self.__rf_set and datetime.now().timestamp() - self.__last_rf_command_sent >= self.__rf_command_period: + # Send a new frequency command if the last one went unacknowledged. + self.__send_comport("FREQ:" + self.__rf_freq + "\n") + self.__last_rf_command_sent = datetime.now().timestamp() + + + # Read all from the comport + packets = self.__read_comport() + + if(len(packets) == 0): + # Defer if no data in COM port + continue + + + + # Process raw to packet + self.__log.console_log(f"Processing {len(packets) - 1} packets..") + self.__log.set_waiting(len(packets) - 1) + for pkt_r in packets: + pkt = pkt_r.rstrip() # Strip whitespace characters + if(len(pkt) == 0): + continue # Ignore empty data + try: + packet_in = json.loads(pkt) + + # self.__log.console_log(str(packet_in)) + if type(packet_in) == int: + self.__log.console_log("Read int? Discarding") + continue + if type(packet_in) == float: + self.__log.console_log("Read float? Discarding") + continue + + if type(packet_in) != dict: + print("Read non dict?" + str(pkt) + " type " + str(type(packet_in))) + + # print(packet_in) + if not self.__rf_set: + # Wait for freq change and periodically send new command to try to change freq. + if packet_in['type'] == "freq_success": + + if float(packet_in['frequency']) == float(self.__rf_freq): + self.__log.console_log_always("Frequency set: Listening on " + str(self.__rf_freq)) + self.__rf_set = True + else: + self.__log.console_log("Recieved incorrect frequency!") + continue + else: + self.__log.console_log("Recieved packet from wrong stream.. Discarding due to freq change.") + continue + + if packet_in['type'] == "heartbeat": + # Send heartbeat to common stream + heartbeat_only = {"battery_voltage": packet_in['value']['battery_voltage'], 'rssi': packet_in['value']['RSSI']} + raw_in = {"source": "gss_combiner", "action": "heartbeat", "time": datetime.now().timestamp(), "data": heartbeat_only} + proc_string = json.dumps(raw_in).encode('utf-8') + self.__mqttclient.publish("Common", proc_string) + self.__log.success() + continue + + if packet_in['type'] == 'data': + self.__log.console_log("reading packet type: " + ("sustainer" if packet_in['value']['is_sustainer'] else "booster")) + else: + # print() + # print(packet_in) + continue + except json.decoder.JSONDecodeError as json_err: + self.__log.console_log(f"Recieved corrupted JSON packet. Flushing buffer.") + self.__log.console_log(f" ---> DUMP_ERR: Recieved invalid packet of len {len(pkt)} : ") + self.__log.fail() + self.__log.waiting_delta(-1) + continue + + # Process and queue the packet + processed = self.process_packet(packet_in) + + for combiner in self.__combiners: + combiner.enqueue_packet(processed) + + + # Log all packets and send it to the all-data stream + proc_json = json.dumps(processed) + proc_string = proc_json.encode('utf-8') + self.__mqttclient.publish(self.__topic, proc_string) + self.__log.file_log(proc_json) + self.__log.console_log(f"Processed packet @ {processed['unix']} --> '{self.__topic}'") + self.__log.waiting_delta(-1) + self.__log.success() + + except Exception as e: + try: + print(f"[Telem {self.__comport.name}] Ran into an uncaught exception.. continuing gracefully.") # Always print these. + self.__log.console_log(f"Error dump:", traceback.format_exc(e)) + except Exception as e: + print("Exception while handling excpetion!") + +class MQTTThread(threading.Thread): + """A thread to handle all MQTT communication for the GSS combiner service.""" + def __init__(self, server_uri, log_stream: util.logger.LoggerStream) -> None: + super(MQTTThread, self).__init__(daemon=True) + self.__log = log_stream + self.__uri = server_uri + self.__mqttclient = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2) + + self.__log.console_log("Connecting to broker @ " + str(self.__uri)) + self.__mqttclient.connect(self.__uri) + self.__log.console_log("Subscribing to control streams...") + + # As of now this system has no need to listen to the control stream. + # def on_message(client, userdata, msg): + # print(msg.topic+" "+str(msg.payload)) + + # self.__mqttclient.on_message = on_message + + self.__log.console_log("MQTT systems initialized.") + + + def subscribe_control(self, combiner): + """Subscribe to a combiner's `control` topic""" + topic = combiner.get_mqtt_control_topic() + self.__mqttclient.subscribe(topic) + self.__log.console_log("Subscribed to control stream " + str(topic)) + + def publish(self, packet_list, topic) -> None: + """Publish a set of packets to a `Data` topic""" + self.__log.waiting_delta(len(packet_list)) + for data in packet_list: + data_encoded = json.dumps(data).encode("utf-8") + + self.__log.console_log(f"Publishing {getsizeof(data_encoded)} bytes to MQTT stream --> '{topic}'") + + try: + self.__mqttclient.publish(topic, data_encoded) + self.__log.success() + + except Exception as e: + self.__log.console_log("Unresolved packet dump: ", data_encoded) + print(f"Unable to publish to '{topic}' : ", str(e)) # Always print + self.__log.fail() + self.__log.waiting_delta(-1) + + def run(self) -> None: + self.__mqttclient.loop_start() + + while True: + pass + + def publish_common(self, data: str): + """Publish arbitrary data to the `Common` topic""" + try: + self.__mqttclient.publish("Common", data) + self.__log.success() + except Exception as e: + print(f"Unable to publish to 'Common' : ", str(e)) # Always print + self.__log.fail() \ No newline at end of file diff --git a/ground/gss_combiner/util/print_util.py b/ground/gss_combiner/util/print_util.py new file mode 100644 index 00000000..a1e999e9 --- /dev/null +++ b/ground/gss_combiner/util/print_util.py @@ -0,0 +1,17 @@ +# Stores the console printing for the GSS combiner to not clog up `main.py`. + +HELP_OUTPUT = """ +USAGE: +py ./main.py [options] + + --booster [source1],[source2],[etc..] -> Selects which COM ports should be interpreted as a data stream from the booster stage + --sustainer [source1],[source2],[etc..] -> Selects which COM ports should be interpreted as a data stream from the sustainer stage + --local (or -l) -> Streams all data to 'localhost' for testing. (Same as --ip localhost) + --no-log (or -n) -> Will not log data to logfiles for this run + --verbose (or -v) -> Prints all telemetry events to console + --no-vis (or -nv) -> Shows a visual display of all systems + --ip [IP] (or -i [IP]) -> Connects to a specific IP. (Overrides --local) + --help (or -h) -> Prints this menu + --config [config] (or -c [config]) -> Uses an argument config defined in config.ini. Added on top of existing params. + --no-rf -> Does not overwrite feather frequencies on startup +""" \ No newline at end of file diff --git a/ground/lib/RadioHead/RH_RF95.cpp b/ground/lib/RadioHead/RH_RF95.cpp index 655bbabe..9a4b5a6d 100644 --- a/ground/lib/RadioHead/RH_RF95.cpp +++ b/ground/lib/RadioHead/RH_RF95.cpp @@ -98,11 +98,6 @@ bool RH_RF95::init() bool RH_RF95::setupInterruptHandler() { - //were using polling so no interrupts should be setup - pinMode(_interruptPin, INPUT); - return true; - - // For some subclasses (eg RH_ABZ) we dont want to set up interrupt int interruptNumber = NOT_AN_INTERRUPT; if (_interruptPin != RH_INVALID_PIN) diff --git a/ground/lib/RadioHead/RH_RF95.h b/ground/lib/RadioHead/RH_RF95.h index 1ed0fdf2..e723d032 100644 --- a/ground/lib/RadioHead/RH_RF95.h +++ b/ground/lib/RadioHead/RH_RF95.h @@ -857,9 +857,7 @@ class RH_RF95 : public RHSPIDriver /// This is a low level function to handle the interrupts for one instance of RH_RF95. /// Called automatically by isr*() /// Should not need to be called by user code. - public: void handleInterrupt(); -protected: /// Examine the revceive buffer to determine whether the message is for this node void validateRxBuf(); @@ -894,9 +892,7 @@ class RH_RF95 : public RHSPIDriver static uint8_t _interruptCount; /// The configured interrupt pin connected to this instance -public: uint8_t _interruptPin; -private: /// The index into _deviceForInterrupt[] for this device (if an interrupt is already allocated) /// else 0xff diff --git a/ground/lib/RadioHead/RadioHead.h b/ground/lib/RadioHead/RadioHead.h index 3e4948d0..a87a5204 100644 --- a/ground/lib/RadioHead/RadioHead.h +++ b/ground/lib/RadioHead/RadioHead.h @@ -1797,11 +1797,8 @@ these examples and explanations and extend them to suit your needs. // #define ATOMIC_BLOCK_END portCLEAR_INTERRUPT_MASK_FROM_ISR(RH_ATOMIC_state); // These appear to be defined for all ESP32 type: #include "freertos/atomic.h" -// Hacky fix, if all spi stuff is on the same thread then this is unnessary -#define ATOMIC_BLOCK_START -#define ATOMIC_BLOCK_END -// #define ATOMIC_BLOCK_START ATOMIC_ENTER_CRITICAL() -// #define ATOMIC_BLOCK_END ATOMIC_EXIT_CRITICAL() + #define ATOMIC_BLOCK_START ATOMIC_ENTER_CRITICAL() + #define ATOMIC_BLOCK_END ATOMIC_EXIT_CRITICAL() #else // TO BE DONE: #define ATOMIC_BLOCK_START diff --git a/ground/platformio.ini b/ground/platformio.ini index a9d89727..2bb4bcf7 100644 --- a/ground/platformio.ini +++ b/ground/platformio.ini @@ -13,4 +13,23 @@ platform = atmelsam board = adafruit_feather_m0 framework = arduino build_src_filter = +<*> +build_flags = -DIS_GROUND +; test_ignore = test_local + + + +[env:Drone] +platform = atmelsam +board = adafruit_feather_m0 +framework = arduino +build_src_filter = +<*> +build_flags = -DIS_DRONE +; test_ignore = test_local + +[env:Test] +platform = atmelsam +board = adafruit_feather_m0 +framework = arduino +build_src_filter = +<*> +build_flags = -DIS_TEST ; test_ignore = test_local \ No newline at end of file diff --git a/ground/src/feather/main.cpp b/ground/src/feather/main.cpp index 04a9a430..a45fae11 100644 --- a/ground/src/feather/main.cpp +++ b/ground/src/feather/main.cpp @@ -10,6 +10,7 @@ * Patrick Marschoun * Peter Giannetos * Aaditya Voruganti + * Micheal Karpov */ #include @@ -19,6 +20,7 @@ #include #include #include +#include #include "SerialParser.h" @@ -28,18 +30,15 @@ #define RFM95_RST 4 // #define RFM95_EN #define RFM95_INT 3 +#define VoltagePin 14 // #define LED 13 // Blinks on receipt -/* Pins for Teensy 31*/ -// Ensure to change depending on wiring -// #define RFM95_CS 10 -// #define RFM95_RST 15 -// #define RFM95_EN 14 -// #define RFM95_INT 16 -// #define LED 13 // Blinks on receipt +float RF95_FREQ = 420; +float SUSTAINER_FREQ = 426.15; +float BOOSTER_FREQ = 425.15; +float GROUND_FREQ = 420; -// Change to 434.0 or other frequency, must match RX's freq! -#define RF95_FREQ 433.0 +float current_freq = 0; #define DEFAULT_CMD 0 #define MAX_CMD_LEN 10 @@ -55,126 +54,57 @@ short readySend = 0; int command_ID = 0; short cmd_number = 0; +constexpr const char* json_command_success = R"({"type": "command_success"})"; +constexpr const char* json_command_parse_error = R"({"type": "command_error", "error": "serial parse error"})"; +constexpr const char* json_buffer_full_error = R"({"type": "command_error", "error": "command buffer not empty"})"; + +constexpr const char* json_init_failure = R"({"type": "init_error", "error": "failed to initilize LORA"})"; +constexpr const char* json_init_success = R"({"type": "init_success"})"; +constexpr const char* json_set_frequency_failure = R"({"type": "freq_error", "error": "set_frequency failed"})"; +constexpr const char* json_receive_failure = R"({"type": "receive_error", "error": "recv failed"})"; +constexpr const char* json_send_failure = R"({"type": "send_error", "error": "command_retries_exceded"})"; +constexpr int max_command_retries = 5; + + template float convert_range(T val, float range) { size_t numeric_range = (int64_t)std::numeric_limits::max() - (int64_t)std::numeric_limits::min() + 1; - return val * range / (float)numeric_range; + return static_cast(val) * range / (float)numeric_range; } -struct TelemetryDataLite { - uint32_t timestamp; //[0, 2^32] - - uint16_t barometer_pressure; //[0, 4096] - int16_t highG_ax; //[128, -128] - int16_t highG_ay; //[128, -128] - int16_t highG_az; - - int16_t bno_roll; //[-4,4] - int16_t bno_pitch; //[128, -128] - int16_t bno_yaw; //[-4,4] - - -}; - struct TelemetryPacket { - int8_t datapoint_count; - //[0, 2^16] - TelemetryDataLite datapoints[4]; - float gps_lat; - float gps_long; - float gps_alt; - float yaw; - float pitch; - float roll; - int16_t mag_x; //[-4, 4] - int16_t mag_y; //[-4, 4] - int16_t mag_z; //[-4, 4] - int16_t gyro_x; //[-4096, 4096] - int16_t gyro_y; //[-4096, 4096] - int16_t gyro_z; //[-4096, 4096] - int16_t response_ID; //[0, 2^16] - int8_t rssi; //[-128, 128] - uint8_t voltage_battery; //[0, 16] - uint8_t FSM_State; //[0,256] - int16_t barometer_temp; //[-128, 128] - - // Add pyros array for pyro channels - bool continuity[4]; - bool pyros_armed[4]; - bool pyros_firing[4]; - - // // Add continuity array for continuity pins - // uint8_t continuity[4]; - - - // float gnc_state_x; - // float gnc_state_vx; - // float gnc_state_ax; - // float gnc_state_y; - // float gnc_state_vy; - // float gnc_state_ay; - // float gnc_state_z; - // float gnc_state_vz; - // float gnc_state_az; - // float gnc_state_apo; - // uint8_t FSM_State; //[0,256] - - char callsign[8]; + int32_t lat; + int32_t lon; + int16_t alt; + int16_t baro_alt; + uint16_t highg_ax; //14 bit signed ax [-16,16) 2 bit tilt angle + uint16_t highg_ay; //1bit sign 13 bit unsigned [0,16) 2 bit tilt angle + uint16_t highg_az; //1bit sign 13 bit unsigned [0,16) 2 bit tilt angle + uint8_t batt_volt; + uint8_t fsm_satcount; + float RSSI = 0.0; }; struct FullTelemetryData { systime_t timestamp; //[0, 2^32] - - float barometer_pressure; //[0, 4096] - float highG_ax; //[128, -128] - float highG_ay; //[128, -128] - float highG_az; - - float bno_roll; //[-4,4] - //[-4,4] - float bno_yaw; //[-4,4] - float bno_pitch; //[128, -128] - float gps_lat; - float gps_long; - float gps_alt; - float yaw; - float pitch; - float roll; - float mag_x; //[-4, 4] - float mag_y; //[-4, 4] - float mag_z; //[-4, 4] - float gyro_x; //[-4096, 4096] - float gyro_y; //[-4096, 4096] - float gyro_z; //[-4096, 4096] - int16_t response_ID; //[0, 2^16] - int8_t rssi; //[-128, 128] - int8_t datapoint_count; //[0,4] - float voltage_battery; //[0, 16] - float barometer_temp; //[-128, 128] - - // Add pyros array for pyro channels - bool pyros_armed[4]; - bool pyros_firing[4]; - - // Add continuity array for continuity pins - bool continuity[4]; - - - float gnc_state_x; - float gnc_state_vx; - float gnc_state_ax; - float gnc_state_y; - float gnc_state_vy; - float gnc_state_ay; - float gnc_state_z; - float gnc_state_vz; - float gnc_state_az; - float gnc_state_apo; - uint8_t FSM_State; //[0,256] - long unsigned int print_time; - char callsign[8]; + uint16_t altitude; // [0, 4096] + float latitude; // [-90, 90] + float longitude; // [-180, 180] + float barometer_altitude; // [0, 4096] + float highG_ax; // [-16, 16] + float highG_ay; // [-16, 16] + float highG_az; // [-16, 16] + float battery_voltage; // [0, 5] + uint8_t FSM_State; // [0, 255] + float tilt_angle; // [-90, 90] + float freq; + float rssi; + float sat_count; + bool is_sustainer; }; + + enum class CommandType { SET_FREQ, SET_CALLSIGN, ABORT, TEST_FLAP, EMPTY }; // Commands transmitted from ground station to rocket struct telemetry_command { @@ -196,18 +126,8 @@ struct TelemetryCommandQueueElement { std::queue cmd_queue; std::queue print_queue; -constexpr const char* json_command_success = R"({"type": "command_success"})"; -constexpr const char* json_command_parse_error = R"({"type": "command_error", "error": "serial parse error"})"; -constexpr const char* json_buffer_full_error = R"({"type": "command_error", "error": "command buffer not empty"})"; -constexpr const char* json_init_failure = R"({"type": "init_error", "error": "failed to initilize LORA"})"; -constexpr const char* json_init_success = R"({"type": "init_success"})"; -constexpr const char* json_set_frequency_failure = R"({"type": "freq_error", "error": "set_frequency failed"})"; -constexpr const char* json_receive_failure = R"({"type": "receive_error", "error": "recv failed"})"; -constexpr const char* json_send_failure = R"({"type": "send_error", "error": "command_retries_exceded"})"; -constexpr int max_command_retries = 5; -float current_freq = RF95_FREQ; void printFloat(float f, int precision = 5) { if (isinf(f) || isnan(f)) { @@ -216,68 +136,62 @@ void printFloat(float f, int precision = 5) { Serial.print(f, precision); } } +int decodeLastTwoBits(uint16_t ax, uint16_t ay, uint16_t az) { + int tilt_ax = ax & 0b11; + int tilt_ay = ay & 0b11; + int tilt_az = az & 0b11; + int tilt = (tilt_ax << 0) | (tilt_ay << 2) | (tilt_az << 4); + return tilt; +} + +double ConvertGPS(int32_t coord) { + double mins = fmod(static_cast(std::abs(coord)), 10000000) / 100000.; + double degs = floor(static_cast(std::abs(coord)) / 10000000.); + double complete = (degs + (mins / 60.)); + if (coord < 0) { + complete *= -1.; + } + return complete; +} void EnqueuePacket(const TelemetryPacket& packet, float frequency) { - if (packet.datapoint_count == 0) return; - int64_t start_timestamp = packet.datapoints[0].timestamp; int64_t start_printing = millis(); - for (int i = 0; i < packet.datapoint_count && i < 4; i++) { - FullTelemetryData item; - TelemetryDataLite data = packet.datapoints[i]; - item.barometer_pressure = convert_range(data.barometer_pressure, 4096); - item.barometer_temp = convert_range(packet.barometer_temp, 256); - item.highG_ax = convert_range(data.highG_ax, 256); - item.highG_ay = convert_range(data.highG_ay, 256); - item.highG_az = convert_range(data.highG_az, 256); - item.gyro_x = convert_range(packet.gyro_x, 8192); - item.gyro_y = convert_range(packet.gyro_y, 8192); - item.gyro_z = convert_range(packet.gyro_z, 8192); - item.bno_roll = convert_range(data.bno_roll, 8); - item.bno_pitch= convert_range(data.bno_pitch, 8); - item.bno_yaw = convert_range(data.bno_yaw, 8); - item.mag_x = convert_range(packet.mag_x, 8); - item.mag_y = convert_range(packet.mag_y, 8); - item.mag_z = convert_range(packet.mag_z, 8); - //item.flap_extension = data.flap_extension; - item.gps_alt = packet.gps_alt; - item.gps_lat = packet.gps_lat; - item.gps_long = packet.gps_long; - //item.freq = frequency; - item.FSM_State = packet.FSM_State; - // item.gnc_state_ax = packet.gnc_state_ax; - // item.gnc_state_vx = packet.gnc_state_vx; - // item.gnc_state_x = packet.gnc_state_x; - // item.gnc_state_apo = packet.gnc_state_apo; - item.response_ID = packet.response_ID; - item.rssi = packet.rssi; - item.voltage_battery = convert_range(packet.voltage_battery, 16); - item.print_time = start_printing - start_timestamp + data.timestamp; - item.continuity[0] = packet.continuity[0]; - item.continuity[1] = packet.continuity[1]; - item.continuity[2] = packet.continuity[2]; - item.continuity[3] = packet.continuity[3]; - item.pyros_armed[0] = packet.pyros_armed[0]; - item.pyros_armed[1] = packet.pyros_armed[1]; - item.pyros_armed[2] = packet.pyros_armed[2]; - item.pyros_armed[3] = packet.pyros_armed[3]; - item.pyros_firing[0] = packet.pyros_firing[0]; - item.pyros_firing[1] = packet.pyros_firing[1]; - item.pyros_firing[2] = packet.pyros_firing[2]; - item.pyros_firing[3] = packet.pyros_firing[3]; - - item.callsign[0] = packet.callsign[0]; - item.callsign[1] = packet.callsign[1]; - item.callsign[2] = packet.callsign[2]; - item.callsign[3] = packet.callsign[3]; - item.callsign[4] = packet.callsign[4]; - item.callsign[5] = packet.callsign[5]; - item.callsign[6] = packet.callsign[6]; - item.callsign[7] = packet.callsign[7]; - - print_queue.emplace(item); + FullTelemetryData data; + data.timestamp = start_printing; + + data.altitude = static_cast(packet.alt); + data.latitude = ConvertGPS(packet.lat); + data.longitude = ConvertGPS(packet.lon); + data.barometer_altitude = convert_range(packet.baro_alt, 1 << 17); + int tilt = decodeLastTwoBits(packet.highg_ax, packet.highg_ay, packet.highg_az); + int16_t ax = packet.highg_ax & 0xfffc; + int16_t ay = packet.highg_ay & 0xfffc; + int16_t az = packet.highg_az & 0xfffc; + data.highG_ax = convert_range(ax, 32); + data.highG_ay = convert_range(ay, 32); + data.highG_az = convert_range(az, 32); + data.tilt_angle = tilt; //convert_range(tilt, 180); // [-90, 90] + data.battery_voltage = convert_range(packet.batt_volt, 16); + data.sat_count = packet.fsm_satcount >> 4 & 0b0111; + data.is_sustainer = (packet.fsm_satcount >> 7); + data.FSM_State = packet.fsm_satcount & 0b1111; + + // kinda hacky but it will work + if (packet.fsm_satcount == static_cast(-1)) { + data.FSM_State = static_cast(-1); + } + + data.freq = RF95_FREQ; + if(packet.RSSI == 0.0) { + data.rssi = packet.RSSI; + } else { + data.rssi = rf95.lastRssi(); } + data.rssi = rf95.lastRssi(); + print_queue.emplace(data); + } void printJSONField(const char* name, float val, bool comma = true) { @@ -306,56 +220,34 @@ void printJSONField(const char* name, const char* val, bool comma = true) { } void printPacketJson(FullTelemetryData const& packet) { - Serial.print(R"({"type": "data", "value": {)"); - printJSONField("response_ID", packet.response_ID); - printJSONField("gps_lat", packet.gps_lat); - printJSONField("gps_long", packet.gps_long); - printJSONField("gps_alt", packet.gps_alt); - printJSONField("KX_IMU_ax", packet.highG_ax); - printJSONField("KX_IMU_ay", packet.highG_ay); - printJSONField("KX_IMU_az", packet.highG_az); - printJSONField("IMU_gx", packet.gyro_x); - printJSONField("IMU_gy", packet.gyro_y); - printJSONField("IMU_gz", packet.gyro_z); - printJSONField("IMU_mx", packet.mag_x); - printJSONField("IMU_my", packet.mag_y); - printJSONField("IMU_mz", packet.mag_z); - printJSONField("FSM_state", packet.FSM_State); - printJSONField("sign", packet.callsign); - printJSONField("RSSI", rf95.lastRssi()); - printJSONField("Voltage", packet.voltage_battery); - // printJSONField("frequency", packet.freq); - // printJSONField("flap_extension", packet.flap_extension); - printJSONField("Continuity1", packet.continuity[0]); - printJSONField("Continuity2", packet.continuity[1]); - printJSONField("Continuity3", packet.continuity[2]); - printJSONField("Continuity4", packet.continuity[3]); - printJSONField("Pyro1", packet.pyros_armed[0]); - printJSONField("Pyro2", packet.pyros_armed[1]); - printJSONField("Pyro3", packet.pyros_armed[2]); - printJSONField("Pyro4", packet.pyros_armed[3]); - printJSONField("Pyro1Firing", packet.pyros_firing[0]); - printJSONField("Pyro2Firing", packet.pyros_firing[1]); - printJSONField("Pyro3Firing", packet.pyros_firing[2]); - printJSONField("Pyro4Firing", packet.pyros_firing[3]); - - // printJSONField("STE_ALT", packet.gnc_state_x); - // printJSONField("STE_VEL", packet.gnc_state_vx); - // printJSONField("STE_ACC", packet.gnc_state_ax); - // printJSONField("STE_APO", packet.gnc_state_apo); - printJSONField("BNO_YAW", packet.bno_yaw); - printJSONField("BNO_PITCH", packet.bno_pitch); - printJSONField("BNO_ROLL", packet.bno_roll); - printJSONField("TEMP", packet.barometer_temp); - printJSONField("pressure", packet.barometer_pressure, false); + + bool is_heartbeat = packet.FSM_State == static_cast(-1); + + Serial.print(R"({"type": ")"); + Serial.print(is_heartbeat ? "heartbeat" : "data"); + Serial.print(R"(", "value": {)"); + printJSONField("barometer_altitude", packet.barometer_altitude); + printJSONField("latitude", packet.latitude); + printJSONField("longitude", packet.longitude); + printJSONField("altitude", packet.altitude); + printJSONField("highG_ax", packet.highG_ax); + printJSONField("highG_ay", packet.highG_ay); + printJSONField("highG_az", packet.highG_az); + printJSONField("battery_voltage", packet.battery_voltage); + printJSONField("FSM_State", packet.FSM_State); + printJSONField("tilt_angle", packet.tilt_angle); + printJSONField("frequency", packet.freq); + printJSONField("RSSI", packet.rssi); + printJSONField("sat_count", packet.sat_count); + printJSONField("is_sustainer", packet.is_sustainer, false); Serial.println("}}"); } + void PrintDequeue() { if (print_queue.empty()) return; auto packet = print_queue.front(); - if (packet.print_time > millis()) return; print_queue.pop(); printPacketJson(packet); } @@ -365,19 +257,21 @@ void SerialError() { Serial.println(json_command_parse_error); } void set_freq_local_bug_fix(float freq) { telemetry_command t; t.command = CommandType::EMPTY; - rf95.send((uint8_t*)&t, sizeof(t)); + rf95.send((uint8_t*)&t, 0); + Serial.println(sizeof(t)); rf95.waitPacketSent(); rf95.setFrequency(freq); - current_freq = freq; + Serial.println(json_command_success); + Serial.print(R"({"type": "freq_success", "frequency":)"); + Serial.print(freq); + Serial.println("}"); } void SerialInput(const char* key, const char* value) { - /* If queue is not empty, do not accept new command*/ if (!cmd_queue.empty()) { Serial.println(json_buffer_full_error); return; } - telemetry_command command{}; if (strcmp(key, "ABORT") == 0) { command.command = CommandType::ABORT; @@ -413,7 +307,6 @@ void SerialInput(const char* key, const char* value) { void process_command_queue() { if (cmd_queue.empty()) return; - TelemetryCommandQueueElement cmd = cmd_queue.front(); rf95.send((uint8_t*)&cmd.command, sizeof(cmd.command)); rf95.waitPacketSent(); @@ -425,108 +318,73 @@ void setup() { while (!Serial) ; Serial.begin(9600); - if (!rf95.init()) { Serial.println(json_init_failure); - while (1) - ; + while (1); } - pinMode(LED_BUILTIN, OUTPUT); Serial.println(json_init_success); - - // Defaults after init are 434.0MHz, modulation GFSK_Rb250Fd250, +13dbM + #ifdef IS_GROUND if (!rf95.setFrequency(RF95_FREQ)) { Serial.println(json_set_frequency_failure); - while (1) - ; + while (1); } + + current_freq = RF95_FREQ; + #endif + #ifdef IS_DRONE + if (!rf95.setFrequency(SUSTAINER_FREQ)) { + Serial.println(json_set_frequency_failure); + + while (1); + } + current_freq = SUSTAINER_FREQ; + #endif + rf95.setCodingRate4(8); + rf95.setSpreadingFactor(10); + rf95.setPayloadCRC(true); + rf95.setSignalBandwidth(125000); Serial.print(R"({"type": "freq_success", "frequency":)"); - Serial.print(RF95_FREQ); + Serial.print(current_freq); Serial.println("}"); - - // Defaults after init are 434.0MHz, 13dBm, Bw = 125 kHz, Cr = 4/5, Sf = - // 128chips/symbol, CRC on - - // The default transmitter power is 13dBm, using PA_BOOST. - // If you are using RFM95/96/97/98 modules which uses the PA_BOOST - // transmitter pin, then you can set transmitter powers from 5 to 23 dBm: rf95.setTxPower(23, false); } +void ChangeFrequency(float freq) { + float current_time = millis(); + rf95.setFrequency(freq); + Serial.println(json_command_success); + Serial.print(R"({"type": "freq_success", "frequency":)"); + Serial.print(freq); + Serial.println("}"); +} + +#ifdef IS_GROUND + void loop() { + PrintDequeue(); - // static float f = 0; - // static float f2 = 0; - // f+=0.1; - // f2 += 0.01; - // if(f > 3.14) f -= 6.28; - // delay(30); - // FullTelemetryData d{}; - // d.barometer_pressure = 1000; - // d.barometer_temp = 20; - // d.bno_pitch = cos(f2); - // d.bno_roll = sin(f2); - // d.bno_yaw = 0; - // d.flap_extension = f / 10; - // d.freq = 434; - // d.FSM_State = 3; - // d.gnc_state_ax = f * 100; - // d.gnc_state_vx = f * 10; - // d.gnc_state_x = f * 1000; - // d.gnc_state_apo = 100; - // d.gps_alt = 1000+100*f; - // d.gps_lat = 40; - // d.gps_long = 80 + f; - // d.gyro_x = sin(f2); - // d.gyro_y = f+30; - // d.gyro_z = f+40; - // d.highG_ax = 10+f; - // d.highG_ay = f/10; - // d.highG_az = f/10; - // d.mag_x = f; - // d.mag_y = f+1; - // d.mag_z = f+2; - // d.voltage_battery = f + 4; - // printPacketJson(d); - if (rf95.available()) { - // Should be a message for us now uint8_t buf[RH_RF95_MAX_MESSAGE_LEN]; - // telemetry_data data{}; TelemetryPacket packet; - TelemetryDataLite data; uint8_t len = sizeof(buf); if (rf95.recv(buf, &len)) { - Serial.println(len); - Serial.println("Received packet"); - Serial.println(packet.datapoints[0].barometer_pressure); - digitalWrite(LED_BUILTIN, HIGH); delay(50); digitalWrite(LED_BUILTIN, LOW); + // Serial.println("Received packet"); + // Serial.println(len); memcpy(&packet, buf, sizeof(packet)); EnqueuePacket(packet, current_freq); - if (!cmd_queue.empty()) { auto& cmd = cmd_queue.front(); - if (cmd.command.id == packet.response_ID) { - if (cmd.command.command == CommandType::SET_FREQ) { - set_freq_local_bug_fix(cmd.command.freq); - Serial.print(R"({"type": "freq_success", "frequency":)"); - Serial.print(cmd.command.freq); - Serial.println("}"); - } - cmd_queue.pop(); - } else { cmd.retry_count++; if (cmd.retry_count >= max_command_retries) { cmd_queue.pop(); Serial.println(json_send_failure); } - } } process_command_queue(); @@ -536,4 +394,105 @@ void loop() { } } serial_parser.read(); -} \ No newline at end of file + if (Serial.available()) { + String input = Serial.readStringUntil('\n'); + if (input.startsWith("FREQ:")) { + float freq = input.substring(5).toFloat(); // Extract frequency value + set_freq_local_bug_fix(freq); + RF95_FREQ = freq; + current_freq = freq; + } + } +} +#endif + + + +#ifdef IS_DRONE +unsigned long prev_time = 0; +unsigned long heartbeat_time = 0; + +uint8_t readBatteryVoltage() { + int batteryADC = analogRead(9); + float batteryVoltage = (batteryADC * 3.3 * 2) / 1024.0; //5.0Vmax + if (batteryVoltage > 5.0) { + batteryVoltage = 5.0; + } + if (batteryVoltage < 0.0) { + batteryVoltage = 0.0; + } + + uint8_t battery = static_cast((batteryVoltage/5.0)*255); + return battery; +} + + +void loop() { + + PrintDequeue(); + unsigned long current_time = millis(); + if (current_time - prev_time > 2000) { + if(current_freq == SUSTAINER_FREQ) { + ChangeFrequency(BOOSTER_FREQ); + current_freq = BOOSTER_FREQ; + Serial.println("Sustainer timeout, Switching to booster freq"); + } else { + ChangeFrequency(SUSTAINER_FREQ); + current_freq = SUSTAINER_FREQ; + Serial.println("Booster timeout, Switching to sustainer freq"); + } + prev_time = millis(); + } + if(millis() - heartbeat_time > 2000) { + Serial.println("Heartbeat"); + TelemetryPacket packet; + rf95.setFrequency(GROUND_FREQ); + packet.batt_volt = readBatteryVoltage(); + packet.fsm_satcount = -1; + rf95.send((uint8_t*)&packet, sizeof(packet)); + rf95.waitPacketSent(); + rf95.setFrequency(current_freq); + heartbeat_time = millis(); + } + if (rf95.available()) { + TelemetryPacket packet; + uint8_t buf[RH_RF95_MAX_MESSAGE_LEN]; + uint8_t len = sizeof(buf); + + if (rf95.recv(buf, &len)) { + Serial.print("Recieved "); + Serial.print(len); + Serial.print(" bytes on "); + Serial.println(current_freq); + + + digitalWrite(LED_BUILTIN, HIGH); + delay(50); + digitalWrite(LED_BUILTIN, LOW); + memcpy(&packet, buf, sizeof(packet)); + packet.RSSI = rf95.lastRssi(); + EnqueuePacket(packet, current_freq); + set_freq_local_bug_fix(GROUND_FREQ); + rf95.send((uint8_t*)&packet, sizeof(packet)); + + if(current_freq == SUSTAINER_FREQ) { + set_freq_local_bug_fix(BOOSTER_FREQ); + current_freq = BOOSTER_FREQ; + Serial.println("Switching to booster freq"); + } else { + set_freq_local_bug_fix(SUSTAINER_FREQ); + current_freq = SUSTAINER_FREQ; + Serial.println("Switching to sust69ainer freq"); + } + prev_time = millis(); + // Serial.print(current_time); + } else { + Serial.println(json_receive_failure); + } + } + serial_parser.read(); +} +#endif + + + diff --git a/ground/tiles/8/64/96.jpeg b/ground/tiles/8/64/96.jpeg new file mode 100644 index 00000000..ea29eba0 Binary files /dev/null and b/ground/tiles/8/64/96.jpeg differ diff --git a/ground/tiles/8/64/97.jpeg b/ground/tiles/8/64/97.jpeg new file mode 100644 index 00000000..84cff75a Binary files /dev/null and b/ground/tiles/8/64/97.jpeg differ diff --git a/ground/tiles/8/65/96.jpeg b/ground/tiles/8/65/96.jpeg new file mode 100644 index 00000000..c8f33a75 Binary files /dev/null and b/ground/tiles/8/65/96.jpeg differ diff --git a/ground/tiles/8/65/97.jpeg b/ground/tiles/8/65/97.jpeg new file mode 100644 index 00000000..42f2ce99 Binary files /dev/null and b/ground/tiles/8/65/97.jpeg differ diff --git a/ground/tiles/8/66/96.jpeg b/ground/tiles/8/66/96.jpeg new file mode 100644 index 00000000..65b007bb Binary files /dev/null and b/ground/tiles/8/66/96.jpeg differ diff --git a/ground/tiles/8/66/97.jpeg b/ground/tiles/8/66/97.jpeg new file mode 100644 index 00000000..a0368351 Binary files /dev/null and b/ground/tiles/8/66/97.jpeg differ diff --git a/ground/tiles/9/128/192.jpeg b/ground/tiles/9/128/192.jpeg new file mode 100644 index 00000000..b70e8221 Binary files /dev/null and b/ground/tiles/9/128/192.jpeg differ diff --git a/ground/tiles/9/128/193.jpeg b/ground/tiles/9/128/193.jpeg new file mode 100644 index 00000000..7d7ed220 Binary files /dev/null and b/ground/tiles/9/128/193.jpeg differ diff --git a/ground/tiles/9/128/194.jpeg b/ground/tiles/9/128/194.jpeg new file mode 100644 index 00000000..cf8d808f Binary files /dev/null and b/ground/tiles/9/128/194.jpeg differ diff --git a/ground/tiles/9/129/192.jpeg b/ground/tiles/9/129/192.jpeg new file mode 100644 index 00000000..99b6f8dd Binary files /dev/null and b/ground/tiles/9/129/192.jpeg differ diff --git a/ground/tiles/9/129/193.jpeg b/ground/tiles/9/129/193.jpeg new file mode 100644 index 00000000..a5fa2b64 Binary files /dev/null and b/ground/tiles/9/129/193.jpeg differ diff --git a/ground/tiles/9/129/194.jpeg b/ground/tiles/9/129/194.jpeg new file mode 100644 index 00000000..07381c14 Binary files /dev/null and b/ground/tiles/9/129/194.jpeg differ diff --git a/ground/tiles/9/130/192.jpeg b/ground/tiles/9/130/192.jpeg new file mode 100644 index 00000000..cec966bb Binary files /dev/null and b/ground/tiles/9/130/192.jpeg differ diff --git a/ground/tiles/9/130/193.jpeg b/ground/tiles/9/130/193.jpeg new file mode 100644 index 00000000..7dff2e9d Binary files /dev/null and b/ground/tiles/9/130/193.jpeg differ diff --git a/ground/tiles/9/130/194.jpeg b/ground/tiles/9/130/194.jpeg new file mode 100644 index 00000000..af0782ab Binary files /dev/null and b/ground/tiles/9/130/194.jpeg differ diff --git a/ground/tiles/9/131/192.jpeg b/ground/tiles/9/131/192.jpeg new file mode 100644 index 00000000..fde9a105 Binary files /dev/null and b/ground/tiles/9/131/192.jpeg differ diff --git a/ground/tiles/9/131/193.jpeg b/ground/tiles/9/131/193.jpeg new file mode 100644 index 00000000..e7913699 Binary files /dev/null and b/ground/tiles/9/131/193.jpeg differ diff --git a/ground/tiles/9/131/194.jpeg b/ground/tiles/9/131/194.jpeg new file mode 100644 index 00000000..7e5f01bd Binary files /dev/null and b/ground/tiles/9/131/194.jpeg differ diff --git a/ground/tiles/9/132/192.jpeg b/ground/tiles/9/132/192.jpeg new file mode 100644 index 00000000..e9c164d4 Binary files /dev/null and b/ground/tiles/9/132/192.jpeg differ diff --git a/ground/tiles/9/132/193.jpeg b/ground/tiles/9/132/193.jpeg new file mode 100644 index 00000000..62cbf111 Binary files /dev/null and b/ground/tiles/9/132/193.jpeg differ diff --git a/ground/tiles/9/132/194.jpeg b/ground/tiles/9/132/194.jpeg new file mode 100644 index 00000000..6a590683 Binary files /dev/null and b/ground/tiles/9/132/194.jpeg differ diff --git a/ground/tiles/metadata.json b/ground/tiles/metadata.json new file mode 100644 index 00000000..7c7313f3 --- /dev/null +++ b/ground/tiles/metadata.json @@ -0,0 +1 @@ +{"minzoom":8,"maxzoom":9,"bounds":[-89.76636410993605,39.52058120409785,-86.89506357882254,40.704515803077186]} \ No newline at end of file diff --git a/mqtt_map.html b/mqtt_map.html new file mode 100644 index 00000000..1c5f428e --- /dev/null +++ b/mqtt_map.html @@ -0,0 +1,7763 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + \ No newline at end of file diff --git a/offline_osm_map.html b/offline_osm_map.html new file mode 100644 index 00000000..c2e9fdd7 --- /dev/null +++ b/offline_osm_map.html @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + \ No newline at end of file