mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
Even Realities G1: Move Packet Sequence Generation to the shared space
Previously, each side of the glasses had it's own sequence number, after longer periods of time, these numbers would go out of sync and the glasses would crash because the values are expected to be the same. Now there is a single sequence number, and packets that are sent to both lenses will now share the same sequence number. The heartbeat runner is also moved to the shared space so that the time outs of the BLE connections do not get skewed.
This commit is contained in:
+46
-38
@@ -11,6 +11,7 @@ import org.slf4j.LoggerFactory;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
@@ -23,23 +24,25 @@ public class G1Communications {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(G1Communications.class);
|
||||
|
||||
public abstract static class CommandHandler {
|
||||
protected final byte sequence;
|
||||
private final boolean expectResponse;
|
||||
private final Function<byte[], Boolean> callback;
|
||||
protected byte sequence;
|
||||
private byte[] responsePayload;
|
||||
private int retryCount;
|
||||
|
||||
public CommandHandler(boolean expectResponse, Function<byte[], Boolean> callback) {
|
||||
protected CommandHandler(byte sequence, boolean expectResponse, Function<byte[], Boolean> callback) {
|
||||
this.sequence = sequence;
|
||||
this.expectResponse = expectResponse;
|
||||
this.callback = callback;
|
||||
this.responsePayload = null;
|
||||
this.retryCount = 0;
|
||||
}
|
||||
|
||||
public boolean needsGlobalSequence() { return false; }
|
||||
public void setGlobalSequence(byte sequence) {
|
||||
this.sequence = sequence;
|
||||
protected CommandHandler(boolean expectResponse, Function<byte[], Boolean> callback) {
|
||||
/* sequence is not used */
|
||||
this((byte)0, expectResponse, callback);
|
||||
}
|
||||
|
||||
public int getTimeout() {
|
||||
return G1Constants.DEFAULT_COMMAND_TIMEOUT_MS;
|
||||
}
|
||||
@@ -119,9 +122,11 @@ public class G1Communications {
|
||||
private byte currentChunk;
|
||||
protected final byte[] payload;
|
||||
protected final byte chunkCount;
|
||||
Callable<Byte> getNextSequence;
|
||||
|
||||
public ChunkedCommandHandler(Consumer<CommandHandler> sendCallback, Function<byte[], Boolean> callback, byte[] payload) {
|
||||
super(true, callback);
|
||||
protected ChunkedCommandHandler(byte sequence, Consumer<CommandHandler> sendCallback,
|
||||
Function<byte[], Boolean> callback, byte[] payload) {
|
||||
super(sequence, true, callback);
|
||||
this.sendCallback = sendCallback;
|
||||
this.currentChunk = 0;
|
||||
this.payload = payload;
|
||||
@@ -158,15 +163,17 @@ public class G1Communications {
|
||||
// Copy the chunk of the payload into the packet.
|
||||
System.arraycopy(this.payload, chunkBegin, packet, getHeaderSize(), payloadSize);
|
||||
|
||||
// Advance the chunk.
|
||||
currentChunk++;
|
||||
|
||||
return packet;
|
||||
}
|
||||
|
||||
@Override
|
||||
final public boolean responseMatches(byte[] payload) {
|
||||
return chunkMatches(currentChunk, payload);
|
||||
if (chunkMatches(currentChunk, payload)) {
|
||||
// Advance the chunk when the response is received.
|
||||
currentChunk++;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean sendNextChunk(byte[] payload) {
|
||||
@@ -283,8 +290,7 @@ public class G1Communications {
|
||||
|
||||
public static class CommandSendHeartBeat extends CommandHandler {
|
||||
public CommandSendHeartBeat(byte sequence) {
|
||||
super(false, null);
|
||||
setGlobalSequence(sequence);
|
||||
super(sequence, false, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -317,8 +323,9 @@ public class G1Communications {
|
||||
byte weatherIcon;
|
||||
boolean useFahrenheit;
|
||||
|
||||
public CommandSetTimeAndWeather(long timeMilliseconds, boolean use12HourFormat, WeatherSpec weatherInfo, boolean useFahrenheit) {
|
||||
super(true, null);
|
||||
public CommandSetTimeAndWeather(byte sequence, long timeMilliseconds, boolean use12HourFormat,
|
||||
WeatherSpec weatherInfo, boolean useFahrenheit) {
|
||||
super(sequence, true, null);
|
||||
this.timeMilliseconds = timeMilliseconds;
|
||||
this.use12HourFormat = use12HourFormat;
|
||||
if (weatherInfo != null) {
|
||||
@@ -340,13 +347,11 @@ public class G1Communications {
|
||||
}
|
||||
this.useFahrenheit = useFahrenheit;
|
||||
}
|
||||
public CommandSetTimeAndWeather(long timeMilliseconds, boolean use12HourFormat, boolean useFahrenheit) {
|
||||
this(timeMilliseconds, use12HourFormat, null, useFahrenheit);
|
||||
public CommandSetTimeAndWeather(byte sequence, long timeMilliseconds, boolean use12HourFormat,
|
||||
boolean useFahrenheit) {
|
||||
this(sequence, timeMilliseconds, use12HourFormat, null, useFahrenheit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean needsGlobalSequence() { return true; }
|
||||
|
||||
@Override
|
||||
public byte[] serialize() {
|
||||
byte[] packet = new byte[] {
|
||||
@@ -402,15 +407,12 @@ public class G1Communications {
|
||||
public static class CommandSetDashboardModeSettings extends CommandHandler {
|
||||
byte mode;
|
||||
byte secondaryPaneMode;
|
||||
public CommandSetDashboardModeSettings(byte mode, byte secondaryPaneMode) {
|
||||
super(true, null);
|
||||
public CommandSetDashboardModeSettings(byte sequence, byte mode, byte secondaryPaneMode) {
|
||||
super(sequence, true, null);
|
||||
this.mode = mode;
|
||||
this.secondaryPaneMode = secondaryPaneMode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean needsGlobalSequence() { return true; }
|
||||
|
||||
@Override
|
||||
public byte[] serialize() {
|
||||
return new byte[]{
|
||||
@@ -523,16 +525,13 @@ public class G1Communications {
|
||||
private final boolean preview;
|
||||
private final byte height;
|
||||
private final byte depth;
|
||||
public CommandSetDisplaySettings(boolean preview, byte height, byte depth) {
|
||||
super(true, null);
|
||||
public CommandSetDisplaySettings(byte sequence, boolean preview, byte height, byte depth) {
|
||||
super(sequence, true, null);
|
||||
this.preview = preview;
|
||||
this.height = height;
|
||||
this.depth = depth;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean needsGlobalSequence() { return true; }
|
||||
|
||||
@Override
|
||||
public byte[] serialize() {
|
||||
return new byte[] {
|
||||
@@ -841,7 +840,8 @@ public class G1Communications {
|
||||
boolean enableCalendar,
|
||||
boolean enableCalls,
|
||||
boolean enableSMS) {
|
||||
super(sendCallback, null,
|
||||
// Sequence is not used.
|
||||
super((byte)0, sendCallback, null,
|
||||
generatePayload(appIdentifiers, enableCalendar, enableCalls, enableSMS));
|
||||
}
|
||||
|
||||
@@ -871,7 +871,8 @@ public class G1Communications {
|
||||
// Need to allocate one larger in order to null terminate.
|
||||
String jsonString = json.toString();
|
||||
byte[] bytes = new byte[jsonString.length() + 1];
|
||||
System.arraycopy(jsonString.getBytes(StandardCharsets.US_ASCII), 0, bytes, 0, jsonString.length());
|
||||
System.arraycopy(jsonString.getBytes(StandardCharsets.US_ASCII),
|
||||
0, bytes, 0, jsonString.length());
|
||||
bytes[jsonString.length()] = 0;
|
||||
return bytes;
|
||||
} catch (JSONException e) {
|
||||
@@ -906,7 +907,8 @@ public class G1Communications {
|
||||
private final int messageId;
|
||||
|
||||
public CommandSendNotification(Consumer<CommandHandler> sendCallback, NotificationSpec notificationSpec) {
|
||||
super(sendCallback, null, generatePayload(notificationSpec));
|
||||
// Sequence is not used.
|
||||
super((byte)0, sendCallback, null, generatePayload(notificationSpec));
|
||||
this.messageId = notificationSpec.getId();
|
||||
}
|
||||
|
||||
@@ -916,10 +918,15 @@ public class G1Communications {
|
||||
JSONObject notificationJson = new JSONObject();
|
||||
notificationJson.put("msg_id", notificationSpec.getId());
|
||||
notificationJson.put("action", 0);
|
||||
notificationJson.put("app_identifier", notificationSpec.sourceAppId.substring(0,Math.min(notificationSpec.sourceAppId.length(), 31)));
|
||||
if (notificationSpec.title != null) notificationJson.put("title", notificationSpec.title);
|
||||
if (notificationSpec.subject != null) notificationJson.put("subtitle", notificationSpec.subject);
|
||||
if (notificationSpec.body != null) notificationJson.put("message", notificationSpec.body);
|
||||
notificationJson.put("app_identifier",
|
||||
notificationSpec.sourceAppId.substring(
|
||||
0,Math.min(notificationSpec.sourceAppId.length(), 31)));
|
||||
if (notificationSpec.title != null)
|
||||
notificationJson.put("title", notificationSpec.title);
|
||||
if (notificationSpec.subject != null)
|
||||
notificationJson.put("subtitle", notificationSpec.subject);
|
||||
if (notificationSpec.body != null)
|
||||
notificationJson.put("message", notificationSpec.body);
|
||||
notificationJson.put("time_s", notificationSpec.when / 1000);
|
||||
notificationJson.put("date", new Date(notificationSpec.when).toString());
|
||||
notificationJson.put("display_name", notificationSpec.sourceName);
|
||||
@@ -930,7 +937,8 @@ public class G1Communications {
|
||||
// Need to allocate one larger in order to null terminate.
|
||||
String jsonString = json.toString();
|
||||
byte[] bytes = new byte[jsonString.length() + 1];
|
||||
System.arraycopy(jsonString.getBytes(StandardCharsets.US_ASCII), 0, bytes, 0, jsonString.length());
|
||||
System.arraycopy(jsonString.getBytes(StandardCharsets.US_ASCII),
|
||||
0, bytes, 0, jsonString.length());
|
||||
bytes[jsonString.length()] = 0;
|
||||
return bytes;
|
||||
} catch (JSONException e) {
|
||||
|
||||
+124
-6
@@ -7,6 +7,7 @@ import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.Process;
|
||||
@@ -37,6 +38,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEQueue;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.preferences.DevicePrefs;
|
||||
|
||||
/**
|
||||
* Support class for the Even Realities G1. This sends and receives commands to and from the device.
|
||||
@@ -50,11 +52,15 @@ import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
public class G1DeviceSupport extends AbstractBTLEMultiDeviceSupport {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(G1DeviceSupport.class);
|
||||
private final HandlerThread backgroundThread = new HandlerThread("even_g1_background_thread", Process.THREAD_PRIORITY_DEFAULT);
|
||||
private final Runnable heartBeatRunner;
|
||||
private final Runnable displaySettingsPreviewCloserRunner;
|
||||
private Handler backgroundTasksHandler = null;
|
||||
private BroadcastReceiver intentReceiver = null;
|
||||
private final Object lensSkewLock = new Object();
|
||||
private G1SideManager leftSide = null;
|
||||
private G1SideManager rightSide = null;
|
||||
private long lastHeartBeatTime;
|
||||
private byte globalSequence;
|
||||
|
||||
public G1DeviceSupport() {
|
||||
this(LOG);
|
||||
@@ -67,6 +73,58 @@ public class G1DeviceSupport extends AbstractBTLEMultiDeviceSupport {
|
||||
|
||||
addSupportedService(G1Constants.UUID_SERVICE_NORDIC_UART,
|
||||
G1Constants.Side.RIGHT.getDeviceIndex());
|
||||
|
||||
this.heartBeatRunner = () -> {
|
||||
Calendar c = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
|
||||
long currentMilliseconds = c.getTimeInMillis();
|
||||
LOG.info("{}ms since the last heartbeat", currentMilliseconds - lastHeartBeatTime);
|
||||
lastHeartBeatTime = currentMilliseconds;
|
||||
if (getDevice().isConnected()) {
|
||||
// We can send any command as a heart beat. The official app uses this one.
|
||||
G1Communications.CommandGetSilentModeSettings leftCommand = new G1Communications.CommandGetSilentModeSettings(b -> { return true;});
|
||||
G1Communications.CommandGetSilentModeSettings rightCommand = new G1Communications.CommandGetSilentModeSettings(b -> { return true;});
|
||||
leftSide.send(leftCommand);
|
||||
rightSide.send(rightCommand);
|
||||
|
||||
// Wait for both sides to respond. Resend if there is no response.
|
||||
while(!leftCommand.waitForResponsePayload() || !rightCommand.waitForResponsePayload()) {
|
||||
if (!leftCommand.waitForResponsePayload()) {
|
||||
leftSide.send(leftCommand);
|
||||
}
|
||||
|
||||
if (!rightCommand.waitForResponsePayload()) {
|
||||
rightSide.send(rightCommand);
|
||||
}
|
||||
}
|
||||
|
||||
scheduleHeatBeat();
|
||||
} else {
|
||||
// Don't reschedule if the device is disconnected.
|
||||
LOG.debug("Stopping heartbeat runner since side is in state: {}", getDevice().getState());
|
||||
}
|
||||
};
|
||||
|
||||
this.displaySettingsPreviewCloserRunner = () -> {
|
||||
DevicePrefs prefs = getDevicePrefs();
|
||||
G1Communications.CommandSetDisplaySettings command =
|
||||
new G1Communications.CommandSetDisplaySettings(getNextSequence(),
|
||||
false /* preview */,
|
||||
(byte) prefs.getInt(
|
||||
DeviceSettingsPreferenceConst.PREF_EVEN_REALITIES_SCREEN_HEIGHT,
|
||||
0),
|
||||
// Depth ranges from 1-9 instead of 0-8, so offset by one to convert from
|
||||
// the slider space.
|
||||
(byte) (prefs.getInt(
|
||||
DeviceSettingsPreferenceConst.PREF_EVEN_REALITIES_SCREEN_DEPTH,
|
||||
0) + 1));
|
||||
leftSide.send(command);
|
||||
rightSide.send(command);
|
||||
};
|
||||
|
||||
// Non Finals
|
||||
Calendar c = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
|
||||
this.lastHeartBeatTime = c.getTimeInMillis();
|
||||
this.globalSequence = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -176,6 +234,7 @@ public class G1DeviceSupport extends AbstractBTLEMultiDeviceSupport {
|
||||
// Both sides are initialized. The whole device is initialized, don't use a device
|
||||
// index here. Device 0 is the device that the reset of GB sees.
|
||||
builder.setDeviceState(GBDevice.State.INITIALIZED);
|
||||
|
||||
// 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
|
||||
@@ -185,6 +244,11 @@ public class G1DeviceSupport extends AbstractBTLEMultiDeviceSupport {
|
||||
rightSide.postInitializeRight();
|
||||
onSetDashboardMode();
|
||||
onSetTime();
|
||||
|
||||
// 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();
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
@@ -278,6 +342,51 @@ public class G1DeviceSupport extends AbstractBTLEMultiDeviceSupport {
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized byte getNextSequence() {
|
||||
// Synchronized so the sequence increments atomically.
|
||||
// This number will eventually overflow, and that is fine. The sequence number is just to
|
||||
// match the request and response together.
|
||||
return globalSequence++;
|
||||
}
|
||||
|
||||
private void scheduleHeatBeat() {
|
||||
backgroundTasksHandler.removeCallbacksAndMessages(heartBeatRunner);
|
||||
LOG.info("Starting heartbeat runner delayed by {}ms", G1Constants.HEART_BEAT_DELAY_MS);
|
||||
backgroundTasksHandler.postDelayed(heartBeatRunner, G1Constants.HEART_BEAT_DELAY_MS);
|
||||
}
|
||||
|
||||
private synchronized void sendDisplaySettings() {
|
||||
DevicePrefs prefs = getDevicePrefs();
|
||||
// Synchronized so that there can only ever be one background task.
|
||||
// Clear any existing runner in case the user has changed the value multiple times
|
||||
// before th delay expired.
|
||||
backgroundTasksHandler.removeCallbacksAndMessages(displaySettingsPreviewCloserRunner);
|
||||
|
||||
// The glasses expect the setting to be sent with the preview mode set to true.
|
||||
G1Communications.CommandSetDisplaySettings command = new G1Communications.CommandSetDisplaySettings(
|
||||
getNextSequence(),
|
||||
true /* preview */,
|
||||
(byte)prefs.getInt(DeviceSettingsPreferenceConst.PREF_EVEN_REALITIES_SCREEN_HEIGHT, 0),
|
||||
// Depth ranges from 1-9 instead of 0-8, so offset by one to convert from
|
||||
// the slider space.
|
||||
(byte)(prefs.getInt(DeviceSettingsPreferenceConst.PREF_EVEN_REALITIES_SCREEN_DEPTH, 0) + 1));
|
||||
|
||||
// Send to both sides.
|
||||
leftSide.send(command);
|
||||
rightSide.send(command);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
// On newer APIs, use the runner as the token.
|
||||
backgroundTasksHandler.postDelayed(displaySettingsPreviewCloserRunner,
|
||||
displaySettingsPreviewCloserRunner,
|
||||
G1Constants.DISPLAY_SETTINGS_PREVIEW_DELAY);
|
||||
} else {
|
||||
backgroundTasksHandler.postDelayed(displaySettingsPreviewCloserRunner,
|
||||
G1Constants.DISPLAY_SETTINGS_PREVIEW_DELAY);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
// Below are all the onXXX() handlers overridden from the base class. //
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
@@ -358,6 +467,10 @@ public class G1DeviceSupport extends AbstractBTLEMultiDeviceSupport {
|
||||
if (rightSide != null)
|
||||
rightSide.onSendConfiguration(config);
|
||||
break;
|
||||
case DeviceSettingsPreferenceConst.PREF_EVEN_REALITIES_SCREEN_HEIGHT:
|
||||
case DeviceSettingsPreferenceConst.PREF_EVEN_REALITIES_SCREEN_DEPTH:
|
||||
sendDisplaySettings();
|
||||
break;
|
||||
case SettingsActivity.PREF_MEASUREMENT_SYSTEM:
|
||||
case DeviceSettingsPreferenceConst.PREF_TIMEFORMAT:
|
||||
// Units or time format updated, update the time and weather on the glasses to match
|
||||
@@ -406,9 +519,10 @@ public class G1DeviceSupport extends AbstractBTLEMultiDeviceSupport {
|
||||
synchronized (lensSkewLock) {
|
||||
// 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.
|
||||
byte sequence = getNextSequence();
|
||||
G1Communications.CommandHandler leftCommandHandler =
|
||||
new G1Communications.CommandSetTimeAndWeather(timeMilliseconds,
|
||||
use12HourFormat, weather,
|
||||
new G1Communications.CommandSetTimeAndWeather(sequence, timeMilliseconds,
|
||||
use12HourFormat, weather,
|
||||
useFahrenheit);
|
||||
leftSide.send(leftCommandHandler);
|
||||
if (!leftCommandHandler.waitForResponsePayload()) {
|
||||
@@ -416,10 +530,10 @@ public class G1DeviceSupport extends AbstractBTLEMultiDeviceSupport {
|
||||
getDevice().setUpdateState(GBDevice.State.WAITING_FOR_RECONNECT, getContext());
|
||||
}
|
||||
|
||||
rightSide.send(new G1Communications.CommandSetTimeAndWeather(timeMilliseconds,
|
||||
use12HourFormat,
|
||||
weather,
|
||||
useFahrenheit));
|
||||
rightSide.send(
|
||||
new G1Communications.CommandSetTimeAndWeather(sequence, timeMilliseconds,
|
||||
use12HourFormat, weather,
|
||||
useFahrenheit));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -448,8 +562,10 @@ public class G1DeviceSupport extends AbstractBTLEMultiDeviceSupport {
|
||||
// The glasses will ignore the command on the right lens if it arrives before the
|
||||
// left.
|
||||
// TODO: Pull these values from the settings and build a UI to configure it.
|
||||
byte sequence = getNextSequence();
|
||||
G1Communications.CommandHandler leftCommandHandler =
|
||||
new G1Communications.CommandSetDashboardModeSettings(
|
||||
sequence,
|
||||
G1Constants.DashboardConfig.MODE_MINIMAl,
|
||||
G1Constants.DashboardConfig.PANE_EMPTY);
|
||||
|
||||
@@ -460,6 +576,7 @@ public class G1DeviceSupport extends AbstractBTLEMultiDeviceSupport {
|
||||
}
|
||||
|
||||
rightSide.send(new G1Communications.CommandSetDashboardModeSettings(
|
||||
sequence,
|
||||
G1Constants.DashboardConfig.MODE_MINIMAl,
|
||||
G1Constants.DashboardConfig.PANE_EMPTY));
|
||||
}
|
||||
@@ -489,6 +606,7 @@ public class G1DeviceSupport extends AbstractBTLEMultiDeviceSupport {
|
||||
// Rewrite the App Id to the fixed one used for all notifications. See the comment in
|
||||
// G1Constants.java for more information.
|
||||
notificationSpec.sourceAppId = G1Constants.FIXED_NOTIFICATION_APP_ID.first;
|
||||
// Notifications are only sent to the left side.
|
||||
leftSide.send(new G1Communications.CommandSendNotification(leftSide::send, notificationSpec));
|
||||
}
|
||||
|
||||
|
||||
-87
@@ -1,14 +1,11 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.evenrealities;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.TimeZone;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
@@ -51,14 +48,10 @@ public class G1SideManager {
|
||||
private final BiFunction<String, Integer, TransactionBuilder> createTransactionBuilder;
|
||||
private final BluetoothGattCharacteristic rx;
|
||||
private final BluetoothGattCharacteristic tx;
|
||||
private final Runnable heartBeatRunner;
|
||||
private final Runnable displaySettingsPreviewCloserRunner;
|
||||
private final Set<G1Communications.CommandHandler> commandHandlers;
|
||||
private byte globalSequence;
|
||||
private boolean isSilentModeEnabled;
|
||||
private GBDevice.State connectingState;
|
||||
private boolean debugEnabled;
|
||||
private long lastHeartBeatTime;
|
||||
|
||||
public G1SideManager(G1Constants.Side mySide, Handler backgroundTasksHandler,
|
||||
Callable<BtLEQueue> getQueue, Callable<GBDevice> getDevice,
|
||||
@@ -75,38 +68,12 @@ public class G1SideManager {
|
||||
this.rx = rx;
|
||||
this.tx = tx;
|
||||
|
||||
this.heartBeatRunner = () -> {
|
||||
Calendar c = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
|
||||
long currentMilliseconds = c.getTimeInMillis();
|
||||
LOG.info("Side {}: {}ms since the last heartbeat", mySide, currentMilliseconds - lastHeartBeatTime);
|
||||
lastHeartBeatTime = currentMilliseconds;
|
||||
if (getDevice().isConnected()) {
|
||||
// We can send any command as a heart beat. The official app uses this one.
|
||||
send(new G1Communications.CommandSendHeartBeat(getNextSequence()));
|
||||
scheduleHeatBeat();
|
||||
} else {
|
||||
// Don't reschedule if the device is disconnected.
|
||||
LOG.debug("Stopping heartbeat runner since side is in state: {}", getDevice().getState());
|
||||
}
|
||||
};
|
||||
this.displaySettingsPreviewCloserRunner = () -> {
|
||||
DevicePrefs prefs = getDevicePrefs();
|
||||
send(new G1Communications.CommandSetDisplaySettings(
|
||||
false /* preview */,
|
||||
(byte)prefs.getInt(DeviceSettingsPreferenceConst.PREF_EVEN_REALITIES_SCREEN_HEIGHT, 0),
|
||||
// Depth ranges from 1-9 instead of 0-8, so offset by one to convert from
|
||||
// the slider space.
|
||||
(byte)(prefs.getInt(DeviceSettingsPreferenceConst.PREF_EVEN_REALITIES_SCREEN_DEPTH, 0) + 1)));
|
||||
};
|
||||
this.commandHandlers = new HashSet<>();
|
||||
|
||||
// Non Finals
|
||||
this.globalSequence = 0;
|
||||
this.isSilentModeEnabled = false;
|
||||
this.connectingState = GBDevice.State.CONNECTED;
|
||||
this.debugEnabled = false;
|
||||
Calendar c = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
|
||||
this.lastHeartBeatTime = c.getTimeInMillis();
|
||||
|
||||
}
|
||||
|
||||
@@ -151,11 +118,6 @@ public class G1SideManager {
|
||||
.putBoolean(DeviceSettingsPreferenceConst.PREF_DEVICE_LOGS_TOGGLE, this.debugEnabled)
|
||||
.apply();
|
||||
|
||||
// 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();
|
||||
|
||||
connectingState = GBDevice.State.INITIALIZED;
|
||||
}
|
||||
|
||||
@@ -198,16 +160,11 @@ public class G1SideManager {
|
||||
sendInTransaction(transaction, new G1Communications.CommandGetDisplaySettings(this::handleDisplaySettingsPayload));
|
||||
sendInTransaction(transaction, new G1Communications.CommandGetWearDetectionSettings(this::handleWearDetectionSettingsPayload));
|
||||
sendInTransaction(transaction, new G1Communications.CommandGetNotificationDisplaySettings(this::handleNotificationDisplaySettingsPayload));
|
||||
transaction.queue();
|
||||
}
|
||||
|
||||
public void onSendConfiguration(String config) {
|
||||
DevicePrefs prefs = getDevicePrefs();
|
||||
switch (config) {
|
||||
case DeviceSettingsPreferenceConst.PREF_EVEN_REALITIES_SCREEN_HEIGHT:
|
||||
case DeviceSettingsPreferenceConst.PREF_EVEN_REALITIES_SCREEN_DEPTH:
|
||||
sendDisplaySettings(prefs);
|
||||
break;
|
||||
case DeviceSettingsPreferenceConst.PREF_EVEN_REALITIES_SCREEN_ACTIVATION_ANGLE:
|
||||
send(new G1Communications.CommandSetHeadGestureSettings(
|
||||
(byte)prefs.getInt(DeviceSettingsPreferenceConst.PREF_EVEN_REALITIES_SCREEN_ACTIVATION_ANGLE, 40)));
|
||||
@@ -240,44 +197,6 @@ public class G1SideManager {
|
||||
send(new G1Communications.CommandSetSilentModeSettings(isSilentModeEnabled));
|
||||
}
|
||||
|
||||
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 synchronized byte getNextSequence() {
|
||||
// Synchronized so the sequence increments atomically.
|
||||
// This number will eventually overflow, and that is fine. The sequence number is just to
|
||||
// match the request and response together.
|
||||
return globalSequence++;
|
||||
}
|
||||
|
||||
private synchronized void sendDisplaySettings(DevicePrefs prefs) {
|
||||
// Synchronized so that there can only ever be one background task.
|
||||
// Clear any existing runner in case the user has changed the value multiple times
|
||||
// before th delay expired.
|
||||
backgroundTasksHandler.removeCallbacksAndMessages(displaySettingsPreviewCloserRunner);
|
||||
|
||||
// The glasses expect the setting to
|
||||
send(new G1Communications.CommandSetDisplaySettings(
|
||||
true /* preview */,
|
||||
(byte)prefs.getInt(DeviceSettingsPreferenceConst.PREF_EVEN_REALITIES_SCREEN_HEIGHT, 0),
|
||||
// Depth ranges from 1-9 instead of 0-8, so offset by one to convert from
|
||||
// the slider space.
|
||||
(byte)(prefs.getInt(DeviceSettingsPreferenceConst.PREF_EVEN_REALITIES_SCREEN_DEPTH, 0) + 1)));
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
// On newer APIs, use the runner as the token.
|
||||
backgroundTasksHandler.postDelayed(displaySettingsPreviewCloserRunner,
|
||||
displaySettingsPreviewCloserRunner,
|
||||
G1Constants.DISPLAY_SETTINGS_PREVIEW_DELAY);
|
||||
} else {
|
||||
backgroundTasksHandler.postDelayed(displaySettingsPreviewCloserRunner,
|
||||
G1Constants.DISPLAY_SETTINGS_PREVIEW_DELAY);
|
||||
}
|
||||
}
|
||||
|
||||
public void send(G1Communications.CommandHandler command) {
|
||||
TransactionBuilder transaction =
|
||||
createTransactionBuilder.apply(command.getName(), mySide.getDeviceIndex());
|
||||
@@ -286,12 +205,6 @@ public class G1SideManager {
|
||||
}
|
||||
|
||||
private void sendInTransaction(TransactionBuilder transaction, 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.
|
||||
|
||||
Reference in New Issue
Block a user