Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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: 2 additions & 2 deletions .clang-format
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
Language: C
Language: Cpp
BasedOnStyle: LLVM

# Indentation
Expand All @@ -13,7 +13,7 @@ ColumnLimit: 100

# Braces
BreakBeforeBraces: Allman
InsertBraces: false
InsertBraces: true

# Spaces
SpaceAfterCStyleCast: false
Expand Down
54 changes: 54 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: CI

on:
push:
branches: [main]
pull_request:

jobs:
unit-tests:
name: Unit tests (${{ matrix.cc }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
cc: [gcc, clang]
steps:
- uses: actions/checkout@v4
- name: Build library, example and run unit tests
run: make CC=${{ matrix.cc }}
- name: Run example
run: ./build/examples/spacepacket_example

coverage:
name: Coverage (100% line and branch)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install gcovr
run: sudo apt-get update && sudo apt-get install -y gcovr
- name: Build instrumented tests and generate report
run: make coverage-html
- name: Enforce 100% line and branch coverage
run: >
gcovr -r . --filter src
--fail-under-line 100
--fail-under-branch 100
- name: Upload HTML report
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-html
path: build/coverage/
if-no-files-found: ignore

sanitizers:
name: ASan + UBSan
runs-on: ubuntu-latest
env:
ASAN_OPTIONS: detect_leaks=1:abort_on_error=1
UBSAN_OPTIONS: print_stacktrace=1
steps:
- uses: actions/checkout@v4
- name: Build and run unit tests and example under ASan and UBSan
run: make sanitize
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ dkms.conf

# project build directories
build/
.github/
.vscode/
.idea/

50 changes: 38 additions & 12 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,53 +1,79 @@
CC ?= cc
# Stronger warnings for code quality
CFLAGS ?= -O2 -Iinclude -Wall -Wextra -Wpedantic -Wconversion -Wshadow \
# Optimisation / instrumentation flags; overridden by tools/coverage_html.sh
OPT ?= -O2
SANITIZE_OPT = -O1 -g -fno-omit-frame-pointer -fsanitize=address,undefined \
-fno-sanitize-recover=all
# Stronger warnings for code quality; any warning fails the build
CFLAGS ?= $(OPT) -Iinclude -Werror -Wall -Wextra -Wpedantic -Wconversion -Wshadow \
-Wcast-align -Wcast-qual -Wpointer-arith -Wformat=2 \
-Wmissing-prototypes -Wstrict-prototypes -Wredundant-decls -Wundef \
-std=c99
AR ?= ar

PREFIX ?= /usr/local

LIBNAME = libspacepacket.a
BUILD_DIR = build
LIB_PATH = $(BUILD_DIR)/$(LIBNAME)
OBJ_PATH = $(BUILD_DIR)/src/space_packet.o
EXAMPLE_PATH = $(BUILD_DIR)/examples/spacepacket_example
CTEST_PATH = $(BUILD_DIR)/tests/ctest
COVERAGE_MIN ?= 95
SANITIZE_DIR = $(BUILD_DIR)/sanitize

PUBLIC_HEADERS = include/space_packet.h
TEST_SOURCES = tests/unit_tests.c tests/test_space_packet.c
TEST_HEADERS = tests/cunit.h tests/test_runners.h

all: lib example test

lib: $(LIB_PATH)

$(OBJ_PATH): src/space_packet.c
$(OBJ_PATH): src/space_packet.c $(PUBLIC_HEADERS)
mkdir -p $(dir $@)
$(CC) $(CFLAGS) -c src/space_packet.c -o $(OBJ_PATH)
$(CC) $(CFLAGS) -Iinclude -c src/space_packet.c -o $(OBJ_PATH)

$(LIB_PATH): $(OBJ_PATH)
mkdir -p $(dir $@)
$(AR) rcs $(LIB_PATH) $(OBJ_PATH)

example: $(EXAMPLE_PATH)
./$(EXAMPLE_PATH)

$(EXAMPLE_PATH): lib examples/main.c
$(EXAMPLE_PATH): $(LIB_PATH) examples/main.c $(PUBLIC_HEADERS)
mkdir -p $(dir $@)
$(CC) $(CFLAGS) -Iinclude examples/main.c $(LIB_PATH) -o $(EXAMPLE_PATH)


ctest: $(CTEST_PATH)

$(CTEST_PATH): lib tests/unit_tests.c tests/test_space_packet.c
$(CTEST_PATH): $(LIB_PATH) $(TEST_SOURCES) $(TEST_HEADERS) $(PUBLIC_HEADERS)
mkdir -p $(dir $@)
$(CC) $(CFLAGS) -Iinclude tests/unit_tests.c tests/test_space_packet.c $(LIB_PATH) -o $(CTEST_PATH)
$(CC) $(CFLAGS) -Iinclude $(TEST_SOURCES) $(LIB_PATH) -o $(CTEST_PATH)

test: ctest
./$(CTEST_PATH)

coverage-html:
bash tools/coverage_html.sh

# Rebuild everything with ASan + UBSan and run the unit tests and the example.
# Program output is shown only on failure; sanitizer reports go to stderr and are always
# visible. The instrumented build is removed afterwards, on success and on failure.
sanitize:
@$(MAKE) --no-print-directory clean >/dev/null
@$(MAKE) --no-print-directory lib example ctest OPT="$(SANITIZE_OPT)" >/dev/null \
|| { $(MAKE) --no-print-directory clean >/dev/null; exit 1; }
@mkdir -p $(SANITIZE_DIR)
@echo "Sanitizers (ASan + UBSan):"
@./$(CTEST_PATH) >$(SANITIZE_DIR)/unit_tests.log \
&& echo " library via unit tests : no errors detected" \
|| { cat $(SANITIZE_DIR)/unit_tests.log; echo " library via unit tests : FAILED"; \
$(MAKE) --no-print-directory clean >/dev/null; exit 1; }
@./$(EXAMPLE_PATH) >$(SANITIZE_DIR)/example.log \
&& echo " library via example : no errors detected" \
|| { cat $(SANITIZE_DIR)/example.log; echo " library via example : FAILED"; \
$(MAKE) --no-print-directory clean >/dev/null; exit 1; }
@$(MAKE) --no-print-directory clean >/dev/null
@echo "Result: PASS"

clean:
rm -rf $(BUILD_DIR)

.PHONY: all lib example test ctest coverage-html ci clean install
.PHONY: all lib example test ctest coverage-html sanitize clean
67 changes: 63 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ the source end user").

- **Primary Header serializer / parser** — big-endian, bit-exact per CCSDS §4.1.3
- **Sequence Flags** — wire values match the standard (`UNSEGMENTED = 0b11 = 3`)
- **Version enforcement** — always serializes Packet Version Number as `000` (§4.1.3.2)
- **Version enforcement** — always serializes Packet Version Number as `000` and rejects any other value on parse (§4.1.3.2)
- **Zero allocation** — no dynamic memory inside the library; caller supplies all buffers
- **Minimal footprint** — single header + single source file, no external dependencies
- **Pure C99** — no OS primitives, suitable for bare-metal targets
Expand Down Expand Up @@ -62,6 +62,9 @@ appended to the Packet Data Field before calling `sp_packet_serialize`. See

```
EmbeddedSpacePacket/
├── .github/
│ └── workflows/
│ └── ci.yml # CI: unit tests, coverage gate, sanitizers
├── include/
│ └── space_packet.h # Public API and types
├── src/
Expand All @@ -76,7 +79,6 @@ EmbeddedSpacePacket/
├── tools/
│ └── coverage_html.sh # gcovr HTML coverage report
├── build/ # Build artifacts (git-ignored)
├── docs/ # CCSDS reference PDFs
├── Makefile
└── README.md
```
Expand All @@ -90,28 +92,85 @@ make example # example binary → build/examples/spacepacket_example
make test # build and run tests
```

Warnings are treated as errors (`-Werror`). The build is checked with both `gcc` and
`clang`; select the compiler with `make CC=clang`.

### Coverage (requires `gcovr`)

```bash
sudo apt install gcovr
make coverage-html # → build/coverage/index.html
```

### Sanitizers (ASan + UBSan)

```bash
make sanitize # rebuild with ASan + UBSan, run tests and example, then clean up
```

### Clean

```bash
make clean
```

### Continuous Integration

Every push to `main` and every pull request runs three jobs
([.github/workflows/ci.yml](.github/workflows/ci.yml)):

| Job | What it checks |
| ------------ | ------------------------------------------------------------------- |
| Unit tests | `make` with `gcc` and with `clang`, then the example binary |
| Coverage | `make coverage-html`, then requires 100% line and branch coverage |
| ASan + UBSan | `make sanitize` |

## Quick Start

Look at the example/.
Minimal encode / decode round trip, condensed from [examples/main.c](examples/main.c):

```c
#include "space_packet.h"

const uint8_t payload[] = {'H', 'e', 'l', 'l', 'o', ' ', 'S', 'P'};

/* Encode */
sp_packet_t pkt;
sp_packet_init(&pkt);
sp_set_primary_header(&pkt,
SP_PACKET_TYPE_TM,
0, /* no secondary header */
0x100, /* APID */
SP_SEQ_FLAG_UNSEGMENTED,
1 /* sequence count */);
sp_set_data(&pkt, payload, sizeof(payload));

uint8_t buf[256];
uint32_t n = sp_packet_serialize(&pkt, buf, sizeof(buf));
if (n == 0)
{
/* NULL args, empty data, or buffer too small */
}

/* Decode */
sp_packet_t parsed;
if (!sp_packet_parse(&parsed, buf, n))
{
/* non-zero version, or buffer shorter than declared data length */
}
/* parsed.ph.apid, parsed.ph.seq_count, parsed.data, parsed.data_len
* parsed.data points into buf (zero-copy) — keep buf alive. */
```

The full example in [examples/main.c](examples/main.c) additionally shows a
mission-defined secondary header and an application-level CRC-16-CCITT
(`make example`).

## Memory Footprint (estimated, 64-bit host)

| Item | Size |
| ----------------------- | -------------------- |
| `sp_packet_t` struct | ~28 bytes |
| `sp_packet_t` struct | 24 bytes |
| Library code (stripped) | < 1 KB |
| Serialization buffer | 6 + `data_len` bytes |
| Heap usage | none |
Expand Down
Binary file removed docs/130x3g1.pdf
Binary file not shown.
Binary file removed docs/133x0b2e2.pdf
Binary file not shown.
21 changes: 13 additions & 8 deletions examples/main.c
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#include "../include/space_packet.h"
#include "space_packet.h"

#include <stdint.h>
#include <stdio.h>
Expand All @@ -16,7 +16,9 @@ static uint16_t crc16_ccitt(const uint8_t *data, size_t len)
{
crc ^= (uint16_t)((uint16_t)data[i] << 8);
for (int k = 0; k < 8; ++k)
crc = (crc & 0x8000u) ? (uint16_t)((crc << 1) ^ 0x1021u) : (uint16_t)(crc << 1);
{
crc = (uint16_t)(((unsigned)crc << 1) ^ ((crc & 0x8000u) ? 0x1021u : 0u));
}
}
return crc;
}
Expand Down Expand Up @@ -53,19 +55,21 @@ int main(void)
0x100, /* APID */
SP_SEQ_FLAG_UNSEGMENTED,
1 /* sequence count */);
sp_set_data(&pkt, pkt_data, (uint16_t)off);
sp_set_data(&pkt, pkt_data, (uint32_t)off);

uint8_t buf[256];
size_t n = sp_packet_serialize(&pkt, buf, sizeof(buf));
uint32_t n = sp_packet_serialize(&pkt, buf, sizeof(buf));
if (n == 0)
{
printf("serialize failed\n");
return 1;
}

printf("Serialized %zu bytes:\n", n);
printf("Serialized %lu bytes:\n", (unsigned long)n);
for (size_t i = 0; i < n; ++i)
{
printf("%02X ", buf[i]);
}
printf("\n");

sp_packet_t parsed;
Expand All @@ -82,7 +86,8 @@ int main(void)
return 3;
}
size_t crc_area = parsed.data_len - 2;
uint16_t crc_recv = ((uint16_t)parsed.data[crc_area] << 8) | parsed.data[crc_area + 1];
uint16_t crc_recv =
(uint16_t)(((unsigned)parsed.data[crc_area] << 8) | parsed.data[crc_area + 1]);
uint16_t crc_calc = crc16_ccitt(parsed.data, crc_area);
if (crc_recv != crc_calc)
{
Expand All @@ -95,10 +100,10 @@ int main(void)
const uint8_t *pay = parsed.data + sec_total;
size_t pay_len = crc_area - sec_total;

printf("APID=0x%03X seq_count=%u data_len=%u CRC OK\n",
printf("APID=0x%03X seq_count=%u data_len=%lu CRC OK\n",
parsed.ph.apid,
parsed.ph.seq_count,
parsed.data_len);
(unsigned long)parsed.data_len);
printf("Payload: ");
fwrite(pay, 1, pay_len, stdout);
printf("\n");
Expand Down
Loading
Loading