1
0
mirror of https://github.com/oliexdev/openScale.git synced 2025-08-16 13:44:26 +02:00

Init commit of AH100 / CH100. (#873)

* Init commit of AH100 / CH100.
It could connect to device, write user info and current time/date. Could read measurement (online, standing on the scale). In theory it should read offline measurements too, but not working on my scale.

* Date constructor corrected

* Measurement date: month is corrected

* [+++] AH100 could read history from scale
This commit is contained in:
Iliaroz
2022-08-20 18:21:24 +03:00
committed by GitHub
parent ee1d086eef
commit 1e3d15c3f1
3 changed files with 819 additions and 0 deletions

View File

@@ -384,6 +384,17 @@ public abstract class BluetoothCommunication {
Timber.d("Set scale user consent for app user id: Not implemented!");
}
// +++
public byte[] getScaleMacAddress() {
String[] mac = btPeripheral.getAddress().split(":");
byte[] macAddress = new byte[6];
for(int i = 0; i < mac.length; i++) {
macAddress[i] = Integer.decode("0x" + mac[i]).byteValue();
}
return macAddress;
}
// ---
/**
* Convert a byte array to hex for debugging purpose
*

View File

@@ -133,6 +133,9 @@ public class BluetoothFactory {
if (deviceName.equals("Weight Scale")){
return new BluetoothSinocare(context);
}
if (deviceName.equals("CH100")){
return new BluetoothHuaweiAH100(context);
}
return null;
}
}

View File

@@ -0,0 +1,805 @@
/* Copyright (C) 2014 olie.xdev <olie.xdev@googlemail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*/
package com.health.openscale.core.bluetooth;
import android.content.Context;
import com.health.openscale.R;
import com.health.openscale.core.OpenScale;
import com.health.openscale.core.datatypes.ScaleMeasurement;
import com.health.openscale.core.datatypes.ScaleUser;
import com.health.openscale.core.utils.Converters;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.UUID;
import timber.log.Timber;
// +++
import android.os.Handler;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class BluetoothHuaweiAH100 extends BluetoothCommunication {
private static final UUID SERVICE_AH100_CUSTOM_SERVICE = BluetoothGattUuid.fromShortCode(0xfaa0);
private static final UUID SERVICE_AH100_CUSTOM_SEND = BluetoothGattUuid.fromShortCode(0xfaa1);
private static final UUID SERVICE_AH100_CUSTOM_RECEIVE = BluetoothGattUuid.fromShortCode(0xfaa2);
// +++
private static byte[] user_id = {0, 0, 0, 0, 0, 0, 0};
private enum STEPS {
INIT,
INIT_W,
AUTHORISE,
SCALE_UNIT,
SCALE_TIME,
USER_INFO,
SCALE_VERSION,
WAIT_MEASUREMENT,
READ_HIST,
READ_HIST_NEXT,
EXIT,
BIND,
EXIT2
}
private static final byte AH100_NOTIFICATION_WAKEUP = 0x00;
private static final byte AH100_NOTIFICATION_GO_SLEEP = 0x01;
private static final byte AH100_NOTIFICATION_UNITS_SET = 0x02;
private static final byte AH100_NOTIFICATION_REMINDER_SET = 0x03;
private static final byte AH100_NOTIFICATION_SCALE_CLOCK = 0x08;
private static final byte AH100_NOTIFICATION_SCALE_VERSION = 0x0C;
private static final byte AH100_NOTIFICATION_MEASUREMENT = 0x0E;
private static final byte AH100_NOTIFICATION_MEASUREMENT2 = (byte) 0x8E;
private static final byte AH100_NOTIFICATION_MEASUREMENT_WEIGHT = 0x0F;
private static final byte AH100_NOTIFICATION_HISTORY_RECORD = 0x10;
private static final byte AH100_NOTIFICATION_HISTORY_RECORD2 = (byte) 0x90;
private static final byte AH100_NOTIFICATION_UPGRADE_RESPONSE = 0x11;
private static final byte AH100_NOTIFICATION_UPGRADE_RESULT = 0x12;
private static final byte AH100_NOTIFICATION_WEIGHT_OVERLOAD = 0x13;
private static final byte AH100_NOTIFICATION_LOW_POWER = 0x14;
private static final byte AH100_NOTIFICATION_MEASUREMENT_ERROR = 0x15;
private static final byte AH100_NOTIFICATION_SET_CLOCK_ACK = 0x16;
private static final byte AH100_NOTIFICATION_OTA_UPGRADE_READY = 0x17;
private static final byte AH100_NOTIFICATION_SCALE_MAC_RECEIVED = 0x18;
private static final byte AH100_NOTIFICATION_HISTORY_UPLOAD_DONE = 0x19;
private static final byte AH100_NOTIFICATION_USER_CHANGED = 0x20;
private static final byte AH100_NOTIFICATION_AUTHENTICATION_RESULT = 0x26;
private static final byte AH100_NOTIFICATION_BINDING_SUCCESSFUL = 0x27;
private static final byte AH100_NOTIFICATION_FIRMWARE_UPDATE_RECEIVED = 0x28;
private static final byte AH100_CMD_SET_UNIT = 2;
private static final byte AH100_CMD_DELETE_ALARM_CLOCK = 3;
private static final byte AH100_CMD_SET_ALARM_CLOCK = 4;
private static final byte AH100_CMD_DELETE_ALL_ALARM_CLOCK = 5;
private static final byte AH100_CMD_GET_ALARM_CLOCK_BY_NO = 6;
private static final byte AH100_CMD_SET_SCALE_CLOCK = 8;
private static final byte AH100_CMD_SELECT_USER = 10;
private static final byte AH100_CMD_USER_INFO = 9;
private static final byte AH100_CMD_GET_RECORD = 11;
private static final byte AH100_CMD_GET_VERSION = 12;
private static final byte AH100_CMD_GET_SCALE_CLOCK = 14;
private static final byte AH100_CMD_GET_USER_LIST_MARK = 15;
private static final byte AH100_CMD_UPDATE_SIGN = 16;
private static final byte AH100_CMD_DELETE_ALL_USER = 17;
private static final byte AH100_CMD_SET_BLE_BROADCAST_TIME = 18;
private static final byte AH100_CMD_FAT_RESULT_ACK = 19;
private static final byte AH100_CMD_GET_LAST_RECORD = 20;
private static final byte AH100_CMD_DISCONNECT_BT = 22;
private static final byte AH100_CMD_HEART_BEAT = 32;
private static final byte AH100_CMD_AUTH = 36;
private static final byte AH100_CMD_BIND_USER = 37;
private static final byte AH100_CMD_OTA_PACKAGE = (byte) 0xDD;
private Context context;
private byte[] authCode;
private byte[] initialKey ;
private byte[] initialValue ;
private byte[] magicKey ;
private int triesToAuth = 0;
private int triesToBind = 0;
private int lastMeasuredWeight = -1;
private boolean authorised = false;
private boolean scaleWakedUp = false;
private boolean scaleBinded = false;
private byte receivedPacketType = 0x00;
private byte[] receivedPacket1;
private Handler beatHandler;
public BluetoothHuaweiAH100(Context context) {
super(context);
this.context = context;
this.beatHandler = new Handler();
authCode = getUserID();
initialKey = hexToByteArray("3D A2 78 4A FB 87 B1 2A 98 0F DE 34 56 73 21 56");
initialValue = hexToByteArray("4E F7 64 32 2F DA 76 32 12 3D EB 87 90 FE A2 19");
}
@Override
public String driverName() {
return "Huawei AH100 Body Fat Scale";
}
///////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////
@Override
protected boolean onNextStep(int stepNr) {
STEPS step;
try {
step = STEPS.values()[stepNr];
} catch (Exception e) {
// stepNr is bigger then we have in STEPS
return false;
}
switch (step) {
case INIT:
// wait scale wake up
Timber.d("AH100::onNextStep step 0 = set notification");
final ScaleUser selectedUser = OpenScale.getInstance().getSelectedScaleUser();
// Setup notification
setNotificationOn(SERVICE_AH100_CUSTOM_SERVICE, SERVICE_AH100_CUSTOM_RECEIVE);
triesToAuth = 0;
authorised = false;
stopMachineState();
break;
case INIT_W:
stopMachineState();
break;
case AUTHORISE:
if ( scaleWakedUp == false ) {
jumpNextToStepNr( STEPS.INIT.ordinal() );
break;
}
// authorize in scale
Timber.d("AH100::onNextStep = authorize on scale");
triesToAuth++;
AHcmdAutorise();
stopMachineState();
break;
case SCALE_UNIT:
Timber.d("AH100::onNextStep = set scale unit");
AHcmdSetUnit();
stopMachineState();
break;
case SCALE_TIME:
Timber.d("AH100::onNextStep = set scale time");
AHcmdDate();
stopMachineState();
break;
case USER_INFO:
Timber.d("AH100::onNextStep = send user info to scale");
if ( !authorised ) {
jumpNextToStepNr( STEPS.AUTHORISE.ordinal() );
break;
}
// set user data
AHcmdUserInfo();
stopMachineState();
break;
case SCALE_VERSION:
Timber.d("AH100::onNextStep = request scale version");
if ( !authorised ) {
jumpNextToStepNr( STEPS.AUTHORISE.ordinal() );
break;
}
AHcmdGetVersion();
stopMachineState();
break;
case WAIT_MEASUREMENT:
AHcmdGetUserList();
Timber.d("AH100::onNextStep = Do nothing, wait while scale tries disconnect");
sendMessage(R.string.info_step_on_scale, 0);
stopMachineState();
break;
case READ_HIST:
Timber.d("AH100::onNextStep = read history record from scale");
if ( !authorised ) {
jumpNextToStepNr( STEPS.AUTHORISE.ordinal() );
break;
}
AHcmdReadHistory();
stopMachineState();
break;
case READ_HIST_NEXT:
Timber.d("AH100::onNextStep = read NEXT history record from scale");
if ( !authorised ) {
jumpNextToStepNr( STEPS.AUTHORISE.ordinal() );
break;
}
AHcmdReadHistoryNext();
stopMachineState();
break;
case EXIT:
Timber.d("AH100::onNextStep = Exit");
authorised = false;
scaleWakedUp = false;
stopHeartBeat();
disconnect();
return false;
case BIND:
Timber.d("AH100::onNextStep = BIND scale to OpenScale");
// Start measurement
sendMessage(R.string.info_step_on_scale, 0);
triesToBind++;
AHcmdBind();
AHcmdBind();
stopMachineState();
break;
case EXIT2:
authorised = false;
scaleWakedUp = false;
stopHeartBeat();
disconnect();
Timber.d("AH100::onNextStep = BIND Exit");
default:
return false;
}
return true;
}
///////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////
@Override
public void onBluetoothNotify(UUID characteristic, byte[] value) {
final byte[] data = value;
byte cmdlength = 0;
Timber.d("AH100::onBluetoothNotify uuid: %s", characteristic.toString());
if (data != null && data.length > 2) {
Timber.d("===> New NOTIFY hex data: %s", byteInHex(data));
cmdlength = data[1];
// responce from scale received
switch (data[2]) {
case AH100_NOTIFICATION_WAKEUP:
scaleWakedUp = true;
if (getStepNr() - 1 == STEPS.INIT_W.ordinal() ) {
Timber.d("AH100::onNotify = Scale is waked up in Init-stage");
startHeartBeat();
resumeMachineState();
break;
}
// if (getStepNr() - 1 == STEPS.BIND.ordinal() ) {
// Timber.d("AH100::onNotify = Scale is waked up in Init-stage");
// jumpBackOneStep();
// resumeMachineState();
// break;
// }
Timber.d("AH100::onNotify = Scale is waked up");
// authorised = false;
// jumpNextToStepNr(STEPS.AUTHORISE.ordinal());
// resumeMachineState();
break;
case AH100_NOTIFICATION_GO_SLEEP:
resumeMachineState();
break;
case AH100_NOTIFICATION_UNITS_SET:
resumeMachineState();
break;
case AH100_NOTIFICATION_REMINDER_SET:
break;
case AH100_NOTIFICATION_SCALE_CLOCK:
resumeMachineState();
break;
case AH100_NOTIFICATION_SCALE_VERSION:
byte[] VERpayload = getPayload(data);
Timber.d("Get Scale Version: input data: %s", byteInHex(VERpayload));
resumeMachineState();
break;
case AH100_NOTIFICATION_MEASUREMENT:
if (data[0] == (byte) 0xBD) {
Timber.d("Scale plain response received");
}
if (data[0] == (byte) 0xBC) {
Timber.d("Scale encoded response received");
receivedPacket1 = Arrays.copyOfRange(data, 0, data.length );
receivedPacketType = AH100_NOTIFICATION_MEASUREMENT;
}
break;
case AH100_NOTIFICATION_MEASUREMENT2:
if (data[0] == (byte) 0xBC) { /// normal packet
Timber.d("Scale encoded response received");
if (receivedPacketType == AH100_NOTIFICATION_MEASUREMENT) {
AHrcvEncodedMeasurement(receivedPacket1, data, AH100_NOTIFICATION_MEASUREMENT);
receivedPacketType = 0x00;
if (scaleBinded == true) {
AHcmdMeasurementAck();
} else {
if (lastMeasuredWeight > 0) {
AHcmdUserInfo(lastMeasuredWeight);
}
}
jumpNextToStepNr( STEPS.READ_HIST.ordinal() );
resumeMachineState();
}
break;
}
if (data[0] == (byte) 0xBD) {
Timber.d("Scale plain response received");
}
jumpNextToStepNr( STEPS.INIT.ordinal() );
resumeMachineState();
break;
case AH100_NOTIFICATION_MEASUREMENT_WEIGHT:
break;
case AH100_NOTIFICATION_HISTORY_RECORD:
if (data[0] == (byte) 0xBD) {
Timber.d("Scale plain response received");
}
if (data[0] == (byte) 0xBC) {
Timber.d("Scale encoded response received");
receivedPacket1 = Arrays.copyOfRange(data, 0, data.length );
receivedPacketType = AH100_NOTIFICATION_HISTORY_RECORD;
}
break;
case AH100_NOTIFICATION_HISTORY_RECORD2:
if (data[0] == (byte) 0xBC) { /// normal packet
Timber.d("Scale encoded response received");
if (receivedPacketType == AH100_NOTIFICATION_HISTORY_RECORD) {
AHrcvEncodedMeasurement(receivedPacket1, data, AH100_NOTIFICATION_HISTORY_RECORD);
receivedPacketType = 0x00;
// todo: jumpback only in ReadHistoryNext
jumpNextToStepNr(STEPS.READ_HIST_NEXT.ordinal(),
STEPS.READ_HIST_NEXT.ordinal());
resumeMachineState();
}
break;
}
if (data[0] == (byte) 0xBD) {
Timber.d("Scale plain response received");
}
jumpNextToStepNr( STEPS.INIT.ordinal() );
resumeMachineState();
break;
case AH100_NOTIFICATION_UPGRADE_RESPONSE:
break;
case AH100_NOTIFICATION_UPGRADE_RESULT:
break;
case AH100_NOTIFICATION_WEIGHT_OVERLOAD:
break;
case AH100_NOTIFICATION_LOW_POWER:
break;
case AH100_NOTIFICATION_MEASUREMENT_ERROR:
break;
case AH100_NOTIFICATION_SET_CLOCK_ACK:
break;
case AH100_NOTIFICATION_OTA_UPGRADE_READY:
break;
case AH100_NOTIFICATION_SCALE_MAC_RECEIVED:
break;
case AH100_NOTIFICATION_HISTORY_UPLOAD_DONE:
resumeMachineState();
break;
case AH100_NOTIFICATION_USER_CHANGED:
resumeMachineState(STEPS.USER_INFO.ordinal()); // waiting wake up in state 4
break;
case AH100_NOTIFICATION_AUTHENTICATION_RESULT:
byte[] ARpayload = getPayload(data);
if ( 1 == ARpayload[0] ){
authorised = true;
magicKey = hexConcatenate(obfuscate(authCode) , Arrays.copyOfRange(initialKey, 7, initialKey.length ) );
resumeMachineState(STEPS.AUTHORISE.ordinal()); // waiting wake up in state 4
} else {
if (triesToAuth < 3){ // try again
jumpNextToStepNr(STEPS.AUTHORISE.ordinal());
} else { // bind scale to own code
jumpNextToStepNr(STEPS.BIND.ordinal());
}
resumeMachineState();
}
// acknowledge that you received the last history data
break;
case AH100_NOTIFICATION_BINDING_SUCCESSFUL:
// jump to authorise again
jumpNextToStepNr(STEPS.SCALE_TIME.ordinal());
scaleBinded = true;
// TODO: count binding tries
break;
case AH100_NOTIFICATION_FIRMWARE_UPDATE_RECEIVED:
break;
default:
break;
} // switch command
}
}
private void AHcmdHeartBeat() {
AHsendCommand(AH100_CMD_HEART_BEAT, new byte[0] );
}
private void AHcmdAutorise() {
AHsendCommand(AH100_CMD_AUTH, authCode);
}
private void AHcmdBind() {
AHsendCommand(AH100_CMD_BIND_USER, authCode);
}
private void AHcmdDate() {
/*
payload[0]: lowerByte(year)
payload[1]: upperByte(year)
payload[2]: month (1..12)
payload[3]: dayOfMonth
payload[4]: hourOfDay (0-23)
payload[5]: minute
payload[6]: second
payload[7]: day of week (Monday=1, Sunday=7)
*/
Calendar currentDateTime = Calendar.getInstance();
int year = currentDateTime.get(Calendar.YEAR);
byte month = (byte)(currentDateTime.get(Calendar.MONTH)+1);
byte day = (byte)currentDateTime.get(Calendar.DAY_OF_MONTH);
byte hour = (byte)currentDateTime.get(Calendar.HOUR_OF_DAY);
byte min = (byte)currentDateTime.get(Calendar.MINUTE);
byte sec = (byte)currentDateTime.get(Calendar.SECOND);
byte dow = (byte)currentDateTime.get(Calendar.DAY_OF_WEEK);
byte[] date = new byte[]{
0x00, 0x00, // year, fill later
month,
day,
hour,
min,
sec,
dow
};
Converters.toInt16Le(date, 0, year);
Timber.d("AH100::AHcmdDate: data to send: %s", byteInHex(date) );
AHsendCommand(AH100_CMD_SET_SCALE_CLOCK, date);
}
private void AHcmdUserInfo() {
///String user example = "27 af 00 2a 03 ff ff";
ScaleUser currentUser = OpenScale.getInstance().getSelectedScaleUser();
int weight = (int) currentUser.getInitialWeight() * 10;
AHcmdUserInfo(weight);
}
private void AHcmdUserInfo(int weight) {
///String user example = "27 af 00 2a 03 ff ff";
/*
payload[7] = sex == 1 ? age | 0x80 : age
payload[8] = height of the user
payload[9] = 0
payload[10] = lowerByte(weight)
payload[11] = upperByte(weight)
payload[12] = lowerByte(impedance)
payload[13] = upperByte(impedance)
*/
ScaleUser currentUser = OpenScale.getInstance().getSelectedScaleUser();
byte height = (byte) currentUser.getBodyHeight();
byte sex = currentUser.getGender().isMale() ? 0 : (byte) 0x80;
byte age = (byte) ( sex | ((byte) currentUser.getAge()) );
byte[] user = new byte[]{
age,
height,
0,
0x00, 0x00, // weight, fill later
(byte) 0xFF, (byte) 0xFF, // resistance, wkwtfdim
(byte) 0x1C, (byte) 0xE2,
};
Converters.toInt16Le(user, 3, weight);
byte[] userinfo = hexConcatenate( authCode, user );
AHsendCommand(AH100_CMD_USER_INFO, userinfo, 14);
}
private void AHcmdReadHistory() {
byte[] pl;
byte[] xp = {xorChecksum(authCode, 0, authCode.length)};
pl = hexConcatenate( authCode, xp );
AHsendCommand(AH100_CMD_GET_RECORD, pl, 0x07 - 1);
}
private void AHcmdReadHistoryNext() {
byte[] pl = {0x01};
AHsendCommand(AH100_CMD_GET_RECORD, pl);
}
private void AHcmdSetUnit() {
// TODO: set correct units
byte[] pl = new byte[]{0x01}; // 1 = kg; 2 = pounds. set kg only
AHsendCommand(AH100_CMD_SET_UNIT, pl);
}
private void AHcmdGetUserList() {
//byte[] pl = new byte[]{};
// byte[] pl = authCode;
// AHsendCommand(AH100_CMD_SELECT_USER, pl);
}
private void AHcmdGetVersion() {
byte[] pl = new byte[]{};
AHsendCommand(AH100_CMD_GET_VERSION, pl);
}
private void AHcmdMeasurementAck() {
byte[] pl = new byte[]{0x00};
AHsendCommand(AH100_CMD_FAT_RESULT_ACK, pl);
}
private void AHrcvEncodedMeasurement(byte[] encdata, byte[] encdata2, byte type) {
byte[] payload = getPayload(encdata);
byte[] data;
try{
data = decryptAES(payload, magicKey, initialValue);
Timber.d("Decrypted measurement: hex data: %s", byteInHex(data));
if ( (type == AH100_NOTIFICATION_MEASUREMENT) ||
(type == AH100_NOTIFICATION_HISTORY_RECORD) ) {
AHaddFatMeasurement(data);
}
} catch (Exception e) {
Timber.d("Decrypting FAIL!!!");
}
}
private void AHaddFatMeasurement(byte[] data) {
if (data.length < 14) {
Timber.d(":: AHaddFatMeasurement : data is too short. Expected at least 14 bytes of data." );
return ;
}
byte userid = data[0]; ///// Arrays.copyOfRange(data, 0, 0 );
lastMeasuredWeight = Converters.fromUnsignedInt16Le(data, 1);
float weight = lastMeasuredWeight / 10.0f;
float fat = Converters.fromUnsignedInt16Le(data, 3) / 10.0f;
int year = Converters.fromUnsignedInt16Le(data, 5) ;
int resistance = Converters.fromUnsignedInt16Le(data, 13) ;
byte month = (byte) (data[7] - 1); // 1..12 to zero-based month
byte dayInMonth = data[8];
byte hour = data[9];
byte minute = data[10];
byte second = data[11];
byte weekNumber = data[12];
Timber.d("---- measured userid %d",userid );
Timber.d("---- measured weight %f",weight );
Timber.d("---- measured fat %f",fat );
Timber.d("---- measured resistance %d",resistance );
Timber.d("---- measured year %d",year );
Timber.d("---- measured month %d",month );
Timber.d("---- measured dayInMonth %d",dayInMonth );
Timber.d("---- measured hour %d",hour );
Timber.d("---- measured minute %d",minute );
Timber.d("---- measured second %d",second );
Timber.d("---- measured week day %d",weekNumber );
///////////////////////////
Calendar calendar = Calendar.getInstance();
calendar.set( year, month, dayInMonth, hour, minute, second);
Date date = calendar.getTime();
ScaleUser currentUser = OpenScale.getInstance().getSelectedScaleUser();
ScaleMeasurement receivedMeasurement = new ScaleMeasurement();
receivedMeasurement.setUserId(currentUser.getId());
receivedMeasurement.setDateTime( date );
receivedMeasurement.setWeight(weight);
receivedMeasurement.setFat(fat);
// receivedMeasurement.setWater(water);
// receivedMeasurement.setMuscle(muscle);
// receivedMeasurement.setBone(bone);
// todo: calculate water, muscle, bones
addScaleMeasurement(receivedMeasurement);
}
private void startHeartBeat() {
Timber.d("*** Heart beat started");
beatHandler.postDelayed(new Runnable() {
@Override
public void run() {
Timber.d("*** heart beat.");
AHcmdHeartBeat();
}
}, 2000); // 2 s
}
private void resetHeartBeat() {
Timber.d("*** 0 heart beat reset");
beatHandler.removeCallbacksAndMessages(null);
startHeartBeat();
}
private void stopHeartBeat() {
Timber.d("*** ! heart beat stopped");
beatHandler.removeCallbacksAndMessages(null);
}
private void AHsendCommand(byte cmd, byte[] payload ) {
AHsendCommand(cmd, payload, payload.length );
}
private void AHsendCommand(byte cmd, byte[] payload, int len ) {
resetHeartBeat();
if ( (cmd == AH100_CMD_USER_INFO) ) {
AHsendEncryptedCommand(cmd, payload, len);
return;
}
byte[] packet ;
byte[] header;
header = new byte[]{(byte) (0xDB),
(byte) (len + 1),
cmd};
packet = hexConcatenate( header, obfuscate(payload) );
try {
writeBytes(SERVICE_AH100_CUSTOM_SERVICE,
SERVICE_AH100_CUSTOM_SEND,
packet);
} catch (Exception e) {
Timber.d("AHsendCommand: CANNOT WRITE COMMAND");
stopHeartBeat();
}
}
private void AHsendEncryptedCommand(byte cmd, byte[] payload , int len ) {
byte[] packet ;
byte[] header;
byte[] encrypted;
Timber.d("AHsendEncryptedCommand: input data: %s", byteInHex(payload));
encrypted = encryptAES(payload, magicKey, initialValue); //encryptAES
header = new byte[]{(byte) (0xDC),
(byte) (len + 0 ),
cmd};
packet = hexConcatenate( header, obfuscate(encrypted) );
try {
writeBytes(SERVICE_AH100_CUSTOM_SERVICE,
SERVICE_AH100_CUSTOM_SEND,
packet);
} catch (Exception e) {
Timber.d("AHsendEncryptedCommand: CANNOT WRITE COMMAND");
stopHeartBeat();
}
}
public byte[] getUserID() {
ScaleUser currentUser = OpenScale.getInstance().getSelectedScaleUser();
byte id = (byte) currentUser.getId();
byte[] auth = new byte[] {0x11, 0x22, 0x33, 0x44, 0x55, 0x00, id};
auth[5] = xorChecksum(auth, 0, auth.length); // set xor of authorise code to 0x00
return auth;
///// return getfakeUserID();
}
public byte[] getfakeUserID() {
String fid = "0f 00 43 06 7b 4e 7f"; // "c7b25de6bed0b7";
byte[] auth = hexToByteArray(fid) ;
return auth;
}
public byte[] encryptAES(byte[] data, byte[] key, byte[] ivs) {
Timber.d("Encoding : input hex data: %s", byteInHex(data));
Timber.d("Encoding : encoding key : %s", byteInHex(key));
Timber.d("Encoding : initial value : %s", byteInHex(ivs));
try {
Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
byte[] finalIvs = new byte[16];
int len = ivs.length > 16 ? 16 : ivs.length;
System.arraycopy(ivs, 0, finalIvs, 0, len);
IvParameterSpec ivps = new IvParameterSpec(finalIvs);
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivps);
return cipher.doFinal(data);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public byte[] decryptAES(byte[] data, byte[] key, byte[] ivs) {
Timber.d("Decoding : input hex data: %s", byteInHex(data));
Timber.d("Decoding : encoding key : %s", byteInHex(key));
Timber.d("Decoding : initial value : %s", byteInHex(ivs));
try {
Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
byte[] finalIvs = new byte[16];
int len = ivs.length > 16 ? 16 : ivs.length;
System.arraycopy(ivs, 0, finalIvs, 0, len);
IvParameterSpec ivps = new IvParameterSpec(finalIvs);
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivps);
byte[] ret = cipher.doFinal(data);
Timber.d("### decryptAES : hex data: %s", byteInHex(ret));
return ret;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public byte[] hexToByteArray(String hexStr) {
String hex = hexStr.replaceAll (" ","").replaceAll (":","");
hex = hex.length()%2 != 0?"0"+hex:hex;
byte[] b = new byte[hex.length() / 2];
for (int i = 0; i < b.length; i++) {
int index = i * 2;
int v = Integer.parseInt(hex.substring(index, index + 2), 16);
b[i] = (byte) v;
}
return b;
}
public byte[] hexConcatenate(byte[] A, byte[] B) {
byte[] C = new byte[A.length + B.length];
ByteArrayOutputStream outputStream = new ByteArrayOutputStream( );
try {
outputStream.write( A );
outputStream.write( B );
C = outputStream.toByteArray( );
} catch (IOException e) {
e.printStackTrace();
}
return C;
}
public byte[] getPayload(byte[] data) {
byte[] obfpayload = Arrays.copyOfRange(data, 3, data.length );
byte[] payload = obfuscate(obfpayload);
Timber.d("Deobfuscated payload: %s", byteInHex(payload));
return payload;
}
private byte[] obfuscate(byte[] rawdata) {
final byte[] data = Arrays.copyOfRange(rawdata, 0, rawdata.length );
final byte[] MAC;
MAC = getScaleMacAddress();
Timber.d("Obfuscation: input hex data: %s", byteInHex(data));
//Timber.d("Obfuscation: MAC hex data: %s", byteInHex(MAC));
byte m = 0 ;
for(int l=0; l< data.length; l++,m++){
if (MAC.length <= m) { m = 0; }
data[l] ^= MAC[m];
}
//Timber.d("Obfuscation: out hex data: %s", byteInHex(data));
return data;
}
}