1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2025-02-04 14:07:32 +01:00

Even Realities: Add initial support for Even G1 Smart Glasses (#4553)

Co-authored-by: jrthomas270 <jrthomas270@noreply.codeberg.org>
Co-committed-by: jrthomas270 <jrthomas270@noreply.codeberg.org>
This commit is contained in:
jrthomas270 2025-02-02 16:59:39 +00:00 committed by José Rebelo
parent c08285a356
commit 2d6a7d3866
12 changed files with 854 additions and 2 deletions

View File

@ -643,6 +643,10 @@
<activity
android:name=".devices.pebble.PebblePairingActivity"
android:label="@string/title_activity_pebble_pairing" />
<activity
android:name=".devices.evenrealities.G1PairingActivity"
android:label="@string/title_activity_even_realities_g1_pairing"
android:parentActivityName=".activities.discovery.DiscoveryActivityV2" />
<activity
android:name=".devices.watch9.Watch9PairingActivity"
android:label="@string/title_activity_watch9_pairing" />

View File

@ -109,6 +109,7 @@ public class DiscoveryActivityV2 extends AbstractGBActivity implements AdapterVi
MenuProvider {
private static final Logger LOG = LoggerFactory.getLogger(DiscoveryActivityV2.class);
private static final int CHILD_RESULT = 0x826983; // "RES" as ASCII hex
private final Handler handler = new Handler();
private static final long SCAN_DURATION = 30000; // 30s
@ -144,7 +145,13 @@ public class DiscoveryActivityV2 extends AbstractGBActivity implements AdapterVi
@Override
public void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
super.onActivityResult(requestCode, resultCode, data);
BondingUtil.handleActivityResult(this, requestCode, resultCode, data);
if (requestCode == CHILD_RESULT && resultCode == RESULT_OK) {
// A device with a custom pairing activity has finished and indicated that the discovery activity should be
// closed.
finish();
} else {
BondingUtil.handleActivityResult(this, requestCode, resultCode, data);
}
}
@Nullable
@ -655,7 +662,8 @@ public class DiscoveryActivityV2 extends AbstractGBActivity implements AdapterVi
if (pairingActivity != null) {
final Intent intent = new Intent(this, pairingActivity);
intent.putExtra(DeviceCoordinator.EXTRA_DEVICE_CANDIDATE, deviceCandidate);
startActivity(intent);
intent.putParcelableArrayListExtra(DeviceCoordinator.EXTRA_DEVICE_ALL_CANDIDATES, deviceCandidates);
startActivityForResult(intent, CHILD_RESULT);
} else {
if (coordinator.getBondingStyle() == DeviceCoordinator.BONDING_STYLE_NONE ||
coordinator.getBondingStyle() == DeviceCoordinator.BONDING_STYLE_LAZY) {

View File

@ -78,6 +78,8 @@ import nodomain.freeyourgadget.gadgetbridge.service.ServiceDeviceSupport;
*/
public interface DeviceCoordinator {
String EXTRA_DEVICE_CANDIDATE = "nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate.EXTRA_DEVICE_CANDIDATE";
String EXTRA_DEVICE_ALL_CANDIDATES =
"nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate.EXTRA_DEVICE_ALL_CANDIDATES";
/**
* Do not attempt to bond after discovery.
*/

View File

@ -0,0 +1,87 @@
package nodomain.freeyourgadget.gadgetbridge.devices.evenrealities;
import android.app.Activity;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.regex.Pattern;
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.service.DeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.evenrealities.G1DeviceSupport;
/**
* Coordinator for the Even Realities G1 smart glasses. Describes the supported capabilities of the
* device.
* <p>
* This class partners with G1DeviceSupport.java and G1PairingActivity.java
*/
public class G1DeviceCoordinator extends AbstractBLEDeviceCoordinator {
private static final Logger LOG = LoggerFactory.getLogger(G1DeviceCoordinator.class);
@NonNull
@Override
public Class<? extends DeviceSupport> getDeviceSupportClass() {
return G1DeviceSupport.class;
}
@Override
public Class<? extends Activity> getPairingActivity() {
return G1PairingActivity.class;
}
@Override
protected Pattern getSupportedDeviceName() {
// eg. G1_45_L_F2333, G1_63_R_04935.
// Note that the G1_XX_L_YYYYY will have a corresponding G1_XX_R_ZZZZZ. The XX will match,
// but the trailing 5 characters will not.
return Pattern.compile("Even G1_\\d\\d_[L|R]_\\w+");
}
@Override
public int getDeviceNameResource() {
return R.string.devicetype_even_realities_g1;
}
@Override
public String getManufacturer() {
return "Even Realities";
}
@Override
@DrawableRes
public int getDefaultIconResource() {
return R.drawable.ic_device_even_realities_g1;
}
@Override
@DrawableRes
public int getDisabledIconResource() {
return R.drawable.ic_device_even_realities_g1_disabled;
}
@Override
public int getBondingStyle() {
return BONDING_STYLE_LAZY;
}
@Override
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device,
@NonNull DaoSession session) throws GBException {
}
@Override
public boolean addBatteryPollingSettings() {
return true;
}
}

View File

@ -0,0 +1,338 @@
package nodomain.freeyourgadget.gadgetbridge.devices.evenrealities;
import android.bluetooth.BluetoothDevice;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.os.Parcelable;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import androidx.core.content.ContextCompat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
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.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.service.devices.evenrealities.G1DeviceConstants;
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
import nodomain.freeyourgadget.gadgetbridge.util.BondingInterface;
import nodomain.freeyourgadget.gadgetbridge.util.BondingUtil;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
/**
* This class manages the pairing of both the left and right device for G1 glasses.
* The user will select either the left or the right and this activity will search for the other
* side pair both.
*/
public class G1PairingActivity extends AbstractGBActivity
implements BondingInterface, AdapterView.OnItemClickListener {
private static final Logger LOG = LoggerFactory.getLogger(G1PairingActivity.class);
private final ArrayList<GBDeviceCandidate> nextLensCandidates = new ArrayList<>();
private final BroadcastReceiver bluetoothReceiver = new G1PairingActivity.BluetoothReceiver();
// 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;
// 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.
private GBDeviceCandidate currentBondingCandidate;
private boolean currentBondingCompleteFromCallback;
private GBDeviceCandidate leftDeviceCandidate;
private GBDeviceCandidate rightDeviceCandidate;
// References to UI elements so that any function can update the interface.
private TextView hintTextView;
private ProgressBar progressBar;
private ListView nextLensCandidatesListView;
@Override
protected void onDestroy() {
unregisterBroadcastReceivers();
super.onDestroy();
}
@Override
protected void onStop() {
unregisterBroadcastReceivers();
super.onStop();
}
@Override
protected void onPause() {
unregisterBroadcastReceivers();
super.onPause();
}
@Override
protected void onResume() {
registerBroadcastReceivers();
super.onResume();
}
@Override
public GBDeviceCandidate getCurrentTarget() {
return currentBondingCandidate;
}
@Override
public String getMacAddress() {
return currentBondingCandidate.getDevice().getAddress();
}
@Override
public boolean getAttemptToConnect() {
return true;
}
@Override
public Context getContext() {
return this;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_even_realities_g1_pairing);
// 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);
progressBar = findViewById(R.id.pairing_progress_bar);
// Pull the candidate device out of the intent.
Intent intent = getIntent();
intent.setExtrasClassLoader(GBDeviceCandidate.class.getClassLoader());
initialDeviceCandidate =
intent.getParcelableExtra(DeviceCoordinator.EXTRA_DEVICE_CANDIDATE);
// Extract the name of the device and null check it.
String name = initialDeviceCandidate.getName();
if (name == null) {
GB.toast(getContext(),
getString(R.string.pairing_even_realities_g1_invalid_device, "null"),
Toast.LENGTH_LONG, GB.ERROR);
finish();
return;
}
// 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);
if (compositeDeviceName == null) {
GB.toast(getContext(),
getString(R.string.pairing_even_realities_g1_invalid_device, name),
Toast.LENGTH_LONG, GB.ERROR);
finish();
return;
}
// 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());
if (initialDeviceCandidateSide == null) {
GB.toast(getContext(),
getString(R.string.pairing_even_realities_g1_invalid_device, name),
Toast.LENGTH_LONG, GB.ERROR);
finish();
return;
}
// Determine the current and next side. This is used to show the correct UI element to the
// user.
int currentSide = 0;
int nextSide = 0;
if (initialDeviceCandidateSide == G1DeviceConstants.Side.LEFT) {
currentSide = R.string.watchface_dialog_widget_preset_left;
nextSide = R.string.watchface_dialog_widget_preset_right;
} else {
currentSide = R.string.watchface_dialog_widget_preset_right;
nextSide = R.string.watchface_dialog_widget_preset_left;
}
hintTextView.setText(getString(R.string.pairing_even_realities_g1_select_next_lens,
getString(currentSide), getString(nextSide)));
// Populate the list of next side candidates. We examine all other devices in the discovery
// list and filter them based on name.
final List<Parcelable> allCandidates =
intent.getParcelableArrayListExtra(DeviceCoordinator.EXTRA_DEVICE_ALL_CANDIDATES);
if (allCandidates != null) {
nextLensCandidates.clear();
for (final Parcelable p : allCandidates) {
final GBDeviceCandidate nextCandidate = (GBDeviceCandidate) p;
// 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());
if (!initialDeviceCandidate.equals(nextCandidate) &&
compositeDeviceName.equals(nextCandidatePrefix)) {
nextLensCandidates.add(nextCandidate);
}
}
}
// No matching device found.
if (nextLensCandidates.isEmpty()) {
GB.toast(getContext(), R.string.pairing_even_realities_g1_find_both_fail,
Toast.LENGTH_LONG, GB.ERROR);
finish();
return;
}
// Setup the BLE callbacks so we get notified when the devices are done bonding.
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) {
pairDevices(initialDeviceCandidate, nextLensCandidates.get(0));
} else {
pairDevices(nextLensCandidates.get(0), initialDeviceCandidate);
}
} else {
// There is more than one matching candidate, display all of the candidates as a list
// and let the user choose the correct one. This should be rare an only happen if the
// user has multiple pairs of glasses around them. Even then, the two digit id should
// not be the same between devices, but since it can only be 00-99, there are only 100
// options, so collisions are inevitable. Better to have this and not need it than have
// users get stuck.
DeviceCandidateAdapter nextLensCandidatesAdapter =
new DeviceCandidateAdapter(this, nextLensCandidates);
nextLensCandidatesListView.setAdapter(nextLensCandidatesAdapter);
nextLensCandidatesListView.setOnItemClickListener(this);
// Hide the progress bar. The list is visible by default, so it will be shown.
progressBar.setVisibility(View.GONE);
}
}
@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) {
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);
}
@Override
public void unregisterBroadcastReceivers() {
AndroidUtils.safeUnregisterBroadcastReceiver(this, bluetoothReceiver);
}
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 (device != null) {
final int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,
BluetoothDevice.BOND_NONE);
LOG.debug("{} Bond state: {}", device.getAddress(), bondState);
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);
}
}
}
}
}
}
}

