mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
Matson MA98415: Initial support (#6212)
Protocol source: https://github.com/jakka351/Battery-Monitor-Protocol
This commit is contained in:
@@ -158,6 +158,7 @@ public interface DeviceCoordinator {
|
||||
PV_EQUIPMENT,
|
||||
GLUCOSE_METER,
|
||||
BLOOD_PRESSURE_METER,
|
||||
BATTERY_MONITOR,
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.bm2
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.bm2.Bm2Support
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class Bm2Coordinator : AbstractBLEDeviceCoordinator() {
|
||||
override fun getSupportedDeviceName(): Pattern {
|
||||
return Pattern.compile("^MATSON Monitor$")
|
||||
}
|
||||
|
||||
override fun getManufacturer(): String {
|
||||
return "Matson"
|
||||
}
|
||||
|
||||
override fun getDeviceSupportClass(device: GBDevice): Class<out DeviceSupport> {
|
||||
return Bm2Support::class.java
|
||||
}
|
||||
|
||||
override fun getDeviceNameResource(): Int {
|
||||
return R.string.devicetype_battery_monitor
|
||||
}
|
||||
|
||||
override fun suggestUnbindBeforePair(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun getBondingStyle(): Int {
|
||||
return BONDING_STYLE_NONE
|
||||
}
|
||||
|
||||
override fun getDefaultIconResource(): Int {
|
||||
return R.drawable.ic_device_car
|
||||
}
|
||||
|
||||
override fun getDeviceKind(device: GBDevice): DeviceCoordinator.DeviceKind {
|
||||
return DeviceCoordinator.DeviceKind.BATTERY_MONITOR
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.atcbleoepl.ATCBLEOEPLCoordin
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.bandwpseries.BandWPSeriesDeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.banglejs.BangleJSCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.binary_sensor.coordinator.BinarySensorCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.bm2.Bm2Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.braun.BraunBPW4500DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.casio.ecbs100.CasioECBS100DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.casio.gb6900.CasioGB6900DeviceCoordinator;
|
||||
@@ -827,6 +828,7 @@ public enum DeviceType {
|
||||
SONY_WH_CH720N(SonyWHCH720NCoordinator.class),
|
||||
PIXEL_BUDS_A(PixelBudsACoordinator.class),
|
||||
SHOKZ_OPENSWIM_PRO(ShokzOpenSwimProCoordinator.class),
|
||||
BM2_BATTERY_MONITOR(Bm2Coordinator.class),
|
||||
SINILINK(SinilinkCoordinator.class),
|
||||
ONETOUCH(OneTouchCoordinator.class),
|
||||
SOUNDCORE_LIBERTY3_PRO(SoundcoreLiberty3ProCoordinator.class),
|
||||
|
||||
+1
-1
@@ -589,7 +589,7 @@ public class BleNamesResolver {
|
||||
mServices.put("16187f00-0000-1000-8000-00807f9b34fb", "(Propr: Xiaomi Wear Service - Mi Smart Watch 4C/Redmi Band)");
|
||||
mServices.put("1314f000-1000-9000-7000-301291e21220", "(Propr: Xiaomi Wear Service - Mi Watch/Mi Watch Color/Mi Watch Color Sport)");
|
||||
mServices.put("7495fe00-a7f3-424b-92dd-4a006a3aef56", "(Propr: Xiaomi Wear Service - Mi Watch CN)");
|
||||
mServices.put("0000fff0-0000-1000-8000-00805f9b34fb", "(Propr: Nothing CMF Command");
|
||||
mServices.put("0000fff0-0000-1000-8000-00805f9b34fb", "(Propr: Custom Service)");
|
||||
mServices.put("02f00000-0000-0000-0000-00000000ffe0", "(Propr: Nothing CMF Data");
|
||||
mServices.put("02f00000-0000-0000-0000-00000000fe00", "(Propr: Nothing CMF Firmware");
|
||||
mServices.put("77d4e67c-2fe2-2334-0d35-9ccd078f529c", "(Propr: Nothing CMF Shell");
|
||||
|
||||
+36
@@ -23,8 +23,13 @@ import android.content.Intent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdateDeviceInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLESingleDeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic;
|
||||
@@ -177,4 +182,35 @@ public class DeviceInfoProfile<T extends AbstractBTLESingleDeviceSupport> extend
|
||||
intent.putExtra(EXTRA_DEVICE_INFO, deviceInfo); // TODO: broadcast a clone of the info
|
||||
return intent;
|
||||
}
|
||||
|
||||
public static List<GBDeviceEvent> toDeviceEvents(final DeviceInfo deviceInfo) {
|
||||
final GBDeviceEventVersionInfo versionCmd = new GBDeviceEventVersionInfo();
|
||||
final List<GBDeviceEvent> events = new ArrayList<>(1);
|
||||
events.add(versionCmd);
|
||||
|
||||
if (deviceInfo.getHardwareRevision() != null) {
|
||||
versionCmd.hwVersion = deviceInfo.getHardwareRevision();
|
||||
}
|
||||
|
||||
if (deviceInfo.getFirmwareRevision() != null) {
|
||||
versionCmd.fwVersion = deviceInfo.getFirmwareRevision();
|
||||
versionCmd.fwVersion2 = deviceInfo.getSoftwareRevision();
|
||||
} else if (deviceInfo.getSoftwareRevision() != null) {
|
||||
versionCmd.fwVersion = deviceInfo.getSoftwareRevision();
|
||||
}
|
||||
|
||||
if (deviceInfo.getManufacturerName() != null) {
|
||||
events.add(new GBDeviceEventUpdateDeviceInfo("MANUFACTURER: ", deviceInfo.getManufacturerName()));
|
||||
}
|
||||
|
||||
if (deviceInfo.getModelNumber() != null) {
|
||||
events.add(new GBDeviceEventUpdateDeviceInfo("MODEL: ", deviceInfo.getModelNumber()));
|
||||
}
|
||||
|
||||
if (deviceInfo.getSerialNumber() != null) {
|
||||
events.add(new GBDeviceEventUpdateDeviceInfo("SERIAL: ", deviceInfo.getSerialNumber()));
|
||||
}
|
||||
|
||||
return events;
|
||||
}
|
||||
}
|
||||
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.bm2
|
||||
|
||||
import android.bluetooth.BluetoothGatt
|
||||
import android.bluetooth.BluetoothGattCharacteristic
|
||||
import android.content.Intent
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLESingleDeviceSupport
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.IntentListener
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfoProfile
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.UUID
|
||||
|
||||
class Bm2Support : AbstractBTLESingleDeviceSupport(LOG) {
|
||||
private val cipher: Cipher = Cipher.getInstance("AES/CBC/NoPadding").apply {
|
||||
init(Cipher.DECRYPT_MODE, SecretKeySpec(KEY, "AES"), IvParameterSpec(ByteArray(16)))
|
||||
}
|
||||
|
||||
private val deviceInfoProfile: DeviceInfoProfile<Bm2Support>
|
||||
|
||||
init {
|
||||
val mListener = IntentListener { intent: Intent? ->
|
||||
intent?.action?.let { action ->
|
||||
when (action) {
|
||||
DeviceInfoProfile.ACTION_DEVICE_INFO -> {
|
||||
@Suppress("DEPRECATION")
|
||||
var deviceInfo = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
|
||||
intent.getParcelableExtra(DeviceInfoProfile.EXTRA_DEVICE_INFO, DeviceInfo::class.java)
|
||||
} else {
|
||||
intent.getParcelableExtra(DeviceInfoProfile.EXTRA_DEVICE_INFO)
|
||||
}
|
||||
LOG.debug("Device info: {}", deviceInfo)
|
||||
|
||||
val events = DeviceInfoProfile.toDeviceEvents(deviceInfo)
|
||||
for (event in events) {
|
||||
handleGBDeviceEvent(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addSupportedService(UUID_SERVICE_BM2)
|
||||
|
||||
addSupportedService(GattService.UUID_SERVICE_DEVICE_INFORMATION)
|
||||
deviceInfoProfile = DeviceInfoProfile<Bm2Support>(this)
|
||||
deviceInfoProfile.addListener(mListener)
|
||||
addSupportedProfile(deviceInfoProfile)
|
||||
}
|
||||
|
||||
override fun useAutoConnect(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun initializeDevice(builder: TransactionBuilder): TransactionBuilder {
|
||||
builder.setDeviceState(GBDevice.State.INITIALIZING)
|
||||
builder.notify(UUID_CHARACTERISTIC_BM2, true)
|
||||
builder.setDeviceState(GBDevice.State.INITIALIZED)
|
||||
return builder
|
||||
}
|
||||
|
||||
override fun onCharacteristicChanged(
|
||||
gatt: BluetoothGatt,
|
||||
characteristic: BluetoothGattCharacteristic,
|
||||
value: ByteArray
|
||||
): Boolean {
|
||||
if (UUID_CHARACTERISTIC_BM2 == characteristic.uuid) {
|
||||
val (voltage, level) = parse(cipher.doFinal(value)) ?: run {
|
||||
return true // Logged upstream
|
||||
}
|
||||
LOG.debug("Voltage: {}V, Level: {}%", voltage, level)
|
||||
val batteryEvent = GBDeviceEventBatteryInfo()
|
||||
batteryEvent.voltage = voltage
|
||||
batteryEvent.level = level
|
||||
evaluateGBDeviceEvent(batteryEvent)
|
||||
return true
|
||||
}
|
||||
|
||||
return super.onCharacteristicChanged(gatt, characteristic, value)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG = LoggerFactory.getLogger(Bm2Support::class.java)
|
||||
|
||||
private val UUID_SERVICE_BM2: UUID = UUID.fromString("0000fff0-0000-1000-8000-00805f9b34fb")
|
||||
private val UUID_CHARACTERISTIC_BM2: UUID = UUID.fromString("0000fff4-0000-1000-8000-00805f9b34fb")
|
||||
|
||||
private val KEY = byteArrayOf(108, 101, 97, 103, 101, 110, 100, -1, -2, 49, 56, 56, 50, 52, 54, 54)
|
||||
|
||||
internal fun parse(decrypted: ByteArray): Pair<Float, Int>? {
|
||||
val buf = ByteBuffer.wrap(decrypted)
|
||||
val preamble = buf.get()
|
||||
if (preamble != 0xf5.toByte()) {
|
||||
LOG.warn("Unknown preamble 0x{}", preamble.toHexString())
|
||||
return null
|
||||
}
|
||||
val voltageRaw = (buf.getShort().toInt() and 0xFFFF) ushr 4
|
||||
val level = buf.get().toInt() and 0xFF
|
||||
val voltage = voltageRaw / 100.0f
|
||||
return Pair(voltage, level)
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-25
@@ -412,31 +412,9 @@ class OneTouchSupport : AbstractBTLESingleDeviceSupport(LOG) {
|
||||
private fun handleDeviceInfo(deviceInfo: DeviceInfo) {
|
||||
LOG.debug("Device info: {}", deviceInfo)
|
||||
|
||||
val versionCmd = GBDeviceEventVersionInfo()
|
||||
|
||||
if (deviceInfo.hardwareRevision != null) {
|
||||
versionCmd.hwVersion = deviceInfo.hardwareRevision
|
||||
}
|
||||
|
||||
if (deviceInfo.firmwareRevision != null) {
|
||||
versionCmd.fwVersion = deviceInfo.firmwareRevision
|
||||
versionCmd.fwVersion2 = deviceInfo.softwareRevision
|
||||
} else if (deviceInfo.softwareRevision != null) {
|
||||
versionCmd.fwVersion = deviceInfo.softwareRevision
|
||||
}
|
||||
|
||||
handleGBDeviceEvent(versionCmd)
|
||||
|
||||
if (deviceInfo.manufacturerName != null) {
|
||||
handleGBDeviceEvent(GBDeviceEventUpdateDeviceInfo("MANUFACTURER: ", deviceInfo.manufacturerName))
|
||||
}
|
||||
|
||||
if (deviceInfo.modelNumber != null) {
|
||||
handleGBDeviceEvent(GBDeviceEventUpdateDeviceInfo("MODEL: ", deviceInfo.modelNumber))
|
||||
}
|
||||
|
||||
if (deviceInfo.serialNumber != null) {
|
||||
handleGBDeviceEvent(GBDeviceEventUpdateDeviceInfo("SERIAL: ", deviceInfo.serialNumber))
|
||||
val events = DeviceInfoProfile.toDeviceEvents(deviceInfo)
|
||||
for (event in events) {
|
||||
handleGBDeviceEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4644,6 +4644,7 @@
|
||||
<string name="cannot_open_help_url">Could not open URL</string>
|
||||
<string name="auth_settings">Authentication settings</string>
|
||||
<string name="devicetype_shokz_openswim_pro" translatable="false">Shokz OpenSwim Pro</string>
|
||||
<string name="devicetype_battery_monitor" translatable="false">Battery Monitor</string>
|
||||
<string name="devicetype_sinilink" translatable="false">Sinilink</string>
|
||||
<string name="devicetype_onetouch" translatable="false">OneTouch Glucose Meter</string>
|
||||
<string name="equalizer_preset_standard">Standard</string>
|
||||
|
||||
+1
@@ -200,6 +200,7 @@ public class AbstractDeviceCoordinatorTest extends TestBase {
|
||||
put("C 20", DeviceType.C20); // #5495
|
||||
put("OV-Touch2.6_LE", DeviceType.OVTOUCH26); // #5628
|
||||
put("BPW4500", DeviceType.BRAUN_BPW4500); // #5886
|
||||
put("MATSON Monitor", DeviceType.BM2_BATTERY_MONITOR); // #6212
|
||||
}};
|
||||
|
||||
for (Map.Entry<String, DeviceType> e : bluetoothNameToExpectedType.entrySet()) {
|
||||
|
||||
Reference in New Issue
Block a user