/* Copyright (C) 2020-2024 Arjan Schrijver, Taavi Eomäe This file is part of Gadgetbridge. Gadgetbridge is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Gadgetbridge is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.service.devices.nut; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCharacteristic; import android.content.Intent; import android.content.SharedPreferences; import android.widget.Toast; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.math.BigInteger; import java.util.Arrays; import java.util.Map; import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo; import nodomain.freeyourgadget.gadgetbridge.devices.nut.NutConstants; import nodomain.freeyourgadget.gadgetbridge.devices.nut.NutKey; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; 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.service.btle.profiles.IntentListener; import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfo; import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfoProfile; import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo; import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfoProfile; import nodomain.freeyourgadget.gadgetbridge.util.GB; public class NutSupport extends AbstractBTLEDeviceSupport { private static final Logger LOG = LoggerFactory.getLogger(NutSupport.class); private final GBDeviceEventBatteryInfo batteryCmd = new GBDeviceEventBatteryInfo(); private final DeviceInfoProfile deviceInfoProfile; private final BatteryInfoProfile batteryInfoProfile; private final IntentListener listener = new IntentListener() { @Override public void notify(Intent intent) { String action = intent.getAction(); if (action.equals(DeviceInfoProfile.ACTION_DEVICE_INFO)) { handleDeviceInfo((DeviceInfo) intent.getParcelableExtra(DeviceInfoProfile.EXTRA_DEVICE_INFO)); } else if (action.equals(BatteryInfoProfile.ACTION_BATTERY_INFO)) { handleBatteryInfo((BatteryInfo) intent.getParcelableExtra(BatteryInfoProfile.EXTRA_BATTERY_INFO)); } else { LOG.warn("Unhandled intent given to listener"); } } }; private SharedPreferences prefs = null; /** * It uses the proprietary Nut interface. */ private boolean proprietary = false; /** * Proprietary Nut interface needs authentication. *

* Don't write characteristics until authenticated. *

* Will disconnect in a minute if you don't authenticate. */ private boolean authenticated = true; /** * The two keys used for authentication */ private BigInteger key1; private BigInteger key2; public NutSupport() { super(LOG); addSupportedService(NutConstants.SERVICE_BATTERY); addSupportedService(NutConstants.SERVICE_DEVICE_INFO); addSupportedService(NutConstants.SERVICE_IMMEDIATE_ALERT); addSupportedService(NutConstants.SERVICE_LINK_LOSS); addSupportedService(NutConstants.SERVICE_PROPRIETARY_NUT); addSupportedService(NutConstants.SERVICE_UNKNOWN_2); deviceInfoProfile = new DeviceInfoProfile<>(this); deviceInfoProfile.addListener(listener); addSupportedProfile(deviceInfoProfile); batteryInfoProfile = new BatteryInfoProfile<>(this); batteryInfoProfile.addListener(listener); addSupportedProfile(batteryInfoProfile); } private void handleBatteryInfo(BatteryInfo info) { LOG.info("Received Nut battery info"); batteryCmd.level = (short) info.getPercentCharged(); handleGBDeviceEvent(batteryCmd); } private void handleDeviceInfo(DeviceInfo info) { LOG.info("Received Nut device info"); LOG.info(String.valueOf(info)); GBDeviceEventVersionInfo versionInfo = new GBDeviceEventVersionInfo(); if (info.getHardwareRevision() != null) { versionInfo.hwVersion = info.getHardwareRevision(); } if (info.getFirmwareRevision() != null) { versionInfo.fwVersion = info.getFirmwareRevision(); } handleGBDeviceEvent(versionInfo); } @Override protected TransactionBuilder initializeDevice(TransactionBuilder builder) { builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext())); builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext())); // Init prefs prefs = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()); loadKeysFromPrefs(); LOG.debug("Requesting device info!"); deviceInfoProfile.requestDeviceInfo(builder); batteryInfoProfile.requestBatteryInfo(builder); // If this characteristic exists, it has proprietary Nut interface this.proprietary = (getCharacteristic(NutConstants.CHARAC_AUTH_STATUS) != null); if (proprietary) { this.authenticated = false; /** * Part of {@link NutConstants.SERVICE_PROPRIETARY_NUT} * Enables proprietary notification */ builder.notify(getCharacteristic(NutConstants.CHARAC_AUTH_STATUS), true); LOG.info("Enabled authentication status notify"); /** * Part of {@link NutConstants.SERVICE_UNKNOWN_2} * Enables button-press notify */ builder.notify(getCharacteristic(NutConstants.CHARAC_UNKNOWN_2), true); } else { /** * Part of {@link NutConstants.SERVICE_UNKNOWN_1_WEIRDNESS} * Enables button-press notify */ builder.notify(getCharacteristic(NutConstants.CHARAC_CHANGE_POWER), true); } readDeviceInfo(); return builder; } @Override public boolean useAutoConnect() { return true; } @Override public void onFindDevice(boolean enable) { deviceImmediateAlert(enable); } @Override public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { if (super.onCharacteristicChanged(gatt, characteristic)) { return true; } UUID characteristicUUID = characteristic.getUuid(); if (characteristicUUID.equals(NutConstants.CHARAC_AUTH_STATUS)) { handleAuthResult(characteristic.getValue()); return true; } LOG.info("Unhandled characteristic changed: " + characteristicUUID); return false; } @Override public boolean onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { if (super.onCharacteristicRead(gatt, characteristic, status)) { return true; } UUID characteristicUUID = characteristic.getUuid(); if (characteristicUUID.equals(NutConstants.CHARAC_SYSTEM_ID)) { // TODO: Handle System ID read return true; } LOG.info("Unhandled characteristic read: " + characteristicUUID); return false; } /** * Enables or disables link loss alert */ private void deviceLinkLossAlert(boolean enable) { UUID charac; if (this.proprietary) { /** Part of {@link NutConstants.SERVICE_PROPRIETARY_NUT} */ charac = NutConstants.CHARAC_CHANGE_POWER; } else { /** Part of {@link NutConstants.SERVICE_IMMEDIATE_ALERT} */ charac = NutConstants.CHARAC_LINK_LOSS_ALERT_LEVEL; } byte[] payload = new byte[]{(byte) (enable ? 0x00 : 0x01)}; if (enable) { writeCharacteristic("Enable link loss alert", charac, payload); } else { writeCharacteristic("Disable link loss alert", charac, payload); } } /** * Should trigger an immediate alert * * @param enable turn on or not */ private void deviceImmediateAlert(boolean enable) { UUID charac; if (this.proprietary) { /** Part of {@link NutConstants.SERVICE_IMMEDIATE_ALERT} */ charac = NutConstants.CHARAC_LINK_LOSS_ALERT_LEVEL; if (!authenticated) { LOG.warn("Not authenticated, can't alert"); return; } } else { /** Part of {@link NutConstants.SERVICE_PROPRIETARY_NUT} */ charac = NutConstants.CHARAC_CHANGE_POWER; } if (enable) { writeCharacteristic("Start alert", charac, new byte[]{(byte) 0x04}); } else { writeCharacteristic("Stop alert", charac, new byte[]{(byte) 0x03}); } } /** * This will write a new key to the device *