View File

@ -48,6 +48,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.colmi.ColmiR10Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.cycling_sensor.coordinator.CyclingSensorCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.divoom.PixooCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.domyos.DomyosT540Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.evenrealities.G1DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.femometer.FemometerVinca2DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.fitpro.colacao.ColaCao21Coordinator;
@ -517,6 +518,7 @@ public enum DeviceType {
WASPOS(WaspOSCoordinator.class),
UM25(UM25Coordinator.class),
DOMYOS_T540(DomyosT540Coordinator.class),
EVEN_REALITIES_G_1(G1DeviceCoordinator.class),
NOTHING_EAR1(Ear1Coordinator.class),
NOTHING_EAR2(Ear2Coordinator.class),
NOTHING_EAR_STICK(EarStickCoordinator.class),

View File

@ -0,0 +1,72 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.evenrealities;
import java.util.UUID;
public class G1DeviceConstants {
public static final UUID UUID_SERVICE_NORDIC_UART =
UUID.fromString("6e400001-b5a3-f393-e0a9-e50e24dcca9e");
public static final UUID UUID_CHARACTERISTIC_NORDIC_UART_TX =
UUID.fromString("6e400002-b5a3-f393-e0a9-e50e24dcca9e");
public static final UUID UUID_CHARACTERISTIC_NORDIC_UART_RX =
UUID.fromString("6e400003-b5a3-f393-e0a9-e50e24dcca9e");
public static final int MTU = 251;
// Extract the L or R at the end of the device prefix.
public static Side getSideFromFullName(String deviceName) {
int prefixSize = "Even G1_XX_X".length();
if (deviceName.length() < prefixSize) {
return null;
}
String prefix = deviceName.substring(0, prefixSize);
char side = prefix.charAt(prefix.length() - 1);
if (side == 'L' || side == 'R') {
return side == 'L' ? Side.LEFT : Side.RIGHT;
}
return null;
}
public static String getNameFromFullName(String deviceName) {
int prefixSize = "Even G1_XX".length();
if (deviceName.length() < prefixSize) {
return null;
}
return deviceName.substring(0, prefixSize);
}
public enum Side {
LEFT,
RIGHT;
}
// 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),
DASHBOARD((byte) 0x22),
NOTIFICATION((byte) 0x4B),
BMP((byte) 0x15),
FW_INFO_REQUEST((byte) 0x23),
FW_INFO_RESPONSE((byte) 0x6E),
CRC((byte) 0x16);
final public byte id;
CommandId(byte id) {
this.id = id;
}
}
}

