diff --git a/.gitignore b/.gitignore index a709fdd..2301de3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ **/build/ -.DS_Store \ No newline at end of file +.DS_Store +twister-out* +.cache \ No newline at end of file diff --git a/apps/rockets/cloudburst/CMakeLists.txt b/apps/rockets/cloudburst/CMakeLists.txt index f1987fc..5e78576 100644 --- a/apps/rockets/cloudburst/CMakeLists.txt +++ b/apps/rockets/cloudburst/CMakeLists.txt @@ -22,4 +22,23 @@ endif() find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) project(cloudburst) -target_sources(app PRIVATE src/main.c src/data.c src/sensors/imu_thread.c src/logger_thread.c src/sensors/baro_thread.c) +target_sources(app PRIVATE + src/main.c + src/data.c + src/sensors/imu_thread.c + src/logger_thread.c + src/sensors/baro_thread.c + src/state_machine/state_machine.c + src/state_machine/state_machine_common.c + src/state_machine/states/standby.c + src/state_machine/states/ascent.c + src/state_machine/states/mach_lock.c + src/state_machine/states/drogue_descent.c + src/state_machine/states/main_descent.c + src/state_machine/states/landed.c +) + +target_include_directories(app PRIVATE + src + src/state_machine +) diff --git a/apps/rockets/cloudburst/prj.conf b/apps/rockets/cloudburst/prj.conf index 2b93880..26d308d 100644 --- a/apps/rockets/cloudburst/prj.conf +++ b/apps/rockets/cloudburst/prj.conf @@ -10,6 +10,9 @@ CONFIG_SPI=y # Enable C++ support CONFIG_CPP=y +# Enable SMF (state machine framework) +CONFIG_SMF=y + # Enable BMI08X sensor driver CONFIG_BMI08X=y CONFIG_BMI08X_ACCEL_TRIGGER_NONE=y @@ -52,4 +55,4 @@ CONFIG_DEBUG_THREAD_INFO=y CONFIG_THREAD_MONITOR=y CONFIG_THREAD_NAME=y CONFIG_THREAD_STACK_INFO=n -CONFIG_STACK_SENTINEL=y \ No newline at end of file +CONFIG_STACK_SENTINEL=y diff --git a/apps/rockets/cloudburst/src/data.c b/apps/rockets/cloudburst/src/data.c index 1bf0b5e..b65bff0 100644 --- a/apps/rockets/cloudburst/src/data.c +++ b/apps/rockets/cloudburst/src/data.c @@ -7,10 +7,12 @@ LOG_MODULE_REGISTER(data, LOG_LEVEL_INF); // Global instances struct imu_data g_imu_data; struct baro_data g_baro_data; +struct state_data g_state_data; // Mutexes for thread safety K_MUTEX_DEFINE(imu_mutex); K_MUTEX_DEFINE(baro_mutex); +K_MUTEX_DEFINE(state_mutex); // Setter functions void set_imu_data(const struct imu_data *src) @@ -41,3 +43,17 @@ void get_baro_data(struct baro_data *dst) *dst = g_baro_data; k_mutex_unlock(&baro_mutex); } + +void set_state_data(const struct state_data *src) +{ + k_mutex_lock(&state_mutex, K_FOREVER); + g_state_data = *src; + k_mutex_unlock(&state_mutex); +} + +void get_state_data(struct state_data *dst) +{ + k_mutex_lock(&state_mutex, K_FOREVER); + *dst = g_state_data; + k_mutex_unlock(&state_mutex); +} diff --git a/apps/rockets/cloudburst/src/data.h b/apps/rockets/cloudburst/src/data.h index c30f1e9..faa252e 100644 --- a/apps/rockets/cloudburst/src/data.h +++ b/apps/rockets/cloudburst/src/data.h @@ -3,6 +3,16 @@ #include +typedef enum +{ + FLIGHT_STATE_STANDBY = 0, + FLIGHT_STATE_ASCENT, + FLIGHT_STATE_MACH_LOCK, + FLIGHT_STATE_DROGUE_DESCENT, + FLIGHT_STATE_MAIN_DESCENT, + FLIGHT_STATE_LANDED, +} flight_state_id_t; + // Data structures for sensor data struct imu_data { @@ -36,10 +46,17 @@ struct baro_data int64_t timestamp; // Timestamp in milliseconds }; +struct state_data +{ + flight_state_id_t state; + float ground_altitude; + int64_t timestamp; +}; // Global instances extern struct imu_data g_imu_data; extern struct baro_data g_baro_data; +extern struct state_data g_state_data; // Getters and setters void set_imu_data(const struct imu_data *src); @@ -48,4 +65,7 @@ void get_imu_data(struct imu_data *dst); void set_baro_data(const struct baro_data *src); void get_baro_data(struct baro_data *dst); -#endif \ No newline at end of file +void set_state_data(const struct state_data *src); +void get_state_data(struct state_data *dst); + +#endif diff --git a/apps/rockets/cloudburst/src/log_format.h b/apps/rockets/cloudburst/src/log_format.h index 1089c7d..583af42 100644 --- a/apps/rockets/cloudburst/src/log_format.h +++ b/apps/rockets/cloudburst/src/log_format.h @@ -10,6 +10,7 @@ struct log_frame struct imu_data imu; struct baro_data baro; + struct state_data state; }; #endif diff --git a/apps/rockets/cloudburst/src/logger_thread.c b/apps/rockets/cloudburst/src/logger_thread.c index 3e000fa..d637ea6 100644 --- a/apps/rockets/cloudburst/src/logger_thread.c +++ b/apps/rockets/cloudburst/src/logger_thread.c @@ -60,7 +60,8 @@ static int write_csv_header(void) { "Baro_Timestamp(ms)," "Baro0_Pressure(Pa),Baro0_Temperature(C),Baro0_Altitude(m),Baro0_NIS,Baro0_Faults,Baro0_Healthy," "Baro1_Pressure(Pa),Baro1_Temperature(C),Baro1_Altitude(m),Baro1_NIS,Baro1_Faults,Baro1_Healthy," - "KF_Altitude(m),KF_AltVar,KF_Velocity(m/s),KF_VelVar\n"; + "KF_Altitude(m),KF_AltVar,KF_Velocity(m/s),KF_VelVar," + "State,State_Ground_Altitude(m),State_Timestamp(ms)\n"; int ret; // Write the header row to the log file @@ -133,7 +134,7 @@ static int format_log_entry(const struct log_frame *frame, char *buffer, size_t "%lld,%lld,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f,%lld," "%.3f,%.3f,%.3f,%.3f,%u,%d," "%.3f,%.3f,%.3f,%.3f,%u,%d," - "%.3f,%.3f,%.3f,%.3f\n", + "%.3f,%.3f,%.3f,%.3f,%d,%.3f,%lld\n", frame->log_timestamp, frame->imu.timestamp, // IMU timestamp (double)frame->imu.accel[0], @@ -158,7 +159,10 @@ static int format_log_entry(const struct log_frame *frame, char *buffer, size_t (double)frame->baro.altitude, (double)frame->baro.alt_variance, (double)frame->baro.velocity, - (double)frame->baro.vel_variance); + (double)frame->baro.vel_variance, + (int)frame->state.state, + (double)frame->state.ground_altitude, + frame->state.timestamp); } static void write_log_frame_to_file(const struct log_frame *frame) { @@ -204,6 +208,7 @@ static void logger_thread_fn(void *p1, void *p2, void *p3) { frame.log_timestamp = k_uptime_get(); get_imu_data(&frame.imu); get_baro_data(&frame.baro); + get_state_data(&frame.state); // Write the data to the log file write_log_frame_to_file(&frame); diff --git a/apps/rockets/cloudburst/src/main.c b/apps/rockets/cloudburst/src/main.c index bfa0e28..c525456 100644 --- a/apps/rockets/cloudburst/src/main.c +++ b/apps/rockets/cloudburst/src/main.c @@ -6,6 +6,7 @@ #include "sensors/imu_thread.h" #include "sensors/baro_thread.h" #include "logger_thread.h" +#include "state_machine/state_machine.h" #include "data.h" LOG_MODULE_REGISTER(falcon_main, LOG_LEVEL_INF); @@ -18,6 +19,7 @@ int main(void) start_imu_thread(); start_logger_thread(); start_baro_thread(); + start_state_machine_thread(); return 0; } diff --git a/apps/rockets/cloudburst/src/state_machine/state_machine.c b/apps/rockets/cloudburst/src/state_machine/state_machine.c new file mode 100644 index 0000000..52ed77e --- /dev/null +++ b/apps/rockets/cloudburst/src/state_machine/state_machine.c @@ -0,0 +1,156 @@ +#include + +#include +#include +#include + +#include "state_machine.h" +#include "state_machine_internal.h" +#include "state_machine_states.h" + +LOG_MODULE_REGISTER(state_machine, LOG_LEVEL_INF); + +#define STATE_THREAD_STACK_SIZE 2048 +#define STATE_THREAD_PRIORITY 5 +#define STATE_THREAD_PERIOD_MS 20 + +static K_THREAD_STACK_DEFINE(state_stack, STATE_THREAD_STACK_SIZE); +static struct k_thread state_thread; +static struct flight_sm state_machine; + +static const struct smf_state flight_states[]; +static void state_machine_reset(int64_t start_ms); + +/** + * @brief Transition the SMF context to a new state with logging. + */ +void transition_to(struct flight_sm *sm, flight_state_id_t next_state) +{ + if (next_state == sm->current_id) { + return; + } + + LOG_INF("State change: %s -> %s", + flight_state_to_string(sm->current_id), + flight_state_to_string(next_state)); + smf_set_state(SMF_CTX(sm), &flight_states[next_state]); +} + +static const struct smf_state flight_states[] = { + [FLIGHT_STATE_STANDBY] = + SMF_CREATE_STATE(state_standby_entry, state_standby_run, NULL, NULL, NULL), + [FLIGHT_STATE_ASCENT] = + SMF_CREATE_STATE(state_ascent_entry, state_ascent_run, NULL, NULL, NULL), + [FLIGHT_STATE_MACH_LOCK] = + SMF_CREATE_STATE(state_mach_lock_entry, state_mach_lock_run, NULL, NULL, NULL), + [FLIGHT_STATE_DROGUE_DESCENT] = + SMF_CREATE_STATE(state_drogue_descent_entry, state_drogue_descent_run, NULL, NULL, NULL), + [FLIGHT_STATE_MAIN_DESCENT] = + SMF_CREATE_STATE(state_main_descent_entry, state_main_descent_run, NULL, NULL, NULL), + [FLIGHT_STATE_LANDED] = + SMF_CREATE_STATE(state_landed_entry, state_landed_run, NULL, NULL, NULL), +}; + +/** + * @brief State machine thread loop that drives SMF with baro samples. + */ +static void state_machine_thread_fn(void *p1, void *p2, void *p3) +{ + struct baro_data baro; + + while (1) { + get_baro_data(&baro); + int64_t now_ms = (baro.timestamp > 0) ? baro.timestamp : k_uptime_get(); + + state_machine.sample.altitude_m = baro.altitude; + state_machine.sample.velocity_mps = baro.velocity; + state_machine.sample.timestamp_ms = now_ms; + + smf_run_state(SMF_CTX(&state_machine)); + flight_state_id_t current = state_machine.current_id; + + struct state_data data = { + .state = current, + .ground_altitude = state_machine.ground_altitude_m, + .timestamp = now_ms, + }; + set_state_data(&data); + + k_sleep(K_MSEC(STATE_THREAD_PERIOD_MS)); + } +} + +/** + * @brief Start the state machine thread and initialize SMF. + */ +void start_state_machine_thread(void) +{ + state_machine_reset(k_uptime_get()); + + k_thread_create( + &state_thread, + state_stack, + K_THREAD_STACK_SIZEOF(state_stack), + state_machine_thread_fn, + NULL, NULL, NULL, + STATE_THREAD_PRIORITY, + 0, + K_NO_WAIT + ); +} + +#if defined(CONFIG_ZTEST) +void state_machine_test_reset(int64_t start_ms) +{ + state_machine_reset(start_ms); +} + +void state_machine_test_step(float altitude_m, float velocity_mps, int64_t timestamp_ms) +{ + state_machine.sample.altitude_m = altitude_m; + state_machine.sample.velocity_mps = velocity_mps; + state_machine.sample.timestamp_ms = timestamp_ms; + smf_run_state(SMF_CTX(&state_machine)); + + struct state_data data = { + .state = state_machine.current_id, + .ground_altitude = state_machine.ground_altitude_m, + .timestamp = timestamp_ms, + }; + set_state_data(&data); +} + +void state_machine_test_setup_state(flight_state_id_t state, float ground_altitude_m, int64_t timestamp_ms) +{ + state_machine_reset(timestamp_ms); + state_machine.ground_altitude_m = ground_altitude_m; + state_machine.ground_ready = true; + state_machine.sample.timestamp_ms = timestamp_ms; + transition_to(&state_machine, state); +} + +flight_state_id_t state_machine_test_get_state(void) +{ + return state_machine.current_id; +} + +float state_machine_test_get_ground_altitude(void) +{ + return state_machine.ground_altitude_m; +} + +bool state_machine_test_get_drogue_fire_triggered(void) +{ + return state_machine.drogue_fire_triggered; +} +#endif + +/** + * @brief Reset and initialize the state machine context. + */ +static void state_machine_reset(int64_t start_ms) +{ + memset(&state_machine, 0, sizeof(state_machine)); + state_machine.sample.timestamp_ms = start_ms; + smf_set_initial(SMF_CTX(&state_machine), &flight_states[FLIGHT_STATE_STANDBY]); +} diff --git a/apps/rockets/cloudburst/src/state_machine/state_machine.h b/apps/rockets/cloudburst/src/state_machine/state_machine.h new file mode 100644 index 0000000..0ca048a --- /dev/null +++ b/apps/rockets/cloudburst/src/state_machine/state_machine.h @@ -0,0 +1,9 @@ +#ifndef STATE_MACHINE_H +#define STATE_MACHINE_H + +#include "data.h" + +void start_state_machine_thread(void); +const char *flight_state_to_string(flight_state_id_t state); + +#endif diff --git a/apps/rockets/cloudburst/src/state_machine/state_machine_common.c b/apps/rockets/cloudburst/src/state_machine/state_machine_common.c new file mode 100644 index 0000000..00c085f --- /dev/null +++ b/apps/rockets/cloudburst/src/state_machine/state_machine_common.c @@ -0,0 +1,108 @@ +#include + +#include "state_machine_internal.h" + +LOG_MODULE_DECLARE(state_machine); + +/** + * @brief Update a repeated-check counter and report when it reaches the threshold. + */ +bool repeated_check_update(repeated_check_t *check, bool condition, uint8_t required) +{ + if (condition) { + if (check->count < 255) { + check->count++; + } + } else { + check->count = 0; + } + + return check->count >= required; +} + +/** + * @brief Clear a repeated-check counter. + */ +void reset_repeated_check(repeated_check_t *check) +{ + check->count = 0; +} + +/** + * @brief Reset ground altitude averaging state. + */ +void reset_ground_average(struct flight_sm *sm) +{ + sm->ground_altitude_m = 0.0f; + sm->ground_sum_m = 0.0f; + sm->ground_samples = 0; + sm->ground_ready = false; + sm->ground_warmup_start_ms = sm->sample.timestamp_ms; +} + +/** + * @brief Convert an absolute altitude to altitude relative to ground baseline. + */ +float get_relative_altitude(const struct flight_sm *sm, float altitude_m) +{ + return altitude_m - sm->ground_altitude_m; +} + +/** + * @brief Trigger drogue deployment action. + */ +void state_action_fire_drogue(void) +{ + LOG_INF("Drogue deployment triggered"); + // TODO: Signal drogue deployment here +} + +/** + * @brief Trigger main parachute deployment action. + */ +void state_action_fire_main(void) +{ + LOG_INF("Main deployment triggered"); + // TODO: Signal main deployment here +} + +/** + * @brief Trigger landed action. + */ +void state_action_landed(void) +{ + LOG_INF("The rocket has landed"); + // TODO: Need to do anything upon landing? +} + +/** + * @brief Return a human-readable string for a flight state. + */ +const char *flight_state_to_string(flight_state_id_t state) +{ + switch (state) { + case FLIGHT_STATE_STANDBY: + return "STANDBY"; + case FLIGHT_STATE_ASCENT: + return "ASCENT"; + case FLIGHT_STATE_MACH_LOCK: + return "MACH_LOCK"; + case FLIGHT_STATE_DROGUE_DESCENT: + return "DROGUE_DESCENT"; + case FLIGHT_STATE_MAIN_DESCENT: + return "MAIN_DESCENT"; + case FLIGHT_STATE_LANDED: + return "LANDED"; + default: + return "UNKNOWN"; + } +} + +/** + * @brief Common entry bookkeeping shared by all states. + */ +void state_entry_common(struct flight_sm *sm, flight_state_id_t state) +{ + sm->current_id = state; + sm->entry_time_ms = sm->sample.timestamp_ms; +} diff --git a/apps/rockets/cloudburst/src/state_machine/state_machine_config.h b/apps/rockets/cloudburst/src/state_machine/state_machine_config.h new file mode 100644 index 0000000..21c923b --- /dev/null +++ b/apps/rockets/cloudburst/src/state_machine/state_machine_config.h @@ -0,0 +1,33 @@ +#ifndef STATE_MACHINE_CONFIG_H +#define STATE_MACHINE_CONFIG_H + +// Standby baseline +#define GROUND_AVERAGE_SAMPLES 100 +#define GROUND_WARMUP_MS 2000 + +// Ascent detection +#define ASCENT_ALTITUDE_THRESHOLD_M 25.0f +#define ASCENT_VELOCITY_THRESHOLD_MPS 5.0f +#define ASCENT_CHECKS 5 + +// Mach lock +#define MACH_LOCK_VELOCITY_THRESHOLD_MPS 150.0f +#define MACH_LOCK_CHECKS 10 +#define MACH_UNLOCK_VELOCITY_THRESHOLD_MPS 150.0f +#define MACH_UNLOCK_CHECKS 10 + +// Drogue deployment +#define DROGUE_DEPLOY_VELOCITY_THRESHOLD_MPS 5.0f +#define DROGUE_DEPLOY_CHECKS 5 +#define DROGUE_DEPLOY_DELAY_MS 3000 + +// Main deployment +#define MAIN_DEPLOY_ALTITUDE_M 488.0f +#define MAIN_DEPLOY_CHECKS 5 + +// Landing detection +#define LANDED_VELOCITY_THRESHOLD_MPS 4.0f +#define LANDED_CHECKS 6 +#define LANDED_CHECK_INTERVAL_MS 10000 + +#endif diff --git a/apps/rockets/cloudburst/src/state_machine/state_machine_internal.h b/apps/rockets/cloudburst/src/state_machine/state_machine_internal.h new file mode 100644 index 0000000..2de0df6 --- /dev/null +++ b/apps/rockets/cloudburst/src/state_machine/state_machine_internal.h @@ -0,0 +1,57 @@ +#ifndef STATE_MACHINE_INTERNAL_H +#define STATE_MACHINE_INTERNAL_H + +#include +#include + +#include + +#include "data.h" +#include "state_machine_config.h" + +typedef struct +{ + uint8_t count; +} repeated_check_t; + +typedef struct +{ + float altitude_m; + float velocity_mps; + int64_t timestamp_ms; +} state_sample_t; + +struct flight_sm +{ + struct smf_ctx ctx; + flight_state_id_t current_id; + int64_t entry_time_ms; + state_sample_t sample; + float ground_altitude_m; + float ground_sum_m; + uint8_t ground_samples; + bool ground_ready; + int64_t ground_warmup_start_ms; + repeated_check_t standby_check; + repeated_check_t mach_lock_check; + repeated_check_t mach_unlock_check; + repeated_check_t drogue_main_check; + repeated_check_t landed_check; + int64_t last_landed_check_ms; + bool drogue_fire_triggered; +}; + +bool repeated_check_update(repeated_check_t *check, bool condition, uint8_t required); +void reset_repeated_check(repeated_check_t *check); +void reset_ground_average(struct flight_sm *sm); +float get_relative_altitude(const struct flight_sm *sm, float altitude_m); + +void state_action_fire_drogue(void); +void state_action_fire_main(void); +void state_action_landed(void); + +const char *flight_state_to_string(flight_state_id_t state); +void state_entry_common(struct flight_sm *sm, flight_state_id_t state); +void transition_to(struct flight_sm *sm, flight_state_id_t next_state); + +#endif diff --git a/apps/rockets/cloudburst/src/state_machine/state_machine_states.h b/apps/rockets/cloudburst/src/state_machine/state_machine_states.h new file mode 100644 index 0000000..0e2fe98 --- /dev/null +++ b/apps/rockets/cloudburst/src/state_machine/state_machine_states.h @@ -0,0 +1,24 @@ +#ifndef STATE_MACHINE_STATES_H +#define STATE_MACHINE_STATES_H + +#include + +void state_standby_entry(void *obj); +enum smf_state_result state_standby_run(void *obj); + +void state_ascent_entry(void *obj); +enum smf_state_result state_ascent_run(void *obj); + +void state_mach_lock_entry(void *obj); +enum smf_state_result state_mach_lock_run(void *obj); + +void state_drogue_descent_entry(void *obj); +enum smf_state_result state_drogue_descent_run(void *obj); + +void state_main_descent_entry(void *obj); +enum smf_state_result state_main_descent_run(void *obj); + +void state_landed_entry(void *obj); +enum smf_state_result state_landed_run(void *obj); + +#endif diff --git a/apps/rockets/cloudburst/src/state_machine/state_machine_test.h b/apps/rockets/cloudburst/src/state_machine/state_machine_test.h new file mode 100644 index 0000000..174f741 --- /dev/null +++ b/apps/rockets/cloudburst/src/state_machine/state_machine_test.h @@ -0,0 +1,18 @@ +#ifndef STATE_MACHINE_TEST_H +#define STATE_MACHINE_TEST_H + +#include +#include + +#include "data.h" + +#ifdef CONFIG_ZTEST +void state_machine_test_reset(int64_t start_ms); +void state_machine_test_step(float altitude_m, float velocity_mps, int64_t timestamp_ms); +void state_machine_test_setup_state(flight_state_id_t state, float ground_altitude_m, int64_t timestamp_ms); +flight_state_id_t state_machine_test_get_state(void); +float state_machine_test_get_ground_altitude(void); +bool state_machine_test_get_drogue_fire_triggered(void); +#endif + +#endif diff --git a/apps/rockets/cloudburst/src/state_machine/states/ascent.c b/apps/rockets/cloudburst/src/state_machine/states/ascent.c new file mode 100644 index 0000000..94b0bf4 --- /dev/null +++ b/apps/rockets/cloudburst/src/state_machine/states/ascent.c @@ -0,0 +1,44 @@ +#include "state_machine_internal.h" +#include "state_machine_states.h" + +/** + * @brief Evaluate transitions while in ascent. + */ +static flight_state_id_t update_ascent(struct flight_sm *sm, const state_sample_t *sample) +{ + bool mach_lock = sample->velocity_mps > MACH_LOCK_VELOCITY_THRESHOLD_MPS; + + if (repeated_check_update(&sm->mach_lock_check, mach_lock, MACH_LOCK_CHECKS)) { + return FLIGHT_STATE_MACH_LOCK; + } + + bool drogue = sample->velocity_mps < DROGUE_DEPLOY_VELOCITY_THRESHOLD_MPS; + if (repeated_check_update(&sm->drogue_main_check, drogue, DROGUE_DEPLOY_CHECKS)) { + return FLIGHT_STATE_DROGUE_DESCENT; + } + + return FLIGHT_STATE_ASCENT; +} + +/** + * @brief SMF entry handler for ascent state. + */ +void state_ascent_entry(void *obj) +{ + struct flight_sm *sm = obj; + + state_entry_common(sm, FLIGHT_STATE_ASCENT); + reset_repeated_check(&sm->mach_lock_check); + reset_repeated_check(&sm->drogue_main_check); +} + +/** + * @brief SMF run handler for ascent state. + */ +enum smf_state_result state_ascent_run(void *obj) +{ + struct flight_sm *sm = obj; + + transition_to(sm, update_ascent(sm, &sm->sample)); + return SMF_EVENT_HANDLED; +} diff --git a/apps/rockets/cloudburst/src/state_machine/states/drogue_descent.c b/apps/rockets/cloudburst/src/state_machine/states/drogue_descent.c new file mode 100644 index 0000000..53cb195 --- /dev/null +++ b/apps/rockets/cloudburst/src/state_machine/states/drogue_descent.c @@ -0,0 +1,46 @@ +#include "state_machine_internal.h" +#include "state_machine_states.h" + +/** + * @brief Evaluate transitions while in drogue descent. + */ +static flight_state_id_t update_drogue_descent(struct flight_sm *sm, const state_sample_t *sample) +{ + float rel_altitude = get_relative_altitude(sm, sample->altitude_m); + bool below_main_alt = rel_altitude < MAIN_DEPLOY_ALTITUDE_M; + + if (repeated_check_update(&sm->drogue_main_check, below_main_alt, MAIN_DEPLOY_CHECKS)) { + return FLIGHT_STATE_MAIN_DESCENT; + } + + return FLIGHT_STATE_DROGUE_DESCENT; +} + +/** + * @brief SMF entry handler for drogue descent state. + */ +void state_drogue_descent_entry(void *obj) +{ + struct flight_sm *sm = obj; + + state_entry_common(sm, FLIGHT_STATE_DROGUE_DESCENT); + reset_repeated_check(&sm->drogue_main_check); + sm->drogue_fire_triggered = false; +} + +/** + * @brief SMF run handler for drogue descent state. + */ +enum smf_state_result state_drogue_descent_run(void *obj) +{ + struct flight_sm *sm = obj; + + if (!sm->drogue_fire_triggered && + (sm->sample.timestamp_ms - sm->entry_time_ms) >= DROGUE_DEPLOY_DELAY_MS) { + state_action_fire_drogue(); + sm->drogue_fire_triggered = true; + } + + transition_to(sm, update_drogue_descent(sm, &sm->sample)); + return SMF_EVENT_HANDLED; +} diff --git a/apps/rockets/cloudburst/src/state_machine/states/landed.c b/apps/rockets/cloudburst/src/state_machine/states/landed.c new file mode 100644 index 0000000..141ed92 --- /dev/null +++ b/apps/rockets/cloudburst/src/state_machine/states/landed.c @@ -0,0 +1,32 @@ +#include "state_machine_internal.h" +#include "state_machine_states.h" + +/** + * @brief Maintain the terminal landed state. + */ +static flight_state_id_t update_landed(void) +{ + return FLIGHT_STATE_LANDED; +} + +/** + * @brief SMF entry handler for landed state. + */ +void state_landed_entry(void *obj) +{ + struct flight_sm *sm = obj; + + state_entry_common(sm, FLIGHT_STATE_LANDED); + state_action_landed(); +} + +/** + * @brief SMF run handler for landed state. + */ +enum smf_state_result state_landed_run(void *obj) +{ + struct flight_sm *sm = obj; + + transition_to(sm, update_landed()); + return SMF_EVENT_HANDLED; +} diff --git a/apps/rockets/cloudburst/src/state_machine/states/mach_lock.c b/apps/rockets/cloudburst/src/state_machine/states/mach_lock.c new file mode 100644 index 0000000..c51051a --- /dev/null +++ b/apps/rockets/cloudburst/src/state_machine/states/mach_lock.c @@ -0,0 +1,38 @@ +#include "state_machine_internal.h" +#include "state_machine_states.h" + +/** + * @brief Evaluate transitions while in mach lock. + */ +static flight_state_id_t update_mach_lock(struct flight_sm *sm, const state_sample_t *sample) +{ + bool below_unlock = sample->velocity_mps < MACH_UNLOCK_VELOCITY_THRESHOLD_MPS; + + if (repeated_check_update(&sm->mach_unlock_check, below_unlock, MACH_UNLOCK_CHECKS)) { + return FLIGHT_STATE_ASCENT; + } + + return FLIGHT_STATE_MACH_LOCK; +} + +/** + * @brief SMF entry handler for mach lock state. + */ +void state_mach_lock_entry(void *obj) +{ + struct flight_sm *sm = obj; + + state_entry_common(sm, FLIGHT_STATE_MACH_LOCK); + reset_repeated_check(&sm->mach_unlock_check); +} + +/** + * @brief SMF run handler for mach lock state. + */ +enum smf_state_result state_mach_lock_run(void *obj) +{ + struct flight_sm *sm = obj; + + transition_to(sm, update_mach_lock(sm, &sm->sample)); + return SMF_EVENT_HANDLED; +} diff --git a/apps/rockets/cloudburst/src/state_machine/states/main_descent.c b/apps/rockets/cloudburst/src/state_machine/states/main_descent.c new file mode 100644 index 0000000..fddf846 --- /dev/null +++ b/apps/rockets/cloudburst/src/state_machine/states/main_descent.c @@ -0,0 +1,51 @@ +#include + +#include "state_machine_internal.h" +#include "state_machine_states.h" + +/** + * @brief Evaluate transitions while in main descent. + */ +static flight_state_id_t update_main_descent(struct flight_sm *sm, const state_sample_t *sample) +{ + bool landed = fabsf(sample->velocity_mps) < LANDED_VELOCITY_THRESHOLD_MPS; + + if (landed) { + int64_t elapsed = sample->timestamp_ms - sm->last_landed_check_ms; + if (elapsed >= LANDED_CHECK_INTERVAL_MS) { + sm->last_landed_check_ms = sample->timestamp_ms; + if (repeated_check_update(&sm->landed_check, true, LANDED_CHECKS)) { + return FLIGHT_STATE_LANDED; + } + } + } else { + repeated_check_update(&sm->landed_check, false, LANDED_CHECKS); + sm->last_landed_check_ms = sample->timestamp_ms; + } + + return FLIGHT_STATE_MAIN_DESCENT; +} + +/** + * @brief SMF entry handler for main descent state. + */ +void state_main_descent_entry(void *obj) +{ + struct flight_sm *sm = obj; + + state_entry_common(sm, FLIGHT_STATE_MAIN_DESCENT); + reset_repeated_check(&sm->landed_check); + sm->last_landed_check_ms = sm->sample.timestamp_ms; + state_action_fire_main(); +} + +/** + * @brief SMF run handler for main descent state. + */ +enum smf_state_result state_main_descent_run(void *obj) +{ + struct flight_sm *sm = obj; + + transition_to(sm, update_main_descent(sm, &sm->sample)); + return SMF_EVENT_HANDLED; +} diff --git a/apps/rockets/cloudburst/src/state_machine/states/standby.c b/apps/rockets/cloudburst/src/state_machine/states/standby.c new file mode 100644 index 0000000..1cca0ab --- /dev/null +++ b/apps/rockets/cloudburst/src/state_machine/states/standby.c @@ -0,0 +1,54 @@ +#include "state_machine_internal.h" +#include "state_machine_states.h" + +/** + * @brief Evaluate transitions while in standby (includes ground averaging). + */ +static flight_state_id_t update_standby(struct flight_sm *sm, const state_sample_t *sample) +{ + if (!sm->ground_ready) { + if ((sample->timestamp_ms - sm->ground_warmup_start_ms) < GROUND_WARMUP_MS) { + return FLIGHT_STATE_STANDBY; + } + sm->ground_sum_m += sample->altitude_m; + sm->ground_samples++; + if (sm->ground_samples >= GROUND_AVERAGE_SAMPLES) { + sm->ground_altitude_m = sm->ground_sum_m / (float)sm->ground_samples; + sm->ground_ready = true; + } + return FLIGHT_STATE_STANDBY; + } + + float rel_altitude = get_relative_altitude(sm, sample->altitude_m); + bool ascent_condition = (rel_altitude > ASCENT_ALTITUDE_THRESHOLD_M) && + (sample->velocity_mps > ASCENT_VELOCITY_THRESHOLD_MPS); + + if (repeated_check_update(&sm->standby_check, ascent_condition, ASCENT_CHECKS)) { + return FLIGHT_STATE_ASCENT; + } + + return FLIGHT_STATE_STANDBY; +} + +/** + * @brief SMF entry handler for standby state. + */ +void state_standby_entry(void *obj) +{ + struct flight_sm *sm = obj; + + state_entry_common(sm, FLIGHT_STATE_STANDBY); + reset_repeated_check(&sm->standby_check); + reset_ground_average(sm); +} + +/** + * @brief SMF run handler for standby state. + */ +enum smf_state_result state_standby_run(void *obj) +{ + struct flight_sm *sm = obj; + + transition_to(sm, update_standby(sm, &sm->sample)); + return SMF_EVENT_HANDLED; +} diff --git a/apps/rockets/cloudburst/tests/state_machine/CMakeLists.txt b/apps/rockets/cloudburst/tests/state_machine/CMakeLists.txt new file mode 100644 index 0000000..b5b419f --- /dev/null +++ b/apps/rockets/cloudburst/tests/state_machine/CMakeLists.txt @@ -0,0 +1,26 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) + +set(BOARD_ROOT ${CMAKE_CURRENT_LIST_DIR}/../../../../..) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(state_machine_test) + +target_sources(app PRIVATE + ../../src/state_machine/state_machine.c + ../../src/state_machine/state_machine_common.c + ../../src/state_machine/states/standby.c + ../../src/state_machine/states/ascent.c + ../../src/state_machine/states/mach_lock.c + ../../src/state_machine/states/drogue_descent.c + ../../src/state_machine/states/main_descent.c + ../../src/state_machine/states/landed.c + ../../src/data.c + src/main.c +) + +target_include_directories(app PRIVATE + ../../src + ../../src/state_machine +) diff --git a/apps/rockets/cloudburst/tests/state_machine/prj.conf b/apps/rockets/cloudburst/tests/state_machine/prj.conf new file mode 100644 index 0000000..1487959 --- /dev/null +++ b/apps/rockets/cloudburst/tests/state_machine/prj.conf @@ -0,0 +1,10 @@ +# Enable ztest framework +CONFIG_ZTEST=y + +# Enable SMF and logging used by the state machine module +CONFIG_SMF=y +CONFIG_LOG=y +CONFIG_LOG_MODE_IMMEDIATE=y + +CONFIG_CBPRINTF_FP_SUPPORT=y + diff --git a/apps/rockets/cloudburst/tests/state_machine/src/main.c b/apps/rockets/cloudburst/tests/state_machine/src/main.c new file mode 100644 index 0000000..c4eb578 --- /dev/null +++ b/apps/rockets/cloudburst/tests/state_machine/src/main.c @@ -0,0 +1,359 @@ +#include +#include + +#include "data.h" +#include "state_machine_config.h" +#include "state_machine_test.h" + +LOG_MODULE_REGISTER(state_machine_test, LOG_LEVEL_INF); + +/** + * @brief Helper to advance through standby warmup and ground averaging. + * @return timestamp after ground averaging is complete + */ +static int64_t complete_standby_setup(float ground_altitude) +{ + int64_t t = 0; + + // Step through warmup period - state machine won't collect samples during warmup + for (int i = 0; i < GROUND_WARMUP_MS / 100; i++) { + state_machine_test_step(ground_altitude, 0.0f, t); + t += 100; + } + + // Now collect ground samples (warmup is complete) + for (int i = 0; i < GROUND_AVERAGE_SAMPLES; i++) { + state_machine_test_step(ground_altitude, 0.0f, t); + t += 100; + } + + // Verify ground altitude is calculated + zassert_within(state_machine_test_get_ground_altitude(), ground_altitude, 0.001f, + "ground altitude should match average"); + zassert_equal(state_machine_test_get_state(), FLIGHT_STATE_STANDBY, + "should still be in standby after ground averaging"); + + return t; +} + +/** + * @brief Transition from standby to ascent state. + * Assumes state machine is already in STANDBY state with ground altitude set. + * @param ground_altitude Ground altitude for relative altitude calculations + * @param start_t Starting timestamp for the transition + * @return timestamp after entering ascent + */ +static int64_t transition_to_ascent(float ground_altitude, int64_t start_t) +{ + int64_t t = start_t; + + // Transition to ascent: need relative altitude > threshold AND velocity > threshold + float ascent_alt = ground_altitude + ASCENT_ALTITUDE_THRESHOLD_M + 1.0f; + float ascent_vel = ASCENT_VELOCITY_THRESHOLD_MPS + 1.0f; + + for (int i = 0; i < ASCENT_CHECKS; i++) { + state_machine_test_step(ascent_alt, ascent_vel, t); + t += 100; + } + + zassert_equal(state_machine_test_get_state(), FLIGHT_STATE_ASCENT, + "expected ascent after ascent checks"); + + return t; +} + +/** + * @brief Transition from ascent to drogue descent state. + * Assumes state machine is already in ASCENT state. + * @param ground_altitude Ground altitude for relative altitude calculations + * @param start_t Starting timestamp for the transition + * @return timestamp after entering drogue descent + */ +static int64_t transition_to_drogue_descent(float ground_altitude, int64_t start_t) +{ + int64_t t = start_t; + + // Transition to drogue descent: velocity must be below threshold + float current_alt = ground_altitude + ASCENT_ALTITUDE_THRESHOLD_M + 1.0f; + float drogue_vel = DROGUE_DEPLOY_VELOCITY_THRESHOLD_MPS - 1.0f; + + for (int i = 0; i < DROGUE_DEPLOY_CHECKS; i++) { + state_machine_test_step(current_alt, drogue_vel, t); + t += 100; + } + + zassert_equal(state_machine_test_get_state(), FLIGHT_STATE_DROGUE_DESCENT, + "expected drogue descent after drogue checks"); + zassert_false(state_machine_test_get_drogue_fire_triggered(), + "drogue should not fire immediately upon entry"); + + return t; +} + +/** + * @brief Transition from ascent to mach lock state. + * Assumes state machine is already in ASCENT state. + * @param ground_altitude Ground altitude for relative altitude calculations + * @param start_t Starting timestamp for the transition + * @return timestamp after entering mach lock + */ +static int64_t transition_to_mach_lock(float ground_altitude, int64_t start_t) +{ + int64_t t = start_t; + float current_alt = ground_altitude + ASCENT_ALTITUDE_THRESHOLD_M + 1.0f; + + // Enter mach lock + float mach_vel = MACH_LOCK_VELOCITY_THRESHOLD_MPS + 1.0f; + for (int i = 0; i < MACH_LOCK_CHECKS; i++) { + state_machine_test_step(current_alt, mach_vel, t); + t += 100; + } + zassert_equal(state_machine_test_get_state(), FLIGHT_STATE_MACH_LOCK, + "expected mach lock after high velocity"); + + return t; +} + +/** + * @brief Transition from mach lock back to ascent state. + * Assumes state machine is already in MACH_LOCK state. + * @param ground_altitude Ground altitude for relative altitude calculations + * @param start_t Starting timestamp for the transition + * @return timestamp after exiting mach lock back to ascent + */ +static int64_t transition_from_mach_lock(float ground_altitude, int64_t start_t) +{ + int64_t t = start_t; + float current_alt = ground_altitude + ASCENT_ALTITUDE_THRESHOLD_M + 1.0f; + + // Exit mach lock + float unlock_vel = MACH_UNLOCK_VELOCITY_THRESHOLD_MPS - 1.0f; + for (int i = 0; i < MACH_UNLOCK_CHECKS; i++) { + state_machine_test_step(current_alt, unlock_vel, t); + t += 100; + } + zassert_equal(state_machine_test_get_state(), FLIGHT_STATE_ASCENT, + "expected ascent after mach unlock"); + + return t; +} + +/** + * @brief Transition from drogue descent to main descent state. + * Assumes state machine is already in DROGUE_DESCENT state. + * @param ground_altitude Ground altitude for relative altitude calculations + * @param start_t Starting timestamp (should be drogue entry time) + * @return timestamp after entering main descent + */ +static int64_t transition_to_main_descent(float ground_altitude, int64_t start_t) +{ + int64_t t = start_t; + // Transition to main descent: relative altitude must be below threshold + float main_alt = ground_altitude + MAIN_DEPLOY_ALTITUDE_M - 1.0f; + for (int i = 0; i < MAIN_DEPLOY_CHECKS; i++) { + state_machine_test_step(main_alt, 0.0f, t); + t += 100; + } + + zassert_equal(state_machine_test_get_state(), FLIGHT_STATE_MAIN_DESCENT, + "expected main descent after main deploy checks"); + + return t; +} + +/** + * @brief Transition from main descent to landed state. + * Assumes state machine is already in MAIN_DESCENT state. + * @param ground_altitude Ground altitude for relative altitude calculations + * @param start_t Starting timestamp (should be main descent entry time) + * @return timestamp after entering landed state + */ +static int64_t transition_to_landed(float ground_altitude, int64_t start_t) +{ + int64_t t = start_t; + int64_t main_entry_time = start_t; + + // The main descent entry sets last_landed_check_ms to entry time + // We need to wait for the first interval, then do LANDED_CHECKS spaced checks + float slow_vel = LANDED_VELOCITY_THRESHOLD_MPS - 1.0f; + float near_ground_alt = ground_altitude + 1.0f; + + // First check happens after interval + t = main_entry_time + LANDED_CHECK_INTERVAL_MS; + state_machine_test_step(near_ground_alt, slow_vel, t); + + // Remaining checks, each spaced by interval + for (int i = 1; i < LANDED_CHECKS; i++) { + t += LANDED_CHECK_INTERVAL_MS; + state_machine_test_step(near_ground_alt, slow_vel, t); + } + + zassert_equal(state_machine_test_get_state(), FLIGHT_STATE_LANDED, + "expected landed after spaced checks"); + + return t; +} + +ZTEST(state_machine, test_standby_ground_averaging) +{ + float ground_altitude = 100.0f; + + state_machine_test_reset(0); + zassert_equal(state_machine_test_get_state(), FLIGHT_STATE_STANDBY, + "should start in standby"); + + complete_standby_setup(ground_altitude); + + // Verify shared state data + struct state_data shared = {0}; + get_state_data(&shared); + zassert_equal(shared.state, FLIGHT_STATE_STANDBY, + "shared state should match state machine"); + zassert_within(shared.ground_altitude, ground_altitude, 0.001f, + "shared ground altitude should match average"); +} + +ZTEST(state_machine, test_standby_to_ascent) +{ + float ground_altitude = 100.0f; + int64_t t = 0; + + state_machine_test_reset(0); + t = complete_standby_setup(ground_altitude); + transition_to_ascent(ground_altitude, t); + + // Verify shared state data + struct state_data shared = {0}; + get_state_data(&shared); + zassert_equal(shared.state, FLIGHT_STATE_ASCENT, + "shared state should match state machine"); + zassert_within(shared.ground_altitude, ground_altitude, 0.001f, + "shared ground altitude should match average"); +} + +ZTEST(state_machine, test_mach_lock) +{ + float ground_altitude = 100.0f; + int64_t t = 0; + + state_machine_test_setup_state(FLIGHT_STATE_ASCENT, ground_altitude, t); + t = transition_to_mach_lock(ground_altitude, t); + transition_from_mach_lock(ground_altitude, t); +} + +ZTEST(state_machine, test_mach_lock_blocks_drogue) +{ + float ground_altitude = 100.0f; + int64_t t = 0; + + state_machine_test_setup_state(FLIGHT_STATE_ASCENT, ground_altitude, t); + t = transition_to_mach_lock(ground_altitude, t); + + // Verify we're in mach lock + zassert_equal(state_machine_test_get_state(), FLIGHT_STATE_MACH_LOCK, + "should be in mach lock"); + + // Try to trigger drogue descent with low velocity + // This should NOT work - must exit mach lock first + float current_alt = ground_altitude + ASCENT_ALTITUDE_THRESHOLD_M + 1.0f; + float drogue_vel = DROGUE_DEPLOY_VELOCITY_THRESHOLD_MPS - 1.0f; + + for (int i = 0; i < DROGUE_DEPLOY_CHECKS; i++) { + state_machine_test_step(current_alt, drogue_vel, t); + t += 100; + } + + // Should still be in mach lock, NOT drogue descent + zassert_equal(state_machine_test_get_state(), FLIGHT_STATE_MACH_LOCK, + "should remain in mach lock, cannot transition to drogue"); +} + +ZTEST(state_machine, test_ascent_to_drogue_descent) +{ + float ground_altitude = 100.0f; + int64_t t = 0; + + state_machine_test_setup_state(FLIGHT_STATE_ASCENT, ground_altitude, t); + transition_to_drogue_descent(ground_altitude, t); +} + +ZTEST(state_machine, test_drogue_delay) +{ + float ground_altitude = 100.0f; + int64_t t = 0; + int64_t drogue_entry_time = t; + + state_machine_test_setup_state(FLIGHT_STATE_DROGUE_DESCENT, ground_altitude, drogue_entry_time); + + // Before delay, drogue should not fire + t = drogue_entry_time + (DROGUE_DEPLOY_DELAY_MS / 2); + state_machine_test_step(ground_altitude + 1.0f, 0.0f, t); + zassert_false(state_machine_test_get_drogue_fire_triggered(), + "drogue should not fire before delay"); + + // After delay, drogue should fire + t = drogue_entry_time + DROGUE_DEPLOY_DELAY_MS; + state_machine_test_step(ground_altitude + 1.0f, 0.0f, t); + zassert_true(state_machine_test_get_drogue_fire_triggered(), + "drogue should fire after delay"); +} + +ZTEST(state_machine, test_drogue_to_main_descent) +{ + float ground_altitude = 100.0f; + int64_t t = 0; + + state_machine_test_setup_state(FLIGHT_STATE_DROGUE_DESCENT, ground_altitude, t); + transition_to_main_descent(ground_altitude, t); + + // Verify we're in main descent + zassert_equal(state_machine_test_get_state(), FLIGHT_STATE_MAIN_DESCENT, + "expected main descent after main deploy checks"); +} + +ZTEST(state_machine, test_main_to_landed) +{ + float ground_altitude = 100.0f; + int64_t t = 0; + + state_machine_test_setup_state(FLIGHT_STATE_MAIN_DESCENT, ground_altitude, t); + transition_to_landed(ground_altitude, t); +} + +ZTEST(state_machine, test_full_flight_sequence) +{ + float ground_altitude = 100.0f; + int64_t t = 0; + + state_machine_test_reset(t); + + // Standby: ground averaging + t = complete_standby_setup(ground_altitude); + zassert_equal(state_machine_test_get_state(), FLIGHT_STATE_STANDBY, "standby"); + + // Standby -> Ascent + t = transition_to_ascent(ground_altitude, t); + zassert_equal(state_machine_test_get_state(), FLIGHT_STATE_ASCENT, "ascent"); + + // Ascent -> Mach Lock + t = transition_to_mach_lock(ground_altitude, t); + zassert_equal(state_machine_test_get_state(), FLIGHT_STATE_MACH_LOCK, "mach lock"); + + // Mach Lock -> Ascent + t = transition_from_mach_lock(ground_altitude, t); + zassert_equal(state_machine_test_get_state(), FLIGHT_STATE_ASCENT, "ascent after mach"); + + // Ascent -> Drogue Descent + t = transition_to_drogue_descent(ground_altitude, t); + zassert_equal(state_machine_test_get_state(), FLIGHT_STATE_DROGUE_DESCENT, "drogue"); + + // Drogue Descent -> Main Descent + t = transition_to_main_descent(ground_altitude, t); + zassert_equal(state_machine_test_get_state(), FLIGHT_STATE_MAIN_DESCENT, "main"); + + // Main Descent -> Landed + t = transition_to_landed(ground_altitude, t); + zassert_equal(state_machine_test_get_state(), FLIGHT_STATE_LANDED, "landed"); +} + +ZTEST_SUITE(state_machine, NULL, NULL, NULL, NULL, NULL); diff --git a/apps/rockets/cloudburst/tests/state_machine/testcase.yaml b/apps/rockets/cloudburst/tests/state_machine/testcase.yaml new file mode 100644 index 0000000..893790d --- /dev/null +++ b/apps/rockets/cloudburst/tests/state_machine/testcase.yaml @@ -0,0 +1,5 @@ +tests: + cloudburst.state_machine: + platform_allow: ubcrocket_fc_2526_r1 + tags: state_machine + type: unit