IEEE 11073-20601 specifies formats and protocols for representing and exchanging health data between Personal Health Devices such as weighing scales, blood pressure monitors or blood glucose monitors.
Within Bluetooth, IEEE 11073-20601 SFLOAT and FLOAT types are known as medfloat16 and medfloat32, and used to represent some GATT characteristics of non-integer type like Blood Pressure Measurement or Non-Methane Volatile Organic Compounds Concentration. Although IEEE 11073-20601 is not a pubic standard, transcoding Bluetooth characteristics to IEEE 11073-20601 attributes is described in the Personal Health Devices Transcoding white paper.
libcmedfloat is a simple C11 library for transcoding IEEE 11073-20601 SFLOAT and FLOAT values and representations:
- convert IEEE 754 floating point numbers to SFLOAT-Type and FLOAT-Type
- encode and decode SFLOAT-Type and FLOAT-Type binary representations
- compare SFLOAT-Type and FLOAT-Type values
Also includes basic command line tools (convert, encode, decode).
Status
I wrote this library while working on a Bluetooth Environmental Sensing Service implementation.
It's made public so others don't have to reinvent the wheel.
Apache-2.0 license, on an "AS IS" BASIS WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND.
SFLOAT (medfloat16) and FLOAT (medfloat32) values are specified by a mantissa and an exponent to base 10:
Mantissa and exponent are both signed integers in two's-complement binary representation, so that their value ranges are of the well known form
Note that the same number accepts multiple representations: e.g. 2 can be represented in SFLOAT-Type as either 2e0, 20e-1, 200e-2 or 2000e-3.
This is on purpose: although the dot floats with the exponent, the latter is rather intended to be fixed and indicate the precision of the measurement, e.g. to represent degree Celsius measured by a sensor with a precision of one hundredth, a fixed exponent of value -2 would be used.
Some examples can also be found in Personal Health Device Implementation Guide, Mder FLOATs and SFLOATs.
Special medfloat32 and medfloat16 values with a zero-exponent are reserved for representing:
- Positive and negative infinity: equivalent to IEEE 754 signed infinity, e.g. division by zero
- NaN: equivalent to IEEE 754 NaN, e.g. zero divided by zero, but also used to indicate missing data due to the hardware’s inability to provide a valid measurement
- NRes (Not at this resolution): no IEEE 754 equivalent, used to report that the value cannot be represented with the available range and resolution, possibly resulting from an overflow or underflow situation
- RFU: no IEEE 754 equivalent, Reserved for Future Use
| Special value | SFLOAT-Type | FLOAT-Type |
|---|---|---|
| Positive infinity | 0x07FE |
0x007FFFFE |
| Nan | 0x07FF |
0x007FFFFF |
| NRes | 0x0800 |
0x00800000 |
| RFU | 0x0801 |
0x00800001 |
| Negative infinity | 0x0802 |
0x00800002 |
Although IEEE 754 floating points and IEEE 11073-20601 decimal numbers have very different topologies, from an implementation point of view we mostly have SFLOAT-Type
The SFLOAT-Type is defined as a 16-bit value with 12-bit mantissa and 4-bit exponent:
| Maximum positive value | Minimum negative value | Minval | |
|---|---|---|---|
| IEEE 11073-20601 SFLOAT (medfloat16) | 2047e+7 | -2048e+7 | 1e-8 |
| IEEE 754 single-precision (32-bit) | 3.40e+38 | -3.40e+38 | 1.18e−38 |
Encoding: <e3..e0m11..m8> <m7......m0>.
At most
The FLOAT-Type is defined as a 32-bit value with 24-bit mantissa and 8-bit exponent, with a range of values [-8388608e+127, -1e-128]
| Format | Maximum positive value | Minimum negative value | Minval |
|---|---|---|---|
| IEEE 11073-20601 FLOAT (medfloat32) | 8388607e+127 | -8388608e+127 | 1e-128 |
| IEEE 754 double-precision (64-bit) | 1.80e+308 | -1.80e+308 | 2.23e-308 |
Encoding: <e7..e0> <m23......m16><m15......m8><m7......m0>.
At most
| IEEE 11073-20601 data type | Support |
|---|---|
| IEEE 11073-20601 SFLOAT (medfloat16) | #include "cmedfloat/medfloat16.h" |
| IEEE 11073-20601 FLOAT (medfloat32) | #include "cmedfloat/medfloat32.h" |
IEEE 11073-20601 values are naturally represented by mantissa and exponent integers in two's-complement form.
| Value | Exponent | Mantissa | |
|---|---|---|---|
| SFLOAT-Type | struct cmefloat_medfloat16 |
int8_t:4 |
int16_t:12 |
| FLOAT-Type | struct cmefloat_medfloat32 |
int8_t |
int32_t:24 |
The API is essentially by-value:
- SFLOAT-Type and FLOAT-Type values are values
- the size of the C structures representing mantissa and exponent, at most 32-bit, should not exceed the pointer size of micro-controllers commonly used in IoT sensors
Unit tests in the tests directory are a good source of API usage examples.
Most often the sensor API/driver won't support the rather niche IEEE 11073-20601 formats: floating-point values will likely be represented using the more common IEEE 754 or IEEE 854 standards.
libcmedfloat convert IEEE 754 floating point numbers to SFLOAT-Type and FLOAT-Type values, with fixed exponent or floating point.
| Floating-point conversion | Fixed-point conversion | |
|---|---|---|
| SFLOAT-Type | cmedfloat_medfloat16_from_float() |
cmedfloat_medfloat16_from_float_fixed() |
| FLOAT-Type | cmedfloat_medfloat32_from_double() |
cmedfloat_medfloat32_from_double_fixed() |
The initial conversion is always floating: the exponent is adjusted to obtain the mantissa with the maximum number of significant digits (the mantissa with the greatest allowed absolute value).
For example, the decimal number 1 is converted to the SFLOAT-Type representation 1000e-3: 1000 is the greatest possible value we can choose that fits in the 12-bit mantissa, and -3 is the corresponding exponent.
struct cmedfloat_medfloat16 mfloat16 = cmedfloat_medfloat16_from_float(1);
assert(mfloat16.e == -3);
assert(mfloat16.m == 1000)
struct cmedfloat_medfloat32 mfloat32 = cmedfloat_medfloat32_from_double(1);
assert(mfloat16.e == -6);
assert(mfloat16.m == 1000000)Additionally (using SFLOAT-Type in examples):
- when not a power of 10, the mantissa is rounded: 0.0020445 equals to
2044.5e-6, rounded to2045e-6 - when the final representation is a special value, skip the zero-exponent, giving up one digit of precision: 2046 would be represented as
2046e0, but this is the value reserved for positive infinity, hence 2046 is actually converted to205e1
Fixed point conversions indicate the precision of the sensor. For example, if 1 represents one degree Celsius, measured by a sensor with a precision of one hundredth, its SFLOAT-Type representation should be 100e-2 (1.00) instead of 1000e-3 (1.000).
For this, use the conversion API variants that permit to set the exponent:
struct cmedfloat_medfloat16 mfloat16 = cmedfloat_medfloat16_from_float_fixed(1, -2);
assert(mfloat16.e == -2);
assert(mfloat16.m == 100)
struct cmedfloat_medfloat32 mfloat32 = cmedfloat_medfloat32_from_double_fixed(1, -6);
assert(mfloat32.e == -6);
assert(mfloat32.m == 1000000)
mfloat32 = cmedfloat_medfloat32_from_double_fixed(1, -7);
assert(cmedfloat_medfloat32_isnres(mfloat32));IEEE 754 special values are converted to their IEEE 11073-20601 equivalent:
struct cmedfloat_medfloat16 mfloat16 = cmedfloat_medfloat16_from_float(nanf("NaN"));
assert(cmedfloat_medfloat16_isnan(mfloat16));
assert(isnan(cmedfloat_medfloat16_to_float(mfloat16)));
struct cmedfloat_medfloat32 mfloat32 = cmedfloat_medfloat32_from_double(INFINITY);
assert(cmedfloat_medfloat32_isinf(mfloat32));
assert(cmedfloat_medfloat32_to_double(mfloat32) == INFINITY);Values non representable are converted to IEEE 11073-20601 NRes: 23.5 with a precision of one hundredth would be written 2350e-2, that cannot be represented in SFLOAT-Type (2350 is greater than the allowed mantissa).
struct cmedfloat_medfloat16 mfloat16 = cmedfloat_medfloat16_from_float(23.5);
assert(mfloat16.e == -1);
assert(mfloat16.m == 235)
mfloat16 = cmedfloat_medfloat16_from_float_fixed(23.5, -2);
assert(cmedfloat_medfloat16_isnres(mfloat16));libcmedfloat transcode SFLOAT-Type and FLOAT-Type values to and from byte arrays suitable for data exchange.
| Encode to IEEE 11073-20601 data | Decode to IEEE 11073-20601 value | |
|---|---|---|
| SFLOAT-Type | cmedfloat_medfloat16_encode() |
cmedfloat_medfloat16_decode() |
| FLOAT-Type | cmedfloat_medfloat32_encode() |
cmedfloat_medfloat32_decode() |
Encode SFLOAT and FLOAT values to IEEE 11073-20601 data:
uint8_t data[MEDFLOAT16_LEN];
struct cmedfloat_medfloat16 mfloat16;
mfloat16 = cmedfloat_medfloat16_from_float(2047.e7f);
cmedfloat_medfloat16_encode(mfloat16, data);
assert(data[0] == 0x77);
assert(data[1] == 0xFF);
uint8_t data[MEDFLOAT32_LEN];
cmedfloat_medfloat32_encode(MEDFLOAT32_NRES, data);
assert(data[0] == 0x00);
assert(data[1] == 0x80);
assert(data[2] == 0x00);
assert(data[3] == 0x00);Decode IEEE 11073-20601 data to SFLOAT and FLOAT values:
#define UINT8_ARRAY(...) ((uint8_t[]){__VA_ARGS__})
struct cmedfloat_medfloat32 mfloat32;
mfloat32 = cmedfloat_medfloat32_decode(UINT8_ARRAY(0x00, 0xFF, 0xFB, 0x2E));
assert(mfloat32.e == 0);
assert(mfloat32.m == -1234);Produced and expected byte arrays do not depend on endianness:
| Size (bytes) | Data | |
|---|---|---|
| SFLOAT-Type | 2 | <e3..e0m11..m8> <m7......m0> |
| FLOAT-Type | 4 | <e7..e0> <m23......m16> <m15......m8> <m7......m0> |
API clients may have to swap the byte order according to the communication protocol.
Directly comparable IEEE 11073-20601 representations include decimal numbers and signed infinity:
cmedfloat_medfloatNN_cmp(x, y) < 0if x is less than ycmedfloat_medfloatNN_cmp(x, y) > 0if x is greater than ycmedfloat_medfloatNN_cmp(x, y) == 0if x equals to y
struct cmedfloat_medfloat16 mfloat16_pos1 = {.m=1};
struct cmedfloat_medfloat16 mfloat16_neg1 = {.m=-1};
assert(cmedfloat_medfloat16_cmp(mfloat16_pos1, mfloat16_neg1) > 0);
assert(cmedfloat_medfloat16_cmp(MEDFLOAT16_NEG_INF, mfloat16_neg1) < 0);
assert(cmedfloat_medfloat16_cmp(MEDFLOAT16_NAN, MEDFLOAT16_NAN) == 0);Additionally, the sensor/driver API may produce NaN/NRes representations to indicate missing or non representable measurements. These can be compared only for equality, and cannot be less than or greater than any IEEE 11073-20601 value:
cmedfloat_medfloatNN_isequal(x, y): if x and y have either the same decimal value or the same representation (e.g. they're both NaN)cmedfloat_medfloatNN_isless(x, y): if x and y are comparable, and x is less than ycmedfloat_medfloatNN_isless_eq(x, y): if x and y are comparable, and x is less than or equal to ycmedfloat_medfloatNN_isgreater(x, y): if x and y are comparable, and x is greater than ycmedfloat_medfloatNN_isgreater_eq(x, y): if x and y are comparable, and x is greater than or equal to y
struct cmedfloat_medfloat32 mfloat32_pos1 = {.m=1};
struct cmedfloat_medfloat32 mfloat32_neg1 = {.m=-1};
assert(!cmedfloat_medfloat32_isless_eq(MEDFLOAT32_NAN, MEDFLOAT32_NAN));
assert(!cmedfloat_medfloat32_isgreater(mfloat32_pos1, MEDFLOAT32_NAN));
assert(cmedfloat_medfloat32_isgreater(mfloat32_pos1, mfloat32_neg1));
assert(cmedfloat_medfloat32_isless_eq(mfloat32_neg1, mfloat32_pos1));
assert(cmedfloat_medfloat32_isgreater(mfloat32_neg1, MEDFLOAT32_NEG_INF));Comparisons involving RFU representations are undefined behavior.
A few build options are available (all disabled by default):
BUILD_SHARED_LIBS: enable to build libcmedfloat as a shared library; default is to statically link everythingCMEDFLOAT_BUILD_CLI: enable to build command line tools (convert, encode and decode)CMEDFLOAT_BUILD_DOC: enable to build API documentation (requires Doxygen)CMEDFLOAT_BUILD_TESTS: enable to build unit tests (requires Unity submodule)
For example, to enable the CLI tools (from the cmedfloat root directory):
cmake -B build -DCMEDFLOAT_BUILD_CLI=YES
cmake --build buildLibrary and command line tools can be installed, e.g. cmake --install build --prefix ~/.local will install:
- the header files to
~/.local/include/cmedfloat - the library file to
~/.local/lib - the command line tools to
~/.local/bin
MS Windows users
When the MSVC toolchain is detected, the option
/W4is used as an equivalent to-Werror -Wall(GCC/LLVM).If this option is not recognized, try to comment out all calls to the
cmedfloat_compile_options()function inCMakeLists.txt,cli/CMakeLists.txtandtests/CMakeLists.txt.
| Convert and encode | Decode | |
|---|---|---|
| SFLOAT-Type | medfloat16-encode |
medfloat16-decode |
| FLOAT-Type | medfloat32-encode |
medfloat32-decode |
Convert a floating-point literal (e.g. 1e-3, 0.001, INFINITY, NAN) to its IEEE 11073-20601 representation, then encode to 16-bit or 32-bit packets.
$ medfloat16-encode -h
usage: medfloat16-encode [-p <n>] [-v] <x>
-v verbose output
-p <n> requested precision for fixed-point conversions
<x> floating-point literal, e.g. 1e-3, 0.001, INFINITY, NAN
Special values:
$ medfloat16-encode nan
nan: 0x07ff
$ medfloat32-encode -inf
-inf 0x00800002
Normalized conversions (greatest mantissa):
$ medfloat16-encode 1
1000e-3 0xd3e8
$ medfloat32-encode 1
1000000e-6 0xfa0f4240
$ medfloat16-encode 20470000000
2047e7 0x77ff
$ medfloat32-encode 1e-128
1e-128 0x80000001
Fixed point conversions:
$ medfloat16-encode 8651234 -p 6
9e6 0x6009
medfloat32-encode 8651234 -p 3
8651e3 0x030021cb
$ medfloat16-encode 2.045 -p -2
205e-2 0xe0cd
$ medfloat32-encode 2.00865 -p -4
20087e-4 0xfc004e77
$ medfloat16-encode 1 -p -4
NRes 0x0800
Decode 16-bit or 32-bit packets to IEEE 11073-20601 representations.
Special values:
$ medfloat16-decode 0x07ff
0x07ff nan
$ medfloat32-decode 0x00800002
0x00800002 -inf
Decimal numbers:
$ medfloat16-decode 0x1002
0x1002 2e1
$ medfloat32-decode 0xFE0000C8
0xfe0000c8 200e-2
Generate API documentation into cmedfloat/apidoc:
cmake -B build -DCMEDFLOAT_BUILD_DOC=YES
cmake --build build -t apidocPull the Unity library as submodule:
git submodule init
git submodule updateBuild and run unit tests:
cmake -B build -DCMEDFLOAT_BUILD_TESTS=YES
cmake --build build -t test