View File

@ -0,0 +1,215 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.evenrealities;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.os.Handler;
import android.os.Looper;
import android.widget.Toast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.charset.StandardCharsets;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
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.
* 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 {
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;
public G1DeviceSupport() {
this(LOG);
}
public G1DeviceSupport(Logger logger) {
super(logger);
addSupportedService(G1DeviceConstants.UUID_SERVICE_NORDIC_UART);
}
@Override
protected TransactionBuilder initializeDevice(TransactionBuilder builder) {//}, int deviceIdx) {
this.heartBeatSequence = 0;
builder.add(
new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
rxCharacteristic = getCharacteristic(G1DeviceConstants.UUID_CHARACTERISTIC_NORDIC_UART_RX);
txCharacteristic = getCharacteristic(G1DeviceConstants.UUID_CHARACTERISTIC_NORDIC_UART_TX);
builder.requestMtu(G1DeviceConstants.MTU);
if (rxCharacteristic == null || txCharacteristic == 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,
getContext()));
GB.toast(getContext(), "Failed to connect to Glasses, waiting for reconnect.",
Toast.LENGTH_LONG, GB.ERROR);
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");
}
// 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();
// Schedule the battery polling.
scheduleBatteryPolling();
// Device is ready for use.
builder.add(
new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext()));
gbDevice.sendDeviceUpdateIntent(getContext());
return builder;
}
@Override
public void dispose() {
// Remove all callbacks
backgroundTasksHandler.removeCallbacksAndMessages(null);
super.dispose();
}
@Override
public boolean useAutoConnect() {
return false;
}
@Override
public boolean onCharacteristicChanged(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic) {
// Super already handled this.
if (super.onCharacteristicChanged(gatt, characteristic)) {
return true;
}
// If this is the correct UART RX message, parse it.
if (G1DeviceConstants.UUID_CHARACTERISTIC_NORDIC_UART_RX.equals(characteristic.getUuid())) {
byte[] payload = characteristic.getValue();
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);
}
}
}
return false;
}
/**
* If configuration options can be set on the device, this method
* can be overridden and implemented by the device support class.
*
* @param config the device specific option to set on the device
*/
@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;
}
}
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();
};
}

