diff --git a/README.md b/README.md index fff31fb..457dd2c 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@

- Winderoo is open source firmware which adds "smart" functionality, and a GUI, to your OSWW or other microcontroller equipped watch winder. + Winderoo is open source firmware which adds smart functionality to your OSWW build or microcontroller equipped watch winder. Smart functionality includes a Web UI (fully-translated in 5 languages!), OLED screen support, start timer, and more!

@@ -32,13 +32,15 @@ * Estimated cycle duration (how long it'll take to wind your watch). * Cycle progress display (how far along the current winding routine is). * Software or optional physical button to trigger ON/OFF state, so you can disable the winder completely. +* OLED screen support * Simple setup. Flash the firmware and File System with a few clicks, then connect your phone (or other device) to the winder's setup wifi network & add it to your home network. -* There's no app required! You control it from a web browser. +* There's no app required! You control it from a web browser. * Minimal electronics / programming experience required +* Web UI is fully tranlated into 5 langauges (more are welcome!) ### Winderoo Requires a Different Microcontroller -* **You must replace the Pi Pico, as spec'd in the OSWW build guide, with an ESP32. This project will not run on the Pi Pico!** +* **You must replace the Pi Pico, as spec'd in the OSWW build guide, with an ESP32. This project will not run on a Pi Pico!** # User Manual * [Click Here to see the user manual](./docs/user-manual.md) diff --git a/data/3rdpartylicenses.txt.gz b/data/3rdpartylicenses.txt.gz index 25ea888..52cd03e 100644 Binary files a/data/3rdpartylicenses.txt.gz and b/data/3rdpartylicenses.txt.gz differ diff --git a/data/assets/i18n/de-DE.json b/data/assets/i18n/de-DE.json index 1abfdce..5159195 100644 --- a/data/assets/i18n/de-DE.json +++ b/data/assets/i18n/de-DE.json @@ -34,6 +34,7 @@ "COUNTER_CLOCKWISE": "Gegen den Uhrzeigersinn", "BOTH": "Beide Richtungen", "PLEASE_WAIT": "Zyklusfortschritt wird angezeigt, bitte warten", - "PROGRESS": "Zyklusfortschritt" + "PROGRESS": "Zyklusfortschritt", + "SCREEN": "OLED Bildschirm" } } diff --git a/data/assets/i18n/en-US.json b/data/assets/i18n/en-US.json index 17e91d1..6514290 100644 --- a/data/assets/i18n/en-US.json +++ b/data/assets/i18n/en-US.json @@ -34,6 +34,7 @@ "COUNTER_CLOCKWISE": "Counter Clockwise", "BOTH": "Both", "PLEASE_WAIT": "Getting cycle progress, please wait", - "PROGRESS": "Cycle Progress" + "PROGRESS": "Cycle Progress", + "SCREEN": "OLED Screen" } } diff --git a/data/assets/i18n/es-ES.json b/data/assets/i18n/es-ES.json index 1b661a0..20fb51a 100644 --- a/data/assets/i18n/es-ES.json +++ b/data/assets/i18n/es-ES.json @@ -34,6 +34,7 @@ "COUNTER_CLOCKWISE": "En sentido anti-horario", "BOTH": "Ambos", "PLEASE_WAIT": "Obteniendo el progreso del ciclo, espere", - "PROGRESS": "Progreso del ciclo" + "PROGRESS": "Progreso del ciclo", + "SCREEN": "Pantalla OLED" } } diff --git a/data/assets/i18n/fr-FR.json b/data/assets/i18n/fr-FR.json index 93b2ff1..2065e35 100644 --- a/data/assets/i18n/fr-FR.json +++ b/data/assets/i18n/fr-FR.json @@ -34,6 +34,7 @@ "COUNTER_CLOCKWISE": "Dans le sens inverse des aiguilles d'une montre", "BOTH": "Les deux", "PLEASE_WAIT": "Obtenir la progression du cycle, veuillez patienter", - "PROGRESS": "Progression du cycle" + "PROGRESS": "Progression du cycle", + "SCREEN": "Écran OLED" } } diff --git a/data/assets/i18n/pt-BR.json b/data/assets/i18n/pt-BR.json index 902df61..203beda 100644 --- a/data/assets/i18n/pt-BR.json +++ b/data/assets/i18n/pt-BR.json @@ -34,6 +34,7 @@ "COUNTER_CLOCKWISE": "Sentido anti-horário", "BOTH": "Ambos", "PLEASE_WAIT": "Obtendo o progresso do ciclo, aguarde", - "PROGRESS": "Progresso do Ciclo" + "PROGRESS": "Progresso do Ciclo", + "SCREEN": "Ecrã OLED" } } diff --git a/data/index.html.gz b/data/index.html.gz index bbadda9..df83baf 100644 Binary files a/data/index.html.gz and b/data/index.html.gz differ diff --git a/data/main.js.gz b/data/main.js.gz index 888bc38..a3fdacb 100644 Binary files a/data/main.js.gz and b/data/main.js.gz differ diff --git a/data/polyfills.js.gz b/data/polyfills.js.gz index fd8277f..854d377 100644 Binary files a/data/polyfills.js.gz and b/data/polyfills.js.gz differ diff --git a/data/runtime.js.gz b/data/runtime.js.gz index b1cdae4..cfbd0bf 100644 Binary files a/data/runtime.js.gz and b/data/runtime.js.gz differ diff --git a/data/styles.css.gz b/data/styles.css.gz index c6a95ef..dfc73da 100644 Binary files a/data/styles.css.gz and b/data/styles.css.gz differ diff --git a/docs/bom-requirements.md b/docs/bom-requirements.md index c756640..b949813 100644 --- a/docs/bom-requirements.md +++ b/docs/bom-requirements.md @@ -21,6 +21,15 @@ | 🔲 | 1 | [ESP32 Dev Kit](https://s.click.aliexpress.com/e/_Derbd5r) | Select `Color: ESP32` | $8.00 | +### Optional parts +The following parts list will allow you to add: +- 1x SSD1306 compatible OLED screen (0.96" size) + + +| | Quantity per Order | Link / Part Name | Comments | Cost Incl. Shipping | +| :-: | :------------: | :-------------------------------------------------------------------------------: | :-------------------------------------------------------------------: |:-----: +| 🔲 | 1 | [0.96" SSD1306 OLED](https://s.click.aliexpress.com/e/_DFywAW1) | Select: whichever colour you'd like! | $2.00 | + ## Next Steps > [Proceed to software installation 👉](./install-software.md) diff --git a/docs/gui/oled_screen.png b/docs/gui/oled_screen.png new file mode 100644 index 0000000..6ef94d4 Binary files /dev/null and b/docs/gui/oled_screen.png differ diff --git a/docs/gui/overview_v0.1.0.png b/docs/gui/overview_v0.1.0.png deleted file mode 100644 index ad70ba7..0000000 Binary files a/docs/gui/overview_v0.1.0.png and /dev/null differ diff --git a/docs/gui/overview_v1.0.0.png b/docs/gui/overview_v1.0.0.png new file mode 100644 index 0000000..86ecd5f Binary files /dev/null and b/docs/gui/overview_v1.0.0.png differ diff --git a/docs/images/platformio-ini.png b/docs/images/platformio-ini.png new file mode 100644 index 0000000..8918cca Binary files /dev/null and b/docs/images/platformio-ini.png differ diff --git a/docs/images/winderoo.png b/docs/images/winderoo-wiring-diagram.png similarity index 100% rename from docs/images/winderoo.png rename to docs/images/winderoo-wiring-diagram.png diff --git a/docs/images/winderoo-with-oled-wiring-diagram.png b/docs/images/winderoo-with-oled-wiring-diagram.png new file mode 100644 index 0000000..afe9edb Binary files /dev/null and b/docs/images/winderoo-with-oled-wiring-diagram.png differ diff --git a/docs/install-software.md b/docs/install-software.md index 1ecdbd7..6a6803d 100644 --- a/docs/install-software.md +++ b/docs/install-software.md @@ -21,6 +21,16 @@ - If you downloaded the repository as a zip, uzip it before proceeding to step 2.
how to download
1. Open the extracted folder (or cloned repository if using git) in Visual Studio Code +1. **IMPORTANT** -> if you're building Winderoo with an OLED screen attached, you must enable a build flag to tell PlatformIO to include additional libraries. To do this: + - Navigate to the file `platformio.ini` +
how to download
+ - In this file, you'll see the following block of code: + ```yml + build_flags = + -D OLED_ENABLED=false + ``` + - Change `-D OLED_ENABLED=false` to `-D OLED_ENABLED=true` + - PlatformIO will now compile Winderoo with OLED screen support 1. Select 'PlatformIO' (alien/insect looking button) on the workspace menu and wait for visual studio code to finish initializing the project
platformIO button
1. Expand the main heading: **"esp32doit-devkit-v1"**: diff --git a/docs/user-manual.md b/docs/user-manual.md index c2c3569..c18645a 100644 --- a/docs/user-manual.md +++ b/docs/user-manual.md @@ -6,7 +6,7 @@ | v1.0.0 | | | :---: |:---: | -| | This is winderoo's primary interface. From here you can change any settings you need | +| | This is winderoo's primary interface. From here you can change any settings you need | ### Enable / Disable Winding @@ -59,6 +59,16 @@ | | This will enable or disable the winder's timer. When the switch is set to **ENABLED**, Winderoo will begin winding at a your desired 'Cycle Start Time.' If the switch is set to **DISABLED**, you must start the winder using the [control buttons](#control-buttons) | | | Set which time you'd like Winderoo to begin winding at. **_Important!_** WInderoo will _always_ start at this time, even if you've already triggered a manual run with a [control button](#control-buttons). To stop this behaviour, see: [Enable / Disable Winding](#enable--disable-winding) | +### OLED Screen + +>[!IMPORTANT] +> This element will appear _only if_ you've compiled Winderoo with an OLED screen attached, and set the build flag accordingly. + +| UI Element | Function | +| :---: |:---: | +| | This will toggle the attached OLED screen on and off. | + + ### Save / Update Settings | UI Element | Function | | :---: |:---: | diff --git a/docs/winderoo-with-oled-wiring-diagram.pdf b/docs/winderoo-with-oled-wiring-diagram.pdf new file mode 100644 index 0000000..e6125bc Binary files /dev/null and b/docs/winderoo-with-oled-wiring-diagram.pdf differ diff --git a/docs/winderoo-with-oled.fzz b/docs/winderoo-with-oled.fzz new file mode 100644 index 0000000..cda7631 Binary files /dev/null and b/docs/winderoo-with-oled.fzz differ diff --git a/docs/wiring-diagram.md b/docs/wiring-diagram.md index 45a4041..b96026b 100644 --- a/docs/wiring-diagram.md +++ b/docs/wiring-diagram.md @@ -1,7 +1,12 @@ > [👈 Back to main page](../README.md) -# Wiring Diagram - Winderoo -![fritzing wiring diagram](./images/winderoo.png) +# Wiring Diagrams - Winderoo +- [Winderoo](#winderoo) +- [Winderoo With OLED Screen](#winderoo-with-oled-screen) + + +## Winderoo +![fritzing wiring diagram](./images/winderoo-wiring-diagram.png)
Is the image hard to view? Open the PDF instead @@ -15,6 +20,33 @@ - Make sure the L298N's jumpers are in place (small blue wires on board) - Connect the ESP32's `GPIO25` to `IN1` on the L298N driver board (yellow wire) - Connect the ESP32's `GPIO26` to `IN2` on the L298N driver board (blue wire) +- Connect the DC motor to channel 1 on the L298N driver board. + - > Note: polarity does not matter here + - You may add an external button off `GPIO 13` on the ESP32; this will enable and disable winding + +## Winderoo with OLED Screen + +> [!IMPORTANT] +> This wiring diagram assumes you've purchased an I2C 4-wire OLED (SSD1306 driver compatible). Complex LCDs are currently not supported. + +![fritzing wiring diagram](./images/winderoo-with-oled-wiring-diagram.png) + +
+ Is the image hard to view? Open the PDF instead +
+
+ + +- Connect the ESP32's `5V` output pin to the 5V input of the L298N driver board (red wire) +- Connect the ESP32's `5V` output pin to the `VCC` input pin on your OLED screen +- Connect a ground wire from the ESP32 to the L298N driver board (black wire) +- Connect a ground wire from the ESP32 to the OLED screen's `GRND` pin +- Connect a wire from the ESP32's `D22` pin to the OLED screen's `SCL` pin +- Connect a wire from the ESP32's `D21` pin to the OLED screen's `SDA` pin +- Connect the L298N driver board's 5V and 12V terminals together (red jumper wire) +- Make sure the L298N's jumpers are in place (small blue wires on board) +- Connect the ESP32's `GPIO25` to `IN1` on the L298N driver board (yellow wire) +- Connect the ESP32's `GPIO26` to `IN2` on the L298N driver board (blue wire) - Connect the DC motor to channel 1 on the L298N driver board. - > Note: polarity does not matter here - You may add an external button off `GPIO 13` on the ESP32; this will enable and disable winding \ No newline at end of file diff --git a/openapi.yml b/openapi.yml index 09998ab..ce9867b 100644 --- a/openapi.yml +++ b/openapi.yml @@ -15,7 +15,7 @@ paths: post: tags: - Modify - summary: Change the state of Winderoo + summary: Change the timer state of Winderoo parameters: - in: query name: timerEnabled @@ -134,6 +134,7 @@ components: timerEnabled: "0" action: "START" rotationDirection: "BOTH" + screenSleep: false PowerBody: description: a JSON object containing winderoo power information required: true @@ -243,6 +244,14 @@ components: type: number examples: - -67 + screenSleep: + type: boolean + examples: + - false + screenEquipped: + type: boolean + examples: + - false Resetting: type: object properties: diff --git a/platformio.ini b/platformio.ini index b8a56aa..9b7d23e 100644 --- a/platformio.ini +++ b/platformio.ini @@ -20,7 +20,9 @@ monitor_speed = 115200 build_src_filter = +<*> -<./angular/> board_build.filesystem = littlefs check_tool = cppcheck, clangtidy -check_flags= +build_flags = + -D OLED_ENABLED=false +check_flags = clangtidy: -fix-errors,--format-style=google lib_deps = esphome/AsyncTCP-esphome@^1.2.2 @@ -29,3 +31,4 @@ lib_deps = https://github.com/tzapu/WiFiManager.git https://github.com/bblanchon/ArduinoJson.git fbiego/ESP32Time@^2.0.0 + adafruit/Adafruit SSD1306@^2.5.9 diff --git a/src/angular/osww-frontend/src/app/api.service.ts b/src/angular/osww-frontend/src/app/api.service.ts index a6ad716..d14c016 100644 --- a/src/angular/osww-frontend/src/app/api.service.ts +++ b/src/angular/osww-frontend/src/app/api.service.ts @@ -11,6 +11,7 @@ export interface Update { hour: string; minutes: string; timerEnabled: number; + screenSleep: boolean; } export interface Status { @@ -27,6 +28,8 @@ export interface Status { estimatedRoutineFinishEpoch: number; winderEnabled: number; timerEnabled: number; + screenSleep: boolean; + screenEquipped: boolean; } @Injectable({ @@ -69,12 +72,12 @@ export class ApiService { let powerStateToNum; const baseURL = ApiService.constructURL() + 'power'; - if (powerState) { + if (powerState) { powerStateToNum = 1; } else { powerStateToNum = 0; } - + const powerBody = { winderEnabled: powerStateToNum } diff --git a/src/angular/osww-frontend/src/app/settings/settings.component.html b/src/angular/osww-frontend/src/app/settings/settings.component.html index d19c42a..fd0e073 100644 --- a/src/angular/osww-frontend/src/app/settings/settings.component.html +++ b/src/angular/osww-frontend/src/app/settings/settings.component.html @@ -51,7 +51,7 @@
- +
{{ "SETTINGS.DIRECTION" | translate }} @@ -94,7 +94,7 @@
{{ "SETTINGS.CYCLE_START" | translate }}
- +
@@ -110,11 +110,11 @@
- +
{{ this.upload.hour }}:{{ this.upload.minutes }}
- +
@@ -144,10 +144,28 @@
+ +
+
{{ "SETTINGS.SCREEN" | translate }}
+ +
+ + + {{ "SETTINGS.DISABLED" | translate | uppercase }} + + + + {{ "SETTINGS.ENABLED" | translate | uppercase }} + + + +
+
+
+
- \ No newline at end of file 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 e47f968..95d545f 100644 --- a/src/angular/osww-frontend/src/app/settings/settings.component.ts +++ b/src/angular/osww-frontend/src/app/settings/settings.component.ts @@ -52,7 +52,7 @@ export class SettingsComponent implements OnInit, AfterViewChecked { ]; wifiSignalIcon = '' - + upload = { activityState: '', statusMessage: '', @@ -64,7 +64,8 @@ export class SettingsComponent implements OnInit, AfterViewChecked { durationInSecondsToCompleteOneRevolution: 0, startTimeEpoch: 0, estimatedRoutineFinishEpoch: 0, - isTimerEnabledNum: 0 + isTimerEnabledNum: 0, + screenSleep: false } selectedHour: any; @@ -76,6 +77,7 @@ export class SettingsComponent implements OnInit, AfterViewChecked { progressPercentageComplete: number = 0; isWinderEnabled: number; isTimerEnabled: boolean; + screenEquipped: boolean = false; watchWindingParametersURL = 'https://watch-winder.store/watch-winding-table/'; @@ -118,6 +120,8 @@ export class SettingsComponent implements OnInit, AfterViewChecked { this.upload.startTimeEpoch = data.startTimeEpoch; this.upload.estimatedRoutineFinishEpoch = data.estimatedRoutineFinishEpoch; this.upload.isTimerEnabledNum = data.timerEnabled; + this.upload.screenSleep = data.screenSleep; + this.screenEquipped = data.screenEquipped; this.apiService.isWinderEnabled$.next(data.winderEnabled); @@ -211,6 +215,7 @@ export class SettingsComponent implements OnInit, AfterViewChecked { hour: this.selectedHour == null ? this.upload.hour : this.selectedHour, minutes: this.selectedMinutes == null ? this.upload.minutes : this.selectedMinutes, timerEnabled: this.upload.isTimerEnabledNum, + screenSleep: this.upload.screenSleep, } this.apiService.updateState(body).subscribe((response) => { @@ -292,4 +297,9 @@ export class SettingsComponent implements OnInit, AfterViewChecked { }); }; + updateScreenSleepState($state: boolean) { + this.upload.screenSleep = $state; + this.uploadSettings(); + }; + } diff --git a/src/angular/osww-frontend/src/assets/i18n/de-DE.json b/src/angular/osww-frontend/src/assets/i18n/de-DE.json index 1abfdce..5159195 100644 --- a/src/angular/osww-frontend/src/assets/i18n/de-DE.json +++ b/src/angular/osww-frontend/src/assets/i18n/de-DE.json @@ -34,6 +34,7 @@ "COUNTER_CLOCKWISE": "Gegen den Uhrzeigersinn", "BOTH": "Beide Richtungen", "PLEASE_WAIT": "Zyklusfortschritt wird angezeigt, bitte warten", - "PROGRESS": "Zyklusfortschritt" + "PROGRESS": "Zyklusfortschritt", + "SCREEN": "OLED Bildschirm" } } diff --git a/src/angular/osww-frontend/src/assets/i18n/en-US.json b/src/angular/osww-frontend/src/assets/i18n/en-US.json index 17e91d1..6514290 100644 --- a/src/angular/osww-frontend/src/assets/i18n/en-US.json +++ b/src/angular/osww-frontend/src/assets/i18n/en-US.json @@ -34,6 +34,7 @@ "COUNTER_CLOCKWISE": "Counter Clockwise", "BOTH": "Both", "PLEASE_WAIT": "Getting cycle progress, please wait", - "PROGRESS": "Cycle Progress" + "PROGRESS": "Cycle Progress", + "SCREEN": "OLED Screen" } } diff --git a/src/angular/osww-frontend/src/assets/i18n/es-ES.json b/src/angular/osww-frontend/src/assets/i18n/es-ES.json index 1b661a0..20fb51a 100644 --- a/src/angular/osww-frontend/src/assets/i18n/es-ES.json +++ b/src/angular/osww-frontend/src/assets/i18n/es-ES.json @@ -34,6 +34,7 @@ "COUNTER_CLOCKWISE": "En sentido anti-horario", "BOTH": "Ambos", "PLEASE_WAIT": "Obteniendo el progreso del ciclo, espere", - "PROGRESS": "Progreso del ciclo" + "PROGRESS": "Progreso del ciclo", + "SCREEN": "Pantalla OLED" } } diff --git a/src/angular/osww-frontend/src/assets/i18n/fr-FR.json b/src/angular/osww-frontend/src/assets/i18n/fr-FR.json index 93b2ff1..2065e35 100644 --- a/src/angular/osww-frontend/src/assets/i18n/fr-FR.json +++ b/src/angular/osww-frontend/src/assets/i18n/fr-FR.json @@ -34,6 +34,7 @@ "COUNTER_CLOCKWISE": "Dans le sens inverse des aiguilles d'une montre", "BOTH": "Les deux", "PLEASE_WAIT": "Obtenir la progression du cycle, veuillez patienter", - "PROGRESS": "Progression du cycle" + "PROGRESS": "Progression du cycle", + "SCREEN": "Écran OLED" } } diff --git a/src/angular/osww-frontend/src/assets/i18n/pt-BR.json b/src/angular/osww-frontend/src/assets/i18n/pt-BR.json index 902df61..203beda 100644 --- a/src/angular/osww-frontend/src/assets/i18n/pt-BR.json +++ b/src/angular/osww-frontend/src/assets/i18n/pt-BR.json @@ -34,6 +34,7 @@ "COUNTER_CLOCKWISE": "Sentido anti-horário", "BOTH": "Ambos", "PLEASE_WAIT": "Obtendo o progresso do ciclo, aguarde", - "PROGRESS": "Progresso do Ciclo" + "PROGRESS": "Progresso do Ciclo", + "SCREEN": "Ecrã OLED" } } diff --git a/src/platformio/osww-server/src/main.cpp b/src/platformio/osww-server/src/main.cpp index 41c7fbf..bf21d98 100644 --- a/src/platformio/osww-server/src/main.cpp +++ b/src/platformio/osww-server/src/main.cpp @@ -6,6 +6,13 @@ #include #include +#ifdef OLED_ENABLED + #include + #include + #include + #include +#endif + #include "./utils/LedControl.h" #include "./utils/MotorControl.h" @@ -33,6 +40,13 @@ int directionalPinA = 25; int directionalPinB = 26; int ledPin = 0; int externalButton = 13; + +// OLED CONFIG +bool OLED_INVERT_SCREEN = false; +bool OLED_ROTATE_SCREEN_180 = false; +int SCREEN_WIDTH = 128; // OLED display width, in pixels +int SCREEN_HEIGHT = 64; // OLED display height, in pixels +int OLED_RESET = -1; // Reset pin number (or -1 if sharing Arduino reset pin) /* * ************************************************************************************* * ******************************* END CONFIGURABLES *********************************** @@ -51,6 +65,9 @@ unsigned long previousEpoch; unsigned long startTimeEpoch; bool reset = false; bool routineRunning = false; +bool configPortalRunning = false; +bool screenSleep = false; +bool screenEquipped = OLED_ENABLED; struct RUNTIME_VARS { String status = ""; @@ -74,6 +91,172 @@ HTTPClient http; WiFiClient client; ESP32Time rtc; +#ifdef OLED_ENABLED + Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); +#endif + + +void drawCentreStringToMemory(const char *buf, int x, int y) +{ + int16_t x1, y1; + uint16_t w, h; + display.getTextBounds(buf, 0, y, &x1, &y1, &w, &h); //calc width of new string + display.setCursor(x - (w / 2), y); + display.print(buf); +} + +static void drawStaticGUI(bool drawHeaderTitle = false, String title = "Winderoo") { + if (OLED_ENABLED) + { + display.clearDisplay(); + + display.setTextSize(1); + display.setTextColor(WHITE); + + if (drawHeaderTitle) + { + drawCentreStringToMemory(title.c_str(), 64, 3); + } + // top horizontal line + display.drawLine(0, 14, display.width(), 14, WHITE); + // vertical line + display.drawLine(64, 14, 64, 50, WHITE); + // bottom horizontal line + display.drawLine(0, 50, display.width(), 50, WHITE); + + display.setCursor(4, 18); + display.println(F("TPD")); + + display.setCursor(71, 18); + display.println(F("DIR")); + + display.display(); + } +} + +static void drawTimerStatus() { + if (OLED_ENABLED) + { + if (userDefinedSettings.timerEnabled == "1") + { + // right aligned timer + display.fillRect(60, 51, 64, 13, BLACK); + display.setCursor(60, 56); + display.print("TIMER " + userDefinedSettings.hour + ":" + userDefinedSettings.minutes); + } + else + { + display.fillRect(60, 51, 68, 13, BLACK); + } + } +} + +static void drawWifiStatus() { + if (OLED_ENABLED) + { + // left aligned cell reception icon + display.drawTriangle(4, 54, 10, 54, 7, 58, WHITE); + display.drawLine(7, 58, 7, 62, WHITE); + + // Clear reception bars + display.fillRect(12, 54, 58, 10, BLACK); + + if (WiFi.RSSI() > -50) + { + // Excelent reception - 4 bars + display.fillRect(14, 55+8, 2, 2, WHITE); + display.fillRect(18, 55+6, 2, 4, WHITE); + display.fillRect(22, 55+4, 2, 6, WHITE); + display.fillRect(26, 55+2, 2, 8, WHITE); + } + else if (WiFi.RSSI() > -60) + { + // Good reception - 3 bars + display.fillRect(14, 55+8, 2, 2, WHITE); + display.fillRect(18, 55+6, 2, 4, WHITE); + display.fillRect(22, 55+4, 2, 6, WHITE); + } + else if (WiFi.RSSI() > -70) + { + // Fair reception - 2 bars + display.fillRect(14, 55+8, 2, 2, WHITE); + display.fillRect(18, 55+6, 2, 4, WHITE); + } + else + { + // Terrible reception - 1 bar + display.fillRect(14, 55+8, 2, 2, WHITE); + } + } +} + +static void drawDynamicGUI() { + if (OLED_ENABLED && !screenSleep) + { + + display.fillRect(8, 25, 54, 25, BLACK); + display.setCursor(8, 30); + display.setTextSize(2); + display.print(userDefinedSettings.rotationsPerDay); + + display.fillRect(66, 25, 62, 25, BLACK); + display.setCursor(74, 30); + display.print(userDefinedSettings.direction); + display.setTextSize(1); + + drawWifiStatus(); + drawTimerStatus(); + + display.display(); + } +} + +static void drawNotification(String message) { + if (OLED_ENABLED && !screenSleep) + { + display.setCursor(0, 0); + display.drawRect(0, 0, 128, 14, WHITE); + display.fillRect(0, 0, 128, 14, WHITE); + display.setTextColor(BLACK); + drawCentreStringToMemory(message.c_str(), 64, 3); + display.display(); + display.setTextColor(WHITE); + delay(200); + display.setCursor(0, 0); + display.drawRect(0, 0, 128, 14, BLACK); + display.fillRect(0, 0, 128, 14, BLACK); + display.setTextColor(WHITE); + drawCentreStringToMemory(message.c_str(), 64, 3); + + // Underline notification, which is shared with Static GUI + display.drawLine(0, 14, display.width(), 14, WHITE); + display.display(); + } +} + +template static void drawMultiLineText(const String (&message)[N]) { + if (OLED_ENABLED && !screenSleep) + { + int yInitial = 20; + int yOffset = 16; + + display.fillRect(0, 18, 128, 64, BLACK); + + for (int i = 0; i < N; i++) + { + if (i == 0) + { + drawCentreStringToMemory(message[i].c_str(), 64, yInitial); + } + else + { + drawCentreStringToMemory(message[i].c_str(), 64, yInitial + (yOffset * i)); + } + } + display.display(); + } +} + /** * Calclates the duration and estimated finish time of the winding routine * @@ -118,6 +301,8 @@ void beginWindingRoutine() Serial.print("[STATUS] - Estimated finish time: "); Serial.println(finishTime); + + drawNotification("Winding"); } /** @@ -138,14 +323,17 @@ void getTime() int day = date.substring(8, 10).toInt(); int month = date.substring(5, 7).toInt(); int year = date.substring(0, 4).toInt(); - + String time = datetime.substring(datetime.indexOf("T") + 1); int seconds = time.substring(6, 8).toInt(); int hours = time.substring(0, 2).toInt(); int minutes = time.substring(3, 5).toInt(); rtc.setTime(seconds, minutes, hours, day, month, year); - Serial.println("[STATUS] - Time: " + datetime); + } + else + { + Serial.println("[ERROR] - Failed to get time from Worldtime API"); } http.end(); @@ -260,6 +448,8 @@ void startWebserver() json["winderEnabled"] = userDefinedSettings.winderEnabled; json["timerEnabled"] = userDefinedSettings.timerEnabled; json["db"] = WiFi.RSSI(); + json["screenSleep"] = screenSleep; + json["screenEquipped"] = screenEquipped; serializeJson(json, *response); request->send(response); @@ -320,6 +510,11 @@ void startWebserver() userDefinedSettings.status = "Stopped"; routineRunning = false; motor.stop(); + display.clearDisplay(); + display.display(); + } else { + drawStaticGUI(true); + drawDynamicGUI(); } request->send(204); @@ -329,8 +524,8 @@ void startWebserver() { JsonDocument json; DeserializationError error = deserializeJson(json, data); - int arraySize = 6; - String requiredKeys[arraySize] = {"rotationDirection", "tpd", "action", "hour", "minutes", "timerEnabled"}; + int arraySize = 7; + String requiredKeys[arraySize] = {"rotationDirection", "tpd", "action", "hour", "minutes", "timerEnabled", "screenSleep"}; if (error) { @@ -357,6 +552,7 @@ void startWebserver() String requestRotationDirection = json["rotationDirection"].as(); String requestTPD = json["tpd"].as(); String requestAction = json["action"].as(); + screenSleep = json["screenSleep"].as(); // Update motor direction if (strcmp(requestRotationDirection.c_str(), userDefinedSettings.direction.c_str()) != 0) @@ -405,6 +601,23 @@ void startWebserver() motor.stop(); routineRunning = false; userDefinedSettings.status = "Stopped"; + drawNotification("Stopped"); + } + + // Update screen sleep state + if (screenSleep && OLED_ENABLED) + { + display.clearDisplay(); + display.display(); + } + else + { + if (OLED_ENABLED) + { + // Draw gui with updated values from _this_ update request + drawStaticGUI(true, userDefinedSettings.status); + drawDynamicGUI(); + } } // Write new parameters to file @@ -469,18 +682,18 @@ void triggerLEDCondition(int blinkState) switch (blinkState) { - case 1: - LED.slowBlink(); - break; - case 2: - LED.fastBlink(); - break; - case 3: - LED.pwm(); - break; - default: - Serial.println("[WARN] - blinkState not recognized"); - break; + case 1: + LED.slowBlink(); + break; + case 2: + LED.fastBlink(); + break; + case 3: + LED.pwm(); + break; + default: + Serial.println("[WARN] - blinkState not recognized"); + break; } } @@ -502,10 +715,10 @@ void awaitWhileListening(int pauseInSeconds) { if (userDefinedSettings.winderEnabled == "0") { - Serial.println("[STATUS] - Switched off!"); - userDefinedSettings.status = "Stopped"; - routineRunning = false; motor.stop(); + routineRunning = false; + userDefinedSettings.status = "Stopped"; + Serial.println("[STATUS] - Switched off!"); } } else @@ -516,15 +729,38 @@ void awaitWhileListening(int pauseInSeconds) } } +/** + * Callback triggered from WifiManager when successfully connected to new WiFi network + */ +void saveParamsCallback() +{ + if (OLED_ENABLED) + { + display.clearDisplay(); + display.display(); + drawNotification("Connecting..."); + } +} + /** * Callback triggered from WifiManager when successfully connected to new WiFi network */ void saveWifiCallback() { + if (OLED_ENABLED) + { + display.clearDisplay(); + display.display(); + drawNotification("Connected to WiFi"); + String rebootingMessage[2] = {"Device is", "rebooting..."}; + drawMultiLineText(rebootingMessage); + } + // slow blink to confirm connection success triggerLEDCondition(1); + ESP.restart(); - delay(2000); + delay(1500); } void setup() @@ -546,9 +782,30 @@ void setup() wm.setConfigPortalBlocking(false); wm.setHostname("Winderoo"); wm.setSaveConfigCallback(saveWifiCallback); + wm.setSaveParamsCallback(saveParamsCallback); userDefinedSettings.winderEnabled = true; + if(OLED_ENABLED) + { + display.begin(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); + if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) + { + Serial.println(F("SSD1306 allocation failed")); + for(;;); // Don't proceed, loop forever + } + drawStaticGUI(); + + int rotate = OLED_ROTATE_SCREEN_180 ? 2 : 4; + display.clearDisplay(); + display.invertDisplay(OLED_INVERT_SCREEN); + display.setRotation(rotate); + drawNotification("Winderoo"); + } + + String savedNetworkMessage[2] = {"Connecting to", "saved network..."}; + drawMultiLineText(savedNetworkMessage); + // Connect using saved credentials, if they exist // If connection fails, start setup Access Point if (wm.autoConnect("Winderoo Setup")) @@ -562,9 +819,16 @@ void setup() if (!MDNS.begin("winderoo")) { Serial.println("[STATUS] - Failed to start mDNS"); + drawNotification("Failed to start mDNS"); } MDNS.addService("_winderoo", "_tcp", 80); Serial.println("[STATUS] - mDNS started"); + if (OLED_ENABLED) + { + display.clearDisplay(); + drawStaticGUI(); + drawNotification("Connected to WiFi"); + } getTime(); startWebserver(); @@ -573,19 +837,40 @@ void setup() { beginWindingRoutine(); } + else + { + drawNotification("Winderoo"); + } } else { + configPortalRunning = true; Serial.println("[STATUS] - WiFi Config Portal running"); ledcWrite(LED.getChannel(), 255); + + String setupNetworkMessage[3] = {"Connect to", "\"Winderoo Setup\"", "wifi to begin"}; + drawMultiLineText(setupNetworkMessage); }; } void loop() { + if (configPortalRunning) + { + wm.process(); + return; + } if (reset) { + if (OLED_ENABLED) + { + display.clearDisplay(); + drawNotification("Resetting"); + + String rebootingMessage[2] = {"Device is", "rebooting..."}; + drawMultiLineText(rebootingMessage); + } // fast blink triggerLEDCondition(2); @@ -611,6 +896,7 @@ void loop() userDefinedSettings.winderEnabled == "1") { beginWindingRoutine(); + drawNotification("Winding Started"); } } @@ -656,6 +942,10 @@ void loop() userDefinedSettings.status = "Stopped"; routineRunning = false; motor.stop(); + if (OLED_ENABLED && !screenSleep) + { + drawNotification("Winding Complete"); + } bool writeSuccess = writeConfigVarsToFile(settingsFile, userDefinedSettings); if ( !writeSuccess ) @@ -672,6 +962,10 @@ void loop() { triggerLEDCondition(3); } + else + { + drawDynamicGUI(); + } wm.process(); } diff --git a/src/platformio/osww-server/src/utils/LedControl.cpp b/src/platformio/osww-server/src/utils/LedControl.cpp index 75a135a..7e4f007 100644 --- a/src/platformio/osww-server/src/utils/LedControl.cpp +++ b/src/platformio/osww-server/src/utils/LedControl.cpp @@ -28,7 +28,7 @@ void LedControl::slowBlink() // Slow blink to confirm success & restart Serial.println("[STATUS] - slow blink"); - for (int dutyCycle = 0; dutyCycle <= 4; dutyCycle++) + for (int dutyCycle = 0; dutyCycle <= 3; dutyCycle++) { for (int dutyCycle = 0; dutyCycle <= 255; dutyCycle++) {