# Project Structure

How the code is structured and organized

# Layout

## Code Structure

### Architecture (Summary)

<span style="white-space: pre-wrap;">Each board’s </span>`<span class="editor-theme-code">main.c</span>`<span style="white-space: pre-wrap;"> acts strictly as an orchestrator. It initializes the runtime, creates tasks, and delegates all functional behavior to component modules.</span>

### Core Design Contract

<span style="white-space: pre-wrap;">The repository enforces a strict separation between </span>**entrypoints**<span style="white-space: pre-wrap;"> and </span>**components**:

- `<span class="editor-theme-code">src/<board>/main.c</span>`<span style="white-space: pre-wrap;"> defines the process entrypoint for each board target.</span>
- `<span class="editor-theme-code">components/common/*</span>`<span style="white-space: pre-wrap;"> contains reusable logic shared across multiple boards.</span>
- `<span class="editor-theme-code">components/<board>/*</span>`<span style="white-space: pre-wrap;"> contains board-specific functionality.</span>
- `<span class="editor-theme-code">components/<board>/firmware/*</span>`<span style="white-space: pre-wrap;"> contains generated or vendor-provided firmware and MCU integration code.</span>

### Primary Rule

> `<span class="editor-theme-code">main.c</span>`<span style="white-space: pre-wrap;"> must not contain domain logic. It is responsible only for system wiring and task startup.</span>
> 
> \- Albert Einstein

## Repository Layout

```
erc/
├─ src/
│  ├─ arm_board/main.c
│  ├─ driving_board/main.c
│  ├─ sensor_board/main.c
│  ├─ network_board/main.c
│  └─ debugging_board/main.c
│
├─ components/
│  ├─ common/              # Shared modules across boards
│  ├─ arm_board/           # Arm board-specific modules
│  ├─ driving_board/       # Driving board-specific modules
│  ├─ sensor_board/        # Sensor board-specific modules
│  ├─ network_board/       # Network board-specific modules
│  └─ debugging_board/     # Debugging board-specific modules
│
├─ test/
│  ├─ common/
│  ├─ arm_board/
│  ├─ driving_board/
│  ├─ sensor_board/
│  └─ debugging_board/
│
├─ scripts/                # Utility scripts (codegen, post-processing)
└─ platformio.ini          # Build environments and board filters
```

## Entrypoint Responsibilities (`<span class="editor-theme-code">src/<board>/main.c</span>`)

<span style="white-space: pre-wrap;">The </span>`<span class="editor-theme-code">main.c</span>`<span style="white-space: pre-wrap;"> file is intentionally minimal and should perform only the following:</span>

1. Execute mandatory low-level initialization  
    **(HAL, clock, cache, MPU, RTOS initialization as required)**
2. Initialize infrastructure dependencies  
    **(GPIO, UART, timers, networking wrappers, etc.)**
3. Create one or more RTOS tasks
4. Start the scheduler/kernel
5. Delegate all functional behavior to components

### <span style="white-space: pre-wrap;">What Must NOT Be Implemented in </span>`<span class="editor-theme-code">main.c</span>`

<span style="white-space: pre-wrap;">The following must never reside in </span>`<span class="editor-theme-code">main.c</span>`:

- Sensor processing algorithms
- Business or control logic
- Packet parsing or dispatch policy
- Device-specific runtime behavior beyond initialization
- Long-running loops implementing application logic

If logic grows beyond simple initialization or task creation, it must be moved into a component module and invoked from a task.

## Component Responsibilities (`<span class="editor-theme-code">components/*</span>`)

All functional behavior belongs in components. Tasks must delegate to components rather than implementing logic inline.

### Examples

- <span style="white-space: pre-wrap;">Sensor-related behavior → </span>`<span class="editor-theme-code">components/sensor_board/*</span>`  
    **(e.g., GPS, IMU, pH sensors, acquisition pipelines)**
- <span style="white-space: pre-wrap;">Driving logic → </span>`<span class="editor-theme-code">components/driving_board/*</span>`  
    **(e.g., motor control, calculations, protocol parsing, Simulink integration)**