View File

@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="45sp"
android:height="45sp"
android:viewportWidth="30"
android:viewportHeight="30">
<path
android:fillColor="?attr/deviceIconLight"
android:pathData="M3.871 3.877h20.925a0.947 0.947 0 0 1 0.948 0.947v20.01a0.947 0.947 0 0 1-0.948 0.948H3.871a0.947 0.947 0 0 1-0.947-0.948V4.824a0.947 0.947 0 0 1 0.947-0.947z"
android:strokeWidth="3.57" />
<path
android:fillColor="?attr/deviceIconDark"
android:pathData="M3.879 3.035h20.925a0.947 0.947 0 0 1 0.947 0.947v20.01a0.947 0.947 0 0 1-0.947 0.948H3.88a0.947 0.947 0 0 1-0.947-0.948V3.982A0.947 0.947 0 0 1 3.88 3.035z"
android:strokeWidth="3.57" />
<path
android:fillColor="?attr/deviceIconPrimary"
android:pathData="M3.871 3.413h20.925a0.947 0.947 0 0 1 0.948 0.947v20.01a0.947 0.947 0 0 1-0.948 0.948H3.871a0.947 0.947 0 0 1-0.947-0.948V4.36A0.947 0.947 0 0 1 3.87 3.413z"
android:strokeWidth="3.57" />
<path
android:fillColor="?attr/deviceIconOnPrimary"
android:pathData= "m 9.26,11.01c -2.72,-0.06 -5.67,0.62 -5.83,0.92 -0.11,0.21 -0.11,0.21 0.56,0.21 0.72,0 0.72,0 0.64,0.41 -0.07,0.36 -0.12,0.39 -0.76,0.39 -0.29,0 -0.52,0.01 -0.52,0.02 0,0.74 1.18,0.05 1.44,1.27 0.63,2.91 1.4,3.08 3.11,3.26 3.47,0.35 4.4,-0.32 5.47,-3.94 0.28,-0.97 1.65,-0.97 1.94,0 1.07,3.63 1.92,4.25 5.37,3.94 2.39,-0.26 2.81,-1.38 3.21,-3.24 0.27,-1.25 1.44,-0.53 1.44,-1.29 0,-0.01 -0.23,-0.02 -0.52,-0.02 -0.64,0 -0.69,-0.02 -0.76,-0.39 -0.07,-0.41 -0.08,-0.41 0.64,-0.41 0.67,0 0.67,0 0.56,-0.21 -0.22,-0.43 -6.36,-1.68 -9.12,-0.27 -0.94,0.48 -2.94,0.36 -3.58,0 -0.86,-0.44 -2.05,-0.62 -3.29,-0.65z"
/>
<path
android:fillColor="?attr/deviceIconPrimary"
android:pathData="m 8.84,11.43c -0.32,0 -0.66,0.01 -1.01,0.04 -1.69,0.11 -3.13,0.27 -2.51,2.79 0.48,1.94 0.38,2.48 2.1,2.79 1.99,0.36 4.07,0.33 4.93,-1.77 0.32,-0.78 0.54,-2.15 0.41,-2.54 -0.29,-0.87 -1.69,-1.32 -3.92,-1.3z"
/>
<path
android:fillColor="?attr/deviceIconPrimary"
android:pathData="m 19.85,11.44c -2.23,-0.02 -3.63,0.44 -3.92,1.31 -0.13,0.39 0.09,1.75 0.41,2.54 0.67,1.87 1.72,1.98 3.55,1.88 2.7,-0.16 2.88,-0.62 3.49,-2.95 0.58,-2.07 -0.75,-2.62 -2.52,-2.74 -0.35,-0.02 -0.69,-0.04 -1.01,-0.04z"
/>
</vector>

