├── .gitattributes ├── LICENSE ├── README.md ├── RuipuBattery.cpp ├── RuipuBattery.h ├── examples └── RuipuBatteryExample │ └── RuipuBatteryExample.ino └── keywords.txt /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Justin Sutcliff 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 | # OKAI Battery Interface Library 2 | This library is intended to make interfacing with the batteries found in the OKAI ES200 scooters simple and easy. These batteries appear to be manufactured by 'Ruipu Electronic Technology co.' which is apparently a daughter company to OKAI. 3 | 4 | # Usage 5 | The main RuipuBattery class expects a pointer to a Stream in its constructor. See below for examples: 6 | 7 | Using a HardwareSerial port: 8 | ~~~ 9 | RuipuBattery myBattery(&Serial); 10 | ~~~ 11 | 12 | Using a SoftwareSerial port: 13 | ~~~ 14 | SoftwareSerial mySerial(10, 11); 15 | RuipuBattery myBattery(&mySerial); 16 | ~~~ 17 | 18 | Whichever serial port is chose must be initialized to 9600 baud rate. 19 | 20 | ~~~ 21 | Serial.begin(9600); 22 | ~~~ 23 | or 24 | ~~~ 25 | mySerial.begin(9600); 26 | ~~~ 27 | 28 | The scooter battery will not output a significant amount of power unless the unlock command is issued. Calling the unlock command every 5 seconds seems to be all that is required to keep the battery operating. 29 | 30 | ~~~ 31 | myBattery.unlock(); 32 | ~~~ 33 | 34 | Every time the unlock message is sent to the battery, the BMS will reply with a status message. In order to read this message, the read() function must be called as often as possible. This function will return true if a status message is successfully read. 35 | 36 | ~~~ 37 | myBattery.read(); 38 | ~~~ 39 | 40 | The status message can now be accessed through the following methods: 41 | 42 | Battery state of charge (0% - 100%) 43 | ~~~ 44 | myBattery.soc(); 45 | ~~~ 46 | 47 | Battery voltage as a float 48 | ~~~ 49 | myBattery.voltage(); 50 | ~~~ 51 | 52 | Battery current as a float 53 | ~~~ 54 | myBattery.current(); 55 | ~~~ 56 | 57 | Highest cell voltage as a float 58 | ~~~ 59 | myBattery.high(); 60 | ~~~ 61 | 62 | Lowest cell voltage as a float 63 | ~~~ 64 | myBattery.low(); 65 | ~~~ 66 | 67 | Lowest temperature in celsius 68 | ~~~ 69 | myBattery.minTemp(); 70 | ~~~ 71 | 72 | Highest temperature in celsius 73 | ~~~ 74 | myBattery.maxTemp(); 75 | ~~~ 76 | 77 | These four values are also available in their two-byte raw forms as received from the BMS: 78 | 79 | ~~~ 80 | myBattery.rawVoltage(); 81 | myBattery.rawCurrent(); 82 | myBattery.rawLow(); 83 | myBattery.rawHigh(); 84 | ~~~ 85 | 86 | # Full Example 87 | 88 | ~~~ 89 | #include 90 | #include "RuipuBattery.h" 91 | 92 | SoftwareSerial mySerial(10, 11); // RX (green wire), TX (blue wire) 93 | RuipuBattery myBattery(&mySerial); 94 | 95 | unsigned long timer = 0; 96 | bool haveReadData = false; 97 | 98 | void setup() 99 | { 100 | Serial.begin(9600); // Initialize serial for printing results 101 | mySerial.begin(9600); // Initialize serial for BMS communications 102 | } 103 | 104 | void loop() 105 | { 106 | // Non-blocking timer 107 | if (millis() - timer > 1000) 108 | { 109 | timer = millis(); 110 | 111 | myBattery.unlock(); // Send the unlock command 112 | haveReadData = false; // Reset the read flag 113 | } 114 | 115 | if (myBattery.read() && !haveReadData) 116 | { 117 | haveReadData = true; // Set the read flag 118 | 119 | // Serial plotter friendly outputs 120 | Serial.print("State of Charge:"); 121 | Serial.print(myBattery.soc()); 122 | Serial.print(", "); 123 | Serial.print("Voltage:"); 124 | Serial.print(myBattery.voltage()); 125 | Serial.print(", "); 126 | Serial.print("Current:"); 127 | Serial.print(myBattery.current()); 128 | Serial.print(", "); 129 | Serial.print("Lowest:"); 130 | Serial.print(myBattery.low()); 131 | Serial.print(", "); 132 | Serial.print("Highest:"); 133 | Serial.print(myBattery.high()); 134 | Serial.print(", "); 135 | Serial.print("Max Temp:"); 136 | Serial.print(myBattery.maxTemp()); 137 | Serial.print(", "); 138 | Serial.print("Min Temp:"); 139 | Serial.print(myBattery.minTemp()); 140 | Serial.print(", "); 141 | Serial.println(); 142 | } 143 | } 144 | ~~~ 145 | -------------------------------------------------------------------------------- /RuipuBattery.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | RuipuBattery.cpp - Library for interfacing with Ruipu (OKAI) Scooter Batteries 3 | Created by Justin Sutcliff, December 13, 2020. 4 | Released into the public domain. 5 | */ 6 | 7 | #include "Arduino.h" 8 | #include "RuipuBattery.h" 9 | 10 | RuipuBattery::RuipuBattery(Stream *stream) 11 | { 12 | _stream = stream; 13 | } 14 | 15 | byte RuipuBattery::crc(const byte *data, byte len) 16 | { 17 | byte crc = 0x00; 18 | while (len--) 19 | { 20 | byte extract = *data++; 21 | for (byte tempI = 8; tempI; tempI--) 22 | { 23 | byte sum = (crc ^ extract) & 0x01; 24 | crc >>= 1; 25 | if (sum) 26 | { 27 | crc ^= 0x8C; 28 | } 29 | extract >>= 1; 30 | } 31 | } 32 | return crc; 33 | } 34 | 35 | void RuipuBattery::unlock() 36 | { 37 | reset(); 38 | byte buf[5] = {0x3A, 0x13, 0x01, 0x16, 0x79}; 39 | _stream->write(buf, sizeof(buf)); 40 | } 41 | 42 | bool RuipuBattery::read() 43 | { 44 | // Read all available serial data 45 | while (_stream->available() > 0) 46 | { 47 | byte b = _stream->read(); 48 | 49 | if (_bytesRead < 36) 50 | { 51 | _buf[_bytesRead] = b; 52 | _bytesRead++; 53 | } 54 | } 55 | 56 | // Check for complete message 57 | if (_bytesRead > 35) 58 | { 59 | byte test = crc(_buf, 35); 60 | 61 | // Check crc 62 | if (_buf[35] == test) 63 | { 64 | return true; 65 | } 66 | 67 | _bytesRead = 0; 68 | } 69 | 70 | return false; 71 | } 72 | 73 | void RuipuBattery::reset() 74 | { 75 | while (_stream->available() > 0) 76 | { 77 | _stream->read(); 78 | } 79 | _bytesRead = 0; 80 | } 81 | 82 | byte *RuipuBattery::buf() 83 | { 84 | return _buf; 85 | } 86 | 87 | uint8_t RuipuBattery::soc() 88 | { 89 | return _buf[5]; 90 | } 91 | 92 | uint16_t RuipuBattery::rawVoltage() 93 | { 94 | return _buf[22] << 8 | _buf[21]; 95 | } 96 | 97 | uint16_t RuipuBattery::rawLow() 98 | { 99 | return _buf[32] << 8 | _buf[31]; 100 | } 101 | 102 | uint16_t RuipuBattery::rawHigh() 103 | { 104 | return _buf[30] << 8 | _buf[29]; 105 | } 106 | 107 | int16_t RuipuBattery::rawCurrent() 108 | { 109 | return _buf[26] << 8 | _buf[25]; 110 | } 111 | 112 | float RuipuBattery::voltage() 113 | { 114 | return rawVoltage() / 1000.0; 115 | } 116 | 117 | float RuipuBattery::current() 118 | { 119 | return rawCurrent() / 1000.0; 120 | } 121 | 122 | float RuipuBattery::low() 123 | { 124 | return rawLow() / 1000.0; 125 | } 126 | 127 | float RuipuBattery::high() 128 | { 129 | return rawHigh() / 1000.0; 130 | } 131 | 132 | uint8_t RuipuBattery::maxTemp() 133 | { 134 | uint8_t maxTemp = 0; 135 | 136 | for (int i = 0; i < 4; i++) 137 | { 138 | if (_buf[7 + i] > maxTemp) 139 | { 140 | maxTemp = _buf[7 + i]; 141 | } 142 | } 143 | 144 | return maxTemp; 145 | } 146 | 147 | uint8_t RuipuBattery::minTemp() 148 | { 149 | uint8_t minTemp = 255; 150 | 151 | for (int i = 0; i < 4; i++) 152 | { 153 | if (_buf[7 + i] < minTemp) 154 | { 155 | minTemp = _buf[7 + i]; 156 | } 157 | } 158 | 159 | return minTemp; 160 | } 161 | 162 | uint8_t RuipuBattery::rawStatus() 163 | { 164 | return _buf[3]; 165 | } 166 | 167 | bool RuipuBattery::isChargingBulk() 168 | { 169 | return (rawStatus() >> 5) & 1; 170 | } 171 | 172 | bool RuipuBattery::isCellUndervoltage() 173 | { 174 | return (rawStatus() >> 4) & 1; 175 | } 176 | 177 | bool RuipuBattery::isChargerOK() 178 | { 179 | return (rawStatus() >> 3) & 1; 180 | } 181 | 182 | bool RuipuBattery::isChargerDetected() 183 | { 184 | return (rawStatus() >> 2) & 1; 185 | } 186 | 187 | bool RuipuBattery::isChargeFETEnabled() 188 | { 189 | return rawStatus() & 1; 190 | } 191 | 192 | bool RuipuBattery::isDischargeFETEnabled() 193 | { 194 | return (rawStatus() >> 1) & 1; 195 | } 196 | 197 | uint8_t RuipuBattery::maxCellTemp() 198 | { 199 | return _buf[7]; 200 | } 201 | 202 | uint8_t RuipuBattery::avgCellTemp() 203 | { 204 | return _buf[8]; 205 | } 206 | 207 | uint8_t RuipuBattery::dischargeFETTemp() 208 | { 209 | return _buf[9]; 210 | } 211 | uint8_t RuipuBattery::microcontrollerTemp() 212 | { 213 | return _buf[10]; 214 | } 215 | 216 | uint16_t RuipuBattery::chargeCycleCount() 217 | { 218 | return _buf[12] << 8 | _buf[11]; 219 | } 220 | 221 | RuipuBattery::ChargerState RuipuBattery::chargerState() 222 | { 223 | switch(_buf[13]){ 224 | case 0x00: 225 | return RuipuBattery::ChargerState::DISCHARGING; 226 | case 0x19: 227 | return RuipuBattery::ChargerState::BEGIN_CHARGING; 228 | case 0x7C: 229 | return RuipuBattery::ChargerState::CHARGING; 230 | default: 231 | return RuipuBattery::ChargerState::INVALID; 232 | } 233 | } -------------------------------------------------------------------------------- /RuipuBattery.h: -------------------------------------------------------------------------------- 1 | /* 2 | RuipuBattery.h - Library for interfacing with Ruipu (OKAI) Scooter Batteries 3 | Created by Justin Sutcliff, December 13, 2020. 4 | Released into the public domain. 5 | */ 6 | 7 | #ifndef RuipuBattery_h 8 | #define RuipuBattery_h 9 | 10 | #include "Arduino.h" 11 | 12 | class RuipuBattery 13 | { 14 | public: 15 | RuipuBattery(Stream *stream); 16 | void unlock(); 17 | void reset(); 18 | bool read(); 19 | 20 | byte *buf(); 21 | 22 | typedef enum ChargerState{ 23 | DISCHARGING, 24 | BEGIN_CHARGING, 25 | CHARGING, 26 | INVALID 27 | }; 28 | 29 | uint16_t rawVoltage(); 30 | uint16_t rawLow(); 31 | uint16_t rawHigh(); 32 | int16_t rawCurrent(); 33 | 34 | uint8_t soc(); 35 | uint8_t maxTemp(); 36 | uint8_t minTemp(); 37 | 38 | float voltage(); 39 | float current(); 40 | float low(); 41 | float high(); 42 | 43 | uint8_t rawStatus(); 44 | bool isChargingBulk(); 45 | bool isCellUndervoltage(); 46 | bool isChargerOK(); 47 | bool isChargerDetected(); 48 | bool isChargeFETEnabled(); 49 | bool isDischargeFETEnabled(); 50 | 51 | uint8_t maxCellTemp(); 52 | uint8_t avgCellTemp(); 53 | uint8_t dischargeFETTemp(); 54 | uint8_t microcontrollerTemp(); 55 | 56 | uint16_t chargeCycleCount(); 57 | ChargerState chargerState(); 58 | 59 | private: 60 | byte crc(const byte *data, byte len); 61 | Stream *_stream; 62 | uint8_t _bytesRead; 63 | byte _buf[36]; 64 | }; 65 | 66 | #endif 67 | -------------------------------------------------------------------------------- /examples/RuipuBatteryExample/RuipuBatteryExample.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include "RuipuBattery.h" 3 | 4 | SoftwareSerial mySerial(10, 11); // RX (green wire), TX (blue wire) 5 | RuipuBattery myBattery(&mySerial); 6 | 7 | unsigned long timer = 0; 8 | bool haveReadData = false; 9 | 10 | void setup() 11 | { 12 | Serial.begin(9600); // Initialize serial for printing results 13 | mySerial.begin(9600); // Initialize serial for BMS communications 14 | } 15 | 16 | void loop() 17 | { 18 | // Non-blocking timer 19 | if (millis() - timer > 1000) 20 | { 21 | timer = millis(); 22 | 23 | myBattery.unlock(); // Send the unlock command 24 | haveReadData = false; // Reset the read flag 25 | } 26 | 27 | if (myBattery.read() && !haveReadData) 28 | { 29 | haveReadData = true; // Set the read flag 30 | 31 | // Serial plotter friendly outputs 32 | Serial.print("State of Charge:"); 33 | Serial.print(myBattery.soc()); 34 | Serial.print(", "); 35 | Serial.print("Voltage:"); 36 | Serial.print(myBattery.voltage()); 37 | Serial.print(", "); 38 | Serial.print("Current:"); 39 | Serial.print(myBattery.current()); 40 | Serial.print(", "); 41 | Serial.print("Lowest:"); 42 | Serial.print(myBattery.low()); 43 | Serial.print(", "); 44 | Serial.print("Highest:"); 45 | Serial.print(myBattery.high()); 46 | Serial.print(", "); 47 | Serial.print("Max Temp:"); 48 | Serial.print(myBattery.maxTemp()); 49 | Serial.print(", "); 50 | Serial.print("Min Temp:"); 51 | Serial.print(myBattery.minTemp()); 52 | Serial.print(", "); 53 | Serial.println(); 54 | } 55 | } -------------------------------------------------------------------------------- /keywords.txt: -------------------------------------------------------------------------------- 1 | RuipuBattery KEYWORD1 2 | unlock KEYWORD2 3 | readStatus KEYWORD2 4 | rawPackVoltage KEYWORD2 5 | rawLowestVoltage KEYWORD2 6 | rawHighestVoltage KEYWORD2 7 | rawPackCurrent KEYWORD2 8 | packVoltage KEYWORD2 9 | packCurrent KEYWORD2 10 | lowestVoltage KEYWORD2 11 | highestVoltage KEYWORD2 --------------------------------------------------------------------------------