wangzhibo
4 天以前 ae6f40460dcd56af6c5f60ba52c883854c3bac55
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
/*  Copyright (C) 2021-2024 Daniele Gobbetti, 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 <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.nothing;
 
import android.content.SharedPreferences;
 
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
 
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
import nodomain.freeyourgadget.gadgetbridge.devices.nothing.AbstractEarCoordinator;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
 
import static nodomain.freeyourgadget.gadgetbridge.util.CheckSums.getCRC16ansi;
import static nodomain.freeyourgadget.gadgetbridge.util.GB.hexdump;
 
public class NothingProtocol extends GBDeviceProtocol {
    private static final Logger LOG = LoggerFactory.getLogger(NothingProtocol.class);
 
    final UUID UUID_DEVICE_CTRL = UUID.fromString("aeac4a03-dff5-498f-843a-34487cf133eb");
 
    private boolean isFirstExchange = true;
 
    private static final byte CONTROL_DEVICE_TYPE_TWS_HEADSET = 1;
 
    private static final int CONTROL_CRC = 0x20;
 
    private static final byte MASK_RSP_CODE = 0x1f;
    private static final short MASK_DEVICE_TYPE = 0x0F00;
 
 
    private static final short MASK_REQUEST_CMD = (short) 0x8000;
 
    private static final byte MASK_BATTERY = 0x7f;
    private static final byte MASK_BATTERY_CHARGING = (byte) 0x80;
 
 
    //incoming
    private static final short battery_status = (short) 0xe001;
    private static final short battery_status2 = (short) 0xc007;
    private static final short audio_mode_status = (short) 0xc01e;
 
    private static final short unk_maybe_ack = (short) 0xf002;
    private static final short unk_close_case = (short) 0xe002; //sent twice when the case is closed with earphones in
 
    //outgoing
    private static final short find_device = (short) 0xf002;
    private static final short in_ear_detection = (short) 0xf004;
    private static final short audio_mode = (short) 0xf00f;
 
    private final boolean incrementCounter;
    private int messageCounter = 0x00;
 
    private HashMap<Byte, GBDeviceEventBatteryInfo> batteries;
    private static final byte battery_earphone_left = 0x02;
    private static final byte battery_earphone_right = 0x03;
    private static final byte battery_case = 0x04;
 
    @Override
    public GBDeviceEvent[] decodeResponse(byte[] responseData) {
        List<GBDeviceEvent> devEvts = new ArrayList<>();
 
        if (isFirstExchange) {
            isFirstExchange = false;
            devEvts.add(new GBDeviceEventVersionInfo()); //TODO: this is a weird hack to make the DBHelper happy. Replace with proper firmware detection
        }
 
        ByteBuffer incoming = ByteBuffer.wrap(responseData);
        incoming.order(ByteOrder.LITTLE_ENDIAN);
 
        byte sof = incoming.get();
        if (sof != 0x55) {
            LOG.error("Error in message, wrong start of frame: " + hexdump(responseData));
            return null;
        }
 
        short control = incoming.getShort();
        if (!isSupportedDevice(control)) {
            LOG.error("Unsupported device specified in message: " + hexdump(responseData));
            return null;
        }
        if (!isOk(control)) {
            LOG.error("Message is not ok: " + hexdump(responseData));
            return null;
        }
        short command = incoming.getShort();
        short length = incoming.getShort();
        incoming.get();
 
        byte[] payload = Arrays.copyOfRange(responseData, incoming.position(), incoming.position() + length);
 
 
        switch (getRequestCommand(command)) {
            case battery_status:
            case battery_status2:
                devEvts.addAll(handleBatteryInfo(payload));
                break;
            case audio_mode_status:
                devEvts.add(handleAudioModeStatus(payload));
                break;
 
            case unk_maybe_ack:
                LOG.debug("received ack");
                break;
            case unk_close_case:
                LOG.debug("case closed");
                break;
 
            default:
                LOG.debug("Incoming message - control:" + control + " requestCommand: " + (getRequestCommand(command) & 0xffff) + "length: " + length + " dump: " + hexdump(responseData));
 
        }
        return devEvts.toArray(new GBDeviceEvent[devEvts.size()]);
    }
 
    boolean isCrcNeeded(short control) {
        return (control & CONTROL_CRC) != 0;
    }
 
    byte[] encodeMessage(short control, short command, byte[] payload) {
 
        ByteBuffer msgBuf = ByteBuffer.allocate(8 + payload.length);
        msgBuf.order(ByteOrder.LITTLE_ENDIAN);
        msgBuf.put((byte) 0x55); //sof
        msgBuf.putShort((short) (incrementCounter ? (control | 0x40) : control));
        msgBuf.putShort(command);
        msgBuf.putShort((short) payload.length);
        msgBuf.put((byte) messageCounter); //fsn
        if (incrementCounter) {
            messageCounter++;
            if ((byte) messageCounter == (byte) 0xfd) {
                messageCounter = 0x00;
            }
        }
        msgBuf.put(payload);
 
        if (isCrcNeeded(control)) {
            msgBuf.position(0);
            ByteBuffer crcBuf = ByteBuffer.allocate(msgBuf.capacity() + 2);
            crcBuf.order(ByteOrder.LITTLE_ENDIAN);
            crcBuf.put(msgBuf);
            crcBuf.putShort((short) getCRC16ansi(msgBuf.array()));
            return crcBuf.array();
        }
 
        return msgBuf.array();
    }
 
    byte[] encodeBatteryStatusReq() {
        return encodeMessage((short) 0x5120, battery_status2, new byte[]{});
    }
 
    byte[] encodeAudioModeStatusReq() {
        return encodeMessage((short) 0x120, audio_mode_status, new byte[]{});
    }
 
    //TODO: unify mapping between bytes and strings in the following two functions
    private GBDeviceEvent handleAudioModeStatus(byte[] payload) {
        SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress());
        SharedPreferences.Editor editor = prefs.edit();
 
        if (Arrays.equals(payload, new byte[]{0x01, 0x01, 0x00})) {
            editor.putString(DeviceSettingsPreferenceConst.PREF_NOTHING_EAR1_AUDIOMODE, "anc").apply();
        } else if (Arrays.equals(payload, new byte[]{0x01, 0x03, 0x00})) {
            editor.putString(DeviceSettingsPreferenceConst.PREF_NOTHING_EAR1_AUDIOMODE, "anc-light").apply();
        } else if (Arrays.equals(payload, new byte[]{0x01, 0x05, 0x00})) {
            editor.putString(DeviceSettingsPreferenceConst.PREF_NOTHING_EAR1_AUDIOMODE, "off").apply();
        } else if (Arrays.equals(payload, new byte[]{0x01, 0x07, 0x00})) {
            editor.putString(DeviceSettingsPreferenceConst.PREF_NOTHING_EAR1_AUDIOMODE, "transparency").apply();
        } else {
            LOG.warn("Unknown audio mode. Payload: " + hexdump(payload));
        }
        return null;
    }
 
    byte[] encodeAudioMode(String desired) {
        byte[] payload = new byte[]{0x01, 0x05, 0x00};
 
        switch (desired) {
            case "anc":
                payload[1] = 0x01;
                break;
            case "anc-light":
                payload[1] = 0x03;
                break;
            case "transparency":
                payload[1] = 0x07;
                break;
            case "off":
            default:
        }
        return encodeMessage((short) 0x120, audio_mode, payload);
    }
 
 
    @Override
    public byte[] encodeFindDevice(boolean start) {
        byte payload = (byte) (start ? 0x01 : 0x00);
        return encodeMessage((short) 0x120, find_device, new byte[]{payload});
    }
 
    @Override
    public byte[] encodeSendConfiguration(String config) {
 
        SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress());
 
        switch (config) {
            case DeviceSettingsPreferenceConst.PREF_NOTHING_EAR1_INEAR:
                byte enabled = (byte) (prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_NOTHING_EAR1_INEAR, true) ? 0x01 : 0x00);
                return encodeMessage((short) 0x120, in_ear_detection, new byte[]{0x01, 0x01, enabled});
            // response: 55 20 01 04 70 00 00 00
            case DeviceSettingsPreferenceConst.PREF_NOTHING_EAR1_AUDIOMODE:
                return encodeAudioMode(prefs.getString(DeviceSettingsPreferenceConst.PREF_NOTHING_EAR1_AUDIOMODE, "off"));
            // response: 55 20 01 0F 70 00 00 00
 
            default:
                LOG.debug("CONFIG: " + config);
        }
 
        return super.encodeSendConfiguration(config);
    }
 
    @Override
    public byte[] encodeSetTime() {
        // This are earphones, there is no time to set here. However this method gets called soon
        // after connecting, hence we use it to perform some initializations.
        // TODO: Find a way to send more requests during the first connection
        return encodeAudioModeStatusReq();
    }
 
    private List<GBDeviceEvent> handleBatteryInfo(byte[] payload) {
        List<GBDeviceEvent> batEvts = new ArrayList<>();
 
        //LOG.debug("Battery payload: " + hexdump(payload));
 
        /* payload:
        1st byte is number of batteries, then $number pairs follow:
        {idx, value}
 
        idx is 0x02 for left ear, 0x03 for right ear, 0x04 for case
        value goes from 0-64 (equivalent of 0-100 in hexadecimal)
 
 
        Since Gadgetbridge supports only one battery, we use an average of the levels for the
        battery level.
        If one of the batteries is recharging, we consider the battery as recharging.
         */
 