View File

@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="45sp"
android:height="45sp"
android:viewportWidth="30"
android:viewportHeight="30">
<path
android:fillColor="#7a7a7a"
android:pathData="M3.871 3.877h20.925a0.947 0.947 0 0 1 0.948 0.947v20.01a0.947 0.947 0 0 1-0.948 0.948H3.871a0.947 0.947 0 0 1-0.947-0.948V4.824a0.947 0.947 0 0 1 0.947-0.947z"
android:strokeWidth="3.57" />
<path
android:fillColor="#9f9f9f"
android:pathData="M3.879 3.035h20.925a0.947 0.947 0 0 1 0.947 0.947v20.01a0.947 0.947 0 0 1-0.947 0.948H3.88a0.947 0.947 0 0 1-0.947-0.948V3.982A0.947 0.947 0 0 1 3.88 3.035z"
android:strokeWidth="3.57" />
<path
android:fillColor="#8a8a8a"
android:pathData="M3.871 3.413h20.925a0.947 0.947 0 0 1 0.948 0.947v20.01a0.947 0.947 0 0 1-0.948 0.948H3.871a0.947 0.947 0 0 1-0.947-0.948V4.36A0.947 0.947 0 0 1 3.87 3.413z"
android:strokeWidth="3.57" />
<path
android:fillColor="#fcfcfc"
android:pathData= "m 9.26,11.01c -2.72,-0.06 -5.67,0.62 -5.83,0.92 -0.11,0.21 -0.11,0.21 0.56,0.21 0.72,0 0.72,0 0.64,0.41 -0.07,0.36 -0.12,0.39 -0.76,0.39 -0.29,0 -0.52,0.01 -0.52,0.02 0,0.74 1.18,0.05 1.44,1.27 0.63,2.91 1.4,3.08 3.11,3.26 3.47,0.35 4.4,-0.32 5.47,-3.94 0.28,-0.97 1.65,-0.97 1.94,0 1.07,3.63 1.92,4.25 5.37,3.94 2.39,-0.26 2.81,-1.38 3.21,-3.24 0.27,-1.25 1.44,-0.53 1.44,-1.29 0,-0.01 -0.23,-0.02 -0.52,-0.02 -0.64,0 -0.69,-0.02 -0.76,-0.39 -0.07,-0.41 -0.08,-0.41 0.64,-0.41 0.67,0 0.67,0 0.56,-0.21 -0.22,-0.43 -6.36,-1.68 -9.12,-0.27 -0.94,0.48 -2.94,0.36 -3.58,0 -0.86,-0.44 -2.05,-0.62 -3.29,-0.65z"
/>
<path
android:fillColor="#8a8a8a"
android:pathData="m 8.84,11.43c -0.32,0 -0.66,0.01 -1.01,0.04 -1.69,0.11 -3.13,0.27 -2.51,2.79 0.48,1.94 0.38,2.48 2.1,2.79 1.99,0.36 4.07,0.33 4.93,-1.77 0.32,-0.78 0.54,-2.15 0.41,-2.54 -0.29,-0.87 -1.69,-1.32 -3.92,-1.3z"
/>
<path
android:fillColor="#8a8a8a"
android:pathData="m 19.85,11.44c -2.23,-0.02 -3.63,0.44 -3.92,1.31 -0.13,0.39 0.09,1.75 0.41,2.54 0.67,1.87 1.72,1.98 3.55,1.88 2.7,-0.16 2.88,-0.62 3.49,-2.95 0.58,-2.07 -0.75,-2.62 -2.52,-2.74 -0.35,-0.02 -0.69,-0.04 -1.01,-0.04z"
/>
</vector>

