Skip to content

Commit

Permalink
Merge branch 'main' into bach/feature/config_parser
Browse files Browse the repository at this point in the history
  • Loading branch information
waridh committed Aug 14, 2024
2 parents d0b39bc + 3c3529a commit ecc5fd4
Show file tree
Hide file tree
Showing 18 changed files with 1,087 additions and 18 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,5 @@ cython_debug/

# MacOS
.DS_Store

.idea/
2 changes: 2 additions & 0 deletions ADCS/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*.un~
*.swp
75 changes: 75 additions & 0 deletions ADCS/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# ADCS Simulated Subsystem

## Overview

The ADCS, also known as the Altitude Determination and Control System, is a
system that controls the orientation of ex3.

The ADCS is used to help steer the satellite.

## Commands
All currently implemented commands are listed in the following table. [Here](https://docs.google.com/spreadsheets/d/1rWde3jjrgyzO2fsg2rrVAKxkPa2hy-DDaqlfQTDaNxg/edit?gid=0#gid=0) is a spreadsheet on the commands.
|COMMAND NAME| PARAMETERS | PARAMETER TYPE | RETURN | RETURN TYPE | DESCRIPTION |
|-|-|-|-|-|-|
| `HELP` | N/A | N/A | list of all commands | string | Returns all working commands
| `GS` | N/A | N/A | State of the ADCS ("WORKING" or "OFF") | string | Gets the current state of the ADCS
| `ON` | N/A | N/A | N/A | N/A | Sets the ADCS state to "WORKING"
| `OFF` | N/A | N/A | N/A | N/A | Sets the ADCS state to "OFF"
| `GWS` | N/A | N/A | Wheel speed x, y, & z | tuple of floats | Records and sends the wheel speed of the ADCS
| `SWS` | Wheel speed x, y, & z | floats in RPM | N/A | N/A | Sets the ADCS wheel speed on all three axes in RPM
| `SC` | N/A | N/A | House keeping data | string | Generates random number for voltage, current, and temperature of the adcs. Also generates an "OK" or "BAD" overall status
| `SMC` | Magnetorquer current x, y, & z | floats in mA | N/A | N/A | Sets the ADCS's magnetorquer current on all three axes in mA
| `GMC` | N/A | N/A | Magnetorquer current x, y, & z | tuple of floats | Records and sends the magnetorquer currents of the ADCS
| `GTM` | N/A | N/A | Time | float in s | Sends the time recorded by the ADCS
| `STM` | Time | float in s | N/A | N/A | Sets the time currently recorded by the ADCS
| `GOR` | N/A | N/A | ADCS angles x, y, z | tuple of floats | Returns the current orientation of the ADCS in degrees
| `RESET` | N/A | N/A | N/A | N/A | Sets the wheel speeds to 0 RPM, and the magnetorquer currents to 0 mA
| `EXIT` | N/A | N/A | N/A | N/A | Disconnect the client from the server, and closes the server. This is the only command that is not stored with the `adcs_subsystem.py` file

## Usage

Start the server by running `adcs_server.py`, and connect to the server using some program/utility such as `nc`

```bash
$ python3 .ADCS/adcs_server.py 8000
Starting ADCS subsystem on port 8000
```
Then in a separate terminal process you can connect to the server on localhost:8000. Note if no host or port is specified the program defaults to localhost:42123

## Example usage
Here is an example of connecting to the server sending the `HELP`, `GWS`, and `SWS` command. Exit by sending `^C` from the client side.
```
$ nc localhost 8000
HELP
HELP | help (string)
GS | Get State (OFF or WORKING)
ON | Set state WORKING
OFF | Set state OFF
GWS | Get wheel speed (tuple)
SWS:x:y:z | set wheel speed
GMC | get magnetorquer current (tuple)
SMC:x:y:z | set magnetorquer current
SC | Status Check (string)
GTM | Get time (float)
STM:float | Set time
GOR | Get orientation (tuple)
RESET | Resets wheels and magnetorquer
GWS
(0, 0, 0)
SWS:10.3:-32.4:13.536
GWS
(10.3, -32.4, 13.536)
EXIT
```

## TODO

- [ ] Convert to a more realistic packet format using Bach's branch
- [ ] Utilize Bach's unittest structure
- [ ] Make a status report transaction code
- [ ] Implement an echo command
30 changes: 30 additions & 0 deletions ADCS/abstract_interface.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""Holds the abstract class ConnectionProtocol"""


class ConnectionProtocol:
"""TODO: We should put the packet serialization and so on be the
responsibility of the connection protocol, since it will be
changing as we get further into the project."""

def send(self, data: bytes):
"""
This method will send data. Force the input to be bytes so that the
caller must format the data to be bytes already.
"""
raise NotImplementedError

def recv(self, timeout: float) -> bytes:
"""
This method will recieve data. It should return it.
timeout specifies in seconds how long the recv function
should block before raising an exception
Fundamental assumption is that this will work with bytes
"""
raise NotImplementedError

def connect(self):
"""
This method will start the connection
"""
raise NotImplementedError
68 changes: 68 additions & 0 deletions ADCS/adcs_components.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""
Holds classes for each type of measurements on the ADCS
"""
# pylint: disable=too-few-public-methods
# pylint: disable=useless-parent-delegation


class ThreeDimensionalMeasurements:
"""
Abstract class that deals with having multiple three
dimensional stored values
"""

@staticmethod
def new():
"""Factory method test"""
return ThreeDimensionalMeasurements(0, 0, 0)

def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z

def __repr__(self):
"""This function is for debugging by printing the object"""
return f"({self.x!r}, {self.y!r}, {self.z!r})"


class AngularMeasurement(ThreeDimensionalMeasurements):
"""Stores the angle in 3D"""

def __init__(self, x: float, y: float, z: float):
super().__init__(x, y, z)


class AngularSpeed(ThreeDimensionalMeasurements):
"""Stores the angular speed in 3D"""

def __init__(self, x: float, y: float, z: float):
super().__init__(x, y, z)


class MagneticMeasurements(ThreeDimensionalMeasurements):
"""Stores the magnetic measurements in 3D"""

def __init__(self, x, y, z):
super().__init__(x, y, z)


class MagneticCurrent(ThreeDimensionalMeasurements):
"""Stores the current in 3D"""

def __init__(self, x, y, z):
super().__init__(x, y, z)


class WheelSpeed(ThreeDimensionalMeasurements):
"""Stores the wheels speeds in 3D"""

def __init__(self, x, y, z):
super().__init__(x, y, z)


class SystemClock:
"""Stores the time"""

def __init__(self, time: float):
self.time = time
163 changes: 163 additions & 0 deletions ADCS/adcs_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
"""
This will be the main file for the simulated ADCS subsystem.
"""

import sys
import threading
import time
from queue import Empty # Import is needed from use of queue timeouts

from tcp_server import TcpListener
from adcs_subsystem import ADCSSubsystem

SLEEP_TIME = 5 # in seconds
EXIT_FLAG = b"EXIT"
DEFAULT_HOST = "127.0.0.1"
DEFAULT_PORT = 42123


def command_line_handler(argv) -> tuple[int, str]:
"""
Control flow for what to return depending on the commandline arg.
**Change here if you need to change the port and address values**
Returns:
(PORT, HOST)
"""

ret_port = int(argv[1]) if len(argv) > 1 else DEFAULT_PORT
ret_host = argv[2] if len(argv) > 2 else DEFAULT_HOST

return ret_port, ret_host


def command_parser(data: str):
"""Splits the command from its parameters using colons (:)"""
parsed = data.rstrip().split(sep=":")
return parsed


def listen(adcs: ADCSSubsystem, stop: threading.Event):
"""
function that will continuously listen for client messages
when a message is received it will be put onto the rx buffer
"""
while not stop.is_set():
try:
received = adcs.read_bytes(timeout=SLEEP_TIME)
print(received)
if not received or received.rstrip() == EXIT_FLAG:
stop.set() # begin closing server
return
adcs.rx_buffer.put(received)
except TimeoutError:
continue


def handle_input(adcs: ADCSSubsystem, stop: threading.Event):
"""
function that will continuously check the rx buffer for any
data if there is it will take the data from the buffer and
handle it putting any output onto the tx buffer for transmitting
"""
while not stop.is_set():
try:
received = adcs.rx_buffer.get(timeout=SLEEP_TIME)

decoded = received.decode("utf-8")
data_list = command_parser(decoded)

handle_output_thread = threading.Thread(
target=handle_output, args=(adcs, data_list), daemon=True)
handle_output_thread.start()
except Empty:
continue


def handle_output(adcs: ADCSSubsystem, data_list: list):
"""
function that will only run when a handle_output_thread is spawned
from the input handler. Once spawned, it will run its command and
push to tx_buffer.
This prevents tasks with short run times from being blocked by
longer tasks
"""
transmit = run_command(data_list, adcs)

if transmit is not None:
encoded = str(transmit).encode("utf-8")
adcs.tx_buffer.put(encoded)


def send(adcs: ADCSSubsystem, stop: threading.Event):
"""
function that will continuously check the tx buffer for
any data if there is it will send it back to the server.
It is assumed that all data in the tx buffer are bytes object.
"""
while not stop.is_set():
try:
transmitting = adcs.tx_buffer.get(timeout=SLEEP_TIME)
adcs.send_bytes(transmitting)
except Empty:
continue


def run_command(data: list, adcs: ADCSSubsystem):
"""
Given a list of stings this will run an adcs command with
some given arguments. Returns anything that the adcs command
wishes to send back to the OBC
"""
command = data[0]
try:
func = adcs.commands[command][0]
num_params = adcs.commands[command][1]

if len(data) != num_params + 1:
return f"INVALID COMMAND: command {command} requires {num_params} arguments\n"

if len(data) == 1: # No args
return func()

arg_list = data[1:]
return func(*arg_list)
except KeyError:
return "INVALID COMMAND: run \"HELP\" for list of commands\n"


if __name__ == "__main__":
port, host = command_line_handler(sys.argv)
addr = (host, port)

print(f"Starting ADCS subsystem on port {port}")

server = TcpListener(port, host)
server.set_debug(True)

adcs_subsystem = ADCSSubsystem(server)
adcs_subsystem.init_link() # opens the server

stop_event = threading.Event() # used to stop child threads

rx_thread = threading.Thread(target=listen, args=(
adcs_subsystem, stop_event), daemon=True)
tx_thread = threading.Thread(target=send, args=(
adcs_subsystem, stop_event), daemon=True)
handle_input_thread = threading.Thread(target=handle_input, args=(
adcs_subsystem, stop_event), daemon=True)

rx_thread.start()
tx_thread.start()
handle_input_thread.start()

while not stop_event.is_set():
try:
time.sleep(SLEEP_TIME)
except KeyboardInterrupt:
stop_event.set()

handle_input_thread.join()
tx_thread.join()
rx_thread.join()
15 changes: 15 additions & 0 deletions ADCS/adcs_states.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""
Stores the enum states of the ADCS
"""

from enum import Enum
from enum import auto


class ADCSState(Enum):
"""
Enum to represent the states of the ADCS
"""

OFF = auto()
WORKING = auto()
Loading

0 comments on commit ecc5fd4

Please sign in to comment.