Skip to content

Quantities and Units

just-some-entity edited this page Sep 29, 2026 · 2 revisions

Physical values in the framework carry their unit, such as a diameter in millimeters, a speed in meters per minute or a temperature in °C. qitech_framework_core/quantities.toml is the single list of every physical quantity and unit the framework supports. Build-time code generation turns it into schema tags, Rust types, conversions and macros.

The file

[[quantity]]
name = "Length"                                  # quantity type name
units = ["Millimeter", "Centimeter", "Meter"]    # supported units, PascalCase

[[quantity]]
name = "Velocity"
units = ["MillimeterPerSecond", "MeterPerSecond", "MeterPerMinute"]

Each [[quantity]] entry has:

Field Meaning Must match
name the quantity, in PascalCase a type qitech_lib::units::<Name> and a module qitech_lib::units::<snake_name>
units the units offered for that quantity, in PascalCase a unit qitech_lib::units::<snake_name>::<snake_unit>

For example, Length / Millimeter refers to the type qitech_lib::units::Length and the unit qitech_lib::units::length::millimeter.

The file lists which units are exposed. The units themselves, with their conversion factors and symbols, are defined in the units crate inside qitech_lib: a custom uom system with f64 storage. A unit can only be listed here if qitech_lib defines it.

One name, three places

A unit's PascalCase name is converted to snake_case. That one name is then used in three places:

Where Form Example
quantities.toml PascalCase MeterPerMinute
schema type tag !snake_case speed: !meter_per_minute
Rust unit type (turbofish) snake_case ctx.config::<meter_per_minute>("speed")

The value lives in Rust as the quantity, Velocity, whatever unit you declare it in. uom stores it internally in SI base units and converts on access:

let speed: ConfigProperty<Velocity> = ctx.config::<meter_per_minute>("speed").default(12.0).build()?;

speed.get()                          // Velocity (unit-agnostic)
speed.get_as::<meter_per_second>()   // 0.2
speed.set_as::<meter_per_minute>(15.0)?;

The unit in the schema is the unit on the wire. When a value crosses the session, whether in a Registered record, a Written record, a measurement snapshot or a SetConfigProperty request, it is a plain f64 in the schema's unit. A controller that reads speed: !meter_per_minute from the schema knows that 12.0 means 12 m/min, and it sends values back in m/min. That's why the TUI's value editor expects input "in the schema's unit" (see TUI).

Nullable values work the same way: !?millimeter in the schema, ::<Option<millimeter>> in Rust, and Option<Length> as the handle's value.

Supported quantities

Quantity Schema tags
Acceleration !meter_per_second_squared !meter_per_minute_per_second
AmountOfSubstance !mole
Angle !radian !degree !revolution
AngularAcceleration !radian_per_second_squared !degree_per_second_squared !revolution_per_minute_per_second
AngularJerk !radian_per_second_cubed !degree_per_second_cubed !revolution_per_minute_per_second_squared
AngularVelocity !radian_per_second !degree_per_second !revolution_per_second !revolution_per_minute
ElectricCurrent !milliampere !centiampere !ampere
ElectricPotential !millivolt !centivolt !volt
Energy !joule !watt_hour !kilowatt_hour
Frequency !millihertz !centihertz !hertz !cycle_per_minute
Jerk !meter_per_second_cubed !meter_per_minute_per_second_squared
Length !millimeter !centimeter !meter
LuminousIntensity !candela
Mass !kilogram
Power !milliwatt !watt !kilowatt
Pressure !pascal !bar
Ratio !ratio
ThermodynamicTemperature !kelvin !degree_celsius
Time !second
Velocity !millimeter_per_second !meter_per_second !meter_per_minute
VolumeRate !cubic_meter_per_second !liter_per_second !liter_per_minute

quantities.toml is the source of truth. This table only reflects it at the time of writing.

For dimensionless numbers there are also the non-unit float tags !float, !fraction and !percentage. They aren't part of quantities.toml (see FloatSemantic in qitech_framework_core/src/schema/mod.rs).

What gets generated

qitech_framework_core/build.rs reads the file on every build (cargo:rerun-if-changed) and writes three things into OUT_DIR:

