tan-cli 0.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tan/__init__.py +0 -0
- tan/__main__.py +120 -0
- tan/cli.py +939 -0
- tan/command_table.py +265 -0
- tan/commands/__init__.py +8 -0
- tan/commands/bootstrap_cmd.py +4856 -0
- tan/commands/bootstrap_patches.py +164 -0
- tan/commands/bootstrap_toolchain_fetch.py +155 -0
- tan/commands/bootstrap_toolchain_pin.py +36 -0
- tan/commands/build/__init__.py +1 -0
- tan/commands/build/configure_inputs.py +277 -0
- tan/commands/build/execute.py +1852 -0
- tan/commands/build/host_python.py +59 -0
- tan/commands/build/link_stale.py +78 -0
- tan/commands/build/manifest.py +428 -0
- tan/commands/build/materialise.py +81 -0
- tan/commands/build/token_substitution.py +381 -0
- tan/commands/build/toolchain.py +482 -0
- tan/commands/build/user_defines_wipe.py +115 -0
- tan/commands/build/workspace_patches.py +65 -0
- tan/commands/build_cmd.py +2236 -0
- tan/commands/build_output.py +331 -0
- tan/commands/clean_cmd.py +1183 -0
- tan/commands/completion_cmd.py +786 -0
- tan/commands/debug_config_cmd.py +2425 -0
- tan/commands/diff_cmd.py +940 -0
- tan/commands/doctor_cmd.py +4767 -0
- tan/commands/examples_cmd.py +644 -0
- tan/commands/explain_cmd.py +1319 -0
- tan/commands/faultdecode_cmd.py +931 -0
- tan/commands/flash_atoc_guard.py +240 -0
- tan/commands/flash_cmd.py +5731 -0
- tan/commands/flash_mram_guard.py +150 -0
- tan/commands/flash_ram.py +578 -0
- tan/commands/flash_raw.py +583 -0
- tan/commands/generate_cmd.py +1456 -0
- tan/commands/image_cmd.py +753 -0
- tan/commands/init_cmd.py +1740 -0
- tan/commands/inspect_cmd.py +377 -0
- tan/commands/kconfig_cmd.py +734 -0
- tan/commands/model_cmd.py +1514 -0
- tan/commands/model_device_cmd.py +351 -0
- tan/commands/model_device_live.py +234 -0
- tan/commands/model_host_cmd.py +430 -0
- tan/commands/model_zoo_cmd.py +363 -0
- tan/commands/monitor_actions.py +125 -0
- tan/commands/monitor_cmd.py +626 -0
- tan/commands/monitor_session.py +505 -0
- tan/commands/new_som_cmd.py +1522 -0
- tan/commands/pinmux_cmd.py +568 -0
- tan/commands/presets_cmd.py +1123 -0
- tan/commands/probe_cmd.py +421 -0
- tan/commands/reset_cmd.py +354 -0
- tan/commands/reset_confirm.py +168 -0
- tan/commands/run_cmd.py +637 -0
- tan/commands/scaffold_cmd.py +490 -0
- tan/commands/sdk_cmd.py +1723 -0
- tan/commands/size_cmd.py +1020 -0
- tan/commands/support_bundle_cmd.py +1073 -0
- tan/commands/trace_cmd.py +341 -0
- tan/commands/validate_cmd.py +2009 -0
- tan/commands/west_forward_cmd.py +736 -0
- tan/commands/workspace_patch_check.py +296 -0
- tan/core/__init__.py +1 -0
- tan/core/alp_conf_pregen.py +100 -0
- tan/core/artifact_provenance.py +240 -0
- tan/core/atoc_guard.py +276 -0
- tan/core/atoc_guard_messages.py +232 -0
- tan/core/atoc_replacement.py +118 -0
- tan/core/atomic_write.py +178 -0
- tan/core/board_context.py +351 -0
- tan/core/board_diagnostic.py +111 -0
- tan/core/board_files.py +221 -0
- tan/core/board_validator.py +542 -0
- tan/core/board_validator_compat.py +196 -0
- tan/core/board_validator_run.py +129 -0
- tan/core/board_validator_schema.py +186 -0
- tan/core/board_validator_skew.py +46 -0
- tan/core/board_yaml_edit.py +295 -0
- tan/core/board_yaml_pos.py +118 -0
- tan/core/bootstrap.py +2572 -0
- tan/core/build_plan.py +625 -0
- tan/core/consent.py +104 -0
- tan/core/console_filter.py +245 -0
- tan/core/debug_launch.py +1803 -0
- tan/core/devicetree_lint.py +168 -0
- tan/core/dir_removal.py +229 -0
- tan/core/doctor_git.py +172 -0
- tan/core/doctor_libraries.py +253 -0
- tan/core/doctor_render.py +185 -0
- tan/core/doctor_scope.py +66 -0
- tan/core/doctor_setools.py +237 -0
- tan/core/doctor_stale.py +180 -0
- tan/core/document_guards.py +723 -0
- tan/core/dp_id.py +97 -0
- tan/core/elf_load.py +63 -0
- tan/core/error_catalog.py +171 -0
- tan/core/example_catalog.py +499 -0
- tan/core/example_facets.py +151 -0
- tan/core/faultdecode.py +521 -0
- tan/core/flash_plan.py +2501 -0
- tan/core/flow_d_report.py +381 -0
- tan/core/flow_d_uptime.py +164 -0
- tan/core/fs_confine.py +130 -0
- tan/core/global_flags.py +230 -0
- tan/core/host_python.py +210 -0
- tan/core/image_bundle.py +247 -0
- tan/core/inert.py +147 -0
- tan/core/jlink_binary.py +128 -0
- tan/core/jlink_probe.py +481 -0
- tan/core/jsonc_splice.py +404 -0
- tan/core/launch_provenance.py +257 -0
- tan/core/link_refusal.py +76 -0
- tan/core/metadata_schema.py +376 -0
- tan/core/model_check.py +227 -0
- tan/core/model_device.py +449 -0
- tan/core/model_doctor.py +198 -0
- tan/core/model_host.py +48 -0
- tan/core/model_list.py +112 -0
- tan/core/model_zoo.py +353 -0
- tan/core/module_template.py +340 -0
- tan/core/mram_link.py +87 -0
- tan/core/new_som_ospi.py +94 -0
- tan/core/os_class.py +159 -0
- tan/core/patch_paths.py +87 -0
- tan/core/pending.py +40 -0
- tan/core/plain_zephyr_plan.py +138 -0
- tan/core/plan_exec.py +373 -0
- tan/core/plan_tokens.py +600 -0
- tan/core/probe.py +155 -0
- tan/core/probe_plan.py +237 -0
- tan/core/proxy.py +168 -0
- tan/core/publish.py +31 -0
- tan/core/python_floor.py +178 -0
- tan/core/ram_run.py +694 -0
- tan/core/ram_watch.py +231 -0
- tan/core/raw_write.py +160 -0
- tan/core/reset_plan.py +56 -0
- tan/core/run.py +161 -0
- tan/core/scaffold.py +2203 -0
- tan/core/scaffold_selftest_identity.py +225 -0
- tan/core/sdk_default_registry.py +565 -0
- tan/core/sdk_discovery.py +996 -0
- tan/core/sdk_removal.py +465 -0
- tan/core/serial_actions.py +175 -0
- tan/core/serial_capture.py +185 -0
- tan/core/serial_url.py +45 -0
- tan/core/setools.py +608 -0
- tan/core/setools_scratch.py +243 -0
- tan/core/shapes.py +194 -0
- tan/core/size.py +610 -0
- tan/core/som_buildability.py +204 -0
- tan/core/som_schema_version.py +62 -0
- tan/core/subprocess_env.py +201 -0
- tan/core/sysbuild_image.py +94 -0
- tan/core/system_manifest.py +827 -0
- tan/core/text_layout.py +73 -0
- tan/core/timestamp.py +182 -0
- tan/core/tool_lookup.py +383 -0
- tan/core/toolchain_pin.py +117 -0
- tan/core/toolchain_provision.py +1224 -0
- tan/core/trusted_path.py +134 -0
- tan/core/uboot_breakin.py +154 -0
- tan/core/uri_reference.py +415 -0
- tan/core/user_defines.py +152 -0
- tan/core/venv.py +332 -0
- tan/core/west_patches.py +143 -0
- tan/core/west_workspace_refusal.py +159 -0
- tan/core/zephyr_env.py +124 -0
- tan/env.py +185 -0
- tan/envelope.py +647 -0
- tan/exit_codes.py +12 -0
- tan/lazy_typer.py +185 -0
- tan/model/__init__.py +2 -0
- tan/model/_gen_fixture.py +132 -0
- tan/model/adapters/__init__.py +116 -0
- tan/model/adapters/cpu.py +26 -0
- tan/model/adapters/deepx.py +94 -0
- tan/model/adapters/drpai.py +242 -0
- tan/model/adapters/ethos_u.py +951 -0
- tan/model/adapters/executorch.py +55 -0
- tan/model/analyze.py +694 -0
- tan/model/build.py +304 -0
- tan/model/check.py +682 -0
- tan/model/manifest.py +625 -0
- tan/model/measure.py +216 -0
- tan/model/npu_ops_validate.py +130 -0
- tan/model/package.py +125 -0
- tan/model/perf.py +635 -0
- tan/model/perf_apply.py +721 -0
- tan/model/prep.py +193 -0
- tan/model/sram_fit.py +502 -0
- tan/model/targets.py +374 -0
- tan/model/tensorio.py +287 -0
- tan/net.py +73 -0
- tan/output_format.py +213 -0
- tan/planner/__init__.py +219 -0
- tan/planner/aperture.py +202 -0
- tan/planner/buildplan.py +1092 -0
- tan/planner/camera_owner.py +192 -0
- tan/planner/cameras.py +132 -0
- tan/planner/carveout.py +755 -0
- tan/planner/cli.py +274 -0
- tan/planner/headers.py +315 -0
- tan/planner/kconfig.py +2435 -0
- tan/planner/kconfig_symbols.py +473 -0
- tan/planner/libraries.py +677 -0
- tan/planner/link_target.py +237 -0
- tan/planner/linux_ownership.py +216 -0
- tan/planner/loader.py +1525 -0
- tan/planner/manifest.py +116 -0
- tan/planner/memory.py +161 -0
- tan/planner/memregion.py +56 -0
- tan/planner/models.py +533 -0
- tan/planner/orchestrator.py +874 -0
- tan/planner/ownership.py +203 -0
- tan/planner/partition.py +1160 -0
- tan/planner/paths.py +32 -0
- tan/planner/plain_slice.py +139 -0
- tan/planner/project_emit/__init__.py +60 -0
- tan/planner/project_emit/bom_netlist.py +375 -0
- tan/planner/project_emit/dts.py +580 -0
- tan/planner/project_emit/hw_info.py +221 -0
- tan/planner/project_emit/native_sim.py +136 -0
- tan/planner/project_emit/west_libs.py +213 -0
- tan/planner/project_loader.py +264 -0
- tan/planner/sdk_capability.py +203 -0
- tan/planner/sdk_compat.py +395 -0
- tan/planner/secure.py +350 -0
- tan/planner/slugs.py +394 -0
- tan/planner/som_metadata.py +325 -0
- tan/planner/strict_loaders.py +131 -0
- tan/planner/template.py +1037 -0
- tan/planner/template_pins.py +535 -0
- tan/planner/template_rewrite.py +714 -0
- tan/planner/topology.py +208 -0
- tan/planner/validate.py +365 -0
- tan/planner/whole_device_alias.py +67 -0
- tan/planner/zephyr_board.py +3510 -0
- tan/planner_cli.py +97 -0
- tan/planner_emit.py +617 -0
- tan/planner_root.py +119 -0
- tan/soc_ref.py +58 -0
- tan/templates/__init__.py +34 -0
- tan/templates/vendored/MANIFEST.md +1862 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/README.md +112 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/board.yaml +38 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.conf +11 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.overlay +29 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/prj.conf +5 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/src/main.c +266 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/testcase.yaml +53 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/README.md +112 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/board.yaml +38 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.conf +11 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.overlay +29 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/prj.conf +5 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/src/main.c +266 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/testcase.yaml +53 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/CMakeLists.txt +10 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/README.md +75 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/board.yaml +38 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/prj.conf +8 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.c +371 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.h +178 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/main.c +373 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/testcase.yaml +62 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/CMakeLists.txt +10 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/README.md +75 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/board.yaml +37 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/prj.conf +8 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.c +371 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.h +178 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/main.c +373 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/testcase.yaml +62 -0
- tan/templates/vendored/iot/E1M-AEN801/CMakeLists.txt +14 -0
- tan/templates/vendored/iot/E1M-AEN801/README.md +103 -0
- tan/templates/vendored/iot/E1M-AEN801/board.yaml +69 -0
- tan/templates/vendored/iot/E1M-AEN801/prj.conf +15 -0
- tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.c +196 -0
- tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.h +173 -0
- tan/templates/vendored/iot/E1M-AEN801/src/main.c +262 -0
- tan/templates/vendored/iot/E1M-AEN801/testcase.yaml +68 -0
- tan/templates/vendored/minimal/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/minimal/E1M-AEN801/README.md +108 -0
- tan/templates/vendored/minimal/E1M-AEN801/board.yaml +32 -0
- tan/templates/vendored/minimal/E1M-AEN801/prj.conf +8 -0
- tan/templates/vendored/minimal/E1M-AEN801/src/main.c +111 -0
- tan/templates/vendored/minimal/E1M-AEN801/testcase.yaml +49 -0
- tan/templates/vendored/minimal/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/minimal/E1M-V2N101/README.md +108 -0
- tan/templates/vendored/minimal/E1M-V2N101/board.yaml +32 -0
- tan/templates/vendored/minimal/E1M-V2N101/prj.conf +8 -0
- tan/templates/vendored/minimal/E1M-V2N101/src/main.c +111 -0
- tan/templates/vendored/minimal/E1M-V2N101/testcase.yaml +49 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/README.md +120 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/board.yaml +57 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/boards/native_sim_native_64.overlay +40 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/CMakeLists.txt +13 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/main.c +171 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/prj.conf +17 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/testcase.yaml +23 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/prj.conf +22 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/src/main.c +185 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/testcase.yaml +26 -0
- tan/templates/vendored/sensor/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/sensor/E1M-AEN801/README.md +112 -0
- tan/templates/vendored/sensor/E1M-AEN801/board.yaml +57 -0
- tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.conf +10 -0
- tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.overlay +28 -0
- tan/templates/vendored/sensor/E1M-AEN801/prj.conf +5 -0
- tan/templates/vendored/sensor/E1M-AEN801/src/main.c +186 -0
- tan/templates/vendored/sensor/E1M-AEN801/testcase.yaml +52 -0
- tan/templates/vendored/sensor/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/sensor/E1M-V2N101/README.md +112 -0
- tan/templates/vendored/sensor/E1M-V2N101/board.yaml +57 -0
- tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.conf +10 -0
- tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.overlay +28 -0
- tan/templates/vendored/sensor/E1M-V2N101/prj.conf +5 -0
- tan/templates/vendored/sensor/E1M-V2N101/src/main.c +186 -0
- tan/templates/vendored/sensor/E1M-V2N101/testcase.yaml +52 -0
- tan/version.py +102 -0
- tan_cli-0.7.0.dist-info/METADATA +125 -0
- tan_cli-0.7.0.dist-info/RECORD +330 -0
- tan_cli-0.7.0.dist-info/WHEEL +5 -0
- tan_cli-0.7.0.dist-info/entry_points.txt +2 -0
- tan_cli-0.7.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Alp Lab AB
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*
|
|
5
|
+
* cold_chain implementation -- see cold_chain.h. The two non-obvious formulas
|
|
6
|
+
* (Mean Kinetic Temperature and the Magnus dewpoint) are derived inline.
|
|
7
|
+
*/
|
|
8
|
+
#include "cold_chain.h"
|
|
9
|
+
|
|
10
|
+
#include <math.h>
|
|
11
|
+
#include <string.h>
|
|
12
|
+
|
|
13
|
+
/* -------------------------------------------------------------------------
|
|
14
|
+
* Window management.
|
|
15
|
+
*
|
|
16
|
+
* The window is a plain fixed-size array (not a ring buffer): samples are
|
|
17
|
+
* written once and not overwritten. A new monitoring epoch always starts
|
|
18
|
+
* with cc_window_reset(), which resets only the count, not the array -- the
|
|
19
|
+
* next cc_window_push() calls will overwrite stale entries naturally.
|
|
20
|
+
* This design keeps the implementation interrupt-safe (no head/tail pair to
|
|
21
|
+
* update atomically) and cache-friendly (linear scan for feature extraction).
|
|
22
|
+
* ------------------------------------------------------------------------- */
|
|
23
|
+
|
|
24
|
+
void cc_window_reset(struct cc_window_state *st)
|
|
25
|
+
{
|
|
26
|
+
/* Zero the counter; the sample array is written lazily as samples arrive. */
|
|
27
|
+
st->count = 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
void cc_window_push(struct cc_window_state *st, struct cc_sample s)
|
|
31
|
+
{
|
|
32
|
+
/* Silently ignore pushes after the window is full. The caller is
|
|
33
|
+
* expected to call cc_window_reset() to begin a new epoch. Dropping
|
|
34
|
+
* excess samples (rather than wrapping around a ring buffer) keeps all
|
|
35
|
+
* n samples in chronological order at s[0..n-1], which the slope
|
|
36
|
+
* computation relies on for its mirror-index approach. */
|
|
37
|
+
if (st->count < CC_WINDOW_N) {
|
|
38
|
+
st->s[st->count++] = s;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
bool cc_window_full(const struct cc_window_state *st)
|
|
43
|
+
{
|
|
44
|
+
/* The window is full when count reaches the compile-time maximum.
|
|
45
|
+
* The Task-2 classifier gates on this before running inference. */
|
|
46
|
+
return st->count >= CC_WINDOW_N;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* -------------------------------------------------------------------------
|
|
50
|
+
* Mean Kinetic Temperature (ICH Q1A / USP <1079>).
|
|
51
|
+
*
|
|
52
|
+
* MKT is the constant temperature that would inflict the same Arrhenius-rate
|
|
53
|
+
* cumulative degradation as the real, fluctuating profile:
|
|
54
|
+
*
|
|
55
|
+
* MKT = (ΔH/R) / ( -ln( (1/n) Σ exp( -(ΔH/R) / T_i ) ) )
|
|
56
|
+
*
|
|
57
|
+
* with T_i in KELVIN. Because exp(-c/T) is convex, Jensen's inequality gives
|
|
58
|
+
* MKT >= the arithmetic mean -- a short hot excursion raises MKT more than a
|
|
59
|
+
* plain average would, which is exactly why cold-chain auditing uses MKT.
|
|
60
|
+
* The accumulation runs in double precision because the exponentials are tiny.
|
|
61
|
+
* ------------------------------------------------------------------------- */
|
|
62
|
+
static float mkt_celsius(const struct cc_window_state *st, int n)
|
|
63
|
+
{
|
|
64
|
+
/* Accumulate Σ exp(-ΔH/R / T_i) in double to preserve relative precision
|
|
65
|
+
* in the sum; individual terms are tiny -- ~2e-16 at 4 °C, ~1.4e-14 at
|
|
66
|
+
* 40 °C -- so a hot spike's term dwarfs the cold-baseline terms. */
|
|
67
|
+
double sum_exp = 0.0;
|
|
68
|
+
for (int i = 0; i < n; i++) {
|
|
69
|
+
/* Convert each Celsius reading to Kelvin before taking the exponential.
|
|
70
|
+
* Using double here preserves ~15 significant digits vs. ~7 for float. */
|
|
71
|
+
double t_kelvin = (double)st->s[i].temp_c + 273.15;
|
|
72
|
+
sum_exp += exp(-(double)CC_DH_OVER_R / t_kelvin);
|
|
73
|
+
}
|
|
74
|
+
/* Each Boltzmann factor is proportional to the Arrhenius reaction rate at
|
|
75
|
+
* its temperature. The ratio between any two factors is
|
|
76
|
+
* exp(ΔH/R × (1/T_cold − 1/T_hot)); for T_cold=277 K (4 C) versus
|
|
77
|
+
* T_hot=313 K (40 C) that ratio is exp(10000·(1/277.15 − 1/313.15)) ≈ 63.
|
|
78
|
+
* A single hour at 40 C therefore delivers the same cumulative chemical
|
|
79
|
+
* damage as ~63 hours at 4 C -- exactly the asymmetry MKT captures. */
|
|
80
|
+
/* Divide by n to get the mean of the Boltzmann-weighted exponentials. */
|
|
81
|
+
double mean_exp = sum_exp / (double)n;
|
|
82
|
+
/* mean_exp is in (0,1) for any real temperature, so -ln(mean_exp) > 0 and
|
|
83
|
+
* no runtime guard is needed; invert to MKT_K = (ΔH/R) / (-ln(mean_exp)). */
|
|
84
|
+
double mkt_kelvin = (double)CC_DH_OVER_R / (-log(mean_exp));
|
|
85
|
+
/* Convert back to Celsius for the application layer. */
|
|
86
|
+
return (float)(mkt_kelvin - 273.15);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* -------------------------------------------------------------------------
|
|
90
|
+
* Magnus dewpoint approximation (a = 17.62, b = 243.12 °C):
|
|
91
|
+
*
|
|
92
|
+
* γ = ln(RH/100) + a·T / (b + T)
|
|
93
|
+
* Td = b·γ / (a - γ)
|
|
94
|
+
*
|
|
95
|
+
* Valid for RH in (0, 100]; we clamp RH to >= 1% to keep the log finite.
|
|
96
|
+
* Error < 0.35 °C over 0..60 °C / 1..100% RH, which is adequate for
|
|
97
|
+
* condensation-risk detection.
|
|
98
|
+
* ------------------------------------------------------------------------- */
|
|
99
|
+
static float dewpoint_celsius(float temp_c, float rh_pct)
|
|
100
|
+
{
|
|
101
|
+
/* Magnus coefficients (Alduchov & Eskridge 1996 optimisation). */
|
|
102
|
+
const float a = 17.62f, b = 243.12f;
|
|
103
|
+
/* Clamp RH to a safe range: log(0) = -∞, log(>100%) is physically wrong.
|
|
104
|
+
* The 1% floor is conservative: at RH=1% the dewpoint is roughly -50 C
|
|
105
|
+
* for a 20 C ambient -- far below any real cold-chain scenario. This
|
|
106
|
+
* guard only fires on corrupt or uninitialised sensor readings. */
|
|
107
|
+
float rh = (rh_pct < 1.0f) ? 1.0f : ((rh_pct > 100.0f) ? 100.0f : rh_pct);
|
|
108
|
+
/* γ combines the saturation vapour-pressure (log term, <= 0 for RH <= 100%)
|
|
109
|
+
* and the temperature term (positive). γ may be negative (cold/dry air) or
|
|
110
|
+
* positive (warm/humid, e.g. +0.65 at 20 C / 50% RH); either way γ < a, so
|
|
111
|
+
* the denominator (a - γ) stays positive and Td <= T for RH <= 100% -- the
|
|
112
|
+
* dewpoint never exceeds the ambient, as physically expected. */
|
|
113
|
+
float gamma = logf(rh / 100.0f) + (a * temp_c) / (b + temp_c);
|
|
114
|
+
/* Solve for the temperature at which RH would equal 100%. */
|
|
115
|
+
return (b * gamma) / (a - gamma);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* -------------------------------------------------------------------------
|
|
119
|
+
* Main feature extraction pass.
|
|
120
|
+
* ------------------------------------------------------------------------- */
|
|
121
|
+
|
|
122
|
+
void cc_feat_extract(const struct cc_window_state *st,
|
|
123
|
+
const struct cc_config *cfg,
|
|
124
|
+
struct cc_features *out)
|
|
125
|
+
{
|
|
126
|
+
/* Cap at CC_WINDOW_N; cc_window_push() never exceeds it but the cast
|
|
127
|
+
* from uint16_t makes the compiler happy with signed loop indices. */
|
|
128
|
+
const int n = (st->count < CC_WINDOW_N) ? st->count : CC_WINDOW_N;
|
|
129
|
+
|
|
130
|
+
/* Zero all output fields; early return leaves them at zero for empty windows. */
|
|
131
|
+
memset(out, 0, sizeof(*out));
|
|
132
|
+
if (n <= 0) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/* ------------------------------------------------------------------
|
|
137
|
+
* First pass: means, min/max temperature, and the out-of-band sample
|
|
138
|
+
* count. A single pass keeps cache pressure low on the M55.
|
|
139
|
+
*
|
|
140
|
+
* We intentionally combine all four aggregates (mean_t, mean_rh,
|
|
141
|
+
* min_t, max_t) into one loop iteration to maximise data locality:
|
|
142
|
+
* each cc_sample is 12 bytes and fits in a single cache line fetch.
|
|
143
|
+
* ------------------------------------------------------------------ */
|
|
144
|
+
float sum_t = 0.0f, sum_rh = 0.0f;
|
|
145
|
+
/* Seed min/max with the first sample to avoid a sentinel like FLT_MAX. */
|
|
146
|
+
float min_t = st->s[0].temp_c, max_t = st->s[0].temp_c;
|
|
147
|
+
int out_of_band = 0;
|
|
148
|
+
for (int i = 0; i < n; i++) {
|
|
149
|
+
float t = st->s[i].temp_c;
|
|
150
|
+
sum_t += t;
|
|
151
|
+
sum_rh += st->s[i].rh_pct;
|
|
152
|
+
/* Track the coldest and warmest readings seen in the window. */
|
|
153
|
+
if (t < min_t) {
|
|
154
|
+
min_t = t;
|
|
155
|
+
}
|
|
156
|
+
if (t > max_t) {
|
|
157
|
+
max_t = t;
|
|
158
|
+
}
|
|
159
|
+
/* A reading counts as an excursion when it leaves the product's band. */
|
|
160
|
+
if (t < cfg->t_lo || t > cfg->t_hi) {
|
|
161
|
+
out_of_band++;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/* Store the four scalar aggregates. */
|
|
165
|
+
out->mean_temp_c = sum_t / (float)n;
|
|
166
|
+
out->mean_rh_pct = sum_rh / (float)n;
|
|
167
|
+
out->min_temp_c = min_t;
|
|
168
|
+
out->max_temp_c = max_t;
|
|
169
|
+
/* Convert out-of-band sample count to minutes via the per-sample duration. */
|
|
170
|
+
out->excursion_min = (float)out_of_band * CC_SAMPLE_MIN;
|
|
171
|
+
|
|
172
|
+
/* ------------------------------------------------------------------
|
|
173
|
+
* Warming/cooling trend: last-quarter mean minus first-quarter mean,
|
|
174
|
+
* divided by the window duration in minutes (positive = warming).
|
|
175
|
+
*
|
|
176
|
+
* Using quarter-window means rather than raw endpoints suppresses the
|
|
177
|
+
* noise of a single outlier reading biasing the trend estimate. This
|
|
178
|
+
* is a simple but effective linear trend approximation that avoids the
|
|
179
|
+
* overhead of a full least-squares regression on the M55.
|
|
180
|
+
* ------------------------------------------------------------------ */
|
|
181
|
+
int q = n / 4;
|
|
182
|
+
/* Guard: if the window is very short (< 4 samples), use a single sample. */
|
|
183
|
+
if (q < 1) {
|
|
184
|
+
q = 1;
|
|
185
|
+
}
|
|
186
|
+
/* Accumulate the first and last quarters separately. */
|
|
187
|
+
float first = 0.0f, last = 0.0f;
|
|
188
|
+
for (int i = 0; i < q; i++) {
|
|
189
|
+
first += st->s[i].temp_c;
|
|
190
|
+
/* Mirror-index from the end to get the last quarter. */
|
|
191
|
+
last += st->s[n - 1 - i].temp_c;
|
|
192
|
+
}
|
|
193
|
+
/* Slope = ΔT / Δt in °C/min; normalise both differences by the same q.
|
|
194
|
+
* Expanding: slope = (mean_last_q - mean_first_q) / (n * CC_SAMPLE_MIN),
|
|
195
|
+
* where n * CC_SAMPLE_MIN is the full window duration in minutes. Using
|
|
196
|
+
* the full-window span (not just the quarter) gives a globally consistent
|
|
197
|
+
* rate: a positive value means the window is warming, negative is cooling. */
|
|
198
|
+
out->temp_slope_c_per_min = ((last - first) / (float)q) / ((float)n * CC_SAMPLE_MIN);
|
|
199
|
+
|
|
200
|
+
/* Dewpoint uses the window-mean T/RH so it lines up with the classifier's
|
|
201
|
+
* mean-temperature comparison. */
|
|
202
|
+
out->dewpoint_c = dewpoint_celsius(out->mean_temp_c, out->mean_rh_pct);
|
|
203
|
+
|
|
204
|
+
/* Mean kinetic temperature over the whole window (expensive: n exp() calls;
|
|
205
|
+
* runs last so the earlier cheap fields are ready for early-exit callers). */
|
|
206
|
+
out->mkt_c = mkt_celsius(st, n);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/* -------------------------------------------------------------------------
|
|
210
|
+
* Feature packing -- lay the metrics out in a fixed order for the AI model.
|
|
211
|
+
*
|
|
212
|
+
* The order here must match the column order used when the classifier was
|
|
213
|
+
* trained (the training script is not shipped with this example). If a feature
|
|
214
|
+
* is added or removed, CC_FEATURE_DIM must be updated AND the model must be
|
|
215
|
+
* retrained.
|
|
216
|
+
*
|
|
217
|
+
* The function intentionally takes a raw float pointer + capacity so that the
|
|
218
|
+
* same vector can be passed directly to the .alpmodel inference call without
|
|
219
|
+
* an intermediate copy.
|
|
220
|
+
* ------------------------------------------------------------------------- */
|
|
221
|
+
|
|
222
|
+
size_t cc_feat_pack(const struct cc_features *f, float *vec, size_t cap)
|
|
223
|
+
{
|
|
224
|
+
/* Refuse to write a partial vector: the model expects exactly CC_FEATURE_DIM
|
|
225
|
+
* floats and must not receive a shorter, ambiguously-padded slice. */
|
|
226
|
+
if (cap < (size_t)CC_FEATURE_DIM) {
|
|
227
|
+
return 0;
|
|
228
|
+
}
|
|
229
|
+
/* Pack in the same order the training pipeline used to build the model.
|
|
230
|
+
* Any reordering here must be mirrored in the training pipeline. */
|
|
231
|
+
size_t i = 0;
|
|
232
|
+
/* Temperature statistics (3 floats): mean captures the typical exposure
|
|
233
|
+
* level; min and max bound the extremes. Together they let the autoencoder
|
|
234
|
+
* distinguish a fluctuating-but-safe profile from one that is narrowly
|
|
235
|
+
* within band but trending toward the edge of the safe region. */
|
|
236
|
+
vec[i++] = f->mean_temp_c;
|
|
237
|
+
vec[i++] = f->min_temp_c;
|
|
238
|
+
vec[i++] = f->max_temp_c;
|
|
239
|
+
/* Humidity mean (1 float). */
|
|
240
|
+
vec[i++] = f->mean_rh_pct;
|
|
241
|
+
/* Trend and derived physics metrics (4 floats): temp_slope gives the
|
|
242
|
+
* direction of change; dewpoint and mkt carry the condensation-risk and
|
|
243
|
+
* cumulative-damage signals respectively; excursion_min directly encodes
|
|
244
|
+
* the regulatory breach duration independent of the current temperature. */
|
|
245
|
+
vec[i++] = f->temp_slope_c_per_min;
|
|
246
|
+
vec[i++] = f->dewpoint_c;
|
|
247
|
+
vec[i++] = f->mkt_c;
|
|
248
|
+
vec[i++] = f->excursion_min;
|
|
249
|
+
return i; /* == CC_FEATURE_DIM */
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/* -------------------------------------------------------------------------
|
|
253
|
+
* 4-state integrity classifier.
|
|
254
|
+
*
|
|
255
|
+
* The three checks are ordered by severity: ACUTE (product currently outside
|
|
256
|
+
* its temperature band, or it has already spent too many minutes out of band)
|
|
257
|
+
* takes priority over CUMULATIVE MKT damage, which takes priority over
|
|
258
|
+
* HUMIDITY risk. This ordering means the most urgent condition is always
|
|
259
|
+
* surfaced first and the operator sees one unambiguous action.
|
|
260
|
+
* ------------------------------------------------------------------------- */
|
|
261
|
+
|
|
262
|
+
cc_state_t cc_classify(const struct cc_features *f, const struct cc_config *cfg)
|
|
263
|
+
{
|
|
264
|
+
/* ACUTE first: the product is currently too warm/cold, or it has spent more
|
|
265
|
+
* than the allowed minutes out of band. We compare the window mean (not a
|
|
266
|
+
* single instantaneous reading) against t_lo/t_hi so that a lone noisy
|
|
267
|
+
* sample does not trigger a false alert; the excursion_min counter catches
|
|
268
|
+
* that same spike as a cumulative time-in-breach penalty. Acute alerts
|
|
269
|
+
* demand immediate operator action: move product, repair the cold store. */
|
|
270
|
+
if (f->mean_temp_c < cfg->t_lo || f->mean_temp_c > cfg->t_hi ||
|
|
271
|
+
f->excursion_min > cfg->excursion_min_limit) {
|
|
272
|
+
return CC_TEMP_EXCURSION;
|
|
273
|
+
}
|
|
274
|
+
/* CUMULATIVE: even after the temperature has recovered to the safe band, a
|
|
275
|
+
* brief hot excursion may have delivered enough Arrhenius-weighted thermal
|
|
276
|
+
* energy to push MKT past the regulatory limit. Because the Boltzmann
|
|
277
|
+
* exponential grows steeply with temperature, a spike to 20 C for 1 hour
|
|
278
|
+
* raises MKT far more than 1 hour at 9 C. ICH Q1A / USP <1079> reference
|
|
279
|
+
* limit for most vaccines: MKT <= 8 C (same as the storage ceiling). */
|
|
280
|
+
if (f->mkt_c > cfg->mkt_limit_c) {
|
|
281
|
+
return CC_MKT_EXCEEDED;
|
|
282
|
+
}
|
|
283
|
+
/* HUMIDITY: condensation risk arises from two independent conditions --
|
|
284
|
+
* (a) The gap between ambient temperature and the dewpoint is smaller than
|
|
285
|
+
* dewpoint_margin_c: the air is close to saturation and a small drop
|
|
286
|
+
* (draught, cold surface) will start condensing water on the goods.
|
|
287
|
+
* (b) RH > 90% regardless of the gap: so-saturated air condenses on any
|
|
288
|
+
* surface even 1-2 C cooler than ambient.
|
|
289
|
+
* Both conditions risk label detachment, mould growth, and corrosion. */
|
|
290
|
+
if ((f->mean_temp_c - f->dewpoint_c) < cfg->dewpoint_margin_c || f->mean_rh_pct > 90.0f) {
|
|
291
|
+
return CC_CONDENSATION_RISK;
|
|
292
|
+
}
|
|
293
|
+
return CC_OK;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const char *cc_state_name(cc_state_t s)
|
|
297
|
+
{
|
|
298
|
+
/* Map each state to a stable, upper-case string (the enum member name
|
|
299
|
+
* without the CC_ prefix). Returns "UNKNOWN" for any out-of-range
|
|
300
|
+
* value — safe to log even if the caller passes a corrupt byte. */
|
|
301
|
+
switch (s) {
|
|
302
|
+
case CC_OK:
|
|
303
|
+
return "OK";
|
|
304
|
+
case CC_TEMP_EXCURSION:
|
|
305
|
+
return "TEMP_EXCURSION";
|
|
306
|
+
case CC_MKT_EXCEEDED:
|
|
307
|
+
return "MKT_EXCEEDED";
|
|
308
|
+
case CC_CONDENSATION_RISK:
|
|
309
|
+
return "CONDENSATION_RISK";
|
|
310
|
+
default:
|
|
311
|
+
return "UNKNOWN";
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/* -------------------------------------------------------------------------
|
|
316
|
+
* Deterministic anomaly score.
|
|
317
|
+
*
|
|
318
|
+
* Combines two contributions into a single 0..1 float so that the main
|
|
319
|
+
* application loop can log an anomaly severity even when no AI model is
|
|
320
|
+
* loaded (e.g., first boot, model download in progress, OTA mid-flight).
|
|
321
|
+
*
|
|
322
|
+
* Score is the MAX of:
|
|
323
|
+
* (a) excursion depth: how far the mean T is outside the band, normalised
|
|
324
|
+
* by the band width -- score reaches 1.0 when the mean is one full band
|
|
325
|
+
* width past an edge (e.g. for a 2..8 C band, mean = 14 C);
|
|
326
|
+
* (b) MKT overshoot: how much MKT exceeds the limit, normalised by the
|
|
327
|
+
* limit itself so that 2x the MKT limit = score 1.0.
|
|
328
|
+
*
|
|
329
|
+
* Taking the max (not the sum) keeps the score interpretable: 0.9 always
|
|
330
|
+
* means "very close to or past a hard limit", regardless of which limit.
|
|
331
|
+
* The score is clamped to [0, 1] and saturates at 1.0.
|
|
332
|
+
* ------------------------------------------------------------------------- */
|
|
333
|
+
|
|
334
|
+
float cc_anomaly_fallback(const struct cc_features *f, const struct cc_config *cfg)
|
|
335
|
+
{
|
|
336
|
+
float score = 0.0f;
|
|
337
|
+
|
|
338
|
+
/* Excursion depth: how far the mean T is past the band edge, normalised by
|
|
339
|
+
* the band width. Score = 0 when in-band; score = 1 when the mean is one
|
|
340
|
+
* full band width outside the edge. For a 2..8 C fridge (band = 6 C),
|
|
341
|
+
* mean_temp_c = 14 C gives score = 6/6 = 1.0. Using the mean (not an
|
|
342
|
+
* instantaneous reading) keeps the score robust against sensor glitches. */
|
|
343
|
+
float band = cfg->t_hi - cfg->t_lo;
|
|
344
|
+
if (band > 1e-6f) {
|
|
345
|
+
float over = 0.0f;
|
|
346
|
+
if (f->mean_temp_c > cfg->t_hi) {
|
|
347
|
+
over = f->mean_temp_c - cfg->t_hi;
|
|
348
|
+
} else if (f->mean_temp_c < cfg->t_lo) {
|
|
349
|
+
over = cfg->t_lo - f->mean_temp_c;
|
|
350
|
+
}
|
|
351
|
+
score = over / band;
|
|
352
|
+
}
|
|
353
|
+
/* MKT overshoot: how far MKT exceeds the cumulative-damage limit, normalised
|
|
354
|
+
* by the limit so 2× the MKT limit saturates at score = 1.0. For a
|
|
355
|
+
* vaccine fridge with mkt_limit_c = 8 C, MKT = 12 C → mkt_term = 4/8 =
|
|
356
|
+
* 0.5. Taking max() (not sum) keeps the score interpretable: 0.9 always
|
|
357
|
+
* means "very close to or past a hard limit", regardless of which one. */
|
|
358
|
+
if (f->mkt_c > cfg->mkt_limit_c && cfg->mkt_limit_c > 1e-6f) {
|
|
359
|
+
float mkt_term = (f->mkt_c - cfg->mkt_limit_c) / cfg->mkt_limit_c;
|
|
360
|
+
if (mkt_term > score) {
|
|
361
|
+
score = mkt_term;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (score < 0.0f) {
|
|
365
|
+
score = 0.0f;
|
|
366
|
+
}
|
|
367
|
+
if (score > 1.0f) {
|
|
368
|
+
score = 1.0f;
|
|
369
|
+
}
|
|
370
|
+
return score;
|
|
371
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Alp Lab AB
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*
|
|
5
|
+
* cold_chain -- pure-C cold-chain integrity metrics for the BME280 example.
|
|
6
|
+
*
|
|
7
|
+
* A slow time series of (temperature, humidity, pressure) is reduced to the
|
|
8
|
+
* metrics a pharma/food cold-chain auditor actually uses:
|
|
9
|
+
* - excursion time : how long the temperature spent outside the safe band;
|
|
10
|
+
* - Mean Kinetic Temp : the single temperature delivering the same cumulative
|
|
11
|
+
* thermal stress as the fluctuating profile (ICH Q1A /
|
|
12
|
+
* USP <1079>) -- a brief hot spike counts for more than
|
|
13
|
+
* its duration, which a plain average would hide;
|
|
14
|
+
* - dewpoint : condensation / mould risk when ambient T nears it.
|
|
15
|
+
* Arch-neutral (stdint/math only): builds for native_sim and the M55 alike,
|
|
16
|
+
* and is host-unit-tested.
|
|
17
|
+
*/
|
|
18
|
+
#ifndef COLD_CHAIN_H
|
|
19
|
+
#define COLD_CHAIN_H
|
|
20
|
+
|
|
21
|
+
#include <stdbool.h>
|
|
22
|
+
#include <stddef.h>
|
|
23
|
+
#include <stdint.h>
|
|
24
|
+
|
|
25
|
+
#ifdef __cplusplus
|
|
26
|
+
extern "C" {
|
|
27
|
+
#endif
|
|
28
|
+
|
|
29
|
+
/* -------------------------------------------------------------------------
|
|
30
|
+
* Compile-time constants.
|
|
31
|
+
*
|
|
32
|
+
* All constants are chosen to match the example's BME280 sampling cadence
|
|
33
|
+
* and the ICH Q1A/USP <1079> pharmaceutical cold-chain standard.
|
|
34
|
+
* ------------------------------------------------------------------------- */
|
|
35
|
+
|
|
36
|
+
/* Window length (samples) and the wall-clock minutes each sample represents.
|
|
37
|
+
* The demo logs one reading per simulated minute, so excursion counts convert
|
|
38
|
+
* directly to minutes.
|
|
39
|
+
* 256 samples = ~4.3 hours at 1 sample/min; enough for a full refrigeration
|
|
40
|
+
* cycle and short enough to fit in M55 SRAM without fragmenting the heap. */
|
|
41
|
+
#define CC_WINDOW_N 256
|
|
42
|
+
#define CC_SAMPLE_MIN 1.0f
|
|
43
|
+
|
|
44
|
+
/* Feature vector length: mean/min/max T + mean RH + slope + dewpoint + MKT +
|
|
45
|
+
* excursion-minutes. Task 2 (classifier) expects exactly this many floats. */
|
|
46
|
+
#define CC_FEATURE_DIM 8
|
|
47
|
+
|
|
48
|
+
/* Arrhenius activation term ΔH/R for MKT. ΔH = 83.144 kJ/mol (the ICH
|
|
49
|
+
* default for pharmaceutical stability), R = 8.314 J/mol·K → ΔH/R = 10000 K.
|
|
50
|
+
* This is the standard constant used by USP <1079> and ICH Q1A regulators. */
|
|
51
|
+
#define CC_DH_OVER_R 10000.0f
|
|
52
|
+
|
|
53
|
+
/* -------------------------------------------------------------------------
|
|
54
|
+
* Data types.
|
|
55
|
+
* ------------------------------------------------------------------------- */
|
|
56
|
+
|
|
57
|
+
/** One environmental reading in physical units. */
|
|
58
|
+
struct cc_sample {
|
|
59
|
+
float temp_c; /**< temperature, degrees Celsius. */
|
|
60
|
+
float rh_pct; /**< relative humidity, percent. */
|
|
61
|
+
float pressure_pa; /**< barometric pressure, pascals (logged, not classified). */
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** Sliding window of readings.
|
|
65
|
+
* @p count saturates at CC_WINDOW_N; the window is "full" once that count is
|
|
66
|
+
* reached and no further samples are accepted. */
|
|
67
|
+
struct cc_window_state {
|
|
68
|
+
/* Plain fill-once array of up to CC_WINDOW_N readings (NOT a ring buffer:
|
|
69
|
+
* samples are written once per report window and not overwritten). */
|
|
70
|
+
struct cc_sample s[CC_WINDOW_N];
|
|
71
|
+
/* How many samples have been pushed so far; capped at CC_WINDOW_N. */
|
|
72
|
+
uint16_t count;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** Derived cold-chain metrics over the window.
|
|
76
|
+
* Produced by cc_feat_extract(); consumed by the Task-2 classifier and by
|
|
77
|
+
* cc_feat_pack() to build the float vector the AI model expects. */
|
|
78
|
+
struct cc_features {
|
|
79
|
+
float mean_temp_c; /**< arithmetic mean temperature. */
|
|
80
|
+
float min_temp_c; /**< coldest sample. */
|
|
81
|
+
float max_temp_c; /**< warmest sample. */
|
|
82
|
+
float mean_rh_pct; /**< mean relative humidity. */
|
|
83
|
+
float temp_slope_c_per_min; /**< warming/cooling trend (>0 = warming). */
|
|
84
|
+
float dewpoint_c; /**< Magnus dewpoint of the mean T/RH. */
|
|
85
|
+
float mkt_c; /**< mean kinetic temperature, Celsius. */
|
|
86
|
+
float excursion_min; /**< minutes spent outside the safe band. */
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/** Per-product thresholds (a vaccine fridge by default).
|
|
90
|
+
* All temperature limits are in degrees Celsius; all time limits in minutes.
|
|
91
|
+
* The caller populates this from NVS or a compile-time default and passes it
|
|
92
|
+
* to cc_feat_extract(); the library never writes to it. */
|
|
93
|
+
struct cc_config {
|
|
94
|
+
float t_lo; /**< safe-band low edge, Celsius. */
|
|
95
|
+
float t_hi; /**< safe-band high edge, Celsius. */
|
|
96
|
+
float mkt_limit_c; /**< MKT above this = cumulative damage. */
|
|
97
|
+
float excursion_min_limit; /**< excursion minutes above this = breach. */
|
|
98
|
+
float dewpoint_margin_c; /**< T within this of dewpoint = condensation risk. */
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/* -------------------------------------------------------------------------
|
|
102
|
+
* Window management.
|
|
103
|
+
* ------------------------------------------------------------------------- */
|
|
104
|
+
|
|
105
|
+
/** Reset the window (count = 0).
|
|
106
|
+
* Call this once at start-up or whenever a new monitoring session begins. */
|
|
107
|
+
void cc_window_reset(struct cc_window_state *st);
|
|
108
|
+
|
|
109
|
+
/** Append one reading; ignored once the window is full.
|
|
110
|
+
* Typical call rate: once per CC_SAMPLE_MIN minutes (the demo fires every
|
|
111
|
+
* simulated minute). */
|
|
112
|
+
void cc_window_push(struct cc_window_state *st, struct cc_sample s);
|
|
113
|
+
|
|
114
|
+
/** True once CC_WINDOW_N readings have been pushed.
|
|
115
|
+
* The classifier (Task 2) gates on this: it should not run until the window
|
|
116
|
+
* is full so that every statistic is computed over a complete epoch. */
|
|
117
|
+
bool cc_window_full(const struct cc_window_state *st);
|
|
118
|
+
|
|
119
|
+
/* -------------------------------------------------------------------------
|
|
120
|
+
* Feature extraction and packing.
|
|
121
|
+
* ------------------------------------------------------------------------- */
|
|
122
|
+
|
|
123
|
+
/** Reduce the window to the cold-chain metrics. @p cfg supplies the safe band
|
|
124
|
+
* used for the excursion count.
|
|
125
|
+
*
|
|
126
|
+
* All eight fields of @p out are always written; if the window is empty every
|
|
127
|
+
* field is zero. Calling on a partially-filled window is legal (useful for
|
|
128
|
+
* early-warning before the window is full). */
|
|
129
|
+
void cc_feat_extract(const struct cc_window_state *st,
|
|
130
|
+
const struct cc_config *cfg,
|
|
131
|
+
struct cc_features *out);
|
|
132
|
+
|
|
133
|
+
/** Pack the metrics into the AI feature vector; returns the count (CC_FEATURE_DIM).
|
|
134
|
+
* Returns 0 without writing anything if @p cap < CC_FEATURE_DIM.
|
|
135
|
+
* Field order matches the training pipeline; do NOT reorder. */
|
|
136
|
+
size_t cc_feat_pack(const struct cc_features *f, float *vec, size_t cap);
|
|
137
|
+
|
|
138
|
+
/* -------------------------------------------------------------------------
|
|
139
|
+
* 4-state integrity classifier.
|
|
140
|
+
*
|
|
141
|
+
* The classifier reduces the rich feature vector to a single actionable state
|
|
142
|
+
* using deterministic thresholds from @p cfg. Severity levels are ordered:
|
|
143
|
+
* an acute excursion (product currently out of band) is flagged before the
|
|
144
|
+
* cumulative MKT breach, which is flagged before the humidity-driven
|
|
145
|
+
* condensation risk. This ordering ensures the most urgent condition is
|
|
146
|
+
* always visible to the operator.
|
|
147
|
+
* ------------------------------------------------------------------------- */
|
|
148
|
+
|
|
149
|
+
/** Integrity state (reference-grade; customers retune the config per product). */
|
|
150
|
+
typedef enum {
|
|
151
|
+
CC_OK = 0, /**< inside band, MKT under limit, no condensation. */
|
|
152
|
+
CC_TEMP_EXCURSION = 1, /**< acute: mean out of band, or excursion-time over limit. */
|
|
153
|
+
CC_MKT_EXCEEDED = 2, /**< cumulative thermal damage (MKT over limit). */
|
|
154
|
+
CC_CONDENSATION_RISK = 3, /**< T near dewpoint or very high humidity. */
|
|
155
|
+
CC_STATE_COUNT
|
|
156
|
+
} cc_state_t;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Classify the integrity state. Order matters: an ACUTE excursion (currently
|
|
160
|
+
* out of band) is reported before the CUMULATIVE MKT breach, which is reported
|
|
161
|
+
* before the humidity-driven condensation risk.
|
|
162
|
+
*/
|
|
163
|
+
cc_state_t cc_classify(const struct cc_features *f, const struct cc_config *cfg);
|
|
164
|
+
|
|
165
|
+
/** Stable upper-case state name for the record. */
|
|
166
|
+
const char *cc_state_name(cc_state_t s);
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Deterministic 0..1 anomaly score = max(excursion depth, MKT overshoot),
|
|
170
|
+
* saturating. Used when no AI model is loaded.
|
|
171
|
+
*/
|
|
172
|
+
float cc_anomaly_fallback(const struct cc_features *f, const struct cc_config *cfg);
|
|
173
|
+
|
|
174
|
+
#ifdef __cplusplus
|
|
175
|
+
}
|
|
176
|
+
#endif
|
|
177
|
+
|
|
178
|
+
#endif /* COLD_CHAIN_H */
|