- <span style="white-space: pre-wrap;">Debugging UI and diagnostics → </span>`<span class="editor-theme-code">components/debugging_board/*</span>`
- <span style="white-space: pre-wrap;">Shared infrastructure → </span>`<span class="editor-theme-code">components/common/*</span>`  
    **(e.g., result handling, logging, queues, dispatch systems)**

## Execution Model

The execution flow for each board follows a consistent structure:

```
main.c
  → platform/runtime initialization
  → create RTOS task(s)
  → each task calls component APIs
  → component modules execute all functional logic
```

This ensures that:

- `<span class="editor-theme-code">main.c</span>`<span style="white-space: pre-wrap;"> remains stable and minimal</span>
- behavior is modular and testable
- functionality is reusable across boards

# Post-Generation Scripts

### Introduction

<span style="white-space: pre-wrap;">We use one post code generation script. We do this because we do not want to write code inside the auto-generated code, and this helps with that. If you are on Linux or (possibly, untested but likely) mac, you can refer to the script in the cubeMX software by going into Project Manager -&gt; Code Generator -&gt; User Actions -&gt; After Code Generation. If you are not on a UNIX based system you have to run it every time after generating code manually from the folder where the file is located. The file is located under scripts and is called post\_code\_generation.bash. For mac, the script is called post\_code\_generator\_mac.bash, because there are some small formatting changes, more explained in Mac Changes. </span>

### Functions

The script has 4 different functions

#### Renaming main files

It starts by renaming all main.h and main.c files that are in components. It also saved all boards that have main.c files, because those are the newly generated boards. This way you don't do certain actions on the files twice, if you would run the script again.

```bash
while IFS= read -r FILE; do
  # Extract the basename (filename without path)
  base="$(basename "$FILE")"

  if [[ "$base" == "main.c" ]]; then
    
    subdir="${FILE#"$BASE"/}" # Path from the board dir
    main_dir="${subdir%%/*}" # The board dir
    GENERATED_BOARDS+=("$main_dir") # Gets all boards that are generated again, and thus have a main
    
    dir=$(dirname "$FILE") # Get directory of the file
    mv "$FILE" "$dir/cubemx_main.c"
    echo "Renamed $FILE to $dir/cubemx_main.c"
  fi

  if [[ "$base" == "main.h" ]]; then
    dir=$(dirname "$FILE") # Get directory of the file
    mv "$FILE" "$dir/cubemx_main.h"
    echo "Renamed $FILE to $dir/cubemx_main.h"
  fi
done < <(find "$BASE" -type f)
```

#### Adding firmware definitions

Certain constants might have to be set in the main.h files from cubemx. This happens most likely because of the order in which the files are build, but I am not totally sure. However, if you do need to have some constants set in the main.h file, you can at them to any file in the folder called "firmware\_definitions" in the common folder of components. This second code block adds it to the .h file.

```bash
find "$BASE" -type d -name firmware_definitions | while read -r FW_DIR; do
    BOARD_DIR_PATH="$(dirname "$FW_DIR")"
    BOARD_DIR="${BOARD_DIR_PATH#"$BASE"/}"
    if printf '%s\n' "$COMMON_COMPONENT" "${GENERATED_BOARDS[@]}" | grep -Fx "$BOARD_DIR" > /dev/null; then
      BOARD_DIR_PATHS=("$BOARD_DIR_PATH")
    
      if [[ "$BOARD_DIR" == "$COMMON_COMPONENT" ]]; then  
        BOARD_DIR_PATHS=(${GENERATED_BOARDS[@]/#/"$BASE"/})
      fi  
      for BOARD_DIR_PATH in "${BOARD_DIR_PATHS[@]}"; do
      
        CUBEMX_FILE="$BOARD_DIR_PATH/firmware/Core/Inc/cubemx_main.h"

        # Skip if cubemx file does not exist
        [[ -f "$CUBEMX_FILE" ]] || continue

        echo "Appending firmware_definitions to: $CUBEMX_FILE"

        TMP_FILE="$(mktemp)"

        head -n -1 "$CUBEMX_FILE" >> "$TMP_FILE"
    
        echo -e "\n/* ---- START firmware_definitions ---- */\n" >> "$TMP_FILE"
        find "$FW_DIR" -type f -exec cat {} \; >> "$TMP_FILE"
        echo -e "\n/* ---- END firmware_definitions ---- */\n" >> "$TMP_FILE"

        tail -n -1 "$CUBEMX_FILE" >> "$TMP_FILE"



        # 3) Replace original file
        mv "$TMP_FILE" "$CUBEMX_FILE"
      done 
    fi
done
```

