Monitoring Bluetti Systems

chromedshark

New Member
Joined
Apr 3, 2022
Messages
8
TLDR: I'm looking for packet captures to confirm message checksum behavior and to verify behavior for other systems than a single AC300 + B300.

The AC300 (and other larger Bluetti devices) support both bluetooth and wifi for control. Bluetooth control does not require a pin, so anybody nearby could connect and control your device. Wifi control requires internet, and connects to Bluetti's server over an unencrypted connection. I want secure local control of my device(s), which means reversing the internet protocol and running my own compatible server (with eventual monitoring and a web interface for control).

The Bluetti devices speak unencrypted MQTT v3.1.1 to iot.bluettipower.com on port 18760, with some non-standard extra message types used during the connection process. The device publishes to the topic "PUB/AC300/[device_serial_number_here]", and subscribes to a similarly named "SUB/..." topic. The payload appears to be a totally custom protocol revolving around reading from and updating 512 byte "pages" of data. The device regularly publishes updates to certain pages at a variety of offsets, and it can be polled more frequently for any data in any of the pages. The device is controlled by publishing updates to these pages through the "SUB/..." topic. All messages have a 2 byte checksum, and if it's wrong it will be ignored by the device.

I have managed to locate all the information the app displays, as well as figure out how to control everything the app does. There appears to be some detailed battery information that I haven't yet bothered to reverse, and there's a lot of extra fields that I have no clue on their purpose. I have some rough code up on Github, but my eventual goal is to re-write it all in Go so that it's easier to distribute and run. My eventual plan is a single solution MQTT server, metrics exporter (Prometheus), web api, and simple web interface for control and monitoring. The simplest deploy would be a raspberry pi running as an access point to simplify the DNS override, but if you wanted to you could deploy it wherever you wanted on your network. That said I've still got a ways to go to get there.

The one big thing holding me back right now is the message checksum. It's definitely a standard CRC16, but there's some kind of "prefix" that they're using that I haven't been able to figure out. I was able to bypass it for my device using a nice tool someone wrote, but if it's different per device then I'll need to write some code to calculate it before I can release anything. If I could get some packet captures of other people's devices, that would help immensely in figuring this out. WARNING: A full packet capture will include device passwords, which it might be possible to abuse, so I would recommend starting your capture after the device is connected to wifi. Additionally, if you go to the "About Device" section in the app, your device will send the current wifi ESSID it's connected to, as well as your wifi password, so I would stay away from that.
 

gsmithsa

New Member
Joined
Apr 8, 2022
Messages
6
TLDR: I'm looking for packet captures to confirm message checksum behavior and to verify behavior for other systems than a single AC300 + B300.

The AC300 (and other larger Bluetti devices) support both bluetooth and wifi for control. Bluetooth control does not require a pin, so anybody nearby could connect and control your device. Wifi control requires internet, and connects to Bluetti's server over an unencrypted connection. I want secure local control of my device(s), which means reversing the internet protocol and running my own compatible server (with eventual monitoring and a web interface for control).

The Bluetti devices speak unencrypted MQTT v3.1.1 to iot.bluettipower.com on port 18760, with some non-standard extra message types used during the connection process. The device publishes to the topic "PUB/AC300/[device_serial_number_here]", and subscribes to a similarly named "SUB/..." topic. The payload appears to be a totally custom protocol revolving around reading from and updating 512 byte "pages" of data. The device regularly publishes updates to certain pages at a variety of offsets, and it can be polled more frequently for any data in any of the pages. The device is controlled by publishing updates to these pages through the "SUB/..." topic. All messages have a 2 byte checksum, and if it's wrong it will be ignored by the device.

