Zepp OS: Derive device sources from PNP ID

This commit is contained in:
José Rebelo
2026-01-08 22:54:40 +00:00
parent 1417a85996
commit 7643fdb404
9 changed files with 237 additions and 20 deletions
@@ -18,6 +18,8 @@ package nodomain.freeyourgadget.gadgetbridge.deviceevents;
import android.content.Context;
import androidx.annotation.NonNull;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.GenericItem;
import nodomain.freeyourgadget.gadgetbridge.model.ItemWithDetails;
@@ -25,6 +27,12 @@ import nodomain.freeyourgadget.gadgetbridge.model.ItemWithDetails;
public class GBDeviceEventUpdateDeviceInfo extends GBDeviceEvent {
public ItemWithDetails item;
@NonNull
@Override
public String toString() {
return super.toString() + (item != null ? ("item: " + item.getName() + "=" + item.getDetails()) : "<null>");
}
public GBDeviceEventUpdateDeviceInfo(final ItemWithDetails item) {
this.item = item;
}
@@ -19,7 +19,11 @@ package nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo;
import android.os.Parcel;
import android.os.Parcelable;
public class DeviceInfo implements Parcelable{
import androidx.annotation.NonNull;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class DeviceInfo implements Parcelable {
private String manufacturerName;
private String modelNumber;
private String serialNumber;
@@ -28,7 +32,7 @@ public class DeviceInfo implements Parcelable{
private String softwareRevision;
private String systemId;
private String regulatoryCertificationDataList;
private String pnpId;
private byte[] pnpId;
public DeviceInfo() {
}
@@ -42,7 +46,11 @@ public class DeviceInfo implements Parcelable{
softwareRevision = in.readString();
systemId = in.readString();
regulatoryCertificationDataList = in.readString();
pnpId = in.readString();
final int pnpLength = in.readInt();
if (pnpLength >= 0) {
pnpId = new byte[pnpLength];
in.readByteArray(pnpId);
}
}
@Override
@@ -55,7 +63,12 @@ public class DeviceInfo implements Parcelable{
dest.writeString(softwareRevision);
dest.writeString(systemId);
dest.writeString(regulatoryCertificationDataList);
dest.writeString(pnpId);
if (pnpId != null) {
dest.writeInt(pnpId.length);
dest.writeByteArray(pnpId);
} else {
dest.writeInt(-1);
}
}
@Override
@@ -139,14 +152,15 @@ public class DeviceInfo implements Parcelable{
this.regulatoryCertificationDataList = regulatoryCertificationDataList;
}
public String getPnpId() {
public byte[] getPnpId() {
return pnpId;
}
public void setPnpId(String pnpId) {
public void setPnpId(byte[] pnpId) {
this.pnpId = pnpId;
}
@NonNull
@Override
public String toString() {
return "DeviceInfo{" +
@@ -158,7 +172,7 @@ public class DeviceInfo implements Parcelable{
", softwareRevision='" + softwareRevision + '\'' +
", systemId='" + systemId + '\'' +
", regulatoryCertificationDataList='" + regulatoryCertificationDataList + '\'' +
", pnpId='" + pnpId + '\'' +
", pnpId='" + GB.hexdump(pnpId) + '\'' +
'}';
}
}
@@ -168,14 +168,8 @@ public class DeviceInfoProfile<T extends AbstractBTLESingleDeviceSupport> extend
}
private void handlePnpId(final byte[] value) {
if (value.length == 7) {
// int vendorSource
//
// deviceInfo.setPnpId(pnpId);
notify(createIntent(deviceInfo));
} else {
// TODO: LOG warning
}
deviceInfo.setPnpId(value);
notify(createIntent(deviceInfo));
}
private Intent createIntent(final DeviceInfo deviceInfo) {
@@ -33,6 +33,7 @@ import java.util.ArrayList;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.capabilities.loyaltycards.LoyaltyCard;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandService;
@@ -54,6 +55,8 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo;
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfoProfile;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiUtils;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsDeviceInfoService;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class ZeppOsBtleSupport extends AbstractBTLESingleDeviceSupport implements ZeppOsCommunicator {
private static final Logger LOG = LoggerFactory.getLogger(ZeppOsBtleSupport.class);
@@ -90,6 +93,13 @@ public class ZeppOsBtleSupport extends AbstractBTLESingleDeviceSupport implement
versionCmd.fwVersion = versionCmd.fwVersion.substring(1);
}
handleGBDeviceEvent(versionCmd);
if (info.getPnpId() != null) {
new GBDeviceEventUpdatePreferences(
ZeppOsDeviceInfoService.PREF_KEY_DEVICE_PNP_ID,
GB.hexdump(info.getPnpId())
);
}
}
});
addSupportedProfile(deviceInfoProfile);
@@ -0,0 +1,7 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos
data class ZeppOsDeviceInfo(
val productId: Int,
val productVersion: Int,
val deviceSource: Int,
)
@@ -0,0 +1,90 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos
object ZeppOsDeviceSources {
// jq '.[] | [.value.productId, .value.productVersion, .deviceSource] | @csv' -r devices.json
// TODO: A few are missing: mi band 7, amazfit band 7, amazfit gtr mini
val DEVICE_SOURCES = listOf(
ZeppOsDeviceInfo(94, 256, 224),
ZeppOsDeviceInfo(94, 257, 225),
ZeppOsDeviceInfo(95, 256, 226),
ZeppOsDeviceInfo(95, 257, 227),
ZeppOsDeviceInfo(93, 256, 229),
ZeppOsDeviceInfo(93, 257, 230),
ZeppOsDeviceInfo(105, 257, 242),
ZeppOsDeviceInfo(115, 256, 246),
ZeppOsDeviceInfo(115, 259, 247),
ZeppOsDeviceInfo(116, 257, 251),
ZeppOsDeviceInfo(117, 259, 254),
ZeppOsDeviceInfo(63, 256, 414),
ZeppOsDeviceInfo(63, 257, 415),
ZeppOsDeviceInfo(113, 256, 418),
ZeppOsDeviceInfo(113, 257, 419),
ZeppOsDeviceInfo(93, 258, 6095106),
ZeppOsDeviceInfo(100, 256, 6553856),
ZeppOsDeviceInfo(100, 257, 6553857),
ZeppOsDeviceInfo(120, 257, 7864577),
ZeppOsDeviceInfo(121, 256, 7930112),
ZeppOsDeviceInfo(121, 257, 7930113),
ZeppOsDeviceInfo(122, 256, 7995648),
ZeppOsDeviceInfo(122, 257, 7995649),
ZeppOsDeviceInfo(124, 256, 8126720),
ZeppOsDeviceInfo(124, 257, 8126721),
ZeppOsDeviceInfo(124, 263, 8126727),
ZeppOsDeviceInfo(125, 256, 8192256),
ZeppOsDeviceInfo(125, 257, 8192257),
ZeppOsDeviceInfo(126, 257, 8257793),
ZeppOsDeviceInfo(127, 256, 8323328),
ZeppOsDeviceInfo(127, 257, 8323329),
ZeppOsDeviceInfo(128, 256, 8388864),
ZeppOsDeviceInfo(128, 257, 8388865),
ZeppOsDeviceInfo(129, 256, 8454400),
ZeppOsDeviceInfo(129, 257, 8454401),
ZeppOsDeviceInfo(130, 256, 8519936),
ZeppOsDeviceInfo(130, 257, 8519937),
ZeppOsDeviceInfo(130, 259, 8519939),
ZeppOsDeviceInfo(133, 256, 8716544),
ZeppOsDeviceInfo(133, 257, 8716545),
ZeppOsDeviceInfo(133, 259, 8716547),
ZeppOsDeviceInfo(134, 257, 8782081),
ZeppOsDeviceInfo(134, 264, 8782088),
ZeppOsDeviceInfo(134, 265, 8782089),
ZeppOsDeviceInfo(136, 256, 8913152),
ZeppOsDeviceInfo(136, 257, 8913153),
ZeppOsDeviceInfo(136, 259, 8913155),
ZeppOsDeviceInfo(136, 263, 8913159),
ZeppOsDeviceInfo(144, 257, 9437441),
ZeppOsDeviceInfo(145, 257, 9502977),
ZeppOsDeviceInfo(146, 256, 9568512),
ZeppOsDeviceInfo(146, 257, 9568513),
ZeppOsDeviceInfo(146, 259, 9568515),
ZeppOsDeviceInfo(149, 256, 9765120),
ZeppOsDeviceInfo(149, 257, 9765121),
ZeppOsDeviceInfo(150, 256, 9830656),
ZeppOsDeviceInfo(150, 257, 9830657),
ZeppOsDeviceInfo(150, 259, 9830659),
ZeppOsDeviceInfo(154, 256, 10092800),
ZeppOsDeviceInfo(154, 257, 10092801),
ZeppOsDeviceInfo(154, 259, 10092803),
ZeppOsDeviceInfo(154, 263, 10092807),
ZeppOsDeviceInfo(155, 257, 10158337),
ZeppOsDeviceInfo(156, 256, 10223872),
ZeppOsDeviceInfo(156, 257, 10223873),
ZeppOsDeviceInfo(156, 259, 10223875),
ZeppOsDeviceInfo(160, 256, 10486016),
ZeppOsDeviceInfo(160, 257, 10486017),
ZeppOsDeviceInfo(160, 259, 10486019),
ZeppOsDeviceInfo(161, 256, 10551552),
ZeppOsDeviceInfo(161, 257, 10551553),
ZeppOsDeviceInfo(161, 259, 10551555),
ZeppOsDeviceInfo(162, 259, 10617091),
ZeppOsDeviceInfo(163, 256, 10682624),
ZeppOsDeviceInfo(163, 257, 10682625),
ZeppOsDeviceInfo(163, 259, 10682627),
ZeppOsDeviceInfo(165, 257, 10813697),
ZeppOsDeviceInfo(165, 259, 10813699),
)
fun resolve(productId: Int, productVersion: Int): ZeppOsDeviceInfo? {
return DEVICE_SOURCES.firstOrNull { it.productId == productId && it.productVersion == productVersion }
}
}
@@ -16,17 +16,31 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services;
import androidx.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdateDeviceInfo;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.AbstractZeppOsService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.ZeppOsDeviceInfo;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.ZeppOsDeviceSources;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.ZeppOsSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.ZeppOsTransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
import nodomain.freeyourgadget.gadgetbridge.util.preferences.DevicePrefs;
public class ZeppOsDeviceInfoService extends AbstractZeppOsService {
private static final Logger LOG = LoggerFactory.getLogger(ZeppOsDeviceInfoService.class);
@@ -36,6 +50,8 @@ public class ZeppOsDeviceInfoService extends AbstractZeppOsService {
private static final byte CMD_REQUEST = 0x01;
private static final byte CMD_REPLY = 0x02;
public static String PREF_KEY_DEVICE_PNP_ID = "zepp_os_pnp_id";
public ZeppOsDeviceInfoService(final ZeppOsSupport support) {
super(support, false);
}
@@ -52,15 +68,20 @@ public class ZeppOsDeviceInfoService extends AbstractZeppOsService {
return;
}
final GBDeviceEventVersionInfo versionInfo = decodeDeviceInfo(payload);
evaluateGBDeviceEvent(versionInfo);
final List<GBDeviceEvent> events = decodeDeviceInfo(payload);
for (GBDeviceEvent event : events) {
evaluateGBDeviceEvent(event);
}
}
public static GBDeviceEventVersionInfo decodeDeviceInfo(final byte[] payload) {
public static List<GBDeviceEvent> decodeDeviceInfo(final byte[] payload) {
final ByteBuffer buf = ByteBuffer.wrap(payload).order(ByteOrder.LITTLE_ENDIAN);
final GBDeviceEventVersionInfo versionInfo = new GBDeviceEventVersionInfo();
final List<GBDeviceEvent> events = new ArrayList<>(2);
events.add(versionInfo);
if (buf.get() != CMD_REPLY) {
throw new IllegalArgumentException("not a device info reply payload");
}
@@ -68,7 +89,7 @@ public class ZeppOsDeviceInfoService extends AbstractZeppOsService {
final byte one = buf.get();
if (one != 1) {
LOG.warn("Unexpected device info payload 2nd byte {}", String.format("0x%02x", one));
return versionInfo;
return events;
}
// Active 2: 0x00000000000000ff
@@ -96,7 +117,56 @@ public class ZeppOsDeviceInfoService extends AbstractZeppOsService {
versionInfo.fwVersion = StringUtils.untilNullTerminator(buf);
}
return versionInfo;
if ((flags & 16) != 0 && buf.remaining() >= 7) {
final byte[] pnpId = new byte[7];
buf.get(pnpId);
events.add(new GBDeviceEventUpdatePreferences(
PREF_KEY_DEVICE_PNP_ID,
GB.hexdump(pnpId)
));
final ZeppOsDeviceInfo deviceInfo = getDeviceInfo(pnpId);
LOG.debug("Got PNP ID={} -> {}", GB.hexdump(pnpId), deviceInfo);
if (deviceInfo != null) {
events.add(new GBDeviceEventUpdateDeviceInfo("PRODUCT_ID: ", String.valueOf(deviceInfo.getProductId())));
events.add(new GBDeviceEventUpdateDeviceInfo("PRODUCT_VERSION: ", String.valueOf(deviceInfo.getProductVersion())));
events.add(new GBDeviceEventUpdateDeviceInfo("DEVICE_SOURCE: ", String.valueOf(deviceInfo.getDeviceSource())));
}
}
return events;
}
@Nullable
public static ZeppOsDeviceInfo getDeviceInfo(final GBDevice gbDevice) {
final DevicePrefs devicePrefs = GBApplication.getDevicePrefs(gbDevice);
final String pnpHex = devicePrefs.getString(PREF_KEY_DEVICE_PNP_ID, null);
if (pnpHex == null || pnpHex.length() != 14) {
LOG.error("Unknown or invalid PNP ID for {}: {}", gbDevice, pnpHex);
return null;
}
final byte[] pnpId = GB.hexStringToByteArray(pnpHex);
return getDeviceInfo(pnpId);
}
@Nullable
public static ZeppOsDeviceInfo getDeviceInfo(final byte[] pnpId) {
final int productId = BLETypeConversions.toUint16(pnpId, 3);
final int productVersion = BLETypeConversions.toUint16(pnpId, 5);
LOG.debug(
"Resolving source from pnpId={} for productId={}, productVersion={}",
GB.hexdump(pnpId),
productId,
productVersion
);
return ZeppOsDeviceSources.INSTANCE.resolve(productId, productVersion);
}
public void requestDeviceInfo(final ZeppOsTransactionBuilder builder) {
@@ -1,5 +1,7 @@
package nodomain.freeyourgadget.gadgetbridge.util.kotlin
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions
fun ByteArray.startsWith(prefix: ByteArray): Boolean {
if (prefix.size > this.size) {
return false
@@ -11,3 +13,7 @@ fun ByteArray.startsWith(prefix: ByteArray): Boolean {
}
return true
}
fun ByteArray.readUint16LE(offset: Int): Int {
return BLETypeConversions.toUint16(this, offset)
}
@@ -0,0 +1,18 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos
import org.junit.Assert.*
import org.junit.Test
class ZeppOsDeviceSourcesTest {
@Test
fun testNoDuplicateProductIdAndVersion() {
// We need all (productId, productVersion) pairs to be distinct
val pairs = ZeppOsDeviceSources.DEVICE_SOURCES.map { Pair(it.productId, it.productVersion) }
val uniquePairs = pairs.toSet()
assertEquals(
"Found duplicate (productId, productVersion) pairs in DEVICE_SOURCES",
pairs.size,
uniquePairs.size
)
}
}