tin  1.5.9
framework_walkthrough

Framework Walkthrough

This page follows one measured value through tin. It is the fastest way to understand how the framework pieces fit together.

The example is a motor drive that receives a voltage sample, turns that raw input into a typed product event, and moves the drive into a safe state when the voltage is too high.

The Mental Model

tin separates four concerns:

Concern Owned by Question it answers
Product behavior tsm Which states and events are legal?
Bounded movement tin::channel, runtime queues, actors, tasks How does work move without hidden allocation?
I/O shape tio What does a portable ADC, GPIO, PWM, CAN, or bus payload look like?
Target boundary thal and adapters Which compiler, RTOS, HAL, or board assumptions exist?

The important rule is that dependencies point inward toward portable behavior. Driver code may know about vendor SDKs. HSM definitions should not.

One Event Through The System

ADC driver or ISR
|
v
bounded channel of VoltageSample
|
v
service task or actor
|
v
VoltageHigh{ measured_mv, limit_mv }
|
v
queued tsm runtime
|
v
MotorDrive transitions Running -> SafeHold
|
v
output adapter disables PWM or records a command

Each boundary has a small job:

  • The driver boundary captures raw data and performs a non-blocking handoff.
  • The channel stores a fixed number of payloads and makes overflow policy visible.
  • The service task or actor translates raw samples into product events.
  • The runtime queues typed events and dispatches them under a caller-owned loop.
  • The HSM owns legal state changes and actions on its context.
  • The adapter observes state or context and talks to hardware, middleware, replay, or simulation.

Step 1: Raw Input Becomes A Payload

The low-level input side should do the smallest useful amount of work. It captures a value and places it in bounded storage.

#include <cstdint>
#include "tin/runtime.h"
struct VoltageSample {
std::uint16_t millivolts{};
};
void voltage_ready_from_isr(std::uint16_t millivolts) {
(void)voltage_samples.try_send_from_isr(
VoltageSample{ .millivolts = millivolts });
}
Definition: sync.h:507
bool try_send_from_isr(T const &value)
Definition: sync.h:539
Tin runtime-kernel facade.

This code does not decide product behavior. It only records a measurement in a bounded handoff point.

Step 2: The Bridge Creates A Product Event

A task, actor, poller, or test harness can drain the channel and emit typed events. Product data travels in the event payload.

struct MotorDrive {
struct Booting {};
struct Running {};
struct SafeHold {};
struct BootComplete {};
// Payload event: the measured value and threshold travel with the event.
struct VoltageHigh {
std::uint16_t measured_mv{};
std::uint16_t limit_mv{};
};
void latch_voltage_fault(VoltageHigh const& event) {
last_fault_mv = event.measured_mv;
fault_limit_mv = event.limit_mv;
pwm_enabled = false;
}
std::uint16_t last_fault_mv{};
std::uint16_t fault_limit_mv{ 3000 };
bool pwm_enabled{};
using transitions =
tsm::T<Running,
VoltageHigh,
SafeHold,
&MotorDrive::latch_voltage_fault>>;
};
consteval auto transitions(TransitionEntries...)
Definition: tsm.h:2448
transition_table< TransitionEntries... > Ts
Definition: tsm.h:2445

The event is more useful than a marker such as OvervoltageFault because the state machine can record exactly what happened.

Step 3: The Runtime Owns Backlog, Not Threads

A queued runtime accepts typed events into fixed storage. It does not create a thread. A caller, executor, RTOS task, host loop, or test decides when to step.

using DriveRuntime =
tsm::Runtime<MotorDrive,
DriveRuntime drive;
void bridge_voltage_samples() {
VoltageSample sample{};
while (voltage_samples.try_receive(sample)) {
if (sample.millivolts > drive.context().fault_limit_mv) {
(void)drive.send_event(MotorDrive::VoltageHigh{
.measured_mv = sample.millivolts,
.limit_mv = drive.context().fault_limit_mv });
}
}
}
void application_step() {
bridge_voltage_samples();
(void)drive.step();
}
bool try_receive(T &value)
Definition: sync.h:549
runtime::Runtime< Definition, Policy, MachinePolicy > Runtime
Definition: runtime.h:35
Definition: policy.h:29
Definition: policy.h:52
Definition: policy.h:89

send_event accepts work into the runtime queue. step dispatches at most one queued event. drain can be used when the caller wants to run until the runtime has no immediate work.

Step 4: Output Stays At The Boundary

The HSM decides that the drive is in SafeHold; a hardware adapter decides what that means for a board.

template<typename Pwm>
void apply_outputs(Pwm& pwm, DriveRuntime& runtime) {
if (runtime.machine().template active<MotorDrive::Running>() &&
runtime.context().pwm_enabled) {
pwm.set_duty(420U);
} else {
pwm.set_duty(0U);
}
}
concept pwm
Definition: io.h:194

This keeps the HSM testable on a host. A unit test can dispatch VoltageHigh{ .measured_mv = 3300, .limit_mv = 3000 } and verify SafeHold without linking a board HAL.

What To Review

When reviewing code like this, check the same points in order:

  1. Are raw inputs captured into bounded storage?
  2. Is overflow behavior explicit?
  3. Does the bridge translate mechanics into typed product events?
  4. Does event payload carry the data needed by behavior and diagnostics?
  5. Is runtime execution caller-owned and bounded per step?
  6. Is target-specific output isolated behind an adapter?