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,
|
PV_EQUIPMENT,
|
||||||
GLUCOSE_METER,
|
GLUCOSE_METER,
|
||||||
BLOOD_PRESSURE_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.bandwpseries.BandWPSeriesDeviceCoordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.banglejs.BangleJSCoordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.banglejs.BangleJSCoordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.binary_sensor.coordinator.BinarySensorCoordinator;
|
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.braun.BraunBPW4500DeviceCoordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.casio.ecbs100.CasioECBS100DeviceCoordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.casio.ecbs100.CasioECBS100DeviceCoordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.casio.gb6900.CasioGB6900DeviceCoordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.casio.gb6900.CasioGB6900DeviceCoordinator;
|
||||||
@@ -827,6 +828,7 @@ public enum DeviceType {
|
|||||||
SONY_WH_CH720N(SonyWHCH720NCoordinator.class),
|
SONY_WH_CH720N(SonyWHCH720NCoordinator.class),
|
||||||
PIXEL_BUDS_A(PixelBudsACoordinator.class),
|
PIXEL_BUDS_A(PixelBudsACoordinator.class),
|
||||||
SHOKZ_OPENSWIM_PRO(ShokzOpenSwimProCoordinator.class),
|
SHOKZ_OPENSWIM_PRO(ShokzOpenSwimProCoordinator.class),
|
||||||
|
BM2_BATTERY_MONITOR(Bm2Coordinator.class),
|
||||||
SINILINK(SinilinkCoordinator.class),
|
SINILINK(SinilinkCoordinator.class),
|
||||||
ONETOUCH(OneTouchCoordinator.class),
|
ONETOUCH(OneTouchCoordinator.class),
|
||||||
SOUNDCORE_LIBERTY3_PRO(SoundcoreLiberty3ProCoordinator.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("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("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("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-00000000ffe0", "(Propr: Nothing CMF Data");
|
||||||
mServices.put("02f00000-0000-0000-0000-00000000fe00", "(Propr: Nothing CMF Firmware");
|
mServices.put("02f00000-0000-0000-0000-00000000fe00", "(Propr: Nothing CMF Firmware");
|
||||||
mServices.put("77d4e67c-2fe2-2334-0d35-9ccd078f529c", "(Propr: Nothing CMF Shell");
|
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.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.UUID;
|
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.AbstractBTLESingleDeviceSupport;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic;
|
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
|
intent.putExtra(EXTRA_DEVICE_INFO, deviceInfo); // TODO: broadcast a clone of the info
|
||||||
return intent;
|
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) {
|
private fun handleDeviceInfo(deviceInfo: DeviceInfo) {
|
||||||
LOG.debug("Device info: {}", deviceInfo)
|
LOG.debug("Device info: {}", deviceInfo)
|
||||||
|
|
||||||
val versionCmd = GBDeviceEventVersionInfo()
|
val events = DeviceInfoProfile.toDeviceEvents(deviceInfo)
|
||||||
|
for (event in events) {
|
||||||
if (deviceInfo.hardwareRevision != null) {
|
handleGBDeviceEvent(event)
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4644,6 +4644,7 @@
|
|||||||
<string name="cannot_open_help_url">Could not open URL</string>
|
<string name="cannot_open_help_url">Could not open URL</string>
|
||||||
<string name="auth_settings">Authentication settings</string>
|
<string name="auth_settings">Authentication settings</string>
|
||||||
<string name="devicetype_shokz_openswim_pro" translatable="false">Shokz OpenSwim Pro</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_sinilink" translatable="false">Sinilink</string>
|
||||||
<string name="devicetype_onetouch" translatable="false">OneTouch Glucose Meter</string>
|
<string name="devicetype_onetouch" translatable="false">OneTouch Glucose Meter</string>
|
||||||
<string name="equalizer_preset_standard">Standard</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("C 20", DeviceType.C20); // #5495
|
||||||
put("OV-Touch2.6_LE", DeviceType.OVTOUCH26); // #5628
|
put("OV-Touch2.6_LE", DeviceType.OVTOUCH26); // #5628
|
||||||
put("BPW4500", DeviceType.BRAUN_BPW4500); // #5886
|
put("BPW4500", DeviceType.BRAUN_BPW4500); // #5886
|
||||||
|
put("MATSON Monitor", DeviceType.BM2_BATTERY_MONITOR); // #6212
|
||||||
}};
|
}};
|
||||||
|
|
||||||
for (Map.Entry<String, DeviceType> e : bluetoothNameToExpectedType.entrySet()) {
|
for (Map.Entry<String, DeviceType> e : bluetoothNameToExpectedType.entrySet()) {
|
||||||
|
|||||||
Reference in New Issue
Block a user