I have managed to locate all the information the app displays, as well as figure out how to control everything the app does. There appears to be some detailed battery information that I haven't yet bothered to reverse, and there's a lot of extra fields that I have no clue on their purpose. I have some rough code up on Github, but my eventual goal is to re-write it all in Go so that it's easier to distribute and run. My eventual plan is a single solution MQTT server, metrics exporter (Prometheus), web api, and simple web interface for control and monitoring. The simplest deploy would be a raspberry pi running as an access point to simplify the DNS override, but if you wanted to you could deploy it wherever you wanted on your network. That said I've still got a ways to go to get there.

The one big thing holding me back right now is the message checksum. It's definitely a standard CRC16, but there's some kind of "prefix" that they're using that I haven't been able to figure out. I was able to bypass it for my device using a nice tool someone wrote, but if it's different per device then I'll need to write some code to calculate it before I can release anything. If I could get some packet captures of other people's devices, that would help immensely in figuring this out. WARNING: A full packet capture will include device passwords, which it might be possible to abuse, so I would recommend starting your capture after the device is connected to wifi. Additionally, if you go to the "About Device" section in the app, your device will send the current wifi ESSID it's connected to, as well as your wifi password, so I would stay away from that.
Hi there, thanks for your work so far !

I'm considering an AC200Max, which only has bluetooth. Any pointers on how to get started with something like a Pi with bluetooth so I could get battery status etc and ideally control ?
 

chromedshark

New Member
Joined
Apr 3, 2022
Messages
8
The general process is to start by getting some form of monitoring that allows you to use the app on your phone and observe what is communicated. Once you have enough data you can start building your own client implementation. I think both iOS and Android have developer tools for doing this, but I’ve not used either of them. I eventually would like to reverse the bluetooth protocol, just to see if it provides more information, but I didn’t want to start there because it can be a pain to program for.

(In theory the faster way to figure it out is to decompile the Android app, but I wasn’t sure on the legality of that, and I didn’t want to have any legal concerns with releasing the result of my reversing.)
 

gsmithsa

New Member
Joined
Apr 8, 2022
Messages
6
I eventually would like to reverse the bluetooth protocol

Hi, not sure if you've had a go at doing this yet

I've captured bluetooth packets between my iOS device and my AC200Max while using the Bluetti app to turn DC and AC outlets on and off

service FF01 is notify
service FF02 is write
service FF03 and FF04 are also advertised, but they weren't involved in my testing

The app sends the following 2 commands repeatedly ? Perhaps to get battery stats etc ?
Code:
 Write Command - FF02 - Value: 0103 000A 0037 241E
Code:
 Write Command - FF02 - Value: 0103 0BB9 003D 57DA

In response to the above commands, I get packets returned on FF01 such as
Code:
0103 6E41 4332 3030 4D00 0000 0000 0003
4002 0100 0000 0000 001D A200 061C 7B00
0100 0000 0000 0000 0000 0000 0000 0000
0000 0000 0000 0000 0000 6400 0000 0000
0100 0000 0000 0000 0000 0000 0000 0000
0000 00C0 0000 0000 0000 0000 0003 E4

which I'm not sure how to decode... could they be encrypted ?


In better news, I did find the commands for DC on/off and AC on/off:

DC on:
Code:
 Write Command - FF02 - Value: 0106 0BC0 0001 4A12

DC off:
Code:
 Write Command - FF02 - Value: 0106 0BC0 0000 8BD2

AC on:
Code:
 Write Command - FF02 - Value: 0106 0BBF 0001 7BCA

AC off:
Code:
 Write Command - FF02 - Value: 0106 0BBF 0000 BA0A


If you had any pointers on how to extract the received info from the Bluetti, that would be great

Thanks !
 

chromedshark

New Member
Joined
Apr 3, 2022
Messages
8
Oh wow, nice work! It looks like it works almost exactly like what I'm seeing over MQTT, so that's great news!

