/* Copyright (C) 2022-2024 José Rebelo 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.huami.zeppos.operations; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCharacteristic; import android.net.Uri; import android.widget.Toast; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.RandomAccessFile; import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventDisplayMessage; import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService; import nodomain.freeyourgadget.gadgetbridge.devices.huami.zeppos.ZeppOsCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.huami.zeppos.ZeppOsFwHelper; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceBusyAction; import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetProgressAction; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiFirmwareType; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.update.UpdateFirmwareOperation2020; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.ZeppOsSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.operations.AbstractMiBandOperation; import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils; import nodomain.freeyourgadget.gadgetbridge.util.GB; /** * This class is basically UpdateFirmwareOperation2020. However, Zepp OS firmwares are growing huge (200+MB), * which crash Gadgetbridge. In order to refactor the upgrade process for Zepp OS devices without having to * refactor all 52 devices, the code was moved to this class so it can be refactored with a smaller impact. *

* The protocol for Zepp OS devices differs from UpdateFirmwareOperation2020 in the following points: * - no crc16 support (not needed for zepp os) * - buildFirmwareInfoCommand is slightly different * - at the end of the update, we request display items / watchfaces depending on what was installed, to refresh the * preferences in the UI */ public class ZeppOsFirmwareUpdateOperation extends AbstractMiBandOperation { private static final Logger LOG = LoggerFactory.getLogger(ZeppOsFirmwareUpdateOperation.class); private final Uri uri; private final BluetoothGattCharacteristic fwCControlChar; private final BluetoothGattCharacteristic fwCDataChar; private ZeppOsFwHelper fwHelper; private RandomAccessFile raf; protected int mChunkLength = -1; public ZeppOsFirmwareUpdateOperation(final Uri uri, final ZeppOsSupport support) { super(support); this.uri = uri; this.fwCControlChar = getCharacteristic(HuamiService.UUID_CHARACTERISTIC_FIRMWARE); this.fwCDataChar = getCharacteristic(HuamiService.UUID_CHARACTERISTIC_FIRMWARE_DATA); } @Override protected void enableNeededNotifications(TransactionBuilder builder, boolean enable) { builder.notify(fwCControlChar, enable); } @Override protected void enableOtherNotifications(final TransactionBuilder builder, final boolean enable) { // Disable 2021 chunked reads, otherwise firmware upgrades and activity sync get interrupted // FIXME is this still needed? builder.notify(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_CHUNKEDTRANSFER_2021_READ), enable); } @Override protected void doPerform() throws IOException { final GBDevice device = getDevice(); final DeviceCoordinator coordinator = device.getDeviceCoordinator(); if (!(coordinator instanceof ZeppOsCoordinator)) { throw new IOException("Not a Zepp OS coordinator for " + getDevice().getAddress()); } fwHelper = new ZeppOsFwHelper( uri, getContext(), ((ZeppOsCoordinator) coordinator).getDeviceBluetoothName(), ((ZeppOsCoordinator) coordinator).getDeviceSources() ); if (!fwHelper.isValid()) { throw new IOException("Firmware is not valid for: " + getDevice().getAddress()); } raf = new RandomAccessFile(fwHelper.getFile(), "r"); if (!requestParameters()) { displayErrorMessage("Error requesting parameters, aborting."); done(); } } protected void done() { LOG.info("Operation done."); operationFinished(); unsetBusy(); } @Override public boolean onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { if (status != BluetoothGatt.GATT_SUCCESS) { operationFailed(); } return super.onCharacteristicWrite(gatt, characteristic, status); } void operationFailed() { GB.updateInstallNotification(getContext().getString(R.string.updatefirmwareoperation_write_failed), false, 0, getContext()); } @Override public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { UUID characteristicUUID = characteristic.getUuid(); if (fwCControlChar.getUuid().equals(characteristicUUID)) { handleNotificationNotif(characteristic.getValue()); return true; // don't let anyone else handle it } else { super.onCharacteristicChanged(gatt, characteristic); } return false; } protected void handleNotificationNotif(byte[] value) { boolean success = (value[2] == HuamiService.SUCCESS) || ((value[1] == UpdateFirmwareOperation2020.REPLY_UPDATE_PROGRESS) && value.length >= 6); // ugly if (value[0] == HuamiService.RESPONSE && success) { try { switch (value[1]) { case UpdateFirmwareOperation2020.COMMAND_REQUEST_PARAMETERS: { mChunkLength = (value[4] & 0xff) | ((value[5] & 0xff) << 8); LOG.info("got chunk length of " + mChunkLength); sendFwInfo(); break; } case UpdateFirmwareOperation2020.COMMAND_SEND_FIRMWARE_INFO: sendTransferStart(); break; case UpdateFirmwareOperation2020.COMMAND_START_TRANSFER: sendFirmwareDataChunk(0); break; // not used in zepp os //case HuamiService.COMMAND_FIRMWARE_START_DATA: // sendChecksum(); // break; case UpdateFirmwareOperation2020.REPLY_UPDATE_PROGRESS: int offset = (value[2] & 0xff) | ((value[3] & 0xff) << 8) | ((value[4] & 0xff) << 16) | ((value[5] & 0xff) << 24); LOG.info("update progress " + offset + " bytes"); sendFirmwareDataChunk(offset); break; case UpdateFirmwareOperation2020.COMMAND_COMPLETE_TRANSFER: sendFinalize(); break; case UpdateFirmwareOperation2020.COMMAND_FINALIZE_UPDATE: { if (fwHelper.getFirmwareType() == HuamiFirmwareType.FIRMWARE) { TransactionBuilder builder = performInitialized("reboot"); getSupport().sendReboot(builder); builder.queue(getQueue()); } else { GB.updateInstallNotification(getContext().getString(R.string.updatefirmwareoperation_update_complete), false, 100, getContext()); done(); } break; } case HuamiService.COMMAND_FIRMWARE_REBOOT: { LOG.info("Reboot command successfully sent."); GB.updateInstallNotification(getContext().getString(R.string.updatefirmwareoperation_update_complete), false, 100, getContext()); done(); break; } default: { LOG.error("Unexpected response during firmware update: "); getSupport().logMessageContent(value); operationFailed(); displayErrorMessage(getContext().getString(R.string.updatefirmwareoperation_updateproblem_do_not_reboot)); done(); } } } catch (Exception ex) { displayErrorMessage(getContext().getString(R.string.updatefirmwareoperation_updateproblem_do_not_reboot)); done(); } } else { LOG.error("Unexpected notification during firmware update: "); operationFailed(); getSupport().logMessageContent(value); int errorMessage = R.string.updatefirmwareoperation_metadata_updateproblem; // Display a more specific error message for known errors if (value[0] == HuamiService.RESPONSE && value[1] == UpdateFirmwareOperation2020.COMMAND_START_TRANSFER && value[2] == UpdateFirmwareOperation2020.REPLY_ERROR_FREE_SPACE) { // Not enough free space on the device errorMessage = R.string.updatefirmwareoperation_updateproblem_free_space; } else if (value[0] == HuamiService.RESPONSE && value[1] == UpdateFirmwareOperation2020.COMMAND_SEND_FIRMWARE_INFO && value[2] == UpdateFirmwareOperation2020.REPLY_ERROR_LOW_BATTERY) { // Battery is too low errorMessage = R.string.updatefirmwareoperation_updateproblem_low_battery; } displayErrorMessage(getContext().getString(errorMessage)); done(); } if (ArrayUtils.startsWith(value, new byte[]{HuamiService.RESPONSE, UpdateFirmwareOperation2020.COMMAND_FINALIZE_UPDATE, HuamiService.SUCCESS})) { if (fwHelper.getFirmwareType() == HuamiFirmwareType.APP) { // After an app is installed, request the display items from the band (new app will be at the end) try { TransactionBuilder builder = performInitialized("request display items and apps"); getSupport().requestDisplayItems(builder); getSupport().requestApps(builder); builder.queue(getQueue()); } catch (final IOException e) { LOG.error("Failed to request display items after app install", e); } } else if (fwHelper.getFirmwareType() == HuamiFirmwareType.WATCHFACE) { // After a watchface is installed, request the watchfaces from the band (new watchface will be at the end) try { TransactionBuilder builder = performInitialized("request watchfaces and apps"); getSupport().requestWatchfaces(builder); getSupport().requestApps(builder); builder.queue(getQueue()); } catch (final IOException e) { LOG.error("Failed to request watchfaces after watchface install", e); } } } } private void displayErrorMessage(String message) { getSupport().handleGBDeviceEvent(new GBDeviceEventDisplayMessage(message, Toast.LENGTH_LONG, GB.ERROR)); } public void sendFwInfo() { try { TransactionBuilder builder = performInitialized("send firmware info"); builder.add(new SetDeviceBusyAction(getDevice(), getContext().getString(R.string.updating_firmware), getContext())); builder.write(fwCControlChar, buildFirmwareInfoCommand()); builder.queue(getQueue()); } catch (IOException e) { LOG.error("Error sending firmware info: " + e.getLocalizedMessage(), e); } } protected byte[] buildFirmwareInfoCommand() { final int fwSize = fwHelper.getSize(); final int crc32 = fwHelper.getCrc32(); final byte[] sizeBytes = BLETypeConversions.fromUint32(fwSize); final byte[] chunkSizeBytes = BLETypeConversions.fromUint16(mChunkLength); final byte[] crcBytes = BLETypeConversions.fromUint32(crc32); return new byte[]{ UpdateFirmwareOperation2020.COMMAND_SEND_FIRMWARE_INFO, fwHelper.getFirmwareType().getValue(), sizeBytes[0], sizeBytes[1], sizeBytes[2], sizeBytes[3], crcBytes[0], crcBytes[1], crcBytes[2], crcBytes[3], chunkSizeBytes[0], chunkSizeBytes[1], 0, // 0 to update in foreground, 1 for background (byte) 0xff, // ?? }; } public boolean requestParameters() { try { TransactionBuilder builder = performInitialized("get update capabilities"); byte[] bytes = new byte[]{UpdateFirmwareOperation2020.COMMAND_REQUEST_PARAMETERS}; builder.write(fwCControlChar, bytes); builder.queue(getQueue()); return true; } catch (IOException e) { LOG.error("Error sending firmware info: " + e.getLocalizedMessage(), e); return false; } } private void sendFirmwareDataChunk(int offset) { int len = fwHelper.getSize(); int remaining = len - offset; final int packetLength = getSupport().getMTU() - 3; int chunkLength = mChunkLength; if (remaining < mChunkLength) { chunkLength = remaining; } int packets = chunkLength / packetLength; int chunkProgress = 0; try { if (remaining <= 0) { sendTransferComplete(); return; } TransactionBuilder builder = performInitialized("send firmware packets"); for (int i = 0; i < packets; i++) { raf.seek(offset + (long) i * packetLength); byte[] fwChunk = new byte[packetLength]; raf.read(fwChunk); builder.write(fwCDataChar, fwChunk); chunkProgress += packetLength; } if (chunkProgress < chunkLength) { raf.seek(offset + (long) packets * packetLength); byte[] lastChunk = new byte[chunkLength - chunkProgress]; raf.read(lastChunk); builder.write(fwCDataChar, lastChunk); } int progressPercent = (int) ((((float) (offset + chunkLength)) / len) * 100); builder.add(new SetProgressAction(getContext().getString(R.string.updatefirmwareoperation_update_in_progress), true, progressPercent, getContext())); builder.queue(getQueue()); } catch (final IOException e) { LOG.error("Unable to send fw to device", e); GB.updateInstallNotification(getContext().getString(R.string.updatefirmwareoperation_firmware_not_sent), false, 0, getContext()); } } protected void sendTransferStart() throws IOException { final TransactionBuilder builder = performInitialized("transfer complete"); builder.write(fwCControlChar, new byte[]{ UpdateFirmwareOperation2020.COMMAND_START_TRANSFER, 1, }); builder.queue(getQueue()); } protected void sendTransferComplete() throws IOException { final TransactionBuilder builder = performInitialized("transfer complete"); builder.write(fwCControlChar, new byte[]{ UpdateFirmwareOperation2020.COMMAND_COMPLETE_TRANSFER, }); builder.queue(getQueue()); } protected void sendFinalize() throws IOException { final TransactionBuilder builder = performInitialized("finalize firmware"); builder.write(fwCControlChar, new byte[]{ UpdateFirmwareOperation2020.COMMAND_FINALIZE_UPDATE, }); builder.queue(getQueue()); } }