#### Adding static wrappers

Some functions generated by cubemx you do need, but they are static so you cannot use them outside of the main file. To still be able to do that, the script adds wrappers for those files.

```bash
find "$BASE" -type f -name "cubemx_main.c" | while read -r FILE; do
  subdir="${FILE#"$BASE"/}" # Path from the board dir
  BOARD_DIR="${subdir%%/*}" # The board dir
  if printf '%s\n' "${GENERATED_BOARDS[@]}" | grep -Fx "$BOARD_DIR" > /dev/null; then
    TMP_FILE="$(mktemp)"
    echo "READING $FILE"
    while read -r line; do
        echo "$line" >> "$TMP_FILE"
        if [[ "$line" =~ ^[[:space:]]*static[[:space:]]+[a-zA-Z_][a-zA-Z0-9_]*[[:space:]]+[a-zA-Z_][a-zA-Z0-9_]*\([^\)]*\)\;[[:space:]]*$ ]]; then        # Remove 'static' and trailing ';'
          echo "Static function found: $line"
          proto=$(echo "$line" | sed -E 's/^[[:space:]]*static[[:space:]]+//; s/;[[:space:]]*$//')

          # Extract function name
          name=$(echo "$proto" | sed -E 's/.*[[:space:]]+([a-zA-Z_][a-zA-Z0-9_]*)\(.*/\1/')

          # Extract return type
          ret=$(echo "$proto" | sed -E "s/[[:space:]]+$name\(.*//")

          # Extract argument list
          args=$(echo "$proto" | sed -E "s/.*$name\((.*)\)/\1/")

          # Build argument names (remove types)
          call_args=$(echo "$args" | sed -E 's/[a-zA-Z_][a-zA-Z0-9_]*[[:space:]]+//g')
          if [[ "$call_args" == "void" ]]; then 
            call_args=""
          fi

          echo "$ret ${name}_wrapper($args) {" >> "$TMP_FILE"
          if [[ "$ret" == "void" ]]; then
              echo "    $name($call_args);" >> "$TMP_FILE"
          else
              echo "    return $name($call_args);" >> "$TMP_FILE"
          fi
          echo "}" >> "$TMP_FILE"
          echo >> "$TMP_FILE"
          echo "added wrapper for static function ${name} in ${FILE}"
        fi
    done < "$FILE"
    mv "$TMP_FILE" "$FILE"
  fi
done
```

#### Changing the includes

Because of the name change from main.c/h, to cubemx\_main.c/h, the includes are now wrong. This last code block changes all the includes to the right name.

```bash
while IFS= read -r FILE; do
  subdir="${FILE#"$BASE"/}" # Path from the board dir
  BOARD_DIR="${subdir%%/*}" # The board dir
  if printf '%s\n' "${GENERATED_BOARDS[@]}" | grep -Fx "$BOARD_DIR" > /dev/null; then
    sed -i 's/#include "main.h"/#include "cubemx_main.h"/g' "$FILE"
    echo "Updated include in $FILE"
  fi
done < <(grep -rl '#include "main.h"' ../components/)
```

### MAC Changes

<span style="white-space: pre-wrap;">1) Added </span><span style="color: rgb(106, 153, 85);">\#! /usr/bin/env bash</span><span style="white-space: pre-wrap;"> in the first line</span>

<span style="white-space: pre-wrap;">2) changed </span><span style="color: rgb(212, 212, 212); white-space: pre-wrap;">sed </span><span style="color: rgb(206, 145, 120);">-i</span><span style="color: rgb(212, 212, 212); white-space: pre-wrap;"> </span><span style="color: rgb(206, 145, 120);">'s/#include "main.h"/#include "cubemx\_main.h"/g'</span><span style="color: rgb(212, 212, 212); white-space: pre-wrap;"> </span><span style="color: rgb(206, 145, 120);">"$FILE"</span><span style="white-space: pre-wrap;"> </span>

