diff --git a/data/main.js.gz b/data/main.js.gz index 714e198..e22eede 100644 Binary files a/data/main.js.gz and b/data/main.js.gz differ diff --git a/openapi.yml b/openapi.yml index 25fa1b2..09998ab 100644 --- a/openapi.yml +++ b/openapi.yml @@ -1,7 +1,7 @@ openapi: 3.1.0 info: title: Winderoo API - version: 0.1.0 + version: 1.0.0 servers: - url: http://winderoo.local/api/ description: local development and deployed devices @@ -11,58 +11,68 @@ tags: - name: Modify description: Modify the state of Winderoo paths: - /update: + /timer: post: tags: - Modify - description: Change the state of Winderoo + summary: Change the state of Winderoo parameters: - - in: query - name: tpd - schema: - type: integer - description: how many turns are required - example: 330 - - in: query - name: hour - schema: - type: integer - description: At what hour winderoo should begin winding at - example: 14 - - in: query - name: minutes - schema: - type: integer - description: At what minute winderoo should begin winding at - example: 50 - in: query name: timerEnabled schema: type: integer - description: Whether Winderoo should enable alarm-start winding; number represents a boolean where 0 == off and 1 == on - example: 0, 1 - - in: query - name: action - schema: - type: string - description: Whether Winderoo should start or stop winding - example: START, STOP - - in: query - name: rotationDirection - schema: - type: string - description: The winding direction - example: CW, CCW, BOTH + description: Whether Winderoo should enable alarm-start winding; number represents a boolean where 0 == off and 1 == on. + example: 1 responses: '204': description: Successful opeation + '400': + description: Missing required field in request body + content: + text/plain: + schema: + type: string + examples: + - "Missing required field: 'tpd'" + '500': + description: Something went wrong when writing to memory or during deserialization + content: + text/plain: + schema: + type: string + examples: + - Failed to deserialize request body + /update: + post: + tags: + - Modify + summary: Change the state of Winderoo + requestBody: + $ref: '#/components/requestBodies/UpdateBody' + responses: + '204': + description: Successful opeation + '400': + description: Missing required field in request body + content: + text/plain: + schema: + type: string + examples: + - "Missing required field: 'tpd'" '500': - description: Something went wrong when writing to memory + description: Something went wrong when writing to memory or during deserialization + content: + text/plain: + schema: + type: string + examples: + - Failed to deserialize request body /status: get: tags: - Status - description: Get the current status of Winderoo + summary: Get the current status of Winderoo responses: '200': description: Service is alive with current winder state @@ -74,15 +84,33 @@ paths: post: tags: - Modify - description: Toggle whether Winderoo is on or off (hard off state) + summary: Toggle whether Winderoo is on or off (hard off state) + requestBody: + $ref: '#/components/requestBodies/PowerBody' responses: '204': description: State toggled succesfully + '400': + description: Missing required field in request body + content: + text/plain: + schema: + type: string + examples: + - "Missing required field: 'winderEnabled'" + '500': + description: Something went wrong when writing to memory or during deserialization + content: + text/plain: + schema: + type: string + examples: + - Failed to deserialize request body /reset: get: tags: - Modify - description: Resets Winderoo's network settings; re-initializes winderoo with setup access point + summary: Resets Winderoo's network settings; re-initializes winderoo with setup access point responses: '200': description: State toggled succesfully @@ -91,7 +119,79 @@ paths: schema: $ref: '#/components/schemas/Resetting' components: + requestBodies: + UpdateBody: + description: a JSON object containing winderoo information + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Update' + example: + tpd: "330" + hour: "14" + minutes: "50" + timerEnabled: "0" + action: "START" + rotationDirection: "BOTH" + PowerBody: + description: a JSON object containing winderoo power information + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Power' + example: + winderEnabled: "1" schemas: + Power: + type: object + propertries: + winderEnabled: + type: string + description: + Whether Winderoo should enable alarm-start winding; number represents a boolean where 0 == off and 1 == on. + examples: + - 0 + - 1 + Update: + type: object + propertries: + tpd: + type: string + description: how many turns are required + examples: + - 330 + hour: + type: string + description: At what hour winderoo should begin winding at + examples: + - 14 + minutes: + type: string + description: At what minute winderoo should begin winding at + examples: + - 50 + timerEnabled: + type: string + description: + Whether Winderoo should enable alarm-start winding; number represents a boolean where 0 == off and 1 == on. + examples: + - 0 + - 1 + action: + type: string + description: Whether Winderoo should start or stop winding + examples: + - START + - STOP + rotationDirection: + type: string + description: The winding direction + examples: + - CW + - CCW + - BOTH Status: type: object properties: diff --git a/src/angular/osww-frontend/package-lock.json b/src/angular/osww-frontend/package-lock.json index 1bd4848..3cbada6 100644 --- a/src/angular/osww-frontend/package-lock.json +++ b/src/angular/osww-frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "osww-frontend", - "version": "0.2.0", + "version": "1.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "osww-frontend", - "version": "0.2.0", + "version": "1.0.0", "dependencies": { "@angular/animations": "^14.2.0", "@angular/cdk": "^13.0.0", diff --git a/src/angular/osww-frontend/package.json b/src/angular/osww-frontend/package.json index 6677753..5e0a95c 100644 --- a/src/angular/osww-frontend/package.json +++ b/src/angular/osww-frontend/package.json @@ -1,6 +1,6 @@ { "name": "osww-frontend", - "version": "0.2.0", + "version": "1.0.0", "scripts": { "ng": "ng", "start": "ng serve", diff --git a/src/angular/osww-frontend/src/app/api.service.ts b/src/angular/osww-frontend/src/app/api.service.ts index 929820f..fa37eb5 100644 --- a/src/angular/osww-frontend/src/app/api.service.ts +++ b/src/angular/osww-frontend/src/app/api.service.ts @@ -5,12 +5,12 @@ import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; export interface Update { - action?: string - rotationDirection?: string, - tpd?: number, - hour?: string; - minutes?: string; - timerEnabled?: number; + action: string + rotationDirection: string, + tpd: number, + hour: string; + minutes: string; + timerEnabled: number; } export interface Status { @@ -34,8 +34,6 @@ export interface Status { }) export class ApiService { - DEFUALT_URL = 'http://winderoo.local'; - isWinderEnabled$ = new BehaviorSubject(0); shouldRefresh$ = new BehaviorSubject(false); @@ -55,49 +53,34 @@ export class ApiService { updatePowerState(powerState: boolean) { let powerStateToNum; - const baseURL = ApiService.constructURL(); - + const baseURL = ApiService.constructURL() + 'power'; + if (powerState) { powerStateToNum = 1; } else { powerStateToNum = 0; } + + const powerBody = { + winderEnabled: powerStateToNum + } - const constructedURL = baseURL - + "power?" - + "winderEnabled=" + powerStateToNum; - - return this.http.post(constructedURL, null, { observe:'response' }); + return this.http.post(baseURL, powerBody, { observe:'response' }); } - updateTimerState(timerState: boolean) { - let timerStateToNum; + updateTimerState(timerState: number) { const baseURL = ApiService.constructURL(); - console.log(timerState) - if (timerState) { - timerStateToNum = 1; - } else { - timerStateToNum = 0; - } const constructedURL = baseURL - + "update?" - + "timerEnabled=" + timerStateToNum; + + "timer?" + + "timerEnabled=" + timerState; return this.http.post(constructedURL, null, { observe: 'response' }); } updateState(update: Update) { - const baseURL = ApiService.constructURL(); - - const constructedURL = baseURL - + 'update?action=' + update.action + '&' - + 'rotationDirection=' + update.rotationDirection + '&' - + 'tpd=' + update.tpd + '&' - + 'hour=' + update.hour + '&' - + 'minutes=' + update.minutes; - - return this.http.post(constructedURL, null, { observe: 'response' }); + const baseURL = ApiService.constructURL() + 'update'; + return this.http.post(baseURL, update, { observe: 'response' }); } resetDevice() { diff --git a/src/angular/osww-frontend/src/app/header/header-dialog.component.html b/src/angular/osww-frontend/src/app/header/header-dialog.component.html index 43f0a0b..8842ac0 100644 --- a/src/angular/osww-frontend/src/app/header/header-dialog.component.html +++ b/src/angular/osww-frontend/src/app/header/header-dialog.component.html @@ -6,12 +6,7 @@

