spin877 / Bruciatore_BLE

This project provides a Python script for interacting with the VEVOR diesel air heater, All-in-One model, via Bluetooth Low Energy (BLE). The code has been developed through Bluetooth data sniffing between the heater and the manufacturer's "AirHeaterBLE" app.
GNU General Public License v2.0
7 stars 0 forks source link

Support custom password code #2

Closed bderleta closed 6 months ago

bderleta commented 7 months ago

I took a peek into the app source code. There is a cryptic state variable called md. For all packets, the first byte is 170 (0xAA). For all packets except when md == 2, second byte is 85 (0x55). If the md != 2, so if second byte is 85, the third constant byte is floor(passkey/100), and fourth is (passkey%100). Which is exactly as in your library - 12 (0x0c) and 34 (0x22). If the md == 2, the second byte is 136 (0x88), and third and fourth bytes are random. I have seen only being it used with command byte == 1 and arg == 0 (see below) and not any other commands.

Fifth byte is a command byte, sixth is (arg % 256), seventh is floor(arg / 256) and eight is the sum of bytes 3 .. 7 (o[2] .. o[6]) .

This is the excerpt responsible (comment and formatting by me):

/** e - command (0-255), t - argument (0-65535), n - "md"-derived value (85 or 136) */
write: function(e, t, n) {
    let i = this,
        o = [];
    o[0] = 170, 
    o[1] = n, 
    136 == n ? (
        Ie[0] = Math.floor(255 * Math.random()), Ie[1] = Math.floor(255 * Math.random()), 
        o[2] = Ie[0],  o[3] = Ie[1]
    ) : (
        o[2] = Math.floor(i.passkey / 100), 
        o[3] = i.passkey % 100
    ), 
    o[4] = e, 
    o[5] = t % 256, 
    o[6] = Math.floor(t / 256), 
    o[7] = o[2] + o[3] + o[4] + o[5] + o[6], 
    y("log", "at ble/ble.js:572", o), 
    Be.write({
        macAddress: i.deviceId,
        uuid_service: "0000ffe0-0000-1000-8000-00805f9b34fb",
        uuid_characteristic_write: "0000ffe1-0000-1000-8000-00805f9b34fb",
        bytes: o,
        hexStr: ""
    }, (e => {
        y("log", "at ble/ble.js:581", e)
    }))
},
bderleta commented 7 months ago

Also, depending on the runningmode (1 == manual mode, 2 == auto mode), for command #4 the argument is just a level (1-10), or temperature (8 - 36 degrees). The checksum you can calculate manually. There are also commands for controlling heater options:

setTime() {
    if (2 == this.$ble.md && this.$ble.isconnecting) {
        let e = new Date;
        this.cmd = 10, this.data = 60 * e.getHours() + e.getMinutes()
    }
}

...

isauto_change: function(e) {
    this.cmd = 13, this.data = e, setTimeout((() => {
        this.cmd = 13, this.data = e
    }), 500), this.isauto = e
},
autotime_change: function(e) {
    this.cmd = 11, this.data = this.time2number(e.detail.value), setTimeout((() => {
        this.cmd = 11, this.data = this.time2number(e.detail.value)
    }), 500), this.autotime = this.time2number(e.detail.value), y("log", "at pages/parameter/parameter.vue:455", e.detail.value), y("log", "at pages/parameter/parameter.vue:456", this.data)
},
runtime_change: function(e) {
    this.cmd = 12, this.data = this.time2number(e.detail.value), setTimeout((() => {
        this.cmd = 12, this.data = this.time2number(e.detail.value)
    }), 500), this.runtime = this.time2number(e.detail.value)
},
tankvolume_change: function(e) {
    this.cmd = 16, this.data = 5 * e.detail.value, setTimeout((() => {
        this.cmd = 16, this.data = 5 * e.detail.value
    }), 500), this.tankvolume = 5 * e.detail.value
},
oilpumptype_change: function(e) {
    this.cmd = 17, this.data = e.detail.value, setTimeout((() => {
        this.cmd = 17, this.data = e.detail.value
    }), 500), this.oilpumptype = e.detail.value
},
automaticheating_change: function(e) {
    this.cmd = 18, this.data = e, setTimeout((() => {
        this.cmd = 18, this.data = e
    }), 800), this.automaticheating = e
},
tempoffset_change: function(e) {
    this.cmd = 15, this.data = e.detail.value, setTimeout((() => {
        this.cmd = 15, this.data = e.detail.value
    }), 800), this.tempoffset = e.detail.value
},
language_change: function(e) {
    this.cmd = 14, this.data = e.detail.value, this.language = e.detail.value
},
number2time(e) {
    if (e > 1439 || e < 0) return "--:--";
    var t = e / 60,
        n = t.toFixed().length,
        i = t.toFixed();
    return 1 == n && (i = "0" + i), i = 1 == (n = (t = e % 60).toFixed().length) ? i + ":0" + t.toFixed() : i + ":" + t.toFixed()
},
time2number(e) {
    var t = e.slice(0, 2),
        n = parseInt(t);
    return t = e.slice(-2), n = 60 * n + parseInt(t)
},
bderleta commented 7 months ago

