├── README.md └── esp32-smartcube.ino /README.md: -------------------------------------------------------------------------------- 1 | # esp32-smartcube 2 | ESP32 interface to Bluetooth LE "Smart" Rubik's Cube (Giiker, Xiaomi etc.), e.g. from https://www.banggood.com/custlink/D3DvQbsa4f 3 | -------------------------------------------------------------------------------- /esp32-smartcube.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * ESP32-SmartCube 3 | * Copyright (c) 2019 Playful Technology 4 | * 5 | * ESP32 sketch to connect to Xiaomi "Smart Magic Cube" Rubik's Cube via Bluetooth LE 6 | * and decode notification messages containing puzzle state. 7 | * Prints output to serial connection of the last move made, and when the cube is fully 8 | * solved, triggers a relay output 9 | */ 10 | 11 | // INCLUDES 12 | // ESP32 library for Bluetooth LE 13 | #include "BLEDevice.h" 14 | 15 | // CONSTANTS 16 | // The MAC address of the Rubik's Cube 17 | // This can be discovered by starting a scan BEFORE touching the cube. Then twist any face 18 | // to wake the cube up and see what new device appears 19 | static BLEAddress *pServerAddress = new BLEAddress("d9:47:6f:3b:f4:e1"); 20 | // The remote service we wish to connect to 21 | static BLEUUID serviceUUID("0000aadb-0000-1000-8000-00805f9b34fb"); 22 | // The characteristic of the remote service we want to track 23 | static BLEUUID charUUID("0000aadc-0000-1000-8000-00805f9b34fb"); 24 | // The following constants are used to decrypt the data representing the state of the cube 25 | // see https://github.com/cs0x7f/cstimer/blob/master/src/js/bluetooth.js 26 | const uint8_t decryptionKey[] = {176, 81, 104, 224, 86, 137, 237, 119, 38, 26, 193, 161, 210, 126, 150, 81, 93, 13, 236, 249, 89, 235, 88, 24, 113, 81, 214, 131, 130, 199, 2, 169, 39, 165, 171, 41}; 27 | // This pin will have a HIGH pulse sent when the cube is solved 28 | const byte relayPin = 33; 29 | // This is the data array representing a solved cube 30 | const byte solution[16] = {0x12,0x34,0x56,0x78,0x33,0x33,0x33,0x33,0x12,0x34,0x56,0x78,0x9a,0xbc,0x00,0x00}; 31 | 32 | // GLOBALS 33 | // Have we found a cube with the right MAC address to connect to? 34 | static boolean deviceFound = false; 35 | // Are we currently connected to the cube? 36 | static boolean connected = false; 37 | // Properties of the device found via scan 38 | static BLEAdvertisedDevice* myDevice; 39 | // BT characteristic of the connected device 40 | static BLERemoteCharacteristic* pRemoteCharacteristic; 41 | 42 | // HELPER FUNCTIONS 43 | /** 44 | * Return the ith bit from an integer array 45 | */ 46 | int getBit(uint8_t* val, int i) { 47 | int n = ((i / 8) | 0); 48 | int shift = 7 - (i % 8); 49 | return (val[n] >> shift) & 1; 50 | } 51 | 52 | /** 53 | * Return the ith nibble (half-byte, i.e. 16 possible values) 54 | */ 55 | uint8_t getNibble(uint8_t val[], int i) { 56 | if(i % 2 == 1) { 57 | // return val[(i/2)|0] % 16; 58 | return val[i / 2] & 0x0F; 59 | } 60 | //return 0|(val[(i/2)|0] / 16); 61 | return (val[i / 2] >> 4) & 0x0F; 62 | } 63 | 64 | // CALLBACKS 65 | /** 66 | * Callbacks for devices found via a Bluetooth scan of advertised devices 67 | */ 68 | class AdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks { 69 | // The onResult callback is called for every advertised device found by the scan 70 | void onResult(BLEAdvertisedDevice advertisedDevice) { 71 | // Print the MAC address of this device 72 | Serial.print(" - "); 73 | Serial.print(advertisedDevice.getAddress().toString().c_str()); 74 | // Does this device match the MAC address we're looking for? 75 | if(advertisedDevice.getAddress().equals(*pServerAddress)) { 76 | // Stop scanning for further devices 77 | advertisedDevice.getScan()->stop(); 78 | // Create a new device based on properties of advertised device 79 | myDevice = new BLEAdvertisedDevice(advertisedDevice); 80 | // Set flag 81 | deviceFound = true; 82 | Serial.println(F(" - Connecting!")); 83 | } 84 | else { 85 | Serial.println(F("... MAC address does not match")); 86 | } 87 | } 88 | }; 89 | 90 | /** 91 | * Callbacks for device we connect to 92 | */ 93 | class ClientCallbacks : public BLEClientCallbacks { 94 | // Called when a new connection is established 95 | void onConnect(BLEClient* pclient) { 96 | digitalWrite(LED_BUILTIN, HIGH); 97 | connected = true; 98 | } 99 | // Called when a connection is lost 100 | void onDisconnect(BLEClient* pclient) { 101 | digitalWrite(LED_BUILTIN, LOW); 102 | connected = false; 103 | } 104 | }; 105 | 106 | /** 107 | * Called whenever a notication is received that the tracked BLE characterisic has changed 108 | */ 109 | static void notifyCallback(BLERemoteCharacteristic* pBLERemoteCharacteristic, uint8_t* pData, size_t length, bool isNotify) { 110 | // DECRYPT DATA 111 | // Early Bluetooth cubes used an unencrypted data format that sent the corner/edge indexes as a 112 | // simple raw string, e.g. pData for a solved cube would be 1234567833333333123456789abc000041414141 113 | // However, newer cubes e.g. Giiker i3s encrypt data with a rotating key, so that same state might be 114 | // 706f6936b1edd1b5e00264d099a4e8a19d3ea7f1 then d9f67772c3e9a5ea6e84447abb527156f9dca705 etc. 115 | 116 | // To find out whether the data is encrypted, we first read the penultimate byte of the characteristic data. 117 | // As in the two examples above, if this is 0xA7, we know it's encrypted 118 | bool isEncrypted = (pData[18] == 0xA7); 119 | 120 | // If it *is* encrypted... 121 | if(isEncrypted) { 122 | // Split the last byte into two 4-bit values 123 | int offset1 = getNibble(pData, 38); 124 | int offset2 = getNibble(pData, 39); 125 | 126 | // Retrieve a pair of offset values from the decryption key 127 | for (int i=0; i<20; i++) { 128 | // Apply the offset to each value in the data 129 | pData[i] += (decryptionKey[offset1 + i] + decryptionKey[offset2 + i]); 130 | } 131 | } 132 | 133 | // First 16 bytes represent state of the cube - 8 corners (with 3 orientations), and 12 edges (can be flipped) 134 | Serial.print("Current State: "); 135 | for (int i=0; i<16; i++) { 136 | Serial.print(pData[i], HEX); 137 | Serial.print(" "); 138 | } 139 | Serial.println(""); 140 | 141 | // Byte 17 represents the last twist made - first half-byte is face, and second half-byte is direction of rotation 142 | int lastMoveFace = getNibble(pData, 32); 143 | int lastMoveDirection = getNibble(pData, 33); 144 | char* faceNames[6] = {"Front", "Bottom", "Right", "Top", "Left", "Back"}; 145 | Serial.print("Last Move: "); 146 | Serial.print(faceNames[lastMoveFace-1]); 147 | Serial.print(lastMoveDirection == 1 ? " Face Clockwise" : " Face Anti-Clockwise" ); 148 | Serial.println(""); 149 | 150 | Serial.println("----"); 151 | 152 | if(memcmp(pData, solution, 16) == 0) { 153 | digitalWrite(relayPin, HIGH); 154 | delay(100); 155 | digitalWrite(relayPin, LOW); 156 | } 157 | 158 | } 159 | 160 | /* 161 | * Connect to the BLE server of the correct MAC address 162 | */ 163 | bool connectToServer() { 164 | Serial.print(F("Creating BLE client... ")); 165 | BLEClient* pClient = BLEDevice::createClient(); 166 | delay(500); 167 | Serial.println(F("Done.")); 168 | 169 | Serial.print(F("Assigning callbacks... ")); 170 | pClient->setClientCallbacks(new ClientCallbacks()); 171 | delay(500); 172 | Serial.println(F(" - Done.")); 173 | 174 | // Connect to the remove BLE Server. 175 | Serial.print(F("Connecting to ")); 176 | Serial.print(myDevice->getAddress().toString().c_str()); 177 | Serial.print(F("... ")); 178 | pClient->connect(myDevice); 179 | delay(500); 180 | Serial.println(" - Done."); 181 | 182 | // Obtain a reference to the service we are after in the remote BLE server. 183 | Serial.print(F("Finding service ")); 184 | Serial.print(serviceUUID.toString().c_str()); 185 | Serial.print(F("... ")); 186 | BLERemoteService* pRemoteService = pClient->getService(serviceUUID); 187 | delay(500); 188 | if (pRemoteService == nullptr) { 189 | Serial.println(F("FAILED.")); 190 | return false; 191 | } 192 | Serial.println(" - Done."); 193 | delay(500); 194 | 195 | // Obtain a reference to the characteristic in the service of the remote BLE server. 196 | Serial.print(F("Finding characteristic ")); 197 | Serial.print(charUUID.toString().c_str()); 198 | Serial.print(F("... ")); 199 | pRemoteCharacteristic = pRemoteService->getCharacteristic(charUUID); 200 | if (pRemoteCharacteristic == nullptr) { 201 | Serial.println(F("FAILED.")); 202 | return false; 203 | } 204 | Serial.println(" - Done."); 205 | delay(500); 206 | 207 | Serial.print(F("Registering for notifications... ")); 208 | if(pRemoteCharacteristic->canNotify()) { 209 | pRemoteCharacteristic->registerForNotify(notifyCallback); 210 | Serial.println(" - Done."); 211 | } 212 | else { 213 | Serial.println(F("FAILED.")); 214 | return false; 215 | } 216 | 217 | Serial.println("READY!"); 218 | return true; 219 | } 220 | 221 | /** 222 | * Search for any advertised devices 223 | */ 224 | void scanForDevices(){ 225 | Serial.println("Scanning for Bluetooth devices..."); 226 | // Retrieve a Scanner and set the callback we want to use to be informed when we 227 | // have detected a new device. Specify that we want active scanning and start the 228 | // scan to run for 30 seconds. 229 | BLEScan* pBLEScan = BLEDevice::getScan(); 230 | pBLEScan->setAdvertisedDeviceCallbacks(new AdvertisedDeviceCallbacks()); 231 | pBLEScan->setActiveScan(true); 232 | pBLEScan->start(30); 233 | } 234 | 235 | // Initial setup 236 | void setup() { 237 | // Start the serial connection to be able to track debug data 238 | Serial.begin(115200); 239 | 240 | Serial.print("Initialising BLE..."); 241 | BLEDevice::init(""); 242 | delay(500); 243 | Serial.println(F("Done.")); 244 | 245 | // relayPin will be set HIGH when the cube is solved 246 | pinMode(relayPin, OUTPUT); 247 | digitalWrite(relayPin, LOW); 248 | 249 | // ledPin will be set HIGH when cube is connected 250 | pinMode(LED_BUILTIN, OUTPUT); 251 | } 252 | 253 | // Main program loop function 254 | void loop() { 255 | // If the cube has been found, connect to it 256 | if (deviceFound) { 257 | if(!connected) { 258 | connectToServer(); 259 | } 260 | } 261 | else { 262 | scanForDevices(); 263 | } 264 | // Introduce a little delay 265 | delay(1000); 266 | } 267 | --------------------------------------------------------------------------------