Implementation & Test Code¶
The implementation is part of the traceability chain by:
Source files (download from the built docs): Included sources: #pragma once
#include <stdbool.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
// Temperature in 0.1°C units (e.g., 100.0°C => 1000)
typedef int16_t tsim_temp_x10_t;
typedef enum {
TSIM_STATE_SAFE = 0,
TSIM_STATE_UNSAFE = 1,
} tsim_state_t;
// REQ_FUNC_001: ADC (12-bit) to temperature conversion
// Output range: -40.0°C..+125.0°C => -400..1250 (0.1°C)
tsim_temp_x10_t tsim_adc_to_temp_x10(uint16_t adc_counts);
// REQ_FUNC_002: 5-sample moving average filter
typedef struct {
tsim_temp_x10_t window[5];
uint8_t count;
uint8_t index;
int32_t sum;
} tsim_filter_t;
void tsim_filter_init(tsim_filter_t* filter);
// Returns true when output is valid (after 5 samples)
bool tsim_filter_update(tsim_filter_t* filter, tsim_temp_x10_t sample, tsim_temp_x10_t* out_filtered);
// REQ_FUNC_003/004: threshold + hysteresis state machine
typedef struct {
tsim_temp_x10_t threshold_high_x10;
tsim_temp_x10_t threshold_low_x10;
tsim_state_t state;
} tsim_sm_t;
void tsim_sm_init(tsim_sm_t* sm, tsim_temp_x10_t high_x10, tsim_temp_x10_t low_x10);
tsim_state_t tsim_sm_evaluate(tsim_sm_t* sm, tsim_temp_x10_t filtered_temp_x10);
#ifdef __cplusplus
}
#endif
#include "tsim.h"
#include <stddef.h>
/*
OSQAR-CODE-TRACE (implementation tags)
REQ: REQ_SAFETY_001 REQ_SAFETY_002 REQ_SAFETY_003 REQ_FUNC_001 REQ_FUNC_002 REQ_FUNC_003 REQ_FUNC_004
ARCH: ARCH_001 ARCH_DESIGN_001 ARCH_DESIGN_002 ARCH_DESIGN_003 ARCH_ERROR_001 ARCH_ERROR_002 ARCH_FUNC_001 ARCH_FUNC_002 ARCH_FUNC_003 ARCH_SEOOC_001 ARCH_SEOOC_002 ARCH_SIGNAL_001 ARCH_SIGNAL_002 ARCH_SIGNAL_003
*/
// Linear mapping of ADC counts (0..4095) to Celsius (-40..125)
// Returned as 0.1°C integer.
tsim_temp_x10_t tsim_adc_to_temp_x10(uint16_t adc_counts) {
if (adc_counts > 4095u) {
// Hold last safe state behavior is integration-specific; for this example clamp.
adc_counts = 4095u;
}
// Use integer math with rounding to nearest.
// celsius = -40 + adc * (165 / 4095)
// x10: temp_x10 = -400 + adc * (1650 / 4095)
const int32_t numerator = (int32_t)adc_counts * 1650;
const int32_t scaled = (numerator + 2047) / 4095; // round
int32_t temp_x10 = -400 + scaled;
if (temp_x10 < -400) temp_x10 = -400;
if (temp_x10 > 1250) temp_x10 = 1250;
return (tsim_temp_x10_t)temp_x10;
}
void tsim_filter_init(tsim_filter_t* filter) {
if (!filter) return;
for (size_t i = 0; i < 5; i++) {
filter->window[i] = 0;
}
filter->count = 0;
filter->index = 0;
filter->sum = 0;
}
bool tsim_filter_update(tsim_filter_t* filter, tsim_temp_x10_t sample, tsim_temp_x10_t* out_filtered) {
if (!filter) return false;
if (filter->count < 5) {
filter->window[filter->index] = sample;
filter->sum += sample;
filter->index = (uint8_t)((filter->index + 1u) % 5u);
filter->count++;
return false;
}
// Replace oldest sample
const tsim_temp_x10_t old = filter->window[filter->index];
filter->sum -= old;
filter->window[filter->index] = sample;
filter->sum += sample;
filter->index = (uint8_t)((filter->index + 1u) % 5u);
if (out_filtered) {
*out_filtered = (tsim_temp_x10_t)(filter->sum / 5);
}
return true;
}
void tsim_sm_init(tsim_sm_t* sm, tsim_temp_x10_t high_x10, tsim_temp_x10_t low_x10) {
if (!sm) return;
sm->threshold_high_x10 = high_x10;
sm->threshold_low_x10 = low_x10;
sm->state = TSIM_STATE_SAFE;
}
tsim_state_t tsim_sm_evaluate(tsim_sm_t* sm, tsim_temp_x10_t filtered_temp_x10) {
if (!sm) return TSIM_STATE_SAFE;
if (sm->state == TSIM_STATE_SAFE) {
if (filtered_temp_x10 >= sm->threshold_high_x10) {
sm->state = TSIM_STATE_UNSAFE;
}
} else {
if (filtered_temp_x10 <= sm->threshold_low_x10) {
sm->state = TSIM_STATE_SAFE;
}
}
return sm->state;
}
|
The native test runner writes test_results.xml in JUnit format and maps to the TEST_* needs. #include "tsim.h"
#include "osqar_shared.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/*
OSQAR-CODE-TRACE (test tags)
TEST: TEST_CODE_001 TEST_VERIFY_001 TEST_METHOD_001 TEST_METHOD_002 TEST_METHOD_003 TEST_CONVERSION_001 TEST_FILTER_001 TEST_THRESHOLD_001 TEST_HYSTERESIS_001 TEST_END_TO_END_001 TEST_ERROR_RECOVERY_001 TEST_FAIL_SAFE_001 TEST_EXEC_001 TEST_REPORT_001
*/
typedef struct {
const char* name;
bool passed;
char message[256];
} test_result_t;
static void set_fail(test_result_t* r, const char* msg) {
r->passed = false;
strncpy(r->message, msg, sizeof(r->message) - 1);
r->message[sizeof(r->message) - 1] = '\0';
}
static test_result_t test_conversion_full_range(void) {
// TEST_CONVERSION_001
test_result_t r = {"test_conversion_full_range", true, {0}};
struct { uint16_t adc; tsim_temp_x10_t expected; int16_t tol; } cases[] = {
{0u, -400, 10},
{2048u, 425, 10},
{4095u, 1250, 10},
};
for (size_t i = 0; i < sizeof(cases)/sizeof(cases[0]); i++) {
const tsim_temp_x10_t got = tsim_adc_to_temp_x10(cases[i].adc);
const int32_t diff = (int32_t)got - (int32_t)cases[i].expected;
if (diff > cases[i].tol || diff < -cases[i].tol) {
char buf[256];
snprintf(buf, sizeof(buf), "ADC %u => %d, expected %d±%d", cases[i].adc, got, cases[i].expected, cases[i].tol);
set_fail(&r, buf);
return r;
}
}
return r;
}
static test_result_t test_filter_noise_rejection(void) {
// TEST_FILTER_001
test_result_t r = {"test_filter_noise_rejection", true, {0}};
const tsim_temp_x10_t noisy[] = {500, 600, 450, 550, 500, 480, 520, 490};
tsim_filter_t f;
tsim_filter_init(&f);
tsim_temp_x10_t outputs[16];
size_t out_count = 0;
for (size_t i = 0; i < sizeof(noisy)/sizeof(noisy[0]); i++) {
tsim_temp_x10_t out;
if (tsim_filter_update(&f, noisy[i], &out)) {
outputs[out_count++] = out;
}
}
if (out_count < 1) {
set_fail(&r, "Filter produced no outputs");
return r;
}
// outputs should be near nominal 500
for (size_t i = 0; i < out_count; i++) {
if (outputs[i] < 480 || outputs[i] > 520) {
set_fail(&r, "Filtered output out of expected band (480..520)");
return r;
}
}
return r;
}
static test_result_t test_threshold_and_hysteresis(void) {
// TEST_THRESHOLD_001 + TEST_HYSTERESIS_001
test_result_t r = {"test_threshold_and_hysteresis", true, {0}};
tsim_sm_t sm;
tsim_sm_init(&sm, 1000, 950);
if (sm.state != TSIM_STATE_SAFE) {
set_fail(&r, "Initial state must be SAFE");
return r;
}
if (tsim_sm_evaluate(&sm, 999) != TSIM_STATE_SAFE) {
set_fail(&r, "Must remain SAFE at 99.9C");
return r;
}
if (tsim_sm_evaluate(&sm, 1000) != TSIM_STATE_UNSAFE) {
set_fail(&r, "Must transition to UNSAFE at 100.0C");
return r;
}
if (tsim_sm_evaluate(&sm, 990) != TSIM_STATE_UNSAFE) {
set_fail(&r, "Must remain UNSAFE at 99.0C due to hysteresis");
return r;
}
if (tsim_sm_evaluate(&sm, 950) != TSIM_STATE_SAFE) {
set_fail(&r, "Must recover to SAFE at 95.0C");
return r;
}
return r;
}
static test_result_t test_shared_magic_constant(void) {
// TEST_METHOD_001
test_result_t r = {"test_shared_magic_constant", true, {0}};
const int got = osqar_shared_magic();
if (got != 42) {
char buf[256];
snprintf(buf, sizeof(buf), "osqar_shared_magic() => %d, expected 42", got);
set_fail(&r, buf);
return r;
}
return r;
}
static void write_junit(const char* path, const test_result_t* results, size_t count) {
FILE* f = fopen(path, "w");
if (!f) {
fprintf(stderr, "Failed to open %s for writing\n", path);
exit(2);
}
size_t failures = 0;
for (size_t i = 0; i < count; i++) {
if (!results[i].passed) failures++;
}
fprintf(f, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
fprintf(
f,
"<testsuite name=\"tsim_c\" tests=\"%zu\" failures=\"%zu\" errors=\"0\" skipped=\"0\" time=\"0\">\n",
count,
failures
);
for (size_t i = 0; i < count; i++) {
fprintf(f, " <testcase classname=\"tsim_c\" name=\"%s\" time=\"0\">\n", results[i].name);
if (!results[i].passed) {
fprintf(f, " <failure message=\"%s\"/>\n", results[i].message[0] ? results[i].message : "failed");
}
fprintf(f, " </testcase>\n");
}
fprintf(f, "</testsuite>\n");
fclose(f);
}
int main(int argc, char** argv) {
const char* out = (argc >= 2) ? argv[1] : "test_results.xml";
test_result_t results[] = {
test_conversion_full_range(),
test_filter_noise_rejection(),
test_threshold_and_hysteresis(),
test_shared_magic_constant(),
};
const size_t count = sizeof(results)/sizeof(results[0]);
write_junit(out, results, count);
for (size_t i = 0; i < count; i++) {
if (!results[i].passed) {
fprintf(stderr, "FAIL: %s: %s\n", results[i].name, results[i].message);
return 1;
}
}
printf("PASS: %zu tests\n", count);
return 0;
}
|
This example keeps documentation (*.rst) next to the C source and test artifacts for end-to-end traceability. |
Code Organization¶
This TSIM implementation is written in C and keeps the same requirement IDs as the reference example.
examples/c_hello_world/
├── include/
│ └── tsim.h
├── src/
│ └── tsim.c
├── tests/
│ └── test_tsim.c # Native test runner writing JUnit XML
├── CMakeLists.txt
├── test_results.xml # Generated by ./build-and-test.sh
└── build-and-test.sh
Traceability in C Code¶
In C/C++ projects, requirement traceability is typically captured via:
structured comments (e.g., REQ_FUNC_001, TEST_CONVERSION_001)
module-level headers (interfaces as architecture artifacts)
In OSQAr, you can additionally trace into the actual source by embedding excerpts via
literalinclude and attaching full files via :download: (see above).
The functions in include/tsim.h map directly to:
(FR) The module shall read ... (REQ_FUNC_001) (ADC conversion)
(FR) The module shall filte... (REQ_FUNC_002) (5-sample moving average)
(FR) The module shall trigg... (REQ_FUNC_003) / (FR) The module shall trigg... (REQ_FUNC_004) (threshold + hysteresis)
Building & Running Tests¶
cd examples/c_hello_world
# Build
cmake -S . -B build
cmake --build build
# Run tests (writes JUnit XML)
./build/junit_tests test_results.xml
Then build the documentation (imports test_results.xml):
osqar build-docs --project examples/c_hello_world