The catalogue in
mirobody/res/catalog/metrics.tsv takes
only the confident codes as a metric’s identity. An unverified code stays
visible here and in the tables, and the metric keeps its own namespace until
someone confirms it.
What is in it
All tables live inmirobody/res/crosswalks/ and
ship with pip install mirobody.
Loading them:
By vendor
Fields read is every field or data type the vendor documents for health data; a field with a code is one the review could place on a LOINC code; confident is the subset that needs no further confirmation. The source column links the document each table was read from, all on 2026-09-09.
Garmin publishes only its metric families; field-level schemas need
developer approval, so most Garmin rows are unverified until the schema is
read. Honor’s service mirrors Huawei’s and its cloud side is read-only. OPPO
exposes four values to watch-face scripts and no typed health API; Xiaomi’s
public namespace is a workout data cloud with no sleep, blood oxygen, HRV,
temperature or stress types.
What no code fits
The reason is the point: it decides what to do with the quantity.
The one
ALGO_MISMATCH row is the most consequential line in the whole
crosswalk, and it is the first trap below. The nine QUALITY_META rows
(Fitbit sleep.infoCode and tempSkin.logType, WHOOP score_state,
percent_recorded, user_calibrating and total_no_data_time_milli, Oura
non_wear_time, Huawei’s ventilator maskOff) are not health data, but
they say how far to trust the health data beside them, and they are worth
storing next to the reading rather than dropping.
Read this before normalising across vendors
These are the places where two vendors’ numbers look comparable and are not. Each one silently produces a wrong value if ignored.- HRV: one vendor publishes SDNN, five publish RMSSD, four publish
nothing. Apple’s
heartRateVariabilitySDNNis SDNN. Health Connect’sHeartRateVariabilityRmssdRecord, Huawei’sheartRateVariabilityRMSSD, Fitbit’shrv.value.dailyRmssd, WHOOP’srecovery.score.hrv_rmssd_milliand Oura’ssleep.average_hrvare RMSSD. Samsung, vivo, Xiaomi and Zepp publish no HRV type. Garmin publishes beat-to-beat RR intervals, from which SDNN can be computed. LOINC 2.83 codes only the SDNN family (112429-6, 76643-6, 80404-7);RMSSDhas zero hits in the axis table. The two are different definitions and cannot share a row: the catalogue keepshrvSDNN(coded),hrvRMSSD(uncoded) andhrvDatasfor a series whose statistic is not stated. Syncing an Oura ring into Apple Health writes RMSSD into the field Apple defines as SDNN; a downstream reader that trusts the field name is then wrong. - Skin temperature depends on the site. 61008-9 Body surface
temperature (Temp/Qn/Surface) is the quantitative code for a wrist
device, marked unverified because the wrist has no code of its own and the
code’s IEEE origin is a monitor’s body temperature. 60830-7 Finger
temperature is the confident code for a ring (Oura). 60833-1 is the toe.
39106-0 Temperature of Skin is a nominal finding and cannot hold a
number; 60839-8 is the infant-incubator context. Health Connect’s
SkinTemperatureRecordcarriesmeasurementLocation, the one source that lets a decoder pick the site automatically. - Deviation is not temperature. Fitbit’s
tempSkin.value.nightlyRelativeand Oura’stemperature_deviationare offsets from a personal baseline; WHOOP’sskin_temp_celsiusand Health Connect’sbaselineare absolute. An offset goes totemperatureDelta, which has no LOINC code and must never be filed under 61008-9 or 8310-5. - Weighted minutes are not minutes. Fitbit active-zone minutes count cardio and peak minutes twice, by the documentation’s own words; Garmin intensity minutes weight vigorous minutes the same way. Undo the weighting before any LOINC duration code, or do not code them.
- Energy units. WHOOP’s
cycle.score.kilojouleis kilojoules; every other vendor is kilocalories. Divide by 4.184. - Heart-rate zones align nowhere. Fitbit has four, WHOOP five, the catalogue five, LOINC three plus a maximum zone. Not coded; the only comparable quantity across vendors is total moderate-plus-vigorous time.
- Activity intensity bands half align. Oura’s low, medium and high map
onto LOINC’s light, moderate and vigorous durations (101688-0, 101689-8,
101690-6) and is the one confident case. Health Connect’s
ActivityIntensityRecordhas two bands and no light. Huawei’sexercise_intensity.v2is not an intensity band at all; its field isexercise_type, the kind of exercise. - Four vocabularies for the sleep stages. Deep sleep is Apple
asleepDeep, Health ConnectDEEP, Huawei 深睡, Fitbitdeep, WHOOPslow_wave_sleepand Ouradeep_sleep_duration; light sleep is Huawei 浅睡, Health ConnectLIGHT, Fitbit and Ouralight, and AppleasleepCore. All of them land on 93831-6 and 93830-8, but Apple’s Core and WHOOP’s slow-wave sleep are marked unverified: synonymous, not the same name. - The sleep mode is a routing key. Fitbit has two log modes,
classicwithout stages andstageswith them. Huawei’ssleep_typehas three values: 1 with every field, 2 with no REM, 3 with only a total. Oura’ssleep.typeincludeslate_nap. Route on the mode before coding stages, or a stage the mode does not provide is stored as zero. This is the most dangerous line in this list. - A field name is not its meaning. vivo’s
DATA_TYPES.WALKING_SPEEDis in steps per minute: a cadence, not a speed. Read the unit column of the vendor’s documentation, never the name. - Statistical capability is a constraint. vivo’s watch API supports minimum and maximum for heart rate, blood oxygen and stress, and sums for standing, intensity, steps, distance and calories, and a mean for nothing. A mean code such as 103205-1 has no vivo source; do not build the mapping because it “should” exist.
- Glucose depends on the unit. Honor’s
BLOOD_GLUCOSE.measureValueis in mmol/L and codes to 15074-8, not to the catalogue’s 2339-0 (mg/dL). The catalogue alias names the analyte and the printed unit picks the variant;mirobody.translate.codedoes this for every alias. - Total is not active. Oura’s
total_caloriesand Health Connect’sTotalCaloriesBurnedRecordinclude basal metabolism; the catalogue’sactiveCaloriesdoes not. Only the active figure is coded confidently.
LOINC’s own axis defects
The LOINC 2.75 batch that added consumer-device concepts is uneven. Four codes carry axes that contradict their names; every one is marked unverified here, and all four are worth raising with Regenstrief.How the catalogue uses it
- A metric’s identity is
("loinc", code)only when the code is confident (Metric.canonical); otherwise it is("mirobody-device", name), so an unverified code never becomes a series key. - The device catalogue’s answer for a metric is handed to
mirobody.translate.codeas a catalogue alias. The alias names the analyte; when the printed unit fits a sibling of that code in the same specimen, the sibling is used (glucose in mmol/L), and when it fits none, the alias stands. - Apple’s SDNN identifier is decoded to
hrvSDNN; the Apple gait, perfusion and six-minute-walk identifiers, previously quarantined, now have rows. - Five invariants in
test_engine_coverage.pyhold the tables and the catalogue together: every code is in the axis table with a confidence, no two metrics share a code unless registered as one quantity at two grains, every crosswalk row names a real code and a real catalogue row, the Apple table says what the Apple decoder does, and the unit gate on an alias.
The base table
One row per code. The vendor column lists who produces the quantity; the field names are in the TSV.Sources
Every table names the document it was read from in its header comment; the same list ismirobody.translate.devices.SOURCES. All were read on
2026-09-09 against LOINC 2.83.
LOINC is copyright Regenstrief Institute, Inc., used under the
LOINC license; see
LICENSE-3RD-PARTY
section 3. The codes and the shortened long common names in these tables are
LOINC content; the mapping judgements and the notes are this project’s.
Changing it
Edit the TSV and open a pull request. The gate tests reject a code the axis table does not have, a metric name the catalogue does not have, a confident vendor code that contradicts the metric’s confident catalogue code (unless the two are unit variants of one analyte), and an Apple row that disagrees with the Apple decoder. A new vendor is a new<vendor>.tsv, a Source in
mirobody.translate.devices, and a column in the base table. Rerun
scripts/device_crosswalk_report.py and paste the tables here.
Notes inside the tables are in the language they were written in; many are
in Chinese, since the review was done against several vendors’ Chinese
documentation. The column names and the confidence vocabulary are English.