And this is the parsing code for various types of characteristics change message (it seems there are three different formats possible)

if (e.onnotify = !0, 
    y("log", "at ble/ble.js:700", je = new Uint8Array(t.data)), 
    170 == e.u8tonumber(je[0]) && 85 == e.u8tonumber(je[1])
) 
    e.md = 1, 
    e.lasttime = Date.now(), 
    e.runningstate = e.u8tonumber(je[3]).valueOf(), 
    e.errcode = e.u8tonumber(je[4]).valueOf(), 
    e.runningstep = e.u8tonumber(je[5]).valueOf(), 
    e.altitude = e.u8tonumber(je[6]) + 256 * e.u8tonumber(je[7]), 
    e.runningmode = e.u8tonumber(je[8]).valueOf(), 
    1 == e.runningmode ? e.setlevel = e.u8tonumber(je[9]).valueOf() : 2 == e.runningmode ? (e.settemp = e.u8tonumber(je[9]).valueOf(), 
    e.setlevel = e.u8tonumber(je[10]).valueOf() + 1) : 0 == e.runningmode && (e.setlevel = e.u8tonumber(je[10]).valueOf() + 1), 
    e.supplyvoltage = ((256 * e.u8tonumber(je[12]) + e.u8tonumber(je[11])) / 10).toFixed(1), 
    e.casetemp = e.UnsignToSign(256 * je[14] + je[13]), 
    e.cabtemp = e.UnsignToSign(256 * je[16] + je[15]), 
    clearInterval(0), 
    recved = !0;
else if (170 == e.u8tonumber(je[0]) && 102 == e.u8tonumber(je[1])) 
    e.lasttime = Date.now(), 
    e.runningstate = e.u8tonumber(je[3]).valueOf(), 
    e.errcode = e.u8tonumber(je[17]).valueOf(), 
    e.runningstep = e.u8tonumber(je[5]).valueOf(), 
    e.altitude = e.u8tonumber(je[6]) + 256 * e.u8tonumber(je[7]), 
    e.runningmode = e.u8tonumber(je[8]).valueOf(), 
    1 == e.runningmode ? e.setlevel = e.u8tonumber(je[9]).valueOf() : 2 == e.runningmode ? (e.settemp = e.u8tonumber(je[9]).valueOf(), 
    e.setlevel = e.u8tonumber(je[10]).valueOf() + 1) : 0 == e.runningmode && (e.setlevel = e.u8tonumber(je[10]).valueOf() + 1), 
    e.supplyvoltage = ((256 * e.u8tonumber(je[12]) + e.u8tonumber(je[11])) / 10).toFixed(1), 
    e.casetemp = e.UnsignToSign(256 * je[14] + je[13]), 
    e.cabtemp = e.UnsignToSign(256 * je[16] + je[15]), 
    e.md = 3, 
    clearInterval(0), 
    e.isconnecting = !0, 
    recved = !0;
