From f83a9cbccfa8e27108ad7804567177cc73b0f529 Mon Sep 17 00:00:00 2001 From: AJ-Koenig <39343698+AJ-Koenig@users.noreply.github.com> Date: Tue, 23 Apr 2024 14:01:45 -0600 Subject: [PATCH] Draft: Huge refactor (#71) Major refactor to introduce state machine; new ui and temporarily disable stroke engine --- Software/OSSM ESP32 Architecture.png | Bin 73093 -> 0 bytes Software/lib/Encoder/Encoder.cpp | 300 ------ Software/lib/Encoder/Encoder.h | 862 ----------------- Software/lib/Encoder/README.md | 9 - Software/lib/Encoder/docs/issue_template.md | 64 -- Software/lib/Encoder/examples/Basic/Basic.pde | 29 - .../examples/NoInterrupts/NoInterrupts.pde | 46 - .../Encoder/examples/SpeedTest/SpeedTest.pde | 113 --- .../Encoder/examples/TwoKnobs/TwoKnobs.pde | 46 - Software/lib/Encoder/keywords.txt | 4 - Software/lib/Encoder/library.json | 16 - Software/lib/Encoder/library.properties | 10 - .../lib/Encoder/utility/direct_pin_read.h | 110 --- .../lib/Encoder/utility/interrupt_config.h | 87 -- Software/lib/Encoder/utility/interrupt_pins.h | 370 -------- Software/platformio.ini | 29 +- Software/pre_build_script.py | 40 + Software/src/OSSM_Config.h | 63 -- Software/src/OSSM_PinDef.h | 58 -- Software/src/OssmUi/OssmUi.cpp | 91 -- Software/src/OssmUi/OssmUi.h | 19 - Software/src/Stroke_Engine_Helper.h | 43 - Software/src/Utilities.cpp | 879 ------------------ Software/src/Utilities.h | 139 --- Software/src/constants/Config.h | 92 ++ Software/src/constants/Images.h | 24 +- Software/src/constants/LogTags.h | 2 + Software/src/constants/Menu.h | 29 +- Software/src/constants/Pins.h | 92 ++ Software/src/constants/UserConfig.h | 17 + Software/src/constants/copy/en-us.h | 40 + Software/src/constants/copy/fr.h | 43 + Software/src/extensions/u8g2Extensions.h | 194 ++++ Software/src/main.cpp | 264 ++---- Software/src/ossm/Actions.h | 9 + Software/src/ossm/Events.h | 33 + Software/src/ossm/Guard.h | 8 + Software/src/ossm/OSSM.Help.cpp | 46 + Software/src/ossm/OSSM.Homing.cpp | 131 +++ Software/src/ossm/OSSM.Menu.cpp | 138 +++ Software/src/ossm/OSSM.PlayControls.cpp | 199 ++++ Software/src/ossm/OSSM.SimplePenetration.cpp | 88 ++ Software/src/ossm/OSSM.StrokeEngine.cpp | 13 + Software/src/ossm/OSSM.Update.cpp | 28 + Software/src/ossm/OSSM.WiFi.cpp | 51 + Software/src/ossm/OSSM.cpp | 120 +++ Software/src/ossm/OSSM.h | 253 +++++ Software/src/services/board.h | 30 + Software/src/services/display.h | 8 +- Software/src/services/encoder.h | 28 + Software/src/services/stepper.h | 33 + Software/src/services/tasks.h | 17 + Software/src/state/actions.h | 40 - Software/src/state/events.h | 40 - Software/src/state/guards.h | 38 - Software/src/state/state.h | 86 -- Software/src/state/type.h | 11 - Software/src/structs/LanguageStruct.h | 36 + Software/src/utils/analog.h | 27 + Software/src/utils/format.h | 98 ++ Software/src/utils/stringMethods.h | 50 - Software/src/utils/update.h | 101 ++ Software/src/workspace.code-workspace | 12 - Software/test/test_actions/MockOSSM.h | 19 - Software/test/test_actions/main.cpp | 24 - Software/test/test_measurements/main.cpp | 42 + Software/test/test_measurements/mock.h | 31 + Software/test/test_strings/main.cpp | 202 ++-- 68 files changed, 2354 insertions(+), 3960 deletions(-) delete mode 100644 Software/OSSM ESP32 Architecture.png delete mode 100644 Software/lib/Encoder/Encoder.cpp delete mode 100644 Software/lib/Encoder/Encoder.h delete mode 100644 Software/lib/Encoder/README.md delete mode 100644 Software/lib/Encoder/docs/issue_template.md delete mode 100644 Software/lib/Encoder/examples/Basic/Basic.pde delete mode 100644 Software/lib/Encoder/examples/NoInterrupts/NoInterrupts.pde delete mode 100644 Software/lib/Encoder/examples/SpeedTest/SpeedTest.pde delete mode 100644 Software/lib/Encoder/examples/TwoKnobs/TwoKnobs.pde delete mode 100644 Software/lib/Encoder/keywords.txt delete mode 100644 Software/lib/Encoder/library.json delete mode 100644 Software/lib/Encoder/library.properties delete mode 100644 Software/lib/Encoder/utility/direct_pin_read.h delete mode 100644 Software/lib/Encoder/utility/interrupt_config.h delete mode 100644 Software/lib/Encoder/utility/interrupt_pins.h create mode 100644 Software/pre_build_script.py delete mode 100644 Software/src/OSSM_Config.h delete mode 100644 Software/src/OSSM_PinDef.h delete mode 100644 Software/src/OssmUi/OssmUi.cpp delete mode 100644 Software/src/OssmUi/OssmUi.h delete mode 100644 Software/src/Stroke_Engine_Helper.h delete mode 100644 Software/src/Utilities.cpp delete mode 100644 Software/src/Utilities.h create mode 100644 Software/src/constants/Config.h create mode 100644 Software/src/constants/Pins.h create mode 100644 Software/src/constants/UserConfig.h create mode 100644 Software/src/constants/copy/en-us.h create mode 100644 Software/src/constants/copy/fr.h create mode 100644 Software/src/extensions/u8g2Extensions.h create mode 100644 Software/src/ossm/Actions.h create mode 100644 Software/src/ossm/Events.h create mode 100644 Software/src/ossm/Guard.h create mode 100644 Software/src/ossm/OSSM.Help.cpp create mode 100644 Software/src/ossm/OSSM.Homing.cpp create mode 100644 Software/src/ossm/OSSM.Menu.cpp create mode 100644 Software/src/ossm/OSSM.PlayControls.cpp create mode 100644 Software/src/ossm/OSSM.SimplePenetration.cpp create mode 100644 Software/src/ossm/OSSM.StrokeEngine.cpp create mode 100644 Software/src/ossm/OSSM.Update.cpp create mode 100644 Software/src/ossm/OSSM.WiFi.cpp create mode 100644 Software/src/ossm/OSSM.cpp create mode 100644 Software/src/ossm/OSSM.h create mode 100644 Software/src/services/board.h create mode 100644 Software/src/services/encoder.h create mode 100644 Software/src/services/stepper.h create mode 100644 Software/src/services/tasks.h delete mode 100644 Software/src/state/actions.h delete mode 100644 Software/src/state/events.h delete mode 100644 Software/src/state/guards.h delete mode 100644 Software/src/state/state.h delete mode 100644 Software/src/state/type.h create mode 100644 Software/src/structs/LanguageStruct.h create mode 100644 Software/src/utils/analog.h create mode 100644 Software/src/utils/format.h delete mode 100644 Software/src/utils/stringMethods.h create mode 100644 Software/src/utils/update.h delete mode 100644 Software/src/workspace.code-workspace delete mode 100644 Software/test/test_actions/MockOSSM.h delete mode 100644 Software/test/test_actions/main.cpp create mode 100644 Software/test/test_measurements/main.cpp create mode 100644 Software/test/test_measurements/mock.h diff --git a/Software/OSSM ESP32 Architecture.png b/Software/OSSM ESP32 Architecture.png deleted file mode 100644 index 8439a3d594e0e96d2869385923862e7f63859ff6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 73093 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 diff --git a/Software/lib/Encoder/Encoder.cpp b/Software/lib/Encoder/Encoder.cpp deleted file mode 100644 index 52ec9bfa..00000000 --- a/Software/lib/Encoder/Encoder.cpp +++ /dev/null @@ -1,300 +0,0 @@ - -#include "Encoder.h" - -// Most the code is in the header file, to provide the user -// configure options with #define (before they include it), and -// to facilitate some crafty optimizations! - -Encoder_internal_state_t * Encoder::interruptArgs[]; - -void ENCODER_ISR_ATTR Encoder::update(Encoder_internal_state_t *arg) { -#if defined(__AVR__) - // The compiler believes this is just 1 line of code, so - // it will inline this function into each interrupt - // handler. That's a tiny bit faster, but grows the code. - // Especially when used with ENCODER_OPTIMIZE_INTERRUPTS, - // the inline nature allows the ISR prologue and epilogue - // to only save/restore necessary registers, for very nice - // speed increase. - asm volatile ( - "ld r30, X+" "\n\t" - "ld r31, X+" "\n\t" - "ld r24, Z" "\n\t" // r24 = pin1 input - "ld r30, X+" "\n\t" - "ld r31, X+" "\n\t" - "ld r25, Z" "\n\t" // r25 = pin2 input - "ld r30, X+" "\n\t" // r30 = pin1 mask - "ld r31, X+" "\n\t" // r31 = pin2 mask - "ld r22, X" "\n\t" // r22 = state - "andi r22, 3" "\n\t" - "and r24, r30" "\n\t" - "breq L%=1" "\n\t" // if (pin1) - "ori r22, 4" "\n\t" // state |= 4 - "L%=1:" "and r25, r31" "\n\t" - "breq L%=2" "\n\t" // if (pin2) - "ori r22, 8" "\n\t" // state |= 8 - "L%=2:" "ldi r30, lo8(pm(L%=table))" "\n\t" - "ldi r31, hi8(pm(L%=table))" "\n\t" - "add r30, r22" "\n\t" - "adc r31, __zero_reg__" "\n\t" - "asr r22" "\n\t" - "asr r22" "\n\t" - "st X+, r22" "\n\t" // store new state - "ld r22, X+" "\n\t" - "ld r23, X+" "\n\t" - "ld r24, X+" "\n\t" - "ld r25, X+" "\n\t" - "ijmp" "\n\t" // jumps to update_finishup() - // TODO move this table to another static function, - // so it doesn't get needlessly duplicated. Easier - // said than done, due to linker issues and inlining - "L%=table:" "\n\t" - "rjmp L%=end" "\n\t" // 0 - "rjmp L%=plus1" "\n\t" // 1 - "rjmp L%=minus1" "\n\t" // 2 - "rjmp L%=plus2" "\n\t" // 3 - "rjmp L%=minus1" "\n\t" // 4 - "rjmp L%=end" "\n\t" // 5 - "rjmp L%=minus2" "\n\t" // 6 - "rjmp L%=plus1" "\n\t" // 7 - "rjmp L%=plus1" "\n\t" // 8 - "rjmp L%=minus2" "\n\t" // 9 - "rjmp L%=end" "\n\t" // 10 - "rjmp L%=minus1" "\n\t" // 11 - "rjmp L%=plus2" "\n\t" // 12 - "rjmp L%=minus1" "\n\t" // 13 - "rjmp L%=plus1" "\n\t" // 14 - "rjmp L%=end" "\n\t" // 15 - "L%=minus2:" "\n\t" - "subi r22, 2" "\n\t" - "sbci r23, 0" "\n\t" - "sbci r24, 0" "\n\t" - "sbci r25, 0" "\n\t" - "rjmp L%=store" "\n\t" - "L%=minus1:" "\n\t" - "subi r22, 1" "\n\t" - "sbci r23, 0" "\n\t" - "sbci r24, 0" "\n\t" - "sbci r25, 0" "\n\t" - "rjmp L%=store" "\n\t" - "L%=plus2:" "\n\t" - "subi r22, 254" "\n\t" - "rjmp L%=z" "\n\t" - "L%=plus1:" "\n\t" - "subi r22, 255" "\n\t" - "L%=z:" "sbci r23, 255" "\n\t" - "sbci r24, 255" "\n\t" - "sbci r25, 255" "\n\t" - "L%=store:" "\n\t" - "st -X, r25" "\n\t" - "st -X, r24" "\n\t" - "st -X, r23" "\n\t" - "st -X, r22" "\n\t" - "L%=end:" "\n" - : : "x" (arg) : "r22", "r23", "r24", "r25", "r30", "r31"); -#else - uint8_t p1val = DIRECT_PIN_READ(arg->pin1_register, arg->pin1_bitmask); - uint8_t p2val = DIRECT_PIN_READ(arg->pin2_register, arg->pin2_bitmask); - uint8_t state = arg->state & 3; - if (p1val) state |= 4; - if (p2val) state |= 8; - arg->state = (state >> 2); - switch (state) { - case 1: case 7: case 8: case 14: - arg->position++; - return; - case 2: case 4: case 11: case 13: - arg->position--; - return; - case 3: case 12: - arg->position += 2; - return; - case 6: case 9: - arg->position -= 2; - return; - } -#endif -} - -#if defined(ENCODER_USE_INTERRUPTS) && !defined(ENCODER_OPTIMIZE_INTERRUPTS) - #ifdef CORE_INT0_PIN - void ENCODER_ISR_ATTR Encoder::isr0(void) { update(interruptArgs[0]); } - #endif - #ifdef CORE_INT1_PIN - void ENCODER_ISR_ATTR Encoder::isr1(void) { update(interruptArgs[1]); } - #endif - #ifdef CORE_INT2_PIN - void ENCODER_ISR_ATTR Encoder::isr2(void) { update(interruptArgs[2]); } - #endif - #ifdef CORE_INT3_PIN - void ENCODER_ISR_ATTR Encoder::isr3(void) { update(interruptArgs[3]); } - #endif - #ifdef CORE_INT4_PIN - void ENCODER_ISR_ATTR Encoder::isr4(void) { update(interruptArgs[4]); } - #endif - #ifdef CORE_INT5_PIN - void ENCODER_ISR_ATTR Encoder::isr5(void) { update(interruptArgs[5]); } - #endif - #ifdef CORE_INT6_PIN - void ENCODER_ISR_ATTR Encoder::isr6(void) { update(interruptArgs[6]); } - #endif - #ifdef CORE_INT7_PIN - void ENCODER_ISR_ATTR Encoder::isr7(void) { update(interruptArgs[7]); } - #endif - #ifdef CORE_INT8_PIN - void ENCODER_ISR_ATTR Encoder::isr8(void) { update(interruptArgs[8]); } - #endif - #ifdef CORE_INT9_PIN - void ENCODER_ISR_ATTR Encoder::isr9(void) { update(interruptArgs[9]); } - #endif - #ifdef CORE_INT10_PIN - void ENCODER_ISR_ATTR Encoder::isr10(void) { update(interruptArgs[10]); } - #endif - #ifdef CORE_INT11_PIN - void ENCODER_ISR_ATTR Encoder::isr11(void) { update(interruptArgs[11]); } - #endif - #ifdef CORE_INT12_PIN - void ENCODER_ISR_ATTR Encoder::isr12(void) { update(interruptArgs[12]); } - #endif - #ifdef CORE_INT13_PIN - void ENCODER_ISR_ATTR Encoder::isr13(void) { update(interruptArgs[13]); } - #endif - #ifdef CORE_INT14_PIN - void ENCODER_ISR_ATTR Encoder::isr14(void) { update(interruptArgs[14]); } - #endif - #ifdef CORE_INT15_PIN - void ENCODER_ISR_ATTR Encoder::isr15(void) { update(interruptArgs[15]); } - #endif - #ifdef CORE_INT16_PIN - void ENCODER_ISR_ATTR Encoder::isr16(void) { update(interruptArgs[16]); } - #endif - #ifdef CORE_INT17_PIN - void ENCODER_ISR_ATTR Encoder::isr17(void) { update(interruptArgs[17]); } - #endif - #ifdef CORE_INT18_PIN - void ENCODER_ISR_ATTR Encoder::isr18(void) { update(interruptArgs[18]); } - #endif - #ifdef CORE_INT19_PIN - void ENCODER_ISR_ATTR Encoder::isr19(void) { update(interruptArgs[19]); } - #endif - #ifdef CORE_INT20_PIN - void ENCODER_ISR_ATTR Encoder::isr20(void) { update(interruptArgs[20]); } - #endif - #ifdef CORE_INT21_PIN - void ENCODER_ISR_ATTR Encoder::isr21(void) { update(interruptArgs[21]); } - #endif - #ifdef CORE_INT22_PIN - void ENCODER_ISR_ATTR Encoder::isr22(void) { update(interruptArgs[22]); } - #endif - #ifdef CORE_INT23_PIN - void ENCODER_ISR_ATTR Encoder::isr23(void) { update(interruptArgs[23]); } - #endif - #ifdef CORE_INT24_PIN - void ENCODER_ISR_ATTR Encoder::isr24(void) { update(interruptArgs[24]); } - #endif - #ifdef CORE_INT25_PIN - void ENCODER_ISR_ATTR Encoder::isr25(void) { update(interruptArgs[25]); } - #endif - #ifdef CORE_INT26_PIN - void ENCODER_ISR_ATTR Encoder::isr26(void) { update(interruptArgs[26]); } - #endif - #ifdef CORE_INT27_PIN - void ENCODER_ISR_ATTR Encoder::isr27(void) { update(interruptArgs[27]); } - #endif - #ifdef CORE_INT28_PIN - void ENCODER_ISR_ATTR Encoder::isr28(void) { update(interruptArgs[28]); } - #endif - #ifdef CORE_INT29_PIN - void ENCODER_ISR_ATTR Encoder::isr29(void) { update(interruptArgs[29]); } - #endif - #ifdef CORE_INT30_PIN - void ENCODER_ISR_ATTR Encoder::isr30(void) { update(interruptArgs[30]); } - #endif - #ifdef CORE_INT31_PIN - void ENCODER_ISR_ATTR Encoder::isr31(void) { update(interruptArgs[31]); } - #endif - #ifdef CORE_INT32_PIN - void ENCODER_ISR_ATTR Encoder::isr32(void) { update(interruptArgs[32]); } - #endif - #ifdef CORE_INT33_PIN - void ENCODER_ISR_ATTR Encoder::isr33(void) { update(interruptArgs[33]); } - #endif - #ifdef CORE_INT34_PIN - void ENCODER_ISR_ATTR Encoder::isr34(void) { update(interruptArgs[34]); } - #endif - #ifdef CORE_INT35_PIN - void ENCODER_ISR_ATTR Encoder::isr35(void) { update(interruptArgs[35]); } - #endif - #ifdef CORE_INT36_PIN - void ENCODER_ISR_ATTR Encoder::isr36(void) { update(interruptArgs[36]); } - #endif - #ifdef CORE_INT37_PIN - void ENCODER_ISR_ATTR Encoder::isr37(void) { update(interruptArgs[37]); } - #endif - #ifdef CORE_INT38_PIN - void ENCODER_ISR_ATTR Encoder::isr38(void) { update(interruptArgs[38]); } - #endif - #ifdef CORE_INT39_PIN - void ENCODER_ISR_ATTR Encoder::isr39(void) { update(interruptArgs[39]); } - #endif - #ifdef CORE_INT40_PIN - void ENCODER_ISR_ATTR Encoder::isr40(void) { update(interruptArgs[40]); } - #endif - #ifdef CORE_INT41_PIN - void ENCODER_ISR_ATTR Encoder::isr41(void) { update(interruptArgs[41]); } - #endif - #ifdef CORE_INT42_PIN - void ENCODER_ISR_ATTR Encoder::isr42(void) { update(interruptArgs[42]); } - #endif - #ifdef CORE_INT43_PIN - void ENCODER_ISR_ATTR Encoder::isr43(void) { update(interruptArgs[43]); } - #endif - #ifdef CORE_INT44_PIN - void ENCODER_ISR_ATTR Encoder::isr44(void) { update(interruptArgs[44]); } - #endif - #ifdef CORE_INT45_PIN - void ENCODER_ISR_ATTR Encoder::isr45(void) { update(interruptArgs[45]); } - #endif - #ifdef CORE_INT46_PIN - void ENCODER_ISR_ATTR Encoder::isr46(void) { update(interruptArgs[46]); } - #endif - #ifdef CORE_INT47_PIN - void ENCODER_ISR_ATTR Encoder::isr47(void) { update(interruptArgs[47]); } - #endif - #ifdef CORE_INT48_PIN - void ENCODER_ISR_ATTR Encoder::isr48(void) { update(interruptArgs[48]); } - #endif - #ifdef CORE_INT49_PIN - void ENCODER_ISR_ATTR Encoder::isr49(void) { update(interruptArgs[49]); } - #endif - #ifdef CORE_INT50_PIN - void ENCODER_ISR_ATTR Encoder::isr50(void) { update(interruptArgs[50]); } - #endif - #ifdef CORE_INT51_PIN - void ENCODER_ISR_ATTR Encoder::isr51(void) { update(interruptArgs[51]); } - #endif - #ifdef CORE_INT52_PIN - void ENCODER_ISR_ATTR Encoder::isr52(void) { update(interruptArgs[52]); } - #endif - #ifdef CORE_INT53_PIN - void ENCODER_ISR_ATTR Encoder::isr53(void) { update(interruptArgs[53]); } - #endif - #ifdef CORE_INT54_PIN - void ENCODER_ISR_ATTR Encoder::isr54(void) { update(interruptArgs[54]); } - #endif - #ifdef CORE_INT55_PIN - void ENCODER_ISR_ATTR Encoder::isr55(void) { update(interruptArgs[55]); } - #endif - #ifdef CORE_INT56_PIN - void ENCODER_ISR_ATTR Encoder::isr56(void) { update(interruptArgs[56]); } - #endif - #ifdef CORE_INT57_PIN - void ENCODER_ISR_ATTR Encoder::isr57(void) { update(interruptArgs[57]); } - #endif - #ifdef CORE_INT58_PIN - void ENCODER_ISR_ATTR Encoder::isr58(void) { update(interruptArgs[58]); } - #endif - #ifdef CORE_INT59_PIN - void ENCODER_ISR_ATTR Encoder::isr59(void) { update(interruptArgs[59]); } - #endif -#endif diff --git a/Software/lib/Encoder/Encoder.h b/Software/lib/Encoder/Encoder.h deleted file mode 100644 index 02061a8d..00000000 --- a/Software/lib/Encoder/Encoder.h +++ /dev/null @@ -1,862 +0,0 @@ -/* Encoder Library, for measuring quadrature encoded signals - * http://www.pjrc.com/teensy/td_libs_Encoder.html - * Copyright (c) 2011,2013 PJRC.COM, LLC - Paul Stoffregen - * - * Version 1.2 - fix -2 bug in C-only code - * Version 1.1 - expand to support boards with up to 60 interrupts - * Version 1.0 - initial release - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - - -#ifndef Encoder_h_ -#define Encoder_h_ - -#if defined(ARDUINO) && ARDUINO >= 100 -#include "Arduino.h" -#elif defined(WIRING) -#include "Wiring.h" -#else -#include "WProgram.h" -#include "pins_arduino.h" -#endif - -#include "utility/direct_pin_read.h" - -#if defined(ENCODER_USE_INTERRUPTS) || !defined(ENCODER_DO_NOT_USE_INTERRUPTS) -#define ENCODER_USE_INTERRUPTS -#define ENCODER_ARGLIST_SIZE CORE_NUM_INTERRUPT -#include "utility/interrupt_pins.h" -#ifdef ENCODER_OPTIMIZE_INTERRUPTS -#include "utility/interrupt_config.h" -#endif -#else -#define ENCODER_ARGLIST_SIZE 0 -#endif - -// Use ICACHE_RAM_ATTR for ISRs to prevent ESP8266 resets -#if defined(ESP8266) || defined(ESP32) -#define ENCODER_ISR_ATTR IRAM_ATTR -#else -#define ENCODER_ISR_ATTR -#endif - - - -// All the data needed by interrupts is consolidated into this ugly struct -// to facilitate assembly language optimizing of the speed critical update. -// The assembly code uses auto-incrementing addressing modes, so the struct -// must remain in exactly this order. -typedef struct { - volatile IO_REG_TYPE * pin1_register; - volatile IO_REG_TYPE * pin2_register; - IO_REG_TYPE pin1_bitmask; - IO_REG_TYPE pin2_bitmask; - uint8_t state; - int32_t position; -} Encoder_internal_state_t; - -class Encoder -{ -public: - Encoder(uint8_t pin1, uint8_t pin2) { - pinMode(pin1, INPUT_PULLDOWN); - pinMode(pin2, INPUT_PULLDOWN); - encoder.pin1_register = PIN_TO_BASEREG(pin1); - encoder.pin1_bitmask = PIN_TO_BITMASK(pin1); - encoder.pin2_register = PIN_TO_BASEREG(pin2); - encoder.pin2_bitmask = PIN_TO_BITMASK(pin2); - encoder.position = 0; - // allow time for a passive R-C filter to charge - // through the pullup resistors, before reading - // the initial state - delayMicroseconds(2000); - uint8_t s = 0; - if (DIRECT_PIN_READ(encoder.pin1_register, encoder.pin1_bitmask)) s |= 1; - if (DIRECT_PIN_READ(encoder.pin2_register, encoder.pin2_bitmask)) s |= 2; - encoder.state = s; -#ifdef ENCODER_USE_INTERRUPTS - interrupts_in_use = attach_interrupt(pin1, &encoder); - interrupts_in_use += attach_interrupt(pin2, &encoder); -#endif - //update_finishup(); // to force linker to include the code (does not work) - } - - -#ifdef ENCODER_USE_INTERRUPTS - inline int32_t read() { - if (interrupts_in_use < 2) { - noInterrupts(); - update(&encoder); - } else { - noInterrupts(); - } - int32_t ret = encoder.position; - interrupts(); - return ret; - } - inline int32_t readAndReset() { - if (interrupts_in_use < 2) { - noInterrupts(); - update(&encoder); - } else { - noInterrupts(); - } - int32_t ret = encoder.position; - encoder.position = 0; - interrupts(); - return ret; - } - inline void write(int32_t p) { - noInterrupts(); - encoder.position = p; - interrupts(); - } -#else - inline int32_t read() { - update(&encoder); - return encoder.position; - } - inline int32_t readAndReset() { - update(&encoder); - int32_t ret = encoder.position; - encoder.position = 0; - return ret; - } - inline void write(int32_t p) { - encoder.position = p; - } -#endif -private: - Encoder_internal_state_t encoder; -#ifdef ENCODER_USE_INTERRUPTS - uint8_t interrupts_in_use; -#endif -public: - static Encoder_internal_state_t * interruptArgs[ENCODER_ARGLIST_SIZE]; - -// _______ _______ -// Pin1 ______| |_______| |______ Pin1 -// negative <--- _______ _______ __ --> positive -// Pin2 __| |_______| |_______| Pin2 - - // new new old old - // pin2 pin1 pin2 pin1 Result - // ---- ---- ---- ---- ------ - // 0 0 0 0 no movement - // 0 0 0 1 +1 - // 0 0 1 0 -1 - // 0 0 1 1 +2 (assume pin1 edges only) - // 0 1 0 0 -1 - // 0 1 0 1 no movement - // 0 1 1 0 -2 (assume pin1 edges only) - // 0 1 1 1 +1 - // 1 0 0 0 +1 - // 1 0 0 1 -2 (assume pin1 edges only) - // 1 0 1 0 no movement - // 1 0 1 1 -1 - // 1 1 0 0 +2 (assume pin1 edges only) - // 1 1 0 1 -1 - // 1 1 1 0 +1 - // 1 1 1 1 no movement -/* - // Simple, easy-to-read "documentation" version :-) - // - void update(void) { - uint8_t s = state & 3; - if (digitalRead(pin1)) s |= 4; - if (digitalRead(pin2)) s |= 8; - switch (s) { - case 0: case 5: case 10: case 15: - break; - case 1: case 7: case 8: case 14: - position++; break; - case 2: case 4: case 11: case 13: - position--; break; - case 3: case 12: - position += 2; break; - default: - position -= 2; break; - } - state = (s >> 2); - } -*/ - -public: - // update() is not meant to be called from outside Encoder, - // but it is public to allow static interrupt routines. - // DO NOT call update() directly from sketches. - static void update(Encoder_internal_state_t *arg); -private: -/* -#if defined(__AVR__) - // TODO: this must be a no inline function - // even noinline does not seem to solve difficult - // problems with this. Oh well, it was only meant - // to shrink code size - there's no performance - // improvement in this, only code size reduction. - __attribute__((noinline)) void update_finishup(void) { - asm volatile ( - "ldi r30, lo8(pm(Ltable))" "\n\t" - "ldi r31, hi8(pm(Ltable))" "\n\t" - "Ltable:" "\n\t" - "rjmp L%=end" "\n\t" // 0 - "rjmp L%=plus1" "\n\t" // 1 - "rjmp L%=minus1" "\n\t" // 2 - "rjmp L%=plus2" "\n\t" // 3 - "rjmp L%=minus1" "\n\t" // 4 - "rjmp L%=end" "\n\t" // 5 - "rjmp L%=minus2" "\n\t" // 6 - "rjmp L%=plus1" "\n\t" // 7 - "rjmp L%=plus1" "\n\t" // 8 - "rjmp L%=minus2" "\n\t" // 9 - "rjmp L%=end" "\n\t" // 10 - "rjmp L%=minus1" "\n\t" // 11 - "rjmp L%=plus2" "\n\t" // 12 - "rjmp L%=minus1" "\n\t" // 13 - "rjmp L%=plus1" "\n\t" // 14 - "rjmp L%=end" "\n\t" // 15 - "L%=minus2:" "\n\t" - "subi r22, 2" "\n\t" - "sbci r23, 0" "\n\t" - "sbci r24, 0" "\n\t" - "sbci r25, 0" "\n\t" - "rjmp L%=store" "\n\t" - "L%=minus1:" "\n\t" - "subi r22, 1" "\n\t" - "sbci r23, 0" "\n\t" - "sbci r24, 0" "\n\t" - "sbci r25, 0" "\n\t" - "rjmp L%=store" "\n\t" - "L%=plus2:" "\n\t" - "subi r22, 254" "\n\t" - "rjmp L%=z" "\n\t" - "L%=plus1:" "\n\t" - "subi r22, 255" "\n\t" - "L%=z:" "sbci r23, 255" "\n\t" - "sbci r24, 255" "\n\t" - "sbci r25, 255" "\n\t" - "L%=store:" "\n\t" - "st -X, r25" "\n\t" - "st -X, r24" "\n\t" - "st -X, r23" "\n\t" - "st -X, r22" "\n\t" - "L%=end:" "\n" - : : : "r22", "r23", "r24", "r25", "r30", "r31"); - } -#endif -*/ - - -#ifdef ENCODER_USE_INTERRUPTS - // this giant function is an unfortunate consequence of Arduino's - // attachInterrupt function not supporting any way to pass a pointer - // or other context to the attached function. - static uint8_t attach_interrupt(uint8_t pin, Encoder_internal_state_t *state) { - switch (pin) { - #ifdef CORE_INT0_PIN - case CORE_INT0_PIN: - interruptArgs[0] = state; - attachInterrupt(0, isr0, CHANGE); - break; - #endif - #ifdef CORE_INT1_PIN - case CORE_INT1_PIN: - interruptArgs[1] = state; - attachInterrupt(1, isr1, CHANGE); - break; - #endif - #ifdef CORE_INT2_PIN - case CORE_INT2_PIN: - interruptArgs[2] = state; - attachInterrupt(2, isr2, CHANGE); - break; - #endif - #ifdef CORE_INT3_PIN - case CORE_INT3_PIN: - interruptArgs[3] = state; - attachInterrupt(3, isr3, CHANGE); - break; - #endif - #ifdef CORE_INT4_PIN - case CORE_INT4_PIN: - interruptArgs[4] = state; - attachInterrupt(4, isr4, CHANGE); - break; - #endif - #ifdef CORE_INT5_PIN - case CORE_INT5_PIN: - interruptArgs[5] = state; - attachInterrupt(5, isr5, CHANGE); - break; - #endif - #ifdef CORE_INT6_PIN - case CORE_INT6_PIN: - interruptArgs[6] = state; - attachInterrupt(6, isr6, CHANGE); - break; - #endif - #ifdef CORE_INT7_PIN - case CORE_INT7_PIN: - interruptArgs[7] = state; - attachInterrupt(7, isr7, CHANGE); - break; - #endif - #ifdef CORE_INT8_PIN - case CORE_INT8_PIN: - interruptArgs[8] = state; - attachInterrupt(8, isr8, CHANGE); - break; - #endif - #ifdef CORE_INT9_PIN - case CORE_INT9_PIN: - interruptArgs[9] = state; - attachInterrupt(9, isr9, CHANGE); - break; - #endif - #ifdef CORE_INT10_PIN - case CORE_INT10_PIN: - interruptArgs[10] = state; - attachInterrupt(10, isr10, CHANGE); - break; - #endif - #ifdef CORE_INT11_PIN - case CORE_INT11_PIN: - interruptArgs[11] = state; - attachInterrupt(11, isr11, CHANGE); - break; - #endif - #ifdef CORE_INT12_PIN - case CORE_INT12_PIN: - interruptArgs[12] = state; - attachInterrupt(12, isr12, CHANGE); - break; - #endif - #ifdef CORE_INT13_PIN - case CORE_INT13_PIN: - interruptArgs[13] = state; - attachInterrupt(13, isr13, CHANGE); - break; - #endif - #ifdef CORE_INT14_PIN - case CORE_INT14_PIN: - interruptArgs[14] = state; - attachInterrupt(14, isr14, CHANGE); - break; - #endif - #ifdef CORE_INT15_PIN - case CORE_INT15_PIN: - interruptArgs[15] = state; - attachInterrupt(15, isr15, CHANGE); - break; - #endif - #ifdef CORE_INT16_PIN - case CORE_INT16_PIN: - interruptArgs[16] = state; - attachInterrupt(16, isr16, CHANGE); - break; - #endif - #ifdef CORE_INT17_PIN - case CORE_INT17_PIN: - interruptArgs[17] = state; - attachInterrupt(17, isr17, CHANGE); - break; - #endif - #ifdef CORE_INT18_PIN - case CORE_INT18_PIN: - interruptArgs[18] = state; - attachInterrupt(18, isr18, CHANGE); - break; - #endif - #ifdef CORE_INT19_PIN - case CORE_INT19_PIN: - interruptArgs[19] = state; - attachInterrupt(19, isr19, CHANGE); - break; - #endif - #ifdef CORE_INT20_PIN - case CORE_INT20_PIN: - interruptArgs[20] = state; - attachInterrupt(20, isr20, CHANGE); - break; - #endif - #ifdef CORE_INT21_PIN - case CORE_INT21_PIN: - interruptArgs[21] = state; - attachInterrupt(21, isr21, CHANGE); - break; - #endif - #ifdef CORE_INT22_PIN - case CORE_INT22_PIN: - interruptArgs[22] = state; - attachInterrupt(22, isr22, CHANGE); - break; - #endif - #ifdef CORE_INT23_PIN - case CORE_INT23_PIN: - interruptArgs[23] = state; - attachInterrupt(23, isr23, CHANGE); - break; - #endif - #ifdef CORE_INT24_PIN - case CORE_INT24_PIN: - interruptArgs[24] = state; - attachInterrupt(24, isr24, CHANGE); - break; - #endif - #ifdef CORE_INT25_PIN - case CORE_INT25_PIN: - interruptArgs[25] = state; - attachInterrupt(25, isr25, CHANGE); - break; - #endif - #ifdef CORE_INT26_PIN - case CORE_INT26_PIN: - interruptArgs[26] = state; - attachInterrupt(26, isr26, CHANGE); - break; - #endif - #ifdef CORE_INT27_PIN - case CORE_INT27_PIN: - interruptArgs[27] = state; - attachInterrupt(27, isr27, CHANGE); - break; - #endif - #ifdef CORE_INT28_PIN - case CORE_INT28_PIN: - interruptArgs[28] = state; - attachInterrupt(28, isr28, CHANGE); - break; - #endif - #ifdef CORE_INT29_PIN - case CORE_INT29_PIN: - interruptArgs[29] = state; - attachInterrupt(29, isr29, CHANGE); - break; - #endif - - #ifdef CORE_INT30_PIN - case CORE_INT30_PIN: - interruptArgs[30] = state; - attachInterrupt(30, isr30, CHANGE); - break; - #endif - #ifdef CORE_INT31_PIN - case CORE_INT31_PIN: - interruptArgs[31] = state; - attachInterrupt(31, isr31, CHANGE); - break; - #endif - #ifdef CORE_INT32_PIN - case CORE_INT32_PIN: - interruptArgs[32] = state; - attachInterrupt(32, isr32, CHANGE); - break; - #endif - #ifdef CORE_INT33_PIN - case CORE_INT33_PIN: - interruptArgs[33] = state; - attachInterrupt(33, isr33, CHANGE); - break; - #endif - #ifdef CORE_INT34_PIN - case CORE_INT34_PIN: - interruptArgs[34] = state; - attachInterrupt(34, isr34, CHANGE); - break; - #endif - #ifdef CORE_INT35_PIN - case CORE_INT35_PIN: - interruptArgs[35] = state; - attachInterrupt(35, isr35, CHANGE); - break; - #endif - #ifdef CORE_INT36_PIN - case CORE_INT36_PIN: - interruptArgs[36] = state; - attachInterrupt(36, isr36, CHANGE); - break; - #endif - #ifdef CORE_INT37_PIN - case CORE_INT37_PIN: - interruptArgs[37] = state; - attachInterrupt(37, isr37, CHANGE); - break; - #endif - #ifdef CORE_INT38_PIN - case CORE_INT38_PIN: - interruptArgs[38] = state; - attachInterrupt(38, isr38, CHANGE); - break; - #endif - #ifdef CORE_INT39_PIN - case CORE_INT39_PIN: - interruptArgs[39] = state; - attachInterrupt(39, isr39, CHANGE); - break; - #endif - #ifdef CORE_INT40_PIN - case CORE_INT40_PIN: - interruptArgs[40] = state; - attachInterrupt(40, isr40, CHANGE); - break; - #endif - #ifdef CORE_INT41_PIN - case CORE_INT41_PIN: - interruptArgs[41] = state; - attachInterrupt(41, isr41, CHANGE); - break; - #endif - #ifdef CORE_INT42_PIN - case CORE_INT42_PIN: - interruptArgs[42] = state; - attachInterrupt(42, isr42, CHANGE); - break; - #endif - #ifdef CORE_INT43_PIN - case CORE_INT43_PIN: - interruptArgs[43] = state; - attachInterrupt(43, isr43, CHANGE); - break; - #endif - #ifdef CORE_INT44_PIN - case CORE_INT44_PIN: - interruptArgs[44] = state; - attachInterrupt(44, isr44, CHANGE); - break; - #endif - #ifdef CORE_INT45_PIN - case CORE_INT45_PIN: - interruptArgs[45] = state; - attachInterrupt(45, isr45, CHANGE); - break; - #endif - #ifdef CORE_INT46_PIN - case CORE_INT46_PIN: - interruptArgs[46] = state; - attachInterrupt(46, isr46, CHANGE); - break; - #endif - #ifdef CORE_INT47_PIN - case CORE_INT47_PIN: - interruptArgs[47] = state; - attachInterrupt(47, isr47, CHANGE); - break; - #endif - #ifdef CORE_INT48_PIN - case CORE_INT48_PIN: - interruptArgs[48] = state; - attachInterrupt(48, isr48, CHANGE); - break; - #endif - #ifdef CORE_INT49_PIN - case CORE_INT49_PIN: - interruptArgs[49] = state; - attachInterrupt(49, isr49, CHANGE); - break; - #endif - #ifdef CORE_INT50_PIN - case CORE_INT50_PIN: - interruptArgs[50] = state; - attachInterrupt(50, isr50, CHANGE); - break; - #endif - #ifdef CORE_INT51_PIN - case CORE_INT51_PIN: - interruptArgs[51] = state; - attachInterrupt(51, isr51, CHANGE); - break; - #endif - #ifdef CORE_INT52_PIN - case CORE_INT52_PIN: - interruptArgs[52] = state; - attachInterrupt(52, isr52, CHANGE); - break; - #endif - #ifdef CORE_INT53_PIN - case CORE_INT53_PIN: - interruptArgs[53] = state; - attachInterrupt(53, isr53, CHANGE); - break; - #endif - #ifdef CORE_INT54_PIN - case CORE_INT54_PIN: - interruptArgs[54] = state; - attachInterrupt(54, isr54, CHANGE); - break; - #endif - #ifdef CORE_INT55_PIN - case CORE_INT55_PIN: - interruptArgs[55] = state; - attachInterrupt(55, isr55, CHANGE); - break; - #endif - #ifdef CORE_INT56_PIN - case CORE_INT56_PIN: - interruptArgs[56] = state; - attachInterrupt(56, isr56, CHANGE); - break; - #endif - #ifdef CORE_INT57_PIN - case CORE_INT57_PIN: - interruptArgs[57] = state; - attachInterrupt(57, isr57, CHANGE); - break; - #endif - #ifdef CORE_INT58_PIN - case CORE_INT58_PIN: - interruptArgs[58] = state; - attachInterrupt(58, isr58, CHANGE); - break; - #endif - #ifdef CORE_INT59_PIN - case CORE_INT59_PIN: - interruptArgs[59] = state; - attachInterrupt(59, isr59, CHANGE); - break; - #endif - default: - return 0; - } - return 1; - } -#endif // ENCODER_USE_INTERRUPTS - - -#if defined(ENCODER_USE_INTERRUPTS) && !defined(ENCODER_OPTIMIZE_INTERRUPTS) - #ifdef CORE_INT0_PIN - static void isr0(void); - #endif - #ifdef CORE_INT1_PIN - static void isr1(void); - #endif - #ifdef CORE_INT2_PIN - static void isr2(void); - #endif - #ifdef CORE_INT3_PIN - static void isr3(void); - #endif - #ifdef CORE_INT4_PIN - static void isr4(void); - #endif - #ifdef CORE_INT5_PIN - static void isr5(void); - #endif - #ifdef CORE_INT6_PIN - static void isr6(void); - #endif - #ifdef CORE_INT7_PIN - static void isr7(void); - #endif - #ifdef CORE_INT8_PIN - static void isr8(void); - #endif - #ifdef CORE_INT9_PIN - static void isr9(void); - #endif - #ifdef CORE_INT10_PIN - static void isr10(void); - #endif - #ifdef CORE_INT11_PIN - static void isr11(void); - #endif - #ifdef CORE_INT12_PIN - static void isr12(void); - #endif - #ifdef CORE_INT13_PIN - static void isr13(void); - #endif - #ifdef CORE_INT14_PIN - static void isr14(void); - #endif - #ifdef CORE_INT15_PIN - static void isr15(void); - #endif - #ifdef CORE_INT16_PIN - static void isr16(void); - #endif - #ifdef CORE_INT17_PIN - static void isr17(void); - #endif - #ifdef CORE_INT18_PIN - static void isr18(void); - #endif - #ifdef CORE_INT19_PIN - static void isr19(void); - #endif - #ifdef CORE_INT20_PIN - static void isr20(void); - #endif - #ifdef CORE_INT21_PIN - static void isr21(void); - #endif - #ifdef CORE_INT22_PIN - static void isr22(void); - #endif - #ifdef CORE_INT23_PIN - static void isr23(void); - #endif - #ifdef CORE_INT24_PIN - static void isr24(void); - #endif - #ifdef CORE_INT25_PIN - static void isr25(void); - #endif - #ifdef CORE_INT26_PIN - static void isr26(void); - #endif - #ifdef CORE_INT27_PIN - static void isr27(void); - #endif - #ifdef CORE_INT28_PIN - static void isr28(void); - #endif - #ifdef CORE_INT29_PIN - static void isr29(void); - #endif - #ifdef CORE_INT30_PIN - static void isr30(void); - #endif - #ifdef CORE_INT31_PIN - static void isr31(void); - #endif - #ifdef CORE_INT32_PIN - static void isr32(void); - #endif - #ifdef CORE_INT33_PIN - static void isr33(void); - #endif - #ifdef CORE_INT34_PIN - static void isr34(void); - #endif - #ifdef CORE_INT35_PIN - static void isr35(void); - #endif - #ifdef CORE_INT36_PIN - static void isr36(void); - #endif - #ifdef CORE_INT37_PIN - static void isr37(void); - #endif - #ifdef CORE_INT38_PIN - static void isr38(void); - #endif - #ifdef CORE_INT39_PIN - static void isr39(void); - #endif - #ifdef CORE_INT40_PIN - static void isr40(void); - #endif - #ifdef CORE_INT41_PIN - static void isr41(void); - #endif - #ifdef CORE_INT42_PIN - static void isr42(void); - #endif - #ifdef CORE_INT43_PIN - static void isr43(void); - #endif - #ifdef CORE_INT44_PIN - static void isr44(void); - #endif - #ifdef CORE_INT45_PIN - static void isr45(void); - #endif - #ifdef CORE_INT46_PIN - static void isr46(void); - #endif - #ifdef CORE_INT47_PIN - static void isr47(void); - #endif - #ifdef CORE_INT48_PIN - static void isr48(void); - #endif - #ifdef CORE_INT49_PIN - static void isr49(void); - #endif - #ifdef CORE_INT50_PIN - static void isr50(void); - #endif - #ifdef CORE_INT51_PIN - static void isr51(void); - #endif - #ifdef CORE_INT52_PIN - static void isr52(void); - #endif - #ifdef CORE_INT53_PIN - static void isr53(void); - #endif - #ifdef CORE_INT54_PIN - static void isr54(void); - #endif - #ifdef CORE_INT55_PIN - static void isr55(void); - #endif - #ifdef CORE_INT56_PIN - static void isr56(void); - #endif - #ifdef CORE_INT57_PIN - static void isr57(void); - #endif - #ifdef CORE_INT58_PIN - static void isr58(void); - #endif - #ifdef CORE_INT59_PIN - static void isr59(void); - #endif -#endif -}; - -#if defined(ENCODER_USE_INTERRUPTS) && defined(ENCODER_OPTIMIZE_INTERRUPTS) -#if defined(__AVR__) -#if defined(INT0_vect) && CORE_NUM_INTERRUPT > 0 -ISR(INT0_vect) { Encoder::update(Encoder::interruptArgs[SCRAMBLE_INT_ORDER(0)]); } -#endif -#if defined(INT1_vect) && CORE_NUM_INTERRUPT > 1 -ISR(INT1_vect) { Encoder::update(Encoder::interruptArgs[SCRAMBLE_INT_ORDER(1)]); } -#endif -#if defined(INT2_vect) && CORE_NUM_INTERRUPT > 2 -ISR(INT2_vect) { Encoder::update(Encoder::interruptArgs[SCRAMBLE_INT_ORDER(2)]); } -#endif -#if defined(INT3_vect) && CORE_NUM_INTERRUPT > 3 -ISR(INT3_vect) { Encoder::update(Encoder::interruptArgs[SCRAMBLE_INT_ORDER(3)]); } -#endif -#if defined(INT4_vect) && CORE_NUM_INTERRUPT > 4 -ISR(INT4_vect) { Encoder::update(Encoder::interruptArgs[SCRAMBLE_INT_ORDER(4)]); } -#endif -#if defined(INT5_vect) && CORE_NUM_INTERRUPT > 5 -ISR(INT5_vect) { Encoder::update(Encoder::interruptArgs[SCRAMBLE_INT_ORDER(5)]); } -#endif -#if defined(INT6_vect) && CORE_NUM_INTERRUPT > 6 -ISR(INT6_vect) { Encoder::update(Encoder::interruptArgs[SCRAMBLE_INT_ORDER(6)]); } -#endif -#if defined(INT7_vect) && CORE_NUM_INTERRUPT > 7 -ISR(INT7_vect) { Encoder::update(Encoder::interruptArgs[SCRAMBLE_INT_ORDER(7)]); } -#endif -#endif // AVR -#if defined(attachInterrupt) -// Don't intefere with other libraries or sketch use of attachInterrupt() -// https://github.com/PaulStoffregen/Encoder/issues/8 -#undef attachInterrupt -#endif -#endif // ENCODER_OPTIMIZE_INTERRUPTS - - -#endif diff --git a/Software/lib/Encoder/README.md b/Software/lib/Encoder/README.md deleted file mode 100644 index 6a4f6383..00000000 --- a/Software/lib/Encoder/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# Encoder Library - -Encoder counts pulses from quadrature encoded signals, which are commonly available from rotary knobs, motor or shaft sensors and other position sensors. - -http://www.pjrc.com/teensy/td_libs_Encoder.html - -http://www.youtube.com/watch?v=2puhIong-cs - -![Encoder Knobs Demo](http://www.pjrc.com/teensy/td_libs_Encoder_1.jpg) diff --git a/Software/lib/Encoder/docs/issue_template.md b/Software/lib/Encoder/docs/issue_template.md deleted file mode 100644 index 06109925..00000000 --- a/Software/lib/Encoder/docs/issue_template.md +++ /dev/null @@ -1,64 +0,0 @@ -Please use this form only to report code defects or bugs. - -For any question, even questions directly pertaining to this code, post your question on the forums related to the board you are using. - -Arduino: forum.arduino.cc -Teensy: forum.pjrc.com -ESP8266: www.esp8266.com -ESP32: www.esp32.com -Adafruit Feather/Metro/Trinket: forums.adafruit.com -Particle Photon: community.particle.io - -If you are experiencing trouble but not certain of the cause, or need help using this code, ask on the appropriate forum. This is not the place to ask for support or help, even directly related to this code. Only use this form you are certain you have discovered a defect in this code! - -Please verify the problem occurs when using the very latest version, using the newest version of Arduino and any other related software. - - ------------------------------ Remove above ----------------------------- - - - -### Description - -Describe your problem. - - - -### Steps To Reproduce Problem - -Please give detailed instructions needed for anyone to attempt to reproduce the problem. - - - -### Hardware & Software - -Board -Shields / modules used -Arduino IDE version -Teensyduino version (if using Teensy) -Version info & package name (from Tools > Boards > Board Manager) -Operating system & version -Any other software or hardware? - - -### Arduino Sketch - -```cpp -// Change the code below by your sketch (please try to give the smallest code which demonstrates the problem) -#include - -// libraries: give links/details so anyone can compile your code for the same result - -void setup() { -} - -void loop() { -} -``` - - -### Errors or Incorrect Output - -If you see any errors or incorrect output, please show it here. Please use copy & paste to give an exact copy of the message. Details matter, so please show (not merely describe) the actual message or error exactly as it appears. - - diff --git a/Software/lib/Encoder/examples/Basic/Basic.pde b/Software/lib/Encoder/examples/Basic/Basic.pde deleted file mode 100644 index 3394b585..00000000 --- a/Software/lib/Encoder/examples/Basic/Basic.pde +++ /dev/null @@ -1,29 +0,0 @@ -/* Encoder Library - Basic Example - * http://www.pjrc.com/teensy/td_libs_Encoder.html - * - * This example code is in the public domain. - */ - -#include - -// Change these two numbers to the pins connected to your encoder. -// Best Performance: both pins have interrupt capability -// Good Performance: only the first pin has interrupt capability -// Low Performance: neither pin has interrupt capability -Encoder myEnc(5, 6); -// avoid using pins with LEDs attached - -void setup() { - Serial.begin(9600); - Serial.println("Basic Encoder Test:"); -} - -long oldPosition = -999; - -void loop() { - long newPosition = myEnc.read(); - if (newPosition != oldPosition) { - oldPosition = newPosition; - Serial.println(newPosition); - } -} diff --git a/Software/lib/Encoder/examples/NoInterrupts/NoInterrupts.pde b/Software/lib/Encoder/examples/NoInterrupts/NoInterrupts.pde deleted file mode 100644 index b890652e..00000000 --- a/Software/lib/Encoder/examples/NoInterrupts/NoInterrupts.pde +++ /dev/null @@ -1,46 +0,0 @@ -/* Encoder Library - NoInterrupts Example - * http://www.pjrc.com/teensy/td_libs_Encoder.html - * - * This example code is in the public domain. - */ - -// If you define ENCODER_DO_NOT_USE_INTERRUPTS *before* including -// Encoder, the library will never use interrupts. This is mainly -// useful to reduce the size of the library when you are using it -// with pins that do not support interrupts. Without interrupts, -// your program must call the read() function rapidly, or risk -// missing changes in position. -#define ENCODER_DO_NOT_USE_INTERRUPTS -#include - -// Beware of Serial.print() speed. Without interrupts, if you -// transmit too much data with Serial.print() it can slow your -// reading from Encoder. Arduino 1.0 has improved transmit code. -// Using the fastest baud rate also helps. Teensy has USB packet -// buffering. But all boards can experience problems if you print -// too much and fill up buffers. - -// Change these two numbers to the pins connected to your encoder. -// With ENCODER_DO_NOT_USE_INTERRUPTS, no interrupts are ever -// used, even if the pin has interrupt capability -Encoder myEnc(5, 6); -// avoid using pins with LEDs attached - -void setup() { - Serial.begin(9600); - Serial.println("Basic NoInterrupts Test:"); -} - -long position = -999; - -void loop() { - long newPos = myEnc.read(); - if (newPos != position) { - position = newPos; - Serial.println(position); - } - // With any substantial delay added, Encoder can only track - // very slow motion. You may uncomment this line to see - // how badly a delay affects your encoder. - //delay(50); -} diff --git a/Software/lib/Encoder/examples/SpeedTest/SpeedTest.pde b/Software/lib/Encoder/examples/SpeedTest/SpeedTest.pde deleted file mode 100644 index f136fbb2..00000000 --- a/Software/lib/Encoder/examples/SpeedTest/SpeedTest.pde +++ /dev/null @@ -1,113 +0,0 @@ -/* Encoder Library - SpeedTest - for measuring maximum Encoder speed - * http://www.pjrc.com/teensy/td_libs_Encoder.html - * - * This example code is in the public domain. - */ - - -// This SpeedTest example provides a simple way to verify how much -// CPU time Encoder is consuming. Connect a DC voltmeter to the -// output pin and measure the voltage while the encoder is stopped -// or running at a very slow speed. Even though the pin is rapidly -// pulsing, a DC voltmeter will show the average voltage. Due to -// software timing, it will read a number much less than a steady -// logic high, but this number will give you a baseline reading -// for output with minimal interrupt overhead. Then increase the -// encoder speed. The voltage will decrease as the processor spends -// more time in Encoder's interrupt routines counting the pulses -// and less time pulsing the output pin. When the voltage is -// close to zero and will not decrease any farther, you have reached -// the absolute speed limit. Or, if using a mechanical system where -// you reach a speed limit imposed by your motors or other hardware, -// the amount this voltage has decreased, compared to the baseline, -// should give you a good approximation of the portion of available -// CPU time Encoder is consuming at your maximum speed. - -// Encoder requires low latency interrupt response. Available CPU -// time does NOT necessarily prove or guarantee correct performance. -// If another library, like NewSoftSerial, is disabling interrupts -// for lengthy periods of time, Encoder can be prevented from -// properly counting the intput signals while interrupt are disabled. - - -// This optional setting causes Encoder to use more optimized code, -// but the downside is a conflict if any other part of your sketch -// or any other library you're using requires attachInterrupt(). -// It must be defined before Encoder.h is included. -//#define ENCODER_OPTIMIZE_INTERRUPTS - -#include -#include "pins_arduino.h" - -// Change these two numbers to the pins connected to your encoder -// or shift register circuit which emulates a quadrature encoder -// case 1: both pins are interrupts -// case 2: only first pin used as interrupt -Encoder myEnc(5, 6); - -// Connect a DC voltmeter to this pin. -const int outputPin = 12; - -/* This simple circuit, using a Dual Flip-Flop chip, can emulate - quadrature encoder signals. The clock can come from a fancy - function generator or a cheap 555 timer chip. The clock - frequency can be measured with another board running FreqCount - http://www.pjrc.com/teensy/td_libs_FreqCount.html - - +5V - | Quadrature Encoder Signal Emulator - Clock | - Input o----*-------------------------- ---------------------------o Output1 - | |14 | | - | _______|_______ | | _______________ - | | CD4013 | | | | CD4013 | - | 5 | | 1 | | 9 | | 13 - ---------| D Q |-----|----*----| D Q |------o Output2 - | | | | | | | - | | 3 | | | 11 | | - | ----|> Clk | ---------|> Clk | - | | | | | - | 6 | | 8 | | - | ----| S | ----| S | - | | | | | | | - | | 4 | _ | 2 | 10 | _ | 12 - | *----| R Q |--- *----| R Q |---- - | | | | | | | | - | | |_______________| | |_______________| | - | | | | | - | | | 7 | | - | | | | | - -------------------------------------------------------------- - | | | - | | | - ----- ----- ----- - --- --- --- - - - - -*/ - - -void setup() { - pinMode(outputPin, OUTPUT); -} - -#if defined(__AVR__) || defined(TEENSYDUINO) -#define REGTYPE unsigned char -#else -#define REGTYPE unsigned long -#endif - -void loop() { - volatile int count = 0; - volatile REGTYPE *reg = portOutputRegister(digitalPinToPort(outputPin)); - REGTYPE mask = digitalPinToBitMask(outputPin); - - while (1) { - myEnc.read(); // Read the encoder while interrupts are enabled. - noInterrupts(); - *reg |= mask; // Pulse the pin high, while interrupts are disabled. - count = count + 1; - *reg &= ~mask; - interrupts(); - } -} - diff --git a/Software/lib/Encoder/examples/TwoKnobs/TwoKnobs.pde b/Software/lib/Encoder/examples/TwoKnobs/TwoKnobs.pde deleted file mode 100644 index 306b33e7..00000000 --- a/Software/lib/Encoder/examples/TwoKnobs/TwoKnobs.pde +++ /dev/null @@ -1,46 +0,0 @@ -/* Encoder Library - TwoKnobs Example - * http://www.pjrc.com/teensy/td_libs_Encoder.html - * - * This example code is in the public domain. - */ - -#include - -// Change these pin numbers to the pins connected to your encoder. -// Best Performance: both pins have interrupt capability -// Good Performance: only the first pin has interrupt capability -// Low Performance: neither pin has interrupt capability -Encoder knobLeft(5, 6); -Encoder knobRight(7, 8); -// avoid using pins with LEDs attached - -void setup() { - Serial.begin(9600); - Serial.println("TwoKnobs Encoder Test:"); -} - -long positionLeft = -999; -long positionRight = -999; - -void loop() { - long newLeft, newRight; - newLeft = knobLeft.read(); - newRight = knobRight.read(); - if (newLeft != positionLeft || newRight != positionRight) { - Serial.print("Left = "); - Serial.print(newLeft); - Serial.print(", Right = "); - Serial.print(newRight); - Serial.println(); - positionLeft = newLeft; - positionRight = newRight; - } - // if a character is sent from the serial monitor, - // reset both back to zero. - if (Serial.available()) { - Serial.read(); - Serial.println("Reset both knobs to zero"); - knobLeft.write(0); - knobRight.write(0); - } -} diff --git a/Software/lib/Encoder/keywords.txt b/Software/lib/Encoder/keywords.txt deleted file mode 100644 index a4baa016..00000000 --- a/Software/lib/Encoder/keywords.txt +++ /dev/null @@ -1,4 +0,0 @@ -ENCODER_USE_INTERRUPTS LITERAL1 -ENCODER_OPTIMIZE_INTERRUPTS LITERAL1 -ENCODER_DO_NOT_USE_INTERRUPTS LITERAL1 -Encoder KEYWORD1 diff --git a/Software/lib/Encoder/library.json b/Software/lib/Encoder/library.json deleted file mode 100644 index 0a0f653e..00000000 --- a/Software/lib/Encoder/library.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "Encoder", - "keywords": "encoder, signal, pulse", - "description": "Encoder counts pulses from quadrature encoded signals, which are commonly available from rotary knobs, motor or shaft sensors and other position sensors", - "repository": - { - "type": "git", - "url": "https://github.com/PaulStoffregen/Encoder.git" - }, - "frameworks": "arduino", - "platforms": - [ - "atmelavr", - "teensy" - ] -} diff --git a/Software/lib/Encoder/library.properties b/Software/lib/Encoder/library.properties deleted file mode 100644 index 712abd4a..00000000 --- a/Software/lib/Encoder/library.properties +++ /dev/null @@ -1,10 +0,0 @@ -name=Encoder -version=1.4.2 -author=Paul Stoffregen -maintainer=Paul Stoffregen -sentence=Counts quadrature pulses from rotary & linear position encoders. -paragraph=Encoder counts pulses from quadrature encoded signals, which are commonly available from rotary knobs, motor or shaft sensors and other position sensors. -category=Signal Input/Output -url=http://www.pjrc.com/teensy/td_libs_Encoder.html -architectures=* - diff --git a/Software/lib/Encoder/utility/direct_pin_read.h b/Software/lib/Encoder/utility/direct_pin_read.h deleted file mode 100644 index d5b26ef6..00000000 --- a/Software/lib/Encoder/utility/direct_pin_read.h +++ /dev/null @@ -1,110 +0,0 @@ -#ifndef direct_pin_read_h_ -#define direct_pin_read_h_ - -#if defined(__AVR__) - -#define IO_REG_TYPE uint8_t -#define PIN_TO_BASEREG(pin) (portInputRegister(digitalPinToPort(pin))) -#define PIN_TO_BITMASK(pin) (digitalPinToBitMask(pin)) -#define DIRECT_PIN_READ(base, mask) (((*(base)) & (mask)) ? 1 : 0) - -#elif defined(TEENSYDUINO) && (defined(KINETISK) || defined(KINETISL)) - -#define IO_REG_TYPE uint8_t -#define PIN_TO_BASEREG(pin) (portInputRegister(digitalPinToPort(pin))) -#define PIN_TO_BITMASK(pin) (digitalPinToBitMask(pin)) -#define DIRECT_PIN_READ(base, mask) (((*(base)) & (mask)) ? 1 : 0) - -#elif defined(__IMXRT1052__) || defined(__IMXRT1062__) - -#define IO_REG_TYPE uint32_t -#define PIN_TO_BASEREG(pin) (portOutputRegister(pin)) -#define PIN_TO_BITMASK(pin) (digitalPinToBitMask(pin)) -#define DIRECT_PIN_READ(base, mask) (((*(base)) & (mask)) ? 1 : 0) - -#elif defined(__SAM3X8E__) // || defined(ESP8266) - -#define IO_REG_TYPE uint32_t -#define PIN_TO_BASEREG(pin) (portInputRegister(digitalPinToPort(pin))) -#define PIN_TO_BITMASK(pin) (digitalPinToBitMask(pin)) -#define DIRECT_PIN_READ(base, mask) (((*(base)) & (mask)) ? 1 : 0) - -#elif defined(__PIC32MX__) - -#define IO_REG_TYPE uint32_t -#define PIN_TO_BASEREG(pin) (portModeRegister(digitalPinToPort(pin))) -#define PIN_TO_BITMASK(pin) (digitalPinToBitMask(pin)) -#define DIRECT_PIN_READ(base, mask) (((*(base+4)) & (mask)) ? 1 : 0) - -/* ESP8266 v2.0.0 Arduino workaround for bug https://github.com/esp8266/Arduino/issues/1110 */ -#elif defined(ESP8266) - -#define IO_REG_TYPE uint32_t -#define PIN_TO_BASEREG(pin) ((volatile uint32_t *)(0x60000000+(0x318))) -#define PIN_TO_BITMASK(pin) (digitalPinToBitMask(pin)) -#define DIRECT_PIN_READ(base, mask) (((*(base)) & (mask)) ? 1 : 0) - -/* ESP32 Arduino (https://github.com/espressif/arduino-esp32) */ -#elif defined(ESP32) - -#define IO_REG_TYPE uint32_t -#define PIN_TO_BASEREG(pin) (portInputRegister(digitalPinToPort(pin))) -#define PIN_TO_BITMASK(pin) (digitalPinToBitMask(pin)) -#define DIRECT_PIN_READ(base, mask) (((*(base)) & (mask)) ? 1 : 0) - -#elif defined(__SAMD21G18A__) || defined(__SAMD21E18A__) - -#define IO_REG_TYPE uint32_t -#define PIN_TO_BASEREG(pin) portModeRegister(digitalPinToPort(pin)) -#define PIN_TO_BITMASK(pin) (digitalPinToBitMask(pin)) -#define DIRECT_PIN_READ(base, mask) (((*((base)+8)) & (mask)) ? 1 : 0) - -#elif defined(__SAMD51__) - -#define IO_REG_TYPE uint32_t -#define PIN_TO_BASEREG(pin) portInputRegister(digitalPinToPort(pin)) -#define PIN_TO_BITMASK(pin) (digitalPinToBitMask(pin)) -#define DIRECT_PIN_READ(base, mask) (((*(base)) & (mask)) ? 1 : 0) - -#elif defined(RBL_NRF51822) - -#define IO_REG_TYPE uint32_t -#define PIN_TO_BASEREG(pin) (0) -#define PIN_TO_BITMASK(pin) (pin) -#define DIRECT_PIN_READ(base, pin) nrf_gpio_pin_read(pin) - -#elif defined(ARDUINO_ARCH_NRF52840) -#define IO_REG_TYPE uint32_t -#define PIN_TO_BASEREG(pin) (0) -#define PIN_TO_BITMASK(pin) digitalPinToPinName(pin) -#define DIRECT_PIN_READ(base, pin) nrf_gpio_pin_read(pin) - -#elif defined(__arc__) /* Arduino101/Genuino101 specifics */ - -#include "scss_registers.h" -#include "portable.h" -#include "avr/pgmspace.h" -#define GPIO_ID(pin) (g_APinDescription[pin].ulGPIOId) -#define GPIO_TYPE(pin) (g_APinDescription[pin].ulGPIOType) -#define GPIO_BASE(pin) (g_APinDescription[pin].ulGPIOBase) -#define EXT_PORT_OFFSET_SS 0x0A -#define EXT_PORT_OFFSET_SOC 0x50 -#define PIN_TO_BASEREG(pin) ((volatile uint32_t *)g_APinDescription[pin].ulGPIOBase) -#define PIN_TO_BITMASK(pin) pin -#define IO_REG_TYPE uint32_t -static inline __attribute__((always_inline)) -IO_REG_TYPE directRead(volatile IO_REG_TYPE *base, IO_REG_TYPE pin) -{ - IO_REG_TYPE ret; - if (SS_GPIO == GPIO_TYPE(pin)) { - ret = READ_ARC_REG(((IO_REG_TYPE)base + EXT_PORT_OFFSET_SS)); - } else { - ret = MMIO_REG_VAL_FROM_BASE((IO_REG_TYPE)base, EXT_PORT_OFFSET_SOC); - } - return ((ret >> GPIO_ID(pin)) & 0x01); -} -#define DIRECT_PIN_READ(base, pin) directRead(base, pin) - -#endif - -#endif diff --git a/Software/lib/Encoder/utility/interrupt_config.h b/Software/lib/Encoder/utility/interrupt_config.h deleted file mode 100644 index cde6adf2..00000000 --- a/Software/lib/Encoder/utility/interrupt_config.h +++ /dev/null @@ -1,87 +0,0 @@ -#if defined(__AVR__) - -#include -#include - -#define attachInterrupt(num, func, mode) enableInterrupt(num) -#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) -#define SCRAMBLE_INT_ORDER(num) ((num < 4) ? num + 2 : ((num < 6) ? num - 4 : num)) -#define DESCRAMBLE_INT_ORDER(num) ((num < 2) ? num + 4 : ((num < 6) ? num - 2 : num)) -#else -#define SCRAMBLE_INT_ORDER(num) (num) -#define DESCRAMBLE_INT_ORDER(num) (num) -#endif - -static void enableInterrupt(uint8_t num) -{ - switch (DESCRAMBLE_INT_ORDER(num)) { - #if defined(EICRA) && defined(EIMSK) - case 0: - EICRA = (EICRA & 0xFC) | 0x01; - EIMSK |= 0x01; - return; - case 1: - EICRA = (EICRA & 0xF3) | 0x04; - EIMSK |= 0x02; - return; - case 2: - EICRA = (EICRA & 0xCF) | 0x10; - EIMSK |= 0x04; - return; - case 3: - EICRA = (EICRA & 0x3F) | 0x40; - EIMSK |= 0x08; - return; - #elif defined(MCUCR) && defined(GICR) - case 0: - MCUCR = (MCUCR & ~((1 << ISC00) | (1 << ISC01))) | (mode << ISC00); - GICR |= (1 << INT0); - return; - case 1: - MCUCR = (MCUCR & ~((1 << ISC10) | (1 << ISC11))) | (mode << ISC10); - GICR |= (1 << INT1); - return; - #elif defined(MCUCR) && defined(GIMSK) - case 0: - MCUCR = (MCUCR & ~((1 << ISC00) | (1 << ISC01))) | (mode << ISC00); - GIMSK |= (1 << INT0); - return; - case 1: - MCUCR = (MCUCR & ~((1 << ISC10) | (1 << ISC11))) | (mode << ISC10); - GIMSK |= (1 << INT1); - return; - #endif - #if defined(EICRB) && defined(EIMSK) - case 4: - EICRB = (EICRB & 0xFC) | 0x01; - EIMSK |= 0x10; - return; - case 5: - EICRB = (EICRB & 0xF3) | 0x04; - EIMSK |= 0x20; - return; - case 6: - EICRB = (EICRB & 0xCF) | 0x10; - EIMSK |= 0x40; - return; - case 7: - EICRB = (EICRB & 0x3F) | 0x40; - EIMSK |= 0x80; - return; - #endif - } -} - -#elif defined(__PIC32MX__) - -#ifdef ENCODER_OPTIMIZE_INTERRUPTS -#undef ENCODER_OPTIMIZE_INTERRUPTS -#endif - -#else - -#ifdef ENCODER_OPTIMIZE_INTERRUPTS -#undef ENCODER_OPTIMIZE_INTERRUPTS -#endif - -#endif diff --git a/Software/lib/Encoder/utility/interrupt_pins.h b/Software/lib/Encoder/utility/interrupt_pins.h deleted file mode 100644 index ca82bf09..00000000 --- a/Software/lib/Encoder/utility/interrupt_pins.h +++ /dev/null @@ -1,370 +0,0 @@ -// interrupt pins for known boards - -// Teensy (and maybe others) define these automatically -#if !defined(CORE_NUM_INTERRUPT) - -// Wiring boards -#if defined(WIRING) - #define CORE_NUM_INTERRUPT NUM_EXTERNAL_INTERRUPTS - #if NUM_EXTERNAL_INTERRUPTS > 0 - #define CORE_INT0_PIN EI0 - #endif - #if NUM_EXTERNAL_INTERRUPTS > 1 - #define CORE_INT1_PIN EI1 - #endif - #if NUM_EXTERNAL_INTERRUPTS > 2 - #define CORE_INT2_PIN EI2 - #endif - #if NUM_EXTERNAL_INTERRUPTS > 3 - #define CORE_INT3_PIN EI3 - #endif - #if NUM_EXTERNAL_INTERRUPTS > 4 - #define CORE_INT4_PIN EI4 - #endif - #if NUM_EXTERNAL_INTERRUPTS > 5 - #define CORE_INT5_PIN EI5 - #endif - #if NUM_EXTERNAL_INTERRUPTS > 6 - #define CORE_INT6_PIN EI6 - #endif - #if NUM_EXTERNAL_INTERRUPTS > 7 - #define CORE_INT7_PIN EI7 - #endif - -// Arduino Uno, Duemilanove, Diecimila, LilyPad, Mini, Fio, etc... -#elif defined(__AVR_ATmega328P__) || defined(__AVR_ATmega328PB__) ||defined(__AVR_ATmega168__) || defined(__AVR_ATmega8__) - #define CORE_NUM_INTERRUPT 2 - #define CORE_INT0_PIN 2 - #define CORE_INT1_PIN 3 - -// Arduino Mega -#elif defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) - #define CORE_NUM_INTERRUPT 6 - #define CORE_INT0_PIN 2 - #define CORE_INT1_PIN 3 - #define CORE_INT2_PIN 21 - #define CORE_INT3_PIN 20 - #define CORE_INT4_PIN 19 - #define CORE_INT5_PIN 18 - -// Arduino Nano Every, Uno R2 Wifi -#elif defined(__AVR_ATmega4809__) - #define CORE_NUM_INTERRUPT 22 - #define CORE_INT0_PIN 0 - #define CORE_INT1_PIN 1 - #define CORE_INT2_PIN 2 - #define CORE_INT3_PIN 3 - #define CORE_INT4_PIN 4 - #define CORE_INT5_PIN 5 - #define CORE_INT6_PIN 6 - #define CORE_INT7_PIN 7 - #define CORE_INT8_PIN 8 - #define CORE_INT9_PIN 9 - #define CORE_INT10_PIN 10 - #define CORE_INT11_PIN 11 - #define CORE_INT12_PIN 12 - #define CORE_INT13_PIN 13 - #define CORE_INT14_PIN 14 - #define CORE_INT15_PIN 15 - #define CORE_INT16_PIN 16 - #define CORE_INT17_PIN 17 - #define CORE_INT18_PIN 18 - #define CORE_INT19_PIN 19 - #define CORE_INT20_PIN 20 - #define CORE_INT21_PIN 21 - -// Arduino Leonardo (untested) -#elif defined(__AVR_ATmega32U4__) && !defined(CORE_TEENSY) - #define CORE_NUM_INTERRUPT 5 - #define CORE_INT0_PIN 3 - #define CORE_INT1_PIN 2 - #define CORE_INT2_PIN 0 - #define CORE_INT3_PIN 1 - #define CORE_INT4_PIN 7 - -// Sanguino (untested) and ATmega1284P -#elif defined(__AVR_ATmega644P__) || defined(__AVR_ATmega644__) || defined(__AVR_ATmega1284P__) - #define CORE_NUM_INTERRUPT 3 - #define CORE_INT0_PIN 10 - #define CORE_INT1_PIN 11 - #define CORE_INT2_PIN 2 - -// ATmega32u2 and ATmega32u16 based boards with HoodLoader2 -#elif defined(__AVR_ATmega32U2__) || defined(__AVR_ATmega16U2__) - #define CORE_NUM_INTERRUPT 8 - #define CORE_INT0_PIN 8 - #define CORE_INT1_PIN 17 - #define CORE_INT2_PIN 13 - #define CORE_INT3_PIN 14 - #define CORE_INT4_PIN 15 - #define CORE_INT5_PIN 16 - #define CORE_INT6_PIN 19 - #define CORE_INT7_PIN 20 - -// Chipkit Uno32 - attachInterrupt may not support CHANGE option -#elif defined(__PIC32MX__) && defined(_BOARD_UNO_) - #define CORE_NUM_INTERRUPT 5 - #define CORE_INT0_PIN 38 - #define CORE_INT1_PIN 2 - #define CORE_INT2_PIN 7 - #define CORE_INT3_PIN 8 - #define CORE_INT4_PIN 35 - -// Chipkit Uno32 - attachInterrupt may not support CHANGE option -#elif defined(__PIC32MX__) && defined(_BOARD_MEGA_) - #define CORE_NUM_INTERRUPT 5 - #define CORE_INT0_PIN 3 - #define CORE_INT1_PIN 2 - #define CORE_INT2_PIN 7 - #define CORE_INT3_PIN 21 - #define CORE_INT4_PIN 20 - -// http://hlt.media.mit.edu/?p=1229 -#elif defined(__AVR_ATtiny45__) || defined(__AVR_ATtiny85__) - #define CORE_NUM_INTERRUPT 1 - #define CORE_INT0_PIN 2 - - // ATtiny44 ATtiny84 -#elif defined(__AVR_ATtiny44__) || defined(__AVR_ATtiny84__) - #define CORE_NUM_INTERRUPT 1 - #define CORE_INT0_PIN 8 - -// ATtiny441 ATtiny841 -#elif defined(__AVR_ATtiny441__) || defined(__AVR_ATtiny841__) - #define CORE_NUM_INTERRUPT 1 - #define CORE_INT0_PIN 9 - -//https://github.com/SpenceKonde/ATTinyCore/blob/master/avr/extras/ATtiny_x313.md -#elif defined(__AVR_ATtinyX313__) - #define CORE_NUM_INTERRUPT 2 - #define CORE_INT0_PIN 4 - #define CORE_INT1_PIN 5 - -// Attiny167 same core as abobe -#elif defined(__AVR_ATtiny167__) - #define CORE_NUM_INTERRUPT 2 - #define CORE_INT0_PIN 14 - #define CORE_INT1_PIN 3 - - -// Arduino Due -#elif defined(__SAM3X8E__) - #define CORE_NUM_INTERRUPT 54 - #define CORE_INT0_PIN 0 - #define CORE_INT1_PIN 1 - #define CORE_INT2_PIN 2 - #define CORE_INT3_PIN 3 - #define CORE_INT4_PIN 4 - #define CORE_INT5_PIN 5 - #define CORE_INT6_PIN 6 - #define CORE_INT7_PIN 7 - #define CORE_INT8_PIN 8 - #define CORE_INT9_PIN 9 - #define CORE_INT10_PIN 10 - #define CORE_INT11_PIN 11 - #define CORE_INT12_PIN 12 - #define CORE_INT13_PIN 13 - #define CORE_INT14_PIN 14 - #define CORE_INT15_PIN 15 - #define CORE_INT16_PIN 16 - #define CORE_INT17_PIN 17 - #define CORE_INT18_PIN 18 - #define CORE_INT19_PIN 19 - #define CORE_INT20_PIN 20 - #define CORE_INT21_PIN 21 - #define CORE_INT22_PIN 22 - #define CORE_INT23_PIN 23 - #define CORE_INT24_PIN 24 - #define CORE_INT25_PIN 25 - #define CORE_INT26_PIN 26 - #define CORE_INT27_PIN 27 - #define CORE_INT28_PIN 28 - #define CORE_INT29_PIN 29 - #define CORE_INT30_PIN 30 - #define CORE_INT31_PIN 31 - #define CORE_INT32_PIN 32 - #define CORE_INT33_PIN 33 - #define CORE_INT34_PIN 34 - #define CORE_INT35_PIN 35 - #define CORE_INT36_PIN 36 - #define CORE_INT37_PIN 37 - #define CORE_INT38_PIN 38 - #define CORE_INT39_PIN 39 - #define CORE_INT40_PIN 40 - #define CORE_INT41_PIN 41 - #define CORE_INT42_PIN 42 - #define CORE_INT43_PIN 43 - #define CORE_INT44_PIN 44 - #define CORE_INT45_PIN 45 - #define CORE_INT46_PIN 46 - #define CORE_INT47_PIN 47 - #define CORE_INT48_PIN 48 - #define CORE_INT49_PIN 49 - #define CORE_INT50_PIN 50 - #define CORE_INT51_PIN 51 - #define CORE_INT52_PIN 52 - #define CORE_INT53_PIN 53 - -// ESP8266 (https://github.com/esp8266/Arduino/) -#elif defined(ESP8266) - #define CORE_NUM_INTERRUPT EXTERNAL_NUM_INTERRUPTS - #define CORE_INT0_PIN 0 - #define CORE_INT1_PIN 1 - #define CORE_INT2_PIN 2 - #define CORE_INT3_PIN 3 - #define CORE_INT4_PIN 4 - #define CORE_INT5_PIN 5 - // GPIO6-GPIO11 are typically used to interface with the flash memory IC on - // most esp8266 modules, so we should avoid adding interrupts to these pins. - #define CORE_INT12_PIN 12 - #define CORE_INT13_PIN 13 - #define CORE_INT14_PIN 14 - #define CORE_INT15_PIN 15 - -// ESP32 (https://github.com/espressif/arduino-esp32) -#elif defined(ESP32) - - #define CORE_NUM_INTERRUPT 40 - #define CORE_INT0_PIN 0 - #define CORE_INT1_PIN 1 - #define CORE_INT2_PIN 2 - #define CORE_INT3_PIN 3 - #define CORE_INT4_PIN 4 - #define CORE_INT5_PIN 5 - // GPIO6-GPIO11 are typically used to interface with the flash memory IC on - // esp32, so we should avoid adding interrupts to these pins. - #define CORE_INT12_PIN 12 - #define CORE_INT13_PIN 13 - #define CORE_INT14_PIN 14 - #define CORE_INT15_PIN 15 - #define CORE_INT16_PIN 16 - #define CORE_INT17_PIN 17 - #define CORE_INT18_PIN 18 - #define CORE_INT19_PIN 19 - #define CORE_INT21_PIN 21 - #define CORE_INT22_PIN 22 - #define CORE_INT23_PIN 23 - #define CORE_INT25_PIN 25 - #define CORE_INT26_PIN 26 - #define CORE_INT27_PIN 27 - #define CORE_INT32_PIN 32 - #define CORE_INT33_PIN 33 - #define CORE_INT34_PIN 34 - #define CORE_INT35_PIN 35 - #define CORE_INT36_PIN 36 - #define CORE_INT39_PIN 39 - - -// Arduino Zero - TODO: interrupts do not seem to work -// please help, contribute a fix! -#elif defined(__SAMD21G18A__) || defined(__SAMD21E18A__) - #define CORE_NUM_INTERRUPT 31 - #define CORE_INT0_PIN 0 - #define CORE_INT1_PIN 1 - #define CORE_INT2_PIN 2 - #define CORE_INT3_PIN 3 - #define CORE_INT4_PIN 4 - #define CORE_INT5_PIN 5 - #define CORE_INT6_PIN 6 - #define CORE_INT7_PIN 7 - #define CORE_INT8_PIN 8 - #define CORE_INT9_PIN 9 - #define CORE_INT10_PIN 10 - #define CORE_INT11_PIN 11 - #define CORE_INT12_PIN 12 - #define CORE_INT13_PIN 13 - #define CORE_INT14_PIN 14 - #define CORE_INT15_PIN 15 - #define CORE_INT16_PIN 16 - #define CORE_INT17_PIN 17 - #define CORE_INT18_PIN 18 - #define CORE_INT19_PIN 19 - #define CORE_INT20_PIN 20 - #define CORE_INT21_PIN 21 - #define CORE_INT22_PIN 22 - #define CORE_INT23_PIN 23 - #define CORE_INT24_PIN 24 - #define CORE_INT25_PIN 25 - #define CORE_INT26_PIN 26 - #define CORE_INT27_PIN 27 - #define CORE_INT28_PIN 28 - #define CORE_INT29_PIN 29 - #define CORE_INT30_PIN 30 - -#elif defined(__SAMD51__) - #define CORE_NUM_INTERRUPT 26 - #define CORE_INT0_PIN 0 - #define CORE_INT1_PIN 1 - #define CORE_INT2_PIN 2 - #define CORE_INT3_PIN 3 - #define CORE_INT4_PIN 4 - #define CORE_INT5_PIN 5 - #define CORE_INT6_PIN 6 - #define CORE_INT7_PIN 7 - #define CORE_INT8_PIN 8 - #define CORE_INT9_PIN 9 - #define CORE_INT10_PIN 10 - #define CORE_INT11_PIN 11 - #define CORE_INT12_PIN 12 - #define CORE_INT13_PIN 13 - #define CORE_INT14_PIN 14 - #define CORE_INT15_PIN 15 - #define CORE_INT16_PIN 16 - #define CORE_INT17_PIN 17 - #define CORE_INT18_PIN 18 - #define CORE_INT19_PIN 19 - #define CORE_INT20_PIN 20 - #define CORE_INT21_PIN 21 - #define CORE_INT22_PIN 22 - #define CORE_INT23_PIN 23 - #define CORE_INT24_PIN 24 - #define CORE_INT25_PIN 25 - -// Arduino 101 -#elif defined(__arc__) - #define CORE_NUM_INTERRUPT 14 - #define CORE_INT2_PIN 2 - #define CORE_INT5_PIN 5 - #define CORE_INT7_PIN 7 - #define CORE_INT8_PIN 8 - #define CORE_INT10_PIN 10 - #define CORE_INT11_PIN 11 - #define CORE_INT12_PIN 12 - #define CORE_INT13_PIN 13 - -// Arduino Nano 33 BLE -#elif defined(ARDUINO_ARCH_NRF52840) - #define CORE_NUM_INTERRUPT 22 - #define CORE_INT0_PIN 0 - #define CORE_INT1_PIN 1 - #define CORE_INT2_PIN 2 - #define CORE_INT3_PIN 3 - #define CORE_INT4_PIN 4 - #define CORE_INT5_PIN 5 - #define CORE_INT6_PIN 6 - #define CORE_INT7_PIN 7 - #define CORE_INT8_PIN 8 - #define CORE_INT9_PIN 9 - #define CORE_INT10_PIN 10 - #define CORE_INT11_PIN 11 - #define CORE_INT12_PIN 12 - #define CORE_INT13_PIN 13 - #define CORE_INT14_PIN A0 - #define CORE_INT15_PIN A1 - #define CORE_INT16_PIN A2 - #define CORE_INT17_PIN A3 - #define CORE_INT18_PIN A4 - #define CORE_INT19_PIN A5 - #define CORE_INT20_PIN A6 - #define CORE_INT21_PIN A7 -#endif -#endif - -#if !defined(CORE_NUM_INTERRUPT) -#error "Interrupts are unknown for this board, please add to this code" -#endif -#if CORE_NUM_INTERRUPT <= 0 -#error "Encoder requires interrupt pins, but this board does not have any :(" -#error "You could try defining ENCODER_DO_NOT_USE_INTERRUPTS as a kludge." -#endif - diff --git a/Software/platformio.ini b/Software/platformio.ini index 5017daea..9c4a4463 100644 --- a/Software/platformio.ini +++ b/Software/platformio.ini @@ -23,32 +23,38 @@ lib_deps = bblanchon/ArduinoJson@^6.21.4 fastled/FastLED@^3.6.0 olikraus/U8g2@^2.35.8 - + ricmoo/QRCode@^0.0.1 + igorantolic/Ai Esp32 Rotary Encoder @ ^1.6 upload_speed = 921600 - check_skip_packages = true check_tool = clangtidy check_flags = clangtidy: --checks=-\*,bugprone-*,boost-*,modernize-*,performance-*,clang-analyzer-*,cert-dcl03-c,cert-dcl21-cpp,cert-dcl58-cpp,cert-err34-c,cert-err52-cpp,cert-err58-cpp,cert-err60-cpp,cert-flp30-c,cert-msc50-cpp,cert-msc51-cpp,cert-oop54-cpp,cert-str34-c,cppcoreguidelines-interfaces-global-init,cppcoreguidelines-narrowing-conversions,cppcoreguidelines-pro-type-member-init,cppcoreguidelines-pro-type-static-cast-downcast,cppcoreguidelines-slicing,google-default-arguments,google-explicit-constructor,google-runtime-operator,hicpp-exception-baseclass,hicpp-multiway-paths-covered,hicpp-signed-bitwise,portability-simd-intrinsics,readability-avoid-const-params-in-decls,readability-const-return-type,readability-container-size-empty,readability-convert-member-functions-to-static,readability-delete-null-pointer,readability-deleted-default,readability-inconsistent-declaration-parameter-name,readability-make-member-function-const,readability-misleading-indentation,readability-misplaced-array-index,readability-non-const-parameter,readability-redundant-control-flow,readability-redundant-declaration,readability-redundant-function-ptr-dereference,readability-redundant-smartptr-get,readability-simplify-subscript-expr,readability-static-accessed-through-instance,readability-static-definition-in-anonymous-namespace,readability-string-compare,readability-uniqueptr-delete-release,readability-use-anyofallof,-modernize-use-trailing-return-type,-readability-convert-member-functions-to-static,-bugprone-easily-swappable-parameters,-readability-make-member-function-const --fix +extra_scripts = pre:pre_build_script.py [env:development] build_flags = -std=gnu++17 ; CORE_DEBUG_LEVEL Details: https://docs.platformio.org/en/latest/platforms/espressif32.html#debug-level -D CORE_DEBUG_LEVEL=4 + -D VERSIONDEV + -D SW_VERSION=0 + -D HTTPCLIENT_1_1_COMPATIBLE=0 extends = common -platform = espressif32@3.5.0 +platform = espressif32 board = esp32dev framework = arduino monitor_speed = 115200 -monitor_filters = colorize, esp32_exception_decoder, time, --rtscts +monitor_filters = colorize, esp32_exception_decoder, time [env:staging] build_flags = -std=gnu++17 -D CORE_DEBUG_LEVEL=1 + -D VERSIONSTAGING + -D HTTPCLIENT_1_1_COMPATIBLE=0 extends = common -platform = espressif32@3.5.0 +platform = espressif32 board = esp32dev framework = arduino monitor_speed = 115200 @@ -57,20 +63,25 @@ monitor_speed = 115200 build_flags = -std=gnu++17 -D CORE_DEBUG_LEVEL=0 + -D VERSIONLIVE + -D HTTPCLIENT_1_1_COMPATIBLE=0 extends = common -platform = espressif32@3.5.0 +platform = espressif32 board = esp32dev framework = arduino monitor_speed = 115200 [env:test] +lib_deps = + fabiobatsilva/ArduinoFake@^0.4.0 + bblanchon/ArduinoJson@^6.21.4 build_flags = -std=gnu++17 -D DEBUGLOG_DEFAULT_LOG_LEVEL_DEBUG -D UNITY_INCLUDE_DOUBLE - -D CORE_DEBUG_LEVEL=5 + -D CORE_DEBUG_LEVEL=0 + -D SW_VERSION="0.0.0" + -D VERSIONTEST extends = common platform = native -lib_deps = - fabiobatsilva/ArduinoFake@^0.4.0 \ No newline at end of file diff --git a/Software/pre_build_script.py b/Software/pre_build_script.py new file mode 100644 index 00000000..c86ecdf1 --- /dev/null +++ b/Software/pre_build_script.py @@ -0,0 +1,40 @@ +Import("env") +import os + + +# Function to check if SW_VERSION is already in CPPDEFINES +def get_build_flag_value(flag_name): + # Check if BUILD_FLAGS is defined in the environment + try: + build_flags = env.ParseFlags(env['BUILD_FLAGS']) + flags_with_value_list = [build_flag for build_flag in build_flags.get('CPPDEFINES') if type(build_flag) == list] + defines = {k: v for (k, v) in flags_with_value_list} + value = defines.get(flag_name) + + print(f"Found Bluid Flag {flag_name} = {value}") + return value + except KeyError: + return None + +swVersion = get_build_flag_value('SW_VERSION') + +if not swVersion: + # SW_VERSION not already defined, check environment variable or GITHUB_ENV + + swVersion = os.getenv('SW_VERSION') + if not swVersion: + env_file = os.getenv('GITHUB_ENV') + if env_file: + with open(env_file, 'r') as f: + for line in f: + if line.startswith('SW_VERSION='): + swVersion = line.split('=')[1].strip() + print(f"Found SW_VERSION in {env_file} = {swVersion}") + break + + env.Append(CPPDEFINES=('SW_VERSION', swVersion)); + print(f"SW_VERSION is now {swVersion}") +else: + print(f"SW_VERSION already defined as ${swVersion}"); + +print(f"SW_VERSION is {swVersion}") \ No newline at end of file diff --git a/Software/src/OSSM_Config.h b/Software/src/OSSM_Config.h deleted file mode 100644 index 41832efd..00000000 --- a/Software/src/OSSM_Config.h +++ /dev/null @@ -1,63 +0,0 @@ -#ifndef OSSM_CONFIG_H -#define OSSM_CONFIG_H - -#define DEBUG - -#define SW_VERSION "0.23" -#define HW_VERSION 22 // divide by 10 for real hw version -#define EEPROM_SIZE 200 - -// #define INITIAL_SETUP //should only be defined at initial burn to configure -// HW version - -extern volatile int encoderButtonPresses; -extern volatile long lastEncoderButtonPressMillis; - -/* - User Config for OSSM - Reference board users should tweak this to match - their personal build. -*/ - -/* - Motion System Config -*/ -// Top linear speed of the device. -const float hardcode_maxSpeedMmPerSecond = 900.0f; -// This should match the step/rev of your stepper or servo. -// N.b. the iHSV57 has a table on the side for setting the DIP switches to your -// preference. -const float hardcode_motorStepPerRevolution = 800.0f; -// Number of teeth the pulley that is attached to the servo/stepper shaft has. -const float hardcode_pulleyToothCount = 20.0f; -// Set to your belt pitch (Distance between two teeth on the belt) (E.g. GT2 -// belt has 2mm tooth pitch) -const float hardcode_beltPitchMm = 2.0f; -// This is in millimeters, and is what's used to define how much of -// your rail is usable. -// The absolute max your OSSM would have is the distance between the belt -// attachments subtract the linear block holder length (75mm on OSSM) -// Recommended to also subtract e.g. 20mm to keep the backstop well away from -// the device. -const float hardcode_maxStrokeLengthMm = 100.f; -/* - Web Config -*/ -// This should be unique to your device. You will use this on the -// web portal to interact with your OSSM. -// there is NO security other than knowing this name, make this unique to avoid -// collisions with other users -extern const char *ossmId; - -/* - Advanced Config -*/ -// After homing this is the physical buffer distance from the effective zero to -// the home switch This is to stop the home switch being smacked constantly -const float hardcode_strokeZeroOffsetmm = 6.0f; -// The minimum value of the pot in percent -// prevents noisy pots registering commands when turned down to zero by user -const float hardcode_commandDeadzonePercentage = 2.0f; -// affects acceleration in stepper trajectory (Aggressiveness of motion) -const float hardcode_accelerationScaling = 120.0f; - -#endif diff --git a/Software/src/OSSM_PinDef.h b/Software/src/OSSM_PinDef.h deleted file mode 100644 index 9fd0ff80..00000000 --- a/Software/src/OSSM_PinDef.h +++ /dev/null @@ -1,58 +0,0 @@ -#ifndef OSSM_PIN_H -#define OSSM_PIN_H -/* - Pin Definitions - Drivers, Buttons and Remotes - OSSM Reference board users are unlikely to need to modify this! See - OSSM_Config.h -*/ - -/* - Driver pins -*/ -// Pin that pulses on servo/stepper steps - likely labelled PUL on drivers. -#define MOTOR_STEP_PIN 14 -// Pin connected to driver/servo step direction - likely labelled DIR on -// drivers. N.b. to iHSV57 users - DIP switch #5 can be flipped to invert motor -// direction entirely -#define MOTOR_DIRECTION_PIN 27 -// Pin for motor enable - likely labelled ENA on drivers. -#define MOTOR_ENABLE_PIN 26 - -/* - Homing and safety pins -*/ -// define the IO pin the emergency stop switch is connected to -#define STOP_PIN 19 -// define the IO pin where the limit(homing) switch(es) are connected to -// (switches in series in normally open setup) Switches wired from IO pin to -// ground. -#define LIMIT_SWITCH_PIN 12 - -/* - Wifi Control Pins -*/ -// Pin for WiFi reset button (optional) -#define WIFI_RESET_PIN 23 - -// Pin for the toggle for wifi control (Can be left alone if no hardware toggle -// is required) -#define WIFI_CONTROL_TOGGLE_PIN 22 - -#define LOCAL_CONTROLLER INPUT_PULLDOWN -#define WIFI_CONTROLLER INPUT_PULLUP -// Choose whether the default control scheme is local (e.g. OSSM remote, -// potentiometers, etc.) or through Wi-Fi If both are desired, a hardware toggle -// will need to be installed and wired to WIFI_CONTROL_TOGGLE_PIN -#define WIFI_CONTROL_DEFAULT LOCAL_CONTROLLER - -/*These are configured for the OSSM Remote - which has a screen, a potentiometer - * and an encoder which clicks*/ -#define SPEED_POT_PIN 34 -#define ENCODER_SWITCH 35 -#define ENCODER_A 18 -#define ENCODER_B 5 -#define REMOTE_SDA 21 -#define REMOTE_CLK 19 -#define REMOTE_ADDRESS 0x3c - -#endif \ No newline at end of file diff --git a/Software/src/OssmUi/OssmUi.cpp b/Software/src/OssmUi/OssmUi.cpp deleted file mode 100644 index bae2d01d..00000000 --- a/Software/src/OssmUi/OssmUi.cpp +++ /dev/null @@ -1,91 +0,0 @@ - -#include "OssmUi/OssmUi.h" - -#include "constants/Images.h" - -void OssmUi::Setup() { - display.begin(); - - display.clearBuffer(); - display.drawXBMP(40, 14, 50, 50, Images::KMLogo); - display.sendBuffer(); -} - -void OssmUi::UpdateMessage(const String& message_in) { - // compute the string width to center it later. - display.setFont(u8g2_font_helvR08_tf); - - int x = - (display.getDisplayWidth() - display.getUTF8Width(message_in.c_str())) / - 2; - - // draw inverted rectangle to clear the screen - display.setColorIndex(0); - display.drawBox(0, 0, 128, 12); - display.setColorIndex(1); - display.drawUTF8(x, 10, message_in.c_str()); - - display.sendBuffer(); -} - -static String last_mode_label = ""; -static int last_speed_percentage = 0; -static int last_encoder_position = 0; - -void OssmUi::UpdateState(const String& mode_label, const int speed_percentage, - const int encoder_position) { - if (last_mode_label == mode_label && - last_speed_percentage == speed_percentage && - last_encoder_position == encoder_position) { - return; - } - last_encoder_position = encoder_position; - last_speed_percentage = speed_percentage; - last_mode_label = mode_label; - - // clear the left area of the screen, below 12 px - display.setColorIndex(0); - display.drawBox(0, 12, 24, 64); - - // clear the right area of the screen below 12 px - display.drawBox(104, 12, 24, 64); - - // Clear the label area - display.drawBox(0, 0, 128, 12); - - // draw the speed percentage rect - display.setColorIndex(1); - int speed_height = (64 - 12) * speed_percentage / 100; - display.drawBox(0, 64 - speed_height, 24, speed_height); - - // Speed gradation - display.setColorIndex(2); // XOR mode - display.drawLine(0, 12, 23, 12); // 100% - display.drawLine(0, 25, 6, 25); // 75% - display.drawLine(0, 38, 12, 38); // 50% - display.drawLine(0, 51, 6, 51); // 25% - display.drawLine(0, 63, 23, 63); // 0% - - // draw the encoder position rect - display.setColorIndex(1); - int height = constrain((64 - 12) * encoder_position / 100, 0, 64 - 12); - display.drawBox(128 - 24, 64 - height, 24, height); - - // Encoder position gradation (this should be obvious, but I want it - // symetrical!) - display.setColorIndex(2); // XOR mode - display.drawLine(128, 12, 128 - 23, 12); // 100% - display.drawLine(128, 25, 128 - 6, 25); // 75% - display.drawLine(128, 38, 128 - 12, 38); // 50% - display.drawLine(128, 51, 128 - 6, 51); // 25% - display.drawLine(128, 63, 128 - 23, 63); // 0% - - display.setColorIndex(1); - display.setFont(u8g2_font_helvR08_tf); - display.drawUTF8(0, 10, speed_percentage == 0 ? "STOPPED" : "SPEED"); - - int width = display.getUTF8Width(mode_label.c_str()); - display.drawUTF8(128 - width, 10, mode_label.c_str()); - - display.sendBuffer(); -} diff --git a/Software/src/OssmUi/OssmUi.h b/Software/src/OssmUi/OssmUi.h deleted file mode 100644 index c44b5d11..00000000 --- a/Software/src/OssmUi/OssmUi.h +++ /dev/null @@ -1,19 +0,0 @@ - -#ifndef OSSM_UI_H -#define OSSM_UI_H - -#include - -#include "services/display.h" - -class OssmUi { - public: - void Setup(); - - static void UpdateState(const String& mode_label, int speed_percentage, - int encoder_position); - - static void UpdateMessage(const String& message_in); -}; - -#endif diff --git a/Software/src/Stroke_Engine_Helper.h b/Software/src/Stroke_Engine_Helper.h deleted file mode 100644 index efd5d8a8..00000000 --- a/Software/src/Stroke_Engine_Helper.h +++ /dev/null @@ -1,43 +0,0 @@ -#ifndef STROKE_ENGINE_HELPER_H -#define STROKE_ENGINE_HELPER_H -#include -#include - -#include "OSSM_Config.h" -#include "OSSM_PinDef.h" - -/*################################################################################################# -## -## G L O B A L D E F I N I T I O N S & D E C L A R A T I O N S -## -##################################################################################################*/ - -// Calculation Aid: -#define STEP_PER_REV \ - 2000 // How many steps per revolution of the motor (S1 off, S2 on, S3 on, - // S4 off) -#define PULLEY_TEETH 20 // How many teeth has the pulley -#define BELT_PITCH 2 // What is the timing belt pitch in mm -#define MAX_RPM 3000.0 // Maximum RPM of motor -#define STEP_PER_MM (STEP_PER_REV / (PULLEY_TEETH * BELT_PITCH)) -#define MAX_SPEED ((MAX_RPM / 60.0) * PULLEY_TEETH * BELT_PITCH) -#define DEBUG_TALKATIVE - -static motorProperties servoMotor{ - .maxSpeed = 60 * (hardcode_maxSpeedMmPerSecond / - (hardcode_pulleyToothCount * hardcode_beltPitchMm)), - .maxAcceleration = 10000, - .stepsPerMillimeter = hardcode_motorStepPerRevolution / - (hardcode_pulleyToothCount * hardcode_beltPitchMm), - .invertDirection = true, - .enableActiveLow = true, - .stepPin = MOTOR_STEP_PIN, - .directionPin = MOTOR_DIRECTION_PIN, - .enablePin = MOTOR_ENABLE_PIN}; - -static endstopProperties endstop = {.homeToBack = false, - .activeLow = true, - .endstopPin = LIMIT_SWITCH_PIN, - .pinMode = INPUT}; - -#endif diff --git a/Software/src/Utilities.cpp b/Software/src/Utilities.cpp deleted file mode 100644 index f3695412..00000000 --- a/Software/src/Utilities.cpp +++ /dev/null @@ -1,879 +0,0 @@ -#include "Utilities.h" - -#include - -#include "Stroke_Engine_Helper.h" -#include "esp_log.h" - -void OSSM::setup() { - WiFi.mode(WIFI_STA); // explicitly set mode, esp defaults to STA+AP - ESP_LOGD("UTILS", "Software version: %s", SW_VERSION); - - g_ui.Setup(); - delay(50); - String message = ""; - message += "V"; - message += SW_VERSION; - message += " Booting up!"; - OssmUi::UpdateMessage(message); -#ifdef INITIAL_SETUP - FastLED.setBrightness(150); - fill_rainbow(ossmleds, NUM_LEDS, 34, 1); - FastLED.show(); - writeEepromSettings(); - WiFi.begin("IoT_PHB", "penthouseb"); // donthackmyguestnetworkplz - wifiAutoConnect(); - updateFirmware(); -#endif - readEepromSettings(); - initializeStepperParameters(); - initializeInputs(); - strcpy(Id, ossmId); - wifiAutoConnect(); - delay(500); - if (checkForUpdate()) { - updatePrompt(); - }; -} - -[[noreturn]] void OSSM::runPenetrate() { - // poll at 200Hz for when motion is complete - for (;;) { - while ((stepper.getDistanceToTargetSigned() != 0) || - (strokePercentage <= commandDeadzonePercentage) || - (speedPercentage <= commandDeadzonePercentage)) { - vTaskDelay(5); // wait for motion to complete and requested stroke - // more than zero - } - - float targetPosition = (strokePercentage / 100.0f) * maxStrokeLengthMm; - float currentStrokeMm = abs(targetPosition); - - stepper.setDecelerationInMillimetersPerSecondPerSecond( - maxSpeedMmPerSecond * speedPercentage * speedPercentage / - accelerationScaling); - stepper.setTargetPositionInMillimeters(targetPosition); - vTaskDelay(2); - - while ((stepper.getDistanceToTargetSigned() != 0) || - (strokePercentage <= commandDeadzonePercentage) || - (speedPercentage <= commandDeadzonePercentage)) { - vTaskDelay(5); // wait for motion to complete, since we are going - // back to zero, don't care about stroke value - } - targetPosition = 0; - vTaskDelay(1); - stepper.setDecelerationInMillimetersPerSecondPerSecond( - maxSpeedMmPerSecond * speedPercentage * speedPercentage / - accelerationScaling); - stepper.setTargetPositionInMillimeters(targetPosition); - vTaskDelay(1); - // if (currentStrokeMm > 1) - numberStrokes++; - travelledDistanceMeters += (0.002 * currentStrokeMm); - updateLifeStats(); - } -} -void OSSM::handleStopCondition() // handles e-stop condition -{ - // check is speed is greater than deadzone value and emergency stop if not - if (speedPercentage <= commandDeadzonePercentage) { - if (!isStopped) { - ESP_LOGD("UTILS", - "Speed: %f Stroke: %f Distance to target: %d steps", - speedPercentage, strokePercentage, - stepper.getDistanceToTargetSigned()); - stepper.emergencyStop(true); - ESP_LOGE("UTILS", "Emergency Stop"); - isStopped = true; - delay(100); - } - } else { - // release emergency stop - if (isStopped) { - ESP_LOGD("UTILS", - "Speed: %f Stroke: %f Distance to target: %d steps", - speedPercentage, strokePercentage, - stepper.getDistanceToTargetSigned()); - stepper.releaseEmergencyStop(); - ESP_LOGD("UTILS", "Emergency Stop Released"); - isStopped = false; - delay(100); - } - } -} - -bool isChangeSignificant(float oldPct, float newPct) { - return oldPct != newPct && - (abs(newPct - oldPct) > 2 || newPct == 0 || newPct == 100); -} - -float calculateSensation(float sensationPercentage) { - return float((sensationPercentage * 200.0) / 100.0) - 100.0f; -} - -[[noreturn]] void OSSM::runStrokeEngine() { - stepper.stopService(); - - machineGeometry strokingMachine = {.physicalTravel = abs(maxStrokeLengthMm), - .keepoutBoundary = 6.0}; - StrokeEngine Stroker; - - Stroker.begin(&strokingMachine, &servoMotor); - Stroker.thisIsHome(); - - float lastSpeedPercentage = speedPercentage; - float lastStrokePercentage = strokePercentage; - float lastDepthPercentage = depthPercentage; - float lastSensationPercentage = sensationPercentage; - int lastEncoderButtonPresses = encoderButtonPresses; - strokePattern = 0; - strokePatternCount = Stroker.getNumberOfPattern(); - - Stroker.setSensation(calculateSensation(sensationPercentage), true); - - Stroker.setPattern(int(strokePattern), true); - Stroker.setDepth(0.01f * depthPercentage * abs(maxStrokeLengthMm), true); - Stroker.setStroke(0.01f * strokePercentage * abs(maxStrokeLengthMm), true); - Stroker.moveToMax(10 * 3); - - ESP_LOGD("UTILS", "Stroker State: %d", Stroker.getState()); - - strokerPatternName = Stroker.getPatternName( - strokePattern); // Set the initial stroke engine pattern name - - for (;;) { - ESP_LOGV("UTILS", "Looping"); - if (isChangeSignificant(lastSpeedPercentage, speedPercentage)) { - ESP_LOGD("UTILS", "changing speed: %f", speedPercentage * 3); - if (speedPercentage == 0) { - Stroker.stopMotion(); - } else if (Stroker.getState() == READY) { - Stroker.startPattern(); - } - - Stroker.setSpeed( - speedPercentage * 3, - true); // multiply by 3 to get to sane thrusts per minute speed - lastSpeedPercentage = speedPercentage; - } - - int buttonPressCount = encoderButtonPresses - lastEncoderButtonPresses; - if (!modeChanged && buttonPressCount > 0 && - (millis() - lastEncoderButtonPressMillis) > 200) { - ESP_LOGD("UTILS", "switching mode pre: %i %i", rightKnobMode, - buttonPressCount); - - // If we are coming from the pattern selection, apply the new - // pattern upon switching out of it. This is to prevent sudden - // jarring pattern changes while scrolling through them "live". - if (rightKnobMode == MODE_PATTERN) { - Stroker.setPattern(int(strokePattern), - false); // Pattern, index must be < - // Stroker.getNumberOfPattern() - } - - if (buttonPressCount > 1) // Enter pattern-selection mode if the - // button is pressed more than once - { - rightKnobMode = MODE_PATTERN; - } else if (strokePattern == - 0) // If the button was only pressed once and we're in - // the basic stroke engine pattern... - { - // ..clamp the right knob mode so that we bypass the "sensation" - // mode - rightKnobMode += 1; - if (rightKnobMode > MODE_DEPTH) { - rightKnobMode = MODE_STROKE; - } - } else { - // Otherwise allow us to select the sensation control mode - rightKnobMode += 1; - if (rightKnobMode > MODE_SENSATION) { - rightKnobMode = MODE_STROKE; - } - } - - ESP_LOGD("UTILS", "switching mode: %i", rightKnobMode); - - modeChanged = true; - lastEncoderButtonPresses = encoderButtonPresses; - } - - if (lastStrokePercentage != strokePercentage) { - float newStroke = 0.01f * strokePercentage * abs(maxStrokeLengthMm); - ESP_LOGD("UTILS", "change stroke: %f %f", strokePercentage, - newStroke); - Stroker.setStroke(newStroke, true); - lastStrokePercentage = strokePercentage; - } - - if (lastDepthPercentage != depthPercentage) { - float newDepth = 0.01f * depthPercentage * abs(maxStrokeLengthMm); - ESP_LOGD("UTILS", "change depth: %f %f", depthPercentage, newDepth); - Stroker.setDepth(newDepth, false); - lastDepthPercentage = depthPercentage; - } - - if (lastSensationPercentage != sensationPercentage) { - float newSensation = calculateSensation(sensationPercentage); - ESP_LOGD("UTILS", "change sensation: %f %f", sensationPercentage, - newSensation); - Stroker.setSensation(newSensation, false); - lastSensationPercentage = sensationPercentage; - } - - if (!modeChanged && changePattern != 0) { - strokePattern += changePattern; - - if (strokePattern < 0) { - strokePattern = Stroker.getNumberOfPattern() - 1; - } else if (strokePattern >= Stroker.getNumberOfPattern()) { - strokePattern = 0; - } - - ESP_LOGD("UTILS", "change pattern: %i", strokePattern); - - strokerPatternName = Stroker.getPatternName( - strokePattern); // Update the stroke pattern name (used by the - // UI) - - modeChanged = true; - } - - vTaskDelay(400); - } -} - -String getPatternJSON(StrokeEngine Stroker) { - String JSON = "[{\""; - for (size_t i = 0; i < Stroker.getNumberOfPattern(); i++) { - JSON += String(Stroker.getPatternName(i)); - JSON += "\": "; - JSON += String(i, DEC); - if (i < Stroker.getNumberOfPattern() - 1) { - JSON += "},{\""; - } else { - JSON += "}]"; - } - } - ESP_LOGD("UTILS", "Pattern JSON: %s", JSON.c_str()); - return JSON; -} - -void OSSM::setRunMode() { - int initialEncoderFlag = encoderButtonPresses; - int runModeVal; - int encoderVal; - while (initialEncoderFlag == encoderButtonPresses) { - encoderVal = abs(g_encoder.read()); - runModeVal = (encoderVal % (2 * runModeCount)) / - 2; // scale by 2 because encoder counts by 2 - - ESP_LOGD("UTILS", "encoder: %d; count: %d, runMode: %d", encoderVal, - encoderVal, runModeVal); - - switch (runModeVal) { - case simpleMode: - OssmUi::UpdateMessage("Simple Penetration"); - activeRunMode = simpleMode; - break; - - case strokeEngineMode: - OssmUi::UpdateMessage("Stroke Engine"); - activeRunMode = strokeEngineMode; - break; - - default: - OssmUi::UpdateMessage("Simple Penetration"); - activeRunMode = simpleMode; - break; - } - } - g_encoder.write(0); // reset encoder to zero -} - -void OSSM::wifiAutoConnect() { - // This is here in case you want to change WiFi settings - pull IO High - if (digitalRead(WIFI_RESET_PIN) == HIGH) { - // reset settings - for testing - wm.resetSettings(); - ESP_LOGD("UTILS", "settings reset"); - - delay(100); - wm.setConfigPortalTimeout(60); - if (!wm.autoConnect("OSSM Setup")) { - ESP_LOGD("UTILS", "failed to connect and hit timeout"); - } - } - - wm.setConfigPortalTimeout(1); - if (!wm.autoConnect("OSSM Setup")) { - ESP_LOGD("UTILS", "failed to connect and hit timeout"); - } - ESP_LOGD("UTILS", "exiting autoconnect"); -} - -[[noreturn]] void OSSM::wifiConnectOrHotspotNonBlocking() { - int wifiTimeoutSeconds = 15; - float threadStartTimeMillis = millis(); - float threadRuntimeSeconds; - // This should always be run in a thread!!! - wm.setConfigPortalTimeout(wifiTimeoutSeconds); - wm.setConfigPortalBlocking(false); - // here we try to connect to WiFi or launch settings hotspot for you to - // enter WiFi credentials - String message = "Connected"; - if (!wm.autoConnect("OSSM setup")) { - // TODO: Set Status LED to indicate failure - message = "No connection, launching config portal"; - } - ESP_LOGD("UTILS", "%s", message.c_str()); - - for (;;) { - wm.process(); - vTaskDelay(1); - // delete this task once connected! - threadRuntimeSeconds = (millis() - threadStartTimeMillis) / 1000; - if (WiFi.status() == WL_CONNECTED || - (threadRuntimeSeconds > (wifiTimeoutSeconds + 10))) { - WiFi.disconnect(true); - WiFi.mode(WIFI_OFF); - vTaskDelay(100); - vTaskDelete(NULL); - } - } -} - -void OSSM::enableWifiControl() { - if (!wifiControlActive) - // this is a transition to WiFi, we should tell the server it has control - { - wifiControlActive = true; - if (WiFi.status() != WL_CONNECTED) { - delay(5000); - } - setInternetControl(wifiControlActive); - } - getInternetSettings(); // we load ossm.speedPercentage and - // ossm.strokePercentage in this routine. -} - -bool OSSM::setInternetControl(bool setWifiControl) { - wifiControlActive = setWifiControl; - // here we will SEND the WiFi control permission, and current speed and - // stroke to the remote server. The cloudfront redirect allows http - // connection with bubble backend hosted at app.researchanddesire.com - - String serverNameBubble = - "http://d2g4f7zewm360.cloudfront.net/ossm-set-control"; // live server - // String serverNameBubble = - // "http://d2oq8yqnezqh3r.cloudfront.net/ossm-set-control"; // this is - // version-test server - - // Add values in the document to send to server - StaticJsonDocument<200> doc; - doc["ossmId"] = ossmId; - doc["wifiControlEnabled"] = wifiControlActive; - doc["stroke"] = strokePercentage; - doc["speed"] = speedPercentage; - String requestBody; - serializeJson(doc, requestBody); - - // Http request - HTTPClient http; - http.begin(serverNameBubble); - http.addHeader("Content-Type", "application/json"); - // post and wait for response - int httpResponseCode = http.POST(requestBody); - String payload = "{}"; - payload = http.getString(); - http.end(); - - // deserialize JSON - StaticJsonDocument<200> bubbleResponse; - deserializeJson(bubbleResponse, payload); - - // TODO: handle status response - // const char *status = bubbleResponse["status"]; // "success" - - const char *wifiEnabledStr = (wifiControlActive ? "true" : "false"); - ESP_LOGD("UTILS", "Setting Wifi Control: %s\n%s\n%s\n", wifiEnabledStr, - requestBody.c_str(), payload.c_str()); - ESP_LOGD("UTILS", "HTTP Response code: %d\n", httpResponseCode); - - return true; -} - -bool OSSM::getInternetSettings() { - // here we will request speed and stroke settings from the remote server. - // The cloudfront redirect allows http connection with bubble backend hosted - // at app.researchanddesire.com - - String serverNameBubble = - "http://d2g4f7zewm360.cloudfront.net/ossm-get-settings"; // live server - // String serverNameBubble = - // "http://d2oq8yqnezqh3r.cloudfront.net/ossm-get-settings"; // this is - // version-test - // server - - // Add values in the document - StaticJsonDocument<200> doc; - doc["ossmId"] = ossmId; - String requestBody; - serializeJson(doc, requestBody); - - // Http request - HTTPClient http; - http.begin(serverNameBubble); - http.addHeader("Content-Type", "application/json"); - // post and wait for response - int httpResponseCode = http.POST(requestBody); - String payload = "{}"; - payload = http.getString(); - http.end(); - - // deserialize JSON - StaticJsonDocument<200> bubbleResponse; - deserializeJson(bubbleResponse, payload); - - // TODO: handle status response - // const char *status = bubbleResponse["status"]; // "success" - strokePercentage = bubbleResponse["response"]["stroke"]; - speedPercentage = bubbleResponse["response"]["speed"]; - - // debug info on the http payload - ESP_LOGD("UTILS", "payload: %s", payload.c_str()); - ESP_LOGD("UTILS", "HTTP Response code: %d", httpResponseCode); - - return true; -} - -void OSSM::updatePrompt() { - ESP_LOGD("UTILS", "about to start httpOtaUpdate"); - if (WiFi.status() != WL_CONNECTED) { - // return if no WiFi - return; - } - if (!checkForUpdate()) { - return; - } - // Tell user we are updating! - - OssmUi::UpdateMessage("Press to update SW"); - - if (!waitForAnyButtonPress(5000)) { - // user did not accept update - return; - } - - updateFirmware(); -} -void OSSM::updateFirmware() { - FastLED.setBrightness(150); - fill_rainbow(ossmleds, NUM_LEDS, 192, 1); - FastLED.show(); - OssmUi::UpdateMessage("Updating - 1 minute..."); - - WiFiClient client; - t_httpUpdate_return ret = httpUpdate.update( - client, "http://d2sy3zdr3r1gt5.cloudfront.net/ossmfirmware2.bin"); - // Or: - // t_httpUpdate_return ret = httpUpdate.update(client, "server", 80, - // "file.bin"); - - switch (ret) { - case HTTP_UPDATE_FAILED: - ESP_LOGD("UTILS", "HTTP_UPDATE_FAILED Error (%d): %s\n", - httpUpdate.getLastError(), - httpUpdate.getLastErrorString().c_str()); - break; - - case HTTP_UPDATE_NO_UPDATES: - ESP_LOGD("UTILS", "HTTP_UPDATE_NO_UPDATES"); - break; - - case HTTP_UPDATE_OK: - ESP_LOGD("UTILS", "HTTP_UPDATE_OK"); - break; - } -} - -bool OSSM::checkForUpdate() { - String serverNameBubble = - "http://d2g4f7zewm360.cloudfront.net/check-for-ossm-update"; // live - // url -#ifdef VERSIONTEST - serverNameBubble = - "http://d2oq8yqnezqh3r.cloudfront.net/check-for-ossm-update"; // version-test -#endif - ESP_LOGD("UTILS", "about to hit http for update"); - HTTPClient http; - http.begin(serverNameBubble); - http.addHeader("Content-Type", "application/json"); - StaticJsonDocument<200> doc; - // Add values in the document - doc["ossmSwVersion"] = SW_VERSION; - - String requestBody; - serializeJson(doc, requestBody); - ESP_LOGD("UTILS", "about to POST"); - int httpResponseCode = http.POST(requestBody); - ESP_LOGD("UTILS", "POSTed"); - String payload = "{}"; - payload = http.getString(); - ESP_LOGD("UTILS", "HTTP Response code: %d", httpResponseCode); - StaticJsonDocument<200> bubbleResponse; - - deserializeJson(bubbleResponse, payload); - - bool response_needUpdate = bubbleResponse["response"]["needUpdate"]; - - ESP_LOGD("UTILS", "Payload: %s", payload.c_str()); - - if (httpResponseCode <= 0) { - ESP_LOGD("UTILS", "Failed to reach update server"); - } - http.end(); - return response_needUpdate; -} - -bool OSSM::checkConnection() { - if (WiFi.status() != WL_CONNECTED) { - return false; - } else { - return true; - } -} - -void OSSM::initializeStepperParameters() { - stepper.connectToPins(MOTOR_STEP_PIN, MOTOR_DIRECTION_PIN); - float stepsPerMm = - motorStepPerRevolution / (pulleyToothCount * beltPitchMm); - stepper.setStepsPerMillimeter(stepsPerMm); - stepper.setLimitSwitchActive(LIMIT_SWITCH_PIN); - stepper.startAsService(); // Kinky Makers - we have modified this function - // from default library to run on core 1 and suggest you don't run anything - // else on that core. -} - -void OSSM::initializeInputs() { - pinMode(MOTOR_ENABLE_PIN, OUTPUT); - pinMode(WIFI_RESET_PIN, INPUT_PULLDOWN); - pinMode(WIFI_CONTROL_TOGGLE_PIN, - LOCAL_CONTROLLER); // choose between WIFI_CONTROLLER and - // LOCAL_CONTROLLER - // Set analog pots (control knobs) - pinMode(SPEED_POT_PIN, INPUT); - adcAttachPin(SPEED_POT_PIN); - - analogReadResolution(12); - analogSetAttenuation(ADC_11db); // allows us to read almost full 3.3V range -} - -bool OSSM::findHome() { - maxStrokeLengthMm = sensorlessHoming(); - if (maxStrokeLengthMm > 20) { - return true; - } - return false; - - ESP_LOGD("UTILS", "Homing returning"); -} - -float OSSM::sensorlessHoming() { - // find retracted position, mark as zero, find extended position, calc total - // length, subtract 2x offsets and record length. - // move to offset and call it zero. homing complete. - - pinMode(LIMIT_SWITCH_PIN, INPUT_PULLUP); - - float currentLimit = 1.5; - currentSensorOffset = (getAnalogAveragePercent(36, 1000)); - float current; - float measuredStrokeMm; - stepper.setAccelerationInMillimetersPerSecondPerSecond(1000); - stepper.setDecelerationInMillimetersPerSecondPerSecond(10000); - - OssmUi::UpdateMessage("Finding Home Sensorless"); - - // disable motor briefly in case we are against a hard stop. - digitalWrite(MOTOR_ENABLE_PIN, HIGH); - delay(600); - digitalWrite(MOTOR_ENABLE_PIN, LOW); - delay(100); - - int limitSwitchActivated = digitalRead(LIMIT_SWITCH_PIN); - current = getAnalogAveragePercent(36, 200) - currentSensorOffset; - - ESP_LOGV("UTILS", - "Sensorless Homing, current: %f, position: %f, limitSwitch: %d", - current, stepper.getCurrentPositionInMillimeters(), - limitSwitchActivated); - - // find reverse limit - - stepper.setSpeedInMillimetersPerSecond(25); - stepper.setTargetPositionInMillimeters(-400); - - while (current < currentLimit && limitSwitchActivated != 0) { - current = getAnalogAveragePercent(36, 25) - currentSensorOffset; - limitSwitchActivated = digitalRead(LIMIT_SWITCH_PIN); - - ESP_LOGV( - "UTILS", - "Sensorless Homing, current: %f, position: %f, limitSwitch: %d", - current, stepper.getCurrentPositionInMillimeters(), - limitSwitchActivated); - } - if (limitSwitchActivated == 0) { - stepper.setTargetPositionToStop(); - delay(100); - stepper.setSpeedInMillimetersPerSecond(10); - stepper.moveRelativeInMillimeters((1 * maxStrokeLengthMm) - - strokeZeroOffsetmm); - ESP_LOGD("UTILS", "OSSM has moved out, will now set new home?"); - stepper.setCurrentPositionAsHomeAndStop(); - return -maxStrokeLengthMm; - } - stepper.setTargetPositionToStop(); - stepper.moveRelativeInMillimeters( - strokeZeroOffsetmm); //"move to" is blocking - stepper.setCurrentPositionAsHomeAndStop(); - OssmUi::UpdateMessage("Checking Stroke"); - delay(100); - - // find forward limit - - stepper.setSpeedInMillimetersPerSecond(25); - stepper.setTargetPositionInMillimeters(400); - delay(300); - current = getAnalogAveragePercent(36, 200) - currentSensorOffset; - while (current < currentLimit) { - current = getAnalogAveragePercent(36, 25) - currentSensorOffset; - ESP_LOGV("UTILS", "Sensorless Homing, current: %f, position: %f", - current, stepper.getCurrentPositionInMillimeters()); - } - - stepper.setTargetPositionToStop(); - stepper.moveRelativeInMillimeters(-strokeZeroOffsetmm); - measuredStrokeMm = -stepper.getCurrentPositionInMillimeters(); - stepper.setCurrentPositionAsHomeAndStop(); - - ESP_LOGD("UTILS", "Sensorless Homing complete! %f mm", measuredStrokeMm); - - OssmUi::UpdateMessage("Homing Complete"); - - ESP_LOGD("UTILS", "Sensorless Homing complete! %f mm", measuredStrokeMm); - - return measuredStrokeMm; -} -void OSSM::sensorHoming() { - // find limit switch and then move to end of stroke and call it zero - stepper.setAccelerationInMillimetersPerSecondPerSecond(300); - stepper.setDecelerationInMillimetersPerSecondPerSecond(10000); - - ESP_LOGD("UTILS", "OSSM will now home"); - OssmUi::UpdateMessage("Finding Home Switch"); - stepper.setSpeedInMillimetersPerSecond(15); - stepper.moveToHomeInMillimeters(1, 25, 300, LIMIT_SWITCH_PIN); - ESP_LOGD("UTILS", "OSSM has homed, will now move out to max length"); - OssmUi::UpdateMessage("Moving to Max"); - stepper.setSpeedInMillimetersPerSecond(10); - stepper.moveToPositionInMillimeters((-1 * maxStrokeLengthMm) - - strokeZeroOffsetmm); - ESP_LOGD("UTILS", "OSSM has moved out, will now set new home"); - stepper.setCurrentPositionAsHomeAndStop(); - ESP_LOGD("UTILS", "OSSM should now be home and happy"); -} - -int OSSM::readEepromSettings() { - ESP_LOGD("UTILS", "read eeprom"); - EEPROM.begin(EEPROM_SIZE); - EEPROM.get(0, hardwareVersion); - EEPROM.get(4, numberStrokes); - EEPROM.get(12, travelledDistanceMeters); - EEPROM.get(20, lifeSecondsPoweredAtStartup); - - if (numberStrokes == NAN || numberStrokes <= 0) { - hardwareVersion = HW_VERSION; - numberStrokes = 0; - travelledDistanceMeters = 0; - lifeSecondsPoweredAtStartup = 0; - writeEepromSettings(); - } - - return hardwareVersion; -} - -void OSSM::writeEepromSettings() { - // Be very careful with this so you don't break your configuration! - ESP_LOGD("UTILS", "write eeprom"); - EEPROM.begin(EEPROM_SIZE); - EEPROM.put(0, HW_VERSION); - EEPROM.put(4, 0); - EEPROM.put(12, 0); - EEPROM.put(20, 0); - EEPROM.commit(); - ESP_LOGD("UTILS", "eeprom written"); -} -void OSSM::writeEepromLifeStats() { - // Be very careful with this so you don't break your configuration! - ESP_LOGD("UTILS", "writing eeprom life stats"); - ESP_LOGD("UTILS", "writing eeprom life stats"); - - EEPROM.begin(EEPROM_SIZE); - EEPROM.put(4, numberStrokes); - EEPROM.put(12, travelledDistanceMeters); - EEPROM.put(20, lifeSecondsPowered); - EEPROM.commit(); - ESP_LOGD("UTILS", "eeprom written"); -} - -void OSSM::updateLifeStats() { - float minutes; - float hours; - float days; - double travelledDistanceKilometers; - - travelledDistanceKilometers = (0.001 * travelledDistanceMeters); - lifeSecondsPowered = (0.001 * millis()) + lifeSecondsPoweredAtStartup; - minutes = lifeSecondsPowered / 60; - hours = minutes / 60; - days = hours / 24; - if ((millis() - lastLifeUpdateMillis) > 5000) { - ESP_LOGD("UTILS", "%i %i %i %i", (int(days)), (int(hours) % 24), - (int(minutes) % 60), (int(lifeSecondsPowered) % 60)); - ESP_LOGD("UTILS", "Strokes: %.0f", numberStrokes); - ESP_LOGD("UTILS", "Distance: %.2f km", travelledDistanceKilometers); - ESP_LOGD("UTILS", "Current: %.2f A", averageCurrent); - - lastLifeUpdateMillis = millis(); - } - if ((millis() - lastLifeWriteMillis) > 180000) { - // write eeprom every 3 minutes - writeEepromLifeStats(); - lastLifeWriteMillis = millis(); - } -} - -void OSSM::updateAnalogInputs() { - speedPercentage = getAnalogAveragePercent(SPEED_POT_PIN, 50); - - if (modeChanged) { - switch (rightKnobMode) { - case MODE_STROKE: - setEncoderPercentage(strokePercentage); - break; - case MODE_DEPTH: - setEncoderPercentage(depthPercentage); - break; - case MODE_SENSATION: - setEncoderPercentage(sensationPercentage); - break; - case MODE_PATTERN: - changePattern = 0; - setEncoderPercentage(50); - break; - } - - modeChanged = false; - } else { - switch (rightKnobMode) { - case MODE_STROKE: - strokePercentage = getEncoderPercentage(); - break; - case MODE_DEPTH: - depthPercentage = getEncoderPercentage(); - break; - case MODE_SENSATION: - sensationPercentage = getEncoderPercentage(); - break; - case MODE_PATTERN: - float patternPercentage = getEncoderPercentage(); - if (patternPercentage >= 52) { - changePattern = 1; - } else if (patternPercentage <= 48) { - changePattern = -1; - } else { - changePattern = 0; - } - break; - } - } - - immediateCurrent = getCurrentReadingAmps(20); - averageCurrent = immediateCurrent * 0.02 + averageCurrent * 0.98; -} - -float OSSM::getCurrentReadingAmps(int samples) { - float currentAnalogPercent = - getAnalogAveragePercent(36, samples) - currentSensorOffset; - float current = currentAnalogPercent * 0.13886f; - // 0.13886 is a scaling factor determined by real life testing. Convert - // percent full scale to amps. - return current; -} -float OSSM::getVoltageReading(int samples) {} - -void OSSM::setEncoderPercentage(float percentage) { - const int encoderFullScale = 100; - if (percentage < 0) { - percentage = 0; - } else if (percentage > 100) { - percentage = 100; - } - - int position = int(encoderFullScale * percentage / 100); - g_encoder.write(position); -} - -float OSSM::getEncoderPercentage() { - const int encoderFullScale = 100; - int position = g_encoder.read(); - float outputPositionPercentage; - if (position < 0) { - g_encoder.write(0); - position = 0; - } else if (position > encoderFullScale) { - g_encoder.write(encoderFullScale); - position = encoderFullScale; - } - - outputPositionPercentage = - 100.0f * float(position) / float(encoderFullScale); - - return outputPositionPercentage; -} - -float OSSM::getAnalogAveragePercent(int pinNumber, int samples) { - float sum = 0; - float average; - float percentage; - for (int i = 0; i < samples; i++) { - // TODO: Possibly use fancier filters? - sum += analogRead(pinNumber); - } - average = float(sum) / float(samples); - // TODO: Might want to add a deadband - percentage = 100.0f * average / 4096.0f; // 12 bit resolution - return percentage; -} - -bool OSSM::waitForAnyButtonPress(float waitMilliseconds) { - float timeStartMillis = millis(); - int initialEncoderFlag = encoderButtonPresses; - ESP_LOGD("UTILS", "Waiting for button press"); - while ((digitalRead(WIFI_RESET_PIN) == LOW) && - (initialEncoderFlag == encoderButtonPresses)) { - if ((millis() - timeStartMillis) > waitMilliseconds) { - ESP_LOGD("UTILS", "button not pressed"); - - return false; - } - delay(10); - } - ESP_LOGD("UTILS", "button pressed"); - return true; -} diff --git a/Software/src/Utilities.h b/Software/src/Utilities.h deleted file mode 100644 index 72087360..00000000 --- a/Software/src/Utilities.h +++ /dev/null @@ -1,139 +0,0 @@ -#ifndef UTILITIES_H -#define UTILITIES_H - -#include -#include -#include -#include // Current Motion Control -#include // Used for the Remote Encoder Input -#include -#include -#include - -#include "FastLED.h" // Used for the LED on the Reference Board (or any other pixel LEDS you may add) -#include "OSSM_Config.h" -#include "OSSM_PinDef.h" -#include "OssmUi/OssmUi.h" // Separate file that helps contain the OLED screen functions -#include "WiFi.h" -#include "WiFiManager.h" -#include "state/type.h" - -#define LED_TYPE WS2811 -#define COLOR_ORDER GRB -#define LED_PIN 25 -#define NUM_LEDS 1 -#define MODE_STROKE 0 -#define MODE_DEPTH 1 -#define MODE_SENSATION 2 -#define MODE_PATTERN 3 - -class OSSM { - public: - /** - * @brief Construct a new ossm object - */ - WiFiManager wm; - ESP_FlexyStepper stepper; - Encoder g_encoder; - OssmUi g_ui; - CRGB ossmleds[NUM_LEDS]{}; - - enum runMode { simpleMode, strokeEngineMode }; - int runModeCount = 2; - - runMode activeRunMode = strokeEngineMode; - float maxSpeedMmPerSecond = hardcode_maxSpeedMmPerSecond; - float motorStepPerRevolution = hardcode_motorStepPerRevolution; - float pulleyToothCount = hardcode_pulleyToothCount; - float beltPitchMm = hardcode_beltPitchMm; - float maxStrokeLengthMm = hardcode_maxStrokeLengthMm; - float strokeZeroOffsetmm = hardcode_strokeZeroOffsetmm; - float commandDeadzonePercentage = hardcode_commandDeadzonePercentage; - float accelerationScaling = hardcode_accelerationScaling; - - int hardwareVersion = 10; // V2.7 = integer value 27 - float currentSensorOffset = 0; - float immediateCurrent = 0; - double averageCurrent = 0; - float numberStrokes = 0; - double travelledDistanceMeters = 0; - float lifeSecondsPowered = 0; - float lifeSecondsPoweredAtStartup = 0; - float lastLifeUpdateMillis = 0; - float lastLifeWriteMillis = 0; - char Id[20]{}; - - bool wifiControlActive = false; - - float speedPercentage = 0; // percentage 0-100 - float depthPercentage = 100; // percentage 0-100 - float strokePercentage = 10; // percentage 0-100 - bool isStopped = false; - float sensationPercentage = 40; // percentage 0-100, maps to sensation -100 - // - 100, so 40 default = -20 sensation - unsigned int strokePattern = 0; - unsigned int strokePatternCount = 0; - int changePattern = 0; // -1 = prev, 1 = next - bool modeChanged = true; // initialize encoder state - int rightKnobMode = - 0; // MODE_STROKE, MODE_DEPTH, MODE_SENSATION, MODE_PATTERN - - String strokerPatternName = - "Default"; // The name of the current stroke engine pattern - - OSSM() - : g_encoder(ENCODER_A, ENCODER_B), - g_ui() // this just creates the objects with parameters - { - CFastLED::addLeds(ossmleds, NUM_LEDS) - .setCorrection(TypicalLEDStrip); - FastLED.setBrightness(100); - } - - void setup(); - void handleStopCondition(); // handles e-stop condition - [[noreturn]] void - runPenetrate(); // runs actual penetration motion one cycle - [[noreturn]] void runStrokeEngine(); // runs stroke Engine - String getPatternJSON(StrokeEngine Stroker); - void setRunMode(); - - // WiFi helper functions - void wifiAutoConnect(); - [[noreturn]] void wifiConnectOrHotspotNonBlocking(); - void enableWifiControl(); - bool setInternetControl(bool setWifiControl); - bool getInternetSettings(); - - void updatePrompt(); - void updateFirmware(); - bool checkForUpdate(); - bool checkConnection(); - - // hardware helper functions - void initializeStepperParameters(); - void initializeInputs(); - bool findHome(); - float sensorlessHoming(); - void sensorHoming(); - int readEepromSettings(); - void writeEepromSettings(); - void writeEepromLifeStats(); - void updateLifeStats(); - void startLeds(); - - // inputs - void updateAnalogInputs(); - float getCurrentReadingAmps(int samples); - float getVoltageReading(int samples); - - float getAnalogAveragePercent(int pinNumber, int samples); - void setEncoderPercentage(float percentage); - float getEncoderPercentage(); - bool waitForAnyButtonPress(float waitMilliseconds); - - OSSMState* sm; - void setStateMachine(OSSMState* a) { sm = a; } -}; - -#endif diff --git a/Software/src/constants/Config.h b/Software/src/constants/Config.h new file mode 100644 index 00000000..de2f87a4 --- /dev/null +++ b/Software/src/constants/Config.h @@ -0,0 +1,92 @@ +#ifndef OSSM_SOFTWARE_CONFIG_H +#define OSSM_SOFTWARE_CONFIG_H + +/** + Default Config for OSSM - Reference board users should tweak UserConfig to + match their personal build. + + //TODO: restore user overrides with a clever null coalescing operator +*/ +namespace Config { + + /** + Motion System Config + */ + namespace Driver { + + // Top linear speed of the device. + constexpr float maxSpeedMmPerSecond = 900.0f; + + // This should match the step/rev of your stepper or servo. + // N.b. the iHSV57 has a table on the side for setting the DIP switches + // to your preference. + constexpr float motorStepPerRevolution = 800.0f; + + // Number of teeth the pulley that is attached to the servo/stepper + // shaft has. + constexpr float pulleyToothCount = 20.0f; + + // Set to your belt pitch (Distance between two teeth on the belt) (E.g. + // GT2 belt has 2mm tooth pitch) + constexpr float beltPitchMm = 2.0f; + + // This is in millimeters, and is what's used to define how much of + // your rail is usable. + // The absolute max your OSSM would have is the distance between the + // belt attachments subtract the linear block holder length (75mm on + // OSSM) Recommended to also subtract e.g. 20mm to keep the backstop + // well away from the device. + constexpr float maxStrokeLengthMm = 150.f; + + // If the stroke length is less than this value, then the stroke is + // likely the result of a poor homing. + constexpr float minStrokeLengthMm = 50.0f; + + // This is the measured current that use to infer when the device has + // reached the end of its stroke. during "Homing". + constexpr float sensorlessCurrentLimit = 1.5f; + } + + /** + Web Config +*/ + namespace Web { + // This should be unique to your device. You will use this on the + // web portal to interact with your OSSM. + // there is NO security other than knowing this name, make this unique + // to avoid collisions with other users + constexpr char *ossmId = nullptr; + } + + /** + Font Config. These must be the "f" variants of the font to support other + languages. +*/ + namespace Font { + static auto bold = u8g2_font_helvB08_tf; + static auto base = u8g2_font_helvR08_tf; + static auto small = u8g2_font_6x10_tf; + } + + /** + Advanced Config +*/ + namespace Advanced { + + // After homingStart this is the physical buffer distance from the + // effective zero to the home switch This is to stop the home switch + // being smacked constantly + constexpr float strokeZeroOffsetMm = 6.0f; + // The minimum value of the pot in percent + // prevents noisy pots registering commands when turned down to zero by + // user + constexpr float commandDeadZonePercentage = 1.0f; + // affects acceleration in stepper trajectory (Aggressiveness of motion) + + constexpr float accelerationScaling = 100.0f; + + } + +} + +#endif // OSSM_SOFTWARE_CONFIG_H diff --git a/Software/src/constants/Images.h b/Software/src/constants/Images.h index d8c45d90..7e8fa6e3 100644 --- a/Software/src/constants/Images.h +++ b/Software/src/constants/Images.h @@ -72,6 +72,28 @@ namespace Images { 0xff, 0xff, 0xff, 0xff, 0x01, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xf8, 0xff, 0xff, 0xff, 0xff, 0x7f, 0x00, 0xe0, 0xff, 0xff, 0xff, 0xff, 0x1f, 0x00}; -} // namespace Images +} + +namespace WifiIcon { + + const int x = 120; + const int y = 0; + const int w = 8; + const int h = 8; + const unsigned char Connected[] PROGMEM = { + // 'wifi, 8x8px + 0x00, 0x0f, 0x10, 0x27, 0x48, 0x53, 0x54, 0x55}; + + const unsigned char First[] PROGMEM = { + // 'wifi, 8x8px + 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x04, 0x05}; + const unsigned char Second[] PROGMEM = { + // 'wifi, 8x8px + 0x00, 0x00, 0x00, 0x07, 0x08, 0x13, 0x14, 0x15, + }; + const unsigned char Error[] PROGMEM = { + // 'error, 8x8px + 0x00, 0x20, 0x20, 0x20, 0x20, 0x03, 0x24, 0x05}; +} #endif // OSSM_SOFTWARE_IMAGES_H diff --git a/Software/src/constants/LogTags.h b/Software/src/constants/LogTags.h index 781001f2..3e2592f8 100644 --- a/Software/src/constants/LogTags.h +++ b/Software/src/constants/LogTags.h @@ -5,4 +5,6 @@ static const char* STARTUP_TAG = "OSSM"; static const char* STATE_MACHINE_TAG = "StateLogger"; +static const char* UPDATE_TAG = "Update"; + #endif // SOFTWARE_LOGTAGS_H diff --git a/Software/src/constants/Menu.h b/Software/src/constants/Menu.h index 38ec4d04..606ce0a4 100644 --- a/Software/src/constants/Menu.h +++ b/Software/src/constants/Menu.h @@ -1,5 +1,26 @@ -#ifndef SOFTWARE_MENU_H -#define SOFTWARE_MENU_H +#ifndef OSSM_SOFTWARE_MENU_H +#define OSSM_SOFTWARE_MENU_H -enum Menu { SimplePenetration, StrokeEngine, Help, Restart, NUM_OPTIONS }; -#endif // SOFTWARE_MENU_H +#include + +#include "constants/UserConfig.h" + +enum Menu { + SimplePenetration, + StrokeEngine, + UpdateOSSM, + WiFiSetup, + Help, + Restart, + NUM_OPTIONS +}; + +static String menuStrings[Menu::NUM_OPTIONS] = { + UserConfig::language.SimplePenetration, + UserConfig::language.StrokeEngine, + UserConfig::language.Update, + UserConfig::language.WiFiSetup, + UserConfig::language.GetHelp, + UserConfig::language.Restart}; + +#endif // OSSM_SOFTWARE_MENU_H diff --git a/Software/src/constants/Pins.h b/Software/src/constants/Pins.h new file mode 100644 index 00000000..b6eda7e1 --- /dev/null +++ b/Software/src/constants/Pins.h @@ -0,0 +1,92 @@ +#ifndef OSSM_SOFTWARE_PINS_CPP +#define OSSM_SOFTWARE_PINS_CPP + +/** + * Pin definitions for the OSSM + * + * These are all the hardware pins used by the OSSM. + * These include pin definitions for Drivers, Buttons and Remotes + * + * To reference a pin, import this file and use the Pins namespace as follows: + * + * ```cpp + * #include "constants/pins.cpp" + * + * Pins::button_in + * Pins::Driver::motorStepPin + * Pins::Homing::stopPin + * + * ``` + */ + +namespace Pins { + + namespace Display { + // This pin is used by u8g2 to reset the display. + // Use -1 if this is shared with the board reset pin. + constexpr int oledReset = -1; + + // Pin used by RGB LED + constexpr int ledPin = 25; + } + + namespace Driver { + constexpr int currentSensorPin = 36; + + // Pin that pulses on servo/stepper steps - likely labelled PUL on + // drivers. + constexpr int motorStepPin = 14; + // Pin connected to driver/servo step direction - likely labelled DIR on + // drivers. N.b. to iHSV57 users - DIP switch #5 can be flipped to + // invert motor direction entirely + constexpr int motorDirectionPin = 27; + // Pin for motor enable - likely labelled ENA on drivers. + constexpr int motorEnablePin = 26; + + // define the IO pin the emergency stop switch is connected to + constexpr int stopPin = 19; + // define the IO pin where the limit(homingStart) switch(es) are + // connected to (switches in series in normally open setup) Switches + // wired from IO pin to ground. + constexpr int limitSwitchPin = 12; + } + + namespace Wifi { + // Pin for Wi-Fi reset button (optional) + constexpr int resetPin = 23; + // Pin for the toggle for Wi-Fi control (Can be targeted alone if no + // hardware toggle is required) + constexpr int controlTogglePin = 22; + + // TODO: #OSSM-21 No toggle switch should be required for control. The + // OSSM should always listen for commands and should decide whether to + // act on them based on the current control mode. constexpr int + // control_default = LOCAL_CONTROLLER; + } + + /** These are configured for the OSSM Remote - which has a screen, a + * potentiometer and an encoder which clicks*/ + namespace Remote { + + constexpr int speedPotPin = 34; + + // This switch occurs when you press the right button in. + // With the current state of the code this will send out a "ButtonPress" + // event automatically. + constexpr int encoderSwitch = 35; + + // The rotary encoder requires at least two pins to function. + constexpr int encoderA = 18; + constexpr int encoderB = 5; + constexpr int encoderPower = + -1; /* Put -1 of Rotary encoder Vcc is connected directly to 3,3V; + else you can use declared output pin for powering rotary + encoder */ + + constexpr int displayData = 21; + constexpr int displayClock = 19; + constexpr int encoderStepsPerNotch = 2; + } +} + +#endif // OSSM_SOFTWARE_PINS_CPP diff --git a/Software/src/constants/UserConfig.h b/Software/src/constants/UserConfig.h new file mode 100644 index 00000000..1826dbf1 --- /dev/null +++ b/Software/src/constants/UserConfig.h @@ -0,0 +1,17 @@ +#ifndef OSSM_SOFTWARE_USERCONFIG_H +#define OSSM_SOFTWARE_USERCONFIG_H + +#include "constants/copy/en-us.h" +#include "constants/copy/fr.h" + +namespace UserConfig { + // TODO: restore user overrides. + + // TODO: Create a simple way in the UI to change the language. + // Minimally, all we need to do is change the copy struct to the following: + // const LanguageStruct copy = fr; + // or any other language. + static LanguageStruct language = enUs; + static bool displayMetric = true; +} +#endif // OSSM_SOFTWARE_USERCONFIG_H diff --git a/Software/src/constants/copy/en-us.h b/Software/src/constants/copy/en-us.h new file mode 100644 index 00000000..1cb5f651 --- /dev/null +++ b/Software/src/constants/copy/en-us.h @@ -0,0 +1,40 @@ +#ifndef OSSM_SOFTWARE_EN_US_H +#define OSSM_SOFTWARE_EN_US_H + +#include "structs/LanguageStruct.h" + +// English copy +static const LanguageStruct enUs = { + .DeepThroatTrainerSync = "DeepThroat Sync", + .Error = "Error", + .GetHelp = "Get Help", + .GetHelpLine1 = "On Discord,", + .GetHelpLine2 = "or GitHub", + .Homing = "Homing", + .HomingTookTooLong = + "Homing took too long. Please check your wiring and try again.", + .Idle = "Initializing", + .InDevelopment = "This feature is in development.", + .MeasuringStroke = "Measuring Stroke", + .NoInternalLoop = "No display handler implemented.", + .Restart = "Restart", + .Settings = "Settings", + .SimplePenetration = "Simple Penetration", + .Skip = "Click to exit", + .Speed = "Speed", + .SpeedWarning = "Decrease the speed to begin playing.", + .StateNotImplemented = "State: %u not implemented.", + .Stroke = "Stroke", + .StrokeEngine = "Stroke Engine", + .StrokeTooShort = "Stroke too short. Please check you drive belt.", + .Update = "Update", + .UpdateMessage = "Update is in progress. This may take up to 60s.", + .WiFi = "Wi-Fi", + .WiFiSetup = "Wi-Fi Setup", + .WiFiSetupLine1 = "Connect to", + .WiFiSetupLine2 = "'Ossm Setup'", + .YouShouldNotBeHere = "You should not be here.", + +}; + +#endif // OSSM_SOFTWARE_EN_US_H diff --git a/Software/src/constants/copy/fr.h b/Software/src/constants/copy/fr.h new file mode 100644 index 00000000..a12ba895 --- /dev/null +++ b/Software/src/constants/copy/fr.h @@ -0,0 +1,43 @@ +#ifndef OSSM_SOFTWARE_FR_H +#define OSSM_SOFTWARE_FR_H + +#include "structs/LanguageStruct.h" + +// TODO: Requires validation by a native french speaker. +// These have been translated by Google Translate. +static const LanguageStruct fr = { + .DeepThroatTrainerSync = "DeepThroat Sync", + .Error = "Erreur", + .GetHelp = "Aide", + .GetHelpLine1 = "Sur Discord,", + .GetHelpLine2 = "ou GitHub", + .Homing = "FR - Homing", + .HomingTookTooLong = + "Le homing a pris trop de temps.Veuillez vérifier votre câblage et " + "réessayer.", + .Idle = "Inactif", + .InDevelopment = "Ceci est en développement.", + .MeasuringStroke = "Mesure de la course", + .NoInternalLoop = "Aucun gestionnaire d'affichage implémenté.", + .Restart = "Redémarrage", + .Settings = "Paramètres", + .SimplePenetration = "Pénétration simple", + .Skip = "Quitter ->", + .Speed = "Vitesse", + .SpeedWarning = "Réduisez la vitesse pour commencer à jouer.", + .StateNotImplemented = "État: %u non implémenté.", + .Stroke = "Coup", + .StrokeEngine = "Stroke Engine", + .StrokeTooShort = + "Course trop courte. Veuillez vérifier votre courroie d'entraînement.", + .Update = "Mettre à jour", + .UpdateMessage = + "La mise à jour est en cours. Ça peut prendre jusqu'à 60 secondes.", + .WiFi = "Wi-Fi", + .WiFiSetup = "Config. Wi-Fi", + .WiFiSetupLine1 = "Se connecter à", + .WiFiSetupLine2 = "'Ossm Setup'", + .YouShouldNotBeHere = "Vous ne devriez pas être ici.", +}; + +#endif // OSSM_SOFTWARE_FR_H diff --git a/Software/src/extensions/u8g2Extensions.h b/Software/src/extensions/u8g2Extensions.h new file mode 100644 index 00000000..3a152ed9 --- /dev/null +++ b/Software/src/extensions/u8g2Extensions.h @@ -0,0 +1,194 @@ +#ifndef OSSM_SOFTWARE_U8G2EXTENSIONS_H +#define OSSM_SOFTWARE_U8G2EXTENSIONS_H + +#include + +#include "constants/Config.h" +#include "services/display.h" + +// NOLINTBEGIN(hicpp-signed-bitwise) +static int getUTF8CharLength(const unsigned char c) { + if ((c & 0x80) == 0) + return 1; // ASCII + else if ((c & 0xE0) == 0xC0) + return 2; // 2-byte UTF8 + else if ((c & 0xF0) == 0xE0) + return 3; // 3-byte UTF8 + else if ((c & 0xF8) == 0xF0) + return 4; // 4-byte UTF8 + return 1; // default to 1 byte for unexpected scenarios +} +// NOLINTEND(hicpp-signed-bitwise) + +/** + * This namespace contains functions that extend the functionality of the + * U8G2 library. + * + * Specifically, it contains functions that are used to draw text on the + * display. + */ +namespace drawStr { + static void centered(int y, String str) { + // Convert the String object to a UTF-8 string. + // The c_str() function ensures we're passing a null-terminated string, + // which is required by getStrWidth(). + const char *utf8Str = str.c_str(); + + // Calculate the X position where the text should start in order to be + // centered. + int x = (display.getDisplayWidth() - display.getUTF8Width(utf8Str)) / 2; + + // Draw the string at the calculated position. + display.drawUTF8(x, y, utf8Str); + } + + /** + * u8g2E a string, breaking it into multiple lines if necessary. + * This will attempt to break at "/n" characters, or at spaces if no + * newlines are handlePress. + * @param x + * @param y + * @param str + */ + static void multiLine(int x, int y, String string, int lineHeight = 12) { + const char *str = string.c_str(); + // Set the font for the text to be displayed. + display.setFont(Config::Font::base); + + // Retrieve the width of the display. + int displayWidth = display.getDisplayWidth(); + + // Initialize a variable to keep track of the width of the string being + // processed. + int currentLineWidth = 0; + + // Buffer to store individual characters (glyphs) from the string. + // A single UTF-8 character can be up to 4 bytes in length, +1 for the + // null terminator. + char glyph[5] = {0}; + + // Pointers to keep track of the current position in the string and the + // last space character. + const char *currentChar = str; + const char *lastSpace = nullptr; + + // Loop through each character in the string. + while (*currentChar) { + // Skip any leading spaces or newline characters at the beginning of + // each line. + while (x == 0 && (*str == ' ' || *str == '\n')) { + if (currentChar == str++) ++currentChar; + } + + // Determine the length of the current UTF-8 character. + int charLength = getUTF8CharLength(*currentChar); + + // Copy the character to the glyph buffer. + strncpy(glyph, currentChar, charLength); + + // Null-terminate the copied text. + glyph[charLength] = 0; + + // Advance the current character pointer by the length of the + // character just processed. + currentChar += charLength; + + // Add the width of the current glyph to the current line width. + currentLineWidth += display.getStrWidth(glyph); + + // Check for space character; if found, remember its position. + if (*glyph == ' ') lastSpace = currentChar - charLength; + + // If not a space, increment the width (for the space between + // characters). + else + ++currentLineWidth; + + // Check for a new line character or if the text exceeds the display + // width. + if (*glyph == '\n' || x + currentLineWidth > displayWidth) { + int startingPosition = + x; // Remember the starting horizontal position. + + // Display characters up to the last space (or current position + // if no space). + while (str < (lastSpace ? lastSpace : currentChar)) { + // Get the length of the next character to display. + charLength = getUTF8CharLength(*str); + // Copy the character to the glyph buffer. + strncpy(glyph, str, charLength); + glyph[charLength] = 0; // Null-terminate the copied text. + + // Draw the character on the display and advance the x + // position. + x += display.drawUTF8(x, y, glyph); + + // Advance the main string pointer. + str += charLength; + } + + // Adjust the current line width for the new line. + currentLineWidth -= x - startingPosition; + + // Move to the next line on the display. + y += lineHeight; + + // Reset the horizontal position for the new line. + x = 0; + + // Reset the last space pointer for the new line. + lastSpace = nullptr; + } + } + + // Process any remaining characters in the string. + while (*str) { + // Get the length of the next character to display. + int charLength = getUTF8CharLength(*str); + + // Copy the character to the glyph buffer. + strncpy(glyph, str, charLength); + glyph[charLength] = 0; // Null-terminate the copied text. + + // Draw the character on the display and advance the x position. + x += display.drawUTF8(x, y, glyph); + + // Advance the main string pointer. + str += charLength; + } + } + + static void title(String str) { + display.setFont(Config::Font::bold); + centered(8, std::move(str)); + } +}; + +namespace drawShape { + static void scroll(long position) { + int topMargin = 10; // Margin at the top of the screen + + int scrollbarHeight = 64 - topMargin; // Height of the scrollbar + int scrollbarWidth = 3; // Width of the scrollbar + int scrollbarX = + 125; // X position of the scrollbar (right edge of the screen) + int scrollbarY = (64 - scrollbarHeight + topMargin) / + 2; // Y position of the scrollbar (centered) + + // Draw the dotted line + for (int i = 0; i < scrollbarHeight; i += 4) { + display.drawHLine(scrollbarX + 1, scrollbarY + i, 1); + } + + // Calculate the Y position of the rectangle based on the current + // position + int rectY = + scrollbarY + (scrollbarHeight - scrollbarWidth) * position / 100; + + // Draw the rectangle to represent the current position + display.drawBox(scrollbarX, rectY, scrollbarWidth, scrollbarWidth); + }; + +} + +#endif // OSSM_SOFTWARE_U8G2EXTENSIONS_H diff --git a/Software/src/main.cpp b/Software/src/main.cpp index 74435918..5cc2af55 100644 --- a/Software/src/main.cpp +++ b/Software/src/main.cpp @@ -1,221 +1,67 @@ -#include // Basic Needs -#include // Used for i2c connections (Remote OLED Screen) - -#include "OSSM_PinDef.h" // This is where you set pins specific for your board -#include "Utilities.h" // Utility helper functions - wifi update and homing -#include "state/state.h" -#include "state/type.h" -#include "utils/StateLogger.h" - -namespace sml = boost::sml; - -// OSSM name setup -const char *ossmId = "OSSM1"; -volatile int encoderButtonPresses = 0; // increment for each click -volatile long lastEncoderButtonPressMillis = 0; - -IRAM_ATTR void encoderPushButton() { - // debounce check - long currentTime = millis(); - if ((currentTime - lastEncoderButtonPressMillis) > 200) { - // run interrupt if not run in last 50ms - encoderButtonPresses++; - lastEncoderButtonPressMillis = currentTime; - } -} - -// Create tasks for checking pot input or web server control, and task to handle -// planning the motion profile (this task is high level only and does not pulse -// the stepper!) -TaskHandle_t wifiTask = nullptr; -TaskHandle_t getInputTask = nullptr; -TaskHandle_t motionTask = nullptr; - -// Declarations -void getUserInputTask(void *pvParameters); -void motionCommandTask(void *pvParameters); -void wifiConnectionTask(void *pvParameters); - -// create the OSSM hardware object -OSSM ossm; -StateLogger ossmLogger; -OSSMState stateMachine{ossmLogger, ossm}; - -/////////////////////////////////////////// -//// -//// VOID SETUP -- Here's where it's hiding -//// -/////////////////////////////////////////// +#include "Arduino.h" +#include "WiFi.h" +#include "ossm/Events.h" +#include "ossm/OSSM.h" +#include "services/board.h" +#include "services/display.h" +#include "services/encoder.h" + +/* + * ██████╗ ███████╗███████╗███╗ ███╗ + * ██╔═══██╗██╔════╝██╔════╝████╗ ████║ + * ██║ ██║███████╗███████╗██╔████╔██║ + * ██║ ██║╚════██║╚════██║██║╚██╔╝██║ + * ╚██████╔╝███████║███████║██║ ╚═╝ ██║ + * ╚═════╝ ╚══════╝╚══════╝╚═╝ ╚═╝ + * + * Welcome to the open source sex machine! + * This is a product of Kinky Makers and is licensed under the MIT license. + * + * Research and Desire is a financial sponsor of this project. + * + * But our biggest sponsor is you! If you want to support this project, please + * contribute, fork, branch and share! + */ + +OSSM *ossm; + +// TODO: Move this to a service +bool handlePress = false; +int counter = 0; +bool isDouble = false; +long lastPressed = 0; + +void IRAM_ATTR encoderPressed() { handlePress = true; } void setup() { - Serial.begin(115200); - ESP_LOGD(STARTUP_TAG, "Starting"); - pinMode(ENCODER_SWITCH, INPUT_PULLDOWN); // Rotary Encoder Pushbutton - attachInterrupt(digitalPinToInterrupt(ENCODER_SWITCH), encoderPushButton, - RISING); - ossm.setStateMachine(&stateMachine); - stateMachine.process_event(Done{}); - - xTaskCreatePinnedToCore( - wifiConnectionTask, /* Task function. */ - "wifiConnectionTask", /* name of task. */ - 10000, /* Stack size of task */ - nullptr, /* parameter of the task */ - 1, /* priority of the task */ - &wifiTask, /* Task handle to keep track of created task */ - 0); /* pin task to core 0 */ - delay(100); - - // Kick off the http and motion tasks - they begin executing as soon as they - // are created here! Do not change the priority of the task, or do so with - // caution. RTOS runs first in first out, so if there are no delays in your - // tasks they will prevent all other code from running on that core! - xTaskCreatePinnedToCore( - getUserInputTask, /* Task function. */ - "getUserInputTask", /* name of task. */ - 10000, /* Stack size of task */ - nullptr, /* parameter of the task */ - 1, /* priority of the task */ - &getInputTask, /* Task handle to keep track of created task */ - 0); /* pin task to core 0 */ - - delay(100); - - xTaskCreatePinnedToCore( - motionCommandTask, /* Task function. */ - "motionCommandTask", /* name of task. */ - 20000, /* Stack size of task */ - nullptr, /* parameter of the task */ - 1, /* priority of the task */ - &motionTask, /* Task handle to keep track of created task */ - 0); /* pin task to core 0 */ + /** Board setup */ + initBoard(); - delay(100); + /** Service setup */ + // Encoder + initEncoder(); + // Display + display.begin(); - OssmUi::UpdateMessage("OSSM Ready to Play"); -} // Void Setup() + ossm = new OSSM(display, encoder); -/////////////////////////////////////////// -//// -//// -//// VOID LOOP - Hides here -//// -//// -/////////////////////////////////////////// + attachInterrupt(digitalPinToInterrupt(Pins::Remote::encoderSwitch), + encoderPressed, RISING); +}; void loop() { - switch (ossm.rightKnobMode) { - case MODE_STROKE: - OssmUi::UpdateState("STROKE", - static_cast(ossm.speedPercentage), - static_cast(ossm.strokePercentage + 0.5f)); - break; - case MODE_DEPTH: - OssmUi::UpdateState("DEPTH", static_cast(ossm.speedPercentage), - static_cast(ossm.depthPercentage + 0.5f)); - break; - case MODE_SENSATION: - OssmUi::UpdateState( - "SENSTN", static_cast(ossm.speedPercentage), - static_cast(ossm.sensationPercentage + 0.5f)); - break; - case MODE_PATTERN: - OssmUi::UpdateState( - ossm.strokerPatternName, static_cast(ossm.speedPercentage), - ossm.strokePattern * 100 / (ossm.strokePatternCount - 1)); - break; - } -} - -/////////////////////////////////////////// -//// -//// -//// freeRTOS multitasking -//// -//// -/////////////////////////////////////////// - -void wifiConnectionTask(void *pvParameters) { - ossm.wifiConnectOrHotspotNonBlocking(); -} - -// Task to read settings from server - only need to check this when in WiFi -// control mode -void getUserInputTask(void *pvParameters) { - float targetSpeedMmPerSecond = 0; - float targetAccelerationMm = 0; - float previousSpeedPercentage = 0; - for (;;) // tasks should loop forever and not return - or will throw error - // in OS - { - ossm.updateAnalogInputs(); - ossm.handleStopCondition(); + // TODO: Relocate this code. + if (handlePress) { + handlePress = false; - if (digitalRead(WIFI_CONTROL_TOGGLE_PIN) == - HIGH) // TODO: check if wifi available and handle gracefully - { - ossm.enableWifiControl(); + // detect if a double click occurred + if (millis() - lastPressed < 300) { + ossm->sm->process_event(DoublePress{}); } else { - if (ossm.wifiControlActive == true) { - // this is a transition to local control, we should tell the - // server it cannot control - - ossm.setInternetControl(false); - } + ossm->sm->process_event(ButtonPress{}); } + lastPressed = millis(); - // We should scale these values with initialized settings not hard coded - // values! - if (ossm.speedPercentage > ossm.commandDeadzonePercentage) { - targetSpeedMmPerSecond = - ossm.maxSpeedMmPerSecond * ossm.speedPercentage / 100.0; - if ((ossm.speedPercentage - previousSpeedPercentage) > 30) { - targetSpeedMmPerSecond = - 0.5 * (ossm.speedPercentage + previousSpeedPercentage) * - ossm.maxSpeedMmPerSecond / 100.0; - } - targetAccelerationMm = ossm.maxSpeedMmPerSecond * - ossm.speedPercentage * ossm.speedPercentage / - ossm.accelerationScaling; - - ossm.stepper.setAccelerationInMillimetersPerSecondPerSecond( - targetAccelerationMm); - if (targetSpeedMmPerSecond > - ossm.stepper - .getCurrentVelocityInMillimetersPerSecond()) { // we are - // speeding - // up, we - // need to - // increase - // deccel - // rate! - ossm.stepper.setDecelerationInMillimetersPerSecondPerSecond( - targetAccelerationMm); - } - - ossm.stepper.setSpeedInMillimetersPerSecond(targetSpeedMmPerSecond); - - // If target speed is lower than current, we do not update deccel as - // setting a low decel when going from high to low speed causes the - // motor to travel a long distance before slowing. - } - previousSpeedPercentage = ossm.speedPercentage; - vTaskDelay(50); // let other code run! - } -} - -void motionCommandTask(void *pvParameters) { - for (;;) // tasks should loop forever and not return - or will throw error - // in OS - { - switch (ossm.activeRunMode) { - case ossm.simpleMode: - ossm.runPenetrate(); - break; - - case ossm.strokeEngineMode: - ossm.runStrokeEngine(); - break; - } + ESP_LOGD("Loop", "%sButton Press", isDouble ? "Double " : ""); } -} +}; \ No newline at end of file diff --git a/Software/src/ossm/Actions.h b/Software/src/ossm/Actions.h new file mode 100644 index 00000000..0a26e3db --- /dev/null +++ b/Software/src/ossm/Actions.h @@ -0,0 +1,9 @@ +#ifndef SOFTWARE_ACTIONS_H +#define SOFTWARE_ACTIONS_H + +// include the ESP +#include "Esp.h" + +auto restart = []() { ESP.restart(); }; + +#endif // SOFTWARE_ACTIONS_H diff --git a/Software/src/ossm/Events.h b/Software/src/ossm/Events.h new file mode 100644 index 00000000..28cf73cd --- /dev/null +++ b/Software/src/ossm/Events.h @@ -0,0 +1,33 @@ +#ifndef OSSM_SOFTWARE_EVENTS_H +#define OSSM_SOFTWARE_EVENTS_H + +#include "boost/sml.hpp" +namespace sml = boost::sml; + +/** + * These are the events that the OSSM state machine can respond to. + * They are used in OSSM.h and can be called from anywhere in the code that has + * access to an OSSM state machine + * + * For Example: + * ossm->sm.process_event(ButtonPress{}); + * + * + * There's nothing special about these events, they are just structs. + * They just happen to be defined inside of the OSSM State Machine class. + */ +struct ButtonPress {}; + +struct DoublePress {}; + +struct Done {}; + +struct Error {}; + +// Definitions to make the table easier to read. +static auto buttonPress = sml::event; +static auto doublePress = sml::event; +static auto done = sml::event; +static auto error = sml::event; + +#endif // OSSM_SOFTWARE_EVENTS_H diff --git a/Software/src/ossm/Guard.h b/Software/src/ossm/Guard.h new file mode 100644 index 00000000..df1d5258 --- /dev/null +++ b/Software/src/ossm/Guard.h @@ -0,0 +1,8 @@ +#ifndef SOFTWARE_GUARD_H +#define SOFTWARE_GUARD_H + +#include "WiFi.h" +#include "constants/Menu.h" +static auto isOnline = []() { return WiFiClass::status() == WL_CONNECTED; }; + +#endif // SOFTWARE_GUARD_H diff --git a/Software/src/ossm/OSSM.Help.cpp b/Software/src/ossm/OSSM.Help.cpp new file mode 100644 index 00000000..53ae8851 --- /dev/null +++ b/Software/src/ossm/OSSM.Help.cpp @@ -0,0 +1,46 @@ + +#include "OSSM.h" + +#include "extensions/u8g2Extensions.h" +#include "qrcode.h" + +void OSSM::drawHelp() { + display.clearBuffer(); + + static QRCode qrcode; + const int scale = 2; + // This Version of QR Codes can handle ~61 alphanumeric characters with ECC + // LEVEL M + + // NOLINTBEGIN(modernize-avoid-c-arrays) + uint8_t qrcodeData[qrcode_getBufferSize(3)]; + // NOLINTEND(modernize-avoid-c-arrays) + + String url = "https://unbox.ossm.tech"; + + qrcode_initText(&qrcode, qrcodeData, 3, 0, url.c_str()); + + int yOffset = constrain((64 - qrcode.size * scale) / 2, 0, 64); + int xOffset = constrain((128 - qrcode.size * scale), 0, 128); + + // Draw the QR code + for (uint8_t y = 0; y < qrcode.size; y++) { + for (uint8_t x = 0; x < qrcode.size; x++) { + if (qrcode_getModule(&qrcode, x, y)) { + display.drawBox(xOffset + x * scale, yOffset + y * scale, scale, + scale); + } + } + } + + display.setFont(Config::Font::bold); + display.drawUTF8(0, 10, UserConfig::language.GetHelp.c_str()); + // Draw line + display.drawHLine(0, 12, xOffset - 10); + + display.setFont(Config::Font::base); + display.drawUTF8(0, 26, UserConfig::language.GetHelpLine1.c_str()); + display.drawUTF8(0, 38, UserConfig::language.GetHelpLine2.c_str()); + display.drawUTF8(0, 62, UserConfig::language.Skip.c_str()); + display.sendBuffer(); +} diff --git a/Software/src/ossm/OSSM.Homing.cpp b/Software/src/ossm/OSSM.Homing.cpp new file mode 100644 index 00000000..d7c301ba --- /dev/null +++ b/Software/src/ossm/OSSM.Homing.cpp @@ -0,0 +1,131 @@ +#include "OSSM.h" + +#include "Events.h" +#include "constants/UserConfig.h" +#include "services/stepper.h" +#include "utils/analog.h" + +namespace sml = boost::sml; +using namespace sml; + +/** OSSM Homing methods + * + * This is a collection of methods that are associated with the homing state on + * the OSSM. technically anything can go here, but please try to keep it + * organized. + */ +void OSSM::clearHoming() { + ESP_LOGD("Homing", "Homing started"); + isForward = true; + + // Drop the speed and acceleration to something reasonable. + stepper.setAccelerationInMillimetersPerSecondPerSecond(1000); + stepper.setDecelerationInMillimetersPerSecondPerSecond(10000); + stepper.setSpeedInMillimetersPerSecond(25); + + // Clear the stored values. + this->measuredStrokeMm = 0; + + // Recalibrate the current sensor offset. + this->currentSensorOffset = (getAnalogAveragePercent( + SampleOnPin{Pins::Driver::currentSensorPin, 1000})); +}; + +void OSSM::startHomingTask(void *pvParameters) { + TickType_t xTaskStartTime = xTaskGetTickCount(); + + // parse parameters to get OSSM reference + OSSM *ossm = (OSSM *)pvParameters; + + float target = ossm->isForward ? -400 : 400; + ossm->stepper.setTargetPositionInMillimeters(target); + + auto isInCorrectState = [](OSSM *ossm) { + // Add any states that you want to support here. + return ossm->sm->is("homing"_s) || ossm->sm->is("homing.idle"_s) || + ossm->sm->is("homing.backward"_s); + }; + // run loop for 15second or until loop exits + while (isInCorrectState(ossm)) { + TickType_t xCurrentTickCount = xTaskGetTickCount(); + // Calculate the time in ticks that the task has been running. + TickType_t xTicksPassed = xCurrentTickCount - xTaskStartTime; + + // If you need the time in milliseconds, convert ticks to milliseconds. + // 'portTICK_PERIOD_MS' is the number of milliseconds per tick. + uint32_t msPassed = xTicksPassed * portTICK_PERIOD_MS; + + if (msPassed > 15000) { + ESP_LOGE("Homing", "Homing took too long. Check power and restart"); + ossm->errorMessage = UserConfig::language.HomingTookTooLong; + ossm->sm->process_event(Error{}); + break; + } + + // measure the current analog value. + float current = getAnalogAveragePercent( + SampleOnPin{Pins::Driver::currentSensorPin, 200}) - + ossm->currentSensorOffset; + + ESP_LOGV("Homing", "Current: %f", current); + + // If we have not detected a "bump" with a hard stop, then return and + // let the loop continue. + if (current < Config::Driver::sensorlessCurrentLimit && + ossm->stepper.getCurrentPositionInMillimeters() < + Config::Driver::maxStrokeLengthMm) { + // Saying hi to the watchdog :). + vTaskDelay(1); + continue; + } + + ESP_LOGD("Homing", "Homing bump detected"); + + // Otherwise, if we have detected a bump, then we need to stop the + // motor. + ossm->stepper.setTargetPositionToStop(); + + // And then move the motor back by the configured offset. + // This offset will be positive for reverse homing and negative for + // forward homing. + float sign = ossm->isForward ? 1.0f : -1.0f; + ossm->stepper.moveRelativeInMillimeters( + sign * + Config::Advanced::strokeZeroOffsetMm); //"move to" is blocking + + if (!ossm->isForward) { + // If we are homing backward, then we need to measure the stroke + // length before resetting the home position. + ossm->measuredStrokeMm = + min(abs(ossm->stepper.getCurrentPositionInMillimeters()), + Config::Driver::maxStrokeLengthMm); + + ESP_LOGD("Homing", "Measured stroke %d", ossm->measuredStrokeMm); + } + + // And finally, we'll set the most forward position as the new "zero" + // position. + ossm->stepper.setCurrentPositionAsHomeAndStop(); + + // Set the event to done so that the machine will move to the next + // state. + ossm->sm->process_event(Done{}); + break; + }; + + vTaskDelete(nullptr); +} + +void OSSM::startHoming() { + // Create task + xTaskCreatePinnedToCore(startHomingTask, "startHomingTask", 10000, this, 1, + &operationTask, 0); +} + +auto OSSM::isStrokeTooShort() -> bool { + if (measuredStrokeMm > Config::Driver::minStrokeLengthMm) { + return false; + } + this->errorMessage = UserConfig::language.StrokeTooShort; + return true; +} diff --git a/Software/src/ossm/OSSM.Menu.cpp b/Software/src/ossm/OSSM.Menu.cpp new file mode 100644 index 00000000..bb2edb9e --- /dev/null +++ b/Software/src/ossm/OSSM.Menu.cpp @@ -0,0 +1,138 @@ +#include "OSSM.h" + +#include "constants/Config.h" +#include "constants/Images.h" +#include "extensions/u8g2Extensions.h" +#include "utils/analog.h" + +void OSSM::drawMenuTask(void *pvParameters) { + bool isFirstDraw = true; + OSSM *ossm = (OSSM *)pvParameters; + + int lastEncoderValue = ossm->encoder.readEncoder(); + int currentEncoderValue; + int clicksPerRow = 3; + const int maxClicks = clicksPerRow * (Menu::NUM_OPTIONS)-1; + ossm->encoder.setBoundaries(0, maxClicks, true); + ossm->encoder.setAcceleration(0); + + ossm->menuOption = (Menu)floor(ossm->encoder.readEncoder() / clicksPerRow); + + ossm->encoder.setAcceleration(0); + // ossm->encoder.setEncoderValue(0); + + // get the encoder position + + auto isInCorrectState = [](OSSM *ossm) { + // Add any states that you want to support here. + return ossm->sm->is("menu"_s) || ossm->sm->is("menu.idle"_s); + }; + + while (isInCorrectState(ossm)) { + if (!isFirstDraw && !ossm->encoder.encoderChanged()) { + vTaskDelay(1); + continue; + } + + isFirstDraw = false; + currentEncoderValue = ossm->encoder.readEncoder(); + + ossm->display.clearBuffer(); + + // Drawing Variables. + int leftPadding = 6; // Padding on the left side of the screen + int fontSize = 8; + int itemHeight = 20; // Height of each item + int visibleItems = 3; // Number of items visible on the screen + + auto menuOption = ossm->menuOption; + if (abs(currentEncoderValue % maxClicks - + lastEncoderValue % maxClicks) >= clicksPerRow) { + lastEncoderValue = currentEncoderValue % maxClicks; + menuOption = (Menu)floor(lastEncoderValue / clicksPerRow); + + ossm->menuOption = menuOption; + } + + ESP_LOGD( + "Menu", + "currentEncoderValue: %d, lastEncoderValue: %d, menuOption: %d", + currentEncoderValue, lastEncoderValue, menuOption); + + drawShape::scroll(100 * ossm->encoder.readEncoder() / + (clicksPerRow * Menu::NUM_OPTIONS - 1)); + String menuName = menuStrings[menuOption]; + ESP_LOGD("Menu", "Hovering over state: %s", menuName.c_str()); + + // Loop around to make an infinite menu. + int lastIdx = + menuOption - 1 < 0 ? Menu::NUM_OPTIONS - 1 : menuOption - 1; + int nextIdx = + menuOption + 1 > Menu::NUM_OPTIONS - 1 ? 0 : menuOption + 1; + + ossm->display.setFont(Config::Font::base); + + // Draw the previous item + if (lastIdx >= 0) { + ossm->display.drawUTF8(leftPadding, itemHeight * (1), + menuStrings[lastIdx].c_str()); + } + + // Draw the next item + if (nextIdx < Menu::NUM_OPTIONS) { + ossm->display.drawUTF8(leftPadding, itemHeight * (3), + menuStrings[nextIdx].c_str()); + } + + // Draw the current item + ossm->display.setFont(Config::Font::bold); + ossm->display.drawUTF8(leftPadding, itemHeight * (2), menuName.c_str()); + + // Draw a rounded rectangle around the center item + ossm->display.drawRFrame( + 0, itemHeight * (visibleItems / 2) - (fontSize - itemHeight) / 2, + 120, itemHeight, 2); + + // Draw Shadow. + ossm->display.drawLine(2, 2 + fontSize / 2 + 2 * itemHeight, 119, + 2 + fontSize / 2 + 2 * itemHeight); + ossm->display.drawLine(120, 4 + fontSize / 2 + itemHeight, 120, + 1 + fontSize / 2 + 2 * itemHeight); + + // Draw the wifi icon + + // Display the appropriate Wi-Fi icon based on the current Wi-Fi status + switch (WiFiClass::status()) { + case WL_CONNECTED: + ossm->display.drawXBMP(WifiIcon::x, WifiIcon::y, WifiIcon::w, + WifiIcon::h, WifiIcon::Connected); + break; + case WL_NO_SSID_AVAIL: + case WL_CONNECT_FAILED: + case WL_DISCONNECTED: + ossm->display.drawXBMP(WifiIcon::x, WifiIcon::y, WifiIcon::w, + WifiIcon::h, WifiIcon::Error); + break; + case WL_IDLE_STATUS: + ossm->display.drawXBMP(WifiIcon::x, WifiIcon::y, WifiIcon::w, + WifiIcon::h, WifiIcon::First); + break; + default: + ossm->display.drawXBMP(WifiIcon::x, WifiIcon::y, WifiIcon::w, + WifiIcon::h, WifiIcon::Error); + break; + } + + ossm->display.sendBuffer(); + + vTaskDelay(1); + }; + + vTaskDelete(nullptr); +} + +void OSSM::drawMenu() { + // start the draw menu task + xTaskCreatePinnedToCore(drawMenuTask, "drawMenuTask", 2048, this, 1, + &displayTask, 0); +} \ No newline at end of file diff --git a/Software/src/ossm/OSSM.PlayControls.cpp b/Software/src/ossm/OSSM.PlayControls.cpp new file mode 100644 index 00000000..a31e364c --- /dev/null +++ b/Software/src/ossm/OSSM.PlayControls.cpp @@ -0,0 +1,199 @@ + +#include "OSSM.h" + +#include "extensions/u8g2Extensions.h" +#include "utils/analog.h" +#include "utils/format.h" + +void OSSM::drawPlayControlsTask(void *pvParameters) { + // parse ossm from the parameters + OSSM *ossm = (OSSM *)pvParameters; + auto menuString = menuStrings[ossm->menuOption]; + float speedPercentage; + long strokePercentage; + int currentTime; + + ossm->speedPercentage = 0; + ossm->strokePercentage = 0; + + // Set the stepper to the home position + ossm->stepper.setAccelerationInMillimetersPerSecondPerSecond(1000); + ossm->stepper.setAccelerationInMillimetersPerSecondPerSecond(10000); + ossm->stepper.setTargetPositionInMillimeters(0); + + /** + * ///////////////////////////////////////////// + * //// Safely Block High Speeds on Startup /// + * ///////////////////////////////////////////// + * + * This is a safety feature to prevent the user from accidentally beginning + * a session at max speed. After the user decreases the speed to 0.5% or + * less, the state machine will be allowed to continue. + */ + + auto isInPreflight = [](OSSM *ossm) { + // Add your preflight checks states here. + return ossm->sm->is("simplePenetration.preflight"_s); + }; + + do { + speedPercentage = + getAnalogAveragePercent(SampleOnPin{Pins::Remote::speedPotPin, 50}); + if (speedPercentage < 0.5) { + ossm->sm->process_event(Done{}); + break; + }; + + ossm->display.clearBuffer(); + drawStr::title(menuString); + String speedString = UserConfig::language.Speed + ": " + + String((int)speedPercentage) + "%"; + drawStr::centered(25, speedString); + drawStr::multiLine(0, 40, UserConfig::language.SpeedWarning); + + ossm->display.sendBuffer(); + vTaskDelay(100); + } while (isInPreflight(ossm)); + + /** + * ///////////////////////////////////////////// + * /////////// Play Controls Display /////////// + * ///////////////////////////////////////////// + * + * This is a safety feature to prevent the user from accidentally beginning + * a session at max speed. After the user decreases the speed to 0.5% or + * less, the state machine will be allowed to continue. + */ + auto isInCorrectState = [](OSSM *ossm) { + // Add any states that you want to support here. + return ossm->sm->is("simplePenetration"_s) || + ossm->sm->is("simplePenetration.idle"_s); + }; + + // Prepare the encoder + ossm->encoder.setBoundaries(0, 100, false); + ossm->encoder.setAcceleration(25); + ossm->encoder.setEncoderValue(0); + + // TODO: prepare the stepper with safe values. + + int16_t y = 64; + int16_t w = 10; + int16_t x; + int16_t h; + int16_t padding = 4; + + // Line heights + short lh1 = 25; + short lh2 = 37; + short lh3 = 56; + short lh4 = 64; + + // record session start time rounded to the nearest second + ossm->sessionStartTime = floor(millis() / 1000); + ossm->sessionStrokeCount = 0; + ossm->sessionDistanceMeters = 0; + + bool valueChanged = false; + + while (isInCorrectState(ossm)) { + speedPercentage = + getAnalogAveragePercent(SampleOnPin{Pins::Remote::speedPotPin, 50}); + strokePercentage = ossm->encoder.readEncoder(); + currentTime = floor(millis() / 1000); + + if (ossm->speedPercentage != speedPercentage || + ossm->strokePercentage != strokePercentage || + ossm->sessionStartTime != currentTime) { + valueChanged = true; + ossm->speedPercentage = speedPercentage; + ossm->strokePercentage = strokePercentage; + } + + if (!valueChanged) { + vTaskDelay(100); + continue; + } + + ossm->display.clearBuffer(); + ossm->display.setFont(Config::Font::base); + + drawStr::title(menuString); + + /** + * ///////////////////////////////////////////// + * /////////// Play Controls Left ///////////// + * ///////////////////////////////////////////// + * + * These controls are associated with speed and time. + */ + x = 0; + h = ceil(64 * speedPercentage / 100); + ossm->display.drawBox(x, y - h, w, h); + ossm->display.drawFrame(x, 0, w, 64); + String speedString = String((int)speedPercentage) + "%"; + ossm->display.setFont(Config::Font::base); + ossm->display.drawUTF8(x + w + padding, lh1, + UserConfig::language.Speed.c_str()); + ossm->display.drawUTF8(x + w + padding, lh2, speedString.c_str()); + ossm->display.setFont(Config::Font::small); + ossm->display.drawUTF8( + x + w + padding, lh3, + formatTime(currentTime - ossm->sessionStartTime).c_str()); + + /** + * ///////////////////////////////////////////// + * /////////// Play Controls Right //////////// + * ///////////////////////////////////////////// + * + * These controls are associated with stroke and distance + */ + x = 124; + h = ceil(64 * ossm->encoder.readEncoder() / 100); + ossm->display.drawBox(x - w, y - h, w, h); + ossm->display.drawFrame(x - w, 0, w, 64); + + // The word "stroke" + ossm->display.setFont(Config::Font::base); + + String strokeString = UserConfig::language.Stroke; + auto stringWidth = ossm->display.getUTF8Width(strokeString.c_str()); + ossm->display.drawUTF8(x - w - stringWidth - padding, lh1, + strokeString.c_str()); + + // The stroke percent + strokeString = String(ossm->encoder.readEncoder()) + "%"; + stringWidth = ossm->display.getUTF8Width(strokeString.c_str()); + ossm->display.drawUTF8(x - w - stringWidth - padding, lh2, + strokeString.c_str()); + + // The stroke count + ossm->display.setFont(Config::Font::small); + strokeString = "# " + String(ossm->sessionStrokeCount); + stringWidth = ossm->display.getUTF8Width(strokeString.c_str()); + ossm->display.drawUTF8(x - w - stringWidth - padding, lh3, + strokeString.c_str()); + + // The Session travel distance + strokeString = formatDistance(ossm->sessionDistanceMeters); + stringWidth = ossm->display.getUTF8Width(strokeString.c_str()); + ossm->display.drawUTF8(x - w - stringWidth - padding, lh4, + strokeString.c_str()); + + ossm->display.sendBuffer(); + + // Saying hi to the watchdog :). + vTaskDelay(200); + } + + // Clean up! + ossm->encoder.setAcceleration(0); + ossm->encoder.disableAcceleration(); + + vTaskDelete(nullptr); +} + +void OSSM::drawPlayControls() { + xTaskCreatePinnedToCore(drawPlayControlsTask, "drawPlayControlsTask", 2048, + this, 1, &displayTask, 0); +} diff --git a/Software/src/ossm/OSSM.SimplePenetration.cpp b/Software/src/ossm/OSSM.SimplePenetration.cpp new file mode 100644 index 00000000..c685645f --- /dev/null +++ b/Software/src/ossm/OSSM.SimplePenetration.cpp @@ -0,0 +1,88 @@ +#include "OSSM.h" + +#include "constants/Config.h" + +void OSSM::startSimplePenetrationTask(void *pvParameters) { + OSSM *ossm = (OSSM *)pvParameters; + int fullStrokeCount = 0; + + auto isInCorrectState = [](OSSM *ossm) { + // Add any states that you want to support here. + return ossm->sm->is("simplePenetration"_s) || + ossm->sm->is("simplePenetration.idle"_s); + }; + + double lastSpeed = 0; + + while (isInCorrectState(ossm)) { + auto speed = + Config::Driver::maxSpeedMmPerSecond * ossm->speedPercentage / 100.0; + auto acceleration = Config::Driver::maxSpeedMmPerSecond * + ossm->speedPercentage * ossm->speedPercentage / + Config::Advanced::accelerationScaling; + + // If the speed is greater than the dead-zone, and the speed has changed + // by more than the dead-zone, then update the stepper. + // This must be done in the same task that the stepper is running in. + if (ossm->speedPercentage > + Config::Advanced::commandDeadZonePercentage && + abs(speed - lastSpeed) > + Config::Advanced::commandDeadZonePercentage) { + lastSpeed = speed; + + ossm->stepper.setSpeedInMillimetersPerSecond(speed); + ossm->stepper.setAccelerationInMillimetersPerSecondPerSecond( + acceleration); + ossm->stepper.setDecelerationInMillimetersPerSecondPerSecond( + acceleration); + } + + if (ossm->stepper.getDistanceToTargetSigned() != 0) { + vTaskDelay(1); + // more than zero + continue; + } + + ossm->isForward = !ossm->isForward; + + if (ossm->speedPercentage > + Config::Advanced::commandDeadZonePercentage && + ossm->strokePercentage > + (long)Config::Advanced::commandDeadZonePercentage) { + fullStrokeCount++; + ossm->sessionStrokeCount = floor(fullStrokeCount / 2); + + // This calculation assumes that at the end of every stroke you have + // a whole positive distance, equal to maximum target position. + ossm->sessionDistanceMeters += + (0.002 * ((float)ossm->strokePercentage / 100.0) * + ossm->measuredStrokeMm); + } + + double targetPosition = + ossm->isForward ? -abs(((float)ossm->strokePercentage / 100.0) * + ossm->measuredStrokeMm) + : 0; + + if (ossm->speedPercentage < + Config::Advanced::commandDeadZonePercentage) { + targetPosition = 0; + } + + ESP_LOGD("SimplePenetration", "Moving stepper to position %ld", + static_cast(targetPosition)); + + ossm->stepper.setTargetPositionInMillimeters(targetPosition); + + // TODO: update life states. + // updateLifeStats(); + vTaskDelay(1); + } + vTaskDelete(nullptr); +} + +void OSSM::startSimplePenetration() { + xTaskCreatePinnedToCore(startSimplePenetrationTask, + "startSimplePenetrationTask", 2048, this, + configMAX_PRIORITIES - 1, &operationTask, 0); +} \ No newline at end of file diff --git a/Software/src/ossm/OSSM.StrokeEngine.cpp b/Software/src/ossm/OSSM.StrokeEngine.cpp new file mode 100644 index 00000000..d305aaa5 --- /dev/null +++ b/Software/src/ossm/OSSM.StrokeEngine.cpp @@ -0,0 +1,13 @@ +#include "OSSM.h" + +#include "constants/UserConfig.h" +#include "extensions/u8g2Extensions.h" + +void OSSM::startStrokeEngine() { + display.clearBuffer(); + + drawStr::title(UserConfig::language.StrokeEngine); + drawStr::multiLine(0, 40, UserConfig::language.InDevelopment); + + display.sendBuffer(); +} \ No newline at end of file diff --git a/Software/src/ossm/OSSM.Update.cpp b/Software/src/ossm/OSSM.Update.cpp new file mode 100644 index 00000000..210ef6c1 --- /dev/null +++ b/Software/src/ossm/OSSM.Update.cpp @@ -0,0 +1,28 @@ +#include "OSSM.h" + +#include "Arduino.h" +#include "extensions/u8g2Extensions.h" +void OSSM::drawUpdate() { + display.clearBuffer(); + String title = "Checking for update..."; + drawStr::title(title); + + // TODO - Add a spinner here + display.sendBuffer(); +} + +void OSSM::drawNoUpdate() { + display.clearBuffer(); + String title = "No Update Available"; + drawStr::title(title); + display.drawUTF8(0, 62, UserConfig::language.Skip.c_str()); + display.sendBuffer(); +} + +void OSSM::drawUpdating() { + display.clearBuffer(); + String title = "Updating OSSM..."; + drawStr::title(title); + drawStr::multiLine(0, 24, UserConfig::language.UpdateMessage); + display.sendBuffer(); +} diff --git a/Software/src/ossm/OSSM.WiFi.cpp b/Software/src/ossm/OSSM.WiFi.cpp new file mode 100644 index 00000000..5517c6fc --- /dev/null +++ b/Software/src/ossm/OSSM.WiFi.cpp @@ -0,0 +1,51 @@ + +#include "OSSM.h" + +#include "extensions/u8g2Extensions.h" +#include "qrcode.h" + +void OSSM::drawWiFi() { + display.clearBuffer(); + + static QRCode qrcode; + const int scale = 2; + // This Version of QR Codes can handle ~61 alphanumeric characters with ECC + // LEVEL M + + // NOLINTBEGIN(modernize-avoid-c-arrays) + uint8_t qrcodeData[qrcode_getBufferSize(3)]; + // NOLINTEND(modernize-avoid-c-arrays) + + String url = "WIFI:S:OSSM Setup;T:nopass;;"; + + qrcode_initText(&qrcode, qrcodeData, 3, 0, url.c_str()); + + int yOffset = constrain((64 - qrcode.size * scale) / 2, 0, 64); + int xOffset = constrain((128 - qrcode.size * scale), 0, 128); + + // Draw the QR code + for (uint8_t y = 0; y < qrcode.size; y++) { + for (uint8_t x = 0; x < qrcode.size; x++) { + if (qrcode_getModule(&qrcode, x, y)) { + display.drawBox(xOffset + x * scale, yOffset + y * scale, scale, + scale); + } + } + } + + display.setFont(Config::Font::bold); + display.drawUTF8(0, 10, UserConfig::language.WiFiSetup.c_str()); + // Draw line + display.drawHLine(0, 12, xOffset - 10); + + display.setFont(Config::Font::base); + display.drawUTF8(0, 26, UserConfig::language.WiFiSetupLine1.c_str()); + display.drawUTF8(0, 38, UserConfig::language.WiFiSetupLine2.c_str()); + display.drawUTF8(0, 62, UserConfig::language.Restart.c_str()); + display.sendBuffer(); + + wm.setConfigPortalTimeout(120); + wm.setDisableConfigPortal(false); + wm.setConfigPortalBlocking(false); + wm.startConfigPortal("OSSM Setup"); +} diff --git a/Software/src/ossm/OSSM.cpp b/Software/src/ossm/OSSM.cpp new file mode 100644 index 00000000..005767ea --- /dev/null +++ b/Software/src/ossm/OSSM.cpp @@ -0,0 +1,120 @@ +#include "OSSM.h" + +#include "constants/Images.h" +#include "constants/UserConfig.h" +#include "extensions/u8g2Extensions.h" +#include "services/encoder.h" +#include "services/stepper.h" + +namespace sml = boost::sml; +using namespace sml; + +// Now we can define the OSSM constructor since OSSMStateMachine::operator() is +// fully defined +OSSM::OSSM(U8G2_SSD1306_128X64_NONAME_F_HW_I2C &display, + AiEsp32RotaryEncoder &encoder) + : display(display), + encoder(encoder), + sm(std::make_unique< + sml::sm, + sml::logger>>(logger, *this)) { + initStepper(stepper); + + // All initializations are done, so start the state machine. + sm->process_event(Done{}); +} + +/** + * This task will write the word "OSSM" to the screen + * then briefly show the RD logo. + * and then end on the Kinky Makers logo. + * + * The Kinky Makers logo will stay on the screen until the next state is ready + * :). + * @param pvParameters + */ +void OSSM::drawHelloTask(void *pvParameters) { + // parse ossm from the parameters + OSSM *ossm = (OSSM *)pvParameters; + + int frameIdx = 0; + const int nFrames = 8; + + int startX = 24; + int offsetY = 12; + + // Bounce the Y position from 0 to 32, up to 24 and down to 32 + std::array framesY = {6, 12, 24, 48, 44, 42, 44, 48}; + std::array heights = {0, 0, 0, 0}; + int letterSpacing = 20; + + while (frameIdx < nFrames + 9) { + if (frameIdx < nFrames) { + heights[0] = framesY[frameIdx] - offsetY; + } + if (frameIdx - 3 > 0 && frameIdx - 3 < nFrames) { + heights[1] = framesY[frameIdx - 3] - offsetY; + } + if (frameIdx - 6 > 0 && frameIdx - 6 < nFrames) { + heights[2] = framesY[frameIdx - 6] - offsetY; + } + if (frameIdx - 9 > 0 && frameIdx - 9 < nFrames) { + heights[3] = framesY[frameIdx - 9] - offsetY; + } + // increment the frame index + frameIdx++; + + ossm->display.clearBuffer(); + ossm->display.setFont(u8g2_font_maniac_tf); + ossm->display.drawUTF8(startX, heights[0], "O"); + ossm->display.drawUTF8(startX + letterSpacing, heights[1], "S"); + ossm->display.drawUTF8(startX + letterSpacing * 2, heights[2], "S"); + ossm->display.drawUTF8(startX + letterSpacing * 3, heights[3], "M"); + ossm->display.sendBuffer(); + // Saying hi to the watchdog :). + vTaskDelay(1); + }; + + // Delay for a second, then show the RDLogo. + vTaskDelay(1500); + ossm->display.clearBuffer(); + drawStr::title("Research and Desire"); + ossm->display.drawXBMP(35, 14, 57, 50, Images::RDLogo); + ossm->display.sendBuffer(); + + vTaskDelay(1000); + + ossm->display.clearBuffer(); + drawStr::title("Kinky Makers"); + ossm->display.drawXBMP(40, 14, 50, 50, Images::KMLogo); + ossm->display.sendBuffer(); + + vTaskDelay(1000); + + ossm->display.clearBuffer(); + drawStr::title(UserConfig::language.MeasuringStroke); + ossm->display.drawXBMP(40, 14, 50, 50, Images::KMLogo); + ossm->display.sendBuffer(); + + // delete the task + vTaskDelete(nullptr); +} + +void OSSM::drawHello() { + xTaskCreatePinnedToCore(drawHelloTask, "drawHello", 10000, this, 1, + &displayTask, 0); +} + +void OSSM::drawError() { + // Throw the e-break on the stepper + try { + stepper.emergencyStop(); + } catch (const std::exception &e) { + ESP_LOGD("OSSM::drawError", "Caught exception: %s", e.what()); + } + + display.clearBuffer(); + drawStr::title(UserConfig::language.Error); + drawStr::multiLine(0, 20, errorMessage); + display.sendBuffer(); +} diff --git a/Software/src/ossm/OSSM.h b/Software/src/ossm/OSSM.h new file mode 100644 index 00000000..e42b051b --- /dev/null +++ b/Software/src/ossm/OSSM.h @@ -0,0 +1,253 @@ +#ifndef OSSM_SOFTWARE_OSSM_H +#define OSSM_SOFTWARE_OSSM_H + +#include + +#include "Actions.h" +#include "AiEsp32RotaryEncoder.h" +#include "ESP_FlexyStepper.h" +#include "Events.h" +#include "Guard.h" +#include "U8g2lib.h" +#include "WiFiManager.h" +#include "boost/sml.hpp" +#include "constants/Menu.h" +#include "services/tasks.h" +#include "utils/RecusiveMutex.h" +#include "utils/StateLogger.h" +#include "utils/update.h" + +namespace sml = boost::sml; + +class OSSM { + private: + void drawUpdate(); + /** + * /////////////////////////////////////////// + * //// + * //// OSSM State Machine + * //// + * /////////////////////////////////////////// + * + * This is the state machine that controls the OSSM. + * It's based on the Boost SML library, and you don't need to be an expert + *to use it, just follow the examples in this table: + * + * https://boost-ext.github.io/sml/tutorial.html#:~:text=3.%20Create%20a%20transition%20table + * + * Here are the basic rules: + * 1. Each row in the table is a transition. + * 2. Each transition has a source state, an event, a guard, an action, and + * a target state. + * 3. The source state is the state that the machine must be in for the + * transition to be valid. + * 4. (optional) The event is the event that triggers the transition. + * 5. (optional) The guard is a function that returns true or false. If it + * returns true, then the transition is valid. + * 6. (optional) The action is a function that is called when the + *transition is triggered, it can't block the main thread and cannot return + *a value. + * 7. The target state is the state that the machine will be in after the + * transition is complete. + * + * Have fun! + */ + struct OSSMStateMachine { + auto operator()() const { + // Action definitions to make the table easier to read. + auto drawHello = [](OSSM &o) { o.drawHello(); }; + auto drawMenu = [](OSSM &o) { o.drawMenu(); }; + auto startHoming = [](OSSM &o) { + o.clearHoming(); + o.startHoming(); + }; + auto reverseHoming = [](OSSM &o) { + o.isForward = false; + o.startHoming(); + }; + auto drawPlayControls = [](OSSM &o) { o.drawPlayControls(); }; + auto startSimplePenetration = [](OSSM &o) { + o.startSimplePenetration(); + }; + auto startStrokeEngine = [](OSSM &o) { o.startStrokeEngine(); }; + auto emergencyStop = [](OSSM &o) { o.stepper.emergencyStop(); }; + auto drawHelp = [](OSSM &o) { o.drawHelp(); }; + auto drawWiFi = [](OSSM &o) { o.drawWiFi(); }; + auto drawUpdate = [](OSSM &o) { o.drawUpdate(); }; + auto drawNoUpdate = [](OSSM &o) { o.drawNoUpdate(); }; + auto drawUpdating = [](OSSM &o) { o.drawUpdating(); }; + auto stopWifiPortal = [](OSSM &o) { o.wm.stopConfigPortal(); }; + auto drawError = [](OSSM &o) { o.drawError(); }; + + auto startWifi = [](OSSM &o) { + if (WiFiClass::status() == WL_CONNECTED) { + return; + } + // Start the wifi task. + + // If you have saved wifi credentials then connect to wifi + // immediately. + o.wm.setConfigPortalTimeout(1); + o.wm.setConnectTimeout(1); + o.wm.setConnectRetries(1); + o.wm.setConfigPortalBlocking(false); + if (!o.wm.autoConnect()) { + ESP_LOGD("UTILS", "failed to connect and hit timeout"); + } + ESP_LOGD("UTILS", "exiting autoconnect"); + }; + + // Guard definitions to make the table easier to read. + auto isStrokeTooShort = [](OSSM &o) { + return o.isStrokeTooShort(); + }; + + auto isOption = [](Menu option) { + return [option](OSSM &o) { return o.menuOption == option; }; + }; + + return make_transition_table( + // clang-format off + *"idle"_s + done / drawHello = "homing"_s, + + "homing"_s / startHoming = "homing.idle"_s, + "homing.idle"_s + error = "error"_s, + "homing.idle"_s + done / reverseHoming = "homing.backward"_s, + "homing.backward"_s + error = "error"_s, + "homing.backward"_s + done[(isStrokeTooShort)] = "error"_s, + "homing.backward"_s + done = "menu"_s, + + "menu"_s / (drawMenu, startWifi) = "menu.idle"_s, + "menu.idle"_s + buttonPress[(isOption(Menu::SimplePenetration))] = "simplePenetration"_s, + "menu.idle"_s + buttonPress[(isOption(Menu::StrokeEngine))] = "strokeEngine"_s, + "menu.idle"_s + buttonPress[(isOption(Menu::UpdateOSSM))] = "update"_s, + "menu.idle"_s + buttonPress[(isOption(Menu::WiFiSetup))] = "wifi"_s, + "menu.idle"_s + buttonPress[isOption(Menu::Help)] = "help"_s, + "menu.idle"_s + buttonPress[(isOption(Menu::Restart))] = "restart"_s, + + "simplePenetration"_s / drawPlayControls = "simplePenetration.preflight"_s, + "simplePenetration.preflight"_s + done / startSimplePenetration = "simplePenetration.idle"_s, + "simplePenetration.idle"_s + doublePress / emergencyStop = "menu"_s, + + "strokeEngine"_s + on_entry<_> / startStrokeEngine, + "strokeEngine"_s + buttonPress = "menu"_s, + + "update"_s [isOnline] / drawUpdate = "update.checking"_s, + "update"_s = "wifi"_s, + "update.checking"_s [isUpdateAvailable] / (drawUpdating, updateOSSM) = "update.updating"_s, + "update.checking"_s / drawNoUpdate = "update.idle"_s, + "update.idle"_s + buttonPress = "menu"_s, + "update.updating"_s = X, + + "wifi"_s / drawWiFi = "wifi.idle"_s, + "wifi.idle"_s + buttonPress / stopWifiPortal = "menu"_s, + + "help"_s / drawHelp = "help.idle"_s, + "help.idle"_s + buttonPress = "menu"_s, + + "error"_s / drawError = "error.idle"_s, + "error.idle"_s + buttonPress / drawHelp = "error.help"_s, + "error.help"_s + buttonPress / restart = X, + + "restart"_s / restart = X); + // clang-format on + } + }; + + /** + * /////////////////////////////////////////// + * //// + * //// Private Objects and Services + * //// + * /////////////////////////////////////////// + */ + ESP_FlexyStepper stepper; + U8G2_SSD1306_128X64_NONAME_F_HW_I2C &display; + StateLogger logger; + AiEsp32RotaryEncoder &encoder; + + /** + * /////////////////////////////////////////// + * //// + * //// Private Variables and Flags + * //// + * /////////////////////////////////////////// + */ + // Calibration Variables + float currentSensorOffset = 0; + float measuredStrokeMm = 0; + + // Homing Variables + bool isForward = true; + + Menu menuOption = Menu::SimplePenetration; + String errorMessage = ""; + + // Session Variables + float speedPercentage = 0; + long strokePercentage = 0; + + unsigned long sessionStartTime = 0; + int sessionStrokeCount = 0; + double sessionDistanceMeters = 0; + + /** + * /////////////////////////////////////////// + * //// + * //// Private Functions + * //// + * /////////////////////////////////////////// + */ + void clearHoming(); + + void startHoming(); + + void startSimplePenetration(); + + bool isStrokeTooShort(); + + void drawError(); + + void drawHello(); + + void drawHelp(); + + void drawWiFi(); + + void drawMenu(); + + void drawPlayControls(); + + /** + * /////////////////////////////////////////// + * //// + * //// Static Functions and Tasks + * //// + * /////////////////////////////////////////// + */ + static void startHomingTask(void *pvParameters); + + static void drawHelloTask(void *pvParameters); + + static void drawMenuTask(void *pvParameters); + + static void drawPlayControlsTask(void *pvParameters); + + static void startSimplePenetrationTask(void *pvParameters); + + public: + explicit OSSM(U8G2_SSD1306_128X64_NONAME_F_HW_I2C &display, + AiEsp32RotaryEncoder &rotaryEncoder); + + std::unique_ptr< + sml::sm, + sml::logger>> + sm = nullptr; // The state machine + + WiFiManager wm; + void startStrokeEngine(); + void drawNoUpdate(); + void drawUpdating(); +}; + +#endif // OSSM_SOFTWARE_OSSM_H diff --git a/Software/src/services/board.h b/Software/src/services/board.h new file mode 100644 index 00000000..82d563dd --- /dev/null +++ b/Software/src/services/board.h @@ -0,0 +1,30 @@ +#ifndef OSSM_SOFTWARE_BOARD_H +#define OSSM_SOFTWARE_BOARD_H + +#include + +#include "constants/Pins.h" + +/** + * This file changes the configuration of the board. + */ +void initBoard() { + Serial.begin(115200); + + pinMode(Pins::Remote::encoderSwitch, + INPUT_PULLDOWN); // Rotary Encoder Pushbutton + + pinMode(Pins::Driver::motorEnablePin, OUTPUT); + pinMode(Pins::Wifi::resetPin, INPUT_PULLDOWN); + // TODO: Remove wifi toggle pin + // pinMode(Pins::Wifi::controlTogglePin, LOCAL_CONTROLLER); // choose + // between WIFI_CONTROLLER and LOCAL_CONTROLLER + // Set analog pots (control knobs) + pinMode(Pins::Remote::speedPotPin, INPUT); + adcAttachPin(Pins::Remote::speedPotPin); + + analogReadResolution(12); + analogSetAttenuation(ADC_11db); // allows us to read almost full 3.3V range +} + +#endif // OSSM_SOFTWARE_BOARD_H diff --git a/Software/src/services/display.h b/Software/src/services/display.h index 9287440f..708132cf 100644 --- a/Software/src/services/display.h +++ b/Software/src/services/display.h @@ -2,7 +2,7 @@ #define OSSM_SOFTWARE_DISPLAY_H #include -#include "OSSM_PinDef.h" +#include "constants/Pins.h" #pragma once /** @@ -17,7 +17,9 @@ * * https://github.com/olikraus/u8g2/wiki/fntlistallplain */ -static U8G2_SSD1306_128X64_NONAME_F_HW_I2C display(U8G2_R0, -1, REMOTE_CLK, - REMOTE_SDA); +static U8G2_SSD1306_128X64_NONAME_F_HW_I2C display(U8G2_R0, + Pins::Display::oledReset, + Pins::Remote::displayClock, + Pins::Remote::displayData); #endif // OSSM_SOFTWARE_DISPLAY_H diff --git a/Software/src/services/encoder.h b/Software/src/services/encoder.h new file mode 100644 index 00000000..b7faff9a --- /dev/null +++ b/Software/src/services/encoder.h @@ -0,0 +1,28 @@ +#ifndef OSSM_SOFTWARE_ENCODER_H +#define OSSM_SOFTWARE_ENCODER_H + +#include "AiEsp32RotaryEncoder.h" +#include "constants/Pins.h" + +static AiEsp32RotaryEncoder encoder = AiEsp32RotaryEncoder( + Pins::Remote::encoderA, Pins::Remote::encoderB, Pins::Remote::encoderSwitch, + Pins::Remote::encoderPower, Pins::Remote::encoderStepsPerNotch); + +static void IRAM_ATTR readEncoderISR() { encoder.readEncoder_ISR(); } + +static void initEncoder() { + // we must initialize rotary encoder + encoder.begin(); + encoder.setup(readEncoderISR); + // set boundaries and if values should cycle or not + // in this example we will set possible values between 0 and 1000; + encoder.setBoundaries( + 0, 99, false); // minValue, maxValue, circleValues true|false (when max + // go to min and vice versa) + encoder.setAcceleration(0); + + // really disabled acceleration + encoder.disableAcceleration(); +} + +#endif // OSSM_SOFTWARE_ENCODER_H diff --git a/Software/src/services/stepper.h b/Software/src/services/stepper.h new file mode 100644 index 00000000..448d93a8 --- /dev/null +++ b/Software/src/services/stepper.h @@ -0,0 +1,33 @@ +#ifndef OSSM_SOFTWARE_STEPPER_H +#define OSSM_SOFTWARE_STEPPER_H + +#include + +#include "constants/Config.h" +#include "constants/Pins.h" + +/** + * Here are all the initialization steps for the flexyStepper motor. + * + * It is useful the call this function again if the motor is in an unknown + * state. + * + * @param flexyStepper + */ +static void initStepper(ESP_FlexyStepper &flexyStepper) { + flexyStepper.connectToPins(Pins::Driver::motorStepPin, + Pins::Driver::motorDirectionPin); + flexyStepper.setLimitSwitchActive(Pins::Driver::limitSwitchPin); + + float stepsPerMm = + Config::Driver::motorStepPerRevolution / + (Config::Driver::pulleyToothCount * Config::Driver::beltPitchMm); + + flexyStepper.setStepsPerMillimeter(stepsPerMm); + + // This stepper must start on core 1, otherwise it may cause jitter. + // https://github.com/pkerspe/ESP-FlexyStepper#a-view-words-on-jitter-in-the-generated-step-signals + flexyStepper.startAsService(1); +} + +#endif // OSSM_SOFTWARE_STEPPER_H diff --git a/Software/src/services/tasks.h b/Software/src/services/tasks.h new file mode 100644 index 00000000..07319f9a --- /dev/null +++ b/Software/src/services/tasks.h @@ -0,0 +1,17 @@ +#ifndef OSSM_SOFTWARE_TASKS_H +#define OSSM_SOFTWARE_TASKS_H + +#include + +/** + * /////////////////////////////////////////// + * //// + * //// Private Objects and Services + * //// + * /////////////////////////////////////////// + */ + +static TaskHandle_t displayTask = nullptr; +static TaskHandle_t operationTask = nullptr; + +#endif // OSSM_SOFTWARE_TASKS_H diff --git a/Software/src/state/actions.h b/Software/src/state/actions.h deleted file mode 100644 index 2f2b478b..00000000 --- a/Software/src/state/actions.h +++ /dev/null @@ -1,40 +0,0 @@ -/** - * @file actions.h - * @brief Defines actions for the OSSM state machine. - * - * This file contains the definitions of actions used in the OSSM state machine. - * Actions are operations that the state machine performs when certain - * transitions occur. - * - * The actions in this file are used in the transition table of the - * OSSMStateMachine class, which is defined in state.h. They are executed when - * certain events occur and the associated transitions are triggered. - * - * For example, the `drawHello` action might display a welcome message, the - * `drawError` action might display an error message, and the `drawGetHelp` - * action might display a help message. - * - * These actions are used in conjunction with the Boost SML library, which is a - * lightweight, header-only state machine library for C++. - */ - -#ifndef SOFTWARE_ACTIONS_H -#define SOFTWARE_ACTIONS_H - -#pragma once - -class OSSM; // Forward declaration of class OSSM - -// Action definitions -auto initDevice = [](OSSM &o) { - o.setup(); - o.findHome(); -}; - -auto drawHello = [](OSSM &o) {}; -auto drawError = []() {}; -auto drawGetHelp = []() {}; -auto drawPreflight = []() {}; -auto drawPlayControls = []() {}; - -#endif // SOFTWARE_ACTIONS_H diff --git a/Software/src/state/events.h b/Software/src/state/events.h deleted file mode 100644 index 1dbcaec1..00000000 --- a/Software/src/state/events.h +++ /dev/null @@ -1,40 +0,0 @@ -/** - * @file events.h - * @brief Defines events for the OSSM state machine. - * - * This file contains the definitions of events used in the OSSM state machine. - * Events are instances of certain occurrences or changes in the system that can - * trigger state transitions in the state machine. - * - * The events in this file are used in the transition table of the - * OSSMStateMachine class, which is defined in state.h. They are used to trigger - * transitions between different states of the state machine. - * - * For example, the `ButtonPress` event represents a button press action, and - * the `LongPress` event represents a long press action. The `Done` event - * signifies the completion of a task, and the `Error` event signifies an error - * occurrence. - * - * These events are used in conjunction with the Boost SML library, which is a - * lightweight, header-only state machine library for C++. - */ - -#ifndef SOFTWARE_EVENTS_H -#define SOFTWARE_EVENTS_H - -#include "boost/sml.hpp" -namespace sml = boost::sml; -using namespace sml; - -struct ButtonPress {}; -struct LongPress {}; -struct Done {}; -struct Error {}; - -// Definitions to make the table easier to read. -static auto buttonPress = sml::event; -static auto longPress = sml::event; -static auto done = sml::event; -static auto error = sml::event; - -#endif // SOFTWARE_EVENTS_H diff --git a/Software/src/state/guards.h b/Software/src/state/guards.h deleted file mode 100644 index ab2f49b2..00000000 --- a/Software/src/state/guards.h +++ /dev/null @@ -1,38 +0,0 @@ -/** - * @file guards.h - * @brief Defines guard conditions for the OSSM state machine. - * - * This file contains the definitions of guard conditions used in the OSSM state - * machine. Guard conditions are predicates that determine whether a transition - * should be taken in response to an event. They are defined as functions or - * functors that take the necessary parameters and return a boolean value. - * - * The guards in this file are used in the transition table of the - * OSSMStateMachine class, which is defined in state.h. They are used to add - * conditional logic to the state machine's transitions. - * - * For example, the `isPreflightSafe` guard checks if the speed percentage is - * zero, and the `isOption` guard checks if a certain menu option is selected. - * - * These guards are used in conjunction with the Boost SML library, which is a - * lightweight, header-only state machine library for C++. - */ - -#ifndef SOFTWARE_GUARDS_H -#define SOFTWARE_GUARDS_H - -#pragma once - -class OSSM; // Forward declaration of class OSSM - -// Guard definitions -auto isPreflightSafe = [](OSSM &o) { - // TODO: Implement this - return o.speedPercentage == 0; -}; -auto isOption = [](Menu option) { - // TODO: Implement this - return [option]() { return option && true; }; -}; - -#endif // SOFTWARE_GUARDS_H diff --git a/Software/src/state/state.h b/Software/src/state/state.h deleted file mode 100644 index f3e97298..00000000 --- a/Software/src/state/state.h +++ /dev/null @@ -1,86 +0,0 @@ -#ifndef SOFTWARE_STATE_H -#define SOFTWARE_STATE_H - -#include "Utilities.h" -#include "actions.h" -#include "boost/sml.hpp" -#include "constants/Menu.h" -#include "events.h" -#include "guards.h" -#include "utils/RecusiveMutex.h" -#include "utils/StateLogger.h" -namespace sml = boost::sml; -using namespace sml; - -/** - * @brief Defines the state machine for the OSSM project. - * - * The OSSMStateMachine class uses the Boost SML library to define a state - * machine for managing state transitions. It is designed to be thread-safe, so - * events can be processed in any thread or task. - * - * Here are the basics of boost SML: - * - * 1. Each row in the table is a transition. - * 2. Each transition has a source state, an event, a guard, an action, and a - * target state. ex: "idle"_s + done / drawHello = "homing"_s, - * - In this example, the source state is "idle", the event is "done", the - * guard is "none", the action is "drawHello", and the target state is "homing". - * 3. The source state is the state that the machine must be in for the - * transition to be valid. - * 4. (optional) The event is the event that triggers the transition. - * 5. (optional) The guard is a function that returns true or false. If it - * returns true, then the transition is valid. - * 6. (optional) The action is a function that is called when the transition is - * triggered, it can't block the main thread and cannot return a value. - * 7. The target state is the state that the machine will be in after the - * transition is complete. - * - * For more information, see the Boost SML documentation: - * https://boost-ext.github.io/sml/index.html - */ -class OSSMStateMachine { - public: - auto operator()() const { - return make_transition_table( - // clang-format off - *"idle"_s + done / drawHello = "homing"_s, - - "homing"_s + on_entry<_> / initDevice, - "homing"_s + done = "menu"_s, - "homing"_s + error = "error"_s, - - "menu"_s + buttonPress[(isOption(Menu::SimplePenetration))] = "simplePenetration"_s, - "menu"_s + buttonPress[isOption(Menu::StrokeEngine)] = "strokeEngine"_s, - "menu"_s + buttonPress[isOption(Menu::Help)] = "help"_s, - "menu"_s + buttonPress[isOption(Menu::Restart)] = "restart"_s, - - "simplePenetration"_s + on_entry<_> / drawPreflight, - "simplePenetration"_s + done[isPreflightSafe] = "simplePenetration.play"_s, - "simplePenetration"_s + longPress = "menu"_s, - "simplePenetration"_s + error = "error"_s, - "simplePenetration.play"_s + on_entry<_> / drawPlayControls, - "simplePenetration.play"_s + error = "error"_s, - "simplePenetration.play"_s + longPress = "menu"_s, - - "strokeEngine"_s + on_entry<_> / drawPreflight, - "strokeEngine"_s + done[isPreflightSafe] = "strokeEngine.play"_s, - "strokeEngine"_s + longPress = "menu"_s, - "strokeEngine"_s + error = "error"_s, - "strokeEngine.play"_s + on_entry<_> / drawPlayControls, - "strokeEngine.play"_s + error = "error"_s, - "strokeEngine.play"_s + longPress = "menu"_s, - - "help"_s + on_entry<_> / drawGetHelp, - "help"_s + buttonPress = "menu"_s, - "help"_s + longPress = "menu"_s, - - "error"_s + on_entry<_> / drawError, - "error"_s + longPress / drawGetHelp = X, - "error"_s + buttonPress / drawGetHelp = X - ); - // clang-format on - } -}; - -#endif // SOFTWARE_STATE_H diff --git a/Software/src/state/type.h b/Software/src/state/type.h deleted file mode 100644 index 52b2ae8f..00000000 --- a/Software/src/state/type.h +++ /dev/null @@ -1,11 +0,0 @@ -#ifndef SOFTWARE_TYPE_H -#define SOFTWARE_TYPE_H -#pragma once -#include "utils/RecusiveMutex.h" -#include "utils/StateLogger.h" -class OSSMStateMachine; -using OSSMState = - sml::sm, - sml::logger>; - -#endif // SOFTWARE_TYPE_H diff --git a/Software/src/structs/LanguageStruct.h b/Software/src/structs/LanguageStruct.h new file mode 100644 index 00000000..9eda8adc --- /dev/null +++ b/Software/src/structs/LanguageStruct.h @@ -0,0 +1,36 @@ +#ifndef OSSM_SOFTWARE_LANGUAGESTRUCT_H +#define OSSM_SOFTWARE_LANGUAGESTRUCT_H + +struct LanguageStruct { + String DeepThroatTrainerSync; + String Error; + String GetHelp; + String GetHelpLine1; + String GetHelpLine2; + String Homing; + String HomingTookTooLong; + String Idle; + String InDevelopment; + String MeasuringStroke; + String NoInternalLoop; + String Restart; + String Settings; + String SimplePenetration; + String Skip; + String Speed; + String SpeedWarning; + String StateNotImplemented; + String Stroke; + String StrokeEngine; + String StrokeTooShort; + String Update; + String UpdateMessage; + String WiFi; + String WiFiSetup; + String WiFiSetupLine1; + String WiFiSetupLine2; + String YouShouldNotBeHere; + +}; + +#endif // OSSM_SOFTWARE_LANGUAGESTRUCT_H diff --git a/Software/src/utils/analog.h b/Software/src/utils/analog.h new file mode 100644 index 00000000..69d22e41 --- /dev/null +++ b/Software/src/utils/analog.h @@ -0,0 +1,27 @@ +#ifndef OSSM_SOFTWARE_ANALOG_H +#define OSSM_SOFTWARE_ANALOG_H + +#include "Arduino.h" + +typedef struct { + int pinNumber; + int samples; +} SampleOnPin; + +// public static function to get the analog value of a pin +static float getAnalogAveragePercent(SampleOnPin sampleOnPin) { + int sum = 0; + float average; + float percentage; + + for (int i = 0; i < sampleOnPin.samples; i++) { + // TODO: Possibly use fancier filters? + sum += analogRead(sampleOnPin.pinNumber); + } + average = (float)sum / (float)sampleOnPin.samples; + // TODO: Might want to add a dead-band + percentage = 100.0f * average / 4096.0f; // 12 bit resolution + return percentage; +} + +#endif // OSSM_SOFTWARE_ANALOG_H diff --git a/Software/src/utils/format.h b/Software/src/utils/format.h new file mode 100644 index 00000000..5db5cb0e --- /dev/null +++ b/Software/src/utils/format.h @@ -0,0 +1,98 @@ +#ifndef OSSM_SOFTWARE_FORMAT_H +#define OSSM_SOFTWARE_FORMAT_H + +#include + +#include "constants/UserConfig.h" + +String formatTime(unsigned int totalSeconds) { + String formattedTime = ""; + + // Calculate time components + unsigned int days = totalSeconds / 86400; + unsigned int hours = (totalSeconds % 86400) / 3600; + unsigned int minutes = (totalSeconds % 3600) / 60; + unsigned int seconds = totalSeconds % 60; + + // Construct formatted time string + if (days > 0) { + formattedTime += String(days) + "d "; + } + + if (hours > 0 || (days > 0 && (minutes > 0 || seconds > 0))) { + if (hours > 0 || days > 0) { + formattedTime += String(hours) + "h "; + } + } + + // Format minutes and seconds + if (days == 0 && hours == 0) { + // Format as MM:SS when there are no hours and days + formattedTime += (minutes < 10 ? "0" : "") + String(minutes) + ":"; + formattedTime += (seconds < 10 ? "0" : "") + String(seconds); + } else if (minutes > 0 || seconds > 0) { + // Otherwise, just append minutes and seconds normally + formattedTime += String(minutes) + ":"; + formattedTime += (seconds < 10 ? "0" : "") + String(seconds); + } + + // Handle the special case of only seconds less than 60 in the total time + if (totalSeconds < 60) { + formattedTime = String(seconds) + "s"; + } + + // Remove any trailing spaces + if (formattedTime.endsWith(" ")) { + formattedTime = formattedTime.substring(0, formattedTime.length() - 1); + } + + return formattedTime; +} + +String formatImperial(double meters) { + // Convert meters to feet + float feet = meters * 3.28084f; + + if (feet < 1.0f) { + // Convert feet to inches and format + int inches = roundf(feet * 12); + return String(inches) + " in"; + } else if (feet < 5280.0f) { // Less than a mile + // Format to no decimal places for feet if less than a mile + return String(roundf(feet)) + " ft"; + } else { + // Convert feet to miles and format + return String(feet / 5280.0f, 2) + " mi"; + } +} + +String formatMetric(double meters) { + String sign = meters >= 0 ? "" : "-"; + meters = abs(meters); + + if (meters == 0) { + return "0.0 cm"; + } else if (meters < 1.0f) { + // Convert to centimeters and format + return sign + String(meters * 100.0f, 1) + " cm"; + } else if (meters < 100.0f) { + // Keep as meters, but format + return sign + String(meters, 1) + " m"; + } else if (meters < 1000.0f) { + // Round to nearest meter + return sign + String(roundf(meters)) + " m"; + } else { + // Convert to kilometers and format + return sign + String(meters / 1000.0f, 2) + " km"; + } +} + +String formatDistance(double meters) { + if (UserConfig::displayMetric) { + return formatMetric(meters); + } else { + return formatImperial(meters); + } +} + +#endif // OSSM_SOFTWARE_FORMAT_H diff --git a/Software/src/utils/stringMethods.h b/Software/src/utils/stringMethods.h deleted file mode 100644 index 31d9386a..00000000 --- a/Software/src/utils/stringMethods.h +++ /dev/null @@ -1,50 +0,0 @@ -#ifndef DT_TRAINER_STRINGMETHODS_H -#define DT_TRAINER_STRINGMETHODS_H - -#include - -static bool isStringEmpty(const String &str) { - for (unsigned int i = 0; i < str.length(); i++) { - // Check if the character is not a whitespace character or newline or - // return or form feed - if (!isWhitespace(str.charAt(i)) && str.charAt(i) != '\n' && - str.charAt(i) != '\r' && str.charAt(i) != '\f' && - str.charAt(i) != '\v') { - return false; // Found a non-whitespace character - } - } - return true; // Only whitespace characters found -} - -static String wrapText(const String &input) { - String output; - int lineLength = 0; - - for (unsigned int i = 0; i < input.length(); ++i) { - output += input[i]; - lineLength++; - - if (lineLength == 12) { - if (i + 1 < input.length() && input[i + 1] == ' ') { - output += '\n'; - i++; // Skip the space as we've replaced it with a newline - } else { - // Find the nearest previous space to replace with a newline - int spaceIndex = output.lastIndexOf(' ', output.length() - 1); - if (spaceIndex != -1 && spaceIndex >= output.length() - 12) { - output.setCharAt(spaceIndex, '\n'); - lineLength = output.length() - spaceIndex - 1; - } else { - // No space found or the word is too long, insert a newline - // directly - output += '\n'; - } - } - lineLength = 0; - } - } - - return output; -} - -#endif // DT_TRAINER_STRINGMETHODS_H diff --git a/Software/src/utils/update.h b/Software/src/utils/update.h new file mode 100644 index 00000000..964581d4 --- /dev/null +++ b/Software/src/utils/update.h @@ -0,0 +1,101 @@ +#ifndef SOFTWARE_UPDATE_H +#define SOFTWARE_UPDATE_H + +#include +#include +#include + +#include "ArduinoJson.h" +#include "constants/LogTags.h" + +#ifndef SW_VERSION +#define SW_VERSION "0.0.0" +#endif + +static auto isUpdateAvailable = []() { + // check if we're online + if (WiFiClass::status() != WL_CONNECTED) { + ESP_LOGD(UPDATE_TAG, "Not connected to WiFi"); + return false; + } + + String serverNameBubble = + "http://d2g4f7zewm360.cloudfront.net/check-for-ossm-update"; // live + // url +#ifdef VERSIONDEV + serverNameBubble = + "http://d2oq8yqnezqh3r.cloudfront.net/check-for-ossm-update"; // version-test +#endif + +#ifdef VERSIONSTAGING + serverNameBubble = + "http://d2oq8yqnezqh3r.cloudfront.net/check-for-ossm-update"; // version-test +#endif + + ESP_LOGD(UPDATE_TAG, "Checking for updates at %s", + serverNameBubble.c_str()); + + // Making the POST request to the bubble server + HTTPClient http; + WiFiClient client; + http.begin(client, serverNameBubble); + http.addHeader("Content-Type", "application/json"); + StaticJsonDocument<200> doc; + // Add values in the document + doc["ossmSwVersion"] = SW_VERSION; + String requestBody; + serializeJson(doc, requestBody); + int httpResponseCode = http.POST(requestBody); + + // Reading payload + String payload = "{}"; + payload = http.getString(); + ESP_LOGD(UPDATE_TAG, "HTTP Response code: %d", httpResponseCode); + StaticJsonDocument<200> bubbleResponse; + deserializeJson(bubbleResponse, payload); + bool response_needUpdate = bubbleResponse["response"]["needUpdate"]; + + ESP_LOGD("UTILS", "Payload: %s", payload.c_str()); + + if (httpResponseCode <= 0) { + ESP_LOGD("UTILS", "Failed to reach update server"); + } + http.end(); + client.stop(); + return response_needUpdate; +}; + +auto updateOSSM = []() { + // check if we're online + + WiFiClient client; + String url = "http://d2sy3zdr3r1gt5.cloudfront.net/firmware.bin"; + +#ifdef VERSIONDEV + url = "http://d2sy3zdr3r1gt5.cloudfront.net/firmware-dev.bin"; +#endif +#ifdef VERSIONSTAGING + url = "http://d2sy3zdr3r1gt5.cloudfront.net/firmware-dev.bin"; +#endif + + t_httpUpdate_return ret = httpUpdate.update( + client, url); + + switch (ret) { + case HTTP_UPDATE_FAILED: + ESP_LOGD("UTILS", "HTTP_UPDATE_FAILED Error (%d): %s\n", + httpUpdate.getLastError(), + httpUpdate.getLastErrorString().c_str()); + break; + + case HTTP_UPDATE_NO_UPDATES: + ESP_LOGD("UTILS", "HTTP_UPDATE_NO_UPDATES"); + break; + + case HTTP_UPDATE_OK: + ESP_LOGD("UTILS", "HTTP_UPDATE_OK"); + break; + } +}; + +#endif // SOFTWARE_UPDATE_H diff --git a/Software/src/workspace.code-workspace b/Software/src/workspace.code-workspace deleted file mode 100644 index 22f16842..00000000 --- a/Software/src/workspace.code-workspace +++ /dev/null @@ -1,12 +0,0 @@ -{ - "folders": [ - { - "path": ".." - }, - { - "name": "OSSM Remote Testing", - "path": "../../../../../PlatformIO/Projects/OSSM Remote Testing" - } - ], - "settings": {} -} \ No newline at end of file diff --git a/Software/test/test_actions/MockOSSM.h b/Software/test/test_actions/MockOSSM.h deleted file mode 100644 index 15722726..00000000 --- a/Software/test/test_actions/MockOSSM.h +++ /dev/null @@ -1,19 +0,0 @@ -#ifndef SOFTWARE_MOCKOSSM_H -#define SOFTWARE_MOCKOSSM_H - -#ifndef MOCK_OSSM_H -#define MOCK_OSSM_H - -class OSSM { - public: - int setupCalled = 0; - int findHomeCalled = 0; - - void setup() { setupCalled++; } - - void findHome() { findHomeCalled++; } -}; - -#endif // MOCK_OSSM_H - -#endif // SOFTWARE_MOCKOSSM_H diff --git a/Software/test/test_actions/main.cpp b/Software/test/test_actions/main.cpp deleted file mode 100644 index 80e33006..00000000 --- a/Software/test/test_actions/main.cpp +++ /dev/null @@ -1,24 +0,0 @@ -#include "MockOSSM.h" -#include "state/actions.h" -#include "unity.h" - -void test_initDevice_calls_setup_and_homing(void) { - OSSM ossm; - initDevice(ossm); - - TEST_ASSERT_EQUAL(1, ossm.setupCalled); - TEST_ASSERT_EQUAL(1, ossm.findHomeCalled); -} - -int runUnityTests() { - UNITY_BEGIN(); - RUN_TEST(test_initDevice_calls_setup_and_homing); - return UNITY_END(); -} - -// WARNING!!! PLEASE REMOVE UNNECESSARY MAIN IMPLEMENTATIONS // - -/** - * For native dev-platform or for some embedded frameworks - */ -int main(void) { return runUnityTests(); } diff --git a/Software/test/test_measurements/main.cpp b/Software/test/test_measurements/main.cpp new file mode 100644 index 00000000..5a678534 --- /dev/null +++ b/Software/test/test_measurements/main.cpp @@ -0,0 +1,42 @@ +#include "mock.h" +#include "unity.h" +#include "utils/analog.h" + +// Test all readings zero +void test_allReadingsZero() { + prepareAnalogReadData({0, 0, 0, 0, 0}); // 5 samples, all zero + float result = getAnalogAveragePercent({0, 5}); + TEST_ASSERT_FLOAT_WITHIN(0.001, 0.0f, result); +} + +// Test all readings maximum for 12-bit ADC +void test_allReadingsMaximum() { + prepareAnalogReadData( + {4096, 4096, 4096, 4096, 4096}); // 5 samples, all at max + float result = getAnalogAveragePercent({0, 5}); + TEST_ASSERT_FLOAT_WITHIN(0.001, 100.0f, result); +} + +// Test mixed readings +void test_mixedReadings() { + prepareAnalogReadData({1024, 2048, 3072, 4096, 0}); // Mixed values + float result = getAnalogAveragePercent({0, 5}); + TEST_ASSERT_FLOAT_WITHIN( + 0.001, 50.0f, + result); // Expected: (1024+2048+3072+4096+0)/5 / 4096 * 100 +} + +int runUnityTests() { + UNITY_BEGIN(); + RUN_TEST(test_allReadingsZero); + RUN_TEST(test_allReadingsMaximum); + RUN_TEST(test_mixedReadings); + return UNITY_END(); +} + +// WARNING!!! PLEASE REMOVE UNNECESSARY MAIN IMPLEMENTATIONS // + +/** + * For native dev-platform or for some embedded frameworks + */ +int main(void) { return runUnityTests(); } diff --git a/Software/test/test_measurements/mock.h b/Software/test/test_measurements/mock.h new file mode 100644 index 00000000..c405a44c --- /dev/null +++ b/Software/test/test_measurements/mock.h @@ -0,0 +1,31 @@ +#ifndef SOFTWARE_HTTPCLIENT_H +#define SOFTWARE_MOCK_H + +#include + +#include + +// Mock data for analogRead +std::queue mockReadings; + +// Override the analogRead function +uint16_t analogRead(int pin) { + if (!mockReadings.empty()) { + int reading = mockReadings.front(); + mockReadings.pop(); + return reading; + } + return 0; // Default to 0 if no mock data is available +} + +// Function to add test data +void prepareAnalogReadData(const std::vector& readings) { + while (!mockReadings.empty()) { + mockReadings.pop(); // Clear existing data + } + for (int reading : readings) { + mockReadings.push(reading); + } +} + +#endif // SOFTWARE_HTTPCLIENT_H diff --git a/Software/test/test_strings/main.cpp b/Software/test/test_strings/main.cpp index ebbd71ac..a01517e5 100644 --- a/Software/test/test_strings/main.cpp +++ b/Software/test/test_strings/main.cpp @@ -1,73 +1,163 @@ #include "unity.h" -#include "utils/stringMethods.h" - -void test_isStringEmpty(void) { - // Testing with different kinds of empty strings - TEST_ASSERT_TRUE(isStringEmpty("")); - TEST_ASSERT_TRUE(isStringEmpty(" ")); - TEST_ASSERT_TRUE(isStringEmpty(" ")); // Multiple spaces - TEST_ASSERT_TRUE(isStringEmpty("\t")); // Tab character - TEST_ASSERT_TRUE(isStringEmpty(" \t ")); // Spaces and a tab - TEST_ASSERT_TRUE(isStringEmpty("\n")); // Newline character - TEST_ASSERT_TRUE(isStringEmpty(" \n ")); // Spaces and a newline - TEST_ASSERT_TRUE(isStringEmpty("\r\n")); // Carriage return and newline - TEST_ASSERT_TRUE(isStringEmpty("\r")); // Carriage return - TEST_ASSERT_TRUE(isStringEmpty("\f")); // Form feed - TEST_ASSERT_TRUE(isStringEmpty("\v")); // Vertical tab - - // Testing with strings containing non-whitespace characters - TEST_ASSERT_FALSE(isStringEmpty("a")); - TEST_ASSERT_FALSE(isStringEmpty("abc")); - TEST_ASSERT_FALSE( - isStringEmpty(" a")); // Spaces before a character - TEST_ASSERT_FALSE( - isStringEmpty("a ")); // Spaces after a character - TEST_ASSERT_FALSE(isStringEmpty(" a ")); // Spaces around a character - TEST_ASSERT_FALSE( - isStringEmpty(" a b ")); // Spaces around and inside a string - TEST_ASSERT_FALSE(isStringEmpty("\nabc\n")); // Newline around characters - TEST_ASSERT_FALSE(isStringEmpty("\tabc\t")); // Tab around characters -} - -void test_wrapText() { - // Basic checks - TEST_ASSERT_EQUAL_STRING("", wrapText("").c_str()); - TEST_ASSERT_EQUAL_STRING("01234567891", wrapText("01234567891").c_str()); - TEST_ASSERT_EQUAL_STRING("012345678912\n", - wrapText("012345678912").c_str()); +#include "utils/format.h" + +void test_ZeroSeconds(void) { + TEST_ASSERT_EQUAL_STRING("0s", formatTime(0).c_str()); +} + +void test_SingleMinute(void) { + TEST_ASSERT_EQUAL_STRING("01:00", formatTime(60).c_str()); +} + +void test_MultipleUnits(void) { + TEST_ASSERT_EQUAL_STRING("2d 3h", + formatTime(183600).c_str()); // 2 days and 3 hours +} + +void test_EdgeOfUnits(void) { + TEST_ASSERT_EQUAL_STRING("59s", formatTime(59).c_str()); + TEST_ASSERT_EQUAL_STRING( + "59:59", + formatTime(3599).c_str()); // 59 minutes, 59 seconds +} + +void test_CombinedUnits(void) { + TEST_ASSERT_EQUAL_STRING( + "1d 1h 1:01", + formatTime(90061).c_str()); // 1 day, 1 hour, 1 minute, 1 second +} + +void test_Zero(void) { + TEST_ASSERT_EQUAL_STRING("0 in", formatImperial(0).c_str()); +} + +void test_LessThanOneFoot(void) { + TEST_ASSERT_EQUAL_STRING("3 in", + formatImperial(0.0762).c_str()); // 3 inches +} + +void test_ExactlyOneFoot(void) { + TEST_ASSERT_EQUAL_STRING("1.00 ft", + formatImperial(0.3048).c_str()); // 1 foot +} + +void test_MultipleFeet(void) { + TEST_ASSERT_EQUAL_STRING("100.00 ft", + formatImperial(30.48).c_str()); // 100 feet +} + +void test_NearlyOneMile(void) { TEST_ASSERT_EQUAL_STRING( - "012345678912\n345678901234\n567890", - wrapText("012345678912345678901234567890").c_str()); + "5279.00 ft", formatImperial(1609.0).c_str()); // Just below one mile +} - // Case with space exactly at 12th character +void test_ExactlyOneMile(void) { TEST_ASSERT_EQUAL_STRING( - "Hello World!\nThis is a\ntest string.", - wrapText("Hello World! This is a test string.").c_str()); + "1.00 mi", formatImperial(1609.344).c_str()); // Exactly one mile +} - // Case with no spaces - TEST_ASSERT_EQUAL_STRING("012345678901\n234567890123\n", - wrapText("012345678901234567890123").c_str()); +void test_MultipleMiles(void) { + TEST_ASSERT_EQUAL_STRING( + "3.11 mi", formatImperial(5000).c_str()); // More than 3 miles +} - // Case with long word more than 12 characters +void test_VerySmallValues(void) { TEST_ASSERT_EQUAL_STRING( - "Supercalifra\ngilisticexpi\nalidocious", - wrapText("Supercalifragilisticexpialidocious").c_str()); + "0 in", + formatImperial(0.0001).c_str()); // Should round down to 0 inches +} - // Case with multiple spaces and newline characters +void test_NegativeValues(void) { TEST_ASSERT_EQUAL_STRING( - "This is a\nlong string\nwith multiple\nspaces and\nnewlines", - wrapText("This is a long string with multiple spaces and newlines") - .c_str()); + "-394 in", + formatImperial(-10) + .c_str()); // Assuming your function handles negatives +} + +void test_ZeroMeters(void) { + TEST_ASSERT_EQUAL_STRING("0.0 cm", formatMetric(0).c_str()); +} + +void test_LessThanOneMeter(void) { + TEST_ASSERT_EQUAL_STRING("99.0 cm", formatMetric(0.99).c_str()); +} + +void test_ExactlyOneMeter(void) { + TEST_ASSERT_EQUAL_STRING("1.0 m", formatMetric(1.0).c_str()); +} + +void test_MultipleMetersUnder100(void) { + TEST_ASSERT_EQUAL_STRING("50.0 m", formatMetric(50).c_str()); +} + +void test_JustBelow100Meters(void) { + TEST_ASSERT_EQUAL_STRING("99.9 m", formatMetric(99.9).c_str()); +} + +void test_Exactly100Meters(void) { + TEST_ASSERT_EQUAL_STRING("100.00 m", formatMetric(100).c_str()); +} - // Case where last word exactly fits the line - TEST_ASSERT_EQUAL_STRING("123456789012\nabcdefghijk", - wrapText("123456789012 abcdefghijk").c_str()); +void test_Between100And1000Meters(void) { + TEST_ASSERT_EQUAL_STRING("500.00 m", formatMetric(500).c_str()); +} + +void test_JustBelow1000Meters(void) { + TEST_ASSERT_EQUAL_STRING("999.00 m", formatMetric(999).c_str()); +} + +void test_Exactly1000Meters(void) { + TEST_ASSERT_EQUAL_STRING("1.00 km", formatMetric(1000).c_str()); +} + +void test_Above1000Meters(void) { + TEST_ASSERT_EQUAL_STRING("2.50 km", formatMetric(2500).c_str()); +} + +void test_VerySmallValuesMetric(void) { + TEST_ASSERT_EQUAL_STRING("0.1 cm", formatMetric(0.001).c_str()); +} + +void test_NegativeValuesMetric(void) { + // Assuming you want to handle negative values explicitly + TEST_ASSERT_EQUAL_STRING( + "-1.0 m", + formatMetric(-1.0).c_str()); // Depends on your function's behavior } int runUnityTests() { UNITY_BEGIN(); - RUN_TEST(test_isStringEmpty); - RUN_TEST(test_wrapText); + // Time formatting tests + RUN_TEST(test_ZeroSeconds); + RUN_TEST(test_SingleMinute); + RUN_TEST(test_MultipleUnits); + RUN_TEST(test_EdgeOfUnits); + RUN_TEST(test_CombinedUnits); + + // Imperial distance formatting tests + RUN_TEST(test_Zero); + RUN_TEST(test_LessThanOneFoot); + RUN_TEST(test_ExactlyOneFoot); + RUN_TEST(test_MultipleFeet); + RUN_TEST(test_NearlyOneMile); + RUN_TEST(test_ExactlyOneMile); + RUN_TEST(test_MultipleMiles); + RUN_TEST(test_VerySmallValues); + RUN_TEST(test_NegativeValues); + + // Metric + RUN_TEST(test_ZeroMeters); + RUN_TEST(test_LessThanOneMeter); + RUN_TEST(test_ExactlyOneMeter); + RUN_TEST(test_MultipleMetersUnder100); + RUN_TEST(test_JustBelow100Meters); + RUN_TEST(test_Exactly100Meters); + RUN_TEST(test_Between100And1000Meters); + RUN_TEST(test_JustBelow1000Meters); + RUN_TEST(test_Exactly1000Meters); + RUN_TEST(test_Above1000Meters); + RUN_TEST(test_VerySmallValuesMetric); + RUN_TEST(test_NegativeValuesMetric); return UNITY_END(); }