mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
Support Even Realities G1 Glasses as a Single Device
This change switches G1DeviceSupport from managing to individual devices to using AbstractBTLEMultiDeviceSupport which allows it to manage both BLE connections under a single support class. This also updates the Pairing activity to make it much more reliable at bonding and pairing both devices.
This commit is contained in:
+90
@@ -1,6 +1,7 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.evenrealities;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -10,14 +11,22 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.BatteryConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.GenericItem;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ItemWithDetails;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.evenrealities.G1Constants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.evenrealities.G1DeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
|
||||
|
||||
/**
|
||||
* Coordinator for the Even Realities G1 smart glasses. Describes the supported capabilities of the
|
||||
@@ -75,13 +84,94 @@ public class G1DeviceCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
return BONDING_STYLE_LAZY;
|
||||
}
|
||||
|
||||
private int getDeviceIndexForAddress(String address) {
|
||||
return GBApplication.getDeviceSpecificSharedPrefs(address)
|
||||
.getInt(G1Constants.Side.getIndexKey(),
|
||||
G1Constants.Side.INVALID.getDeviceIndex());
|
||||
}
|
||||
|
||||
private GBDevice createDevice(String address, String name, String alias, String parentFolder,
|
||||
DeviceType deviceType) {
|
||||
SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(address);
|
||||
int deviceIndex = getDeviceIndexForAddress(address);
|
||||
if (deviceIndex == G1Constants.Side.INVALID.getDeviceIndex()) {
|
||||
// Still bonding, the devices have not been linked up. Just use the device directly.
|
||||
return new GBDevice(address, name, null, parentFolder, deviceType);
|
||||
} else if (deviceIndex == G1Constants.Side.RIGHT.getDeviceIndex()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Pull the right side info out of the prefs.
|
||||
String rightName = prefs.getString(G1Constants.Side.RIGHT.getNameKey(), "");
|
||||
String rightAddress = prefs.getString(G1Constants.Side.RIGHT.getAddressKey(), "");
|
||||
|
||||
// Create the device with the left name and address.
|
||||
GBDevice gbDevice = new GBDevice(address, name, alias, parentFolder, deviceType);
|
||||
|
||||
// Put the information of the right device into the left device. This will allow the multi
|
||||
// BLE device to manage both connections.
|
||||
gbDevice.addDeviceInfo(
|
||||
new GenericItem(G1Constants.Side.RIGHT.getNameKey(), rightName));
|
||||
gbDevice.addDeviceInfo(
|
||||
new GenericItem(G1Constants.Side.RIGHT.getAddressKey(), rightAddress));
|
||||
|
||||
for (BatteryConfig batteryConfig : getBatteryConfig(gbDevice)) {
|
||||
gbDevice.setBatteryIcon(batteryConfig.getBatteryIcon(),
|
||||
batteryConfig.getBatteryIndex());
|
||||
gbDevice.setBatteryLabel(batteryConfig.getBatteryLabel(),
|
||||
batteryConfig.getBatteryIndex());
|
||||
}
|
||||
return gbDevice;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GBDevice createDevice(GBDeviceCandidate candidate, DeviceType deviceType) {
|
||||
return createDevice(candidate.getMacAddress(), candidate.getName(),
|
||||
G1Constants.getNameFromFullName(candidate.getName()), null,
|
||||
deviceType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GBDevice createDevice(Device dbDevice, DeviceType deviceType) {
|
||||
return createDevice(dbDevice.getIdentifier(), dbDevice.getName(), dbDevice.getAlias(),
|
||||
dbDevice.getParentFolder(), deviceType);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device,
|
||||
@NonNull DaoSession session) throws GBException {
|
||||
// The main device is bonded as the left. Pull out the right device and delete it too if it
|
||||
// is present.
|
||||
ItemWithDetails right_name =
|
||||
gbDevice.getDeviceInfo(G1Constants.Side.RIGHT.getNameKey());
|
||||
ItemWithDetails right_address =
|
||||
gbDevice.getDeviceInfo(G1Constants.Side.RIGHT.getAddressKey());
|
||||
if (right_name != null && !right_name.getDetails().isEmpty() && right_address != null &&
|
||||
!right_address.getDetails().isEmpty()) {
|
||||
GBDevice rightDevice =
|
||||
new GBDevice(right_address.getDetails(), right_name.getDetails(), null,
|
||||
gbDevice.getParentFolder(), gbDevice.getType());
|
||||
super.deleteDevice(rightDevice);
|
||||
DeviceHelper.getInstance().removeBond(rightDevice);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean addBatteryPollingSettings() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBatteryCount(final GBDevice device) {
|
||||
ItemWithDetails right_name =
|
||||
device.getDeviceInfo(G1Constants.Side.RIGHT.getNameKey());
|
||||
ItemWithDetails right_address =
|
||||
device.getDeviceInfo(G1Constants.Side.RIGHT.getAddressKey());
|
||||
if (right_name != null && !right_name.getDetails().isEmpty() && right_address != null &&
|
||||
!right_address.getDetails().isEmpty()) {
|
||||
return 2;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+259
-87
@@ -6,6 +6,8 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Parcelable;
|
||||
import android.view.View;
|
||||
import android.widget.AdapterView;
|
||||
@@ -15,6 +17,7 @@ import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -23,16 +26,20 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.adapter.DeviceCandidateAdapter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.evenrealities.G1DeviceConstants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ItemWithDetails;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.evenrealities.G1Constants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.BondingInterface;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.BondingUtil;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs;
|
||||
|
||||
/**
|
||||
* This class manages the pairing of both the left and right device for G1 glasses.
|
||||
@@ -44,22 +51,33 @@ public class G1PairingActivity extends AbstractGBActivity
|
||||
private static final Logger LOG = LoggerFactory.getLogger(G1PairingActivity.class);
|
||||
private final ArrayList<GBDeviceCandidate> nextLensCandidates = new ArrayList<>();
|
||||
private final BroadcastReceiver bluetoothReceiver = new G1PairingActivity.BluetoothReceiver();
|
||||
private final BroadcastReceiver deviceChangedReceiver =
|
||||
new G1PairingActivity.DeviceChangedReceiver();
|
||||
|
||||
// Variables used to determine the initial state. The user can select the left or right lens to
|
||||
// start the pairing so these are used to determine the other device that needs connection.
|
||||
private GBDeviceCandidate initialDeviceCandidate;
|
||||
private G1DeviceConstants.Side initialDeviceCandidateSide;
|
||||
private G1Constants.Side initialDeviceCandidateSide;
|
||||
|
||||
// Variables used for tracking the bonding state of both devices. The bonding steps involve
|
||||
// setting the current target to left, then initiating bonding on the current target. When the
|
||||
// bond has completed, the current target is set to the right device then bonding is initiated
|
||||
// on the current target again. currentBondingCompleteFromCallback is used to differentiate
|
||||
// calls to onBondingComplete(). onBondingComplete() will be called prematurely by GB so we need
|
||||
// to ignore that call, however when the BLE api invokes ACTION_BOND_STATE_CHANGED, it is before
|
||||
// the device has been marked as bonded so it's impossible to tell who is calling
|
||||
// onBondingComplete() just from the device state.
|
||||
// on the current target again. BondingState is used to track the status of the state machine.
|
||||
// We use a combination of listeners on ACTION_BOND_STATE_CHANGED and ACTION_DEVICE_CHANGED to
|
||||
// advance the state.
|
||||
private GBDeviceCandidate currentBondingCandidate;
|
||||
private boolean currentBondingCompleteFromCallback;
|
||||
private final Object stateLock = new Object();
|
||||
|
||||
private enum BondingState {
|
||||
STARTING,
|
||||
WAITING_ON_LEFT_BOND,
|
||||
WAITING_ON_RIGHT_BOND,
|
||||
WAITING_FOR_FIRST_DISCONNECT,
|
||||
WAITING_FOR_SECOND_DISCONNECT,
|
||||
READY_TO_FINISH
|
||||
}
|
||||
|
||||
private BondingState state;
|
||||
private GBDeviceCandidate leftDeviceCandidate;
|
||||
private GBDeviceCandidate rightDeviceCandidate;
|
||||
|
||||
@@ -117,6 +135,9 @@ public class G1PairingActivity extends AbstractGBActivity
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_even_realities_g1_pairing);
|
||||
|
||||
// State machine starts in the starting state.
|
||||
state = BondingState.STARTING;
|
||||
|
||||
// Initialize the references to all the UI element objects.
|
||||
hintTextView = findViewById(R.id.even_g1_pairing_status);
|
||||
nextLensCandidatesListView = findViewById(R.id.next_lens_candidates_list);
|
||||
@@ -140,7 +161,7 @@ public class G1PairingActivity extends AbstractGBActivity
|
||||
|
||||
// The name of the device will be something like 'Even G1_87_L_39E92'.
|
||||
// Extract the Even G1_87 out and null check it.
|
||||
String compositeDeviceName = G1DeviceConstants.getNameFromFullName(name);
|
||||
String compositeDeviceName = G1Constants.getNameFromFullName(name);
|
||||
if (compositeDeviceName == null) {
|
||||
GB.toast(getContext(),
|
||||
getString(R.string.pairing_even_realities_g1_invalid_device, name),
|
||||
@@ -152,7 +173,7 @@ public class G1PairingActivity extends AbstractGBActivity
|
||||
// The name of the device will be something like 'Even G1_87_L_39E92'.
|
||||
// Extract the L or R from out and null check it.
|
||||
initialDeviceCandidateSide =
|
||||
G1DeviceConstants.getSideFromFullName(initialDeviceCandidate.getName());
|
||||
G1Constants.getSideFromFullName(initialDeviceCandidate.getName());
|
||||
if (initialDeviceCandidateSide == null) {
|
||||
GB.toast(getContext(),
|
||||
getString(R.string.pairing_even_realities_g1_invalid_device, name),
|
||||
@@ -165,7 +186,7 @@ public class G1PairingActivity extends AbstractGBActivity
|
||||
// user.
|
||||
int currentSide = 0;
|
||||
int nextSide = 0;
|
||||
if (initialDeviceCandidateSide == G1DeviceConstants.Side.LEFT) {
|
||||
if (initialDeviceCandidateSide == G1Constants.Side.LEFT) {
|
||||
currentSide = R.string.watchface_dialog_widget_preset_left;
|
||||
nextSide = R.string.watchface_dialog_widget_preset_right;
|
||||
} else {
|
||||
@@ -186,7 +207,7 @@ public class G1PairingActivity extends AbstractGBActivity
|
||||
// Filter out all devices that don't match the selected device name and also filter
|
||||
// out the selected device.
|
||||
String nextCandidatePrefix =
|
||||
G1DeviceConstants.getNameFromFullName(nextCandidate.getName());
|
||||
G1Constants.getNameFromFullName(nextCandidate.getName());
|
||||
if (!initialDeviceCandidate.equals(nextCandidate) &&
|
||||
compositeDeviceName.equals(nextCandidatePrefix)) {
|
||||
nextLensCandidates.add(nextCandidate);
|
||||
@@ -202,12 +223,13 @@ public class G1PairingActivity extends AbstractGBActivity
|
||||
return;
|
||||
}
|
||||
|
||||
// Setup the BLE callbacks so we get notified when the devices are done bonding.
|
||||
// Setup the callbacks so we get notified when the devices are done bonding and when
|
||||
// connection state changes.
|
||||
registerBroadcastReceivers();
|
||||
|
||||
// If there is only one matching device, initiate pairing with it, no need to ask the user.
|
||||
if (nextLensCandidates.size() == 1) {
|
||||
if (initialDeviceCandidateSide == G1DeviceConstants.Side.LEFT) {
|
||||
if (initialDeviceCandidateSide == G1Constants.Side.LEFT) {
|
||||
pairDevices(initialDeviceCandidate, nextLensCandidates.get(0));
|
||||
} else {
|
||||
pairDevices(nextLensCandidates.get(0), initialDeviceCandidate);
|
||||
@@ -229,107 +251,257 @@ public class G1PairingActivity extends AbstractGBActivity
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBondingComplete(boolean success) {
|
||||
// On error, just exit. There will be a toast from the bonding code to says what went wrong.
|
||||
if (!success) {
|
||||
finish();
|
||||
}
|
||||
|
||||
// Left device is done, start pairing the right device. This function will be called again
|
||||
// when the right device is finished.
|
||||
if (currentBondingCandidate == leftDeviceCandidate && currentBondingCompleteFromCallback) {
|
||||
currentBondingCandidate = rightDeviceCandidate;
|
||||
currentBondingCompleteFromCallback = false;
|
||||
String displayName =
|
||||
G1DeviceConstants.getNameFromFullName(rightDeviceCandidate.getName()) + " " +
|
||||
getString(R.string.watchface_dialog_widget_preset_right);
|
||||
hintTextView.setText(
|
||||
getString(R.string.pairing_even_realities_g1_working, displayName));
|
||||
BondingUtil.connectThenComplete(this, currentBondingCandidate);
|
||||
}
|
||||
|
||||
// Both devices are bonded. Finish up.
|
||||
if (currentBondingCandidate == rightDeviceCandidate && currentBondingCompleteFromCallback) {
|
||||
// The initial connection prompts the bonding, but it will be a generic GATT connection.
|
||||
// Now that the device is bonded, we need to disconnect and reconnect one more time to
|
||||
// have full access to all GATT attributes.
|
||||
BondingUtil.attemptToFirstConnect(leftDeviceCandidate.getDevice());
|
||||
BondingUtil.attemptToFirstConnect(rightDeviceCandidate.getDevice());
|
||||
setResult(RESULT_OK, null);
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
final GBDeviceCandidate nextDeviceCandidate = nextLensCandidates.get(position);
|
||||
// The user may have selected either the right or the left lens. We have both devices, pair
|
||||
// them as left and right.
|
||||
if (initialDeviceCandidateSide == G1DeviceConstants.Side.LEFT) {
|
||||
if (initialDeviceCandidateSide == G1Constants.Side.LEFT) {
|
||||
pairDevices(initialDeviceCandidate, nextDeviceCandidate);
|
||||
} else {
|
||||
pairDevices(nextDeviceCandidate, initialDeviceCandidate);
|
||||
}
|
||||
}
|
||||
|
||||
private void pairDevices(GBDeviceCandidate leftCandidate, GBDeviceCandidate rightCandidate) {
|
||||
// Change the UI to pairing in progress mode.
|
||||
progressBar.setVisibility(View.VISIBLE);
|
||||
nextLensCandidatesListView.setVisibility(View.GONE);
|
||||
String displayName = G1DeviceConstants.getNameFromFullName(leftCandidate.getName()) + " " +
|
||||
getString(R.string.watchface_dialog_widget_preset_left);
|
||||
hintTextView.setText(getString(R.string.pairing_even_realities_g1_working, displayName));
|
||||
|
||||
// Set the global left and right for the callback to use later.
|
||||
leftDeviceCandidate = leftCandidate;
|
||||
rightDeviceCandidate = rightCandidate;
|
||||
|
||||
// Bond the left device. When it is completed, onBondingComplete() will be called which will
|
||||
// bond the right.
|
||||
currentBondingCandidate = leftDeviceCandidate;
|
||||
currentBondingCompleteFromCallback = false;
|
||||
BondingUtil.connectThenComplete(this, currentBondingCandidate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerBroadcastReceivers() {
|
||||
final IntentFilter bluetoothIntents = new IntentFilter();
|
||||
bluetoothIntents.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
|
||||
ContextCompat.registerReceiver(this, bluetoothReceiver, bluetoothIntents,
|
||||
ContextCompat.RECEIVER_EXPORTED);
|
||||
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(GBDevice.ACTION_DEVICE_CHANGED);
|
||||
LocalBroadcastManager.getInstance(this).registerReceiver(deviceChangedReceiver, filter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterBroadcastReceivers() {
|
||||
AndroidUtils.safeUnregisterBroadcastReceiver(this, bluetoothReceiver);
|
||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(deviceChangedReceiver);
|
||||
}
|
||||
|
||||
private void pairDevices(GBDeviceCandidate leftCandidate, GBDeviceCandidate rightCandidate) {
|
||||
// Change the UI to pairing in progress mode.
|
||||
progressBar.setVisibility(View.VISIBLE);
|
||||
nextLensCandidatesListView.setVisibility(View.GONE);
|
||||
String displayName = G1Constants.getNameFromFullName(leftCandidate.getName()) + " " +
|
||||
getString(R.string.watchface_dialog_widget_preset_left);
|
||||
hintTextView.setText(getString(R.string.pairing_even_realities_g1_working, displayName));
|
||||
|
||||
// Set the global left and right for the callback to use later.
|
||||
leftDeviceCandidate = leftCandidate;
|
||||
rightDeviceCandidate = rightCandidate;
|
||||
|
||||
// Need to remove the preferences, otherwise creating the devices will get confused.
|
||||
GBApplication.getDeviceSpecificSharedPrefs(leftDeviceCandidate.getDevice().getAddress())
|
||||
.edit().remove(G1Constants.Side.getIndexKey())
|
||||
.remove(G1Constants.Side.RIGHT.getNameKey())
|
||||
.remove(G1Constants.Side.RIGHT.getAddressKey()).apply();
|
||||
GBApplication.getDeviceSpecificSharedPrefs(rightDeviceCandidate.getDevice().getAddress())
|
||||
.edit().remove(G1Constants.Side.getIndexKey())
|
||||
.remove(G1Constants.Side.LEFT.getNameKey())
|
||||
.remove(G1Constants.Side.LEFT.getAddressKey()).apply();
|
||||
|
||||
// Bond the left device. When it is completed, onBondingComplete() will be called which will
|
||||
// bond the right.
|
||||
currentBondingCandidate = leftDeviceCandidate;
|
||||
nextBondingStep();
|
||||
}
|
||||
|
||||
private void nextBondingStep() {
|
||||
synchronized (stateLock) {
|
||||
switch (state) {
|
||||
case STARTING: {
|
||||
state = BondingState.WAITING_ON_LEFT_BOND;
|
||||
// Initiate the connection to the left device.
|
||||
GBDevice device =
|
||||
DeviceHelper.getInstance().toSupportedDevice(currentBondingCandidate);
|
||||
GBApplication.deviceService(device).disconnect();
|
||||
GBApplication.deviceService(device).connect(true);
|
||||
return;
|
||||
}
|
||||
case WAITING_ON_LEFT_BOND: {
|
||||
// Update the UI to reflect that we are bonding the right device now.
|
||||
String displayName =
|
||||
G1Constants.getNameFromFullName(rightDeviceCandidate.getName()) +
|
||||
" " + getString(R.string.watchface_dialog_widget_preset_right);
|
||||
hintTextView.setText(
|
||||
getString(R.string.pairing_even_realities_g1_working, displayName));
|
||||
state = BondingState.WAITING_ON_RIGHT_BOND;
|
||||
|
||||
// Initiate the connection to the right device.
|
||||
currentBondingCandidate = rightDeviceCandidate;
|
||||
GBDevice device =
|
||||
DeviceHelper.getInstance().toSupportedDevice(currentBondingCandidate);
|
||||
GBApplication.deviceService(device).disconnect();
|
||||
GBApplication.deviceService(device).connect(true);
|
||||
return;
|
||||
}
|
||||
case WAITING_ON_RIGHT_BOND: {
|
||||
state = BondingState.WAITING_FOR_FIRST_DISCONNECT;
|
||||
|
||||
// Add the right device info to the left device's prefs and then mark it as the
|
||||
// parent.
|
||||
GBApplication.getDeviceSpecificSharedPrefs(
|
||||
leftDeviceCandidate.getDevice().getAddress()).edit()
|
||||
.putInt(G1Constants.Side.getIndexKey(),
|
||||
G1Constants.Side.LEFT.getDeviceIndex())
|
||||
.putString(G1Constants.Side.RIGHT.getNameKey(),
|
||||
rightDeviceCandidate.getName())
|
||||
.putString(G1Constants.Side.RIGHT.getAddressKey(),
|
||||
rightDeviceCandidate.getDevice().getAddress())
|
||||
.putBoolean(GBPrefs.DEVICE_CONNECT_BACK, true).apply();
|
||||
|
||||
|
||||
// Add the left device info to the right device's pref and then mark it as the
|
||||
// child.
|
||||
GBApplication.getDeviceSpecificSharedPrefs(
|
||||
rightDeviceCandidate.getDevice().getAddress()).edit()
|
||||
.putInt(G1Constants.Side.getIndexKey(),
|
||||
G1Constants.Side.RIGHT.getDeviceIndex())
|
||||
.putString(G1Constants.Side.LEFT.getNameKey(),
|
||||
leftDeviceCandidate.getName())
|
||||
.putString(G1Constants.Side.LEFT.getAddressKey(),
|
||||
leftDeviceCandidate.getDevice().getAddress())
|
||||
.putBoolean(GBPrefs.DEVICE_CONNECT_BACK, true).apply();
|
||||
|
||||
GBDevice leftDevice =
|
||||
DeviceHelper.getInstance().toSupportedDevice(leftDeviceCandidate);
|
||||
GBDevice rightDevice =
|
||||
DeviceHelper.getInstance().toSupportedDevice(rightDeviceCandidate);
|
||||
GBApplication.deviceService(leftDevice).disconnect();
|
||||
GBApplication.deviceService(rightDevice).disconnect();
|
||||
return;
|
||||
}
|
||||
case WAITING_FOR_FIRST_DISCONNECT:
|
||||
state = BondingState.WAITING_FOR_SECOND_DISCONNECT;
|
||||
return;
|
||||
case WAITING_FOR_SECOND_DISCONNECT: {
|
||||
// Update the message on the UI.
|
||||
hintTextView.setText(
|
||||
getString(R.string.pairing_even_realities_g1_final_connect));
|
||||
|
||||
GBDevice leftDevice =
|
||||
DeviceHelper.getInstance().toSupportedDevice(leftDeviceCandidate);
|
||||
if (leftDevice.getDeviceInfo(G1Constants.Side.RIGHT.getAddressKey()) ==
|
||||
null ||
|
||||
leftDevice.getDeviceInfo(G1Constants.Side.RIGHT.getNameKey()) ==
|
||||
null) {
|
||||
GB.toast(getContext(),
|
||||
getString(R.string.pairing_even_realities_g1_invalid_device,
|
||||
leftDevice.getAddress()), Toast.LENGTH_LONG, GB.ERROR);
|
||||
onBondingComplete(false /* success */);
|
||||
return;
|
||||
}
|
||||
state = BondingState.READY_TO_FINISH;
|
||||
GBApplication.deviceService(leftDevice).connect();
|
||||
return;
|
||||
}
|
||||
case READY_TO_FINISH:
|
||||
onBondingComplete(true /* success */);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBondingComplete(boolean success) {
|
||||
// This function can be called from broadcast handlers, if we call finish() under the
|
||||
// context of a broadcast handler, the handler objects will be deleted and things will crash
|
||||
// hard in the android Activity libraries. Instead of causing that, schedule a thread to
|
||||
// close things down.
|
||||
Looper mainLooper = Looper.getMainLooper();
|
||||
new Handler(mainLooper).post(() -> {
|
||||
if (success && state == BondingState.READY_TO_FINISH) {
|
||||
setResult(RESULT_OK, null);
|
||||
finish();
|
||||
} else {
|
||||
// On error, just exit. There will be a toast from the bonding code to says what went wrong.
|
||||
finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private final class BluetoothReceiver extends BroadcastReceiver {
|
||||
@Override
|
||||
public void onReceive(final Context context, final Intent intent) {
|
||||
if (Objects.requireNonNull(intent.getAction())
|
||||
.equals(BluetoothDevice.ACTION_BOND_STATE_CHANGED)) {
|
||||
LOG.debug("ACTION_BOND_STATE_CHANGED");
|
||||
final BluetoothDevice device =
|
||||
intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
|
||||
if (!Objects.requireNonNull(intent.getAction())
|
||||
.equals(BluetoothDevice.ACTION_BOND_STATE_CHANGED)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (device != null) {
|
||||
final int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,
|
||||
BluetoothDevice.BOND_NONE);
|
||||
LOG.debug("{} Bond state: {}", device.getAddress(), bondState);
|
||||
LOG.debug("ACTION_BOND_STATE_CHANGED");
|
||||
final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
|
||||
if (device == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (bondState == BluetoothDevice.BOND_BONDED) {
|
||||
if (device.getAddress().equals(currentBondingCandidate.getMacAddress())) {
|
||||
currentBondingCompleteFromCallback = true;
|
||||
((BondingInterface) context).onBondingComplete(true);
|
||||
} else {
|
||||
// We got a callback from the wrong device. This shouldn't be possible.
|
||||
GB.toast(getContext(),
|
||||
getString(R.string.pairing_even_realities_g1_invalid_device,
|
||||
device.getAddress()), Toast.LENGTH_LONG, GB.ERROR);
|
||||
((BondingInterface) context).onBondingComplete(false);
|
||||
}
|
||||
final int bondState =
|
||||
intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE);
|
||||
LOG.debug("{} | {} Bond state: {}", intent, device.getAddress(), bondState);
|
||||
if (bondState != BluetoothDevice.BOND_BONDED) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!device.getAddress().equals(currentBondingCandidate.getMacAddress())) {
|
||||
// We got a callback from the wrong device. This shouldn't be possible.
|
||||
GB.toast(getContext(), getString(R.string.pairing_even_realities_g1_invalid_device,
|
||||
device.getAddress()), Toast.LENGTH_LONG, GB.ERROR);
|
||||
((BondingInterface) context).onBondingComplete(false);
|
||||
return;
|
||||
}
|
||||
|
||||
synchronized (stateLock) {
|
||||
if (state == BondingState.WAITING_ON_LEFT_BOND ||
|
||||
state == BondingState.WAITING_ON_RIGHT_BOND) {
|
||||
nextBondingStep();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class DeviceChangedReceiver extends BroadcastReceiver {
|
||||
@Override
|
||||
public void onReceive(final Context context, final Intent intent) {
|
||||
if (!GBDevice.ACTION_DEVICE_CHANGED.equals(intent.getAction())) {
|
||||
return;
|
||||
}
|
||||
|
||||
GBDevice device = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
|
||||
if (device == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Ignore devices that we are not trying to pair.
|
||||
if (!(device.getAddress().equals(leftDeviceCandidate.getMacAddress()) ||
|
||||
device.getAddress().equals(rightDeviceCandidate.getMacAddress()))) {
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.info("Device changed with intent {} | {} | {}", intent, device, state);
|
||||
|
||||
synchronized (stateLock) {
|
||||
if ((state == BondingState.WAITING_ON_LEFT_BOND ||
|
||||
state == BondingState.WAITING_ON_RIGHT_BOND) && device.isConnected() &&
|
||||
currentBondingCandidate.isBonded()) {
|
||||
// Device was already bonded, the BLE callback will never be called, so we need
|
||||
// to advance to the next step. This step was a no-op anyway.
|
||||
nextBondingStep();
|
||||
} else if ((state == BondingState.WAITING_FOR_FIRST_DISCONNECT ||
|
||||
state == BondingState.WAITING_FOR_SECOND_DISCONNECT) &&
|
||||
!device.isConnected()) {
|
||||
nextBondingStep();
|
||||
} else if (state == BondingState.READY_TO_FINISH && device.isConnected()) {
|
||||
ItemWithDetails right_name =
|
||||
device.getDeviceInfo(G1Constants.Side.RIGHT.getNameKey());
|
||||
ItemWithDetails right_address =
|
||||
device.getDeviceInfo(G1Constants.Side.RIGHT.getAddressKey());
|
||||
if (right_name != null && !right_name.getDetails().isEmpty() &&
|
||||
right_address != null && !right_address.getDetails().isEmpty() &&
|
||||
right_address.getDetails().equals(rightDeviceCandidate.getMacAddress())) {
|
||||
nextBondingStep();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+24
@@ -298,6 +298,30 @@ public class BLETypeConversions {
|
||||
array[offset + 7] = (byte) (value >> 56);
|
||||
}
|
||||
|
||||
public static void writeUint16BE(byte[] array, int offset, int value) {
|
||||
array[offset + 1] = (byte) value;
|
||||
array[offset] = (byte) (value >> 8);
|
||||
}
|
||||
|
||||
public static void writeUint32BE(byte[] array, int offset, int value) {
|
||||
array[offset + 3] = (byte) value;
|
||||
array[offset + 2] = (byte) (value >> 8);
|
||||
array[offset + 1] = (byte) (value >> 16);
|
||||
array[offset] = (byte) (value >> 24);
|
||||
}
|
||||
|
||||
public static void writeUint64BE(byte[] array, int offset, long value) {
|
||||
array[offset + 7] = (byte) value;
|
||||
array[offset + 6] = (byte) (value >> 8);
|
||||
array[offset + 5] = (byte) (value >> 16);
|
||||
array[offset + 4] = (byte) (value >> 24);
|
||||
array[offset + 3] = (byte) (value >> 32);
|
||||
array[offset + 2] = (byte) (value >> 40);
|
||||
array[offset + 1] = (byte) (value >> 48);
|
||||
array[offset] = (byte) (value >> 56);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a calendar object representing the current date and time.
|
||||
*/
|
||||
|
||||
+256
@@ -0,0 +1,256 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.evenrealities;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
|
||||
|
||||
public class G1Communications {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(G1Communications.class);
|
||||
|
||||
public abstract static class CommandHandler {
|
||||
private final boolean expectResponse;
|
||||
private final Function<byte[], Boolean> callback;
|
||||
protected short sequence;
|
||||
private byte[] responsePayload;
|
||||
private int retryCount;
|
||||
|
||||
public CommandHandler(boolean expectResponse, Function<byte[], Boolean> callback) {
|
||||
this.expectResponse = expectResponse;
|
||||
this.callback = callback;
|
||||
this.responsePayload = null;
|
||||
this.retryCount = 0;
|
||||
}
|
||||
|
||||
public boolean needsGlobalSequence() { return false; }
|
||||
public void setGlobalSequence(short sequence) {
|
||||
this.sequence = sequence;
|
||||
}
|
||||
public int getTimeout() {
|
||||
return G1Constants.DEFAULT_COMMAND_TIMEOUT_MS;
|
||||
}
|
||||
public int getMaxRetryCount() {
|
||||
return G1Constants.DEFAULT_RETRY_COUNT;
|
||||
}
|
||||
private synchronized boolean continueWaiting() {
|
||||
return !hasResponsePayload() && hasRetryRemaining();
|
||||
}
|
||||
|
||||
public synchronized void notifyAttempt() {
|
||||
retryCount++;
|
||||
notify();
|
||||
}
|
||||
|
||||
public synchronized void setResponsePayload(byte[] payload) {
|
||||
this.responsePayload = payload;
|
||||
notify();
|
||||
}
|
||||
|
||||
public synchronized boolean hasRetryRemaining() {
|
||||
return retryCount < getMaxRetryCount();
|
||||
}
|
||||
public synchronized boolean hasResponsePayload() {
|
||||
return responsePayload != null;
|
||||
}
|
||||
|
||||
public boolean waitForResponsePayload() {
|
||||
// Go to sleep until the either a response is gotten or there is a timeout.
|
||||
while (continueWaiting()) {
|
||||
synchronized (this) {
|
||||
try {
|
||||
wait();
|
||||
} catch (InterruptedException ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
// If the reties were exhausted return false to indicate that there was no response from
|
||||
// the glasses.
|
||||
return hasRetryRemaining();
|
||||
}
|
||||
|
||||
public byte[] getResponsePayload(){
|
||||
if (responsePayload == null) {
|
||||
throw new RuntimeException("Null payload. Did you call waitForPayload()?");
|
||||
}
|
||||
return responsePayload;
|
||||
}
|
||||
|
||||
public Function<byte[], Boolean> getCallback() {
|
||||
return callback;
|
||||
}
|
||||
|
||||
public boolean expectResponse() {
|
||||
return expectResponse;
|
||||
}
|
||||
|
||||
public int getRetryCount() {
|
||||
return retryCount;
|
||||
}
|
||||
|
||||
public abstract byte[] serialize();
|
||||
public abstract boolean responseMatches(byte[] payload);
|
||||
public abstract String getName();
|
||||
}
|
||||
|
||||
public static class CommandFirmwareInfo extends CommandHandler {
|
||||
public CommandFirmwareInfo(Function<byte[], Boolean> callback) {
|
||||
super(true, callback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] serialize() {
|
||||
return new byte[] { G1Constants.CommandId.FW_INFO_REQUEST.id, 0x74 };
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean responseMatches(byte[] payload) {
|
||||
if (payload.length < 10) {
|
||||
return false;
|
||||
}
|
||||
return payload[0] == G1Constants.CommandId.FW_INFO_RESPONSE.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "firmware_info";
|
||||
}
|
||||
}
|
||||
|
||||
public static class CommandBatteryLevel extends CommandHandler {
|
||||
public CommandBatteryLevel(Function<byte[], Boolean> callback) {
|
||||
super(true, callback);
|
||||
}
|
||||
@Override
|
||||
public byte[] serialize() {
|
||||
return new byte[] { G1Constants.CommandId.BATTERY_LEVEL.id, 0x01 };
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean responseMatches(byte[] payload) {
|
||||
if (payload.length < 1) {
|
||||
return false;
|
||||
}
|
||||
return payload[0] == G1Constants.CommandId.BATTERY_LEVEL.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "battery_level";
|
||||
}
|
||||
}
|
||||
|
||||
public static class CommandHeartBeat extends CommandHandler {
|
||||
public CommandHeartBeat(short sequence) {
|
||||
super(false, null);
|
||||
super.sequence = sequence;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] serialize() {
|
||||
return new byte[] {
|
||||
G1Constants.CommandId.HEARTBEAT.id,
|
||||
0x00, // length is a short
|
||||
0x06, // length
|
||||
// TODO: What the heck is the 0x04 and why is the sequence split?
|
||||
// Need to look at a real capture for this.
|
||||
(byte) (sequence % 0xFF),
|
||||
0x04,
|
||||
(byte) (sequence % 0xFF)
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean responseMatches(byte[] payload) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "heart_beat";
|
||||
}
|
||||
}
|
||||
|
||||
public static class CommandTimeAndWeather extends CommandHandler {
|
||||
long timeMilliseconds;
|
||||
boolean use12HourFormat;
|
||||
byte tempInCelsius;
|
||||
byte weatherIcon;
|
||||
boolean useFahrenheit;
|
||||
|
||||
public CommandTimeAndWeather(long timeMilliseconds, boolean use12HourFormat, WeatherSpec weatherInfo, boolean useFahrenheit) {
|
||||
super(true, null);
|
||||
this.timeMilliseconds = timeMilliseconds;
|
||||
this.use12HourFormat = use12HourFormat;
|
||||
if (weatherInfo != null) {
|
||||
// TODO need to convert the weather spec enums to the ER enums.
|
||||
this.weatherIcon = 0x01;
|
||||
this.tempInCelsius = (byte) weatherInfo.currentTemp;
|
||||
} else {
|
||||
this.weatherIcon = 0x00;
|
||||
this.tempInCelsius = 0x00;
|
||||
}
|
||||
this.useFahrenheit = useFahrenheit;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] serialize() {
|
||||
byte[] packet = new byte[] {
|
||||
G1Constants.CommandId.DASHBOARD_CONFIG.id,
|
||||
G1Constants.DashboardConfigSubCommand.SET_TIME_AND_WEATHER.id,
|
||||
// Sequence place holder
|
||||
0x00,
|
||||
0x00,
|
||||
// Magic number?
|
||||
0x01,
|
||||
// Time 32bit place holders
|
||||
(byte) 0x00,
|
||||
(byte) 0x00,
|
||||
(byte) 0x00,
|
||||
(byte) 0x00,
|
||||
// Time 64bit place holders
|
||||
(byte) 0xFF,
|
||||
(byte) 0xFF,
|
||||
(byte) 0xFF,
|
||||
(byte) 0xFF,
|
||||
(byte) 0xFF,
|
||||
(byte) 0xFF,
|
||||
(byte) 0xFF,
|
||||
(byte) 0xFF,
|
||||
// Weather info
|
||||
this.weatherIcon,
|
||||
tempInCelsius,
|
||||
// F/C
|
||||
(byte)(useFahrenheit ? 0x01 : 0x00),
|
||||
// 24H/12H
|
||||
(byte)(use12HourFormat ? 0x01 : 0x00)
|
||||
};
|
||||
BLETypeConversions.writeUint16BE(packet, 2, sequence);
|
||||
BLETypeConversions.writeUint32(packet, 5, (int)(timeMilliseconds / 1000));
|
||||
BLETypeConversions.writeUint64(packet, 9, timeMilliseconds);
|
||||
|
||||
return packet;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean responseMatches(byte[] payload) {
|
||||
if (payload.length < 4) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Command should match and the sequence should match.
|
||||
// TODO actually check the sequence (need to confirm the offset).
|
||||
return payload[0] == G1Constants.CommandId.DASHBOARD_CONFIG.id &&
|
||||
payload[1] == G1Constants.DashboardConfigSubCommand.SET_TIME_AND_WEATHER.id;
|
||||
// payload[2] == (byte)(sequence >> 8) &&
|
||||
// payload[3] == (byte)sequence;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "time_and_weather";
|
||||
}
|
||||
}
|
||||
}
|
||||
+53
-18
@@ -2,7 +2,7 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.evenrealities;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class G1DeviceConstants {
|
||||
public class G1Constants {
|
||||
public static final UUID UUID_SERVICE_NORDIC_UART =
|
||||
UUID.fromString("6e400001-b5a3-f393-e0a9-e50e24dcca9e");
|
||||
public static final UUID UUID_CHARACTERISTIC_NORDIC_UART_TX =
|
||||
@@ -10,6 +10,9 @@ public class G1DeviceConstants {
|
||||
public static final UUID UUID_CHARACTERISTIC_NORDIC_UART_RX =
|
||||
UUID.fromString("6e400003-b5a3-f393-e0a9-e50e24dcca9e");
|
||||
public static final int MTU = 251;
|
||||
public static final int HEART_BEAT_DELAY_MS = 28000;
|
||||
public static final int DEFAULT_COMMAND_TIMEOUT_MS = 5000;
|
||||
public static final int DEFAULT_RETRY_COUNT = 5;
|
||||
|
||||
// Extract the L or R at the end of the device prefix.
|
||||
public static Side getSideFromFullName(String deviceName) {
|
||||
@@ -39,29 +42,49 @@ public class G1DeviceConstants {
|
||||
}
|
||||
|
||||
public enum Side {
|
||||
LEFT,
|
||||
RIGHT;
|
||||
INVALID(-1, ""),
|
||||
LEFT(0, "left"),
|
||||
RIGHT(1, "right");
|
||||
|
||||
private final int deviceIndex;
|
||||
private final String stringPrefix;
|
||||
|
||||
Side(int deviceIndex, String stringPrefix) {
|
||||
this.deviceIndex = deviceIndex;
|
||||
this.stringPrefix = stringPrefix;
|
||||
}
|
||||
|
||||
public int getDeviceIndex() {
|
||||
return deviceIndex;
|
||||
}
|
||||
|
||||
public static String getIndexKey() {
|
||||
return "device_index";
|
||||
}
|
||||
|
||||
public String getAddressKey() {
|
||||
return stringPrefix + "_address";
|
||||
}
|
||||
|
||||
public String getNameKey() {
|
||||
return stringPrefix + "_name";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// TODO: Lifted these from a different project, some of them are wrong.
|
||||
public enum CommandId {
|
||||
BATTERY_LEVEL((byte) 0x2C),
|
||||
WEATHER_AND_TIME((byte) 0x06),
|
||||
START_AI((byte) 0xF5),
|
||||
OPEN_MIC((byte) 0x0E),
|
||||
MIC_RESPONSE((byte) 0x0E),
|
||||
RECEIVE_MIC_DATA((byte) 0xF1),
|
||||
INIT((byte) 0x4D),
|
||||
HEARTBEAT((byte) 0x25),
|
||||
SEND_RESULT((byte) 0x4E),
|
||||
QUICK_NOTE((byte) 0x21),
|
||||
SILENT_MODE((byte) 0x03),
|
||||
NOTIFICATION_CONFIG((byte) 0x04),
|
||||
|
||||
DASHBOARD_CONFIG((byte) 0x06),
|
||||
DASHBOARD((byte) 0x22),
|
||||
NOTIFICATION((byte) 0x4B),
|
||||
BMP((byte) 0x15),
|
||||
FW_INFO_REQUEST((byte) 0x23),
|
||||
FW_INFO_RESPONSE((byte) 0x6E),
|
||||
CRC((byte) 0x16);
|
||||
HEARTBEAT((byte) 0x25),
|
||||
BATTERY_LEVEL((byte) 0x2C),
|
||||
INIT((byte) 0x4D),
|
||||
NOTIFICATION((byte) 0x4B),
|
||||
FW_INFO_RESPONSE((byte) 0x6E);
|
||||
|
||||
final public byte id;
|
||||
|
||||
@@ -69,4 +92,16 @@ public class G1DeviceConstants {
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum DashboardConfigSubCommand {
|
||||
SET_MODE((byte) 0x07),
|
||||
UNKNOWN_1((byte) 0x0C),
|
||||
SET_TIME_AND_WEATHER((byte) 0x15);
|
||||
|
||||
final public byte id;
|
||||
|
||||
DashboardConfigSubCommand(byte id) {
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
}
|
||||
+227
-121
@@ -1,7 +1,11 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.evenrealities;
|
||||
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothGatt;
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.content.Context;
|
||||
import android.icu.util.Calendar;
|
||||
import android.icu.util.TimeZone;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.widget.Toast;
|
||||
@@ -9,55 +13,100 @@ import android.widget.Toast;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.function.Function;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.Logging;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.SettingsActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.jyou.BFH16Constants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ItemWithDetails;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEMultiDeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEQueue;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
/**
|
||||
* Support class for the Even Realities G1. This sends and receives commands to and from the device.
|
||||
* The Protocol is mostly defined in G1Constants.java right now. In the future the protocol will be
|
||||
* broken out to a different class.
|
||||
* The Protocol is defined in G1SideManager.
|
||||
* One interesting point about this device is that it requires a constant BLE connection which is
|
||||
* contrary to the way BLE is supposed to work. Unfortunately the device will show the disconnected
|
||||
* icon and stop displaying any information when it is in the disconnected state. Because of this,
|
||||
* we need to send a heartbeat ever 30 seconds, otherwise the device will disconnect and reconnect
|
||||
* every 32 seconds per the BLE spec.
|
||||
*/
|
||||
public class G1DeviceSupport extends AbstractBTLEDeviceSupport {
|
||||
public class G1DeviceSupport extends AbstractBTLEMultiDeviceSupport {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(G1DeviceSupport.class);
|
||||
private final Handler backgroundTasksHandler = new Handler(Looper.getMainLooper());
|
||||
private BluetoothGattCharacteristic rxCharacteristic;
|
||||
private BluetoothGattCharacteristic txCharacteristic;
|
||||
private int heartBeatSequence;
|
||||
private G1SideManager leftSide;
|
||||
private G1SideManager rightSide;
|
||||
private final Object sendTimeLock = new Object();
|
||||
|
||||
public G1DeviceSupport() {
|
||||
this(LOG);
|
||||
}
|
||||
|
||||
public G1DeviceSupport(Logger logger) {
|
||||
super(logger);
|
||||
addSupportedService(G1DeviceConstants.UUID_SERVICE_NORDIC_UART);
|
||||
super(logger, 2);
|
||||
addSupportedService(G1Constants.UUID_SERVICE_NORDIC_UART,
|
||||
G1Constants.Side.LEFT.getDeviceIndex());
|
||||
addSupportedService(BFH16Constants.BFH16_GENERIC_ACCESS_SERVICE,
|
||||
G1Constants.Side.LEFT.getDeviceIndex());
|
||||
addSupportedService(BFH16Constants.BFH16_GENERIC_ATTRIBUTE_SERVICE,
|
||||
G1Constants.Side.LEFT.getDeviceIndex());
|
||||
|
||||
addSupportedService(G1Constants.UUID_SERVICE_NORDIC_UART,
|
||||
G1Constants.Side.RIGHT.getDeviceIndex());
|
||||
addSupportedService(BFH16Constants.BFH16_GENERIC_ACCESS_SERVICE,
|
||||
G1Constants.Side.RIGHT.getDeviceIndex());
|
||||
addSupportedService(BFH16Constants.BFH16_GENERIC_ATTRIBUTE_SERVICE,
|
||||
G1Constants.Side.RIGHT.getDeviceIndex());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TransactionBuilder initializeDevice(TransactionBuilder builder) {//}, int deviceIdx) {
|
||||
this.heartBeatSequence = 0;
|
||||
builder.add(
|
||||
new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
|
||||
public void setContext(GBDevice device, BluetoothAdapter btAdapter, Context context) {
|
||||
// Determine the left and right names based on if this is the parent device or not.
|
||||
// Ignore any context sets from non-left devices.
|
||||
G1Constants.Side side = G1Constants.getSideFromFullName(device.getName());
|
||||
if (side == G1Constants.Side.LEFT) {
|
||||
ItemWithDetails right_name =
|
||||
device.getDeviceInfo(G1Constants.Side.RIGHT.getNameKey());
|
||||
ItemWithDetails right_address =
|
||||
device.getDeviceInfo(G1Constants.Side.RIGHT.getAddressKey());
|
||||
if (right_name != null && !right_name.getDetails().isEmpty() && right_address != null &&
|
||||
!right_address.getDetails().isEmpty()) {
|
||||
GBDevice rightDevice =
|
||||
new GBDevice(right_address.getDetails(), right_name.getDetails(), null,
|
||||
device.getParentFolder(), device.getType());
|
||||
super.setDevice(rightDevice, 1);
|
||||
} else {
|
||||
super.setDevice(null, 1);
|
||||
}
|
||||
|
||||
rxCharacteristic = getCharacteristic(G1DeviceConstants.UUID_CHARACTERISTIC_NORDIC_UART_RX);
|
||||
txCharacteristic = getCharacteristic(G1DeviceConstants.UUID_CHARACTERISTIC_NORDIC_UART_TX);
|
||||
builder.requestMtu(G1DeviceConstants.MTU);
|
||||
// The left device acts as the parent device
|
||||
super.setContext(device, btAdapter, context);
|
||||
} else {
|
||||
// This should only happen during pairing. Once the devices are linked by the
|
||||
// entries for right and left devices in the device specific preferences, this will
|
||||
// never be called on the right device again. BUT we need this to connect to the right
|
||||
// device before the devices are linked.
|
||||
super.setContext(device, btAdapter, context);
|
||||
}
|
||||
}
|
||||
|
||||
if (rxCharacteristic == null || txCharacteristic == null) {
|
||||
@Override
|
||||
protected TransactionBuilder initializeDevice(TransactionBuilder builder, int deviceIdx) {
|
||||
// Verify that the characteristics are present for the current device.
|
||||
BluetoothGattCharacteristic rx =
|
||||
getCharacteristic(G1Constants.UUID_CHARACTERISTIC_NORDIC_UART_RX, deviceIdx);
|
||||
BluetoothGattCharacteristic tx =
|
||||
getCharacteristic(G1Constants.UUID_CHARACTERISTIC_NORDIC_UART_TX, deviceIdx);
|
||||
if (rx == null || tx == null) {
|
||||
// If the characteristics are not received from the device reconnect and try again.
|
||||
LOG.warn("RX/TX characteristics are null, will attempt to reconnect");
|
||||
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.WAITING_FOR_RECONNECT,
|
||||
@@ -67,52 +116,124 @@ public class G1DeviceSupport extends AbstractBTLEDeviceSupport {
|
||||
return builder;
|
||||
}
|
||||
|
||||
// Register callbacks for this device.
|
||||
builder.setCallback(this);
|
||||
builder.notify(rxCharacteristic, true);
|
||||
|
||||
// Send the command to fetch FW version.
|
||||
byte[] packet = new byte[2];
|
||||
packet[0] = G1DeviceConstants.CommandId.FW_INFO_REQUEST.id;
|
||||
packet[1] = 0x74;
|
||||
builder.write(txCharacteristic, packet);
|
||||
|
||||
// Send the command to fetch battery info.
|
||||
packet = new byte[2];
|
||||
packet[0] = G1DeviceConstants.CommandId.BATTERY_LEVEL.id;
|
||||
packet[1] = 0x01;
|
||||
builder.write(txCharacteristic, packet);
|
||||
|
||||
if (getDevice().getFirmwareVersion() == null) {
|
||||
getDevice().setFirmwareVersion("N/A");
|
||||
getDevice().setFirmwareVersion2("N/A");
|
||||
// Create either the left or right side depending on which device is initialized.
|
||||
G1SideManager side;
|
||||
synchronized (this) {
|
||||
side = getSideFromIndex(deviceIdx);
|
||||
if (side == null) {
|
||||
side = createSideFromIndex(deviceIdx, rx, tx);
|
||||
}
|
||||
}
|
||||
|
||||
// The glasses will auto disconnect after 30 seconds of no data on the wire.
|
||||
// Schedule a heartbeat task. If this is not enabled, button presses on the glasses will not
|
||||
// be sent to the phone, so realtime interactions won't work.
|
||||
scheduleHeatBeat();
|
||||
// Paranoid protection from a bad index being passed in.
|
||||
if (side == null) {
|
||||
LOG.error("Device index is not left or right: {}", deviceIdx);
|
||||
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.WAITING_FOR_RECONNECT,
|
||||
getContext()));
|
||||
GB.toast(getContext(), "Unable to manage connection to device.", Toast.LENGTH_LONG,
|
||||
GB.ERROR);
|
||||
return builder;
|
||||
}
|
||||
|
||||
// Schedule the battery polling.
|
||||
scheduleBatteryPolling();
|
||||
// The glasses expect a specific MTU, set that now.
|
||||
builder.requestMtu(G1Constants.MTU);
|
||||
|
||||
// Device is ready for use.
|
||||
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext()));
|
||||
// Register callbacks for this device.
|
||||
builder.setCallback(this);
|
||||
builder.notify(rx, true);
|
||||
|
||||
// If the manager is initializing, then the whole device is initializing as well.
|
||||
if (side.getState() == GBDevice.State.CONNECTED) {
|
||||
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING,
|
||||
getContext()));
|
||||
side.initialize();
|
||||
}
|
||||
|
||||
// Both sides are initialized. The whole device is initialized.
|
||||
synchronized (this) {
|
||||
if (leftSide != null && leftSide.getState() == GBDevice.State.INITIALIZED &&
|
||||
rightSide != null && rightSide.getState() == GBDevice.State.INITIALIZED) {
|
||||
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED,
|
||||
getContext()));
|
||||
// This means that both sides have been connected to and basic info has been collected.
|
||||
// These next steps require that both sides are ready which is why they are done post
|
||||
// individual initialization. We don't know what thread we are handling the update state
|
||||
// event on, so to be safe, schedule these as a background task.
|
||||
backgroundTasksHandler.postDelayed(() -> {
|
||||
leftSide.postInitialize();
|
||||
rightSide.postInitialize();
|
||||
onSetTime();
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
getDevice().sendDeviceUpdateIntent(getContext());
|
||||
return builder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
// Remove all callbacks
|
||||
// Remove all background tasks.
|
||||
backgroundTasksHandler.removeCallbacksAndMessages(null);
|
||||
|
||||
// Kill both sides.
|
||||
leftSide = null;
|
||||
rightSide = null;
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean useAutoConnect() {
|
||||
return false;
|
||||
// Only allow reconnection if both devices are present. When devices are being bonded, if
|
||||
// the auto connect kicks in at the wrong 1ime, it can fragment the devices and break
|
||||
// things.
|
||||
return getDevice(G1Constants.Side.LEFT.getDeviceIndex()) != null &&
|
||||
getDevice(G1Constants.Side.RIGHT.getDeviceIndex()) != null;
|
||||
}
|
||||
|
||||
private G1SideManager createSideFromIndex(int deviceIdx, BluetoothGattCharacteristic rx,
|
||||
BluetoothGattCharacteristic tx) {
|
||||
// Package some of the DeviceSupport functions as callbacks here. We deliberately skip
|
||||
// passing in "this" because we don't want to forward ALL functionality of the device
|
||||
// support and we don't want a hard dependency on G1DeviceSupport in G1SideManager.
|
||||
Callable<BtLEQueue> getQueue = () -> this.getQueue(deviceIdx);
|
||||
Function<GBDeviceEvent, Void> handleEvent = (GBDeviceEvent event) -> {
|
||||
this.evaluateGBDeviceEvent(event);
|
||||
return null;
|
||||
};
|
||||
|
||||
// Create the desired side.
|
||||
if (deviceIdx == G1Constants.Side.LEFT.getDeviceIndex()) {
|
||||
leftSide =
|
||||
new G1SideManager(G1Constants.Side.LEFT, backgroundTasksHandler, getQueue,
|
||||
handleEvent, this::getDevicePrefs, rx, tx);
|
||||
return leftSide;
|
||||
} else if (deviceIdx == G1Constants.Side.RIGHT.getDeviceIndex()) {
|
||||
rightSide = new G1SideManager(G1Constants.Side.RIGHT, backgroundTasksHandler,
|
||||
getQueue, handleEvent, this::getDevicePrefs,
|
||||
rx, tx);
|
||||
return rightSide;
|
||||
}
|
||||
|
||||
// Return null under an unexpected index.
|
||||
return null;
|
||||
}
|
||||
|
||||
private G1SideManager getSideFromIndex(int deviceIdx) {
|
||||
if (deviceIdx == G1Constants.Side.LEFT.getDeviceIndex()) {
|
||||
return leftSide;
|
||||
} else if (deviceIdx == G1Constants.Side.RIGHT.getDeviceIndex()) {
|
||||
return rightSide;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
// Below are all the onXXX() handlers overridden from the base class. //
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public boolean onCharacteristicChanged(BluetoothGatt gatt,
|
||||
BluetoothGattCharacteristic characteristic,
|
||||
@@ -122,26 +243,29 @@ public class G1DeviceSupport extends AbstractBTLEDeviceSupport {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If this is the correct UART RX message, parse it.
|
||||
if (G1DeviceConstants.UUID_CHARACTERISTIC_NORDIC_UART_RX.equals(characteristic.getUuid())) {
|
||||
if (payload[0] == G1DeviceConstants.CommandId.BATTERY_LEVEL.id) {
|
||||
GBDeviceEventBatteryInfo batteryInfo = new GBDeviceEventBatteryInfo();
|
||||
batteryInfo.state = BatteryState.BATTERY_NORMAL;
|
||||
batteryInfo.level = payload[2];
|
||||
handleGBDeviceEvent(batteryInfo);
|
||||
} else if (payload[0] == G1DeviceConstants.CommandId.FW_INFO_RESPONSE.id) {
|
||||
// FW info string
|
||||
String fwInfo = new String(payload, StandardCharsets.US_ASCII).trim();
|
||||
LOG.debug("Got FW: " + fwInfo);
|
||||
int versionStart = fwInfo.lastIndexOf(" ver ") + " ver ".length();
|
||||
int versionEnd = fwInfo.indexOf(',', versionStart);
|
||||
if (versionStart > -1 && versionEnd > versionStart) {
|
||||
String version = fwInfo.substring(versionStart, versionEnd);
|
||||
LOG.debug("Parsed fw version: " + version);
|
||||
getDevice().setFirmwareVersion(version);
|
||||
// If this is the correct UART RX message, forward to the correct side based on the BLE
|
||||
// address.
|
||||
if (characteristic.getUuid().equals(G1Constants.UUID_CHARACTERISTIC_NORDIC_UART_RX)) {
|
||||
String address = gatt.getDevice().getAddress();
|
||||
if (getDevice(G1Constants.Side.LEFT.getDeviceIndex()) != null) {
|
||||
String leftAddress =
|
||||
getDevice(G1Constants.Side.LEFT.getDeviceIndex()).getAddress();
|
||||
if (address.equals(leftAddress) && leftSide != null) {
|
||||
return leftSide.handlePayload(characteristic.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
if (getDevice(G1Constants.Side.RIGHT.getDeviceIndex()) != null) {
|
||||
String rightAddress =
|
||||
getDevice(G1Constants.Side.RIGHT.getDeviceIndex()).getAddress();
|
||||
if (address.equals(rightAddress) && rightSide != null) {
|
||||
return rightSide.handlePayload(characteristic.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Not handled by either side.
|
||||
LOG.debug("Unhandled payload: {}", Logging.formatBytes(characteristic.getValue()));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -153,61 +277,43 @@ public class G1DeviceSupport extends AbstractBTLEDeviceSupport {
|
||||
*/
|
||||
@Override
|
||||
public void onSendConfiguration(String config) {
|
||||
switch (config) {
|
||||
// Reschedule battery polling. The new schedule may be disabled.
|
||||
case DeviceSettingsPreferenceConst.PREF_BATTERY_POLLING_ENABLE:
|
||||
case DeviceSettingsPreferenceConst.PREF_BATTERY_POLLING_INTERVAL:
|
||||
scheduleBatteryPolling();
|
||||
break;
|
||||
// Forward to both sides.
|
||||
if (leftSide != null) leftSide.onSendConfiguration(config);
|
||||
if (rightSide != null) rightSide.onSendConfiguration(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetTime() {
|
||||
if (leftSide == null || rightSide == null) return;
|
||||
|
||||
boolean use12HourFormat = getDevicePrefs().getTimeFormat().equals(
|
||||
DeviceSettingsPreferenceConst.PREF_TIMEFORMAT_12H);
|
||||
Calendar c = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
|
||||
long timeMilliseconds = c.getTimeInMillis();
|
||||
long tzOffset = TimeZone.getDefault().getOffset(timeMilliseconds);
|
||||
timeMilliseconds += tzOffset;
|
||||
|
||||
// Check if the GB settings are set to metric, if not, set the temp to use Fahrenheit.
|
||||
String metricString = GBApplication.getContext().getString(R.string.p_unit_metric);
|
||||
boolean useFahrenheit = !GBApplication.getPrefs().getString(SettingsActivity.PREF_MEASUREMENT_SYSTEM, metricString).equals(metricString);
|
||||
|
||||
// This block is synchronized. We do not want two calls to overlap, otherwise the lenses
|
||||
// could get skewed with different values.
|
||||
synchronized (sendTimeLock) {
|
||||
// Send the left the time synchronously, then once a response is received, send the right.
|
||||
// The glasses will ignore the command on the right lens if it arrives before the left.
|
||||
G1Communications.CommandHandler leftCommandHandler =
|
||||
new G1Communications.CommandTimeAndWeather(
|
||||
timeMilliseconds, use12HourFormat, /*weatherInfo=*/ null, useFahrenheit);
|
||||
leftSide.send(leftCommandHandler);
|
||||
if (!leftCommandHandler.waitForResponsePayload()) {
|
||||
LOG.error("Set time on left lens timed out");
|
||||
getDevice().setState(GBDevice.State.WAITING_FOR_RECONNECT);
|
||||
getDevice().sendDeviceUpdateIntent(getContext());
|
||||
}
|
||||
|
||||
rightSide.send(new G1Communications.CommandTimeAndWeather(
|
||||
timeMilliseconds, use12HourFormat, /*weatherInfo=*/ null, useFahrenheit));
|
||||
}
|
||||
}
|
||||
|
||||
private void scheduleHeatBeat() {
|
||||
backgroundTasksHandler.removeCallbacksAndMessages(heartBeatRunner);
|
||||
backgroundTasksHandler.postDelayed(heartBeatRunner, 30 * 1000);
|
||||
}
|
||||
|
||||
private void scheduleBatteryPolling() {
|
||||
backgroundTasksHandler.removeCallbacksAndMessages(batteryRunner);
|
||||
if (GBApplication.getDevicePrefs(gbDevice).getBatteryPollingEnabled()) {
|
||||
int interval_minutes =
|
||||
GBApplication.getDevicePrefs(gbDevice).getBatteryPollingIntervalMinutes();
|
||||
int interval = interval_minutes * 60 * 1000;
|
||||
LOG.debug("Starting battery runner delayed by {} ({} minutes)", interval,
|
||||
interval_minutes);
|
||||
backgroundTasksHandler.postDelayed(batteryRunner, interval);
|
||||
}
|
||||
}
|
||||
|
||||
private final Runnable batteryRunner = () -> {
|
||||
TransactionBuilder builder = new TransactionBuilder("battery_request");
|
||||
byte[] packet = new byte[2];
|
||||
packet[0] = G1DeviceConstants.CommandId.BATTERY_LEVEL.id;
|
||||
packet[1] = 0x01;
|
||||
builder.write(txCharacteristic, packet);
|
||||
builder.queue(getQueue());
|
||||
|
||||
// Schedule the next check.
|
||||
scheduleBatteryPolling();
|
||||
};
|
||||
|
||||
|
||||
private final Runnable heartBeatRunner = () -> {
|
||||
TransactionBuilder builder = new TransactionBuilder("heart_beat");
|
||||
int length = 6;
|
||||
byte[] packet = new byte[length];
|
||||
packet[0] = G1DeviceConstants.CommandId.HEARTBEAT.id;
|
||||
packet[1] = (byte) (length & 0xFF);
|
||||
packet[2] = 0x00; //(byte)((length >> 8) & 0xFF);
|
||||
packet[3] = (byte) (heartBeatSequence % 0xFF);
|
||||
packet[4] = 0x04;
|
||||
packet[5] = (byte) (heartBeatSequence % 0xFF);
|
||||
builder.write(txCharacteristic, packet);
|
||||
builder.queue(getQueue());
|
||||
|
||||
// Schedule the next heartbeat.
|
||||
scheduleHeatBeat();
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
|
||||
+257
@@ -0,0 +1,257 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.evenrealities;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.os.Handler;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.function.Function;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.Logging;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEQueue;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.preferences.DevicePrefs;
|
||||
|
||||
/**
|
||||
* This is a supporting class for G1DeviceSupport and allows each side of the glasses to be have
|
||||
* some level of independence but also allow the G1DeviceSupport class to control them and offload
|
||||
* functionality. It might be tempting to pass the entire G1DeviceSupport into the constructor
|
||||
* instead of the callbacks, but this is not done deliberately to make this class not directly tied
|
||||
* to the G1DeviceSupport class.
|
||||
*/
|
||||
public class G1SideManager {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(G1SideManager.class);
|
||||
|
||||
private final G1Constants.Side mySide;
|
||||
private final Handler backgroundTasksHandler;
|
||||
private final Callable<BtLEQueue> getQueueHandler;
|
||||
private final Function<GBDeviceEvent, Void> sendEventHandler;
|
||||
private final Callable<DevicePrefs> getPrefsHandler;
|
||||
private final BluetoothGattCharacteristic rx;
|
||||
private final BluetoothGattCharacteristic tx;
|
||||
private final Runnable batteryRunner;
|
||||
private final Runnable heartBeatRunner;
|
||||
private final Set<G1Communications.CommandHandler> commandHandlers;
|
||||
private short heartBeatSequence;
|
||||
private short globalSequence;
|
||||
private GBDevice.State state;
|
||||
|
||||
public G1SideManager(G1Constants.Side mySide, Handler backgroundTasksHandler,
|
||||
Callable<BtLEQueue> getQueue, Function<GBDeviceEvent, Void> sendEvent,
|
||||
Callable<DevicePrefs> getPrefs,
|
||||
BluetoothGattCharacteristic rx, BluetoothGattCharacteristic tx) {
|
||||
this.mySide = mySide;
|
||||
this.backgroundTasksHandler = backgroundTasksHandler;
|
||||
this.getQueueHandler = getQueue;
|
||||
this.sendEventHandler = sendEvent;
|
||||
this.getPrefsHandler = getPrefs;
|
||||
this.rx = rx;
|
||||
this.tx = tx;
|
||||
this.batteryRunner = () -> {
|
||||
send(new G1Communications.CommandBatteryLevel(this::handleBatteryPayload));
|
||||
scheduleBatteryPolling();
|
||||
};
|
||||
|
||||
this.heartBeatRunner = () -> {
|
||||
send(new G1Communications.CommandHeartBeat(heartBeatSequence++));
|
||||
scheduleHeatBeat();
|
||||
};
|
||||
this.commandHandlers = new HashSet<>();
|
||||
|
||||
// Non Finals
|
||||
this.heartBeatSequence = 0;
|
||||
this.globalSequence = 0;
|
||||
this.state = GBDevice.State.CONNECTED;
|
||||
}
|
||||
|
||||
private BtLEQueue getQueue() {
|
||||
try {
|
||||
return getQueueHandler.call();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void evaluateGBDeviceEvent(GBDeviceEvent event) {
|
||||
sendEventHandler.apply(event);
|
||||
}
|
||||
|
||||
private DevicePrefs getDevicePrefs() {
|
||||
try {
|
||||
return getPrefsHandler.call();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public GBDevice.State getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public void initialize() {
|
||||
// The glasses will auto disconnect after 30 seconds of no data on the wire.
|
||||
// Schedule a heartbeat task. If this is not enabled, the glasses will disconnect and be
|
||||
// useless to the user.
|
||||
scheduleHeatBeat();
|
||||
|
||||
// Schedule the battery polling.
|
||||
scheduleBatteryPolling();
|
||||
|
||||
state = GBDevice.State.INITIALIZED;
|
||||
}
|
||||
|
||||
public void postInitialize() {
|
||||
send(new G1Communications.CommandBatteryLevel(this::handleBatteryPayload));
|
||||
send(new G1Communications.CommandFirmwareInfo(this::handleFirmwareInfoPayload));
|
||||
}
|
||||
|
||||
public void onSendConfiguration(String config) {
|
||||
switch (config) {
|
||||
// Reschedule battery polling. The new schedule may be disabled.
|
||||
case DeviceSettingsPreferenceConst.PREF_BATTERY_POLLING_ENABLE:
|
||||
case DeviceSettingsPreferenceConst.PREF_BATTERY_POLLING_INTERVAL:
|
||||
scheduleBatteryPolling();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void scheduleHeatBeat() {
|
||||
backgroundTasksHandler.removeCallbacksAndMessages(heartBeatRunner);
|
||||
LOG.debug("Starting heartbeat runner delayed by {}ms", G1Constants.HEART_BEAT_DELAY_MS);
|
||||
backgroundTasksHandler.postDelayed(heartBeatRunner, G1Constants.HEART_BEAT_DELAY_MS);
|
||||
}
|
||||
|
||||
private void scheduleBatteryPolling() {
|
||||
backgroundTasksHandler.removeCallbacksAndMessages(batteryRunner);
|
||||
DevicePrefs prefs = getDevicePrefs();
|
||||
if (prefs.getBatteryPollingEnabled()) {
|
||||
int interval_minutes = prefs.getBatteryPollingIntervalMinutes();
|
||||
int interval = interval_minutes * 60 * 1000;
|
||||
LOG.debug("Starting battery runner delayed by {} ({} minutes)", interval,
|
||||
interval_minutes);
|
||||
backgroundTasksHandler.postDelayed(batteryRunner, interval);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized short getNextSequence() {
|
||||
// This number will eventually overflow, and that is fine. The sequence number is just to
|
||||
// match the request and response together.
|
||||
return globalSequence++;
|
||||
}
|
||||
|
||||
public void send(G1Communications.CommandHandler command) {
|
||||
TransactionBuilder builder =
|
||||
new TransactionBuilder(command.getName() + "_" + mySide.getDeviceIndex());
|
||||
sendInTransaction(builder, command);
|
||||
builder.queue(getQueue());
|
||||
}
|
||||
|
||||
private void sendInTransaction(TransactionBuilder builder, G1Communications.CommandHandler command) {
|
||||
// Calling getNextSequence() will advance the global sequence, if the command doesn't need
|
||||
// a sequence number, don't call it so we don't waste a sequence number.
|
||||
if (command.needsGlobalSequence()) {
|
||||
command.setGlobalSequence(getNextSequence());
|
||||
}
|
||||
|
||||
LOG.debug("Send command {} on side {}", command.getName(), mySide.getDeviceIndex());
|
||||
|
||||
// Write the packet to the BLE txn.
|
||||
builder.write(tx, command.serialize());
|
||||
|
||||
// If this command expects a response, register the handler.
|
||||
if (command.expectResponse()) {
|
||||
registerResponseHandler(command);
|
||||
}
|
||||
|
||||
// Schedule a task that will sleep for the timeout time, and then wake up and check if the
|
||||
// command is completed. If the command has not completed, a retry is sent if there are
|
||||
// available retries.
|
||||
if (command.expectResponse()) {
|
||||
backgroundTasksHandler.postDelayed(() -> {
|
||||
boolean retry;
|
||||
synchronized (command) {
|
||||
command.notifyAttempt();
|
||||
retry = !command.hasResponsePayload() && command.hasRetryRemaining();
|
||||
}
|
||||
|
||||
// Do this outside the synchronized block, better to avoid comm work while holding
|
||||
// the lock.
|
||||
if (retry) {
|
||||
LOG.debug("Retry {} command {} on side {}", command.getRetryCount(),
|
||||
command.getName(), mySide.getDeviceIndex());
|
||||
// TODO: This will change the global sequence number of the command, is this
|
||||
// what the stock app does on retry? Or does it resend with the same one.
|
||||
send(command);
|
||||
}
|
||||
}, command.getTimeout());
|
||||
}
|
||||
}
|
||||
|
||||
private void registerResponseHandler(G1Communications.CommandHandler commandHandler) {
|
||||
synchronized (commandHandlers) {
|
||||
commandHandlers.add(commandHandler);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean handlePayload(byte[] payload) {
|
||||
for (G1Communications.CommandHandler commandHandler : commandHandlers) {
|
||||
LOG.debug("Got response payload for command {} on side {}: {}", commandHandler.getName(), mySide.getDeviceIndex(), Logging.formatBytes(payload));
|
||||
if (commandHandler.responseMatches(payload)) {
|
||||
synchronized (commandHandlers) {
|
||||
commandHandlers.remove(commandHandler);
|
||||
commandHandler.setResponsePayload(payload);
|
||||
}
|
||||
|
||||
Function<byte[], Boolean> callback = commandHandler.getCallback();
|
||||
return callback != null && callback.apply(payload);
|
||||
}
|
||||
}
|
||||
LOG.debug("Unhandled payload on side {}: {}", mySide.getDeviceIndex(), Logging.formatBytes(payload));
|
||||
|
||||
// Not handled by any handlers.
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean handleBatteryPayload(byte[] payload) {
|
||||
GBDeviceEventBatteryInfo batteryInfo = new GBDeviceEventBatteryInfo();
|
||||
batteryInfo.state = BatteryState.BATTERY_NORMAL;
|
||||
batteryInfo.level = payload[2];
|
||||
batteryInfo.batteryIndex = mySide.getDeviceIndex();
|
||||
evaluateGBDeviceEvent(batteryInfo);
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean handleFirmwareInfoPayload(byte[] payload) {
|
||||
// FW info string
|
||||
String fwString = new String(payload, StandardCharsets.US_ASCII).trim();
|
||||
LOG.debug("Got FW: {}", fwString);
|
||||
int versionStart = fwString.lastIndexOf(" ver ") + " ver ".length();
|
||||
int versionEnd = fwString.indexOf(',', versionStart);
|
||||
if (versionStart > -1 && versionEnd > versionStart) {
|
||||
String version = fwString.substring(versionStart, versionEnd);
|
||||
LOG.debug("Parsed fw version: {}", version);
|
||||
GBDeviceEventVersionInfo fwInfo = new GBDeviceEventVersionInfo();
|
||||
if (mySide == G1Constants.Side.LEFT) {
|
||||
fwInfo.fwVersion = version;
|
||||
} else if (mySide == G1Constants.Side.RIGHT) {
|
||||
fwInfo.fwVersion2 = version;
|
||||
}
|
||||
// Actually get this some how?
|
||||
fwInfo.hwVersion = "G1A";
|
||||
evaluateGBDeviceEvent(fwInfo);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -697,6 +697,7 @@
|
||||
<string name="title_activity_even_realities_g1_pairing">Even G1 Pairing</string>
|
||||
<string name="pairing_even_realities_g1_select_next_lens">%1$s lens selected for pairing. Multiple candidates for the %2$s lens found. Please select the %2$s lens from the list below.</string>
|
||||
<string name="pairing_even_realities_g1_working">Pairing with %1s...</string>
|
||||
<string name="pairing_even_realities_g1_final_connect">Setting up devices, please wait...</string>
|
||||
<string name="pairing_even_realities_g1_find_both_fail">Failed to find both the right and left lens. Please scan again.</string>
|
||||
<string name="pairing_even_realities_g1_invalid_device">Even G1 Device Malformed: %1$s.</string>
|
||||
<string name="pairing_even_realities_g1_bottom_hint">Several pairing dialogs will pop up on your Android device. If not, look in the notification drawer and accept the pairing request. Both the left and right lens will need to be paired.</string>
|
||||
|
||||
Reference in New Issue
Block a user