Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
560cab1
Updated clang format
hrszpuk Sep 29, 2026
5bc14b7
Updated README with better instructions
hrszpuk Sep 29, 2026
627fe96
Added test files for `src` and `libs`
hrszpuk Sep 29, 2026
8dd6785
Updated the finite state machine library
hrszpuk Sep 29, 2026
edf3410
Updated CAN Bus library (this needs cleaning up)
hrszpuk Sep 29, 2026
f979db9
Added testing for master
hrszpuk Sep 29, 2026
31a3ed0
Added a basic bang bang controller implementation - waiting on thermals
hrszpuk Sep 29, 2026
5e2064c
Basic EMA filter added to utilities
hrszpuk Sep 29, 2026
6d2d37c
Math & timer helpers added
hrszpuk Sep 29, 2026
d81ab59
Thermistor code added
hrszpuk Sep 29, 2026
036d167
Updated src/CMakeLists
hrszpuk Sep 29, 2026
38ce0db
Setting up EPS
hrszpuk Sep 29, 2026
fe57f4f
EPS comms.c/.h (renamed from comm; typo?)
hrszpuk Sep 29, 2026
09e3315
Added EPS control.c/.h imple
hrszpuk Sep 29, 2026
f3fd386
Added EPS's HAL with ESP32 implementation
hrszpuk Sep 29, 2026
ad8b34c
Setting up instrumentation
hrszpuk Sep 29, 2026
df73d02
Added contro.c./.h for instrumentaion
hrszpuk Sep 29, 2026
5fe5c8c
Updated comms for instrumentation
hrszpuk Sep 29, 2026
ced0912
instrumentation pins
hrszpuk Sep 29, 2026
585633f
HAL implemented for instrumentation (ESP32)
hrszpuk Sep 29, 2026
92c2e41
base HAL file added for thermals (awaiting on implementation)
hrszpuk Sep 29, 2026
cb85669
Set up Photonics HAL for basic implementation
hrszpuk Sep 29, 2026
0b11a82
added photonics pins
hrszpuk Sep 29, 2026
f62d323
Finish photonics demo code
hrszpuk Sep 29, 2026
fae6175
Updated photonics control.c (added implementation
hrszpuk Sep 29, 2026
f3c0eb6
Implemented mechanisms (needs updating to bridge with thermals)
hrszpuk Sep 29, 2026
a90f676
Added master comms
hrszpuk Sep 29, 2026
84f8631
Flight manager
hrszpuk Sep 29, 2026
866eb79
OBDH control
hrszpuk Sep 29, 2026
b2a94dc
setup.c
hrszpuk Sep 29, 2026
1813f6b
Ethernet drivers
hrszpuk Sep 29, 2026
d27ba86
ESP32 drivers
hrszpuk Sep 29, 2026
3f64f04
finished HAL set up
hrszpuk Sep 29, 2026
2ee8f9e
TTC code
hrszpuk Sep 29, 2026
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
16 changes: 16 additions & 0 deletions .clang-format
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
Language: Cpp
BasedOnStyle: WebKit

IndentWidth: 4

PointerAlignment: Right # This is my preferred setting. Most code examples from books, etc use this too.
DerivePointerAlignment: false

SortIncludes: false
IndentCaseLabels: true # Because I'm sane

AlignTrailingComments: true
BreakBeforeBinaryOperators: NonAssignment # also because I'm sane

BreakBeforeBraces: Attach
153 changes: 152 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,153 @@
# Project HELIA: Flight Software
This repository contains the flight software for Project HELIA: source code for all MCUs, all units tests, all shared libraries, and doxygen for documentation generation

This repository contains the flight software for Project HELIA: source code for all MCUs, all units tests, all shared libraries, and doxygen for documentation generation.

## Code Organisation

Since the repository has gotten pretty big, I'm going to make some notes on where everything is so other people can easily navigate around.
The computer system is 6 ESP32 S3s with a CAN Bus for central communication, so although this is a single codebase it actually feels more like 6 mini code bases in one.

### `libs/`

The `libs/` folder is for shared behaviour. Basically anything that all the ESP32s need to do is placed in here.
At the start this was mostly the `can` and `fsm` packages, and now slowly as I'm noticing code duplication I'm abstracting the behaviour and putting it in a shared library within `libs`.

In the previous codebase, the shared behaviour folder got pretty big and messy, mostly because I started putting actual implementation in there rather than just abstract libraries that each ESP32 could use.
There should be a pretty hard line between what should be in the `src` folder and `libs` folder:

- `src` is for implementation code, anything that will actually run on the ESP32s whether that is logic or hardware specific code
- `libs` is for abstract, shared libraries that code within `src` uses to improve the implementation and reduce code duplication across the codebase

A general rule of thumb, if you're starting FreeRTOS tasks inside `libs` then you're going too far.

Inside libs there are a number of small libraries that are dedicate to specific hardware or groups of functions/structs that are related in some way:

- `can` handles everything CAN Bus related
- `fsm` handles everything finite state machine related (INIT -> SAFE -> ACTIVE -> SLEEP loop)
- `utilities` has a bunch of general utility functions, if you want to introduce a new library function without starting an entirely new library, then utilities is usually a safe bet

### `src/`

This is where the actual hardware specific logic and control logic lives. Usually this is split into `control` which is the primary task of the subsystem/esp32, usually data acquisition, some control flow, or whatever. `comms` is entirely just CAN communication, mostly the heartbeat, responding to commands, and more. These are pretty clearly shown with the `control.c/.h` and `comms.c/.h` convention used throughout the codebase.

There is also `setup.c` this is the main entry point for all ESP32s, think of it as a `main.c` essentially. It contains all the set up code needed, including a `app_main` function and often also the main FreeRTOS tasks for communication and control (which use functions from the relevant headers).

You will notice most of the control, set up, communication code is abract and doesn't feature much hardware interfacing. That's because all hardware specific logic lives within the [Hardware Abstraction Layer (HAL)](https://en.wikipedia.org/wiki/Hardware_abstraction), so ESP32-specific driver code is kept separate from other logic. This is done to make testing easier, as I can run tests locally or on a CI/CD without need ESP32s, but also keeps all the messy hardware specific logic in its own place which I find nicer to work with.

There are 5 folders within `src/`:

- `Master`: for OBDH/TTC; sd card logging, sending data over ethernet, main flight manager, etc
- `EPS`: for the Electric Power Supply (EPS) code; this is mostly just a current and voltage sensor, all the real power shit is buck converters on the main PCB (not a software problem yippeee!)
- `Instrumentation`: this is a bunch of housekeeping sensors, pretty chill, just DAQ
- `Mechanisms`: houses the iris and fluidics code, again pretty easy as it's just motoring
- `Photonics`: DAQ for photodiodes
- `thermals`: a bit more complex, will be a bang bang controller or pid loop controlling the temperature regulation of the experiment

### `tests/`

This is where all the test files live. Split into `libs/` for testing library functions and `src/` for testing source code functions.
I'm using `GoogleTest`, a C++ framework for writing the tests so that's why you might see:

```c
#ifdef __cplusplus
extern "C" {
#endif

// ... code or smth

#ifdef __cplusplus
}
#endif
```

This is especially common in the `libs` code header code.

### Config files

We use CMake for building and platformio.ini for compiling each ESP32 easily. There's also script files like `build.sh` which are handy for quickly building everything. For that reason I recommend using a unix supported operating system such as MacOS, Linux, or WSL2 (if on Windows) as everything will run the same as on my computer.

## Contributing

If you're interested in contributing to the project, I'm going to write some rough guidelines and ideas for getting started.

### How to start

I'd recommend use MacOS, Linux, or Windows Subsystem for Linux (WSL2) for development. All scripts and configs have been testing on those and it should be easier to start rather than trying to get everything to work on Windows.

You will need to install: `cmake`, `platfromio`, `googletest` (`gtest`), `git`, `doxygen`, and `python` (dependency of platformio).
You can run the `setup.sh` that should install all these packages and sets everything up for you. This is probably the best place to start.

I'd recommend using the `platformio` extension and `vscode` for development if you're new, or any set up you're familiar with if you're more experienced.

#### Git Flow

I've used Git Flow methodology in the past, and although it's a ballache if you're managing a repository on your own, when other people start getting interested in contritbing that's usually when I start using it.
The idea is pretty simple: `main` branch for production ready code, `dev` branch for still in progress code, and individual contributors create branches off of `dev` to write and test code before merging into `dev`. Then merges into `main` are reviewed by myself or another collaborator on the project.

If you're getting an error when pushing to `main` that the branch is protected and you can't push, that is intentional. You should stash your work, open a new branch from `dev`, commit, and pull request into `dev` rather than `main`.

### Where to start

It's hard to know exactly where to start, especially when you're dropped into the middle of a codebase written entirely by someone else.
I'd recommend checking out the issues tab, I will start populating it with work that needs being done.
Otherwise, read through the codebase and decide to add new functionality, add tests for something, or document stuff.

#### Documentation

The code should be fairly self documenting, I'd like to avoid code that is overly complex for no reason, but sometimes it is neccessary or just happens. To avoid this I've added Doxygen to the project (and will be setting up a doxygen web build soon). You will see these through out the codebase as comments, but for a brief summary:

```c
/**
* @file some_library.h
* @brief Add a brief description of what's in the library
*/
#ifndef HELIA_SOME_LIBRARY_H
#define HELIA_SOME_LIBRARY_H

#include <stdbool.h>

#ifdef __cplusplus
extern "C" {
#endif

/** @brief Brief description of the point of this struct */
typedef struct {
float value; /**< This value is used for.. Or is in .. format */
bool ok; /**< True if the data is fresh/valid, otherwise disregard */
} some_data;

/**
* @brief Description of the function's behaviour,
* @param f What is this param and how is it relevant
* @param alpha etc etc
*
* @return this function is void, but this would discuss what the return is
*/
void some_init(some_data *f, float alpha);

/**
* @brief Description of the function's behaviour,
* @param f What is this param and how is it relevant
* @param alpha etc etc
*
* @return blah blah blah
*/
float some_func(some_data *f, float sample);

#ifdef __cplusplus
}
#endif

#endif /* HELIA_SOME_LIBRARY_H */

```

The example above should give you an idea of how most of the code is documented now.
Anything else can be found in the [doxygen documentation](https://www.doxygen.nl/manual/docblocks.html) (just skim the code examples tbh).

### Final notes

This repository is for Project HELIA, a BEXUS flight experiment launching in October (2026). After the launch campaign I may not respond as quickly to enquiries or pull requests. You should be able to ping me using @hrszpuk in your issues/pull requests.
However, even then this codebase won't be a top priority for me, as I'll have moved onto other projects.

Happy coding!
2 changes: 1 addition & 1 deletion build.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# Build + test everything, flash every board, then monitor Master.
# Each board's port is set in platformio.ini, so each firmware goes to the right board.
# Each board's port is set in platformio.ini, so each firmware goes to the right board
set -euo pipefail
cd "$(dirname "$0")"
export PATH="$HOME/.local/bin:$HOME/.platformio/penv/bin:$PATH"
Expand Down
54 changes: 27 additions & 27 deletions libs/can/can.h
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
/**
* @file can.h
* @brief Driver-independent CAN helpers: frame type, byte packing, and
* encode/decode for messages every node shares.
* encode/decode for messages every node shares
*
* Pure functions only. Sending and receiving frames is done by each node's
* comms code through its HAL; this library never touches the bus.
* NOTE: Sending and receiving frames is done by each node's
* comms code through its HAL; this library never touches the bus just helpers
*/
#ifndef HELIA_CAN_H
#define HELIA_CAN_H
Expand All @@ -18,16 +18,16 @@
extern "C" {
#endif

/** @brief One classic CAN frame, independent of ESP-IDF's twai_message_t. */
/** @brief Abstract CAN frame without any driver shit (Standard CAN; not FD)*/
typedef struct {
uint32_t id; /**< 11-bit ID, see can_make_id(). */
uint8_t dlc; /**< Payload length, 0-8. */
uint32_t id; /**< 11-bit ID, see can_make_id() */
uint8_t dlc; /**< Payload length, 0-8 */
uint8_t data[CAN_MAX_DLC];
} can_frame_t;

/**
* @brief Start a frame: sets the ID and length, zeroes the payload.
* @param dlc Payload length; values above 8 are clamped to 8.
* @brief Start a frame: sets the ID and length, zeroes the payload
* @param dlc Payload length; values above 8 are clamped to 8
*/
void can_frame_init(can_frame_t *f, can_priority_t prio, can_source_t src,
uint8_t type, uint8_t dlc);
Expand All @@ -42,46 +42,46 @@ int16_t can_get_i16(const uint8_t *buf);
uint32_t can_get_u32(const uint8_t *buf);
/**@}*/

/** @brief Heartbeat contents. Payload: [state u8][uptime_s u32] = 5 bytes. */
/** @brief Heartbeat contents, Payload: [state u8][uptime_s u32] = 5 bytes. */
typedef struct {
can_source_t source; /**< Which node sent it (from the ID). */
uint8_t state; /**< Sender's fsm_state_t, as a byte. */
uint32_t uptime_s; /**< Seconds since boot. A drop means the node rebooted. */
can_source_t source; /**< Which node sent it (from the ID) */
uint8_t state; /**< Sender's fsm_state_t, as a byte */
uint32_t uptime_s; /**< Seconds since boot, if this is lower than the prior log it means the node rebooted! */
} can_heartbeat_t;

/** @brief Build a heartbeat frame (INFO priority, type 0x00). */
/** @brief Build a heartbeat frame (INFO priority, type 0x00) */
void can_encode_heartbeat(can_frame_t *f, const can_heartbeat_t *hb);

/**
* @brief Decode a heartbeat.
* @brief Decode a heartbeat
* @return false if the frame is not a well-formed heartbeat (wrong priority,
* wrong type, command-only source, or too short). @p out is untouched.
* wrong type, command-only source, or too short, etc
* @p out is usuallyuntouched
*/
bool can_decode_heartbeat(const can_frame_t *f, can_heartbeat_t *out);

/** @brief Commands every node must handle, whether broadcast or targeted. */
/** @brief Commands every node must handle, whether broadcast or targeted */
typedef enum {
CAN_SYS_NONE = 0, /**< Frame is not a system command for this node. */
CAN_SYS_UNLOCK, /**< SAFE -> ACTIVE. */
CAN_SYS_SLEEP, /**< Enter SLEEP. */
CAN_SYS_RESET, /**< Reboot the MCU. */
CAN_SYS_NONE = 0, /**< Frame is not a system command for this node */
CAN_SYS_UNLOCK, /**< SAFE -> ACTIVE */
CAN_SYS_SLEEP, /**< Enter SLEEP */
CAN_SYS_RESET, /**< Reboot the MCU */
} can_sys_cmd_t;

/**
* @brief Build a system command frame (Master uses this).
* @brief Build a system command frame (Master uses this)
* @param target A node's source ID, or CAN_SRC_BROADCAST for the *_ALL version.
* @return false if @p cmd is CAN_SYS_NONE or out of range.
* @return false if @p cmd is CAN_SYS_NONE or out of range
*/
bool can_encode_sys_cmd(can_frame_t *f, can_sys_cmd_t cmd, can_source_t target);

/**
* @brief Work out whether a frame is a system command meant for this node.
* @brief Work out whether a frame is a system command meant for this node
*
* Matches the full ID (priority included), so Master's HEARTBEAT (INFO, type
* 0x00) is never mistaken for CMD_UNLOCK_NODE (IMPORTANT, type 0x00).
* Matches the full ID (priority included)
*
* @param me This node's source ID.
* @return The command, or CAN_SYS_NONE if the frame is anything else.
* @param me This node's source ID
* @return The command, or CAN_SYS_NONE if the frame is anything else
*/
can_sys_cmd_t can_decode_sys_cmd(const can_frame_t *f, can_source_t me);

Expand Down
15 changes: 7 additions & 8 deletions libs/can/can_bus.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,23 @@
extern "C" {
#endif

/** @brief Start the bus at CAN_BITRATE. Pins are ignored by the sim. */
/** @brief Start the bus at CAN_BITRATE */
bool can_bus_init(int tx_gpio, int rx_gpio);

/** @brief Queue a frame for sending. False on timeout or bus error. */
/** @brief Queue a frame for sending */
bool can_bus_send(const can_frame_t *f, uint32_t timeout_ms);

/** @brief Wait up to @p timeout_ms for a frame. False if none arrived. */
/** @brief Wait up to @p timeout_ms for a frame, false if none arrived */
bool can_bus_recv(can_frame_t *f, uint32_t timeout_ms);

/** @brief Call regularly: recovers from bus-off without a dedicated task,
* and drains driver alerts (RX_QUEUE_FULL, bus-off, error-passive). */
/** @brief Call regularly: recovers from bus-off without a dedicated RTOS task
* and drains driver alerts (RX_QUEUE_FULL, bus-off, error-passive) */
void can_bus_poll_recovery(void);

/** @brief Log the controller's state and error counters (for debugging the bus). */
/** @brief Log the controller's state and error counters (added for debugging */
void can_bus_log_status(void);

/** @brief Frames lost to RX_QUEUE_FULL since can_bus_init(). For telemetry
* (Master status frame) rather than just log lines. */
/** @brief Frames lost to RX_QUEUE_FULL since can_bus_init() */
uint32_t can_bus_dropped_frames(void);

#ifdef __cplusplus
Expand Down
10 changes: 5 additions & 5 deletions libs/can/can_eps.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ extern "C" {

/** @brief EPS_STATUS contents. */
typedef struct {
int16_t current_ma; /**< Input bus current, mA. */
int16_t shunt_mv_x100; /**< Voltage across the shunt, units of 0.01 mV. */
int16_t temp_c_x10; /**< EPS PCB temperature, units of 0.1 degC. */
int16_t current_ma; /**< Input bus current */
int16_t shunt_mv_x100; /**< units of 0.01 mV */
int16_t temp_c_x10; /**< units of 0.1 degC. */
} can_eps_status_t;

/** @brief Encode EPS_STATUS. */
/** @brief Encode EPS_STATUS */
void can_encode_eps_status(can_frame_t *f, const can_eps_status_t *s);

/** @brief Decode EPS_STATUS. False if the frame isn't one. */
/** @brief Decode EPS_STATUS, false if the frame isn't one */
bool can_decode_eps_status(const can_frame_t *f, can_eps_status_t *out);

#ifdef __cplusplus
Expand Down
5 changes: 1 addition & 4 deletions libs/can/can_instr.c
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
#include "can_instr.h"
#include "protocol.h"

/* Little-endian packing helpers, explicit rather than memcpy'ing the
* struct - avoids relying on host/target sharing endianness or struct
* layout, same approach as can_master.c. */

/* Little-endian packing helpers, explicit rather than memcpy'ing */
static void put_i16(uint8_t *p, int16_t v) { p[0] = (uint8_t)(v & 0xFF); p[1] = (uint8_t)((v >> 8) & 0xFF); }
static void put_u16(uint8_t *p, uint16_t v) { p[0] = (uint8_t)(v & 0xFF); p[1] = (uint8_t)((v >> 8) & 0xFF); }
static void put_i32(uint8_t *p, int32_t v)
Expand Down
Loading
Loading