├── data ├── caravan.png ├── favicon.ico ├── index.html ├── style.css └── main.js ├── Images ├── Hotspot.jpg ├── 3D Wood Case.jpg ├── Screenshot_01.jpg ├── Screenshot_02.jpg ├── Caravan_Leveler.jpg └── Leveler Power Switch.jpg ├── 3D Case ├── CampingLevelerBox.stl ├── CampingLevelerLid.stl ├── Camping Leveler - Box.FCStd └── Camping Leveler - Lid.FCStd ├── DeepPass.ino ├── OTA.ino ├── Valuation.ino ├── Level.ino ├── Caravan_Leveler.ino ├── Data.ino ├── WebFile.ino ├── Helper.ino ├── README.md └── Webserver.ino /data/caravan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HerrRiebmann/Caravan_Leveler/HEAD/data/caravan.png -------------------------------------------------------------------------------- /data/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HerrRiebmann/Caravan_Leveler/HEAD/data/favicon.ico -------------------------------------------------------------------------------- /Images/Hotspot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HerrRiebmann/Caravan_Leveler/HEAD/Images/Hotspot.jpg -------------------------------------------------------------------------------- /Images/3D Wood Case.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HerrRiebmann/Caravan_Leveler/HEAD/Images/3D Wood Case.jpg -------------------------------------------------------------------------------- /Images/Screenshot_01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HerrRiebmann/Caravan_Leveler/HEAD/Images/Screenshot_01.jpg -------------------------------------------------------------------------------- /Images/Screenshot_02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HerrRiebmann/Caravan_Leveler/HEAD/Images/Screenshot_02.jpg -------------------------------------------------------------------------------- /Images/Caravan_Leveler.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HerrRiebmann/Caravan_Leveler/HEAD/Images/Caravan_Leveler.jpg -------------------------------------------------------------------------------- /3D Case/CampingLevelerBox.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HerrRiebmann/Caravan_Leveler/HEAD/3D Case/CampingLevelerBox.stl -------------------------------------------------------------------------------- /3D Case/CampingLevelerLid.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HerrRiebmann/Caravan_Leveler/HEAD/3D Case/CampingLevelerLid.stl -------------------------------------------------------------------------------- /Images/Leveler Power Switch.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HerrRiebmann/Caravan_Leveler/HEAD/Images/Leveler Power Switch.jpg -------------------------------------------------------------------------------- /3D Case/Camping Leveler - Box.FCStd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HerrRiebmann/Caravan_Leveler/HEAD/3D Case/Camping Leveler - Box.FCStd -------------------------------------------------------------------------------- /3D Case/Camping Leveler - Lid.FCStd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HerrRiebmann/Caravan_Leveler/HEAD/3D Case/Camping Leveler - Lid.FCStd -------------------------------------------------------------------------------- /DeepPass.ino: -------------------------------------------------------------------------------- 1 | //Original from combie 2 | //https://forum.arduino.cc/index.php?topic=648602.msg4374622#msg4374622 3 | template 4 | class DeepPass { 5 | private: 6 | const double factor; 7 | double average; 8 | 9 | public: 10 | DeepPass(const double factor): factor(factor), average(0) {} 11 | 12 | void setInitial(double value) { 13 | average = value; 14 | } 15 | 16 | DataType processValue(DataType value) { 17 | average *= 1.0 - factor; 18 | average += factor * value; 19 | return average; 20 | } 21 | 22 | DataType operator= (DataType value) { 23 | return processValue(value); 24 | } 25 | 26 | DataType operator() (DataType value) { 27 | return processValue(value); 28 | } 29 | 30 | DataType operator() () const 31 | { 32 | return average; 33 | } 34 | 35 | operator DataType() const 36 | { 37 | return average; 38 | } 39 | }; 40 | 41 | // 1.0/5.0 is mostly like an array with 5 lines for average measurment 42 | DeepPass deepPassX(1.0 / 5.0); 43 | DeepPass deepPassY(1.0 / 5.0); 44 | // ------------ 45 | 46 | void InitDeepPass() 47 | { 48 | deepPassX.setInitial(0); 49 | deepPassY.setInitial(0); 50 | } 51 | -------------------------------------------------------------------------------- /OTA.ino: -------------------------------------------------------------------------------- 1 | void setupOTA(){ 2 | ArduinoOTA.setHostname(deviceName); 3 | ArduinoOTA.setPassword((const char *)"SportAndFun"); 4 | //Default Port 5 | ArduinoOTA.setPort(1337); 6 | 7 | ArduinoOTA.onStart([]() { 8 | String type; 9 | if (ArduinoOTA.getCommand() == U_FLASH) 10 | type = "sketch"; 11 | else // U_SPIFFS 12 | type = "filesystem"; 13 | 14 | // NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end() 15 | Serial.println("Start updating " + type); 16 | }); 17 | ArduinoOTA.onEnd([]() { 18 | Serial.println("\nEnd"); 19 | }); 20 | ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { 21 | Serial.printf("Progress: %u%%\r", (progress / (total / 100))); 22 | }); 23 | ArduinoOTA.onError([](ota_error_t error) { 24 | Serial.printf("Error[%u]: ", error); 25 | if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed"); 26 | else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed"); 27 | else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed"); 28 | else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed"); 29 | else if (error == OTA_END_ERROR) Serial.println("End Failed"); 30 | }); 31 | ArduinoOTA.begin(); 32 | } 33 | 34 | -------------------------------------------------------------------------------- /Valuation.ino: -------------------------------------------------------------------------------- 1 | int minValuationX = 0; 2 | int minValuationY = 0; 3 | int maxValuationX = 0; 4 | int maxValuationY = 0; 5 | 6 | void Valutation() { 7 | if (!accelInitialized) 8 | return; 9 | 10 | int x = accel.getX(); 11 | int y = accel.getY(); 12 | //-90 -> +90 13 | //-432 -> 109 / -287 -> 247 14 | //541 / 533 15 | //Divide by 2 16 | 17 | if (x > maxValuationX) 18 | maxValuationX = x; 19 | if (x < minValuationX) 20 | minValuationX = x; 21 | 22 | if (y > maxValuationY) 23 | maxValuationY = y; 24 | if (y < minValuationY) 25 | minValuationY = y; 26 | 27 | valuationX = (maxValuationX - minValuationX) / 2; 28 | valuationY = (maxValuationY - minValuationY) / 2; 29 | 30 | if (valuationX < 0) 31 | valuationX *= -1; 32 | 33 | if (valuationY < 0) 34 | valuationY *= -1; 35 | 36 | 37 | Serial.print("X: "); 38 | Serial.print(minValuationX); 39 | Serial.print(" - "); 40 | Serial.print(maxValuationX); 41 | Serial.print(" ("); 42 | Serial.print(valuationX); 43 | Serial.print(")"); 44 | Serial.print(" Y: "); 45 | Serial.print(minValuationY); 46 | Serial.print(" - "); 47 | Serial.print(maxValuationY); 48 | Serial.print(" ("); 49 | Serial.print(valuationX); 50 | Serial.println(")"); 51 | 52 | } 53 | 54 | -------------------------------------------------------------------------------- /Level.ino: -------------------------------------------------------------------------------- 1 | void getLevel() { 2 | if (!accelInitialized) 3 | return; 4 | 5 | if(valuationActive){ 6 | Valutation(); 7 | return; 8 | } 9 | 10 | int x = accel.getX(); 11 | int y = accel.getY(); 12 | int z = accel.getZ(); 13 | //-90 -> +90 14 | //-432 -> 109 / -287 -> 247 15 | //541 / 533 16 | //Divide by 2 17 | 18 | //Zero 19 | //-150 / -25 20 | levelX = map(x, (valuationX - calibrationX) * -1, (valuationX + calibrationX), -900, 900); 21 | levelY = map(y, (valuationY - calibrationY) * -1, (valuationY + calibrationY), -900, 900); 22 | 23 | Serial.print("X: "); 24 | Serial.print(levelX); 25 | Serial.print(" Y: "); 26 | Serial.print(levelY); 27 | Serial.print("\t("); 28 | Serial.print(x); 29 | Serial.print(" / "); 30 | Serial.print(y); 31 | Serial.print(" / "); 32 | Serial.print(z); 33 | Serial.print(")"); 34 | levelX = deepPassX(levelX); 35 | levelY = deepPassY(levelY); 36 | Serial.print("\tDeepPass "); 37 | Serial.print(levelX); 38 | Serial.print(" / "); 39 | Serial.println(levelY); 40 | } 41 | 42 | void CalibrateLevel() { 43 | //Get current values, store as "Zero-levelled" and write to eeprom 44 | calibrationX = accel.getX(); 45 | calibrationY = accel.getY(); 46 | 47 | Serial.print("Calibrated to X: "); 48 | Serial.print(calibrationX); 49 | Serial.print(" Y: "); 50 | Serial.println(calibrationY); 51 | StoreLevel(); 52 | } 53 | -------------------------------------------------------------------------------- /Caravan_Leveler.ino: -------------------------------------------------------------------------------- 1 | //Gyro ADXL345 2 | #include 3 | #include 4 | /* Assign a unique ID to this sensor at the same time */ 5 | Adafruit_ADXL345_Unified accel = Adafruit_ADXL345_Unified(12345); 6 | 7 | //Filesystem 8 | #include "SPIFFS.h" 9 | #include "EEPROM.h" 10 | #define EEPROM_LEVEL_X 0 11 | #define EEPROM_LEVEL_Y 2 12 | #define EEPROM_VALUATION_X 4 13 | #define EEPROM_VALUATION_Y 6 14 | #define EEPROM_LEVEL_THRESHOLD 8 15 | #define EEPROM_LEVEL_INVERTATION 9 16 | #define EEPROM_LEVEL_ACCESSPOINT 10 17 | 18 | //Webserver 19 | #include 20 | #include 21 | const char* ssid = "FindMichDoch2"; 22 | const char* password = "5AF714A8B"; 23 | 24 | WebServer webServer(80); 25 | 26 | #include 27 | const byte DNS_PORT = 53; 28 | DNSServer dnsServer; 29 | 30 | //Over the Air Update 31 | #include 32 | 33 | const char deviceName[] = "Sport&Fun Leveler"; 34 | 35 | bool accelInitialized = false; 36 | int levelX = 0; 37 | int levelY = 0; 38 | int calibrationX = -150; 39 | int calibrationY = -25; 40 | int valuationX = 271; 41 | int valuationY = 267; 42 | bool valuationActive = false; 43 | uint8_t levelThreshold = 5; 44 | bool invertAxis = false; 45 | bool useAcessPointMode = false; 46 | 47 | long lastMillis = 0; 48 | long lastMillisClientAvailable = 0; 49 | 50 | void setup() { 51 | SerialBegin(); 52 | Adxl345Begin(); 53 | EepromBegin(); 54 | 55 | LoadData(); 56 | 57 | SpiffsBegin(); 58 | WiFiBegin(); 59 | setupOTA(); 60 | } 61 | 62 | void loop() { 63 | //WebServer 64 | webServer.handleClient(); 65 | 66 | //DNS 67 | dnsServer.processNextRequest(); 68 | 69 | //OTA 70 | ArduinoOTA.handle(); 71 | 72 | if(millis() - lastMillis > 200){ 73 | //Only update when someone is listening: 74 | if(millis() - lastMillisClientAvailable < 1000) 75 | getLevel(); 76 | lastMillis = millis(); 77 | } 78 | } 79 | 80 | -------------------------------------------------------------------------------- /Data.ino: -------------------------------------------------------------------------------- 1 | void LoadData(){ 2 | LoadLevel(); 3 | LoadValuation(); 4 | LoadLevelThreshold(); 5 | LoadInvertation(); 6 | LoadAP(); 7 | } 8 | void LoadLevel() { 9 | calibrationX = readIntFromEEPROM(EEPROM_LEVEL_X); 10 | calibrationY = readIntFromEEPROM(EEPROM_LEVEL_Y); 11 | 12 | if (calibrationX == 0xffff || calibrationY == 0xffff) { 13 | calibrationX = -150; 14 | calibrationY = -125; 15 | StoreLevel(); 16 | } 17 | Serial.print(F("Loaded X: ")); 18 | Serial.print(calibrationX); 19 | Serial.print(F(" Y: ")); 20 | Serial.println(calibrationY); 21 | } 22 | 23 | void LoadValuation() { 24 | valuationX = readIntFromEEPROM(EEPROM_VALUATION_X); 25 | valuationY = readIntFromEEPROM(EEPROM_VALUATION_Y); 26 | 27 | if (valuationX == 0xffff || valuationY == 0xffff) { 28 | valuationX = 271; 29 | valuationY = 267; 30 | StoreLevelValuation(); 31 | } 32 | Serial.print(F("Loaded Valuation: ")); 33 | Serial.print(F(" X: ")); 34 | Serial.print(valuationX); 35 | Serial.print(F(" Y: ")); 36 | Serial.println(valuationY); 37 | } 38 | void LoadLevelThreshold() { 39 | levelThreshold = EEPROM.read(EEPROM_LEVEL_THRESHOLD); 40 | if (levelThreshold == 0xff || levelThreshold == 0) 41 | levelThreshold = 5; 42 | Serial.print(F("Loaded Threshold: ")); 43 | Serial.println(levelThreshold); 44 | } 45 | void LoadInvertation() { 46 | invertAxis = EEPROM.read(EEPROM_LEVEL_INVERTATION); 47 | Serial.print(F("Loaded Inverted Axis: ")); 48 | Serial.println(invertAxis); 49 | } 50 | 51 | void LoadAP() { 52 | useAcessPointMode = EEPROM.read(EEPROM_LEVEL_ACCESSPOINT); 53 | Serial.print(F("Loaded AccessPoint: ")); 54 | Serial.println(useAcessPointMode); 55 | } 56 | 57 | void StoreLevel() { 58 | writeIntIntoEEPROM(EEPROM_LEVEL_X, calibrationX); 59 | writeIntIntoEEPROM(EEPROM_LEVEL_Y, calibrationY); 60 | EEPROM.commit(); 61 | } 62 | 63 | void StoreLevelValuation() { 64 | writeIntIntoEEPROM(EEPROM_VALUATION_X, valuationX); 65 | writeIntIntoEEPROM(EEPROM_VALUATION_Y, valuationY); 66 | EEPROM.commit(); 67 | } 68 | void StoreLevelThreshold() { 69 | EEPROM.write(EEPROM_LEVEL_THRESHOLD, levelThreshold); 70 | EEPROM.commit(); 71 | } 72 | void StoreInvertation() { 73 | EEPROM.write(EEPROM_LEVEL_INVERTATION, invertAxis); 74 | EEPROM.commit(); 75 | } 76 | 77 | void StoreAP() { 78 | EEPROM.write(EEPROM_LEVEL_ACCESSPOINT, useAcessPointMode); 79 | EEPROM.commit(); 80 | } 81 | -------------------------------------------------------------------------------- /WebFile.ino: -------------------------------------------------------------------------------- 1 | String getContentType(String filename) { // convert the file extension to the MIME type 2 | if (filename.endsWith(".html")) 3 | return "text/html"; 4 | else if (filename.endsWith(".css")) 5 | return "text/css"; 6 | else if (filename.endsWith(".js")) 7 | return "application/javascript"; 8 | else if (filename.endsWith(".ico")) 9 | return "image/x-icon"; 10 | else if (filename.endsWith(".png")) 11 | return "image/png"; 12 | else if (filename.endsWith(".jpg")) 13 | return "image/jpg"; 14 | else if (filename.endsWith(".gz")) 15 | return "application/x-gzip"; 16 | return "text/plain"; 17 | } 18 | 19 | void handleFileRead() { 20 | String path = webServer.uri(); 21 | Serial.println("Handle FileRead: " + path); 22 | 23 | // If a folder is requested, send index.html 24 | if (path.endsWith("/")) 25 | path.concat("index.html"); 26 | 27 | // If request is captive request, followed with a GUID 28 | if(path.startsWith("/generate_204")){ 29 | redirect(); 30 | return; 31 | } 32 | 33 | if (SPIFFS.exists(path)) { 34 | File file = SPIFFS.open(path, "r"); 35 | String fileSize = String(file.size()); 36 | //Check File "Version" (Size) is still the same, otherwise sumbit it 37 | if (ProcessETag(fileSize.c_str())) { 38 | file.close(); 39 | return; 40 | } 41 | size_t sent = webServer.streamFile(file, getContentType(path)); 42 | file.close(); 43 | Serial.print(String("\tSent file: ") + path); 44 | Serial.println(" " + String(sent)); 45 | return; 46 | } 47 | 48 | handleNotFound(); 49 | Serial.println(String("\tFile Not Found: ") + path); 50 | } 51 | 52 | File fsUploadFile; 53 | void handle_fileupload() { 54 | HTTPUpload& upload = webServer.upload(); 55 | 56 | if (upload.status == UPLOAD_FILE_START) { 57 | String filename = upload.filename; 58 | if (!filename.startsWith("/")) 59 | filename = "/" + filename; 60 | 61 | Serial.print("handleFileUpload: "); 62 | Serial.println(filename); 63 | 64 | if (SPIFFS.exists(filename)) { 65 | Serial.println(F("\tFile Deleted")); 66 | SPIFFS.remove(filename); 67 | } 68 | 69 | fsUploadFile = SPIFFS.open(filename, "w"); 70 | filename = String(); 71 | } else if (upload.status == UPLOAD_FILE_WRITE) { 72 | if (fsUploadFile) 73 | fsUploadFile.write(upload.buf, upload.currentSize); 74 | } else if (upload.status == UPLOAD_FILE_END) { 75 | if (fsUploadFile) { 76 | fsUploadFile.close(); 77 | Serial.print("handleFileUpload Size: "); 78 | Serial.println(upload.totalSize); 79 | // Redirect the client to the root page 80 | webServer.sendHeader("Location", "/"); 81 | webServer.send(303); 82 | } else { 83 | webServer.send(500, "text/plain", "500: couldn't create file"); 84 | } 85 | } 86 | } 87 | 88 | -------------------------------------------------------------------------------- /data/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Caravan - Leveler 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 16 |
17 |

