Getting started › Tutorial

Getting started

Tutorial: a water pump, from model to firmware

This tutorial builds one small system in arche-lang, step by step: a pump controller that an operator panel starts and stops, and that drives a motor. On the way it checks the model, links a requirement to the part that meets it and the test that verifies it, runs that test in the simulator, places the controller on a microcontroller, generates its Rust firmware, and puts the whole project under CI.

Every arche block below goes, in order, into one file, pump.arche; put it in an empty folder. (The tutorial's own test compiles exactly these blocks, so they are the language as it is.) Commands are run from that folder; archec is node <arche-lang>/dist/cli.js if it is not on your PATH.

The reference for every declaration and property is patterns.md; the commands are in commands.md.

1. The project, its data and its messages#

project Pump "Water pump" {
  version "0.1"
}

type Flow = float32 unit l/min range 0..120

message START: bool "Start"
message STOP: bool "Stop"
message FLOW_RATE "Flow rate" { rate: Flow }

const MAX_FLOW = 100 unit l/min

A message is named in capitals and carries one type — here a truth value — or fields, like rate, a Flow: a number with a unit and a range. Units are written as they are (l/min, m/s², °C): archec checks that what is compared or added up has the same quantity.

archec check pump.arche

2. Components and their ports#

component Controller "Pump controller" {
  in cmd: can "Commands" receives START, STOP
  out motor: can "Motor" sends FLOW_RATE

  var target: float32 = 0 unit l/min

  fn regulate "Flow loop" {
    wcet 200us
    ```arche
    if state == Running {
      send motor FLOW_RATE { rate: target }
    }
    ```
  }

A port says its direction, its type (can — a port type, here the standard one) and the messages it takes or gives. A function's body is written in the action language, in a ```arche block: state == Running tests the part's state, send motor FLOW_RATE { … } sends on a port, goto Fault would change state. archec checks it — the names, their types, what the port sends — and translates it: to the Python the Arche simulator runs, and to Rust or C for the firmware (step 7). wcet is what it costs at worst, for the timing analysis of step 6. (A body can still be Python, in a ```python block, for what the action language does not say: a library, a list.)

3. A state machine#

  states {
    state Idle "Stopped"
    state Running "Pumping"
    state Fault
    Idle -> Running on START [self.target <= MAX_FLOW]
    Running -> Idle on STOP
    * -> Fault on dry_run / log('dry run')
  }
}

The machine starts in its first state. on START is a transition taken when the message arrives; […] is its guard and / … its action. * -> Fault leaves every state. The linter walks the machine: a state nothing reaches, a state with no way out, a trigger no port receives are pointed out.

The rest of the system:

component Motor "Motor driver" {
  in drive: can "Drive" receives FLOW_RATE
}

component Panel "Operator panel" {
  out buttons: can "Buttons" sends START, STOP
}

system Station "Pumping station" {
  part panel: Panel
  part ctrl: Controller
  part motor: Motor
  panel.buttons -> ctrl.cmd
  ctrl.motor -> motor.drive
}

A system is one model in Arche: its parts are instances of the components, and a.p -> b.q links two ports. archec checks that what one end sends the other receives.

archec build pump.arche -o out
arche --project out

out/ is the Arche project — the YAML the Arche app reads.

4. A requirement, and who meets it#

requirement REQ-START "Start on command" {
  text "The pump shall start within 100 ms of a START command."
  priority high
  method test
}

trace satisfies Station.ctrl -> REQ-START

A requirement's text is checked for words that cannot be verified ("as fast as possible", "TBD"…). trace satisfies says the controller meets it; the test of the next step says it is verified.

5. A test, run in the simulator#

test-plan TP-START "Starting" {
  case TC-START-01 "START starts the pump" {
    verifies REQ-START
    at 0ms do ctrl.target = 50.0
    at 10ms do send(panel.buttons, START(True))
    within 100ms expect ctrl.state == "Running"
  }
}

A case is a timeline: at a moment do something, within a delay expect something. archec test builds the project and runs its test plans in the Arche simulator (arche-cli):

archec test pump.arche -o out --coverage

It says which cases pass, which requirements they verify, and how much of the state machines, messages and functions the tests went through.

6. Where it runs#

component Mcu "Pump MCU" {
  kind processor
  cores 1
  hal stm32
  chip "stm32f446re"
  inout can0: can "CAN"
}

system PumpHw "Pump hardware" {
  part mcu: Mcu
  component bus "CAN bus" {
    kind bus
    bitrate 500kbps
  }
  mcu.can0 -> bus
}

deployment Onboard "On the pump" {
  system Station
  platform PumpHw
  binary pump_fw "pump.elf" on mcu {
    thread ctl "Control" { runs ctrl; period 10ms; step regulate; priority 10 }
  }
}

The platform is a system too: processors and buses. A deployment places the logical system on it without changing it: the controller runs on a thread of the MCU, every 10 ms, calling regulate.

archec analyze pump.arche

gives each core's load and each thread's response time against its deadline (here 200 µs in 10 ms: 2 %).

7. The firmware#

archec gen pump.arche --target embassy -o generated

writes a Rust crate per binary for the Embassy async runtime — generated/embassy/pump_fw/:

  • src/arche.rs, regenerated each time: the messages and their CAN codec, one channel per link, the state machine of part ctrl (CtrlState, CtrlEvent, CtrlMachine::next), its behaviour trait (CtrlBehaviour), and the task of thread ctl. When a START arrives, the task fires the machine before calling the part. (Names come from the part, not its component: two parts of one component are two structs.)
  • src/app.rs, written once and then yours: the part's struct (Ctrl) with target at its initial value and its machine, the guard to implement (guard_target_le_max_flow), and each function: regulate is already Rust, translated from its ```arche body (a Python body would be left in a comment, to port).
  • src/main.rs, Cargo.toml and .cargo/config.toml for the STM32F446RE.
cd generated/embassy/pump_fw && cargo build --release

8. In CI#

archec ci pump.arche -o out --coverage-min 50 --junit report.xml

runs what a pipeline checks, in one command — lint, format, timing, TARA, build, the tests and their coverage — prints one line per gate, writes a JUnit report for the CI server, and exits 1 when a gate fails.

9. Sharing components: libraries#

A component used by several projects goes in a library: a folder with a library.arche that names it and gives its version.

library pump-parts "Pump parts" {
  version "1.2.0"
}

Versions sit side by side as shared/pump-parts@1.2.0/, shared/pump-parts@2.0.0/. A project says where libraries are and which versions it accepts, and includes a file of the version used:

project Pump "Water pump" {
  libraries "../shared"
  uses pump-parts "^1.2"
}

include "pump-parts/motor.arche"

^1.2 is 1.2 or newer, before 2.0 — the newest there is, unless arche.lock says otherwise:

archec lib pump.arche          # each library, the version used, the others there are
archec lib pump.arche lock     # arche.lock: the versions and a fingerprint of their files

Once locked, the version is kept until the lock is taken again, and a library whose files changed under the same version is pointed out.

Where next#

  • patterns.md — every declaration, with its properties and an example.
  • commands.md — every command.
  • The examples: examples/physics (equations, 30 lines), examples/autonomous-car (a full stack: requirements, deployment, test plans, code generation), examples/kacao (a real project, 68 files).