1"use strict";(self.webpackChunkmeshtastic=self.webpackChunkmeshtastic||[]).push([["30627"],{86240(e,t,r){r.r(t),r.d(t,{metadata:()=>n,default:()=>m,frontMatter:()=>o,contentTitle:()=>d,toc:()=>c,assets:()=>l});var n=JSON.parse('{"id":"software/android/developer/measurement","title":"Measurement & Formatting","description":"How the Meshtastic Android/KMP app formats numbers, units, and locale-sensitive values.","source":"@site/versioned_docs/version-2.7/software/android/developer/measurement.md","sourceDirName":"software/android/developer","slug":"/software/android/developer/measurement","permalink":"/docs/2.7/software/android/developer/measurement","draft":false,"unlisted":false,"tags":[],"version":"2.7","lastUpdatedBy":"rcarteraz","sidebarPosition":9,"frontMatter":{"title":"Measurement & Formatting","sidebar_position":9,"last_updated":"2026-08-19T00:00:00.000Z","parent":"Developer Guide"},"sidebar":"Sidebar","previous":{"title":"Contributing","permalink":"/docs/2.7/software/android/developer/contributing"},"next":{"title":"Test Builds & Obtainium","permalink":"/docs/2.7/software/android/developer/test-builds"}}'),i=r(91987),s=r(67008),a=r(87622);let o={title:"Measurement & Formatting",sidebar_position:9,last_updated:new Date("2026-08-19T00:00:00.000Z"),parent:"Developer Guide"},d="Measurement & Formatting",l={},c=[{value:"Overview",id:"overview",level:2},{value:"MetricFormatter API",id:"metricformatter-api",level:2},{value:"Usage",id:"usage",level:3},{value:"NumberFormatter",id:"numberformatter",level:2},{value:"Unit Conversion",id:"unit-conversion",level:2},{value:"Adding a New Measurement Type",id:"adding-a-new-measurement-type",level:2},{value:"DateFormatter",id:"dateformatter",level:2},{value:"Design Decisions",id:"design-decisions",level:2},{value:"Related",id:"related",level:2}];function h(e){let t={a:"a",blockquote:"blockquote",code:"code",h1:"h1",h2:"h2",h3:"h3",header:"header",hr:"hr",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,s.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(t.header,{children:(0,i.jsx)(t.h1,{id:"measurement--formatting",children:"Measurement & Formatting"})}),"\n",(0,i.jsx)(t.p,{children:"How the Meshtastic Android/KMP app formats numbers, units, and locale-sensitive values."}),"\n",(0,i.jsx)(t.hr,{}),"\n",(0,i.jsx)(t.h2,{id:"overview",children:"Overview"}),"\n",(0,i.jsxs)(t.p,{children:["All measurement data transmitted by Meshtastic radios uses ",(0,i.jsx)(t.strong,{children:"metric units"})," (meters, \xb0C, hPa, m/s, etc.). The app converts and formats these values for display using two core utilities:"]}),"\n",(0,i.jsxs)(t.table,{children:[(0,i.jsx)(t.thead,{children:(0,i.jsxs)(t.tr,{children:[(0,i.jsx)(t.th,{children:"Utility"}),(0,i.jsx)(t.th,{children:"Location"}),(0,i.jsx)(t.th,{children:"Purpose"})]})}),(0,i.jsxs)(t.tbody,{children:[(0,i.jsxs)(t.tr,{children:[(0,i.jsx)(t.td,{children:(0,i.jsx)(t.code,{children:"MetricFormatter"})}),(0,i.jsx)(t.td,{children:(0,i.jsx)(t.code,{children:"core/common/.../util/MetricFormatter.kt"})}),(0,i.jsx)(t.td,{children:"Converts and formats physical measurements (temperature, pressure, speed, etc.)"})]}),(0,i.jsxs)(t.tr,{children:[(0,i.jsx)(t.td,{children:(0,i.jsx)(t.code,{children:"NumberFormatter"})}),(0,i.jsx)(t.td,{children:(0,i.jsx)(t.code,{children:"core/common/.../util/NumberFormatter.kt"})}),(0,i.jsx)(t.td,{children:"Low-level fixed-point number formatting with locale-independent dot separator"})]})]})]}),"\n",(0,i.jsxs)(t.p,{children:["Both live in ",(0,i.jsx)(t.code,{children:"org.meshtastic.core.common.util"})," and are available to all KMP targets (Android, Desktop, iOS)."]}),"\n",(0,i.jsx)(t.hr,{}),"\n",(0,i.jsx)(t.h2,{id:"metricformatter-api",children:"MetricFormatter API"}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.code,{children:"MetricFormatter"})," is a Kotlin ",(0,i.jsx)(t.code,{children:"object"})," with pure functions for each measurement type:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-kotlin",children:"object MetricFormatter {\n fun temperature(celsius: Float, isFahrenheit: Boolean): String\n fun voltage(volts: Float, decimalPlaces: Int = 2): String\n fun current(milliAmps: Float, decimalPlaces: Int = 1): String\n fun percent(value: Float, decimalPlaces: Int = 1): String\n fun humidity(value: Float): String\n fun pressure(hPa: Float, decimalPlaces: Int = 1): String\n fun snr(value: Float, decimalPlaces: Int = 1): String\n fun rssi(value: Int): String\n fun windSpeed(metersPerSecond: Float, isImperial: Boolean, decimalPlaces: Int = 1): String\n fun rainfall(millimeters: Float, isImperial: Boolean, decimalPlaces: Int = 1): String\n}\n"})}),"\n",(0,i.jsx)(t.h3,{id:"usage",children:"Usage"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-kotlin",children:'// Temperature \u2014 Fahrenheit conversion is handled automatically\nMetricFormatter.temperature(22.5f, isFahrenheit = true) // "72.5\xb0F"\nMetricFormatter.temperature(22.5f, isFahrenheit = false) // "22.5\xb0C"\n\n// Signal metrics\nMetricFormatter.snr(-5.2f) // "-5.2 dB"\nMetricFormatter.rssi(-97) // "-97 dBm"\n\n// Environment\nMetricFormatter.pressure(1013.25f) // "1013.3 hPa"\nMetricFormatter.humidity(65.0f) // "65%"\nMetricFormatter.windSpeed(3.7f, isImperial = false) // "3.7 m/s"\nMetricFormatter.windSpeed(3.7f, isImperial = true) // "8.3 mph"\nMetricFormatter.rainfall(12.3f, isImperial = false) // "12.3 mm"\nMetricFormatter.rainfall(12.3f, isImperial = true) // "0.5 in"\n\n// Power\nMetricFormatter.voltage(3.95f) // "3.95 V"\nMetricFormatter.current(125.0f) // "125.0 mA"\n'})}),"\n",(0,i.jsx)(t.hr,{}),"\n",(0,i.jsx)(t.h2,{id:"numberformatter",children:"NumberFormatter"}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.code,{children:"NumberFormatter"})," provides locale-independent decimal formatting using pure arithmetic (no ",(0,i.jsx)(t.code,{children:"String.format"})," or ",(0,i.jsx)(t.code,{children:"DecimalFormat"}),"):"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-kotlin",children:"object NumberFormatter {\n fun format(value: Double, decimalPlaces: Int): String\n fun format(value: Float, decimalPlaces: Int): String\n}\n"})}),"\n",(0,i.jsxs)(t.blockquote,{children:["\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:"Why locale-independent?"})," Meshtastic is a mesh networking app where consistency matters \u2014 sensor readings shared between nodes should look the same everywhere. ",(0,i.jsx)(t.code,{children:"NumberFormatter"})," always uses ",(0,i.jsx)(t.code,{children:"."})," as the decimal separator."]}),"\n"]}),"\n",(0,i.jsx)(t.hr,{}),"\n",(0,i.jsx)(t.h2,{id:"unit-conversion",children:"Unit Conversion"}),"\n",(0,i.jsx)(t.p,{children:"Three measurements convert away from metric for display, each gated by a boolean flag sourced from the user's device locale or preferences:"}),"\n",(0,i.jsxs)(t.table,{children:[(0,i.jsx)(t.thead,{children:(0,i.jsxs)(t.tr,{children:[(0,i.jsx)(t.th,{children:"Measurement"}),(0,i.jsx)(t.th,{children:"Flag"}),(0,i.jsx)(t.th,{children:"Source"}),(0,i.jsx)(t.th,{children:"Conversion"})]})}),(0,i.jsxs)(t.tbody,{children:[(0,i.jsxs)(t.tr,{children:[(0,i.jsx)(t.td,{children:(0,i.jsx)(t.code,{children:"temperature"})}),(0,i.jsx)(t.td,{children:(0,i.jsx)(t.code,{children:"isFahrenheit"})}),(0,i.jsx)(t.td,{children:(0,i.jsx)(t.code,{children:"getSystemTemperatureUnit()"})}),(0,i.jsx)(t.td,{children:(0,i.jsx)(t.code,{children:"\xb0F = \xb0C \xd7 1.8 + 32"})})]}),(0,i.jsxs)(t.tr,{children:[(0,i.jsx)(t.td,{children:(0,i.jsx)(t.code,{children:"windSpeed"})}),(0,i.jsx)(t.td,{children:(0,i.jsx)(t.code,{children:"isImperial"})}),(0,i.jsx)(t.td,{children:(0,i.jsx)(t.code,{children:"getSystemMeasurementSystem()"})}),(0,i.jsx)(t.td,{children:"m/s \xd7 2.23694 \u2192 mph"})]}),(0,i.jsxs)(t.tr,{children:[(0,i.jsx)(t.td,{children:(0,i.jsx)(t.code,{children:"rainfall"})}),(0,i.jsx)(t.td,{children:(0,i.jsx)(t.code,{children:"isImperial"})}),(0,i.jsx)(t.td,{children:(0,i.jsx)(t.code,{children:"getSystemMeasurementSystem()"})}),(0,i.jsx)(t.td,{children:"mm \xf7 25.4 \u2192 in"})]})]})]}),"\n",(0,i.jsxs)(t.p,{children:["The two source functions (in ",(0,i.jsx)(t.code,{children:"core/common/.../ut
1il/MeasurementSystem.kt"}),") are deliberately separate: some locales mix systems (the UK uses miles for distance but Celsius for temperature), so temperature must never be derived from the distance unit. On Android, ",(0,i.jsx)(t.code,{children:"getSystemTemperatureUnit()"})," delegates to ",(0,i.jsx)(t.code,{children:"androidx.core.text.util.LocalePreferences"}),", which resolves CLDR locale data and honors the Android 14+ Regional preferences temperature override."]}),"\n",(0,i.jsxs)(t.p,{children:["Everything else (voltage, current, pressure, ",(0,i.jsx)(a.A,{term:"SNR",definition:"A measure used in communications to quantify the level of a desired signal to the level of background noise. In Meshtastic and other wireless systems, a higher SNR indicates a clearer signal that can enhance the reliability and quality of data transmission.",routePath:"/docs/terms/",children:"SNR"}),", ",(0,i.jsx)(a.A,{term:"RSSI",definition:"The raw power level of a received signal, where a value closer to zero is stronger. Clients rate signal quality from SNR rather than RSSI.",routePath:"/docs/terms/",children:"RSSI"}),", humidity, percent) displays in its native metric units. The user-facing ",(0,i.jsx)(t.a,{href:"/docs/2.7/software/android/user/units-and-locale",children:"Units & Locale"})," page explains what end users see."]}),"\n",(0,i.jsx)(t.hr,{}),"\n",(0,i.jsx)(t.h2,{id:"adding-a-new-measurement-type",children:"Adding a New Measurement Type"}),"\n",(0,i.jsx)(t.p,{children:"To add a new measurement formatter:"}),"\n",(0,i.jsxs)(t.ol,{children:["\n",(0,i.jsxs)(t.li,{children:["\n",(0,i.jsxs)(t.p,{children:[(0,i.jsxs)(t.strong,{children:["Add a function to ",(0,i.jsx)(t.code,{children:"MetricFormatter"})]})," in ",(0,i.jsx)(t.code,{children:"core/common/src/commonMain/kotlin/org/meshtastic/core/common/util/MetricFormatter.kt"}),":"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-kotlin",children:'fun radiation(microSieverts: Float, decimalPlaces: Int = 2): String =\n "${NumberFormatter.format(microSieverts, decimalPlaces)} \u03BCSv/h"\n'})}),"\n"]}),"\n",(0,i.jsxs)(t.li,{children:["\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:"Add tests"})," in ",(0,i.jsx)(t.code,{children:"core/common/src/commonTest/"}),":"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-kotlin",children:'@Test\nfun radiationFormatting() {\n assertEquals("0.15 \u03BCSv/h", MetricFormatter.radiation(0.15f))\n assertEquals("1.23 \u03BCSv/h", MetricFormatter.radiation(1.234f))\n}\n'})}),"\n"]}),"\n",(0,i.jsxs)(t.li,{children:["\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:"Use in UI"})," \u2014 call from any ",(0,i.jsx)(t.code,{children:"commonMain"})," composable or ViewModel:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-kotlin",children:"Text(text = MetricFormatter.radiation(node.radiationLevel))\n"})}),"\n"]}),"\n",(0,i.jsxs)(t.li,{children:["\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.strong,{children:"Run verification"}),":"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-bash",children:"./gradlew :core:common:allTests\n"})}),"\n"]}),"\n"]}),"\n",(0,i.jsx)(t.hr,{}),"\n",(0,i.jsx)(t.h2,{id:"dateformatter",children:"DateFormatter"}),"\n",(0,i.jsxs)(t.p,{children:["Date and time formatting uses the ",(0,i.jsx)(t.code,{children:"DateFormatter"})," ",(0,i.jsx)(t.code,{children:"expect object"})," with platform-specific ",(0,i.jsx)(t.code,{children:"actual"})," implementations:"]}),"\n",(0,i.jsxs)(t.table,{children:[(0,i.jsx)(t.thead,{children:(0,i.jsxs)(t.tr,{children:[(0,i.jsx)(t.th,{children:"Function"}),(0,i.jsx)(t.th,{children:"Output Example"})]})}),(0,i.jsxs)(t.tbody,{children:[(0,i.jsxs)(t.tr,{children:[(0,i.jsx)(t.td,{children:(0,i.jsx)(t.code,{children:"formatRelativeTime()"})}),(0,i.jsx)(t.td,{children:'"5 min ago"'})]}),(0,i.jsxs)(t.tr,{children:[(0,i.jsx)(t.td,{children:(0,i.jsx)(t.code,{children:"formatDateTime()"})}),(0,i.jsx)(t.td,{children:'"May 13, 2026 2:30 PM"'})]}),(0,i.jsxs)(t.tr,{children:[(0,i.jsx)(t.td,{children:(0,i.jsx)(t.code,{children:"formatShortDate()"})}),(0,i.jsx)(t.td,{children:'"May 13"'})]}),(0,i.jsxs)(t.tr,{children:[(0,i.jsx)(t.td,{children:(0,i.jsx)(t.code,{children:"formatTime()"})}),(0,i.jsx)(t.td,{children:'"2:30 PM"'})]}),(0,i.jsxs)(t.tr,{children:[(0,i.jsx)(t.td,{children:(0,i.jsx)(t.code,{children:"formatTimeWithSeconds()"})}),(0,i.jsx)(t.td,{children:'"2:30:45 PM"'})]}),(0,i.jsxs)(t.tr,{children:[(0,i.jsx)(t.td,{children:(0,i.jsx)(t.code,{children:"formatDate()"})}),(0,i.jsx)(t.td,{children:'"2026-05-13"'})]}),(0,i.jsxs)(t.tr,{children:[(0,i.jsx)(t.td,{children:(0,i.jsx)(t.code,{children:"formatDateTimeShort()"})}
1),(0,i.jsx)(t.td,{children:'"5/13/26 2:30 PM"'})]})]})]}),"\n",(0,i.jsxs)(t.p,{children:["Unlike ",(0,i.jsx)(t.code,{children:"MetricFormatter"}),", ",(0,i.jsx)(t.code,{children:"DateFormatter"})," is declared with ",(0,i.jsx)(t.code,{children:"expect"}),"/",(0,i.jsx)(t.code,{children:"actual"})," (an ",(0,i.jsx)(t.code,{children:"expect object"})," in ",(0,i.jsx)(t.code,{children:"commonMain"}),", an ",(0,i.jsx)(t.code,{children:"actual object"})," per platform) because date formatting inherently depends on platform locale APIs."]}),"\n",(0,i.jsx)(t.hr,{}),"\n",(0,i.jsx)(t.h2,{id:"design-decisions",children:"Design Decisions"}),"\n",(0,i.jsxs)(t.table,{children:[(0,i.jsx)(t.thead,{children:(0,i.jsxs)(t.tr,{children:[(0,i.jsx)(t.th,{children:"Decision"}),(0,i.jsx)(t.th,{children:"Rationale"})]})}),(0,i.jsxs)(t.tbody,{children:[(0,i.jsxs)(t.tr,{children:[(0,i.jsxs)(t.td,{children:["Locale-independent decimal separator (",(0,i.jsx)(t.code,{children:"."}),")"]}),(0,i.jsx)(t.td,{children:"Mesh data shared between nodes must be consistent"})]}),(0,i.jsxs)(t.tr,{children:[(0,i.jsxs)(t.td,{children:["Pure arithmetic formatting (no ",(0,i.jsx)(t.code,{children:"DecimalFormat"}),")"]}),(0,i.jsx)(t.td,{children:"Works identically on JVM, Native, and JS targets"})]}),(0,i.jsxs)(t.tr,{children:[(0,i.jsx)(t.td,{children:"Only temperature, wind speed, and rainfall convert"}),(0,i.jsx)(t.td,{children:"The remaining metric units are universally understood in their native form"})]}),(0,i.jsxs)(t.tr,{children:[(0,i.jsxs)(t.td,{children:[(0,i.jsx)(t.code,{children:"object"})," singleton pattern"]}),(0,i.jsx)(t.td,{children:"Stateless utility \u2014 no instance management needed"})]})]})]}),"\n",(0,i.jsx)(t.hr,{}),"\n",(0,i.jsx)(t.h2,{id:"related",children:"Related"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.strong,{children:"User-facing docs"}),": ",(0,i.jsx)(t.a,{href:"/docs/2.7/software/android/user/units-and-locale",children:"Units & Locale"})," explains what end users see"]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.strong,{children:"Source code"}),": ",(0,i.jsx)(t.code,{children:"core/common/src/commonMain/kotlin/org/meshtastic/core/common/util/MetricFormatter.kt"})]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.strong,{children:"Tests"}),": ",(0,i.jsx)(t.code,{children:"core/common/src/commonTest/kotlin/org/meshtastic/core/common/util/MetricFormatterTest.kt"})]}),"\n"]})]})}function m(e={}){let{wrapper:t}={...(0,s.R)(),...e.components};return t?(0,i.jsx)(t,{...e,children:(0,i.jsx)(h,{...e})}):h(e)}},67008(e,t,r){r.d(t,{R:()=>a,x:()=>o});var n=r(71763);let i={},s=n.createContext(i);function a(e){let t=n.useContext(s);return n.useMemo(function(){return"function"==typeof e?e(t):{...t,...e}},[t,e])}function o(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:a(e.components),n.createElement(s.Provider,{value:t},e.children)}},87622(e,t,r){r.d(t,{A:()=>a});var n=r(71763),i=r(37773),s=r(91987);function a({term:e,definition:t,abbreviation:r,routePath:o="/glossary",children:d}
1){let[l,c]=(0,n.useState)(!1),[h,m]=(0,n.useState)(null),[u,x]=(0,n.useState)("top"),j=(0,n.useRef)(null),p=(0,n.useRef)(null),f=(0,n.useCallback)(()=>{let e;if(!j.current||!p.current)return;let t=j.current.getBoundingClientRect(),r=p.current.getBoundingClientRect(),n=window.innerWidth,i=window.innerHeight,s=document.querySelector(".navbar"),a=s?s.getBoundingClientRect().bottom:0,o=t.top-a>=r.height+8,d=i-t.bottom>=r.height+8,l=o||!d?"top":"bottom";e="top"===l?t.top-r.height-8:t.bottom+8;let c=t.left+t.width/2-r.width/2;c=Math.max(8,Math.min(c,n-r.width-8)),x(l),m({top:Math.max(a+4,e),left:c})},[]);(0,n.useEffect)(()=>{let e;if(!l)return;let t=requestAnimationFrame(()=>{e=requestAnimationFrame(()=>{f()})}),r=()=>f(),n=()=>f();return window.addEventListener("scroll",r,!0),window.addEventListener("resize",n),()=>{cancelAnimationFrame(t),e&&cancelAnimationFrame(e),window.removeEventListener("scroll",r,!0),window.removeEventListener("resize",n)}},[l,f]);let g=(0,i.P_)("docusaurus-plugin-glossary"),v=(0,n.useMemo)(()=>{if(t&&"string"==typeof t&&t.length>0)return t;let r=(g&&g.terms||[]).find(t=>"string"==typeof t.term&&t.term.toLowerCase()===String(e).toLowerCase());return r&&r.definition?r.definition:void 0},[t,g,e]),w=(0,n.useMemo)(()=>{let t=r;if(!t){let r=(g&&g.terms||[]).find(t=>"string"==typeof t.term&&t.term.toLowerCase()===String(e).toLowerCase());t=r&&r.abbreviation}if("string"!=typeof t)return;let n=t.trim();if(n&&n.toLowerCase()!==String(e).toLowerCase())return n},[r,g,e]),b=(0,n.useMemo)(()=>o&&"string"==typeof o&&o.length>0?o:g&&g.routePath||"/glossary",[g,o]),F=d||e,M=e.toLowerCase().replace(/\s+/g,"-");return(0,s.jsxs)("span",{ref:j,className:"glossaryTermWrapper_ud7W",children:[(0,s.jsx)("a",{href:`${b}#${M}`,className:"glossaryTerm_x2nJ",onMouseEnter:()=>c(!0),onMouseLeave:()=>c(!1),onFocus:()=>c(!0),onBlur:()=>c(!1),"aria-describedby":`tooltip-${M}`,children:F}),v&&(0,s.jsxs)("span",{ref:p,id:`tooltip-${M}`,className:`tooltip_WjgA ${l?"tooltipVisible_AjfD":""} ${"top"===u?"tooltipTop_uMN4":"tooltipBottom_U9P5"} tooltipFloating_nLk7`,role:"tooltip",style:l&&h?{top:`${h.top}px`,left:`${h.left}px`}:void 0,children:[(0,s.jsx)("strong",{children:e}),w?` (${w}). `:"",v]})]})}}}]);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.