├── Private.h ├── README.md ├── LICENSE └── SPA_Monitor.ino /Private.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | //#define ssid (const char*)"" 4 | //#define password (const char*)"" 5 | 6 | const char* ssid = ""; 7 | const char* password = ""; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Intex PureSpa Remote 2 | 3 | *Archived: I have no time to play with this at this time and it's stagnated.* 4 | 5 | Control and monitor your Intex PureSpa remotely. 6 | 7 | First push will be my existing project - warts'n'all. 8 | 9 | It connects in parallel with the display lines, 10 | 'emulating' a 16bit shift register to read data parsing it 11 | for MQTT use or direct Wifi connection. 12 | 13 | Currently it uses an ESP8266 for prototype, though the ESP32 14 | is tempting because of Bluetooth and dual-core will simplify and allow 15 | for a little laziness in the monitoring! 16 | 17 | NOTE: 18 | There are several variations of the display unit and I only support model '11930'. 19 | 20 | You can help expand support by: 21 | - Sending clear, close photos of your display unit PCB - both sides. 22 | or 23 | - Sending me a PCB. 24 | or 25 | - Make a pull-request or gist of the changes. 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 TinWhisker 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 | -------------------------------------------------------------------------------- /SPA_Monitor.ino: -------------------------------------------------------------------------------- 1 | // *** OTA Specific *** 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | // ******************** 8 | 9 | #include 10 | #include 11 | 12 | ESP8266WebServer server(80); 13 | 14 | #define GPIO_IN ((volatile uint32_t*) 0x60000318) //GPIO Read Register 15 | #define GPIO_OUT ((volatile uint32_t*) 0x60000300) //GPIO Write Register 16 | 17 | // Private.h contains the ssid and password as a temporary measure until a config page is added 18 | #include "Private.h" 19 | //const char* ssid = "TestSSID"; 20 | //const char* password = "TestPASS"; 21 | 22 | uint16_t buf = 0; //16-Bit buffer 23 | 24 | char segments[4]; 25 | uint8_t allSegs = 0; 26 | 27 | uint8_t btnRequest; 28 | uint8_t btnCount; 29 | 30 | bool flushSync = false; //True when display reset, used to sync buffer 31 | bool btnSync = false; //True during button frame 32 | 33 | void readSegment(int seg) { 34 | uint16_t digit = buf & 13976; //AND buf with mask to strip off values, like buzzer, etc. 35 | char sx = -1; 36 | 37 | if (digit == 16) sx = '0'; 38 | if (digit == 9368) sx = '1'; 39 | if (digit == 520) sx = '2'; 40 | if (digit == 136) sx = '3'; 41 | if (digit == 9344) sx = '4'; 42 | if (digit == 4224) sx = '5'; 43 | if (digit == 4096) sx = '6'; 44 | if (digit == 1176) sx = '7'; 45 | if (digit == 0) sx = '8'; 46 | if (digit == 1152) sx = '9'; 47 | 48 | if (digit == 4624) sx = 'C'; 49 | if (digit == 5632) sx = 'F'; 50 | if (digit == 4608) sx = 'E'; 51 | 52 | if (seg != -1) 53 | segments[seg] = sx; 54 | 55 | //allSegs++; 56 | } 57 | 58 | uint8_t ledStates; 59 | 60 | void readLEDStates() { 61 | ledStates = 0; 62 | 63 | if (bitRead(buf, 0) == 0) bitSet(ledStates, 0); //Power 64 | if (bitRead(buf, 10) == 0) bitSet(ledStates, 1); //Bubbles 65 | if (bitRead(buf, 9) == 0) bitSet(ledStates, 2); //Heater A 66 | 67 | // if (bitRead(buf, 0) == 1) bitSet(ledStates, 3); // 68 | // if (bitRead(buf, 0) == 1) bitSet(ledStates, 4); // 69 | } 70 | 71 | void writeButtonPress() { 72 | 73 | } 74 | 75 | String ledstat() { 76 | String message = ""; 77 | message += "Power: "; 78 | message += (bitRead(ledStates, 0) == 1)?"ON" : "OFF"; 79 | message += "\n"; 80 | message += "Bubbles: "; 81 | message += (bitRead(ledStates, 1) == 1)?"ON" : "OFF"; 82 | message += "\n"; 83 | message += "Heater A: "; 84 | message += (bitRead(ledStates, 2) == 1)?"ON" : "OFF"; 85 | message += "\n"; 86 | 87 | return message; 88 | } 89 | 90 | void handleRoot() { 91 | server.send(200, "text/plain", "hello from esp8266!"); 92 | } 93 | 94 | void handleNotFound() { 95 | String message = "File Not Found\n\n"; 96 | message += "URI: "; 97 | message += server.uri(); 98 | message += "\nMethod: "; 99 | message += (server.method() == HTTP_GET) ? "GET" : "POST"; 100 | message += "\nArguments: "; 101 | message += server.args(); 102 | message += "\n"; 103 | for (uint8_t i = 0; i < server.args(); i++) { 104 | message += " " + server.argName(i) + ": " + server.arg(i) + "\n"; 105 | } 106 | server.send(404, "text/plain", message); 107 | } 108 | 109 | /******************************************************************************************/ 110 | //16-Bit Shift Register 111 | 112 | const int CLK = 13; 113 | const int LAT = 12; 114 | const int DAT = 14; 115 | 116 | uint8_t clkCount = 0; 117 | 118 | bool outputMode = false; 119 | bool outputEnabled = false; 120 | 121 | //Just clock the data bits into the buffer 122 | void ICACHE_RAM_ATTR handleClock() { 123 | clkCount++; 124 | buf = buf << 1; //Shift buffer along 125 | if (bitRead(*GPIO_IN, DAT) == 1) bitSet(buf, 0); //Flip data bit in buffer if needed. 126 | } 127 | 128 | // 129 | void ICACHE_RAM_ATTR handleLatch() { 130 | if (bitRead(*GPIO_IN, DAT) == 0) { 131 | //pinMode(DAT, INPUT); 132 | } else { 133 | if(flushSync) { 134 | 135 | /* 136 | * Filter 137 | * Heater 138 | * ?FC 139 | * Down 140 | * Bubble 141 | * Power 142 | * ?Up 143 | * ? 144 | */ 145 | 146 | 147 | //Expensive? 148 | /*if ((buf | 0x100) == 0xFFFD) {} 149 | if ((buf | 0x100) == 0x7FFF) {} 150 | if ((buf | 0x100) == 0xEFFF) {} 151 | if ((buf | 0x100) == 0xFF7F) {} 152 | if ((buf | 0x100) == 0xFFF7) {} 153 | if ((buf | 0x100) == 0xFEFF) { 154 | //if (bitRead(ledStates, 0) == 0) { 155 | //pinMode(DAT, OUTPUT); 156 | //digitalWrite(DAT, LOW); 157 | //} 158 | } 159 | if ((buf | 0x100) == 0xDFFF) {} 160 | if ((buf | 0x100) == 0xBFFE) {}*/ 161 | 162 | /* 163 | if (bitRead(buf, 1) == 0)) {} 164 | if (bitRead(buf, 15) == 0)) {} 165 | if (bitRead(buf, 12) == 0)) {} 166 | if (bitRead(buf, 7) == 0)) {} 167 | if (bitRead(buf, 3) == 0)) {} 168 | if (bitRead(buf, 10) == 0)) {} 169 | if (bitRead(buf, 13) == 0)) {} 170 | if (bitRead(buf, 0) == 0)) {} 171 | */ 172 | 173 | 174 | /*if (btnCount > 7) { 175 | btnSync = false; 176 | btnCount = 0; 177 | } 178 | 179 | if (btnSync) { 180 | if (bitRead(btnRequest,btnCount) == 1 && clkCount == 8) { 181 | //pinMode(DAT, OUTPUT); 182 | //digitalWrite(DAT, HIGH); 183 | //bitSet(*GPIO_OUT, DAT); 184 | bitClear(btnRequest,btnCount); 185 | Serial.println(btnCount); 186 | } else { 187 | pinMode(DAT, INPUT); 188 | } 189 | btnCount++; 190 | }*/ 191 | if (clkCount == 16) { 192 | //Decode display if valid 193 | if (bitRead(buf, 6) == 0) readSegment(0); 194 | if (bitRead(buf, 5) == 0) readSegment(1); 195 | if (bitRead(buf, 11) == 0) readSegment(2); 196 | if (bitRead(buf, 2) == 0) readSegment(3); 197 | if (bitRead(buf, 14) == 0) readLEDStates(); 198 | } 199 | 200 | flushSync = false; //((buf | 0xF00) == 0xFFFF); //If idle, we can use to mark a sync. 201 | buf = 0; 202 | clkCount = 0; 203 | 204 | // if (allSegs > 4) { 205 | // allSegs = 0; 206 | //detachInterrupt(digitalPinToInterrupt(CLK)); 207 | // detachInterrupt(digitalPinToInterrupt(LAT)); 208 | // } 209 | } else { 210 | if ((buf | 0xF00) == 0xFFFF) { //If idle, we can use to mark a sync. 211 | flushSync = true; 212 | buf = 0; 213 | clkCount = 0; 214 | } 215 | } 216 | } 217 | } 218 | 219 | //0xFEFF Suggests Idle 220 | //0xFFFF Suggests Button is pressed 221 | 222 | /******************************************************************************************/ 223 | 224 | void initSegs() { 225 | attachInterrupt(digitalPinToInterrupt(CLK), handleClock, RISING); 226 | attachInterrupt(digitalPinToInterrupt(LAT), handleLatch, CHANGE); 227 | } 228 | 229 | void setup() { 230 | Serial.begin(115200); 231 | Serial.println(""); 232 | 233 | WiFi.mode(WIFI_STA); 234 | WiFi.begin(ssid, password); 235 | 236 | int dotCount; 237 | while (WiFi.status() != WL_CONNECTED) { 238 | Serial.print("."); 239 | delay(250); 240 | if (dotCount > 1) { 241 | dotCount = -1; 242 | Serial.print("\b\b\b"); 243 | } 244 | dotCount++; 245 | } 246 | Serial.println(""); 247 | Serial.println("Intex Spa WiFi Controller"); 248 | Serial.print("IP address: "); 249 | Serial.println(WiFi.localIP()); 250 | 251 | ArduinoOTA.setHostname("INTEX_Spa"); 252 | WiFi.hostname("INTEX_Spa"); 253 | MDNS.begin("INTEX_Spa"); 254 | 255 | ArduinoOTA.onStart([]() { 256 | Serial.println("Start"); 257 | }); 258 | ArduinoOTA.onEnd([]() { 259 | Serial.println("\nEnd"); 260 | }); 261 | ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { 262 | Serial.printf("Progress: %u%%\r", (progress / (total / 100))); 263 | }); 264 | ArduinoOTA.onError([](ota_error_t error) { 265 | Serial.printf("Error[%u]: ", error); 266 | if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed"); 267 | else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed"); 268 | else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed"); 269 | else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed"); 270 | else if (error == OTA_END_ERROR) Serial.println("End Failed"); 271 | }); 272 | ArduinoOTA.begin(); 273 | 274 | server.on("/", handleRoot); 275 | 276 | server.on("/initsegs", []() { 277 | initSegs(); 278 | server.send(200, "text/plain", "OK"); 279 | }); 280 | 281 | server.on("/reboot", []() { 282 | server.send(200, "text/plain", "Rebooting..."); 283 | ESP.reset(); 284 | }); 285 | 286 | server.on("/digits", []() { 287 | String message = segments; 288 | message += "\n"; 289 | message += ledstat(); 290 | server.send(200, "text/plain", message); 291 | }); 292 | 293 | server.onNotFound(handleNotFound); 294 | 295 | server.begin(); 296 | 297 | segments[0] = '0'; 298 | segments[1] = '0'; 299 | segments[2] = '0'; 300 | segments[3] = 'C'; 301 | segments[4] = '\0'; 302 | 303 | //Configure shift register 304 | pinMode(CLK, INPUT); 305 | pinMode(LAT, INPUT); 306 | pinMode(DAT, INPUT); 307 | 308 | attachInterrupt(digitalPinToInterrupt(CLK), handleClock, RISING); 309 | attachInterrupt(digitalPinToInterrupt(LAT), handleLatch, CHANGE); 310 | //attachInterrupt(digitalPinToInterrupt(LAT), handleLatch, RISING); 311 | } 312 | 313 | uint32_t mLastTime = 0; 314 | uint32_t mTimeSeconds = 0; 315 | 316 | void loop() { 317 | ArduinoOTA.handle(); 318 | server.handleClient(); 319 | MDNS.update(); 320 | 321 | yield(); 322 | } 323 | --------------------------------------------------------------------------------