quantity.rs → qitech_framework_core::schema

This is the schema-side model of units:

pub enum Quantity { Length(LengthUnit), Velocity(VelocityUnit), /* … */ }
pub enum LengthUnit { Millimeter, Centimeter, Meter }

impl FromStr for Quantity { /* "millimeter" → Quantity::Length(LengthUnit::Millimeter) */ }
impl Quantity { pub fn as_str(&self) -> &'static str { /* → "millimeter" */ } }
impl Display for Quantity { /* → "millimeter" */ }

The schema parser uses Quantity::from_str to recognize unit tags. They are stored as FloatSemantic::Quantity(..) in ScalarPropertyKind::Float and MeasurementKind::Float. It is Serialize/Deserialize, so controllers receive the exact unit with each schema.

with_uom.rs → qitech_framework_core::{with_uom_quantities!, with_uom_units!}

These are two "callback" macros. Each calls a given macro once per quantity, or once per unit:

with_uom_quantities!(my_macro);
// expands to, for every quantity:
// my_macro!(qitech_lib::units::Length,
//           qitech_lib::units::length::Unit,
//           qitech_lib::units::length::Conversion<f64>);

with_uom_units!(my_macro);
// expands to, for every unit:
// my_macro!(qitech_lib::units::Length,
//           qitech_lib::units::length::millimeter,
//           qitech_lib::units::length::Unit,
//           qitech_lib::units::length::Conversion<f64>);

The framework uses them to implement unit support for every listed quantity without writing it out by hand:

Macro Used in Generates
with_uom_quantities! resource/conversion/property_type.rs PropertyType for each quantity (so it can be a resource value)
resource/conversion/statistics_value.rs min/max/avg/stddev support for measurements
machine/{config_property, state_property, measurement, subscribe}.rs get_as::<unit>() / set_as::<unit>() on handles and RemoteProperty
with_uom_units! resource/conversion/property_adapter.rs PropertyAdapter for each unit (the turbofish type): f64 ↔ quantity conversion, and constraints in that unit
resource/conversion/read_measurement.rs measurement snapshot export: quantity → f64 in the unit

The generated paths are absolute (qitech_lib::units::…), so a crate that calls these macros must depend on qitech_lib directly.

Also used by the macros

#[machine_build] compares the turbofish type with the schema's quantity at compile time. For speed: !meter_per_minute, it requires the type to end in meter_per_minute, e.g. ctx.config::<meter_per_minute>(..) or ctx.config::<Option<meter_per_minute>>(..) if the property is nullable.

Adding a unit or quantity

  1. Make sure qitech_lib defines it. Check the units crate in qitech_lib for the quantity module and the unit, for example units/src/velocity.rs for @meter_per_hour. If it's missing, add it there first, then update the qitech_lib rev in the framework's root Cargo.toml.

  2. Add it to quantities.toml, in PascalCase:

    [[quantity]]
    name = "Velocity"
    units = ["MillimeterPerSecond", "MeterPerSecond", "MeterPerMinute", "MeterPerHour"]

    For a new quantity, add a new [[quantity]] block whose name matches the qitech_lib::units type.

  3. Build. The generated code is recompiled automatically. If the name doesn't exist in qitech_lib, the build fails with an unresolved path such as qitech_lib::units::velocity::meter_per_hour.

  4. Use it in a schema (!meter_per_hour) and in build (::<meter_per_hour>).

The schema tags are part of the protocol. Controllers see them in MachineSchema, and Quantity is serialized with postcard (see Protocol). Append new quantities and units at the end of a list, and bump PROTOCOL_VERSION when you add, remove or reorder anything.

Naming rules

  • PascalCase names are converted to snake_case by inserting _ before every capital letter. So write MeterPerSecond, not MeterPersecond. Acronyms become separate letters: KWh would become k_wh.
  • Unit names must be unique across all quantities. Tags are looked up by unit name alone. If two quantities listed the same unit, the tag would resolve to whichever quantity comes first.
  • Don't name a unit float, fraction or percentage. Unit tags are checked first, so such a unit would hide the plain float tags.

Clone this wiki locally