//        GBDeviceEventBatteryInfo evBattery = new GBDeviceEventBatteryInfo();
//        evBattery.level = 0;
//        boolean batteryCharging = false;
 
        int numBatteries = payload[0];
        for (int i = 0; i < numBatteries; i++) {
 
            batteries.get(payload[1 + 2 * i]).level = (payload[2 + 2 * i] & MASK_BATTERY);
            batteries.get(payload[1 + 2 * i]).state =
                    ((payload[2 + 2 * i] & MASK_BATTERY_CHARGING) == MASK_BATTERY_CHARGING) ? BatteryState.BATTERY_CHARGING : BatteryState.BATTERY_NORMAL;
 
            batEvts.add(batteries.get(payload[1 + 2 * i]));
 
//            evBattery.level += (short) ((payload[2 + 2 * i] & MASK_BATTERY) / numBatteries);
//            if (!batteryCharging) {
//                batteryCharging = ((payload[2 + 2 * i] & MASK_BATTERY_CHARGING) == MASK_BATTERY_CHARGING);
//            }
//            LOG.debug("single battery level: " + hexdump(payload, 2+2*i,1) +"-"+ ((payload[2+2*i] & 0xff))+":" + evBattery.level);
            }
 
//        evBattery.state = BatteryState.UNKNOWN;
//        evBattery.state = batteryCharging ? BatteryState.BATTERY_CHARGING : evBattery.state;
 