<span style="white-space: pre-wrap;">to </span><span style="color: rgb(212, 212, 212); white-space: pre-wrap;">sed </span><span style="color: rgb(206, 145, 120);">-i</span><span style="color: rgb(212, 212, 212); white-space: pre-wrap;"> </span><span style="color: rgb(206, 145, 120);">''</span><span style="color: rgb(212, 212, 212); white-space: pre-wrap;"> </span><span style="color: rgb(206, 145, 120);">'s/#include "main.h"/#include "cubemx\_main.h"/g'</span><span style="color: rgb(212, 212, 212); white-space: pre-wrap;"> </span><span style="color: rgb(206, 145, 120); white-space: pre-wrap;">"$FILE" </span><span style="color: rgb(0, 0, 0);">because mac uses a different version of sed.</span>

# Simple PIOC

### Introduction

This Python script processes a custom PlatformIO configuration file (`<span class="editor-theme-code">platformio.pioc</span>`<span style="white-space: pre-wrap;">) and generates a standard </span>`<span class="editor-theme-code">platformio.ini</span>`<span style="white-space: pre-wrap;"> file.</span>

It extends PlatformIO’s configuration capabilities by:

- <span style="white-space: pre-wrap;">Supporting </span>**dynamic include paths**<span style="white-space: pre-wrap;"> using glob patterns</span>
- <span style="white-space: pre-wrap;">Extracting </span>**C preprocessor defines**<span style="white-space: pre-wrap;"> from board-specific Makefiles</span>
- <span style="white-space: pre-wrap;">Expanding </span>**custom syntax**<span style="white-space: pre-wrap;"> into valid </span>`<span class="editor-theme-code">build_flags</span>`
- <span style="white-space: pre-wrap;">Resolving </span>**absolute paths**.

### Key Features

#### Custom build\_flags Processing

Supports two types of entries:

- **+&lt;pattern&gt;**: Include directories
- **-&lt;pattern&gt;**: Exclude directories
- Other entries are treated as standard compiler flags

#### Include Path Resolution

<span style="white-space: pre-wrap;">Glob patterns are expanded into directory paths using recursive search. </span>

#### Board-specific C Defines

Defines are extracted from:

```
components/<board>/firmware/Makefile
```

<span style="white-space: pre-wrap;">The script looks for a </span>`<span class="editor-theme-code">C_DEFS</span>`<span style="white-space: pre-wrap;"> section and includes all compiler defines.</span>

<span style="white-space: pre-wrap;">This is done, because cubeMX generates important definitions in the auto-generated makefile. These are not used if we don't copy them to the .ini file. </span>

#### Environment Detection

```
[env:my_board]
```

This determines which board folder is used.

#### Get Absolute Path

<span style="white-space: pre-wrap;">For some functions, like nanopb, you might need the absolute path. There is no way to get in the default platformio.ini file, so that would mean that you would have to hard code it. We do not want that, so we have a placeholder for an absolute path. </span>

The placeholder:

```
${{project_absolute_path}}$
```

<span style="white-space: pre-wrap;">is replaced with the absolute path of the project. </span>

## Workflow

1. <span style="white-space: pre-wrap;">Read </span>`<span class="editor-theme-code">platformio.pioc</span>`
2. Detect environment
3. <span style="white-space: pre-wrap;">Parse </span>`<span class="editor-theme-code">build_flags</span>`
4. Resolve glob patterns
5. Extract C defines
6. <span style="white-space: pre-wrap;">Write </span>`<span class="editor-theme-code">platformio.ini</span>`
7. Replace placeholders

## Example Input

```
[env:my_board]
build_flags =
    +<lib/**>
    -<lib/exclude/**>
    -DDEBUG
```

## Example Output

```
[env:my_board]
build_flags =
    -I lib/module1
    -I lib/module2
    -DDEBUG
    -DDEFINE_FROM_MAKEFILE
```

# .pioc file

## Introduction