* However, it is irreversible, * if you can't generate the right packets, * the device is basically bricked! *

* If you can generate the correct packets, * it can be reset... somehow * * @param key key */ private void deviceWriteNewKey(byte[] key) { // TODO: Determine each nuance of how this // works before using it! byte[] result_payload = new byte[key.length + 1]; result_payload[0] = (byte) 0x04; System.arraycopy(key, 0, result_payload, 1, key.length); writeCharacteristic("Write new key", NutConstants.CHARAC_DFU_PW, result_payload); } /** * Turns the device off */ private void deviceShutdown() { writeCharacteristic("Shutdown", NutConstants.CHARAC_CHANGE_POWER, new byte[]{0x06}); } /** * Switches the device to Nordic's DFU mode */ private void deviceDFU() { writeCharacteristic("Enable DFU mode", NutConstants.CHARAC_DFU_PW, new byte[]{0x14}); } /** * Specifies how long the alert lasts * * @param duration in seconds (I think) */ private void deviceWriteAlertDuration(int duration) { if (duration == 0) { duration = 15; } UUID charac; if (this.proprietary) { charac = NutConstants.CHARAC_DFU_PW; } else { charac = NutConstants.CHARAC_CHANGE_POWER; } writeCharacteristic("Write alert duration", charac, new byte[]{37, (byte) duration}); } private void readHardwareInfo() { BluetoothGattCharacteristic characteristic = getCharacteristic(NutConstants.CHARAC_HARDWARE_VERSION); if (characteristic != null && ((characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_READ) > 0)) { readCharacteristic("Device read hardware", NutConstants.CHARAC_HARDWARE_VERSION); } BluetoothGattCharacteristic characteristic1 = getCharacteristic(NutConstants.CHARAC_MANUFACTURER_NAME); if (characteristic1 != null && (characteristic1.getProperties() & BluetoothGattCharacteristic.PROPERTY_READ) > 0) { readCharacteristic("Read manufacturer", NutConstants.CHARAC_MANUFACTURER_NAME); } } private void readFirmwareInfo() { BluetoothGattCharacteristic characteristic = getCharacteristic(NutConstants.CHARAC_FIRMWARE_VERSION); if (characteristic != null && (characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_READ) > 0) { readCharacteristic("Read firmware version", NutConstants.CHARAC_FIRMWARE_VERSION); } } private void readBatteryInfo() { BluetoothGattCharacteristic characteristic = getCharacteristic(NutConstants.CHARAC_BATTERY_INFO); if (characteristic != null && (characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_READ) > 0) { readCharacteristic("Read battery info", NutConstants.CHARAC_BATTERY_INFO); } } /** * Loads the three keys from device-specific shared preferences */ private void loadKeysFromPrefs() { if (prefs != null) { LOG.info("Reading keys"); key1 = new BigInteger(prefs.getString("nut_packet_key_1", "0")); key2 = new BigInteger(prefs.getString("nut_packet_key_2", "0")); if (key1.equals(BigInteger.ZERO) || key2.equals(BigInteger.ZERO)) { byte[] challenge = NutKey.hexStringToByteArrayNut(prefs.getString("nut_packet_challenge", "00")); byte[] response = NutKey.hexStringToByteArrayNut(prefs.getString("nut_response_challenge", "00")); if (Arrays.equals(challenge, new byte[]{0x00}) || Arrays.equals(response, new byte[]{0x00})) { GB.toast("No key available for the device", Toast.LENGTH_LONG, GB.ERROR); return; } Map.Entry key = NutKey.reversePasswordGeneration( challenge, response, gbDevice.getAddress() ); if (key == null) { GB.toast("No correct key available for the device", Toast.LENGTH_LONG, GB.ERROR); return; } key1 = key.getKey(); key2 = key.getValue(); LOG.debug("Key was extracted from challenge-response packets"); } else { LOG.debug("Key was preset"); } } } /** * Processes the authentication flow of the proprietary Nut protocol * See more: {@link NutConstants#SERVICE_PROPRIETARY_NUT} * * @param received the notify characteristic's content */ public final void handleAuthResult(byte[] received) { if (received != null && received.length != 0) { if (received[0] == 0x01) { // Password is needed // Preamble, counter, rotating key, static key byte[] payload = new byte[1 + 1 + 3 + 12]; // This is a response to the challenge payload[0] = 0x02; // Modify the challenge byte[] response = NutKey.passwordGeneration(gbDevice.getAddress(), received, key1, key2); System.arraycopy(response, 0, payload, 1, response.length); writeCharacteristic("Authentication", NutConstants.CHARAC_DFU_PW, payload ); LOG.debug("Successfully sent auth"); } else if (received[0] == 0x03) { if (received[1] == 0x55) { LOG.debug("Successful password attempt or uninitialized"); authenticated = true; initChara(); } else { LOG.debug("Error authenticating"); // TODO: Disconnect } } else if (received[0] == 0x05) { LOG.debug("Password has been set"); } else { LOG.debug("Invalid packet"); // TODO: Disconnect } } } /** * Initializes required characteristics */ private void initChara() { if (proprietary) { writeCharacteristic("Init alert 1", NutConstants.CHARAC_LINK_LOSS_ALERT_LEVEL, new byte[]{(byte) 0x00}); writeCharacteristic("Init alert 2", NutConstants.CHARAC_LINK_LOSS_ALERT_LEVEL, new byte[]{(byte) 0x00}); writeCharacteristic("Init alert 3", NutConstants.CHARAC_LINK_LOSS_ALERT_LEVEL, new byte[]{(byte) 0x00}); } } /** * Initiates a read of all the device information characteristics */ private void readDeviceInfo() { readBatteryInfo(); readHardwareInfo(); readFirmwareInfo(); } /** * Just wraps writing into a neat little function * * @param taskName something that describes the task a bit * @param charac the characteristic to write * @param data the data to write */ private void writeCharacteristic(String taskName, UUID charac, byte[] data) { BluetoothGattCharacteristic characteristic = getCharacteristic(charac); TransactionBuilder builder = new TransactionBuilder(taskName); builder.write(characteristic, data); builder.queue(getQueue()); } /** * Just wraps reading into a neat little function * * @param taskName something that describes the task a bit * @param charac the characteristic to read */ private void readCharacteristic(String taskName, UUID charac) { BluetoothGattCharacteristic characteristic = getCharacteristic(charac); TransactionBuilder builder = new TransactionBuilder(taskName); builder.read(characteristic); builder.queue(getQueue()); } @Override public boolean getImplicitCallbackModify() { return true; } @Override public boolean getSendWriteRequestResponse() { return false; } }