View File

@ -0,0 +1,57 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context="nodomain.freeyourgadget.gadgetbridge.activities.discovery.DiscoveryActivityV2">
<TextView
android:id="@+id/even_g1_pairing_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
android:text="@string/pairing_even_realities_g1_select_next_lens"
android:textIsSelectable="true"
android:textStyle="bold" />
<ProgressBar
android:id="@+id/pairing_progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_weight="1" />
<ListView
android:id="@+id/next_lens_candidates_list"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_gravity="bottom|top"
android:layout_weight="1">
</ListView>
<TextView
android:id="@+id/even_g1_discovery_note"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal|bottom"
android:text="@string/discovery_note"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@color/secondarytext"
android:textStyle="bold" />
<TextView
android:id="@+id/even_g1_pairing_hint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:text="@string/pairing_even_realities_g1_bottom_hint"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@color/secondarytext"
android:textIsSelectable="true" />
</LinearLayout>

View File

@ -680,6 +680,12 @@
<string name="discovery_need_to_enter_authkey">This device needs a secret auth key, long press on the device to enter it. Read the wiki.</string>
<string name="discovery_entered_invalid_authkey">The secret auth key you entered is invalid! Long press on the device to edit.</string>
<string name="discovery_note">Note:</string>
<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_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>
<string name="candidate_item_device_image">Device image</string>
<string name="miband_prefs_alias">Name/Alias</string>
<string name="pref_header_vibration_count">Vibration count</string>
@ -1851,6 +1857,7 @@
<string name="devicetype_colacao21">ColaCao 2021</string>
<string name="devicetype_colacao23">ColaCao 2023</string>
<string name="devicetype_domyos_t540">Domyos T540</string>
<string name="devicetype_even_realities_g1">Even Realities G1</string>
<string name="devicetype_sony_wh_1000xm2">Sony WH-1000XM2</string>
<string name="devicetype_sony_wh_1000xm3">Sony WH-1000XM3</string>
<string name="devicetype_sony_wh_1000xm4">Sony WH-1000XM4</string>