//        return evBattery;
        return batEvts;
    }
 
    private short getRequestCommand(short command) {
        return (short) (command | MASK_REQUEST_CMD);
    }
 
    private boolean isOk(short control) {
        return (control & MASK_RSP_CODE) == 0;
    }
 
    private boolean isSupportedDevice(short control) {
        return getDeviceType(control) == CONTROL_DEVICE_TYPE_TWS_HEADSET;
    }
 
    private byte getDeviceType(short control) {
        return (byte) ((control & MASK_DEVICE_TYPE) >> 8);
    }
 
    private AbstractEarCoordinator getCoordinator() {
        return (AbstractEarCoordinator) getDevice().getDeviceCoordinator();
    }
 
    protected NothingProtocol(GBDevice device) {
        super(device);
 
        batteries = new HashMap<>(3);
 
        batteries.put(battery_earphone_left, new GBDeviceEventBatteryInfo());
        batteries.put(battery_earphone_right, new GBDeviceEventBatteryInfo());
        batteries.put(battery_case, new GBDeviceEventBatteryInfo());
 
        batteries.get(battery_case).batteryIndex=0;
        batteries.get(battery_earphone_left).batteryIndex=1;
        batteries.get(battery_earphone_right).batteryIndex=2;
 
        incrementCounter = getCoordinator().incrementCounter();
    }
}