<span style="white-space: pre-wrap;">The </span>`<span class="editor-theme-code">platformio.pioc</span>`<span style="white-space: pre-wrap;"> file is the central configurationhere file used to define build environments, dependencies, compiler flags, and project structure. It uses a format with sections and key-value pairs. It is similar to platformio.ini, which is actually used by platformio, but has some changes to use it easily with our project. For more information, read </span>[Simple PIOC](https://bookstack.roboteamtwente.nl/books/embedded-infastructure/page/simple-pioc "https://bookstack.roboteamtwente.nl/books/embedded-infastructure/page/simple-pioc").

## File Structure

<span style="white-space: pre-wrap;">The configuration is divided into sections such as </span>`<span class="editor-theme-code">[platformio]</span>`<span style="white-space: pre-wrap;">, </span>`<span class="editor-theme-code">[env]</span>`<span style="white-space: pre-wrap;">, and environment-specific sections like </span>`<span class="editor-theme-code">[env:network_board]</span>`.

## Core Sections

### \[platformio\]

Defines global project settings.

- `<span class="editor-theme-code">default_envs</span>`: Default environment(s) to build.
- `<span class="editor-theme-code">src_dir</span>`: Source directory.
- `<span class="editor-theme-code">lib_dir</span>`: Library directory.

### \[extra\]

Custom user-defined variables for reuse.

### \[env\]

Base configuration shared across all environments.

## Environment Sections

<span style="white-space: pre-wrap;">Each </span>`<span class="editor-theme-code">[env:<name>]</span>`<span style="white-space: pre-wrap;"> defines a specific build target. These inherit from </span>`<span class="editor-theme-code">[env]</span>`.

## Custom Enhancements

### 1. Glob Patterns in build\_flags

<span style="white-space: pre-wrap;">Unlike standard PlatformIO, this configuration allows glob-style include/exclude patterns directly in </span>`<span class="editor-theme-code">build_flags</span>`<span style="white-space: pre-wrap;"> using </span>`<span class="editor-theme-code">+<...></span>`<span style="white-space: pre-wrap;"> and </span>`<span class="editor-theme-code">-<...></span>`<span style="white-space: pre-wrap;"> syntax.</span>

```
build_flags=
    +<components/network_board/**>
    -<components/network_board/firmware/Drivers/**>
```

This enables fine-grained control over which directories are included in compilation.

### 2. Absolute Path Variable

<span style="white-space: pre-wrap;">The variable </span>`<span class="editor-theme-code">${{project_absolute_path}}$</span>`<span style="white-space: pre-wrap;"> expands to the absolute path of the project root.</span>

```
custom_nanopb_project_dir = ${{project_absolute_path}}$/ERC-Protobufs
```

## Source Filtering

`<span class="editor-theme-code">build_src_filter</span>`<span style="white-space: pre-wrap;"> defines which source files are compiled. It supports inclusion (+) and exclusion (-) rules.</span>

```
+<src/${this.__env__}/**/*.c>
-<components/${this.__env__}/firmware/Drivers/*>
```

## Variable Substitution

- `<span class="editor-theme-code">${extra.common_lib_deps}</span>`: Reference shared variables.
- `<span class="editor-theme-code">${this.__env__}</span>`: Current environment name.

## Library Dependencies

External libraries can be defined using Git URLs or registry references.

```
lib_deps = https://github.com/nanopb/nanopb.git#commit
```

## Compiler and Linker Flags

Standard compiler flags are also supported alongside glob patterns.

```
-mthumb
-mfpu=fpv4-sp-d16
-D CONFIG_LOG_LEVEL=LOG_INFO
```

## Example Configuration

```
[env:network_board]
board = nucleo_h753zi
build_flags=
    +<components/network_board/**>
    -<components/network_board/firmware/Drivers/**>
    -mthumb
    -D CONFIG_LOG_LEVEL=LOG_INFO
```

## Compilation

<span style="white-space: pre-wrap;">To use the file, you have to convert it to a </span>`<span class="editor-theme-code">platformio.ini</span>`<span style="white-space: pre-wrap;"> file. You can do that by running </span>

```
python3 simple_pioc.py
```

## Extra information

<span style="white-space: pre-wrap;">If more information is needed, look at the documentation specifically for platformio.ini files. You can find it </span>[here](https://docs.platformio.org/en/latest/projectconf/index.html).