Code:
Write Command - FF02 - Value: 0103 000A 0037 241E
  1. The MQTT packets start with "0101", but the bluetooth packets appear to start with just "01". This just appears to be a fixed prefix to each message.
  2. The next byte, 0x03, tells you what kind of command we're looking at. 0x03 is the "range read" command (0x06 and 0x10 are other types with different behavior).
  3. The next byte, 0x00, tells you what "page" we're reading from. Page 0x00 includes all the status information about the power station (power input/output, software version, battery state, inverter mode, etc.). Page 0x0B is where all the configurable things are (output on/off, schedule, the current time, etc.). Page 0x13 on the AC300 has wifi information, but presumably isn't used on the AC200Max.
  4. The next byte, 0x0A, tells you what "offset" we're reading from. Each page seems to consist of 512 bytes, and this protocol works in 2 byte offsets (and sizes). An offset of 0x0A means that it starts at 2 * 10 bytes into the page and then reads from there.
  5. The next two bytes, 0x0037, are used as a 16-bit unsigned integer for the length. Just like the offset, this needs to be multiplied by 2 to get the actual size in bytes it's requesting (110 bytes in this case).
  6. The final two bytes, 0x241E, are the checksum. I can calculate this checksum with the code I already have, which is a good sign that I should be able to use it for any devices they have. When I add "01" to the beginning and checksum everything except the last two bytes, I also get 0x241E (which suggests that I'm probably running the checksum on the wrong stuff if changing the payload does not change the resulting checksum).
Code:
Notify Command - FF01 - Value: 0103 6E41 4332 3030 4D00 0000 0000 0003
  1. We can skip the first byte, 0x01, since that's in every message.
  2. The next byte, 0x03, tells us this is a response to a "range read" command.
  3. The next byte, 0x6E, is the length in bytes of the data we requested, and is twice the length we used above (2 * 55 == 110 bytes).
  4. There should be 112 bytes remaining in the data - 2 extra for the checksum - but you seem to be missing some bytes in what you posted. That said, we can decode at least part of it - the first 12 bytes are used for the device type (string, null-terminated, fixed length). If we decode that we get "AC200M".
If I make this request to my AC300, here's what I get in response:
Code:
Request: 0101 0300 0a00 3724 1e

Response:
0       01 01 03 6e 41 43 33 30 30 00 00 00 00 00 00 00 
8       03 fa db 3b 06 5c 01 f2 00 00 00 00 00 00 27 09 
10      00 06 26 a4 00 06 8c 27 00 01 c1 22 00 0d 00 00 
18      00 00 00 00 00 00 00 00 00 57 00 00 00 00 00 00 
20      00 00 00 68 00 00 00 26 00 01 00 01 00 01 00 01 
28      00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00 
30      00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
38      00 00 f5 93

I'm currently in the process of re-architecting the code on the evented-architecture branch, but both branches include parsing code (data_pages.rb and payload_parser.rb are just different forms of the same underlying rules).

Code:
Write Command - FF02 - Value: 0106 0BC0 0001 4A12
  1. Skip the first byte.
  2. The next byte, 0x06, tells you that we're doing a single field update.
  3. The next byte, 0x0B, tells you that we're updating a single field on the 0x0B page, the config page as I've been calling it.
  4. The next byte, 0xC0, tells you what field offset we're updating. 0xC0 is the DC output field, 0xBF is the AC output field, 0xF5 might be the screen sleep time for you (it is on the AC300).
  5. The next two bytes are the value to set it to, 0x0000 or 0x0001.
  6. The final two bytes are the checksum.
The third message type, 0x10, should allow you to set an entire range (for example to turn off both the AC output and DC output at the same time). You'll have to test this, though, to see if this works over bluetooth.

Code:
0110 0BBF 0002 0400 0000 00CB AB
  1. Skip the first byte.
  2. The next byte, 0x10, tells you that we're doing a "range update".
  3. The next byte, 0x0B, is the page.
  4. The next byte, 0xBF, is the offset.
  5. The next two bytes, 0x0002, are used as a 16-bit unsigned integer for the length.
  6. The next byte, 0x04, is the actual byte length of the payload. Don't ask me why they have the length twice in their protocol.
  7. The next 4 bytes are the payload - two 16-bit unsigned integers for the 0xBF and 0xC0 fields.
  8. The last two bytes are the checksum.
If you can make your own requests, I'd be interested to see what they return for the full contents of page 0x00 and 0x0B. Below are some pre-checksummed requests that you might be able to use to do that. Also, if you have any further questions about this I'm happy to help.

Code:
Read data from the pages with offsets and sizes I expect to work
Write Command - FF02 - VALUE: 0103 0000 0046 c438
Write Command - FF02 - VALUE: 0103 0046 007f e5ff
Write Command - FF02 - VALUE: 0103 0bb9 003d 57da
 

chromedshark

New Member
Joined
Apr 3, 2022
Messages
8
:ROFLMAO: I just re-ran the checksum reverse engineering script and it says the Bluetooth messages are using standard CRC-16/MODBUS. It looks like the extra 0x01 byte at the beginning of the MQTT packet is ignored for the checksum, but it was throwing me off completely. Well that makes things a lot easier.
 

gsmithsa

New Member
Joined
Apr 8, 2022
Messages
6
@chromedshark this is fantastic !

Here are the responses from the pre-checksummed requests you posted:

Code:
ATT Send           Write Request FF02 - Value: 0103 0000 0046 C438  
ATT Receive        Write Response  
ATT Receive        FF01 - Value: 0103 8C00 0000 0000 0000 0000 0000 0000…  
ATT Receive        FF01 - Value: 0000 0041 4332 3030 4D00 0000 0000 0003…  
ATT Receive        FF01 - Value: 4002 0100 0000 0000 001D A200 061C 7B00…  
ATT Receive        FF01 - Value: 0100 0000 0000 0000 0000 0000 0000 0000…  
ATT Receive        FF01 - Value: 0000 0000 0000 0000 0000 6400 0000 0000…  
ATT Receive        FF01 - Value: 0000 0000 0000 0000 0000 0000 0000 0000…  
ATT Receive        FF01 - Value: 0000 00C0 0000 0000 0000 0000 0000 0000…  
ATT Receive        FF01 - Value: 0000 0085 0C

ATT Send           Write Request FF02 - Value: 0103 0046 007F E5FF  
ATT Receive        Write Response  
ATT Receive        FF01 - Value: 0103 FE00 0000 0000 0000 0000 0000 0000…  
ATT Receive        FF01 - Value: 0000 0000 0000 0000 0000 0000 0000 0000…  
ATT Receive        FF01 - Value: 0000 0000 0000 0315 0300 0200 6400 2800…  
ATT Receive        FF01 - Value: 0300 6400 0000 6400 0000 0002 5801 4D01…  
ATT Receive        FF01 - Value: 5101 4E01 5401 5201 5201 4F01 4E01 4E01…  
ATT Receive        FF01 - Value: 5101 5101 4E00 3C00 3B00 3B00 0000 0000…  
ATT Receive        FF01 - Value: 0000 0000 0000 0000 0000 0000 0000 0000…  
ATT Receive        FF01 - Value: 0000 0000 0000 0000 0000 0000 0000 0000…  
ATT Receive        FF01 - Value: 0000 0000 0000 0000 0000 0000 0000 0000…  
ATT Receive        FF01 - Value: 0000 0000 0000 0000 0000 0000 0000 0000…  
ATT Receive        FF01 - Value: 0000 0000 0000 0000 0000 0000 0000 0000…  
ATT Receive        FF01 - Value: 0000 0000 0000 0000 0000 0000 0000 0000…  
ATT Receive        FF01 - Value: 0000 0000 0000 0000 0000 0000 0000 0000…
 
ATT Send           Write Request FF02 - Value: 0103 0BB9 003D 57DA  
ATT Receive        Write Response
ATT Receive        FF01 - Value: 0103 7A00 0000 0000 0000 0000 0000 0100…  
ATT Receive        FF01 - Value: 0000 0000 0000 0000 0000 0000 0000 0000…  
ATT Receive        FF01 - Value: 0000 0000 0000 0000 0000 0000 0000 0000…  
ATT Receive        FF01 - Value: 0000 0016 0509 0815 0300 0000 0000 0000…  
ATT Receive        FF01 - Value: 0000 0000 0000 0000 0000 0000 0000 0000…  
ATT Receive        FF01 - Value: 0000 0000 0000 0000 0000 0000 0000 0000…  
ATT Receive        FF01 - Value: 0000 0000 02B7 0B


Does that provide any useful info ?

Let me know if you want any other packet dumps as well
 

chromedshark

New Member
Joined
Apr 3, 2022
Messages
8
Here are the responses from the pre-checksummed requests you posted:
It looks like you’re still missing data. The “AC200M” should show up at offset 10, or 20 bytes in, and does in your first post. In what you just posted, however, it’s somehow at offset 8 (16 bytes in), so we’re somehow missing 4 bytes. There’s “…” at the end of every line. Is it possible that whatever tool you’re using is truncating each line by 4 bytes? Based on looking around online, that matches up with the Bluetooth protocol supporting 20 bytes of data per packet (each line has only 16 bytes).
 

chromedshark

New Member
Joined
Apr 3, 2022
Messages
8
If I assume each line that ends with an ellipsis is missing 4 bytes at the end, it does appear to match up with the existing parser research I've done. If you were to plug it into the wall and turn on the AC / DC output and hook up some loads to it, then we could use that to confirm that all the numbers are indeed showing up in the right spots.

The pack voltages look odd because they have some missing data in them that I had to fill with zeros, but it's cool to see that the battery information section seems similar between units.

Code:
{"device_type"=>"AC200M", "dc_input_power"=>0, "ac_input_power"=>0, "ac_output_power"=>0, "dc_output_power"=>0, "total_battery_percent"=>100, "ac_output_on"=>false, "dc_output_on"=>false}
{"inverter_mode"=>"Stop", "inverter_voltage"=>0.0, "inverter_frequency"=>0.0, "ac_output_current_probable"=>0.0, "ac_input_voltage"=>0.0, "ac_dc_inverter_power"=>0, "ac_input_frequency"=>0.0, "dc_input_voltage_possible"=>0.0, "dc_input_power_possible"=>0, "dc_input_current_possible"=>0.0, "pack_voltage_probable"=>0.5379e3, "pack_battery_percent"=>100, "packs"=>[{"pack_num"=>0, "voltages"=>[3.33, 2.56, 0.0, 0.81, 3.34, 3.4, 3.38, 3.38, 3.35, 3.34, 3.34, 2.56, 0.0, 0.81, 3.37, 3.34]}]}
{"ups_mode"=>"Unavailable", "ac_output_on"=>false, "dc_output_on"=>false, "grid_charge_on"=>false, "time_control_on"=>false, "battery_range_start"=>0, "battery_range_end"=>0, "device_time"=>"05-09-22 08:21:03", "time_one_mode"=>"Unavailable", "time_one_start"=>"00:00", "time_one_end"=>"00:00", "time_two_mode"=>"Unavailable", "time_two_start"=>"00:00", "time_two_end"=>"00:00", "time_three_mode"=>"Unavailable", "time_three_start"=>"00:00", "time_three_end"=>"00:00", "time_four_mode"=>"Unavailable", "time_four_start"=>"00:00", "time_four_end"=>"00:00", "time_five_mode"=>"Unavailable", "time_five_start"=>"00:00", "time_five_end"=>"00:00", "time_six_mode"=>"Unavailable", "time_six_start"=>"00:00", "time_six_end"=>"00:00", "auto_sleep"=>"30s"}
 

gsmithsa

New Member
Joined
Apr 8, 2022
Messages
6
It looks like you’re still missing data. The “AC200M” should show up at offset 10, or 20 bytes in, and does in your first post. In what you just posted, however, it’s somehow at offset 8 (16 bytes in), so we’re somehow missing 4 bytes. There’s “…” at the end of every line. Is it possible that whatever tool you’re using is truncating each line by 4 bytes? Based on looking around online, that matches up with the Bluetooth protocol supporting 20 bytes of data per packet (each line has only 16 bytes).
Good point with the “…”
I’m using Apple Packet Logger; I’ll dig around and see if there’s some way of stopping it from truncating
 

gsmithsa

New Member
Joined
Apr 8, 2022
Messages
6
If you were to plug it into the wall and turn on the AC / DC output and hook up some loads to it, then we could use that
My first packet log I actually did just that

Can you PM me an email address and I’ll send you the whole thing ?
 

gsmithsa

New Member
Joined
Apr 8, 2022
Messages
6
Ok here are the hopefully full length responses

Code:
ATT Send           Write Request FF02 - Value: 0103 0000 0046 C438 
ATT Receive        Write Response 
ATT Receive        FF01 - Value: 01038c0000000000000000000000000000000000
ATT Receive        FF01 - Value: 00000041433230304d00000000000003f9b4d964 
ATT Receive        FF01 - Value: 4002010000000000001da200061c7b00068a8f00 
ATT Receive        FF01 - Value: 0100000000000000000000000000000000000000
ATT Receive        FF01 - Value: 0000000000000000000064000000000000000000 
ATT Receive        FF01 - Value: 0000000000000000000000000000000000000000 
ATT Receive        FF01 - Value: 000000c000000000000000000000000000000000 
ATT Receive        FF01 - Value: 000000850c

ATT Send           Write Request FF02 - Value: 0103 0046 007F E5FF 
ATT Receive        Write Response 
ATT Receive        FF01 - Value: 0103fe0000000000000000000000000000000000
ATT Receive        FF01 - Value: 0000000000000000000000000000000000000000 
ATT Receive        FF01 - Value: 0000000000000315030002006400280001000115
ATT Receive        FF01 - Value: 03006400000064000000000258014d0152015001 
ATT Receive        FF01 - Value: 51014e015401520152014f014e014e014d015001
ATT Receive        FF01 - Value: 510151014e003c003b003b000000000000000000 
ATT Receive        FF01 - Value: 0000000000000000000000000000000000000000
ATT Receive        FF01 - Value: 0000000000000000000000000000000000000000
ATT Receive        FF01 - Value: 0000000000000000000000000000000000000000
ATT Receive        FF01 - Value: 0000000000000000000000000000000000000000
ATT Receive        FF01 - Value: 0000000000000000000000000000000000000000
ATT Receive        FF01 - Value: 0000000000000000000000000000000000000000
ATT Receive        FF01 - Value: 0000000000000000000000000000000000dcbf
 
ATT Send           Write Request FF02 - Value: 0103 0BB9 003D 57DA 
ATT Receive        Write Response
ATT Receive        FF01 - Value: 01037a0000000000000000000000010000000000 
ATT Receive        FF01 - Value: 0000000000000000000000000000000000000000 
ATT Receive        FF01 - Value: 0000000000000000000000000000000000000000
ATT Receive        FF01 - Value: 0000001605090815030000000000000000000000
ATT Receive        FF01 - Value: 0000000000000000000000000000000000000000
ATT Receive        FF01 - Value: 0000000000000000000000000000000000000000
ATT Receive        FF01 - Value: 0000000002b70b
 

chromedshark

New Member
Joined
Apr 3, 2022
Messages
8
Quick update. I've finally put together a script that logs (raw) data by polling the device over Bluetooth. I have a single AC300 with a B300, so I'm looking for data from other devices and configurations. DM me and I can send you an email address to send logs to if you'd like to help out.

Next step - a solution for integrating a Bluetti power station into Home Assistant using MQTT.
 
Top