├── imgs ├── benchtest1.jpg ├── benchtest2.jpg ├── benchtest3.jpg ├── benchtest4.jpg ├── benchtest5.jpg ├── m365tools1.png ├── m365tools2.png └── m365_bms_monitor_schematics.png ├── src ├── m365_bms_activator.ino ├── m365_esp32BLE_emulator.ino └── m365_esp32BLE_bmsmonitor.ino ├── .gitignore ├── LICENSE └── README.md /imgs/benchtest1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexocn/m365_bms_monitor/HEAD/imgs/benchtest1.jpg -------------------------------------------------------------------------------- /imgs/benchtest2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexocn/m365_bms_monitor/HEAD/imgs/benchtest2.jpg -------------------------------------------------------------------------------- /imgs/benchtest3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexocn/m365_bms_monitor/HEAD/imgs/benchtest3.jpg -------------------------------------------------------------------------------- /imgs/benchtest4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexocn/m365_bms_monitor/HEAD/imgs/benchtest4.jpg -------------------------------------------------------------------------------- /imgs/benchtest5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexocn/m365_bms_monitor/HEAD/imgs/benchtest5.jpg -------------------------------------------------------------------------------- /imgs/m365tools1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexocn/m365_bms_monitor/HEAD/imgs/m365tools1.png -------------------------------------------------------------------------------- /imgs/m365tools2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexocn/m365_bms_monitor/HEAD/imgs/m365tools2.png -------------------------------------------------------------------------------- /imgs/m365_bms_monitor_schematics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexocn/m365_bms_monitor/HEAD/imgs/m365_bms_monitor_schematics.png -------------------------------------------------------------------------------- /src/m365_bms_activator.ino: -------------------------------------------------------------------------------- 1 | //for Xiaomi M365 BMS activation 2 | 3 | byte buf_30[]= {0x55, 0xAA, 0x03, 0x22, 0x01, 0x30, 0x0C, 0x9D, 0xFF}; 4 | void setup() { 5 | Serial.begin(115200); 6 | delay(100); 7 | } 8 | void loop() { 9 | Serial.write(buf_30,9); 10 | delay(10); 11 | while(Serial.available() > 0) Serial.read(); 12 | delay(200); 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files 20 | *.mod 21 | *.smod 22 | 23 | # Compiled Static libraries 24 | *.lai 25 | *.la 26 | *.a 27 | *.lib 28 | 29 | # Executables 30 | *.exe 31 | *.out 32 | *.app 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 alexocn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # m365_bms_monitor 2 | 3 | Xiaomi M365 BMS Monitor based on ESP32 with BLE support. 4 | 5 | ## Overview 6 | 7 | This project is designed for the use of M365 compatible batteries either in M365 clones without BMS monitoring support or just a standalone power bank. There have been reports that without data communication from ESC battery power gets cut off under heavy load(7-10A) which I didn't test myself since I only tested it on the bench and didn't have the appropriate high power load. 8 | 9 | `src/m365_bms_activator.ino` - simple code for activation of the battery only(ignores output from the battery). Work with any kind of Arduino compatible device(UNO, Nano, ESP8266/ESP32). 10 | 11 | `src/m365_esp32BLE_emulator.ino` - BLE emulator using ESP32 to test phone app. It does not support BMS connection and has messages hardcoded. 12 | 13 | `src/m365_esp32BLE_bmsmonitor.ino` - full BLE emulator using ESP32. Not only it activates the battery but also allows connection M365Tools android app to monitor it's health including individual cell voltages. 14 | 15 | **I have tested it with BMS 1.1.5 version which uses 55AA protocol. Newer BMS might use newer 5AA5 protocol so the code will have to be slightly modified. I am too lazy to upgrade and therefore have no way of testing newer BMS.** 16 | 17 | ## Connection 18 | 19 | Data cable from the battery has 3 pins: 20 | - White: RX from point of view ESC/BLE(TX for BMS) 21 | - Blue: GND 22 | - Yellow: TX from point of view ESC/BLE(RX for BMS) 23 | 24 | The connector is regular JST PH2.0 which requires to cut space for the notch(see images below). 25 | 26 | For activator you only need to connect TX from Arduion/ESP to the corresponding connection on the data pigtail. Obviously you also need to have a common ground. 27 | 28 | For full ESP32 fledge monitor you also need to connect RX coming from BMS and have 10K pullup resistor(for some boards it doesn't work without it). I use TX2 here to transmit commands to BMS and reserved regular TX for debuging via serial. But you could just use TX/RX if you don't want debugging. 29 | ESP32 here uses Bluetooth LE to pass the response from BMS to M365Tools monitoring app. It works with old and new M365Tools versions since it converts 55AA to 5AA5 protocol if needed. 30 | 31 | Schematics: P1 header is only for flashing and debuging, I need it since I am using barebone ESP32 module. VCC is regulated 3.3V to power ESP32. 32 | ![Schematics](imgs/m365_bms_monitor_schematics.png) 33 | 34 | ## Reference information 35 | 36 | Thanks to the following projects which helped me to understand the protocol:
37 | 38 | https://github.com/CamiAlfa/M365-BLE-PROTOCOL 39 | 40 | https://github.com/etransport/ninebot-docs/wiki/protocol 41 | 42 | https://github.com/camcamfresh/Xiaomi-M365-BLE-Controller-Replacement 43 | 44 | 45 | ## Bench TEST 46 | 47 | If you still have questions hopefully images will clear those out. 48 | 49 | M365Tools:
50 | 51 | ![Screenshot1](imgs/m365tools1.png) 52 | ![Screenshot2](imgs/m365tools2.png) 53 | 54 | Bench test:
55 | 56 | ![benchtest1](imgs/benchtest1.jpg) 57 | ![benchtest1](imgs/benchtest2.jpg) 58 | ![benchtest1](imgs/benchtest3.jpg) 59 | ![benchtest1](imgs/benchtest4.jpg) 60 | ![benchtest1](imgs/benchtest5.jpg) 61 | -------------------------------------------------------------------------------- /src/m365_esp32BLE_emulator.ino: -------------------------------------------------------------------------------- 1 | /* 2 | based on example ESP32/BLE BLE_UART, use "M655Tools" or M365Battery(cells don't work now, only receives 20bytes) 3 | works with BMS version 1.1.5 which uses 55AA protocol 4 | works with M365Tools v1.2.8, v1.3.0 using 55AA protocol 5 | works with M365Tools V1.3.4 using 5AA5 protocol(uncomment define NBOTFORMAT) 6 | I used it for testing BLE android apps to determine which protocols they use and spoofed BMS messages(no actual connection to BMS) 7 | */ 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | 14 | BLEServer *pServer = NULL; 15 | BLECharacteristic * pTxCharacteristic; 16 | bool deviceConnected = false; 17 | bool oldDeviceConnected = false; 18 | 19 | uint8_t txTypeIdx = 0; 20 | 21 | //Use New format (M365 Tools v1.3.2 or later) 22 | //define NBOTFORMAT 23 | 24 | #ifndef NBOTFORMAT 25 | byte xmbuf_1012[]= {0x55, 0xAA, 0x14, 0x25, 0x01, 0x10, 0x33, 0x4A, 0x43, 0x47, 0x4A, 0x31, 0x38, 0x41, 0x54, 0x44, 0x31, 0x31, 0x32, 0x35, 0x15, 0x01, 0x78, 0x1E, 0xAD, 0xFB}; //26 bytes 26 | byte xmbuf_1b04[]= {0x55, 0xAA, 0x06, 0x25, 0x01, 0x1B, 0x16, 0x00, 0x2B, 0x00, 0x77, 0xFF}; //12 bytes 27 | byte xmbuf_2006[]= {0x55, 0xAA, 0x08, 0x25, 0x01, 0x20, 0x3B, 0x24, 0x00, 0x00, 0x00, 0x00, 0x52, 0xFF}; //14 bytes 28 | byte xmbuf_310a[]= {0x55, 0xAA, 0x0C, 0x25, 0x01, 0x31, 0x31, 0x1A, 0x55, 0x00, 0x00, 0x00, 0x78, 0x0F, 0x2F, 0x2F, 0x17, 0xFE}; //18 bytes 29 | byte xmbuf_401e[]= {0x55, 0xAA, 0x20, 0x25, 0x01, 0x40, 0x6B, 0x0F, 0x6A, 0x0F, 0x6F, 0x0F, 0x6A, 0x0F, 0x6E, 0x0F, 0x84, 0x0F, 0x84, 0x0F, 0x87, 0x0F, 0x86, 0x0F, 0x86, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2C, 0xFA}; //38 bytes 30 | #else 31 | byte nbbuf_1012[]= {0x5A, 0xA5, 0x12, 0x22, 0x3E, 0x01, 0x10, 0x33, 0x4A, 0x43, 0x47, 0x4A, 0x31, 0x38, 0x41, 0x54, 0x44, 0x31, 0x31, 0x32, 0x35, 0x15, 0x01, 0x78, 0x1E, 0x74, 0xFB}; //27 bytes 32 | byte nbbuf_1b04[]= {0x5A, 0xA5, 0x04, 0x22, 0x3E, 0x01, 0x1B, 0x16, 0x00, 0x2B, 0x00, 0x3E, 0xFF}; //13 bytes 33 | byte nbbuf_2006[]= {0x5A, 0xA5, 0x06, 0x22, 0x3E, 0x01, 0x20, 0x3B, 0x24, 0x19, 0x00, 0x00, 0x00, 0x00, 0xFF}; //15 bytes 34 | byte nbbuf_310a[]= {0x5A, 0xA5, 0x0A, 0x22, 0x3E, 0x01, 0x31, 0x31, 0x1A, 0x55, 0x00, 0x00, 0x00, 0x78, 0x0F, 0x2F, 0x2F, 0xDE, 0xFD}; //19 bytes 35 | byte nbbuf_401e[]= {0x5A, 0xA5, 0x1E, 0x22, 0x3E, 0x01, 0x40, 0x6B, 0x0F, 0x6A, 0x0F, 0x6F, 0x0F, 0x6A, 0x0F, 0x6E, 0x0F, 0x84, 0x0F, 0x84, 0x0F, 0x87, 0x0F, 0x86, 0x0F, 0x86, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF3, 0xF9}; //39 bytes 36 | #endif 37 | 38 | // UART service UUID, has to be those specific for M365 protocol 39 | #define SERVICE_UUID "6E400001-B5A3-F393-E0A9-E50E24DCCA9E" 40 | #define CHARACTERISTIC_UUID_RX "6E400002-B5A3-F393-E0A9-E50E24DCCA9E" 41 | #define CHARACTERISTIC_UUID_TX "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" 42 | 43 | 44 | class MyServerCallbacks: public BLEServerCallbacks { 45 | void onConnect(BLEServer* pServer) { 46 | deviceConnected = true; 47 | }; 48 | 49 | void onDisconnect(BLEServer* pServer) { 50 | deviceConnected = false; 51 | } 52 | }; 53 | 54 | class MyCallbacks: public BLECharacteristicCallbacks { 55 | void onWrite(BLECharacteristic *pCharacteristic) { 56 | std::string rxValue = pCharacteristic->getValue(); 57 | 58 | if (rxValue.length() > 0) { 59 | 60 | Serial.print("RX:"); 61 | for (int i = 0; i < rxValue.length(); i++){ 62 | if(rxValue[i]<16) Serial.print('0'); 63 | Serial.print(rxValue[i], HEX); 64 | } 65 | Serial.println(); 66 | 67 | #ifndef NBOTFORMAT 68 | //I hijacked some request which are not 1:1, but it seems it works, M365Tools still process responses no matter what it requested 69 | if(rxValue[5]==0x1A) { 70 | pTxCharacteristic->setValue(xmbuf_1012, 20); //26 71 | pTxCharacteristic->notify(); 72 | for(uint16_t idx=0; idx<1000; ++idx) ++txTypeIdx; //delay 73 | pTxCharacteristic->setValue(xmbuf_1012+20, 6); 74 | pTxCharacteristic->notify(); 75 | } 76 | if(rxValue[5]==0x67) {pTxCharacteristic->setValue(xmbuf_1b04, 12); pTxCharacteristic->notify(); } 77 | if(rxValue[5]==0x72) {pTxCharacteristic->setValue(xmbuf_2006, 14); pTxCharacteristic->notify(); } 78 | if(rxValue[5]==0x30) {pTxCharacteristic->setValue(xmbuf_310a, 18); pTxCharacteristic->notify(); } 79 | if(rxValue[5]==0x40) { 80 | pTxCharacteristic->setValue(xmbuf_401e, 20); //38 81 | pTxCharacteristic->notify(); 82 | for(uint16_t idx=0; idx<1000; ++idx) ++txTypeIdx; //delay 83 | pTxCharacteristic->setValue(xmbuf_401e+20, 18); 84 | pTxCharacteristic->notify(); 85 | } 86 | 87 | 88 | //m365battery app 89 | if(rxValue[5]==0x10&&rxValue[6]==0x12) {pTxCharacteristic->setValue(xmbuf_1012, 26); pTxCharacteristic->notify(); } 90 | if(rxValue[5]==0x1B&&rxValue[6]==0x04) {pTxCharacteristic->setValue(xmbuf_1b04, 12); pTxCharacteristic->notify(); } 91 | if(rxValue[5]==0x20&&rxValue[6]==0x02) {pTxCharacteristic->setValue(xmbuf_2006, 14); pTxCharacteristic->notify(); } 92 | if(rxValue[5]==0x31&&rxValue[6]==0x0A) {pTxCharacteristic->setValue(xmbuf_310a, 18); pTxCharacteristic->notify(); } 93 | //if(rxValue[5]==0x40&&rxValue[6]==0x1E) {pTxCharacteristic->setValue(xmbuf_401e, 38); pTxCharacteristic->notify(); Serial.println("\tCELLS"); } 94 | if(rxValue[5]==0x40&&rxValue[6]==0x1E) { 95 | //pTxCharacteristic->setValue(xmbuf_401e, 38); 96 | pTxCharacteristic->setValue(xmbuf_401e, 20); //38 97 | pTxCharacteristic->notify(); 98 | for(uint16_t idx=0; idx<1000; ++idx) ++txTypeIdx; //delay 99 | pTxCharacteristic->setValue(xmbuf_401e+20, 18); 100 | pTxCharacteristic->notify(); 101 | Serial.println("XM_CELLS"); 102 | } 103 | #else 104 | //I hijacked some request which are not 1:1, but it seems it works, M365Tools still process responses no matter what it requested 105 | if(rxValue[6]==0x10) { 106 | pTxCharacteristic->setValue(nbbuf_1012, 20); //27 107 | pTxCharacteristic->notify(); 108 | for(uint16_t idx=0; idx<1000; ++idx) ++txTypeIdx; //delay 109 | pTxCharacteristic->setValue(nbbuf_1012+20, 7); 110 | pTxCharacteristic->notify(); Serial.println("\tNB_Serial"); 111 | } 112 | if(rxValue[6]==0x1A) {pTxCharacteristic->setValue(nbbuf_1b04, 13); pTxCharacteristic->notify(); Serial.println("\tNB_CYCLES"); } 113 | if(rxValue[6]==0x72) {pTxCharacteristic->setValue(nbbuf_2006, 15); pTxCharacteristic->notify(); Serial.println("\tNB_ManDate"); } 114 | if(rxValue[6]==0x30) {pTxCharacteristic->setValue(nbbuf_310a, 19); pTxCharacteristic->notify(); Serial.println("\tNB_BATTMAIN"); } 115 | if(rxValue[6]==0x40) { 116 | pTxCharacteristic->setValue(nbbuf_401e, 20); //39 117 | pTxCharacteristic->notify(); 118 | for(uint16_t idx=0; idx<1000; ++idx) ++txTypeIdx; //delay 119 | pTxCharacteristic->setValue(nbbuf_401e+20, 19); 120 | pTxCharacteristic->notify(); Serial.println("\tNB_CELLS"); 121 | } 122 | #endif 123 | 124 | 125 | } 126 | } 127 | }; 128 | 129 | 130 | void setup() { 131 | Serial.begin(115200); 132 | 133 | // Create the BLE Device 134 | //BLEDevice::setMTU(64); //doesn't work without doing the same on the client side 135 | BLEDevice::init("MiScooter6560"); //"Mi Electric Scooter", "Xiaomi M365", "MiScooter6560" 136 | 137 | // Create the BLE Server 138 | pServer = BLEDevice::createServer(); 139 | pServer->setCallbacks(new MyServerCallbacks()); 140 | 141 | // Create the BLE Service 142 | BLEService *pService = pServer->createService(SERVICE_UUID); 143 | 144 | // Create a BLE Characteristic 145 | pTxCharacteristic = pService->createCharacteristic(CHARACTERISTIC_UUID_TX, BLECharacteristic::PROPERTY_NOTIFY); //CALE, consider READ property 'BLECharacteristic::PROPERTY_READ |' 146 | pTxCharacteristic->addDescriptor(new BLE2902()); 147 | 148 | BLECharacteristic * pRxCharacteristic = pService->createCharacteristic(CHARACTERISTIC_UUID_RX, BLECharacteristic::PROPERTY_WRITE); 149 | pRxCharacteristic->setCallbacks(new MyCallbacks()); 150 | 151 | // Start the service 152 | pService->start(); 153 | 154 | // Start advertising 155 | pServer->getAdvertising()->start(); 156 | Serial.println("Waiting a client connection to notify..."); 157 | } 158 | 159 | void loop() { 160 | 161 | if (deviceConnected) { 162 | //pTxCharacteristic->setValue(xmbuf_1012, 20); 163 | //for(uint16_t idx=0; idx<1000; ++idx) ++txTypeIdx; //delay 164 | //pTxCharacteristic->setValue(xmbuf_1012+20, 6); 165 | //pTxCharacteristic->notify(); 166 | 167 | txTypeIdx++; 168 | delay(200); // bluetooth stack will go into congestion, if too many packets are sent 169 | } 170 | 171 | // disconnecting 172 | if (!deviceConnected && oldDeviceConnected) { 173 | delay(500); // give the bluetooth stack the chance to get things ready 174 | pServer->startAdvertising(); // restart advertising 175 | Serial.println("start advertising"); 176 | oldDeviceConnected = deviceConnected; 177 | } 178 | // connecting 179 | if (deviceConnected && !oldDeviceConnected) { 180 | // do stuff here on connecting 181 | Serial.println("CALE::connecting"); 182 | oldDeviceConnected = deviceConnected; 183 | } 184 | 185 | } 186 | -------------------------------------------------------------------------------- /src/m365_esp32BLE_bmsmonitor.ino: -------------------------------------------------------------------------------- 1 | /* 2 | based on example ESP32/BLE BLE_UART, use "M655Tools" or M365Battery(cells don't work now, only sends 20bytes) 3 | works with M365Tools v1.2.8, v1.3.0 using 55AA protocol 4 | works with M365Tools V1.3.4 using 5AA5 protocol(uncomment define NBOTFORMAT) 5 | */ 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | BLEServer *pServer = NULL; 12 | BLECharacteristic *pTxCharacteristic; 13 | bool deviceConnected = false; 14 | bool oldDeviceConnected = false; 15 | 16 | 17 | //BMS connectrion 18 | //I am using Serial2_TX(17pin)for transmit to BMS, Serial0_RX(regular) to receive, while Serial0_TX is used for debugging to USB UART. 19 | #define SerialTX Serial2 20 | #define SerialRX Serial 21 | //Serial2, RXD2/TXD2 16/17 are default for ESP32 22 | //#define RXD2 16 23 | //#define TXD2 17 24 | 25 | uint8_t txTypeIdx = 0; 26 | uint16_t delayIdx = 0; 27 | 28 | byte packet[64]; 29 | uint8_t packetSize = 0; 30 | byte nBuf[64]; //for conversion to 5AA5 31 | 32 | //Requests to send to BMS 33 | byte buf_1010[]= {0x55, 0xAA, 0x03, 0x22, 0x01, 0x10, 0x10, 0xB9, 0xFF}; //9bytes 34 | byte buf_1012[]= {0x55, 0xAA, 0x03, 0x22, 0x01, 0x10, 0x12, 0xB7, 0xFF}; //9bytes 35 | byte buf_1b[]= {0x55, 0xAA, 0x03, 0x22, 0x01, 0x1B, 0x04, 0xBA, 0xFF}; //9bytes 36 | byte buf_20[]= {0x55, 0xAA, 0x03, 0x22, 0x01, 0x20, 0x06, 0xB3, 0xFF}; //9bytes 37 | byte buf_30[]= {0x55, 0xAA, 0x03, 0x22, 0x01, 0x30, 0x0C, 0x9D, 0xFF}; //9bytes 38 | byte buf_31[]= {0x55, 0xAA, 0x03, 0x22, 0x01, 0x31, 0x0A, 0x9E, 0xFF}; //9bytes 39 | byte buf_40[]= {0x55, 0xAA, 0x03, 0x22, 0x01, 0x40, 0x1E, 0x7B, 0xFF}; //9bytes 40 | 41 | byte xmbuf_1012[]= {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; //26 bytes 42 | byte xmbuf_1b04[]= {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; //12 bytes 43 | byte xmbuf_2006[]= {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; //14 bytes 44 | byte xmbuf_310a[]= {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; //18 bytes 45 | byte xmbuf_401e[]= {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; //38 bytes 46 | 47 | /* Hardcoded Valid Examples 48 | 0x55 0xAA format: 49 | byte xmbuf_1012[]= {0x55, 0xAA, 0x14, 0x25, 0x01, 0x10, 0x33, 0x4A, 0x43, 0x47, 0x4A, 0x31, 0x38, 0x41, 0x54, 0x44, 0x31, 0x31, 0x32, 0x35, 0x15, 0x01, 0x78, 0x1E, 0xAD, 0xFB}; //26 bytes 50 | byte xmbuf_1b04[]= {0x55, 0xAA, 0x06, 0x25, 0x01, 0x1B, 0x16, 0x00, 0x2B, 0x00, 0x77, 0xFF}; //12 bytes 51 | byte xmbuf_2006[]= {0x55, 0xAA, 0x08, 0x25, 0x01, 0x20, 0x3B, 0x24, 0x00, 0x00, 0x00, 0x00, 0x52, 0xFF}; //14 bytes 52 | byte xmbuf_310a[]= {0x55, 0xAA, 0x0C, 0x25, 0x01, 0x31, 0x31, 0x1A, 0x55, 0x00, 0x00, 0x00, 0x78, 0x0F, 0x2F, 0x2F, 0x17, 0xFE}; //18 bytes 53 | byte xmbuf_401e[]= {0x55, 0xAA, 0x20, 0x25, 0x01, 0x40, 0x6B, 0x0F, 0x6A, 0x0F, 0x6F, 0x0F, 0x6A, 0x0F, 0x6E, 0x0F, 0x84, 0x0F, 0x84, 0x0F, 0x87, 0x0F, 0x86, 0x0F, 0x86, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2C, 0xFA}; //38 bytes 54 | 0x5A 0xA5 format: length(buf[2])->reduced by 2, address(buf[3]) "0x25" -> "0x22 0x3E", so total size is increased by 1 byte. Needs new CRC. See convert5AA5() function. 55 | byte nbbuf_1012[]= {0x5A, 0xA5, 0x12, 0x22, 0x3E, 0x01, 0x10, 0x33, 0x4A, 0x43, 0x47, 0x4A, 0x31, 0x38, 0x41, 0x54, 0x44, 0x31, 0x31, 0x32, 0x35, 0x15, 0x01, 0x78, 0x1E, 0x74, 0xFB}; //27 bytes 56 | byte nbbuf_1b04[]= {0x5A, 0xA5, 0x04, 0x22, 0x3E, 0x01, 0x1B, 0x16, 0x00, 0x2B, 0x00, 0x3E, 0xFF}; //13 bytes 57 | byte nbbuf_2006[]= {{0x5A, 0xA5, 0x06, 0x22, 0x3E, 0x01, 0x20, 0x3B, 0x24, 0x00, 0x00, 0x00, 0x00, 0x19, 0xFF}; //15 bytes 58 | byte nbbuf_310a[]= {0x5A, 0xA5, 0x0A, 0x22, 0x3E, 0x01, 0x31, 0x31, 0x1A, 0x55, 0x00, 0x00, 0x00, 0x78, 0x0F, 0x2F, 0x2F, 0xDE, 0xFD}; //19 bytes 59 | byte nbbuf_401e[]= {0x5A, 0xA5, 0x1E, 0x22, 0x3E, 0x01, 0x40, 0x6B, 0x0F, 0x6A, 0x0F, 0x6F, 0x0F, 0x6A, 0x0F, 0x6E, 0x0F, 0x84, 0x0F, 0x84, 0x0F, 0x87, 0x0F, 0x86, 0x0F, 0x86, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF3, 0xF9}; //39 bytes 60 | */ 61 | 62 | 63 | void printBytes(byte *buf, size_t size){ 64 | for(size_t idx=0; idx 8 && bsize<40)) return; 73 | 74 | uint16_t CRC = 0xFFFF; 75 | for(size_t idx=2; idx>8); //hibyte 80 | } 81 | bool checkCRC(byte *buf, size_t bsize){ 82 | //check integrity of the packet 83 | //minimal size of the 55AA packet is 9 with payload 1byte, buf[2] is size of the packet minus 6 == 2(preambula)+2(bLen + Dest)+2(CRC), includes payload +2bytes(modType and command) 84 | //for 5AA5 packet min size is 10 and buf[2]+9==bsize 85 | if(!(bsize > 8 && bsize<40)) return false; 86 | if(!( (buf[0]==0x55&&buf[2]+6==bsize) || (buf[0]==0x5A&&buf[2]+9==bsize) )) return false; 87 | 88 | 89 | uint16_t CRC = 0xFFFF; 90 | for(size_t idx=2; idx>8); 94 | return isGood; 95 | } 96 | 97 | size_t convert5AA5(byte *buf, size_t size, byte *bufNew){ 98 | bufNew[0] = 0x5A; 99 | bufNew[1] = 0xA5; 100 | bufNew[2] = buf[2]-2; 101 | bufNew[3] = 0x22; 102 | bufNew[4] = 0x3E; 103 | memcpy(bufNew+5, buf+4, size-6); //skip first 4 bytes and last 2 bytes 104 | fillCRC(bufNew, size+1); 105 | return size+1; 106 | } 107 | 108 | template 109 | bool readPacket(T& pserial, unsigned long maxDelayMs=30){ 110 | packetSize = 0; 111 | unsigned long startMs = millis(); 112 | while(pserial.available() > 0 || millis()-startMs 0){ 114 | byte incomingByte = pserial.read(); 115 | if(packetSize==0){ //preambula 1st byte 116 | if(incomingByte==0x55){ 117 | packet[0] = 0x55; 118 | packetSize = 1; 119 | } 120 | }else if (packetSize==1){ //preambula 2nd byte 121 | if(incomingByte==0xAA){ 122 | packet[1] = 0xAA; 123 | packetSize = 2; 124 | }else{ 125 | packetSize = 0; 126 | } 127 | }else{ 128 | packet[packetSize] = incomingByte; 129 | packetSize++; 130 | if(packetSize>8 && packet[2]+6==packetSize) //Reached the end of the packet according to packet[2] byte 131 | break; 132 | if(packetSize>=40) 133 | break; 134 | } 135 | } 136 | } 137 | while(pserial.available()) pserial.read(); //flush the remaining of the serial buffer, something is off. 138 | 139 | //check integrity of the packet 140 | //minimal size of the packet is 9 with payload 1byte, packet[2] is size of the packet minus 2(preambula)+2(bLen + type)+2(CRC) 141 | if(packetSize > 8 && packetSize<40 && packet[2]+6==packetSize){ 142 | if(checkCRC(packet, packetSize)){ 143 | //CRC is good 144 | return true; 145 | } 146 | } 147 | //packetSize = 0; 148 | return false; 149 | } 150 | 151 | 152 | 153 | // UART service UUID, has to be those specific for M365 protocol 154 | #define SERVICE_UUID "6E400001-B5A3-F393-E0A9-E50E24DCCA9E" 155 | #define CHARACTERISTIC_UUID_RX "6E400002-B5A3-F393-E0A9-E50E24DCCA9E" 156 | #define CHARACTERISTIC_UUID_TX "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" 157 | 158 | class MyServerCallbacks: public BLEServerCallbacks { 159 | void onConnect(BLEServer* pServer) { 160 | deviceConnected = true; 161 | }; 162 | 163 | void onDisconnect(BLEServer* pServer) { 164 | deviceConnected = false; 165 | } 166 | }; 167 | 168 | void TxPacket(byte *buf, size_t bsize, uint8_t fType){ 169 | //breaks packet if needed to 20 bytes chunks 170 | if(bsize>40)return; //we don't expect more than 2 packets for M365 protocol 171 | 172 | if(fType==2){ //Convert to 5AA5 format for BLE app 173 | bsize = convert5AA5(buf, bsize, nBuf); 174 | buf = nBuf; 175 | } 176 | 177 | while(bsize>0){ 178 | if(bsize<=20){ 179 | //send everything 180 | pTxCharacteristic->setValue(buf, bsize); 181 | pTxCharacteristic->notify(); 182 | bsize = 0; 183 | }else{ 184 | //send next 20 bytes 185 | pTxCharacteristic->setValue(buf, 20); 186 | pTxCharacteristic->notify(); 187 | for(uint16_t idx=0; idx<1000; ++idx) ++delayIdx; //delay 188 | buf += 20; 189 | bsize -= 20; 190 | } 191 | } 192 | 193 | } 194 | 195 | class MyCallbacks: public BLECharacteristicCallbacks { 196 | void onWrite(BLECharacteristic *pCharacteristic) { 197 | std::string rxValue = pCharacteristic->getValue(); 198 | 199 | if (rxValue.length() > 6) { 200 | 201 | uint8_t fType = 0; 202 | uint8_t reqId; 203 | if(rxValue[0]==0x55 && rxValue[1]==0xAA){ 204 | fType = 1; 205 | reqId = rxValue[5]; 206 | } 207 | if(rxValue[0]==0x5A && rxValue[1]==0xA5){ 208 | fType = 2; 209 | reqId = rxValue[6]; 210 | } 211 | 212 | if(fType == 0) return; 213 | 214 | //I hijacked some request which are not 1:1, but it seems it works, M365Tools still process responses no matter what it requested 215 | if(reqId==0x1A) { 216 | if(checkCRC(xmbuf_1012, 26)){ 217 | TxPacket(xmbuf_1012, 26, fType); 218 | } 219 | } 220 | if(reqId==0x67) { 221 | if(checkCRC(xmbuf_1b04, 12)){ 222 | TxPacket(xmbuf_1b04, 12, fType); 223 | } 224 | } 225 | if(reqId==0x72) { 226 | if(checkCRC(xmbuf_2006, 14)){ 227 | TxPacket(xmbuf_2006, 14, fType); 228 | } 229 | } 230 | if(reqId==0x30) { 231 | if(checkCRC(xmbuf_310a, 18)){ 232 | TxPacket(xmbuf_310a, 18, fType); 233 | } 234 | } 235 | if(reqId==0x40) { 236 | if(checkCRC(xmbuf_401e, 38)){ 237 | TxPacket(xmbuf_401e, 38, fType); 238 | } 239 | } 240 | 241 | } 242 | } 243 | }; 244 | 245 | 246 | void setup() { 247 | Serial.begin(115200); 248 | Serial2.begin(115200); //, SERIAL_8N1, RXD2, TXD2 249 | // Create the BLE Device 250 | BLEDevice::init("MiScooter6560"); //"Mi Electric Scooter", "Xiaomi M365", "MiScooter6560" 251 | 252 | // Create the BLE Server 253 | pServer = BLEDevice::createServer(); 254 | pServer->setCallbacks(new MyServerCallbacks()); 255 | 256 | // Create the BLE Service 257 | BLEService *pService = pServer->createService(SERVICE_UUID); 258 | 259 | // Create a BLE Characteristic 260 | pTxCharacteristic = pService->createCharacteristic(CHARACTERISTIC_UUID_TX, BLECharacteristic::PROPERTY_NOTIFY); 261 | pTxCharacteristic->addDescriptor(new BLE2902()); 262 | 263 | BLECharacteristic * pRxCharacteristic = pService->createCharacteristic(CHARACTERISTIC_UUID_RX, BLECharacteristic::PROPERTY_WRITE); 264 | pRxCharacteristic->setCallbacks(new MyCallbacks()); 265 | 266 | // Start the service 267 | pService->start(); 268 | 269 | // Start advertising 270 | pServer->getAdvertising()->start(); 271 | Serial.println("Waiting a client connection to notify..."); 272 | } 273 | 274 | void loop() { 275 | 276 | //make sure nothing in transmit or received buffer for BMS Serial 277 | SerialTX.flush(); 278 | while(SerialRX.available()) SerialRX.read(); 279 | 280 | //Send request to BMS, alternate request based on txTypeIdx iterator, 0x30 is sent 3 times more frequent than others 281 | uint8_t txType = txTypeIdx%9; 282 | switch(txType){ 283 | case 0: 284 | case 3: 285 | case 6: 286 | SerialTX.write(buf_30,9); 287 | break; 288 | case 1: 289 | SerialTX.write(buf_1010,9); 290 | break; 291 | case 2: 292 | SerialTX.write(buf_1012,9); 293 | break; 294 | case 4: 295 | SerialTX.write(buf_1b,9); 296 | break; 297 | case 5: 298 | SerialTX.write(buf_20,9); 299 | break; 300 | case 7: 301 | SerialTX.write(buf_31,9); 302 | break; 303 | case 8: 304 | SerialTX.write(buf_40,9); 305 | break; 306 | default: 307 | break; 308 | } 309 | 310 | //Read reply from BMS 311 | bool isGood = readPacket(SerialRX); //checkCRC(packet, packetSize) is returned 312 | 313 | //Parse packet consistency(length and CRC) and copy to the corresponding xmbuf 314 | if(packetSize<40 && packet[2]+6==packetSize){ 315 | //valid size of the packet, packet[2](payload + 2 bytes for mod_type and command) and doesn't include preambula, CRC, bLen, Destination) 316 | if(checkCRC(packet, packetSize)){ 317 | //integrity and CRC are good 318 | if(packet[5]==0x10 && packet[2]==0x14) //1012 319 | memcpy(xmbuf_1012, packet, packetSize); 320 | if(packet[5]==0x1B && packet[2]==0x06) //1b04 321 | memcpy(xmbuf_1b04, packet, packetSize); 322 | if(packet[5]==0x20 && packet[2]==0x08) //2006 323 | memcpy(xmbuf_2006, packet, packetSize); 324 | if(packet[5]==0x31 && packet[2]==0x0C) //310a 325 | memcpy(xmbuf_310a, packet, packetSize); 326 | if(packet[5]==0x40 && packet[2]==0x20) //401e 327 | memcpy(xmbuf_401e, packet, packetSize); 328 | } 329 | } 330 | 331 | txTypeIdx++; 332 | delay(200); // bluetooth stack will go into congestion, if too many packets are sent 333 | 334 | 335 | // disconnecting 336 | if (!deviceConnected && oldDeviceConnected) { 337 | delay(500); // give the bluetooth stack the chance to get things ready 338 | pServer->startAdvertising(); // restart advertising 339 | Serial.println("start advertising"); 340 | oldDeviceConnected = deviceConnected; 341 | } 342 | // connecting 343 | if (deviceConnected && !oldDeviceConnected) { 344 | // do stuff here on connecting 345 | Serial.println("connecting"); 346 | oldDeviceConnected = deviceConnected; 347 | } 348 | 349 | } 350 | --------------------------------------------------------------------------------