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 @@
-
-
-
-
{{ "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";