Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Rotary Encoder #123

Closed
wants to merge 39 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
a7e7dff
added csvs, updated metadata and readme
wlsanderson Dec 3, 2024
e74dd29
Add landing coordinates in metadata.json
harshil21 Dec 12, 2024
bc5d42c
Add invalid_fields to interest_launch.csv
harshil21 Dec 12, 2024
b597752
Update readme
harshil21 Dec 12, 2024
d8b53ad
Merge pull request #113 from NCSU-High-Powered-Rocketry-Club/modified…
harshil21 Dec 12, 2024
3fbe323
Change to LandedState based on accel spike
harshil21 Jan 1, 2025
ef0abab
Fix LandedState timestamps for genesis launch 1
harshil21 Jan 1, 2025
72c4da2
Adjust integration test
harshil21 Jan 1, 2025
fec4f51
Fix tests
harshil21 Jan 1, 2025
32e48ef
Refactor IMU: BaseIMU -> [MockIMU, IMU]
unclesam79 Jan 4, 2025
d68c036
Add script to test servo and encoder position
harshil21 Jan 6, 2025
fc242f0
Use the actual pins for the encoder in the script
harshil21 Jan 6, 2025
7124c26
[pre-commit.ci] pre-commit autoupdate
pre-commit-ci[bot] Jan 6, 2025
c17dfda
Add another test with the display running too
harshil21 Jan 6, 2025
bf34d9c
reduced point skipping and changed pins
DirtyPi09 Jan 6, 2025
c8340d9
Merge pull request #119 from NCSU-High-Powered-Rocketry-Club/fix-gene…
harshil21 Jan 7, 2025
bdf6d75
remove coverage report
unclesam79 Jan 7, 2025
4f98307
remove setter from is_running property
unclesam79 Jan 7, 2025
69d6d96
use is_running property instead of direct access
unclesam79 Jan 7, 2025
ffa20fe
use is_running property instead of direct access
unclesam79 Jan 8, 2025
978a4d2
fix formatting (line too long)
unclesam79 Jan 8, 2025
7740bf0
Servo initial values and servo constants
DirtyPi09 Jan 8, 2025
3f1ea27
Revert strictness check
harshil21 Jan 8, 2025
faaaccf
Merge pull request #122 from unclesam79/baseimu-refactor
harshil21 Jan 8, 2025
35b69e7
Merge main and fix conflicts
harshil21 Jan 8, 2025
fda236c
Merge pull request #121 from NCSU-High-Powered-Rocketry-Club/use-uv
wlsanderson Jan 8, 2025
ebb7ac7
fix a test
harshil21 Jan 8, 2025
685047a
Fix windows integration test
harshil21 Jan 8, 2025
d54a995
Merge pull request #99 from NCSU-High-Powered-Rocketry-Club/adjust-qu…
harshil21 Jan 8, 2025
df6eca3
Merge main and fix conflicts
harshil21 Jan 8, 2025
8eadd18
Missed changing an import during merge
harshil21 Jan 8, 2025
54d7f9e
Merge pull request #117 from NCSU-High-Powered-Rocketry-Club/use-py313
harshil21 Jan 8, 2025
4273ff0
Merge branch 'main' into pre-commit-ci-update-config
wlsanderson Jan 8, 2025
e9223e9
Merge pull request #124 from NCSU-High-Powered-Rocketry-Club/pre-comm…
wlsanderson Jan 8, 2025
5a23353
Review WIP: Integration test for accel spike
harshil21 Jan 8, 2025
f4d41c0
Merge main and fix conflicts
harshil21 Jan 8, 2025
2f80620
Add accel spike to integration test
harshil21 Jan 8, 2025
5af32dd
Merge pull request #118 from NCSU-High-Powered-Rocketry-Club/accel-la…
wlsanderson Jan 8, 2025
a8d9ab7
Merge branch 'main' into rotary-encoder
harshil21 Jan 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,6 @@ build/
AirbrakesV2.egg-info/
.venv/

test_logs/
test_logs/

.coverage
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ ci:

repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: 'v0.8.1'
rev: 'v0.8.6'
hooks:
- id: ruff # Runs the Ruff linter
name: ruff linter
Expand Down
27 changes: 20 additions & 7 deletions airbrakes/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,24 @@ class ServoExtension(Enum):
maximum rotation. We obtained these values through guess and check.
"""

MIN_EXTENSION = -0.4
MAX_EXTENSION = 0.7
MIN_NO_BUZZ = -0.23
MAX_NO_BUZZ = 0.58
MIN_EXTENSION = -0.55
MAX_EXTENSION = 0.45
MIN_NO_BUZZ = -0.5
MAX_NO_BUZZ = 0.37


# -------------------------------------------------------
# Encoder Configuration
# -------------------------------------------------------

ENCODER_RESOLUTION = 20
"""The points per revolution of the encoder"""

ENCODER_PIN_A = 23
"""The GPIO pin that the encoder's A pin is connected to."""

ENCODER_PIN_B = 24
"""The GPIO pin that the encoder's B pin is connected to."""


# -------------------------------------------------------
Expand Down Expand Up @@ -158,9 +172,8 @@ class DisplayEndingType(StrEnum):

GROUND_ALTITUDE_METERS = 10.0
"""The altitude in meters that the rocket must be under before we consider it to have landed."""
LANDED_SPEED_METERS_PER_SECOND = 5.0
"""The speed in meters per second that the rocket must be under before we consider it to have
landed."""
LANDED_ACCELERATION_METERS_PER_SECOND_SQUARED = 50.0
"""The acceleration in m/s^2 that the rocket must be above before we consider it to have landed."""

# -------------------------------------------------------
# Apogee Prediction Configuration
Expand Down
5 changes: 5 additions & 0 deletions airbrakes/data_handling/data_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ def max_vertical_velocity(self) -> float:
"""The maximum vertical velocity the rocket has attained during the flight, in m/s."""
return float(self._max_vertical_velocity)

@property
def average_vertical_acceleration(self) -> float:
"""The average vertical acceleration of the rocket in m/s^2."""
return float(np.mean(self._rotated_accelerations))

@property
def current_timestamp(self) -> int:
"""The timestamp of the last data packet in nanoseconds."""
Expand Down
93 changes: 93 additions & 0 deletions airbrakes/hardware/base_imu.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"""Module defining the base class (BaseIMU) for interacting with
the IMU (Inertial measurement unit) on the rocket."""

import collections
import contextlib
import sys

from airbrakes.constants import IMU_TIMEOUT_SECONDS, MAX_FETCHED_PACKETS, STOP_SIGNAL
from airbrakes.data_handling.imu_data_packet import (
IMUDataPacket,
)

# If we are not on windows, we can use the faster_fifo library to speed up the queue operations
if sys.platform != "win32":
from faster_fifo import Empty, Queue
else:
from multiprocessing import Queue
from multiprocessing.queues import Empty

from multiprocessing import Process, TimeoutError, Value


class BaseIMU:
"""
Base class for the IMU and MockIMU classes.
"""

__slots__ = (
"_data_fetch_process",
"_data_queue",
"_running",
)

def __init__(self, data_fetch_process: Process, data_queue: Queue) -> None:
"""
Initialises object using arguments passed by the constructors of the subclasses.
"""
self._data_fetch_process = data_fetch_process
self._data_queue = data_queue
# Makes a boolean value that is shared between processes
self._running = Value("b", False)

def stop(self) -> None:
"""
Stops the process separate from the main process for fetching data from the IMU.
"""
self._running.value = False
# Fetch all packets which are not yet fetched and discard them, so main() does not get
# stuck (i.e. deadlocks) waiting for the process to finish. A more technical explanation:
# Case 1: .put() is blocking and if the queue is full, it keeps waiting for the queue to
# be empty, and thus the process never .joins().
with contextlib.suppress(TimeoutError):
self._data_fetch_process.join(timeout=IMU_TIMEOUT_SECONDS)

def start(self) -> None:
"""
Starts the process separate from the main process for fetching data from the IMU.
"""
self._running.value = True
self._data_fetch_process.start()