Knaus Sport & Fun ⚖️

18 |
19 | 20 |

21 | 22 |
23 | 24 | 25 |

26 | Settings 27 |
28 | 29 | 57 |
58 | 59 |
60 | 61 | -------------------------------------------------------------------------------- /Helper.ino: -------------------------------------------------------------------------------- 1 | void SerialBegin() { 2 | Serial.begin(115200); 3 | Serial.println("Start Leveler"); 4 | Serial.println(""); 5 | } 6 | 7 | void Adxl345Begin() { 8 | accelInitialized = accel.begin(); 9 | if (!accelInitialized) 10 | Serial.println("Ooops, no ADXL345 detected ... Check your wiring!"); 11 | else 12 | accel.setRange(ADXL345_RANGE_2_G); 13 | } 14 | 15 | void SpiffsBegin() { 16 | if (!SPIFFS.begin(true)) 17 | Serial.println("An Error has occurred while mounting SPIFFS"); 18 | } 19 | 20 | void EepromBegin() { 21 | if (!EEPROM.begin(11)) 22 | Serial.println("An Error has occurred while initializing EEPROM"); 23 | } 24 | 25 | boolean isIp(String str) { 26 | Serial.print("-IsIP: "); 27 | for (size_t i = 0; i < str.length(); i++) { 28 | int c = str.charAt(i); 29 | if (c != '.' && (c < '0' || c > '9')) { 30 | Serial.println("false"); 31 | return false; 32 | } 33 | } 34 | Serial.println("true"); 35 | return true; 36 | } 37 | 38 | bool ProcessETag(const char* ETag) { 39 | for (int i = 0; i < webServer.headers(); i++) { 40 | if (webServer.headerName(i).compareTo(F("If-None-Match")) == 0) 41 | if (webServer.header(i).compareTo(ETag) == 0) { 42 | webServer.send(304, "text/plain", F("Not Modified")); 43 | Serial.println(String(F("\t")) + webServer.headerName(i) + F(": ") + webServer.header(i)); 44 | return true; 45 | } 46 | } 47 | webServer.sendHeader("ETag", ETag); 48 | webServer.sendHeader("Cache-Control", "public"); 49 | return false; 50 | } 51 | 52 | void ProcessSetupArguments() { 53 | // /setup?x=123&y=321&inv=0&ap=1&t=10 54 | 55 | bool valutationChanged = false; 56 | 57 | for (uint8_t i = 0; i < webServer.args(); i++) { 58 | Serial.println(String(F(" ")) + webServer.argName(i) + F(": ") + webServer.arg(i)); 59 | 60 | if (webServer.argName(i).compareTo(F("vx")) == 0) { 61 | valuationX = webServer.arg(i).toInt(); 62 | valutationChanged = true; 63 | } 64 | 65 | if (webServer.argName(i).compareTo(F("vy")) == 0) { 66 | valuationY = webServer.arg(i).toInt(); 67 | valutationChanged = true; 68 | } 69 | 70 | if (webServer.argName(i).compareTo(F("inv")) == 0) { 71 | invertAxis = webServer.arg(i) == "1"; 72 | StoreInvertation(); 73 | } 74 | 75 | if (webServer.argName(i).compareTo(F("ap")) == 0) { 76 | useAcessPointMode = webServer.arg(i) == "1"; 77 | StoreAP(); 78 | } 79 | 80 | if (webServer.argName(i).compareTo(F("t")) == 0) { 81 | int j = webServer.arg(i).toInt(); 82 | if (j > 0 && j <= 90) { 83 | levelThreshold = j; 84 | StoreLevelThreshold(); 85 | } 86 | } 87 | if (valutationChanged) 88 | StoreLevelValuation(); 89 | } 90 | } 91 | 92 | String toStringIp(IPAddress ip) { 93 | Serial.println("IptoString"); 94 | String res = ""; 95 | for (int i = 0; i < 3; i++) { 96 | res += String((ip >> (8 * i)) & 0xFF) + "."; 97 | } 98 | res += String(((ip >> 8 * 3)) & 0xFF); 99 | return res; 100 | } 101 | 102 | void writeIntIntoEEPROM(int address, int16_t number) { 103 | EEPROM.write(address, number >> 8); 104 | EEPROM.write(address + 1, number & 0xFF); 105 | } 106 | 107 | int16_t readIntFromEEPROM(int address) { 108 | return (EEPROM.read(address) << 8) + EEPROM.read(address + 1); 109 | } 110 | 111 | String GetCustomText(){ 112 | //Return any text. Pipes ( | ) are not allowed! 113 | //return String("Battery: 12,34V"); 114 | return String(""); 115 | } 116 | -------------------------------------------------------------------------------- /data/style.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-family: Helvetica; 3 | display: inline-block; 4 | margin: 0 auto; 5 | text-align: center; 6 | color: white; 7 | } 8 | 9 | h1 { 10 | color: #008CBA; 11 | padding: 2vh; 12 | text-shadow: 2px 2px #e1e4e8; 13 | } 14 | 15 | p { 16 | font-size: 1.0rem; 17 | } 18 | 19 | input { 20 | text-align: center; 21 | } 22 | 23 | button { 24 | cursor: pointer; 25 | background-color: #008CBA; 26 | border: none; 27 | border-radius: 4px; 28 | color: #c4d9f5; 29 | padding: 15px 32px; 30 | text-align: center; 31 | text-decoration: none; 32 | display: inline-block; 33 | font-size: 16px; 34 | box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); 35 | } 36 | 37 | button:hover { 38 | color: white; 39 | box-shadow: 0 12px 16px 0 rgba(0, 0, 0, 0.24), 0 17px 50px 0 rgba(0, 0, 0, 0.19); 40 | } 41 | 42 | canvas { 43 | box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); 44 | border-radius: 2px; 45 | } 46 | 47 | .dotX { 48 | height: 36px; 49 | width: 30px; 50 | background-color: #bfbfbf; 51 | border-radius: 50%; 52 | border: 2px solid #9C9898; 53 | box-shadow: 2px 2px 5px #222; 54 | display: inline-block; 55 | position: fixed; 56 | top: 240px; 57 | margin: 0 -40px; 58 | } 59 | 60 | .dotY { 61 | height: 30px; 62 | width: 36px; 63 | background-color: #bfbfbf; 64 | border-radius: 50%; 65 | border: 2px solid #9C9898; 66 | box-shadow: 2px 2px 5px #222; 67 | display: inline-block; 68 | position: fixed; 69 | top: 370px; 70 | margin: 0 -40px; 71 | } 72 | 73 | .card { 74 | box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2); 75 | transition: 0.3s; 76 | background: DarkGray; 77 | width: 350px; 78 | margin: auto; 79 | background-image: url('caravan.png'); 80 | background-repeat: no-repeat; 81 | background-position: center 40px; 82 | } 83 | 84 | .slider { 85 | -webkit-appearance: none; 86 | width: 80%; 87 | height: 15px; 88 | border-radius: 5px; 89 | background: #d3d3d3; 90 | outline: none; 91 | opacity: 0.7; 92 | -webkit-transition: .2s; 93 | transition: opacity .2s; 94 | } 95 | 96 | .slider::-webkit-slider-thumb { 97 | -webkit-appearance: none; 98 | appearance: none; 99 | width: 25px; 100 | height: 25px; 101 | border-radius: 50%; 102 | background: #008CBA; 103 | cursor: pointer; 104 | } 105 | 106 | .slider::-moz-range-thumb { 107 | width: 25px; 108 | height: 25px; 109 | border-radius: 50%; 110 | background: #008CBA; 111 | cursor: pointer; 112 | } 113 | 114 | .modal { 115 | display: none; 116 | position: fixed; 117 | z-index: 1; 118 | top: 60px; 119 | width: 350px; 120 | height: 450px; 121 | overflow: auto; 122 | background-color: rgba(0, 0, 0, 0.5); 123 | } 124 | 125 | .modal-content { 126 | background-color: #465c6b; 127 | margin: 5% auto; 128 | padding: 10px; 129 | border: 1px solid #888; 130 | width: 80%; 131 | } 132 | 133 | .close { 134 | color: #eee; 135 | float: right; 136 | font-size: 40px; 137 | font-weight: bold; 138 | } 139 | 140 | .close:hover, 141 | .close:focus { 142 | color: black; 143 | text-decoration: none; 144 | cursor: pointer; 145 | } 146 | .upload-btn-wrapper { 147 | position: relative; 148 | overflow: hidden; 149 | display: inline-block; 150 | } 151 | .upload-btn-wrapper input[type=file] { 152 | font-size: 100px; 153 | position: absolute; 154 | left: 0; 155 | top: 0; 156 | opacity: 0; 157 | } 158 | .buttonRec { 159 | cursor: pointer; 160 | background-color: #008CBA; 161 | border: none; 162 | border-radius: 16px; 163 | color: #c4d9f5; 164 | padding: 10px 10px; 165 | text-align: center; 166 | text-decoration: none; 167 | display: inline-block; 168 | font-size: 12px; 169 | box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); 170 | } 171 | .dimmer { 172 | background:#000; 173 | opacity:0.5; 174 | position:fixed; 175 | top:0; 176 | left:0; 177 | width:100%; 178 | height:100%; 179 | display:none; 180 | z-index:100; 181 | } 182 | .customLabel { 183 | text-align: left; 184 | float: left; 185 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Caravan Leveler 2 | ESP32 digital water level bubble with an ADXL345 Accelerometer 3 | 4 | ![Caravan Leveler](/Images/Caravan_Leveler.jpg) 5 | 6 | An ESP WROOM32 will create a hotspot with a captive portal. 7 | This captive portal will force the phone to open up a login-site (_which represents the water level_). 8 | 9 | ## Contents 10 | * [Usage](#usage) 11 | * [Video](#video) 12 | * [3D Case](#3D-Case) 13 | * [Things](#Things) 14 | * [HardwareComponents](#HardwareComponents) 15 | * [Wiring](#Wiring) 16 | * [SPIFFS Upload](#SPIFFS-Upload) 17 | * [Libraries](#Libraries) 18 | * [OTA (Over the Air Update)](#OTA-Over-the-Air-Update) 19 | * [Power switch](#Power-Switch) 20 | * [Compatibility](#Compatibility) 21 | * [Disclaimer](#Disclaimer) 22 | 23 | ### Intention 24 | The intention was, to attach this to my caravan (_Knaus Sport & Fun_) and provide it with 12V on-board voltage. 25 | When arriving at a campingside, the phone will automatically connect to this as a known hotspot, open up the digital leveler and I can happily crank the supports. 26 | 27 | In short: Lazy me doesn´t want to constantly walk to the inside and check the level 🤷 28 | 29 | ## Usage 30 | 31 | Captive Portal (_Android 10_): 32 | 33 | ![Captive Portal](/Images/Screenshot_01.jpg) 34 | 35 | **Calibrate** sets the values to zero 36 | 37 | Settings: 38 | 39 | ![Settings](/Images/Screenshot_02.jpg) 40 | 41 | **Threshold** describes the water bubble maximum value, when it will reach the scales end 42 | 43 | **Invert Axis** swaps X- and Y-Axis 44 | 45 | **Use Accesspoint** or connect to an existing WiFi (_SSID and Pasword required in code_) 46 | 47 | **Valutation** represents the values range from 0° to 90° (_or -90° to 0°_) 48 | 49 | **Rec** starts recording the minimum and maximum values on both axis. Tip the board over to 90 degrees on every side. Afterwards, hit **End** to see those values. 50 | 51 | **Save** will store all values above, to the ESP32 52 | 53 | **Upload** loads a file to the SPIFFS. To overwrite existing files, the filename must be equal (see [/data](https://github.com/HerrRiebmann/Caravan_Leveler/tree/main/data))! 54 | 55 | ## Video 56 | [![](http://img.youtube.com/vi/iUhjaghWvkY/0.jpg)](http://www.youtube.com/watch?v=iUhjaghWvkY "Caravan Leveler") 57 | 58 | ## 3D Case 59 | 3D Printed case with wood filament: 60 | 61 | ![Captive Portal](/Images/3D%20Wood%20Case.jpg) 62 | 63 | **Attention!** The wood filament comes thicker, so you might have to sand parts of the case. That´s not necesary using regular PLA. 64 | 65 | 66 | Hotspot (_Android 4.4.3_): 67 | 68 | ![Hotspot](/Images/Hotspot.jpg) 69 | 70 | ## Things 71 | ### HardwareComponents 72 | * ESP WROOM32 73 | * ADXL345 (_Accelerometer / Gyro_) 74 | * LM2596 (_DC-DC converter_) 75 | * Rocker switch (_15mm x 10mm_) 76 | * 3D Printed Case 77 | 78 | ### Wiring 79 | ESP 32 | ADXL345 80 | ------- | -------- 81 | G22 | SCL 82 | G21 | SDA 83 | 3.3V | VCC 84 | GND | GND 85 | 86 | ESP 32 | LM2596 87 | ------- | -------- 88 | 5V | OUT+ 89 | GND | OUT- 90 | 91 | ### SPIFFS Upload 92 | To upload the HTML, JS and CSS files, I´ve used the [Arduino ESP32 filesystem uploader](https://github.com/me-no-dev/arduino-esp32fs-plugin) 93 | You can find the latest release [here](https://github.com/me-no-dev/arduino-esp32fs-plugin/releases/) and a tutorial on [RandomNerdTutorials](https://randomnerdtutorials.com/install-esp32-filesystem-uploader-arduino-ide/) 94 | 95 | ### Libraries 96 | * Adafruit Unified Sensor (1.0.3) 97 | * Adafruit ADXL345 (1.2.2) 98 | 99 | ### OTA (Over the Air Update) 100 | You should see the ESP32 in Arduino IDE under Tools -> Port -> Network-Interfaces (Sport&Fun Leveler at _IP-Adress_) 101 | For more information see [RandomNerdTutorials](https://randomnerdtutorials.com/esp32-over-the-air-ota-programming/) 102 | 103 | ### Power Switch 104 | The DC converter is connected to the 12V net. So it will only be powered, enabling the boardnet (_as you can see at the start of the video_). 105 | To prevent the converter being powered all the time, standing on a campingside, I´ve added a illuminated momentary switch: 106 | ![Power Switch](/Images/Leveler%20Power%20Switch.jpg) 107 | 19mm momentary blue led switch. Color and various logos can be ordered on well known, chinese express online shop ;) 108 | 109 | Got a relais in between, which disconnects after about 15 minutes, after powering up. 110 | Not easy to find the right one, which immediately closes the relais and opens it after a specific period of time. 111 | Then remain like that, until the powersupply is cut off. A **JK 11** timing relais (V2.0.2021-1) worked great, using a jumper instead of a trigger button. 112 | But not yet constantly installed... 113 | 114 | ## Compatibility 115 | Tested on iOS and Android 4.4.3, 8 and 10. 116 | 117 | (*And all estabished none-mobile Browsers, like Firefox, Edge or Chrome*) 118 | 119 | ### Disclamer 120 | The ADXL345 is **not** a proper device to show exact degrees! It´s an accelerometer, which goal was to measure movements by gravitation. 121 | So all results are just approximation. 122 | -------------------------------------------------------------------------------- /Webserver.ino: -------------------------------------------------------------------------------- 1 | void WiFiBegin() { 2 | //Manually change between WiFi and Accesspoint. AP will be used as a fallback, after 5 seconds 3 | if (useAcessPointMode) 4 | CreateAccessPoint(); 5 | else 6 | ConnectToAccessPoint(); 7 | 8 | webServer.on("/level", handle_level); 9 | webServer.on("/setup", handle_setup); 10 | webServer.on("/calibrate", handle_calibrate); 11 | webServer.on("/valuate", handle_valuation); 12 | webServer.on("/reset", handle_reset); 13 | webServer.on("/upload", HTTP_POST,[](){ webServer.send(200);}, handle_fileupload); 14 | 15 | //Allways redirect to captive portal. Request comes with IP (8.8.8.8) or URL (connectivitycheck.XXX / captive.apple / etc.) 16 | webServer.on("/generate_204", redirect); //Android captive portal. 17 | webServer.on("/fwlink", redirect); //Microsoft captive portal. 18 | 19 | webServer.on("/connecttest.txt", redirect); //www.msftconnecttest.com 20 | webServer.on("/hotspot-detect.html", redirect); //captive.apple.com 21 | 22 | webServer.on("/success.txt", handle_success); //detectportal.firefox.com/sucess.txt 23 | webServer.onNotFound(handleFileRead); 24 | 25 | const char* Headers[] = {"If-None-Match"}; 26 | webServer.collectHeaders(Headers, sizeof(Headers) / sizeof(Headers[0])); 27 | 28 | webServer.begin(); 29 | Serial.println(F("HTTP webServer started")); 30 | delay(100); 31 | } 32 | 33 | void ConnectToAccessPoint() { 34 | WiFi.begin(ssid, password); 35 | 36 | long start = millis(); 37 | while (WiFi.status() != WL_CONNECTED) { 38 | delay(1000); 39 | Serial.print("."); 40 | if (millis() - start > 5000) { 41 | Serial.println("Wifi not found!"); 42 | CreateAccessPoint(); 43 | return; 44 | } 45 | } 46 | Serial.println(); 47 | Serial.println(F("WiFi connected successfully")); 48 | Serial.print(F("Got IP: ")); 49 | Serial.println(WiFi.localIP()); //Show ESP32 IP on serial 50 | } 51 | 52 | void CreateAccessPoint() { 53 | WiFi.disconnect(); 54 | IPAddress local_ip(8, 8, 8, 8); 55 | IPAddress gateway(8, 8, 8, 8); 56 | IPAddress subnet(255, 255, 255, 0); 57 | 58 | WiFi.mode(WIFI_AP); 59 | WiFi.softAP(deviceName); 60 | delay(500); 61 | WiFi.softAPConfig(local_ip, gateway, subnet); 62 | delay(500); 63 | Serial.print(F("AP IP address: ")); 64 | Serial.println(WiFi.softAPIP()); 65 | 66 | /* Setup the DNS webServer redirecting all the domains to the apIP */ 67 | dnsServer.setErrorReplyCode(DNSReplyCode::NoError); 68 | dnsServer.start(DNS_PORT, "*", local_ip); 69 | } 70 | 71 | void handle_root() { 72 | // / 73 | // /index.html 74 | Serial.println(F("Handle Root")); 75 | 76 | PrintIncomingRequest(); 77 | 78 | String path = "/index.html"; 79 | if (!SPIFFS.exists(path)) 80 | return; 81 | 82 | File f = SPIFFS.open(path); 83 | webServer.streamFile(f, "text/html"); 84 | f.close(); 85 | } 86 | 87 | void handle_level() { 88 | // /level 89 | if (!accelInitialized) { 90 | webServer.send(400, "text/plain", "Gyro not initialized!"); 91 | return; 92 | } 93 | 94 | String txt = String(invertAxis ? levelY : levelX); 95 | txt.concat("|"); 96 | txt.concat(String(invertAxis ? levelX * -1 : levelY)); 97 | txt.concat("|"); 98 | txt.concat(String(levelThreshold)); 99 | String customText = GetCustomText(); 100 | if(customText.length() > 0){ 101 | txt.concat("|"); 102 | txt.concat(customText); 103 | } 104 | webServer.send(200, "text/plain", txt); 105 | 106 | lastMillisClientAvailable = millis(); 107 | } 108 | 109 | void handle_setup() { 110 | // /setup 111 | Serial.println(F("Handle Setup")); 112 | 113 | //With arguments: 114 | // /setup?x=123&y=321&inv=0&ap=1 115 | ProcessSetupArguments(); 116 | 117 | String txt = String(accelInitialized); 118 | txt.concat("|"); 119 | txt.concat(String(valuationX)); 120 | txt.concat("|"); 121 | txt.concat(String(valuationY)); 122 | txt.concat("|"); 123 | txt.concat(String(invertAxis)); 124 | txt.concat("|"); 125 | txt.concat(String(useAcessPointMode)); 126 | webServer.send(200, "text/plain", txt); 127 | } 128 | 129 | void handle_calibrate() { 130 | Serial.println(F("Handle Calibration")); 131 | CalibrateLevel(); 132 | String result = "Calibration OK ("; 133 | result.concat(calibrationX); 134 | result.concat("/"); 135 | result.concat(calibrationY); 136 | result.concat(")"); 137 | webServer.send(200, "text/plaint", result); 138 | } 139 | 140 | void handle_valuation() { 141 | Serial.println(F("Handle Valuation")); 142 | String result = "Calibration "; 143 | if(valuationActive) 144 | result.concat("stopped"); 145 | else { 146 | minValuationX = 0; 147 | minValuationY = 0; 148 | maxValuationX = 0; 149 | maxValuationY = 0; 150 | result.concat("started"); 151 | } 152 | valuationActive = !valuationActive; 153 | webServer.send(200, "text/plaint", result); 154 | } 155 | 156 | void handle_reset(){ 157 | LoadData(); 158 | webServer.send(200, "text/plaint", "OK"); 159 | } 160 | 161 | void handleNotFound() { 162 | Serial.println(F("HandleNotFound")); 163 | 164 | PrintIncomingRequest(); 165 | 166 | if (captivePortal()) 167 | return; 168 | String message = F("File Not Found\n\n"); 169 | message += F("URI: "); 170 | message += webServer.uri(); 171 | message += F("\nMethod: "); 172 | message += (webServer.method() == HTTP_GET) ? "GET" : "POST"; 173 | message += F("\nArguments: "); 174 | message += webServer.args(); 175 | message += F("\n"); 176 | 177 | for (uint8_t i = 0; i < webServer.args(); i++) { 178 | message += String(F(" ")) + webServer.argName(i) + F(": ") + webServer.arg(i) + F("\n"); 179 | } 180 | webServer.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); 181 | webServer.sendHeader("Pragma", "no-cache"); 182 | webServer.sendHeader("Expires", "-1"); 183 | webServer.send(404, "text/plain", message); 184 | } 185 | 186 | void handle_success(){ 187 | Serial.println(F("Handle success.txt")); 188 | webServer.send(200, "text/plain", "success"); 189 | } 190 | 191 | boolean captivePortal() { 192 | Serial.print(F("Captive Check: ")); 193 | Serial.println(webServer.hostHeader()); 194 | if (!isIp(webServer.hostHeader())) { 195 | Serial.println("-Request redirected to captive portal"); 196 | redirect(); 197 | return true; 198 | } 199 | return false; 200 | } 201 | 202 | void redirect(){ 203 | webServer.sendHeader("Location", String("http://") + toStringIp(webServer.client().localIP()), true); 204 | webServer.send(302, "text/plain", ""); // Empty content inhibits Content-length header so we have to close the socket ourselves. 205 | webServer.client().stop(); // Stop is needed because we sent no content length 206 | } 207 | 208 | void PrintIncomingRequest(){ 209 | Serial.println(webServer.hostHeader()); 210 | Serial.print(" "); 211 | Serial.println(webServer.uri()); 212 | 213 | for (uint8_t i = 0; i < webServer.args(); i++) 214 | Serial.println(String(F(" ")) + webServer.argName(i) + F(": ") + webServer.arg(i) + F("\n")); 215 | 216 | for (int i = 0; i < webServer.headers(); i++) 217 | Serial.println(String(F("\t")) + webServer.headerName(i) + F(": ") + webServer.header(i)); 218 | } 219 | 220 | -------------------------------------------------------------------------------- /data/main.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var ADXL345_Initialized = true; 3 | var PCVersion = false; 4 | document.addEventListener("DOMContentLoaded", function(){ 5 | DrawLevel("X"); 6 | DrawLevel("Y"); 7 | var slider = document.getElementById("ThresholdSlider"); 8 | slider.oninput = function() { 9 | document.getElementById("SliderValue").innerHTML = this.value; 10 | }; 11 | SetSetup(false); 12 | }); 13 | function GetLevel(){ 14 | if(PCVersion) return; 15 | var oRequest = new XMLHttpRequest(); 16 | var sURL = '/level'; 17 | oRequest.open("GET",sURL,true); 18 | oRequest.onload = function (e) { 19 | if (oRequest.readyState === 4) { 20 | if (oRequest.status === 200) { 21 | var arr = oRequest.responseText.split("|"); 22 | SetLevel(arr[0], arr[1], arr[2]); 23 | if(!ADXL345_Initialized){ 24 | ADXL345_Initialized = true; 25 | DrawLevel("X"); 26 | DrawLevel("Y"); 27 | SetOutput("", false); 28 | } 29 | if(arr.length > 3) 30 | SetCustomText(arr[3]); 31 | } else { 32 | SetLevel(0,0,10); 33 | ADXL345_Initialized = false; 34 | DrawLevel("X"); 35 | DrawLevel("Y"); 36 | SetOutput(oRequest.responseText, true); 37 | } 38 | } 39 | }; 40 | oRequest.onerror = function (e) { 41 | PCVersion = true; 42 | //Fake values on a computer. Referring to the ESP will alert a cross site scripting error 43 | SetLevel(Math.floor(Math.random() * 90) * -1, Math.floor(Math.random() * 90), 10); 44 | SetOutput("PC random values", false); 45 | }; 46 | oRequest.send(null); 47 | } 48 | function Calibrate(){ 49 | var oRequest = new XMLHttpRequest(); 50 | var sURL = '/calibrate'; 51 | oRequest.open("GET",sURL,true); 52 | oRequest.onload = function (e) { 53 | if(oRequest.readyState === 4 && oRequest.status === 200){ 54 | SetOutput(oRequest.responseText, false); 55 | document.getElementById("Calibrate").style.backgroundColor = "#4CAF50"; 56 | } 57 | else{ 58 | SetOutput(oRequest.responseText, true); 59 | document.getElementById("Calibrate").style.backgroundColor = "#f44336"; 60 | } 61 | }; 62 | oRequest.onerror = function (e) { 63 | SetOutput("Calibration Error!", true); 64 | document.getElementById("Calibrate").style.backgroundColor = "#f44336"; 65 | }; 66 | oRequest.send(null); 67 | } 68 | function SetSetup(submitData = true){ 69 | var oRequest = new XMLHttpRequest(); 70 | var sURL = '/setup'; 71 | if(submitData){ 72 | sURL += '?vx='; 73 | sURL += document.getElementById("ValutationX").value; 74 | sURL += '&vy='; 75 | sURL += document.getElementById("ValutationY").value; 76 | sURL += '&inv='; 77 | sURL += document.getElementById("InvertAxis").checked ? '1' : '0'; 78 | sURL += '&ap='; 79 | sURL += document.getElementById("Accesspoint").checked ? '1' : '0'; 80 | sURL += '&t='; 81 | sURL += document.getElementById("ThresholdSlider").value; 82 | } 83 | oRequest.open("GET",sURL,true); 84 | oRequest.onload = function (e) { 85 | if(oRequest.readyState === 4 && oRequest.status === 200){ 86 | var arr = oRequest.responseText.split("|"); 87 | ADXL345_Initialized = arr[0]; 88 | document.getElementById("ValutationX").value = arr[1]; 89 | document.getElementById("ValutationY").value = arr[2]; 90 | document.getElementById("InvertAxis").checked = arr[3] == '1' ? true : false; 91 | document.getElementById("Accesspoint").checked = arr[4] == '1' ? true : false; 92 | if(submitData){ 93 | document.getElementById("SaveBtn").style.backgroundColor = "#00e600"; 94 | ResetControlsDelayed(); 95 | } 96 | } 97 | }; 98 | oRequest.onerror = function (e) { 99 | SetOutput("Set Setup failed!", true); 100 | document.getElementById("SaveBtn").style.backgroundColor = "#FF0000"; 101 | }; 102 | oRequest.send(null); 103 | } 104 | function MeasureValuation() { 105 | var btn = document.getElementById("MeasureBtn"); 106 | var measuring = btn.innerHTML == "Rec"; 107 | btn.innerHTML = measuring ? "End" : "Rec"; 108 | btn.style.backgroundColor = measuring ? "#f44336" : "#008CBA"; 109 | var oRequest = new XMLHttpRequest(); 110 | var sURL = '/valuate'; 111 | oRequest.open("GET",sURL,true); 112 | oRequest.onload = function (e) { 113 | if(oRequest.readyState === 4 && oRequest.status === 200){ 114 | SetOutput(oRequest.responseText, false); 115 | } 116 | else{ 117 | SetOutput("Recording Error", true); 118 | } 119 | }; 120 | oRequest.onerror = function (e) { 121 | SetOutput("Recording Error", true); 122 | }; 123 | oRequest.send(null); 124 | if(!measuring) 125 | SetSetup(false); 126 | } 127 | function ResetData() { 128 | var oRequest = new XMLHttpRequest(); 129 | var sURL = '/reset'; 130 | oRequest.open("GET",sURL,true); 131 | oRequest.onload = function (e) { 132 | if(oRequest.readyState === 4 && oRequest.status === 200){ 133 | SetOutput(oRequest.responseText, false); 134 | } 135 | else{ 136 | SetOutput("Reset Error", true); 137 | } 138 | }; 139 | oRequest.onerror = function (e) { 140 | SetOutput("Reset Error", true); 141 | }; 142 | oRequest.send(null); 143 | } 144 | function Upload(){ 145 | document.getElementsByClassName('dimmer')[0].style.display = 'block'; 146 | document.body.style.cursor = 'progress'; 147 | if(!PCVersion) 148 | document.getElementById("UploadForm").submit(); 149 | } 150 | function SetLevel(x,y, threshold){ 151 | document.getElementById("LevelX").value = (x / 10).toFixed(1); 152 | document.getElementById("LevelY").value = (y / 10).toFixed(1); 153 | if(document.getElementById("ThresholdValue").innerHTML != threshold){ 154 | document.getElementById("ThresholdSlider").value = threshold; 155 | document.getElementById("ThresholdValue").innerHTML = threshold; 156 | document.getElementById("SliderValue").innerHTML = threshold; 157 | } 158 | SetDots("X"); 159 | SetDots("Y"); 160 | } 161 | function SetDots(desc){ 162 | var can = document.getElementById("Level"+desc+"Canvas"); 163 | var txt = document.getElementById("Level"+desc); 164 | var dot = document.getElementById("Dot"+desc); 165 | dot.style.margin = "0px 0px 0px 0px"; 166 | //Dot + Border (4px) 167 | dot.style.top = can.getBoundingClientRect().top + (can.height / 2 - (dot.clientHeight + 4) / 2) + "px"; 168 | dot.style.left = can.getBoundingClientRect().left + (can.width / 2 - (dot.clientWidth + 4) / 2) + "px"; 169 | var value = parseFloat(txt.value); 170 | var threshold = document.getElementById("ThresholdSlider").value; 171 | var max = desc == "X" ? (can.height/ 2 - dot.clientHeight / 2) : (can.width / 2 - dot.clientWidth / 2); 172 | value = (max / threshold) * value; 173 | if(value < max * -1) 174 | value = max * -1; 175 | if(value > max) 176 | value = max; 177 | if(desc == "X") 178 | dot.style.top = parseInt(dot.style.top, 10) + value + "px"; 179 | if(desc == "Y") 180 | dot.style.left = parseInt(dot.style.left, 10) + value + "px"; 181 | SetColor(txt, (value / max) * 100); 182 | } 183 | function DrawLevel(desc){ 184 | var c = document.getElementById("Level" + desc + "Canvas"); 185 | var height = c.height; 186 | var width = c.width; 187 | var ctx = c.getContext("2d"); 188 | ctx.beginPath(); 189 | ctx.rect(0, 0, width, height); 190 | ctx.fillStyle = ADXL345_Initialized ? "#CCFF99" : "#FF0000"; 191 | ctx.fillRect(2, 2, width-4, height-4); 192 | 193 | var widthTmp = (desc == "Y") ? width / 3 -5 : 0; 194 | var heightTmp = (desc == "X") ? height / 3 -5 : 0; 195 | 196 | ctx.moveTo(widthTmp, heightTmp); 197 | if(desc == "Y") 198 | ctx.lineTo(widthTmp, height); 199 | else 200 | ctx.lineTo(width, heightTmp); 201 | 202 | if(desc == "X") 203 | heightTmp += 10; 204 | else 205 | widthTmp += 10; 206 | ctx.moveTo(widthTmp, heightTmp); 207 | if(desc == "Y") 208 | ctx.lineTo(widthTmp, height); 209 | else 210 | ctx.lineTo(width, heightTmp); 211 | 212 | widthTmp = (desc == "Y") ? width / 3 * 2 -5 : 0; 213 | heightTmp = (desc == "X") ? height / 3 * 2 -5 : 0; 214 | 215 | ctx.moveTo(widthTmp, heightTmp); 216 | if(desc == "Y") 217 | ctx.lineTo(widthTmp, height); 218 | else 219 | ctx.lineTo(width, heightTmp); 220 | 221 | if(desc == "X") 222 | heightTmp += 10; 223 | else 224 | widthTmp += 10; 225 | ctx.moveTo(widthTmp, heightTmp); 226 | if(desc == "Y") 227 | ctx.lineTo(widthTmp, height); 228 | else 229 | ctx.lineTo(width, heightTmp); 230 | ctx.stroke(); 231 | } 232 | function SetColor(element, value){ 233 | if(value < 0) 234 | value = value * -1; 235 | if(value > 100) 236 | value = 100; 237 | //Green -> Red 238 | var colour = hsl_col_perc(value, 120, 0); 239 | element.style.backgroundColor = colour; 240 | } 241 | function hsl_col_perc(percent, start, end) { 242 | var a = percent / 100, 243 | b = (end - start) * a, 244 | c = b + start; 245 | // Return a CSS HSL string 246 | return 'hsl('+c+', 100%, 50%)'; 247 | } 248 | function SetSetupVisibility(){ 249 | SetSetup(false); 250 | document.getElementById("SetupContainer").style.display = "block"; 251 | document.getElementById("SaveBtn").style.backgroundColor = "#008CBA"; 252 | } 253 | function SetOutput(text, error){ 254 | var output = document.getElementById("Output"); 255 | output.innerHTML = text; 256 | output.style.color = error? "#FF0000" : "#008CBA"; 257 | ResetControlsDelayed(); 258 | } 259 | function SetCustomText(text){ 260 | document.getElementsByClassName('customLabel')[0].innerHTML = text; 261 | } 262 | function ResetControlsDelayed() { 263 | setTimeout(ResetControls,3000); 264 | } 265 | function ResetControls() { 266 | var output = document.getElementById("Output"); 267 | output.innerHTML = ""; 268 | output.style.color = "#008CBA"; 269 | document.getElementById("Calibrate").style.backgroundColor = "#008CBA"; 270 | document.getElementById("SaveBtn").style.backgroundColor = "#008CBA"; 271 | } 272 | function CloseModal() { 273 | ResetData(); 274 | document.getElementById("ThresholdValue").innerHTML = document.getElementById("SliderValue").innerHTML; 275 | document.getElementById("SetupContainer").style.display = "none"; 276 | } --------------------------------------------------------------------------------