-
Notifications
You must be signed in to change notification settings - Fork 1
Quantities and Units
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.
[[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.
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.
| 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).
qitech_framework_core/build.rs reads the file on every build (cargo:rerun-if-changed) and writes three things into OUT_DIR:
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.
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.
#[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.
-
Make sure
qitech_libdefines it. Check theunitscrate inqitech_libfor the quantity module and the unit, for exampleunits/src/velocity.rsfor@meter_per_hour. If it's missing, add it there first, then update theqitech_librevin the framework's rootCargo.toml. -
Add it to
quantities.toml, in PascalCase:[[quantity]] name = "Velocity" units = ["MillimeterPerSecond", "MeterPerSecond", "MeterPerMinute", "MeterPerHour"]
For a new quantity, add a new
[[quantity]]block whosenamematches theqitech_lib::unitstype. -
Build. The generated code is recompiled automatically. If the name doesn't exist in
qitech_lib, the build fails with an unresolved path such asqitech_lib::units::velocity::meter_per_hour. -
Use it in a schema (
!meter_per_hour) and inbuild(::<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.
- PascalCase names are converted to snake_case by inserting
_before every capital letter. So writeMeterPerSecond, notMeterPersecond. Acronyms become separate letters:KWhwould becomek_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,fractionorpercentage. Unit tags are checked first, so such a unit would hide the plain float tags.