else if (170 == e.u8tonumber(je[0]) && 136 == e.u8tonumber(je[1])) {
    let t = new Uint8Array(je),
        n = 256 * t[29] + t[30];
    y("log", "at ble/ble.js:765", n), 
    t[29] = Ie[0], 
    t[30] = Ie[1], 
    y("log", "at ble/ble.js:772", Array.prototype.map.call(t, (e => ("00" + e.toString(16)).slice(-2))).join(""));
    let i = Le(t, 31);
    y("log", "at ble/ble.js:774", i), 
    i == n && (
        e.lasttime = Date.now(), 
        e.runningstate = e.u8tonumber(t[3]).valueOf(), 
        e.errcode = e.u8tonumber(t[4]).valueOf(), 
        e.runningstep = e.u8tonumber(t[5]).valueOf(), 
        e.altitude = (e.u8tonumber(t[6]) + 256 * e.u8tonumber(t[7])).toString(), 
        e.runningmode = e.u8tonumber(t[8]).valueOf(), 
        1 == e.runningmode ? e.setlevel = e.u8tonumber(t[9]).valueOf() : 2 == e.runningmode ? (e.settemp = e.u8tonumber(t[9]).valueOf(), 
        e.setlevel = e.u8tonumber(t[10]).valueOf() + 1) : 0 == e.runningmode && (e.setlevel = e.u8tonumber(t[10]).valueOf() + 1), 
        e.supplyvoltage = parseInt((256 * t[12] + t[11]) / 10), 
        e.casetemp = e.UnsignToSign(256 * t[14] + t[13]), 
        e.cabtemp = e.UnsignToSign(256 * t[16] + t[15]), 
        e.autotime = 256 * e.u8tonumber(t[18]) + e.u8tonumber(t[17]), 
        e.runtime = 256 * e.u8tonumber(t[20]) + e.u8tonumber(t[19]), 
        e.isauto = e.u8tonumber(t[21]), 
        e.language = e.u8tonumber(t[22]), 
        e.tempoffset = e.u8tonumber(t[23]), 
        e.tankvolume = e.u8tonumber(t[24]), 
        e.oilpumptype = e.u8tonumber(t[25]), 
        e.bluetoothswitch = !e.u8tonumber(t[26]), 
        e.remotecontrolmatching = !e.u8tonumber(t[27]), 
        e.automaticheating = e.u8tonumber(t[28]), 
        e.md = 2, 
        e.isconnecting = !0, 
        recved = !0, 
        clearInterval(0)
    )
}
y("log", "at ble/ble.js:817", e.md), recved && (this.isconnecting = !0)

...

u8tonumber: function(e) {
    return e < 0 ? e + 256 : e
},
UnsignToSign: function(e) {
    if (e > 32767.5) {
        e |= -65536
    }
    return e
}

...

function Le(e, t) {
    if (t > 0) {
        for (var n = 65535, i = 0; i < t; i++) {
            n ^= e[i];
            for (var o = 0; o < 8; o++) n = 0 != (1 & n) ? n >> 1 ^ 40961 : n >> 1
        }
        return ((65280 & n) >> 8) + 256 * (255 & n)
    }
    return 0
}
spin877 commented 7 months ago

The complexity of your code, which you showed me, surpasses my understanding. Mine was written by ChatGPT, and the commands are simply the dump of Bluetooth data exchanged between the burner and the app.

What those commands specifically do is unknown even to me. As soon as someone more experienced than me writes the code, I'm thinking of removing it. I'm content only with the fact that it served as inspiration for someone else to do better.

bderleta commented 7 months ago

This is not my code, those are fragments of the reverse engineered source of the original application from Android.

was written by ChatGPT

I don't believe... anyway, got it, I will publish all my findings and credit you for inspiration and research. https://github.com/bderleta/vevor-ble-bridge/

spin877 commented 7 months ago

This is not my code, those are fragments of the reverse engineered source of the original application from Android.

was written by ChatGPT

I don't believe... anyway, got it, I will publish all my findings and credit you for inspiration and research. https://github.com/bderleta/vevor-ble-bridge/

I don't speak English either, the entire code, even the README.md, and even this response are being translated for me by ChatGPT. All true.