From 85f01ee6ca5df3dcefc24c701a166f4bf05b431e Mon Sep 17 00:00:00 2001 From: jrthomas270 Date: Thu, 29 May 2025 16:21:57 -0700 Subject: [PATCH] Even Realities G1: Support more things Add support for: - Charging status - Battery level of the case - Fetching Serial Number - Parsing hardware information from serial number - Add Hard Reset button - Toggle for device level debug logging - Bug Fix: kill heartbeat worker on disconnect - Weather - Toggle for 12H/24H time - Set the dashboard to minimal on connection --- .../DeviceSettingsPreferenceConst.java | 1 + .../DeviceSpecificSettingsFragment.java | 2 + .../adapter/GBDeviceAdapterv2.java | 12 +- .../GBDeviceEventBatteryIncrementalInfo.java | 71 ++++++ .../GBDeviceEventBatteryInfo.java | 10 +- .../GBDeviceEventVersionInfo.java | 4 +- .../evenrealities/G1DeviceCoordinator.java | 43 ++-- .../evenrealities/G1Communications.java | 218 ++++++++++++++-- .../devices/evenrealities/G1Constants.java | 232 +++++++++++++++++- .../evenrealities/G1DeviceSupport.java | 148 ++++++++--- .../devices/evenrealities/G1SideManager.java | 172 ++++++++++--- .../drawable/ic_even_realities_g1_case.xml | 40 +++ .../ic_even_realities_g1_case_charging.xml | 48 ++++ .../ic_even_realities_g1_case_unknown.xml | 33 +++ ...el_list_even_realities_g1_case_battery.xml | 36 +++ app/src/main/res/values/strings.xml | 11 + .../xml/devicesettings_debug_logs_toggle.xml | 10 + 17 files changed, 962 insertions(+), 129 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventBatteryIncrementalInfo.java create mode 100644 app/src/main/res/drawable/ic_even_realities_g1_case.xml create mode 100644 app/src/main/res/drawable/ic_even_realities_g1_case_charging.xml create mode 100644 app/src/main/res/drawable/ic_even_realities_g1_case_unknown.xml create mode 100644 app/src/main/res/drawable/level_list_even_realities_g1_case_battery.xml create mode 100644 app/src/main/res/xml/devicesettings_debug_logs_toggle.xml diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java index 804e43292..208e28770 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java @@ -324,6 +324,7 @@ public class DeviceSettingsPreferenceConst { public static final String PREF_APP_LOGS_START = "pref_app_logs_start"; public static final String PREF_APP_LOGS_STOP = "pref_app_logs_stop"; + public static final String PREF_DEVICE_LOGS_TOGGLE = "device_logs_enabled"; public static final String MORNING_UPDATES_ENABLED = "morning_updates_enabled"; public static final String MORNING_UPDATES_CATEGORIES_SORTABLE = "morning_updates_categories"; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java index 220c269a1..701d2193b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java @@ -830,6 +830,8 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i addPreferenceHandlerFor(PREF_DUAL_DEVICE_SUPPORT); + addPreferenceHandlerFor(PREF_DEVICE_LOGS_TOGGLE); + addPreferenceHandlerFor(PREF_USER_FITNESS_GOAL); addPreferenceHandlerFor(PREF_USER_FITNESS_GOAL_NOTIFICATION); addPreferenceHandlerFor(PREF_USER_FITNESS_GOAL_SECONDARY); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAdapterv2.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAdapterv2.java index 63702cda5..737dac10e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAdapterv2.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAdapterv2.java @@ -370,6 +370,7 @@ public class GBDeviceAdapterv2 extends ListAdapter device.setBatteryState(super.state, this.batteryIndex); + case LEVEL -> device.setBatteryLevel(super.level, this.batteryIndex); + case VOLTAGE -> device.setBatteryVoltage(super.voltage, this.batteryIndex); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventBatteryInfo.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventBatteryInfo.java index 7036ec726..d2ad9e4a3 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventBatteryInfo.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventBatteryInfo.java @@ -62,6 +62,12 @@ public class GBDeviceEventBatteryInfo extends GBDeviceEvent { return super.toString() + "index: " + batteryIndex + ", level: " + level; } + protected void setDeviceValues(final GBDevice device) { + device.setBatteryLevel(this.level, this.batteryIndex); + device.setBatteryState(this.state, this.batteryIndex); + device.setBatteryVoltage(this.voltage, this.batteryIndex); + } + @Override public void evaluate(final Context context, final GBDevice device) { if ((level < 0 || level > 100) && level != GBDevice.BATTERY_UNKNOWN) { @@ -74,9 +80,7 @@ public class GBDeviceEventBatteryInfo extends GBDeviceEvent { this.level != GBDevice.BATTERY_UNKNOWN && this.level > device.getBatteryLevel(this.batteryIndex); - device.setBatteryLevel(this.level, this.batteryIndex); - device.setBatteryState(this.state, this.batteryIndex); - device.setBatteryVoltage(this.voltage, this.batteryIndex); + setDeviceValues(device); final DevicePrefs devicePrefs = GBApplication.getDevicePrefs(device); final BatteryConfig batteryConfig = device.getDeviceCoordinator().getBatteryConfig(device)[this.batteryIndex]; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventVersionInfo.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventVersionInfo.java index 28ee853fa..e22ba5b53 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventVersionInfo.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventVersionInfo.java @@ -60,7 +60,9 @@ public class GBDeviceEventVersionInfo extends GBDeviceEvent { if (fwVersion2 != null) { device.setFirmwareVersion2(fwVersion2); } - device.setModel(hwVersion); + if (hwVersion != null) { + device.setModel(hwVersion); + } device.sendDeviceUpdateIntent(context); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/evenrealities/G1DeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/evenrealities/G1DeviceCoordinator.java index e3f4fe745..427c2dff5 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/evenrealities/G1DeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/evenrealities/G1DeviceCoordinator.java @@ -18,6 +18,8 @@ import java.util.regex.Pattern; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.GBException; import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettings; +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsScreen; import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCardAction; import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; @@ -168,16 +170,15 @@ public class G1DeviceCoordinator extends AbstractBLEDeviceCoordinator { @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; - } + return 3; + } + + @Override + public BatteryConfig[] getBatteryConfig(final GBDevice device) { + BatteryConfig battery1 = new BatteryConfig(0, GBDevice.BATTERY_ICON_DEFAULT, R.string.even_realities_left_lens); + BatteryConfig battery2 = new BatteryConfig(1, GBDevice.BATTERY_ICON_DEFAULT, R.string.even_realities_right_lens); + BatteryConfig battery3 = new BatteryConfig(2, R.drawable.level_list_even_realities_g1_case_battery, R.string.battery_case); + return new BatteryConfig[]{battery1, battery2, battery3}; } @Override @@ -203,12 +204,26 @@ public class G1DeviceCoordinator extends AbstractBLEDeviceCoordinator { }); } + @Override - public int[] getSupportedDeviceSpecificSettings(GBDevice device) { + public DeviceSpecificSettings getDeviceSpecificSettings(final GBDevice device) { + final DeviceSpecificSettings deviceSpecificSettings = new DeviceSpecificSettings(); if (device.isConnected()) { - return new int[]{R.xml.devicesettings_even_realities_g1_display}; - } else { - return new int[]{}; + deviceSpecificSettings.addRootScreen(R.xml.devicesettings_even_realities_g1_display); + deviceSpecificSettings.addRootScreen(R.xml.devicesettings_timeformat); + final List developer = + deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.DEVELOPER); + developer.add(R.xml.devicesettings_header_system); + developer.add(R.xml.devicesettings_debug_logs_toggle); } + return deviceSpecificSettings; } + + //////////////////////////////////////////////// + // Gadget bridge feature support declarations // + //////////////////////////////////////////////// + + @Override + public boolean supportsWeather() { return true; } + } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/evenrealities/G1Communications.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/evenrealities/G1Communications.java index 151295792..f652dc390 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/evenrealities/G1Communications.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/evenrealities/G1Communications.java @@ -3,8 +3,10 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.evenrealities; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.nio.charset.StandardCharsets; import java.util.function.Function; +import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec; import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions; @@ -116,6 +118,30 @@ public class G1Communications { } } + public static class CommandSendReset extends CommandHandler { + public CommandSendReset() { + super(false, null); + } + + @Override + public byte[] serialize() { + return new byte[] { + G1Constants.CommandId.SYSTEM.id, + G1Constants.SystemSubCommand.RESET.id + }; + } + + @Override + public boolean responseMatches(byte[] payload) { + return false; + } + + @Override + public String getName() { + return "send_reset"; + } + } + public static class CommandGetFirmwareInfo extends CommandHandler { public CommandGetFirmwareInfo(Function callback) { super(true, callback); @@ -123,7 +149,10 @@ public class G1Communications { @Override public byte[] serialize() { - return new byte[] { G1Constants.CommandId.FW_INFO_REQUEST.id, 0x74 }; + return new byte[] { + G1Constants.CommandId.SYSTEM.id, + G1Constants.SystemSubCommand.GET_FW_INFO.id + }; } @Override @@ -161,6 +190,10 @@ public class G1Communications { public String getName() { return "get_battery_info"; } + + public static byte getBatteryPercent(byte[] payload) { + return payload[2]; + } } public static class CommandSendHeartBeat extends CommandHandler { @@ -204,9 +237,14 @@ public class G1Communications { 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; + this.weatherIcon = G1Constants.fromOpenWeatherCondition(weatherInfo.currentConditionCode); + // Convert sunny to a moon if the current time stamp is between sunrise and sunset. + if (timeMilliseconds / 1000 >= weatherInfo.sunSet && + this.weatherIcon == G1Constants.WeatherId.SUNNY) { + this.weatherIcon = G1Constants.WeatherId.NIGHT; + } + // Convert Kelvin -> Celsius. + this.tempInCelsius = (byte) (weatherInfo.currentTemp - 273); } else { this.weatherIcon = 0x00; this.tempInCelsius = 0x00; @@ -224,11 +262,11 @@ public class G1Communications { public byte[] serialize() { byte[] packet = new byte[] { G1Constants.CommandId.DASHBOARD_CONFIG.id, - G1Constants.DashboardConfigSubCommand.SET_TIME_AND_WEATHER.id, + 0x15, // Length = 21 bytes 0x00, sequence, - // Magic number? - 0x01, + // Subcommand + G1Constants.DashboardConfig.SUB_COMMAND_SET_TIME_AND_WEATHER, // Time 32bit place holders (byte) 0x00, (byte) 0x00, @@ -246,10 +284,10 @@ public class G1Communications { // Weather info this.weatherIcon, tempInCelsius, - // F/C - (byte)(useFahrenheit ? 0x01 : 0x00), - // 24H/12H - (byte)(use12HourFormat ? 0x01 : 0x00) + useFahrenheit ? G1Constants.TemperatureUnit.FAHRENHEIT + : G1Constants.TemperatureUnit.CELSIUS, + use12HourFormat ? G1Constants.TimeFormat.TWELVE_HOUR + : G1Constants.TimeFormat.TWENTY_FOUR_HOUR }; BLETypeConversions.writeUint32(packet, 5, (int)(timeMilliseconds / 1000)); BLETypeConversions.writeUint64(packet, 9, timeMilliseconds); @@ -259,14 +297,11 @@ public class G1Communications { @Override public boolean responseMatches(byte[] payload) { - if (payload.length < 4) { - return false; - } - // Command should match and the sequence should match. - return payload[0] == G1Constants.CommandId.DASHBOARD_CONFIG.id && - payload[1] == G1Constants.DashboardConfigSubCommand.SET_TIME_AND_WEATHER.id && - payload[3] == sequence; + return payload.length >= 5 && + payload[0] == G1Constants.CommandId.DASHBOARD_CONFIG.id && + payload[3] == sequence && + payload[4] == G1Constants.DashboardConfig.SUB_COMMAND_SET_TIME_AND_WEATHER; } @Override @@ -275,6 +310,46 @@ public class G1Communications { } } + public static class CommandSetDashboardModeSettings extends CommandHandler { + byte mode; + byte secondaryPaneMode; + public CommandSetDashboardModeSettings(byte mode, byte secondaryPaneMode) { + super(true, null); + this.mode = mode; + this.secondaryPaneMode = secondaryPaneMode; + } + + @Override + public boolean needsGlobalSequence() { return true; } + + @Override + public byte[] serialize() { + return new byte[]{ + G1Constants.CommandId.DASHBOARD_CONFIG.id, + 0x07, // Length + 0x00, // pad + sequence, + G1Constants.DashboardConfig.SUB_COMMAND_SET_MODE, + mode, + secondaryPaneMode + }; + } + + @Override + public boolean responseMatches(byte[] payload) { + // Command should match and the sequence should match. + return payload.length >= 5 && + payload[0] == G1Constants.CommandId.DASHBOARD_CONFIG.id && + payload[3] == sequence && + payload[4] == G1Constants.DashboardConfig.SUB_COMMAND_SET_MODE; + } + + @Override + public String getName() { + return "set_dashboard_mode_settings"; + } + } + public static class CommandGetSilentModeSettings extends CommandHandler { public CommandGetSilentModeSettings(Function callback) { super(true, callback); @@ -373,7 +448,7 @@ public class G1Communications { public byte[] serialize() { return new byte[] { G1Constants.CommandId.SET_DISPLAY_SETTINGS.id, - 0x08, // Subcommand? + 0x08, // Length 0x00, sequence, 0x02, // Seems to be a magic number? @@ -560,4 +635,109 @@ public class G1Communications { return "set_wear_detection_settings_" + (enable ? "enabled" : "disabled"); } } + + public static class CommandGetSerialNumber extends CommandHandler { + public CommandGetSerialNumber(Function callback) { + super(true, callback); + } + + @Override + public byte[] serialize() { + return new byte[] { G1Constants.CommandId.GET_SERIAL_NUMBER.id }; + } + + @Override + public boolean responseMatches(byte[] payload) { + return payload.length >= 16 && payload[0] == G1Constants.CommandId.GET_SERIAL_NUMBER.id; + } + + @Override + public String getName() { + return "get_serial_number"; + } + + public static int getFrameType(byte[] payload) { + String serialNumber = getSerialNumber(payload); + if (serialNumber.length() < 7) return -1; + switch(serialNumber.substring(4, 7)) { + case G1Constants.HardwareDescriptionKey.COLOR_GREY: + return R.string.even_realities_frame_color_grey; + case G1Constants.HardwareDescriptionKey.COLOR_BROWN: + return R.string.even_realities_frame_color_brown; + case G1Constants.HardwareDescriptionKey.COLOR_GREEN: + return R.string.even_realities_frame_color_green; + default: + return -1; + } + } + + public static int getFrameColor(byte[] payload) { + String serialNumber = getSerialNumber(payload); + if (serialNumber.length() < 4) return -1; + switch(serialNumber.substring(0, 4)) { + case G1Constants.HardwareDescriptionKey.FRAME_ROUND: + return R.string.even_realities_frame_shape_G1A; + case G1Constants.HardwareDescriptionKey.FRAME_SQUARE: + return R.string.even_realities_frame_shape_G1B; + default: + return -1; + } + } + + public static String getSerialNumber(byte[] payload) { + return new String(payload, 2, 16, StandardCharsets.US_ASCII); + } + } + + public static class CommandSetDebugLogSettings extends CommandHandler { + private final boolean enable; + public CommandSetDebugLogSettings(boolean enable) { + super(false, null); + this.enable = enable; + } + + @Override + public byte[] serialize() { + return new byte[]{ + G1Constants.CommandId.SYSTEM.id, + G1Constants.SystemSubCommand.SET_DEBUG_LOGGING.id, + enable ? G1Constants.DebugLoggingStatus.ENABLE + : G1Constants.DebugLoggingStatus.DISABLE + }; + } + + @Override + public boolean responseMatches(byte[] payload) { + return false; + } + + @Override + public String getName() { + return "set_debug_mode_settings_" + (enable ? "enabled" : "disabled"); + } + } + + public static class DebugLog { + public static boolean messageMatches(byte[] payload) { + return payload.length >= 1 && payload[0] == G1Constants.CommandId.DEBUG_LOG.id; + } + + public static String getMessage(byte[] payload) { + return new String(payload, 1, payload.length-2, StandardCharsets.US_ASCII); + } + } + + public static class DeviceEvent { + public static boolean messageMatches(byte[] payload) { + return payload.length >= 2 && payload[0] == G1Constants.CommandId.DEVICE_EVENT.id; + } + + public static byte getEventId(byte[] payload) { + return payload[1]; + } + + public static byte getValue(byte[] payload) { + return payload[2]; + } + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/evenrealities/G1Constants.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/evenrealities/G1Constants.java index 2edc4e48a..1dc0abdb1 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/evenrealities/G1Constants.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/evenrealities/G1Constants.java @@ -14,6 +14,7 @@ public class G1Constants { public static final int DEFAULT_COMMAND_TIMEOUT_MS = 5000; public static final int DISPLAY_SETTINGS_PREVIEW_DELAY = 3000; public static final int DEFAULT_RETRY_COUNT = 5; + public static final int CASE_BATTERY_INDEX = 2; public static final String INTENT_TOGGLE_SILENT_MODE = "nodomain.freeyourgadget.gadgetbridge.evenrealities.silent_mode"; // Extract the L or R at the end of the device prefix. @@ -73,6 +74,14 @@ public class G1Constants { } } + public static class HardwareDescriptionKey { + public static final String FRAME_ROUND = "S100"; + public static final String FRAME_SQUARE = "S110"; + public static final String COLOR_GREY = "LAA"; + public static final String COLOR_BROWN = "LBB"; + public static final String COLOR_GREEN = "LCC"; + } + public static class CommandStatus { public static final byte FAILED = (byte)0xCA; public static final byte DATA_CONTINUES = (byte)0xCA; @@ -83,14 +92,16 @@ public class G1Constants { public enum CommandId { NOTIFICATION_CONFIG((byte) 0x04), DASHBOARD_CONFIG((byte) 0x06), - DASHBOARD((byte) 0x22), - FW_INFO_REQUEST((byte) 0x23), + SYNC_SEQUENCE((byte) 0x22), // 0x05 + DASHBOARD_SHOWN((byte) 0x22), // 0x0A + SYSTEM((byte) 0x23), HEARTBEAT((byte) 0x25), BATTERY_LEVEL((byte) 0x2C), INIT((byte) 0x4D), NOTIFICATION((byte) 0x4B), FW_INFO_RESPONSE((byte) 0x6E), - DEVICE_ACTION((byte) 0xF5), + DEBUG_LOG((byte) 0xF4), + DEVICE_EVENT((byte) 0xF5), GET_SILENT_MODE_SETTINGS((byte) 0x2B), // There is more info in this one SET_SILENT_MODE_SETTINGS((byte) 0x03), GET_DISPLAY_SETTINGS((byte) 0x3B), @@ -100,7 +111,8 @@ public class G1Constants { GET_BRIGHTNESS_SETTINGS((byte) 0x29), SET_BRIGHTNESS_SETTINGS((byte) 0x01), GET_WEAR_DETECTION_SETTINGS((byte) 0x3A), - SET_WEAR_DETECTION_SETTINGS((byte) 0x27); + SET_WEAR_DETECTION_SETTINGS((byte) 0x27), + GET_SERIAL_NUMBER((byte) 0x34); final public byte id; @@ -109,16 +121,31 @@ public class G1Constants { } } - public enum DashboardConfigSubCommand { - SET_MODE((byte) 0x07), - UNKNOWN_1((byte) 0x0C), - SET_TIME_AND_WEATHER((byte) 0x15), - // Not sure why they use this one sometimes. - SET_TIME_AND_WEATHER_ALSO((byte) 0x16); + public static class DashboardConfig { + public static final byte SUB_COMMAND_SET_TIME_AND_WEATHER = 0x01; + public static final byte SUB_COMMAND_SET_MODE = 0x06; + + public static final byte MODE_FULL = 0x00; + public static final byte MODE_DUAL = 0x01; + public static final byte MODE_MINIMAl = 0x02; + + public static final byte PANE_NOTES = 0x00; + public static final byte PANE_STOCKS = 0x01; + public static final byte PANE_NEWS = 0x02; + public static final byte PANE_CALENDAR = 0x03; + public static final byte PANE_NAVIGATION = 0x04; + public static final byte PANE_EMPTY = 0x05; + + } + + public enum SystemSubCommand { + RESET((byte) 0x72), + GET_FW_INFO((byte) 0x74), + SET_DEBUG_LOGGING((byte) 0x6C); final public byte id; - DashboardConfigSubCommand(byte id) { + SystemSubCommand(byte id) { this.id = id; } } @@ -127,4 +154,185 @@ public class G1Constants { public static final byte ENABLE = 0x0C; public static final byte DISABLE = 0x0A; } -} \ No newline at end of file + + public static class DebugLoggingStatus { + public static final byte ENABLE = 0x00; + public static final byte DISABLE = (byte)0x31; + } + + public static class DeviceEventId { + // Used to indicate a double tap, but it was used to close the dashboard. + public static final byte DOUBLE_TAP_FOR_EXIT = 0x00; + public static final byte UNKNOWN_1 = 0x01; + public static final byte HEAD_UP = 0x02; + public static final byte HEAD_DOWN = 0x03; + public static final byte SILENT_MODE_ENABLED = 0x04; + public static final byte SILENT_MODE_DISABLED = 0x05; + public static final byte GLASSES_WORN = 0x06; + public static final byte GLASSES_NOT_WORN_NO_CASE = 0x07; + public static final byte CASE_LID_OPEN = 0x08; + // Sent with a payload of 00 or 01 to indicate charging state. + public static final byte GLASSES_CHARGING = 0x09; + // Comes with a payload 00 - 64 + public static final byte GLASSES_SIDE_BATTERY_LEVEL = 0x0A; + public static final byte CASE_LID_CLOSE = 0x0B; + public static final byte UNKNOWN_4 = 0x0C; + public static final byte UNKNOWN_5 = 0x0D; + // Sent with a payload of 00 or 01 to indicate charging state. + public static final byte CASE_CHARGING = 0x0E; + // Comes with a payload 00 - 64 + public static final byte CASE_BATTERY_LEVEL = 0x0F; + public static final byte UNKNOWN_6 = 0x10; + public static final byte BINDING_SUCCESS = 0x11; + public static final byte DASHBOARD_SHOW = 0x1E; + public static final byte DASHBOARD_CLOSE = 0x1F; + // Used to initiate translate or transcribe in the official app. + // For us it's strictly a double tap that only sends the event. + public static final byte DOUBLE_TAP_FOR_ACTION = 0x20; + } + + public static class TemperatureUnit { + public static final byte CELSIUS = 0x00; + public static final byte FAHRENHEIT = 0x01; + } + + public static class TimeFormat { + public static final byte TWELVE_HOUR = 0x00; + public static final byte TWENTY_FOUR_HOUR = 0x01; + } + + public static class WeatherId { + public static final byte NONE = 0x00; + public static final byte NIGHT = 0x01; + public static final byte CLOUDS = 0x02; + public static final byte DRIZZLE = 0x03; + public static final byte HEAVY_DRIZZLE = 0x04; + public static final byte RAIN = 0x05; + public static final byte HEAVY_RAIN = 0x06; + public static final byte THUNDER = 0x07; + public static final byte THUNDERSTORM = 0x08; + public static final byte SNOW = 0x09; + public static final byte MIST = 0x0A; + public static final byte FOG = 0x0B; + public static final byte SAND = 0x0C; + public static final byte SQUALLS = 0x0D; + public static final byte TORNADO = 0x0E; + public static final byte FREEZING_RAIN = 0x0F; + public static final byte SUNNY = 0x10; + } + + public static byte fromOpenWeatherCondition(int openWeatherMapCondition) { + // http://openweathermap.org/weather-conditions + switch (openWeatherMapCondition) { + //Group 2xx: Thunderstorm + case 200: //thunderstorm with light rain: + case 201: //thunderstorm with rain: + case 202: //thunderstorm with heavy rain: + case 210: //light thunderstorm:: + case 211: //thunderstorm: + case 230: //thunderstorm with light drizzle: + case 231: //thunderstorm with drizzle: + case 232: //thunderstorm with heavy drizzle: + case 212: //heavy thunderstorm: + case 221: //ragged thunderstorm: + return WeatherId.THUNDERSTORM; + //Group 3xx: Drizzle + case 300: //light intensity drizzle: + case 301: //drizzle: + case 310: //light intensity drizzle rain: + return WeatherId.DRIZZLE; + case 302: //heavy intensity drizzle: + case 311: //drizzle rain: + case 312: //heavy intensity drizzle rain: + case 313: //shower rain and drizzle: + case 314: //heavy shower rain and drizzle: + case 321: //shower drizzle: + return WeatherId.HEAVY_DRIZZLE; + //Group 5xx: Rain + case 500: //light rain: + case 501: //moderate rain: + return WeatherId.RAIN; + case 502: //heavy intensity rain: + case 503: //very heavy rain: + case 504: //extreme rain: + case 511: //freezing rain: + case 520: //light intensity shower rain: + case 521: //shower rain: + case 522: //heavy intensity shower rain: + case 531: //ragged shower rain: + return WeatherId.HEAVY_RAIN; + //Group 6xx: Snow + case 600: //light snow: + case 601: //snow: + case 602: //heavy snow: + return WeatherId.SNOW; + case 611: //sleet: + case 612: //shower sleet: + case 615: //light rain and snow: + case 616: //rain and snow: + case 620: //light shower snow: + case 621: //shower snow: + case 622: //heavy shower snow: + return WeatherId.FREEZING_RAIN; + //Group 7xx: Atmosphere + case 701: //mist: + return WeatherId.MIST; + case 711: //smoke: + return WeatherId.FOG; + case 721: //haze: + return WeatherId.MIST; + case 731: //sandcase dust whirls: + return WeatherId.SAND; + case 741: //fog: + return WeatherId.FOG; + case 751: //sand: + case 761: //dust: + case 762: //volcanic ash: + return WeatherId.SAND; + case 771: //squalls: + return WeatherId.SQUALLS; + case 781: //tornado: + case 900: //tornado + return WeatherId.TORNADO; + //Group 800: Clear + case 800: //clear sky: + return WeatherId.SUNNY; + //Group 80x: Clouds + case 801: //few clouds: + case 802: //scattered clouds: + case 803: //broken clouds: + case 804: //overcast clouds: + return WeatherId.CLOUDS; + //Group 90x: Extreme + case 903: //cold + return WeatherId.SNOW; + case 904: //hot + return WeatherId.SUNNY; + case 905: //windy + return WeatherId.NONE; + case 906: //hail + return WeatherId.THUNDERSTORM; + //Group 9xx: Additional + case 951: //calm + return WeatherId.SUNNY; + case 952: //light breeze + case 953: //gentle breeze + case 954: //moderate breeze + case 955: //fresh breeze + case 956: //strong breeze + case 957: //high windcase near gale + case 958: //gale + return WeatherId.SQUALLS; + case 901: //tropical storm + case 959: //severe gale + case 960: //storm + case 961: //violent storm + case 902: //hurricane + case 962: //hurricane + return WeatherId.TORNADO; + default: + return WeatherId.SUNNY; + } + + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/evenrealities/G1DeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/evenrealities/G1DeviceSupport.java index 997ee43d9..3c08643a1 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/evenrealities/G1DeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/evenrealities/G1DeviceSupport.java @@ -13,11 +13,13 @@ import android.os.Handler; import android.os.Looper; import android.widget.Toast; +import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; import java.util.concurrent.Callable; import java.util.function.Function; @@ -27,13 +29,15 @@ import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.SettingsActivity; import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; -import nodomain.freeyourgadget.gadgetbridge.devices.jyou.BFH16Constants; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.ItemWithDetails; +import nodomain.freeyourgadget.gadgetbridge.model.Weather; +import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec; 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.service.serial.GBDeviceProtocol; import nodomain.freeyourgadget.gadgetbridge.util.GB; /** @@ -49,9 +53,10 @@ public class G1DeviceSupport extends AbstractBTLEMultiDeviceSupport { private static final Logger LOG = LoggerFactory.getLogger(G1DeviceSupport.class); private final Handler backgroundTasksHandler = new Handler(Looper.getMainLooper()); private BroadcastReceiver intentReceiver = null; - private final Object sendTimeLock = new Object(); + private final Object lensSkewLock = new Object(); private G1SideManager leftSide = null; private G1SideManager rightSide = null; + public G1DeviceSupport() { this(LOG); } @@ -60,17 +65,9 @@ public class G1DeviceSupport extends AbstractBTLEMultiDeviceSupport { 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 @@ -79,8 +76,7 @@ public class G1DeviceSupport extends AbstractBTLEMultiDeviceSupport { // 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_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 && @@ -106,11 +102,9 @@ public class G1DeviceSupport extends AbstractBTLEMultiDeviceSupport { // Register to receive silent mode intent calls from the UI. if (intentReceiver == null) { intentReceiver = new IntentReceiver(); - ContextCompat.registerReceiver( - context, - intentReceiver, - new IntentFilter(G1Constants.INTENT_TOGGLE_SILENT_MODE), - ContextCompat.RECEIVER_NOT_EXPORTED); + ContextCompat.registerReceiver(context, intentReceiver, + new IntentFilter(G1Constants.INTENT_TOGGLE_SILENT_MODE), + ContextCompat.RECEIVER_NOT_EXPORTED); } } @@ -121,6 +115,7 @@ public class G1DeviceSupport extends AbstractBTLEMultiDeviceSupport { 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"); @@ -161,15 +156,22 @@ public class G1DeviceSupport extends AbstractBTLEMultiDeviceSupport { // IMPORTANT: use getDevice(deviceIdx), not getDevice(/* 0 */) here otherwise the device // will lock up in a half initialized state because GB thinks the left side is initialized, // after because the right ran first. - if (side.getState() == GBDevice.State.CONNECTED) { + if (side.getConnectingState() == GBDevice.State.CONNECTED) { builder.add(new SetDeviceStateAction(getDevice(deviceIdx), GBDevice.State.INITIALIZING, getContext())); side.initialize(builder); } synchronized (this) { - if (leftSide != null && leftSide.getState() == GBDevice.State.INITIALIZED && - rightSide != null && rightSide.getState() == GBDevice.State.INITIALIZED) { + if (leftSide != null && leftSide.getConnectingState() == GBDevice.State.INITIALIZED && + rightSide != null && rightSide.getConnectingState() == GBDevice.State.INITIALIZED) { + // set device firmware to prevent the following error when data is saved to the + // database and device firmware has not been set yet. + // java.lang.IllegalArgumentException: the bind value at index 2 is null. + // Must be called before the PostInitialize down below. + getDevice().setFirmwareVersion("N/A"); + getDevice().setFirmwareVersion2("N/A"); + // 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.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, @@ -181,6 +183,7 @@ public class G1DeviceSupport extends AbstractBTLEMultiDeviceSupport { backgroundTasksHandler.postDelayed(() -> { leftSide.postInitializeLeft(); rightSide.postInitializeRight(); + onSetDashboardMode(); onSetTime(); }, 200); } @@ -222,6 +225,7 @@ public class G1DeviceSupport extends AbstractBTLEMultiDeviceSupport { // 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 getQueue = () -> this.getQueue(deviceIdx); + Callable getDevice = () -> this.getDevice(deviceIdx); Function handleEvent = (GBDeviceEvent event) -> { this.evaluateGBDeviceEvent(event); return null; @@ -229,13 +233,12 @@ public class G1DeviceSupport extends AbstractBTLEMultiDeviceSupport { // Create the desired side. if (deviceIdx == G1Constants.Side.LEFT.getDeviceIndex()) { - leftSide = - new G1SideManager(G1Constants.Side.LEFT, backgroundTasksHandler, getQueue, - handleEvent, this::getDevicePrefs, rx, tx); + leftSide = new G1SideManager(G1Constants.Side.LEFT, backgroundTasksHandler, getQueue, + getDevice, 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); + rightSide = new G1SideManager(G1Constants.Side.RIGHT, backgroundTasksHandler, getQueue, + getDevice, handleEvent, this::getDevicePrefs, rx, tx); return rightSide; } @@ -285,8 +288,7 @@ public class G1DeviceSupport extends AbstractBTLEMultiDeviceSupport { 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(); + String leftAddress = getDevice(G1Constants.Side.LEFT.getDeviceIndex()).getAddress(); if (address.equals(leftAddress) && leftSide != null) { return leftSide.handlePayload(characteristic.getValue()); } @@ -317,22 +319,30 @@ public class G1DeviceSupport extends AbstractBTLEMultiDeviceSupport { switch (config) { case DeviceSettingsPreferenceConst.PREF_EVEN_REALITIES_SCREEN_ACTIVATION_ANGLE: // This setting is only sent to the right arm. - if (rightSide != null) rightSide.onSendConfiguration(config); + if (rightSide != null) + rightSide.onSendConfiguration(config); + 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 + onSetTimeOrWeather(); break; default: // Forward to both sides. - if (leftSide != null) leftSide.onSendConfiguration(config); - if (rightSide != null) rightSide.onSendConfiguration(config); + if (leftSide != null) + leftSide.onSendConfiguration(config); + if (rightSide != null) + rightSide.onSendConfiguration(config); break; } } - @Override - public void onSetTime() { - if (leftSide == null || rightSide == null) return; + private void onSetTimeOrWeather() { + if (leftSide == null || rightSide == null) + return; - boolean use12HourFormat = getDevicePrefs().getTimeFormat().equals( - DeviceSettingsPreferenceConst.PREF_TIMEFORMAT_12H); + boolean use12HourFormat = getDevicePrefs().getTimeFormat() + .equals(DeviceSettingsPreferenceConst.PREF_TIMEFORMAT_12H); Calendar c = Calendar.getInstance(TimeZone.getTimeZone("UTC")); long currentMilliseconds = c.getTimeInMillis(); long tzOffset = TimeZone.getDefault().getOffset(currentMilliseconds); @@ -340,18 +350,25 @@ public class G1DeviceSupport extends AbstractBTLEMultiDeviceSupport { // 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); + boolean useFahrenheit = !GBApplication.getPrefs() + .getString(SettingsActivity.PREF_MEASUREMENT_SYSTEM, + metricString).equals(metricString); + + // Pull the weather into a local variable so that if it changes between the two lenses being + // updated, we won't end up with a skewed value. + @Nullable WeatherSpec weather = Weather.getInstance().getWeatherSpec(); // Run in the background in case the command hangs and this was run from the UI thread. backgroundTasksHandler.post(() -> { // This block is synchronized. We do not want two calls to overlap, otherwise the lenses // could get skewed with different values. - synchronized (sendTimeLock) { + 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. G1Communications.CommandHandler leftCommandHandler = new G1Communications.CommandSetTimeAndWeather(timeMilliseconds, - use12HourFormat, useFahrenheit); + use12HourFormat, weather, + useFahrenheit); leftSide.send(leftCommandHandler); if (!leftCommandHandler.waitForResponsePayload()) { LOG.error("Set time on left lens timed out"); @@ -361,21 +378,72 @@ public class G1DeviceSupport extends AbstractBTLEMultiDeviceSupport { rightSide.send(new G1Communications.CommandSetTimeAndWeather(timeMilliseconds, use12HourFormat, + weather, useFahrenheit)); } }); } private void onToggleSilentMode() { - if (leftSide == null || rightSide == null) return; + if (leftSide == null || rightSide == null) + return; // If both lenses are in sync on what the status is, set them both. Otherwise, only set the // right one so they can be resynchronized. - if(leftSide.getSilentModeStatus() == rightSide.getSilentModeStatus()){ + if (leftSide.getSilentModeStatus() == rightSide.getSilentModeStatus()) { leftSide.onToggleSilentMode(); rightSide.onToggleSilentMode(); } else { rightSide.onToggleSilentMode(); } } + + private void onSetDashboardMode() { + // Run in the background in case the command hangs and this was run from the UI thread. + backgroundTasksHandler.post(() -> { + // This block is synchronized. We do not want two calls to overlap, otherwise the lenses + // could get skewed with different values. + synchronized (lensSkewLock) { + // Send to the left 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. + // TODO: Pull these values from the settings and build a UI to configure it. + G1Communications.CommandHandler leftCommandHandler = + new G1Communications.CommandSetDashboardModeSettings( + G1Constants.DashboardConfig.MODE_MINIMAl, + G1Constants.DashboardConfig.PANE_EMPTY); + + leftSide.send(leftCommandHandler); + if (!leftCommandHandler.waitForResponsePayload()) { + LOG.error("Set dashboard on right lens timed out"); + getDevice().setState(GBDevice.State.WAITING_FOR_RECONNECT); + getDevice().sendDeviceUpdateIntent(getContext()); + } + + rightSide.send(new G1Communications.CommandSetDashboardModeSettings( + G1Constants.DashboardConfig.MODE_MINIMAl, + G1Constants.DashboardConfig.PANE_EMPTY)); + } + }); + } + + @Override + public void onReset(int flags) { + if (flags == GBDeviceProtocol.RESET_FLAGS_REBOOT) { + leftSide.send(new G1Communications.CommandSendReset()); + rightSide.send(new G1Communications.CommandSendReset()); + } + } + + @Override + public void onSendWeather(ArrayList weatherSpecs) { + // onSetTimeAndWeather() fetches the weather directly from the global state, so no need to + // pass in the weatherSpecs. + onSetTimeOrWeather(); + } + + @Override + public void onSetTime() { + onSetTimeOrWeather(); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/evenrealities/G1SideManager.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/evenrealities/G1SideManager.java index 0f28ddf8a..e41d6f235 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/evenrealities/G1SideManager.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/evenrealities/G1SideManager.java @@ -14,10 +14,12 @@ import java.util.Set; 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.devicesettings.DeviceSettingsPreferenceConst; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; -import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryIncrementalInfo; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.BatteryState; @@ -38,6 +40,7 @@ public class G1SideManager { private final G1Constants.Side mySide; private final Handler backgroundTasksHandler; private final Callable getQueueHandler; + private final Callable getDeviceHandler; private final Function sendEventHandler; private final Callable getPrefsHandler; private final BluetoothGattCharacteristic rx; @@ -48,15 +51,17 @@ public class G1SideManager { private final Set commandHandlers; private byte globalSequence; private boolean isSilentModeEnabled; - private GBDevice.State state; + private GBDevice.State connectingState; + private boolean debugEnabled; public G1SideManager(G1Constants.Side mySide, Handler backgroundTasksHandler, - Callable getQueue, Function sendEvent, - Callable getPrefs, + Callable getQueue, Callable getDevice, + Function sendEvent, Callable getPrefs, BluetoothGattCharacteristic rx, BluetoothGattCharacteristic tx) { this.mySide = mySide; this.backgroundTasksHandler = backgroundTasksHandler; this.getQueueHandler = getQueue; + this.getDeviceHandler = getDevice; this.sendEventHandler = sendEvent; this.getPrefsHandler = getPrefs; this.rx = rx; @@ -67,9 +72,14 @@ public class G1SideManager { }; this.heartBeatRunner = () -> { - // We can send any command as a heart beat. The official app uses this one. - send(new G1Communications.CommandGetSilentModeSettings(null)); - scheduleHeatBeat(); + if (getDevice().isConnected()) { + // We can send any command as a heart beat. The official app uses this one. + send(new G1Communications.CommandGetSilentModeSettings(null)); + 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(); @@ -85,7 +95,8 @@ public class G1SideManager { // Non Finals this.globalSequence = 0; this.isSilentModeEnabled = false; - this.state = GBDevice.State.CONNECTED; + this.connectingState = GBDevice.State.CONNECTED; + this.debugEnabled = false; } private BtLEQueue getQueue() { @@ -96,6 +107,14 @@ public class G1SideManager { } } + private GBDevice getDevice() { + try { + return getDeviceHandler.call(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + private void evaluateGBDeviceEvent(GBDeviceEvent event) { sendEventHandler.apply(event); } @@ -108,11 +127,19 @@ public class G1SideManager { } } - public GBDevice.State getState() { - return state; + public GBDevice.State getConnectingState() { + return connectingState; } public void initialize(TransactionBuilder transaction) { + // Disable device logging in the prefs. There is no way to query this state from the device + // so instead, it is always disabled on connection, and then if a debug message arrives, the + // setting will be flipped to true. + this.debugEnabled = false; + getDevicePrefs().getPreferences().edit() + .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. @@ -121,7 +148,7 @@ public class G1SideManager { // Schedule the battery polling. scheduleBatteryPolling(); - state = GBDevice.State.INITIALIZED; + connectingState = GBDevice.State.INITIALIZED; } public byte getSilentModeStatus() { @@ -142,6 +169,7 @@ public class G1SideManager { // These can be sent to both, but the left lens is used as the master for these settings. sendInTransaction(transaction, new G1Communications.CommandGetDisplaySettings(this::handleDisplaySettingsPayload)); sendInTransaction(transaction, new G1Communications.CommandGetBrightnessSettings(this::handleBrightnessSettingsPayload)); + sendInTransaction(transaction, new G1Communications.CommandGetSerialNumber(this::handleSerialNumberPayload)); transaction.queue(getQueue()); } @@ -184,15 +212,17 @@ public class G1SideManager { send(new G1Communications.CommandSetWearDetectionSettings( prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_WEAR_SENSOR_TOGGLE, true))); break; + case DeviceSettingsPreferenceConst.PREF_DEVICE_LOGS_TOGGLE: + this.debugEnabled = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_DEVICE_LOGS_TOGGLE, false); + send(new G1Communications.CommandSetDebugLogSettings(this.debugEnabled)); + break; } } public void onToggleSilentMode() { isSilentModeEnabled = !isSilentModeEnabled; - G1Communications.CommandHandler commandHandler = - new G1Communications.CommandSetSilentModeSettings(isSilentModeEnabled); - send(commandHandler); + send(new G1Communications.CommandSetSilentModeSettings(isSilentModeEnabled)); } private void scheduleHeatBeat() { @@ -299,6 +329,22 @@ public class G1SideManager { } } + private void updateBatteryLevel(int level, int index) { + evaluateGBDeviceEvent(new GBDeviceEventBatteryIncrementalInfo(index, level)); + } + + private void updateBatteryLevel(int level) { + updateBatteryLevel(level, mySide.getDeviceIndex()); + } + + private void updateBatteryState(BatteryState state, int index) { + evaluateGBDeviceEvent(new GBDeviceEventBatteryIncrementalInfo(index, state)); + } + + private void updateBatteryState(BatteryState state) { + updateBatteryState(state, mySide.getDeviceIndex()); + } + public boolean handlePayload(byte[] payload) { for (G1Communications.CommandHandler commandHandler : commandHandlers) { if (commandHandler.responseMatches(payload)) { @@ -315,14 +361,14 @@ public class G1SideManager { } } - // These can come in unprompted from the glasses, call the correct handler based on what the - // command is. - if (payload.length > 1) { - if (payload[0] == G1Constants.CommandId.DEVICE_ACTION.id) { - return handleDeviceActionPayload(payload); - } else if (payload[0] == G1Constants.CommandId.DASHBOARD.id) { - return handleDashboardPayload(payload); - } + // The glasses will send unprompted messages indicating certain events happening. + // ex. glasses are taken off, glasses are charging, or touch pad was pressed. + if (G1Communications.DeviceEvent.messageMatches(payload)) { + return handleDeviceEventPayload(payload); + } + + if (G1Communications.DebugLog.messageMatches(payload)) { + return handleDebugLogPayload(payload); } LOG.debug("Unhandled payload on side {}: {}", @@ -333,11 +379,7 @@ public class G1SideManager { } private boolean handleBatteryPayload(byte[] payload) { - GBDeviceEventBatteryInfo batteryInfo = new GBDeviceEventBatteryInfo(); - batteryInfo.state = BatteryState.BATTERY_NORMAL; - batteryInfo.level = payload[2]; - batteryInfo.batteryIndex = mySide.getDeviceIndex(); - evaluateGBDeviceEvent(batteryInfo); + updateBatteryLevel(G1Communications.CommandGetBatteryInfo.getBatteryPercent(payload)); return true; } @@ -349,15 +391,32 @@ public class G1SideManager { 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"; + fwInfo.hwVersion = null; + fwInfo.fwVersion = mySide == G1Constants.Side.LEFT ? version : null; + fwInfo.fwVersion2 = mySide == G1Constants.Side.RIGHT ? version : null; + evaluateGBDeviceEvent(fwInfo); + return true; + } + return false; + } + + private boolean handleSerialNumberPayload(byte[] payload) { + String serialNumber = G1Communications.CommandGetSerialNumber.getSerialNumber(payload); + + // Parse the hardware information out of the serial number. + int shape = G1Communications.CommandGetSerialNumber.getFrameType(payload); + int color = G1Communications.CommandGetSerialNumber.getFrameColor(payload); + if (shape != -1 && color != -1) { + GBDeviceEventVersionInfo fwInfo = new GBDeviceEventVersionInfo(); + fwInfo.hwVersion = GBApplication.getContext().getString( + R.string.even_realities_frame_description, + GBApplication.getContext().getString(color), + GBApplication.getContext().getString(shape), + GBApplication.getContext().getString(R.string.serial_number), + serialNumber); + fwInfo.fwVersion = null; + fwInfo.fwVersion2 = null; evaluateGBDeviceEvent(fwInfo); return true; } @@ -405,13 +464,48 @@ public class G1SideManager { return true; } - private boolean handleDeviceActionPayload(byte[] payload) { - LOG.debug("Device Action payload on side {}: {}", mySide.getDeviceIndex(), Logging.formatBytes(payload)); + private boolean handleDeviceEventPayload(byte[] payload) { + switch (G1Communications.DeviceEvent.getEventId(payload)) { + case G1Constants.DeviceEventId.GLASSES_CHARGING: + updateBatteryState( + G1Communications.DeviceEvent.getValue(payload) == 0x01 + ? BatteryState.BATTERY_CHARGING + : BatteryState.BATTERY_NORMAL); + break; + case G1Constants.DeviceEventId.GLASSES_SIDE_BATTERY_LEVEL: + updateBatteryLevel(G1Communications.DeviceEvent.getValue(payload)); + break; + case G1Constants.DeviceEventId.CASE_CHARGING: + updateBatteryState( + G1Communications.DeviceEvent.getValue(payload) == 0x01 + ? BatteryState.BATTERY_CHARGING + : BatteryState.BATTERY_NORMAL, + G1Constants.CASE_BATTERY_INDEX); + break; + case G1Constants.DeviceEventId.CASE_BATTERY_LEVEL: + updateBatteryLevel(G1Communications.DeviceEvent.getValue(payload), + G1Constants.CASE_BATTERY_INDEX); + break; + case G1Constants.DeviceEventId.GLASSES_NOT_WORN_NO_CASE: + updateBatteryState(BatteryState.NO_BATTERY, G1Constants.CASE_BATTERY_INDEX); + break; + default: + LOG.debug("Device Event on side {}: {}", mySide.getDeviceIndex(), + Logging.formatBytes(payload)); + return false; + } return true; } - private boolean handleDashboardPayload(byte[] payload) { - LOG.debug("Dashboard payload on side {}: {}", mySide.getDeviceIndex(), Logging.formatBytes(payload)); + private boolean handleDebugLogPayload(byte[] payload) { + // Use the local boolean so that we aren't constantly committing the same value to the prefs + if (!this.debugEnabled) { + this.debugEnabled = true; + // Mark the pref as enabled so that the Setting UI reflects the true state. + getDevicePrefs().getPreferences().edit().putBoolean( + DeviceSettingsPreferenceConst.PREF_DEVICE_LOGS_TOGGLE, this.debugEnabled).apply(); + } + LOG.info("{}: {}", mySide, G1Communications.DebugLog.getMessage(payload)); return true; } } diff --git a/app/src/main/res/drawable/ic_even_realities_g1_case.xml b/app/src/main/res/drawable/ic_even_realities_g1_case.xml new file mode 100644 index 000000000..f587f9963 --- /dev/null +++ b/app/src/main/res/drawable/ic_even_realities_g1_case.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_even_realities_g1_case_charging.xml b/app/src/main/res/drawable/ic_even_realities_g1_case_charging.xml new file mode 100644 index 000000000..7cf2cb3f4 --- /dev/null +++ b/app/src/main/res/drawable/ic_even_realities_g1_case_charging.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_even_realities_g1_case_unknown.xml b/app/src/main/res/drawable/ic_even_realities_g1_case_unknown.xml new file mode 100644 index 000000000..a58d16e00 --- /dev/null +++ b/app/src/main/res/drawable/ic_even_realities_g1_case_unknown.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/level_list_even_realities_g1_case_battery.xml b/app/src/main/res/drawable/level_list_even_realities_g1_case_battery.xml new file mode 100644 index 000000000..4abea46af --- /dev/null +++ b/app/src/main/res/drawable/level_list_even_realities_g1_case_battery.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b73326dd6..b27acc6f0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3330,6 +3330,8 @@ Enable logs from watch apps Start logging from watch apps Stop logging from watch apps + Enable logging from the device + Device Logs App connection duration Title Description @@ -3968,4 +3970,13 @@ Changes how high the wearer needs to lift their head for the dashboard to be shown Calibrate Angle Zeroes the angle of the glasses, look straight ahead and tap here + G1A/Round + G1B/Square + Grey + Brown + Green + %1s %2s (%3s: %4s) + Left Lens + Right Lens + diff --git a/app/src/main/res/xml/devicesettings_debug_logs_toggle.xml b/app/src/main/res/xml/devicesettings_debug_logs_toggle.xml new file mode 100644 index 000000000..8a4e46e27 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_debug_logs_toggle.xml @@ -0,0 +1,10 @@ + + + +