Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 58 additions & 42 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,34 @@

A **simple** library/framework to work with Bluetooth Smart (BLE) GATT services and characteristics.

Note: This is a fork from the no longer maintained project at https://github.com/sputnikdev/bluetooth-gatt-parser.
> This is a fork of the no longer maintained project at
> https://github.com/sputnikdev/bluetooth-gatt-parser.

Parsing a standard characteristic (Battery Level, `0x2A19`) is a one-liner:

Have a look at an example of parsing a standard characteristic ([Battery Level 0x2A19](https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.battery_level.xml)) value:
```java
BluetoothGattParserFactory.getDefault().parse("2A19", new byte[] {51}).get("Level").getInteger(null);
```
This would print 51.

**Features:**
This prints `51`.

## Features

1. Ships the standard [Bluetooth SIG GATT services and characteristics](https://www.bluetooth.com/specifications/assigned-numbers/), plus a number
of characteristics recovered from the SIG's retired characteristic XML and
generated from the current [GATT Specification Supplement](https://www.bluetooth.com/specifications/gss/).
2. Parses single- and multi-field characteristics into a user-friendly data format.
3. Serializes (writes) single- and multi-field characteristics.
4. Validates input against the GATT specification (format types and mandatory fields).
5. Supports variable-length array fields (e.g. the RR-Interval list in Heart Rate Measurement).
6. Extensible: user-defined services and characteristics via drop-in XML.
7. Supports all defined [format types](https://www.bluetooth.com/specifications/assigned-numbers/), including the
IEEE-11073 `SFLOAT`/`FLOAT` (GSS `medfloat16`/`medfloat32`) types.

1. Supports 99% of the existing/standard [GATT services and characteristics specifications](https://www.bluetooth.com/specifications/gatt).
2. Parse/read single and multi field characteristics into a user-friendly data format.
3. Writing single and multi field characteristics.
4. Validating input data whether it conforms to GATT specifications (format types and mandatory fields).
5. Extensibility. User defined services and characteristics.
6. Support for all defined [format types](https://www.bluetooth.com/specifications/assigned-numbers/format-types).
## Usage

Add the Maven dependency:

**Start using the library by including a maven dependency in your project:**
```xml
<dependency>
<groupId>org.openhab</groupId>
Expand All @@ -28,66 +38,72 @@ This would print 51.
</dependency>
```

A more complex example of parsing multi-field characteristics ([Heart Rate service](https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.service.heart_rate.xml)):
Reading and writing multi-field characteristics:

```java
// Getting a default implementation which is capable of reading/writing the standard GATT services and characteristics
// A default parser that reads/writes the bundled standard GATT services and characteristics.
BluetoothGattParser parser = BluetoothGattParserFactory.getDefault();

// Reading Body Sensor Location (0x2A38) characteristic (sigle field)
byte[] data = new byte[] {1}; // 1 == Chest
GattResponse response = parser.parse("2A38", data);
String sensorLocation = response.get("Body Sensor Location").getInteger(null); // prints 1 (Chest)
// Read Body Sensor Location (0x2A38) — a single-field characteristic.
GattResponse response = parser.parse("2A38", new byte[] {1}); // 1 == Chest
int sensorLocation = response.get("Body Sensor Location").getInteger(null); // 1 (Chest)

// Reading Heart Rate Measurement (0x2A37) characteristic (multi field)
byte[] data = new byte[] {20, 74, 13, 3};
GattResponse response = parser.parse("2A37", data);
String heartRateValue = response.get("Heart Rate Measurement Value (uint8)").getInteger(null); // prints 74
String rrIntervalValue = response.get("RR-Interval").getInteger(null); // prints 781
// Read Heart Rate Measurement (0x2A37) — a multi-field characteristic.
response = parser.parse("2A37", new byte[] {20, 74, 13, 3});
int heartRate = response.get("Heart Rate Measurement Value (8 bit resolution)").getInteger(null); // 74

// Writing Heart Rate Control Point (0x2A39) characteristic
// Write Heart Rate Control Point (0x2A39).
GattRequest request = parser.prepare("2A39");
request.setField("Heart Rate Control Point", 1); // control value to be sent to a bluetooth device
request.setField("Heart Rate Control Point", 1);
byte[] data = parser.serialize(request);
```

See more examples in the integration tests: [GenericCharacteristicParserIntegrationTest](src/test/java/org/bluetooth/gattparser/GenericCharacteristicParserIntegrationTest.java)

---
**Extending the library with user defined services and characteristics**
See more in the integration tests:
[GenericCharacteristicParserIntegrationTest](src/test/java/org/openhab/bluetooth/gattparser/GenericCharacteristicParserIntegrationTest.java).

The gatt-parser library is designed to be able to add support for some new custom services/characteristics or to override an existing ("approved") [service and characteristic](https://www.bluetooth.com/specifications/gatt). This can be done by just providing a new GATT XML file which specifies your service and characteristic (have a look at the standard definition for the [Battery Level characteristic](src/main/resources/gatt/characteristic/org.bluetooth.characteristic.battery_level.xml)). The library will read your custom files and build internal rules/conditions for parsing and serialization of your custom characteristics. This means you don't have to write any code to parse/serialize simple or complex custom characteristics.
## Extending with user-defined services and characteristics

_Loading XML GATT specification files (GATT-like specifications) from a folder:_
The library can add support for custom services/characteristics, or override a
bundled one, purely by providing a GATT XML file — no code required. See the
bundled [Battery Level characteristic](src/main/resources/gatt/characteristic/org.bluetooth.characteristic.battery_level.xml)
for the schema.

```java
BluetoothGattParser parser = BluetoothGattParserFactory.getDefault();
File extensionsFolderFile = new File(..);
gattParser.loadExtensionsFromFolder(extensions);
parser.loadExtensionsFromFolder(new File("/path/to/gatt-extensions"));
```

**A custom parser can be added for a characteristic if you are not satisfied with the default one**
If the generic parser is not enough for a given characteristic, register your own:

See the default one for a hint and a reference: [GenericCharacteristicParser](src/main/java/org/bluetooth/gattparser/GenericCharacteristicParser.java)
```java
BluetoothGattParser parser = BluetoothGattParserFactory.getDefault();
CharacteristicParser customParser = new ...; // your own implementation
parser.registerParser(CHARACTERISTIC_UUID, customParser);
parser.registerParser(CHARACTERISTIC_UUID, myCustomParser);
```

---
## Contribution
## Bundled GATT specifications

Each characteristic/service is one XML file under `src/main/resources/gatt/`. A
build-time generator indexes them into `gatt_spec_registry.json` (the `type`
attribute of each file must equal its filename).

You are welcome to contribute to the project.
The Bluetooth SIG **retired** the machine-readable characteristic XML repository
around 2019. Since then it publishes the [GATT Specification Supplement (GSS)](https://www.bluetooth.com/specifications/gss/)
as YAML, where the field structure is present but scaling/unit/presence are
encoded as a rigid mini-grammar inside prose. The characteristics added here were
derived from that GSS: scalars are high fidelity, while cases the prose cannot
express unambiguously carry an inline `<!-- REVIEW -->` marker rather than a
fabricated value.

The build process is streamlined by using standard maven tools.
## Contribution

Contributions are welcome. Build with Maven:

To build the project with maven:
```bash
mvn clean install
```

To cut a new release and upload it to the Maven Central Repository:
Cut a release to Maven Central:

```bash
mvn release:prepare -B
mvn release:perform
Expand Down
27 changes: 25 additions & 2 deletions src/main/java/org/openhab/bluetooth/gattparser/FieldHolder.java
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ public Long getLong(Long def) {
public BigInteger getBigInteger(BigInteger def) {
BigDecimal result = new BigDecimalConverter(null).convert(BigDecimal.class, prepareValue());
return result != null
? result.multiply(BigDecimal.valueOf(getMultiplier()))
? result.multiply(getMultiplierExact())
.add(BigDecimal.valueOf(getOffset())).setScale(0, RoundingMode.HALF_UP).toBigInteger()
: def;
}
Expand All @@ -175,7 +175,7 @@ public BigInteger getBigInteger(BigInteger def) {
public BigDecimal getBigDecimal(BigDecimal def) {
BigDecimal result = new BigDecimalConverter(null).convert(BigDecimal.class, prepareValue());
return result != null
? result.multiply(BigDecimal.valueOf(getMultiplier()))
? result.multiply(getMultiplierExact())
: def;
}

Expand Down Expand Up @@ -585,6 +585,29 @@ private double getMultiplier() {
return multiplier;
}

/**
* Exact BigDecimal counterpart of {@link #getMultiplier()}. The decimal exponent is applied via
* {@link BigDecimal#scaleByPowerOfTen(int)} and the binary exponent via exact powers of two, so
* the BigDecimal paths do not inherit the rounding error of {@code Math.pow(10, e)}.
*/
private BigDecimal getMultiplierExact() {
BigDecimal multiplier = BigDecimal.ONE;
if (field.getDecimalExponent() != null) {
multiplier = multiplier.scaleByPowerOfTen(field.getDecimalExponent());
}
if (field.getBinaryExponent() != null) {
int b = field.getBinaryExponent();
BigDecimal powerOfTwo = new BigDecimal(BigInteger.TWO.pow(Math.abs(b)));
multiplier = b >= 0
? multiplier.multiply(powerOfTwo)
: multiplier.divide(powerOfTwo); // negative power of two is always exact (terminates in binary)
}
if (field.getMultiplier() != null && field.getMultiplier() != 0) {
multiplier = multiplier.multiply(BigDecimal.valueOf(field.getMultiplier()));
}
return multiplier;
}

/**
* Reads offset-to-be-added to field value received from request.
* This is an extension to official GATT characteristic field specification,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,24 @@ public LinkedHashMap<String, FieldHolder> parse(Characteristic characteristic, b
continue;
}
FieldFormat fieldFormat = field.getFormat();
if (field.isRepeated()) {
// Array field (GSS uint16[n] etc.): repeat the element either a fixed number
// of times or, when repeats == 0, until the data is exhausted. Elements are
// exposed as indexed holders "Name[0]", "Name[1]", ...
int elementSize = fieldFormat.getSize();
if (elementSize == FieldFormat.FULL_SIZE || elementSize <= 0) {
throw new CharacteristicFormatException(
"Repeated field \"" + field.getName() + "\" must have a fixed-size element format.");
}
int repeats = field.getRepeats();
int index = 0;
while (repeats == 0 ? offset + elementSize <= raw.length * 8 : index < repeats) {
result.put(field.getName() + "[" + index + "]", parseField(field, raw, offset));
offset += elementSize;
index++;
}
continue;
Comment on lines +102 to +109
}
result.put(field.getName(), parseField(field, raw, offset));
if (fieldFormat.getSize() == FieldFormat.FULL_SIZE) {
// full size field, e.g. a string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,23 +82,81 @@ public Float deserializeFloat(BitSet bits) {

@Override
public Double deserializeDouble(BitSet bits) {
throw new IllegalStateException("Operation not supported");
// IEEE-11073 defines no 64-bit form; the 32-bit FLOAT is the widest, decoded as a Double.
Float value = deserializeFloat(bits);
return value != null ? value.doubleValue() : null;
}


@Override
public BitSet serializeSFloat(Float number) {
throw new IllegalStateException("Operation not supported");
return serialize(number, 12, 4, SFLOAT_NaN, SFLOAT_POSITIVE_INFINITY, SFLOAT_NEGATIVE_INFINITY);
}

@Override
public BitSet serializeFloat(Float number) {
throw new IllegalStateException("Operation not supported");
return serialize(number, 24, 8, FLOAT_NaN, FLOAT_POSITIVE_INFINITY, FLOAT_NEGATIVE_INFINITY);
}

@Override
public BitSet serializeDouble(Double number) {
throw new IllegalStateException("Operation not supported");
return serializeFloat(number != null ? number.floatValue() : null);
}

/**
* Encodes a decimal value as an IEEE-11073 SFLOAT/FLOAT: a two's-complement mantissa in the low
* {@code mantissaSize} bits and a base-10 exponent in the top {@code exponentSize} bits, laid out
* little-endian to match the deserialize path. The exponent is chosen so the mantissa fits its
* signed range while preserving as much precision as the format allows.
*/
private BitSet serialize(Float number, int mantissaSize, int exponentSize,
int nanMantissa, int posInfMantissa, int negInfMantissa) {
int mantissa;
int exponent = 0;
if (number == null || Float.isNaN(number)) {
mantissa = nanMantissa;
} else if (number == Float.POSITIVE_INFINITY) {
mantissa = posInfMantissa;
} else if (number == Float.NEGATIVE_INFINITY) {
mantissa = negInfMantissa;
} else {
long mantissaMax = (1L << (mantissaSize - 1)) - 1; // largest positive signed mantissa
long mantissaMin = -(1L << (mantissaSize - 1));
int expMax = (1 << (exponentSize - 1)) - 1;
int expMin = -(1 << (exponentSize - 1));
double value = number;
// Raise the exponent until the mantissa fits the signed range.
double scaled = value;
while ((Math.round(scaled) > mantissaMax || Math.round(scaled) < mantissaMin) && exponent < expMax) {
scaled /= 10.0;
exponent++;
}
// Lower the exponent to keep fractional precision while the mantissa still fits.
while (exponent > expMin) {
double finer = scaled * 10.0;
if (Math.round(finer) > mantissaMax || Math.round(finer) < mantissaMin) {
break;
}
scaled = finer;
exponent--;
}
mantissa = (int) Math.round(scaled);
Comment on lines +127 to +143
}

BitSet result = new BitSet(mantissaSize + exponentSize);
BitSet mantissaBits = twosComplementNumberFormatter.serialize(mantissa, mantissaSize, true);
BitSet exponentBits = twosComplementNumberFormatter.serialize(exponent, exponentSize, true);
for (int i = 0; i < mantissaSize; i++) {
if (mantissaBits.get(i)) {
result.set(i);
}
}
for (int i = 0; i < exponentSize; i++) {
if (exponentBits.get(i)) {
result.set(mantissaSize + i);
}
}
return result;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -33,42 +33,37 @@ public Integer deserializeInteger(BitSet bits, int size, boolean signed) {
if (size > 32) {
throw new IllegalArgumentException("size must be less or equal 32");
}

if (size == 1) {
signed = false;
}

boolean isNegative = signed && size > 1 && bits.get(size - 1);
int value = isNegative ? -1 : 0;
for (int i = 0; i < bits.length() && i < size; i++) {
if (isNegative && !bits.get(i)) {
value ^= 1 << i;
} else if (!isNegative && bits.get(i)) {
value |= 1 << i;
}
}
return value;
return (int) deserialize(bits, size, signed);
}

@Override
public Long deserializeLong(BitSet bits, int size, boolean signed) {
if (size > 64) {
throw new IllegalArgumentException("size must be less or equal than 64");
}
return deserialize(bits, size, signed);
}

/**
* Reads up to 64 bits, little-endian, into a long and sign-extends when required. The BitSet
* already holds the bits little-endian (bit 0 = LSB), so a straight accumulation of the low
* {@code size} bits yields the unsigned magnitude; a set top bit under two's-complement is then
* extended into the unused high bits.
*/
private long deserialize(BitSet bits, int size, boolean signed) {
if (size == 1) {
signed = false;
}

boolean isNegative = signed && size > 1 && bits.get(size - 1);
long value = isNegative ? -1L : 0L;
for (int i = 0; i < bits.length() && i < size; i++) {
if (isNegative && !bits.get(i)) {
value ^= 1L << i;
} else if (!isNegative && bits.get(i)) {
long value = 0L;
int limit = Math.min(size, bits.length());
for (int i = 0; i < limit; i++) {
if (bits.get(i)) {
value |= 1L << i;
}
}
if (signed && size > 1 && size < 64 && (value & (1L << (size - 1))) != 0) {
value |= -(1L << size); // sign-extend the two's-complement top bit
}
return value;
}

Expand Down
Loading