- - -
  1. {{ "HEADER.DIALOG.POINT_1" | translate }} @@ -37,6 +32,10 @@

+ + diff --git a/src/angular/osww-frontend/src/app/settings/settings.component.ts b/src/angular/osww-frontend/src/app/settings/settings.component.ts index 73fef1b..e47f968 100644 --- a/src/angular/osww-frontend/src/app/settings/settings.component.ts +++ b/src/angular/osww-frontend/src/app/settings/settings.component.ts @@ -231,8 +231,8 @@ export class SettingsComponent implements OnInit, AfterViewChecked { this.uploadSettings('STOP'); } - mapTimerEnabledState($event: number): void { - if ($event == 1) { + mapTimerEnabledState(enabledState: number): void { + if (enabledState == 1) { this.isTimerEnabled = true; } else { this.isTimerEnabled = false; @@ -261,21 +261,34 @@ export class SettingsComponent implements OnInit, AfterViewChecked { const difference = (currentTimeEpoch - startTimeEpoch) / (estimatedRoutineFinishEpoch - startTimeEpoch); const percentage = difference * 100; - // When 'Start" button pressed + // When "Start" button pressed if (percentage <= 0.05) { this.progressMode = 'indeterminate'; - setTimeout(() => this.getData(), 5000); + setTimeout(() => this.getData(), 2500); } - this.progressPercentageComplete = percentage; + // Add 2 percent to make the progress bar look more full at lower percentages + if (percentage < 10) { + this.progressPercentageComplete = percentage + 2; + } else { + this.progressPercentageComplete = percentage; + } } } - updateTimerEnabledState($state: any) { - this.upload.isTimerEnabledNum = $state; - this.apiService.updateTimerState($state).subscribe( - (data) => { - this.mapTimerEnabledState($state) + updateTimerEnabledState($state: boolean) { + let timerStateToNum; + if ($state) { + timerStateToNum = 1; + } else { + timerStateToNum = 0; + } + this.upload.isTimerEnabledNum = timerStateToNum; + this.apiService.updateTimerState(this.upload.isTimerEnabledNum).subscribe( + (response) => { + if (response.status == 204) { + this.mapTimerEnabledState(this.upload.isTimerEnabledNum) + } }); }; diff --git a/src/platformio/osww-server/src/main.cpp b/src/platformio/osww-server/src/main.cpp index 8273246..62f799f 100644 --- a/src/platformio/osww-server/src/main.cpp +++ b/src/platformio/osww-server/src/main.cpp @@ -24,8 +24,8 @@ * directionalPinB = this is the pin that's wired to IN2 on your L298N circuit board * ledPin = by default this is set to the ESP32's onboard LED. If you've wired an external LED, change this value to the GPIO pin the LED is wired to. * externalButton = OPTIONAL - If you want to use an external ON/OFF button, connect it to this pin 13. If you need to use another pin, change the value here. - * - * If you're using a NeoPixel equipped board, you'll need to change directionalPinA, directionalPinB and ledPin (pin 18 on most, I think) to appropriate GPIOs. + * + * If you're using a NeoPixel equipped board, you'll need to change directionalPinA, directionalPinB and ledPin (pin 18 on most, I think) to appropriate GPIOs. * Faiulre to set these pins on NeoPixel boards will result in kernel panics. */ int durationInSecondsToCompleteOneRevolution = 8; @@ -165,7 +165,7 @@ void loadConfigVarsFromFile(String file_name) { result += (char)this_file.read(); } - + userDefinedSettings.status = json["savedStatus"].as(); // Winding || Stopped = 7char userDefinedSettings.rotationsPerDay = json["savedTPD"].as(); // min = 100 || max = 960 userDefinedSettings.hour = json["savedHour"].as(); // 00 @@ -256,10 +256,10 @@ void startWebserver() request->send(response); // Update RTC time ref - getTime(); + getTime(); }); - server.on("/api/power", HTTP_POST, [](AsyncWebServerRequest *request) + server.on("/api/timer", HTTP_POST, [](AsyncWebServerRequest *request) { int params = request->params(); @@ -267,106 +267,147 @@ void startWebserver() { AsyncWebParameter* p = request->getParam(i); - if( strcmp(p->name().c_str(), "winderEnabled") == 0 ) + if( strcmp(p->name().c_str(), "timerEnabled") == 0 ) { - userDefinedSettings.winderEnabled = p->value().c_str(); - - if (userDefinedSettings.winderEnabled == "0") - { - Serial.println("[STATUS] - Switched off!"); - userDefinedSettings.status = "Stopped"; - routineRunning = false; - motor.stop(); - } + userDefinedSettings.timerEnabled = p->value().c_str(); } } - request->send(204); + bool writeSuccess = writeConfigVarsToFile(settingsFile, userDefinedSettings); + if ( !writeSuccess ) + { + Serial.println("[ERROR] - Failed to write [timer] endpoint data to file"); + request->send(500, "text/plain", "Failed to write new configuration to file"); + } + + request->send(204); }); - server.on("/api/update", HTTP_POST, [](AsyncWebServerRequest *request) + server.onRequestBody([](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { - int params = request->params(); - for ( int i = 0; i < params; i++ ) + if (request->url() == "/api/power") { - AsyncWebParameter* p = request->getParam(i); + JsonDocument json; + DeserializationError error = deserializeJson(json, data); - if( strcmp(p->name().c_str(), "rotationDirection") == 0 ) + if (error) { - userDefinedSettings.direction = p->value().c_str(); + Serial.println("[ERROR] - Failed to deserialize [power] request body"); + request->send(500, "text/plain", "Failed to deserialize request body"); + return; + } + + if (!json.containsKey("winderEnabled")) + { + request->send(400, "text/plain", "Missing required field: 'winderEnabled'"); + } + + userDefinedSettings.winderEnabled = json["winderEnabled"].as(); + + if (userDefinedSettings.winderEnabled == "0") + { + Serial.println("[STATUS] - Switched off!"); + userDefinedSettings.status = "Stopped"; + routineRunning = false; + motor.stop(); + } + + request->send(204); + } + + if (request->url() == "/api/update") + { + JsonDocument json; + DeserializationError error = deserializeJson(json, data); + int arraySize = 6; + String requiredKeys[arraySize] = {"rotationDirection", "tpd", "action", "hour", "minutes", "timerEnabled"}; + + if (error) + { + Serial.println("[ERROR] - Failed to deserialize [update] request body"); + request->send(500, "text/plain", "Failed to deserialize request body"); + return; + } + + // validate request body + for (int i = 0; i < arraySize; i++) + { + if(!json.containsKey(requiredKeys[i])) + { + request->send(400, "text/plain", "Missing required field: '" + requiredKeys[i] +"'"); + } + } + + // These values can be mutated / saved directly + userDefinedSettings.hour = json["hour"].as(); + userDefinedSettings.minutes = json["minutes"].as(); + userDefinedSettings.timerEnabled = json["timerEnabled"].as(); + // These values need to be compared to the current settings / running state + String requestRotationDirection = json["rotationDirection"].as(); + String requestTPD = json["tpd"].as(); + String requestAction = json["action"].as(); + + // Update motor direction + if (strcmp(requestRotationDirection.c_str(), userDefinedSettings.direction.c_str()) != 0) + { + userDefinedSettings.direction = requestRotationDirection; motor.stop(); delay(250); // Update motor direction - if (userDefinedSettings.direction == "CW" ) + if (userDefinedSettings.direction == "CW" ) { motor.setMotorDirection(1); } - else if (userDefinedSettings.direction == "CCW") + else if (userDefinedSettings.direction == "CCW") { motor.setMotorDirection(0); } Serial.println("[STATUS] - direction set: " + userDefinedSettings.direction); - } - - if( strcmp(p->name().c_str(), "tpd") == 0 ) + } + else { - const char* newTpd = p->value().c_str(); - - if (strcmp(newTpd, userDefinedSettings.rotationsPerDay.c_str()) != 0) - { - userDefinedSettings.rotationsPerDay = p->value().c_str(); - - unsigned long finishTime = calculateWindingTime(); - estimatedRoutineFinishEpoch = finishTime; - } + userDefinedSettings.direction = requestRotationDirection; } - if( strcmp(p->name().c_str(), "hour") == 0 ) + // Update (turns) rotations per day + if (strcmp(requestTPD.c_str(), userDefinedSettings.rotationsPerDay .c_str()) != 0) { - userDefinedSettings.hour = p->value().c_str(); + userDefinedSettings.rotationsPerDay = requestTPD; + + unsigned long finishTime = calculateWindingTime(); + estimatedRoutineFinishEpoch = finishTime; } - if( strcmp(p->name().c_str(), "timerEnabled") == 0 ) + // Update action (START/STOP) + if ( strcmp(requestAction.c_str(), "START") == 0 ) { - userDefinedSettings.timerEnabled = p->value().c_str(); + if (!routineRunning) + { + userDefinedSettings.status = "Winding"; + beginWindingRoutine(); + } } - - if( strcmp(p->name().c_str(), "minutes") == 0 ) + else { - userDefinedSettings.minutes = p->value().c_str(); + motor.stop(); + routineRunning = false; + userDefinedSettings.status = "Stopped"; } - if( strcmp(p->name().c_str(), "action") == 0) + // Write new parameters to file + bool writeSuccess = writeConfigVarsToFile(settingsFile, userDefinedSettings); + if ( !writeSuccess ) { - if ( strcmp(p->value().c_str(), "START") == 0 ) - { - if (!routineRunning) - { - userDefinedSettings.status = "Winding"; - beginWindingRoutine(); - } - } - else - { - motor.stop(); - routineRunning = false; - userDefinedSettings.status = "Stopped"; - } + Serial.println("[ERROR] - Failed to write [update] endpoint data to file"); + request->send(500, "text/plain", "Failed to write new configuration to file"); } - } - - bool writeSuccess = writeConfigVarsToFile(settingsFile, userDefinedSettings); - if ( !writeSuccess ) - { - request->send(500); + request->send(204); } - - request->send(204); }); server.on("/api/reset", HTTP_GET, [](AsyncWebServerRequest *request) @@ -378,7 +419,7 @@ void startWebserver() serializeJson(json, *response); request->send(response); - reset = true; + reset = true; }); server.serveStatic("/css/", LittleFS, "/css/").setCacheControl("max-age=31536000"); @@ -457,7 +498,7 @@ void awaitWhileListening(int pauseInSeconds) routineRunning = false; motor.stop(); } - } + } else { userDefinedSettings.winderEnabled == "1";