def get_imu_data_packet(self) -> IMUDataPacket | None:
"""
Gets the last available data packet from the IMU.
:return: an IMUDataPacket object containing the latest data from the IMU. If a value is not
available, it will be None.
"""
return self._data_queue.get(timeout=IMU_TIMEOUT_SECONDS)

def get_imu_data_packets(self) -> collections.deque[IMUDataPacket]:
"""
Returns all available data packets from the IMU.
:return: A deque containing the latest data packets from the IMU.
"""
# We use a deque because it's faster than a list for popping from the left
try:
packets = self._data_queue.get_many(
block=True, max_messages_to_get=MAX_FETCHED_PACKETS, timeout=IMU_TIMEOUT_SECONDS
)
except Empty: # If the queue is empty (i.e. timeout hit), don't bother waiting.
return collections.deque()
else:
if STOP_SIGNAL in packets:
return collections.deque()
return collections.deque(packets)

@property
def is_running(self) -> bool:
"""
Returns whether the process fetching data from the IMU is running.
:return: True if the process is running, False otherwise
"""
return self._running.value
78 changes: 8 additions & 70 deletions airbrakes/hardware/imu.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""Module for interacting with the IMU (Inertial measurement unit) on the rocket."""

import collections
import contextlib
import multiprocessing
import sys
Expand All @@ -13,27 +12,25 @@

# If we are not on windows, we can use the faster_fifo library to speed up the queue operations
if sys.platform != "win32":
from faster_fifo import Empty, Queue
from faster_fifo import Queue
else:
from multiprocessing.queues import Empty
pass

from airbrakes.constants import (
BUFFER_SIZE_IN_BYTES,
ESTIMATED_DESCRIPTOR_SET,
IMU_TIMEOUT_SECONDS,
MAX_FETCHED_PACKETS,
MAX_QUEUE_SIZE,
RAW_DESCRIPTOR_SET,
STOP_SIGNAL,
)
from airbrakes.data_handling.imu_data_packet import (
EstimatedDataPacket,
IMUDataPacket,
RawDataPacket,
)
from airbrakes.hardware.base_imu import BaseIMU


class IMU:
class IMU(BaseIMU):
"""
Represents the IMU on the rocket. It's used to get the current acceleration of the rocket.
This is used to interact with the data collected by the Parker-LORD 3DMCX5-AR.
Expand All @@ -47,12 +44,6 @@ class IMU:
Here is the software for configuring the IMU: https://www.microstrain.com/software/sensorconnect
"""

__slots__ = (
"_data_fetch_process",
"_data_queue",
"_running",
)

def __init__(self, port: str) -> None:
"""
Initializes the object that interacts with the physical IMU connected to the pi.
Expand All @@ -62,67 +53,14 @@ def __init__(self, port: str) -> None:
# to prevent memory issues. Realistically, the queue size never exceeds 50 packets when
# it's being logged.
# We will never run the actual IMU on Windows, so we can use the faster_fifo library always:
self._data_queue: Queue[IMUDataPacket] = Queue(
_data_queue: Queue[IMUDataPacket] = Queue(
maxsize=MAX_QUEUE_SIZE, max_size_bytes=BUFFER_SIZE_IN_BYTES
)
# Makes a boolean value that is shared between processes
self._running = multiprocessing.Value("b", False)
# Starts the process that fetches data from the IMU
self._data_fetch_process = multiprocessing.Process(
data_fetch_process = multiprocessing.Process(
target=self._query_imu_for_data_packets, args=(port,), name="IMU Process"
)

@property
def is_running(self) -> bool:
"""
Returns whether the process fetching data from the IMU is running.
:return: True if the process is running, False otherwise
"""
return self._running.value

def start(self) -> None:
"""
Starts the process separate from the main process for fetching data from the IMU.
"""
self._running.value = True
self._data_fetch_process.start()

def stop(self) -> None:
"""
Stops the process separate from the main process for fetching data from the IMU.
"""
self._running.value = False
# Fetch all packets which are not yet fetched and discard them, so main() does not get
# stuck (i.e. deadlocks) waiting for the process to finish. A more technical explanation:
# Case 1: .put() is blocking and if the queue is full, it keeps waiting for the queue to
# be empty, and thus the process never .joins().
with contextlib.suppress(multiprocessing.TimeoutError):
self._data_fetch_process.join(timeout=IMU_TIMEOUT_SECONDS)

def get_imu_data_packet(self) -> IMUDataPacket | None:
"""
Gets the last available data packet from the IMU.
:return: an IMUDataPacket object containing the latest data from the IMU. If a value is not
available, it will be None.
"""
return self._data_queue.get(timeout=IMU_TIMEOUT_SECONDS)

def get_imu_data_packets(self) -> collections.deque[IMUDataPacket]:
"""
Returns all available data packets from the IMU.
:return: A deque containing the latest data packets from the IMU.
"""
# We use a deque because it's faster than a list for popping from the left
try:
packets = self._data_queue.get_many(
block=True, max_messages_to_get=MAX_FETCHED_PACKETS, timeout=IMU_TIMEOUT_SECONDS
)
except Empty: # If the queue is empty (i.e. timeout hit), don't bother waiting.
return collections.deque()
else:
if STOP_SIGNAL in packets:
return collections.deque()
return collections.deque(packets)
super().__init__(data_fetch_process, _data_queue)

# ------------------------ ALL METHODS BELOW RUN IN A SEPARATE PROCESS -------------------------
@staticmethod
Expand Down Expand Up @@ -201,7 +139,7 @@ def _fetch_data_loop(self, port: str) -> None:
connection = mscl.Connection.Serial(port)
node = mscl.InertialNode(connection)

while self._running.value:
while self.is_running:
# Retrieve data packets from the IMU.
packets: mscl.MipDataPackets = node.getDataPackets(timeout=10)

Expand Down
2 changes: 1 addition & 1 deletion airbrakes/hardware/servo.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def __init__(self, gpio_pin_number: int, pin_factory=None) -> None:
warnings.filterwarnings(message="To reduce servo jitter", action="ignore")
gpiozero.Device.pin_factory = pin_factory

self.servo = gpiozero.Servo(gpio_pin_number)
self.servo = gpiozero.Servo(gpio_pin_number,initial_value=ServoExtension.MIN_NO_BUZZ.value)

# We have to use threading to avoid blocking the main thread because our extension methods
# need to run at a specific time. Yes this is bad practice but we had a mechanical issue and
Expand Down
17 changes: 8 additions & 9 deletions airbrakes/mock/mock_imu.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@
IMUDataPacket,
RawDataPacket,
)
from airbrakes.hardware.imu import IMU
from airbrakes.hardware.base_imu import BaseIMU


class MockIMU(IMU):
class MockIMU(BaseIMU):
"""
A mock implementation of the IMU for testing purposes. It doesn't interact with any hardware
and returns data read from a previous log file.
Expand Down Expand Up @@ -73,24 +73,23 @@ def __init__(
if sys.platform == "win32":
# On Windows, we use a multiprocessing.Queue because the faster_fifo.Queue is not
# available on Windows
self._data_queue = multiprocessing.Queue(
data_queue = multiprocessing.Queue(
maxsize=MAX_QUEUE_SIZE if real_time_simulation else MAX_FETCHED_PACKETS
)

self._data_queue.get_many = partial(get_all_from_queue, self._data_queue)
data_queue.get_many = partial(get_all_from_queue, data_queue)
else:
self._data_queue: Queue[IMUDataPacket] = Queue(
data_queue: Queue[IMUDataPacket] = Queue(
maxsize=MAX_QUEUE_SIZE if real_time_simulation else MAX_FETCHED_PACKETS
)
# Starts the process that fetches data from the log file
self._data_fetch_process = multiprocessing.Process(
data_fetch_process = multiprocessing.Process(
target=self._fetch_data_loop,
args=(self._log_file_path, real_time_simulation, start_after_log_buffer),
name="Mock IMU Process",
)

# Makes a boolean value that is shared between processes
self._running = multiprocessing.Value("b", False)
super().__init__(data_fetch_process, data_queue)

@staticmethod
def _convert_invalid_fields(value) -> list:
Expand Down Expand Up @@ -159,7 +158,7 @@ def _read_file(
row_dict = {k: v for k, v in row._asdict().items() if pd.notna(v)}

# Check if the process should stop:
if not self._running.value:
if not self.is_running:
break

# If the row has the scaledAccelX field, it is a raw data packet, otherwise it is an
Expand Down
6 changes: 3 additions & 3 deletions airbrakes/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from airbrakes.constants import (
GROUND_ALTITUDE_METERS,
LANDED_SPEED_METERS_PER_SECOND,
LANDED_ACCELERATION_METERS_PER_SECOND_SQUARED,
MAX_FREE_FALL_SECONDS,
MAX_VELOCITY_THRESHOLD,
TAKEOFF_HEIGHT_METERS,
Expand Down Expand Up @@ -180,10 +180,10 @@ def update(self):

data = self.context.data_processor

# If our altitude and speed are around 0, we have landed
# If our altitude is around 0, and we have an acceleration spike, we have landed
if (
data.current_altitude <= GROUND_ALTITUDE_METERS
and abs(data.vertical_velocity) <= LANDED_SPEED_METERS_PER_SECOND
and data.average_vertical_acceleration >= LANDED_ACCELERATION_METERS_PER_SECOND_SQUARED
):
self.next_state()

Expand Down
9 changes: 5 additions & 4 deletions launch_data/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ This folder contains the launch data of actual previous flights. These are used

## Data

1. `interest_launch.csv`: This is the data from the Interest Launch, on September 28th, 2024. The gravity fields were seperately added on later. The
1. `interest_launch.csv`: This is the data from the Interest Launch, on September 28th, 2024. The gravity fields were seperately added on later, as well as missing quaternion data, and the "invalid_fields". The
"G" state is generated packets. Since we didn't get touchdown data in landed state, we generated a few packets to simulate the touchdown.
See #59 and #78 for more details.
2. `purple_launch.csv`: This is the data from the Purple Nurple launch, on 16th December, 2023. The quaternions, and angular rates fields were seperately
added on later. We do have touchdown data for this launch, however since our rotation data is not accurate, we cannot get a good velocity estimate, and thus the landed state does not trigger. There is no workaround for this, other than using acceleration data to switch to landed state.
2. `purple_launch.csv`: This is the data from the Purple Nurple launch, on 16th December, 2023. The quaternions, quaternion uncertainities, and angular rates fields were seperately
added on later. We do have touchdown data for this launch, however since our rotation data is not accurate, we cannot get a good velocity estimate, and thus the landed state does not trigger. There is no workaround for this, other than using acceleration data to switch to landed state. The quaternion uncertainties are set to a low value for a majority of the flight, except whenever the altitude is between 500 - 600 meters. During this range, we set the quaternion uncertainties to a high value, to test our code.
3. `genesis_launch_1.csv`: This was our first attempt of a control launch with the Genesis subscale rocket. We tried to have it extend its airbrakes for most of coast and then retract them, but later analysis proved that airbrakes didn't deploy during coast. Additionally, the LandedState was incorrectly detected, so for convenience ~100 mb of useless data has been cropped out of this file.
4. `genesis_launch_2.csv`: This was our second attempt of a control launch with Genesis. For this one we told the airbrakes to deploy once at around the start of CoastState and did not tell them to retract at all. When we recovered the rocket, the fins were extended, by analysis of launch data shows that the airbrakes didn't deploy in CoastState, and most likely deployed sometime either in FreeFall or once the rocket hit the ground. LandedState was mostly correctly detected.
The timestamps of the LandedState packets were synced with the timestamps of the last packet in the file. See #91 for more details.
4. `genesis_launch_2.csv`: This was our second attempt of a control launch with Genesis. For this one we told the airbrakes to deploy once at around the start of CoastState and did not tell them to retract at all. When we recovered the rocket, the fins were extended, by analysis of launch data shows that the airbrakes didn't deploy in CoastState, and most likely deployed sometime either in FreeFall or once the rocket hit the ground. LandedState was mostly correctly detected. See #91 for more details.


## Metadata
Expand Down
Loading
Loading