From 7ec4fa339414b834b843646b1de2b929651c38c3 Mon Sep 17 00:00:00 2001 From: Mark van Renswoude Date: Sat, 1 Feb 2020 16:55:33 +0100 Subject: [PATCH] Changed measurements to be async to keep handling button presses Implemented calibration screen Implemented pre-move stabilization Had to remove the nice font to save on program space :-( --- doc/DeskControl UI mockup - LCD version.psd | Bin 181748 -> 188582 bytes doc/testscript.md | 2 +- src/fonts/FreeSansBold18pt7b.trimmed.h | 181 ++++++---- src/fonts/trim.bat | 2 +- src/include/config.h | 18 +- src/include/metrics.h | 21 +- src/include/screenids.h | 15 + src/lib/control.cpp | 363 +++++++++++--------- src/lib/control.h | 32 +- src/lib/debug.h | 17 +- src/lib/motor.cpp | 4 +- src/lib/motor.h | 6 +- src/lib/screen.cpp | 44 ++- src/lib/screen.h | 30 +- src/lib/screen/calibrate.cpp | 190 +++++++++- src/lib/screen/calibrate.h | 24 +- src/lib/screen/home.cpp | 49 ++- src/lib/screen/home.h | 5 +- src/lib/screen/manual.cpp | 2 - src/lib/screen/manual.h | 5 +- src/lib/screen/menu.cpp | 2 - src/lib/screen/menu.h | 5 +- src/lib/screen/move-overcurrent.cpp | 10 +- src/lib/screen/move-overcurrent.h | 7 +- src/lib/screen/move-sensorerror.cpp | 44 ++- src/lib/screen/move-sensorerror.h | 7 +- src/lib/screen/move.cpp | 36 +- src/lib/screen/move.h | 6 +- src/lib/settings.h | 4 +- src/lib/vl53l0x.cpp | 86 ++++- src/lib/vl53l0x.h | 25 +- src/main.cpp | 70 ++-- 32 files changed, 920 insertions(+), 392 deletions(-) create mode 100644 src/include/screenids.h diff --git a/doc/DeskControl UI mockup - LCD version.psd b/doc/DeskControl UI mockup - LCD version.psd index 7c7baed4c0211a61db7d5ab8c01b0c433d7c7999..7ad3a604d6290379f4aaef39f4d309c42f36e774 100644 GIT binary patch literal 188582 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

u&9%KBjaw7%jPoH5dsrJpId!;9ghpm*% zQduw7kDbDXuwfW@WU&!!EE~tpWK-D;b}qY!m9Pp{iP1(4yMirb*Rz}0pV%GjE_OeA zm_5OsV$ZM_*uU6o>}~b|`)H|EqBQ%rVDZp@^Z88H{el*e2eb6L!_F}K9r8S`MwUt*q*c`fFHm@i`1 z$NUzvHcv)J{qTVosI z65_hW8RLe%t^RB;pT*U6P`|3lkiEx zPYHY5wQJX_-RbQ{wVT?`(Js*L`gSYY{iWTj?LKX{v0Z)p4(ii0q!~$bk`^Z2 zneC{eRJDt}l(CL;=Pjq^t)Aya~J9q1x-np=IY3Hjt-`)B7&YyO!O-@W6 zl$@JG5Tc#*_MZDIk zs_S`j&#^t7J(u)+y5|=?8+#dhjq5eH*V0}u^jguZ&+1*%`@Y_9_1=1N>d7Nc zE;)J0$C-klAyQ1$KeYf`O(Qj?$p7jo_*>Kr@nCN=D|G& z7Y@E`@RNhT8`5dWm?4)Ad2q<*LlcH(51l*o-k~3#7IT{Iw2IU2KJBB^V@|i9?mGS6 z(?2;Q{*0V6JZC(3##h5S4$B=jZ`hN=Hl+7RpParZ{e|>e(?HXCrlqELOb5(1v)lZz z`CCg@%bAu%mX|EMGEU1V$+$b?b89E-cyoT{vcAer&7PKhbN0JA@i}=pSLOUGr(w8#cwqQ5!?%wZHe&9GRUwBxkGd3Hz#$RR6OafNk2|LZL)9jOGPn7XBOR7^xc#}Q#?~% zn98OWPQ7jFx@m)_T{>;`SqW!NJL~SVewl8We#P{6in|qGT)e7y-;BH&x6b%>=8&1y zGhd&TJnMp4Pt2-2yWs3)XRklUbk3FMd~j~>b6w}YcwW2nW}Wxwd3(E z!Sxq>dEwv-=U@2VMZGU_U-a_D$rn2=e)f|1OU}Mz)g}8KQymXF_RcPxeb4M2CHW

Ef6=N!vS8R3VyY6!BoHKFG{d4N(PM!PM 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 delta 22484 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 -#include "fonts/FreeSansBold18pt7b.trimmed.h" +//#include "fonts/FreeSansBold18pt7b.trimmed.h" class Metrics { @@ -14,6 +14,7 @@ class Metrics */ static constexpr const GFXfont* SmallFont = nullptr; + static const uint8_t SmallFontTextSize = 1; static const uint16_t SmallFontBaseline = 0; static const uint16_t SmallFontHeight = 8; @@ -23,15 +24,21 @@ class Metrics /* Large font - Uses a trimmed version of the FreeSansBold 18pt font. - + Originally used a trimmed version of the FreeSansBold 18pt font. + Unfortunately due to program size constraints (I already ordered + PCBs based on the ATMega328P) I'll use a scaled version of the + built-in font for now. */ - static constexpr const GFXfont* LargeFont = &FreeSansBold18pt7bTrimmed; + static constexpr const GFXfont* LargeFont = nullptr;//&FreeSansBold18pt7bTrimmed; + static const uint8_t LargeFontTextSize = 3; - static const uint16_t LargeFontBaseline = 25; - static const uint16_t LargeFontHeight = Metrics::LargeFontBaseline; - static const uint16_t LargeFontMaxHeight = 42; + //static const uint16_t LargeFontBaseline = 25; + //static const uint16_t LargeFontHeight = Metrics::LargeFontBaseline; + //static const uint16_t LargeFontMaxHeight = 42; + static const uint16_t LargeFontBaseline = 0; + static const uint16_t LargeFontHeight = 24; + static const uint16_t LargeFontMaxHeight = 24; /* diff --git a/src/include/screenids.h b/src/include/screenids.h new file mode 100644 index 0000000..44942e8 --- /dev/null +++ b/src/include/screenids.h @@ -0,0 +1,15 @@ +#ifndef __screenids +#define __screenids + +enum class ScreenId +{ + Home, + Calibrate, + Move, + MoveOvercurrent, + MoveSensorError, + Menu, + Manual +}; + +#endif \ No newline at end of file diff --git a/src/lib/control.cpp b/src/lib/control.cpp index 516e01d..f92522e 100644 --- a/src/lib/control.cpp +++ b/src/lib/control.cpp @@ -1,9 +1,9 @@ #include "./control.h" #include -#include "./motor.h" -#include "./Control.h" -#include "./settings.h" #include "./debug.h" +#include "./motor.h" +#include "./state.h" +#include "./settings.h" #include "include/config.h" @@ -12,6 +12,7 @@ ControlManager Control = ControlManager(); VL53L0XInitResult ControlManager::init() { + dln("[ CONTROL ] Initializing"); Wire.begin(); this->heightSensor.setTimeout(500); @@ -26,45 +27,182 @@ VL53L0XInitResult ControlManager::init() ControlUpdateResult ControlManager::update() { + // Always read the range if in async reading mode, even if we're not supposed to + // be moving anymore, so it clears the status. + bool asyncReading = heightSensor.asyncReading(); + uint16_t measurement = asyncReading + ? heightSensor.asyncReadRangeSingleMillimeters() + : VL53L0XRangeNotReady; + + + vl("[ CONTROL ] Update: asyncReading = "); vl(asyncReading); vl(", measurement = "); vl(measurement); + vl(", moveDirection = "); vl((uint8_t)this->moveDirection); vl(", stabilizationStart = "); vln(this->stabilizationStart); + + + bool moving = this->moveDirection != MoveDirection::None && this->stabilizationStart == 0; + if (!moving && this->stabilizationStart == 0) + { + vl("[ CONTROL ] Idle"); + return ControlUpdateResult::Idle; + } + + + // Check for over-current + // TODO: don't check current every update + if (moving && motorIsOvercurrent()) + { + dln("[ CONTROL ] Overcurrent detected!"); + this->moveStop(); + + return ControlUpdateResult::Overcurrent; + } + + // Read sensor + if (!asyncReading) + { + vln("[ CONTROL ] Starting async read"); + heightSensor.asyncStartReadRangeSingleMillimeters(); + } + + + if (measurement != VL53L0XRangeNotReady) + { + if (measurement > Config::HeightMeasurementMax) + // Treat invalid results as a timeout, so the checks further on are more readable + measurement = VL53L0XRangeTimeout; + + + dl("[ CONTROL ] Measurement: "); dln(measurement); + this->lastMeasurement = measurement != VL53L0XRangeTimeout ? measurement : 0; + } + + + if (measurement != VL53L0XRangeNotReady && measurement != VL53L0XRangeTimeout) + { + this->currentHeight = measurement; + this->lastValidMeasurement = CurrentTime; + + // Check if target has been reached + if (moving && this->targetReached()) + { + this->moveStop(); + + return ControlUpdateResult::TargetReached; + } + } + + if (measurement == VL53L0XRangeTimeout && moving) + { + // While moving, allow incidental invalid results + if (CurrentTime - lastValidMeasurement >= Config::HeightMeasurementAbortTimeout) + { + dln("[ CONTROL ] Timeout while moving!"); + this->moveStop(); + + return ControlUpdateResult::SensorError; + } + } + + if (measurement == VL53L0XRangeTimeout && this->moveDirection == MoveDirection::None) + { + // In pure stabilization (not pre-move), immediately return the sensor error + return ControlUpdateResult::SensorError; + } + + + + if (this->stabilizationStart && this->moveDirection != MoveDirection::None) + { + // Pre-move stabilization + if (this->stabilized()) + { + dln("[ CONTROL ] Pre-move stabilization successful"); + + // Sensor looks good, let's go! + motorStart(this->moveDirection == MoveDirection::Up ? MotorDirection::Up : MotorDirection::Down); + return ControlUpdateResult::Moving; + } + else if (CurrentTime - this->stabilizationStart >= Config::HeightMeasurementDeltaStableMoveTimeout) + { + dln("[ CONTROL ] Timeout in pre-move stabilization!"); + + // Timeout expired, abort the move + this->stabilizationStart = 0; + this->moveDirection = MoveDirection::None; + + return ControlUpdateResult::SensorError; + } + } + + return moving ? ControlUpdateResult::Moving : ControlUpdateResult::Stabilizing; } void ControlManager::stabilizeStart() { - this->stabilizing = true; + dln("[ CONTROL ] Starting stabilization"); + + this->stabilizationStart = CurrentTime; + this->stabilizeTarget = 0; } bool ControlManager::stabilized() { - // TODO: stabilized + if (this->stabilizeTarget == 0) + { + this->stabilizeTarget = this->currentHeight; + this->stabilizationLastCheck = this->lastValidMeasurement; + return false; + } + + // Only count if we have a new measurement + if (this->stabilizationLastCheck == this->lastValidMeasurement) + return false; + + this->stabilizationLastCheck = this->lastValidMeasurement; + + + int16_t delta = this->currentHeight - this->stabilizeTarget; + if (abs(delta) <= Config::HeightMeasurementDeltaStable) + { + this->stabilizeCount++; + + if (this->stabilizeCount >= Config::HeightMeasurementDeltaStableCount) + { + dln("[ CONTROL ] Stable"); + this->stabilizationStart = 0; + return true; + } + } + else + { + // If it's this much off, chances are the actual value is somewhere in the middle + this->stabilizeTarget += (delta / 2); + this->stabilizeCount = 0; + } + + return false; } -void ControlManager::movePrepare(uint16_t height) +void ControlManager::moveStart(uint16_t height) { + dl("[ CONTROL ] Starting move to: "); dln(height); + this->moveTarget = height; this->moveDirection = height > this->currentHeight ? MoveDirection::Up : MoveDirection::Down; -} - -bool ControlManager::moveStart() -{ - // TODO: moveStart - wait for stable / timeout - - this->moveDirection = MoveDirection::None; + // Wait for a stable result this->stabilizeStart(); - //this->sensorError = true; - return false; - - motorStart(this->moveDirection == MoveDirection::Up ? MotorDirection::Up : MotorDirection::Down); - return true; } void ControlManager::moveStop() { + dln("[ CONTROL ] Stopping move"); + motorStop(); this->moveDirection = MoveDirection::None; } @@ -84,8 +222,52 @@ void ControlManager::snapToPreset() } +bool ControlManager::targetReached() +{ + // Checks depend on the direction, so it returns true immediately if we've overshot the target + switch (this->moveDirection) + { + case MoveDirection::Up: + if (this->currentHeight >= this->moveTarget - Config::HeightMeasurementDeltaStop) + { + // Snap to the target + if (this->currentHeight - this->moveTarget <= Config::HeightMeasurementDeltaOnTarget) + this->currentHeight = this->moveTarget; + + dln("[ CONTROL ] Target reached"); + return true; + } + break; + + case MoveDirection::Down: + if (this->currentHeight <= this->moveTarget + Config::HeightMeasurementDeltaStop) + { + // Snap to the target + if (this->moveTarget - this->currentHeight <= Config::HeightMeasurementDeltaOnTarget) + this->currentHeight = this->moveTarget; + + dln("[ CONTROL ] Target reached"); + return true; + } + break; + + default: + break; + } + + return false; +} + + void ControlManager::getDisplayHeight(char* buffer, uint16_t value) { + if (value == 0) + { + buffer[0] = '-'; + buffer[1] = 0; + return; + } + uint8_t displayValue = (value + Settings.Height.Offset) / 10; if (displayValue > 99) @@ -99,148 +281,3 @@ void ControlManager::getDisplayHeight(char* buffer, uint16_t value) buffer[4] = 'm'; buffer[5] = 0; } - - -/* -bool controlCheckTargetReached() -{ - dl("controlCheckTargetReached: direction = "); dl((uint8_t)Control.getMoveDirection()); dl(", currentHeight = "); dln(Control.getCurrentHeight()); - - switch (Control.getMoveDirection()) - { - case Direction::Up: - if (Control.getCurrentHeight() >= Control.getMoveTarget() - Config::HeightMeasurementDeltaStop) - { - if (Control.getCurrentHeight() - Control.getMoveTarget() <= Config::HeightMeasurementDeltaOnTarget) - Control.getCurrentHeight() = Control.getMoveTarget(); - - controlStop(); - return true; - } - break; - - case Direction::Down: - if (Control.getCurrentHeight() <= Control.getMoveTarget() + Config::HeightMeasurementDeltaStop) - { - if (Control.getMoveTarget() - Control.getCurrentHeight() <= Config::HeightMeasurementDeltaOnTarget) - Control.getCurrentHeight() = Control.getMoveTarget(); - - controlStop(); - return true; - } - break; - - default: - break; - } - - return false; -} - - -bool controlCheckOverCurrent() -{ - if (motorIsOverCurrent()) - { - dln("controlCheckOverCurrent: overcurrent detected!"); - - controlStop(); - return true; - } - - return false; -} - - - - - if (Control.getMoveDirection() != Direction::None) - { - if (controlCheckOverCurrent()) - screenManager.show(); - else - updateHeight(); - } - else if (Control.SensorError) - updateHeight(); - - - - -void updateHeight() -{ - uint16_t measurement; - - if (heightSensorGetRange(&measurement)) - { - Control.getCurrentHeight() = measurement; - - if (Control.getMoveDirection() != Direction::None) - { - lastValidMeasurement = CurrentTime; - - if (controlCheckTargetReached()) - screenManager.show(); - } - } - else if (Control.getMoveDirection() != Direction::None && CurrentTime - lastValidMeasurement >= Config::HeightMeasurementAbortTimeout) - { - dln("Out of range timeout!"); - Control.moveStop(); - - screenManager.show(); - } -} - - -bool heightSensorGetRange(uint16_t* measurement) -{ - *measurement = heightSensor.readRangeSingleMillimeters(); - dl("Range: "); dln(*measurement); - - return *measurement <= Config::HeightMeasurementMax; -} - - - - - - - - - uint16_t reference = 0; - uint8_t closeCount = 0; - uint16_t measurement; - - while (true) - { - if (heightSensorGetRange(&measurement)) - { - initSequenceDisplayHeight(measurement); - - if (abs(measurement - reference) <= Config::HeightMeasurementDeltaStable) - closeCount++; - else - { - reference = measurement; - closeCount = 0; - } - } - else - { - initSequenceDisplayHeight(0); - - reference = 0; - closeCount = 0; - } - - if (closeCount < Config::HeightMeasurementDeltaStableCount) - delay(500); - else - break; - } - - initSequenceSuccess(InitSequenceStep::HeightSensorTest); - return reference; - -*/ \ No newline at end of file diff --git a/src/lib/control.h b/src/lib/control.h index df27946..5b5ff57 100644 --- a/src/lib/control.h +++ b/src/lib/control.h @@ -7,19 +7,20 @@ enum class ControlUpdateResult { - Idle = 0, - Moving = 1, - TargetReached = 2, - SensorError = 3, - OverCurrent = 4 + Idle, + Stabilizing, + Moving, + TargetReached, + SensorError, + Overcurrent }; enum class MoveDirection { - None = 0, - Up = 1, - Down = 2 + None, + Up, + Down }; @@ -34,8 +35,7 @@ class ControlManager void stabilizeStart(); bool stabilized(); - void movePrepare(uint16_t height); - bool moveStart(); + void moveStart(uint16_t height); void moveStop(); void snapToPreset(); @@ -48,18 +48,26 @@ class ControlManager uint16_t getCurrentHeight() { return this->currentHeight; } + uint16_t getLastMeasurement() { return this->lastMeasurement; } MoveDirection getMoveDirection() { return this->moveDirection; } uint16_t getMoveTarget() { return this->moveTarget; } + bool getIsStabilizing() { return this->stabilizationStart != 0; } private: + bool targetReached(); + VL53L0X heightSensor = VL53L0X(); uint16_t currentHeight; + uint16_t lastMeasurement = 0; + uint32_t lastValidMeasurement = 0; MoveDirection moveDirection; uint16_t moveTarget; - bool stabilizing = false; - uint32_t lastValidMeasurement = 0; + uint32_t stabilizationStart = 0; + uint8_t stabilizeCount = 0; + uint16_t stabilizeTarget = 0; + uint32_t stabilizationLastCheck = 0; }; diff --git a/src/lib/debug.h b/src/lib/debug.h index 5d76651..b4e19dc 100644 --- a/src/lib/debug.h +++ b/src/lib/debug.h @@ -3,12 +3,12 @@ #include -#define SerialDebug +#define DebugLog +//#define VerboseLog - -#ifdef SerialDebug - #define DebugInit() Serial.begin(9600) +#ifdef DebugLog + #define DebugInit() Serial.begin(115200) #define dl(value) Serial.print(value) #define dln(value) Serial.println(value) #else @@ -18,4 +18,13 @@ #endif +#ifdef VerboseLog + #define vl(value) Serial.print(value) + #define vln(value) Serial.println(value) +#else + #define vl(value) + #define vln(value) +#endif + + #endif \ No newline at end of file diff --git a/src/lib/motor.cpp b/src/lib/motor.cpp index 5df1f3d..76e255a 100644 --- a/src/lib/motor.cpp +++ b/src/lib/motor.cpp @@ -31,9 +31,9 @@ void motorStop() } -bool motorIsOverCurrent() +bool motorIsOvercurrent() { - // TODO: implement motorIsOverCurrent + // TODO: implement motorIsOvercurrent return false; } diff --git a/src/lib/motor.h b/src/lib/motor.h index 48b6162..2f44430 100644 --- a/src/lib/motor.h +++ b/src/lib/motor.h @@ -5,14 +5,14 @@ // Low-level functions to control the motor enum class MotorDirection { - Up = 0, - Down = 1 + Up, + Down }; extern void motorInit(); extern void motorStart(MotorDirection direction); extern void motorStop(); -extern bool motorIsOverCurrent(); +extern bool motorIsOvercurrent(); #endif \ No newline at end of file diff --git a/src/lib/screen.cpp b/src/lib/screen.cpp index 1b8e64e..0f52fb8 100644 --- a/src/lib/screen.cpp +++ b/src/lib/screen.cpp @@ -3,43 +3,53 @@ #include "include/metrics.h" -Adafruit_GFX* BaseScreen::getDisplay() -{ - return this->screenManager->getDisplay(); -} - - uint16_t BaseScreen::printCentered(const char* text, int16_t y) { - auto display = this->getDisplay(); - int16_t textX; int16_t textY; uint16_t textW; uint16_t textH; - display->getTextBounds(text, 0, 0, &textX, &textY, &textW, &textH); + this->display->getTextBounds(text, 0, 0, &textX, &textY, &textW, &textH); textX = (Config::DisplayWidth - textW) / 2; - display->setCursor(textX, y); - display->print(text); + this->display->setCursor(textX, y); + this->display->print(text); return textW; } +void BaseScreen::drawLargeTextLineCentered(const char* text, int16_t y, uint16_t textColor, uint16_t backgroundColor) +{ + this->display->fillRect(0, y, Config::DisplayWidth, Metrics::LargeTextLineHeight, backgroundColor); + this->display->setTextColor(textColor); + this->printCentered(text, y + Metrics::LargeTextLineYOffset); +} + + void BaseScreen::drawArrowLeft(int16_t x, int16_t y, uint16_t color) { - this->getDisplay()->fillTriangle( - x + Metrics::HArrowWidth, y, // Top right - x, y + (Metrics::HArrowHeight / 2), // Middle left - x + Metrics::HArrowWidth, y + Metrics::HArrowHeight, // Bottom right + this->display->fillTriangle( + x + Metrics::HArrowWidth, y, // Top right + x, y + (Metrics::HArrowHeight / 2), // Middle left + x + Metrics::HArrowWidth, y + Metrics::HArrowHeight, // Bottom right + color); +} + + +void BaseScreen::drawArrowRight(int16_t x, int16_t y, uint16_t color) +{ + this->display->fillTriangle( + x, y, // Top left + x + + Metrics::HArrowWidth, y + (Metrics::HArrowHeight / 2), // Middle right + x, y + Metrics::HArrowHeight, // Bottom left color); } void BaseScreen::drawArrowUp(int16_t x, int16_t y, uint16_t color) { - this->getDisplay()->fillTriangle( + this->display->fillTriangle( x + (Metrics::VArrowWidth / 2), y, // Top middle x, y + Metrics::VArrowHeight, // Bottom left x + Metrics::VArrowWidth, y + Metrics::VArrowHeight, // Bottom right @@ -49,7 +59,7 @@ void BaseScreen::drawArrowUp(int16_t x, int16_t y, uint16_t color) void BaseScreen::drawArrowDown(int16_t x, int16_t y, uint16_t color) { - this->getDisplay()->fillTriangle( + this->display->fillTriangle( x, y, // Top left x + Metrics::VArrowWidth, y, // Top right x + (Metrics::VArrowWidth / 2), y + Metrics::VArrowHeight, // Bottom middle diff --git a/src/lib/screen.h b/src/lib/screen.h index 15f08b7..6ac5962 100644 --- a/src/lib/screen.h +++ b/src/lib/screen.h @@ -3,6 +3,7 @@ #include #include "include/config.h" +#include "include/screenids.h" class ScreenManager; @@ -10,18 +11,19 @@ class ScreenManager; enum class Button { - Up = 0, - Menu = 1, - Down = 2 + Top, + Middle, + Bottom }; class BaseScreen { public: - BaseScreen(ScreenManager* screenManager) + BaseScreen(ScreenManager* screenManager, Adafruit_GFX* display) { this->screenManager = screenManager; + this->display = display; } virtual ~BaseScreen() {} @@ -30,18 +32,20 @@ class BaseScreen virtual void onButton(Button button) = 0; virtual void onTick() = 0; - protected: - ScreenManager* getScreenManager() { return this->screenManager; } - Adafruit_GFX* getDisplay(); + virtual ScreenId screenId() = 0; + protected: uint16_t printCentered(const char* text, int16_t y); + void drawLargeTextLineCentered(const char* text, int16_t y, uint16_t textColor, uint16_t backgroundColor); + void drawArrowLeft(int16_t x, int16_t y, uint16_t color); + void drawArrowRight(int16_t x, int16_t y, uint16_t color); void drawArrowUp(int16_t x, int16_t y, uint16_t color); void drawArrowDown(int16_t x, int16_t y, uint16_t color); - private: ScreenManager* screenManager; + Adafruit_GFX* display; }; @@ -60,7 +64,6 @@ class ScreenManager inline void tick() { this->getCurrentScreen()->onTick(); } inline BaseScreen* getCurrentScreen() { return this->currentScreen; } - inline Adafruit_GFX* getDisplay() { return this->display; } template void show() @@ -71,17 +74,22 @@ class ScreenManager delete this->currentScreen; } - this->currentScreen = new T(this); + this->currentScreen = new T(this, this->display); this->currentScreen->onShow(); } + template void show(ScreenId onlyIfNotOn) + { + if (this->currentScreen == nullptr || this->currentScreen->screenId() != onlyIfNotOn) + this->show(); + } + void displayOff(); void displayOn(); private: Adafruit_ST7789* display; - BaseScreen* currentScreen = nullptr; }; diff --git a/src/lib/screen/calibrate.cpp b/src/lib/screen/calibrate.cpp index 6d286c1..495ada3 100644 --- a/src/lib/screen/calibrate.cpp +++ b/src/lib/screen/calibrate.cpp @@ -1,19 +1,205 @@ #include "./calibrate.h" +#include "./home.h" +#include "include/metrics.h" +#include "lib/settings.h" void CalibrateScreen::onShow() { - //auto display = this->getDisplay(); + this->display->setFont(Metrics::LargeFont); + this->display->setTextSize(Metrics::LargeFontTextSize); - // TODO: implement CalibrateScreen + this->display->fillScreen(Config::ColorHomeBackground); + + this->drawTitle(); + this->height[CalibrateStepCurrent] = Control.getCurrentHeight() + Settings.Height.Offset; + this->height[CalibrateStepMax] = 1969; // TODO: read min/max from settings + this->height[CalibrateStepMin] = 0; + + this->drawArrowUp(Metrics::ArrowMargin, Metrics::LargeTextLineHeight + Metrics::LargeTextLineVArrowYOffset, Config::ColorCalibrateIndicators); + this->drawArrowRight(Metrics::ArrowMargin, Metrics::MiddleLargeTextLineY + Metrics::LargeTextLineHArrowYOffset, Config::ColorCalibrateIndicators); + + this->display->setTextColor(Config::ColorCalibrateIndicators); + this->display->setCursor(Metrics::ArrowMargin, Config::DisplayHeight - Metrics::LargeTextLineHeight + Metrics::LargeTextLineYOffset); + this->display->print("OK"); + + this->drawSetHeight(); +} + + +// Credit: https://stackoverflow.com/questions/101439/the-most-efficient-way-to-implement-an-integer-based-power-function-powint-int +uint16_t ipow(uint16_t base, uint8_t exp) +{ + int result = 1; + for (;;) + { + if (exp & 1) + result *= base; + + exp >>= 1; + if (!exp) + break; + + base *= base; + } + + return result; } void CalibrateScreen::onButton(Button button) { + switch (button) + { + case Button::Top: + { + uint16_t height = this->height[this->step]; + uint16_t increment = ipow(10, 3 - this->editingDigit); + uint16_t modulus = increment * 10; + + uint16_t remainder = height % modulus; + uint16_t offset = height - remainder; + + height = offset + ((remainder + increment) % modulus); + if (height > 1999) + height %= 2000; + + this->height[this->step] = height; + this->drawSetHeight(); + break; + } + + case Button::Middle: + { + this->editingDigit++; + if (this->editingDigit > 3) + this->editingDigit = 0; + + this->drawSetHeight(); + break; + } + + case Button::Bottom: + { + if (!this->isValidHeight()) + return; + + this->step = (CalibrateStep)(this->step + 1); + if (this->step == CalibrateStepCount) + { + // TODO: store new settings + this->screenManager->show(); + } + else + { + this->drawTitle(); + this->drawSetHeight(); + } + + break; + } + } } void CalibrateScreen::onTick() { +} + + +bool CalibrateScreen::isValidHeight() +{ + switch (this->step) + { + case CalibrateStepCurrent: + return this->height[CalibrateStepCurrent] >= Control.getCurrentHeight(); + + case CalibrateStepMin: + return this->height[CalibrateStepMin] <= this->height[CalibrateStepCurrent]; + + case CalibrateStepMax: + return + this->height[CalibrateStepMax] > this->height[CalibrateStepMin] && + this->height[CalibrateStepMax] >= this->height[CalibrateStepCurrent]; + + default: + return true; + } +} + + +void CalibrateScreen::drawTitle() +{ + switch (this->step) + { + case CalibrateStepCurrent: + this->drawLargeTextLineCentered("Calibrate", 0, Config::ColorMenuHeaderText, Config::ColorMenuHeaderBackground); + break; + + case CalibrateStepMin: + this->drawLargeTextLineCentered("Minimum", 0, Config::ColorMenuHeaderText, Config::ColorMenuHeaderBackground); + break; + + case CalibrateStepMax: + this->drawLargeTextLineCentered("Maximum", 0, Config::ColorMenuHeaderText, Config::ColorMenuHeaderBackground); + break; + + default: + break; + } +} + + +void CalibrateScreen::drawSetHeight() +{ + char heightText[7]; + uint16_t height = this->height[this->step]; + + if (height > 999) + heightText[0] = '0' + ((height / 1000) % 10); + else + heightText[0] = '0'; + + heightText[1] = '.'; + heightText[2] = '0' + ((height / 100) % 10); + heightText[3] = '0' + ((height / 10) % 10); + heightText[4] = '0' + (height % 10); + heightText[5] = 'm'; + heightText[6] = 0; + + + int16_t textX; + int16_t textY; + uint16_t textW; + uint16_t textH; + this->display->getTextBounds(&heightText[0], 0, 0, &textX, &textY, &textW, &textH); + textX = (Config::DisplayWidth - textW) / 2; + + + + uint8_t editingChar = this->editingDigit; + + // Skip the dot + if (editingChar > 0) + editingChar++; + + + if (this->lastTextWidth > 0) + this->display->fillRect((Config::DisplayWidth - this->lastTextWidth) / 2, Metrics::MiddleLargeTextLineY, this->lastTextWidth, Metrics::LargeTextLineHeight, Config::ColorCalibrateBackground); + + this->lastTextWidth = textW; + + this->display->setTextColor(this->isValidHeight() ? Config::ColorCalibrateValue : Config::ColorCalibrateInvalidValue); + this->display->setCursor(textX, Metrics::MiddleLargeTextLineY + Metrics::LargeTextLineYOffset); + + // Draw each character ourselves so we can keep track of where the line should be drawn + for (uint8_t i = 0; i < sizeof(heightText); i++) + { + int16_t cursorStart = this->display->getCursorX(); + + this->display->print(heightText[i]); + + if (i == editingChar) + this->display->drawFastHLine(cursorStart, Metrics::MiddleLargeTextLineY + Metrics::LargeTextLineHeight - 1, this->display->getCursorX() - cursorStart, Config::ColorCalibrateDigitMarker); + } } \ No newline at end of file diff --git a/src/lib/screen/calibrate.h b/src/lib/screen/calibrate.h index 6291a42..6c9aa2e 100644 --- a/src/lib/screen/calibrate.h +++ b/src/lib/screen/calibrate.h @@ -1,10 +1,21 @@ #ifndef __screen_calibrate #define __screen_calibrate +#include "include/screenids.h" #include "../screen.h" #include "../control.h" +enum CalibrateStep : uint8_t +{ + CalibrateStepCurrent = 0, + CalibrateStepMax, + CalibrateStepMin, + + CalibrateStepCount +}; + + /* * Calibrate screen * Configures the absolute height (calculates the offset) and @@ -13,13 +24,24 @@ class CalibrateScreen : public BaseScreen { public: - CalibrateScreen(ScreenManager* screenManager) : BaseScreen(screenManager) { } + CalibrateScreen(ScreenManager* screenManager, Adafruit_GFX* display) : BaseScreen(screenManager, display) { } void onShow(); void onButton(Button button); void onTick(); + ScreenId screenId() { return ScreenId::Calibrate; }; + private: + bool isValidHeight(); + void drawTitle(); + void drawSetHeight(); + + CalibrateStep step = CalibrateStepCurrent; + + uint16_t height[CalibrateStepCount]; + uint16_t lastTextWidth = 0; + uint8_t editingDigit = 0; }; #endif diff --git a/src/lib/screen/home.cpp b/src/lib/screen/home.cpp index 2d87247..d55ede0 100644 --- a/src/lib/screen/home.cpp +++ b/src/lib/screen/home.cpp @@ -1,5 +1,6 @@ #include "./home.h" #include "./move.h" +#include "./move-sensorerror.h" #include "include/config.h" #include "include/metrics.h" #include "lib/settings.h" @@ -13,10 +14,10 @@ void HomeScreen::onShow() { this->showTime = CurrentTime; - auto display = this->getDisplay(); + this->display->setFont(Metrics::LargeFont); + this->display->setTextSize(Metrics::LargeFontTextSize); - display->setFont(Metrics::LargeFont); - display->fillScreen(Config::ColorHomeBackground); + this->display->fillScreen(Config::ColorHomeBackground); this->drawPreset1(); this->drawMenu(); @@ -30,33 +31,35 @@ void HomeScreen::onButton(Button button) { if (this->idle) { - this->getScreenManager()->displayOn(); + this->screenManager->displayOn(); this->idle = false; this->showTime = CurrentTime; // Preset buttons activate immediately - if (button == Button::Menu) + if (button == Button::Middle) return; } + uint16_t targetHeight; + switch (button) { - case Button::Up: - Control.movePrepare(Settings.Height.Preset[0]); - this->getScreenManager()->show(); - Control.moveStart(); + case Button::Top: + targetHeight = Settings.Height.Preset[0]; break; - case Button::Down: - Control.movePrepare(Settings.Height.Preset[1]); - this->getScreenManager()->show(); - Control.moveStart(); + case Button::Bottom: + targetHeight = Settings.Height.Preset[1]; break; - case Button::Menu: - this->getScreenManager()->show(); - break; + case Button::Middle: + this->screenManager->show(); + return; } + + + Control.moveStart(targetHeight); + this->screenManager->show(); } @@ -64,7 +67,7 @@ void HomeScreen::onTick() { if (!this->idle && CurrentTime - this->showTime >= Config::DisplayIdleTime) { - this->getScreenManager()->displayOff(); + this->screenManager->displayOff(); this->idle = true; } } @@ -84,13 +87,12 @@ void HomeScreen::drawPreset2() void HomeScreen::drawNonPresetHeight() { - auto display = this->getDisplay(); auto y = Metrics::LargeTextLineHeight; if (Control.getCurrentHeight() != Settings.Height.Preset[0] && Control.getCurrentHeight() != Settings.Height.Preset[1]) { - display->setTextColor(Config::ColorNonPresetText); + this->display->setTextColor(Config::ColorNonPresetText); this->drawHeight(y, Control.getCurrentHeight()); } } @@ -98,7 +100,6 @@ void HomeScreen::drawNonPresetHeight() void HomeScreen::drawPreset(int16_t y, uint16_t value) { - auto display = this->getDisplay(); uint16_t textColor; uint16_t backgroundColor; uint16_t arrowColor; @@ -117,12 +118,12 @@ void HomeScreen::drawPreset(int16_t y, uint16_t value) arrowColor = Config::ColorPresetArrow; } - display->fillRect(0, y, Config::DisplayWidth, Metrics::LargeTextLineHeight, backgroundColor); + this->display->fillRect(0, y, Config::DisplayWidth, Metrics::LargeTextLineHeight, backgroundColor); if (arrowColor) this->drawArrowLeft(Metrics::ArrowMargin, y + Metrics::LargeTextLineHArrowYOffset, arrowColor); - display->setTextColor(textColor); + this->display->setTextColor(textColor); this->drawHeight(y, value); } @@ -138,10 +139,8 @@ void HomeScreen::drawHeight(int16_t y, uint16_t value) void HomeScreen::drawMenu() { - auto display = this->getDisplay(); - this->drawArrowLeft(Metrics::ArrowMargin, Metrics::MiddleLargeTextLineY, Config::ColorHomeMenuArrow); - display->setTextColor(Config::ColorHomeMenuText); + this->display->setTextColor(Config::ColorHomeMenuText); this->printCentered("Menu", Metrics::MiddleLargeTextLineY + Metrics::LargeTextLineYOffset); } diff --git a/src/lib/screen/home.h b/src/lib/screen/home.h index 253a504..e06ceab 100644 --- a/src/lib/screen/home.h +++ b/src/lib/screen/home.h @@ -1,6 +1,7 @@ #ifndef __screen_home #define __screen_home +#include "include/screenids.h" #include "../screen.h" #include "../Control.h" @@ -12,12 +13,14 @@ class HomeScreen : public BaseScreen { public: - HomeScreen(ScreenManager* screenManager) : BaseScreen(screenManager) { } + HomeScreen(ScreenManager* screenManager, Adafruit_GFX* display) : BaseScreen(screenManager, display) { } void onShow(); void onButton(Button button); void onTick(); + ScreenId screenId() { return ScreenId::Home; }; + private: uint32_t showTime = 0; bool idle = false; diff --git a/src/lib/screen/manual.cpp b/src/lib/screen/manual.cpp index 18bd6b3..abf5366 100644 --- a/src/lib/screen/manual.cpp +++ b/src/lib/screen/manual.cpp @@ -3,8 +3,6 @@ void ManualScreen::onShow() { - //auto display = this->getDisplay(); - // TODO: implement ManualScreen } diff --git a/src/lib/screen/manual.h b/src/lib/screen/manual.h index 8efa894..627beed 100644 --- a/src/lib/screen/manual.h +++ b/src/lib/screen/manual.h @@ -1,6 +1,7 @@ #ifndef __screen_manual #define __screen_manual +#include "include/screenids.h" #include "../screen.h" #include "../Control.h" @@ -12,12 +13,14 @@ class ManualScreen : public BaseScreen { public: - ManualScreen(ScreenManager* screenManager) : BaseScreen(screenManager) { } + ManualScreen(ScreenManager* screenManager, Adafruit_GFX* display) : BaseScreen(screenManager, display) { } void onShow(); void onButton(Button button); void onTick(); + ScreenId screenId() { return ScreenId::Manual; }; + private: }; diff --git a/src/lib/screen/menu.cpp b/src/lib/screen/menu.cpp index f54e86f..ebed8fc 100644 --- a/src/lib/screen/menu.cpp +++ b/src/lib/screen/menu.cpp @@ -3,8 +3,6 @@ void MenuScreen::onShow() { - //auto display = this->getDisplay(); - // TODO: implement MenuScreen } diff --git a/src/lib/screen/menu.h b/src/lib/screen/menu.h index 454c4ec..96950ec 100644 --- a/src/lib/screen/menu.h +++ b/src/lib/screen/menu.h @@ -1,6 +1,7 @@ #ifndef __screen_menu #define __screen_menu +#include "include/screenids.h" #include "../screen.h" #include "../Control.h" @@ -13,12 +14,14 @@ class MenuScreen : public BaseScreen { public: - MenuScreen(ScreenManager* screenManager) : BaseScreen(screenManager) { } + MenuScreen(ScreenManager* screenManager, Adafruit_GFX* display) : BaseScreen(screenManager, display) { } void onShow(); void onButton(Button button); void onTick(); + ScreenId screenId() { return ScreenId::Menu; }; + private: }; diff --git a/src/lib/screen/move-overcurrent.cpp b/src/lib/screen/move-overcurrent.cpp index 26d3f59..d71b4f7 100644 --- a/src/lib/screen/move-overcurrent.cpp +++ b/src/lib/screen/move-overcurrent.cpp @@ -1,19 +1,17 @@ #include "./move-overcurrent.h" -void MoveOverCurrentScreen::onShow() +void MoveOvercurrentScreen::onShow() { - //auto display = this->getDisplay(); - - // TODO: implement MoveOverCurrentScreen + // TODO: implement MoveOvercurrentScreen } -void MoveOverCurrentScreen::onButton(Button button) +void MoveOvercurrentScreen::onButton(Button button) { } -void MoveOverCurrentScreen::onTick() +void MoveOvercurrentScreen::onTick() { } \ No newline at end of file diff --git a/src/lib/screen/move-overcurrent.h b/src/lib/screen/move-overcurrent.h index b9f4242..0264160 100644 --- a/src/lib/screen/move-overcurrent.h +++ b/src/lib/screen/move-overcurrent.h @@ -1,6 +1,7 @@ #ifndef __screen_move_overcurrent #define __screen_move_overcurrent +#include "include/screenids.h" #include "../screen.h" #include "../Control.h" @@ -9,15 +10,17 @@ * Move overcurrent screen * Displays a warning that the motor driver has reached the set maximum current. */ -class MoveOverCurrentScreen : public BaseScreen +class MoveOvercurrentScreen : public BaseScreen { public: - MoveOverCurrentScreen(ScreenManager* screenManager) : BaseScreen(screenManager) { } + MoveOvercurrentScreen(ScreenManager* screenManager, Adafruit_GFX* display) : BaseScreen(screenManager, display) { } void onShow(); void onButton(Button button); void onTick(); + ScreenId screenId() { return ScreenId::MoveOvercurrent; }; + private: }; diff --git a/src/lib/screen/move-sensorerror.cpp b/src/lib/screen/move-sensorerror.cpp index 94d4d39..7eaa9af 100644 --- a/src/lib/screen/move-sensorerror.cpp +++ b/src/lib/screen/move-sensorerror.cpp @@ -1,4 +1,5 @@ #include "./move-sensorerror.h" +#include "./home.h" #include "include/config.h" #include "include/metrics.h" #include "lib/control.h" @@ -7,15 +8,21 @@ void MoveSensorErrorScreen::onShow() { - auto display = this->getDisplay(); auto y = Metrics::LargeTextLineHeight + Metrics::LargeTextLineYOffset; - display->setFont(Metrics::LargeFont); - display->setTextColor(Config::ColorMoveErrorText); + this->display->fillScreen(Config::ColorErrorBackground); + + + this->display->setFont(Metrics::LargeFont); + this->display->setTextSize(Metrics::LargeFontTextSize); + + this->display->setTextColor(Config::ColorErrorText); this->printCentered("ERROR", y); y += Metrics::LargeTextLineHeight; - display->setFont(Metrics::SmallFont); + this->display->setFont(Metrics::SmallFont); + this->display->setTextSize(Metrics::SmallFontTextSize); + this->printCentered("height sensor failed", y); y += Metrics::SmallTextLineHeight; @@ -24,8 +31,14 @@ void MoveSensorErrorScreen::onShow() this->currentHeightY = y; + + this->display->setFont(Metrics::LargeFont); + this->display->setTextSize(Metrics::LargeFontTextSize); + this->lastRefresh = CurrentTime; - this->drawCurrentHeight(); + this->drawLastMeasurement(); + + Control.stabilizeStart(); } @@ -36,19 +49,28 @@ void MoveSensorErrorScreen::onButton(Button button) void MoveSensorErrorScreen::onTick() { + if (Control.stabilized()) + { + this->screenManager->show(); + return; + } + + if (CurrentTime - this->lastRefresh >= Config::DisplayRefreshRate) + { + this->drawLastMeasurement(); + this->lastRefresh = CurrentTime; + } } -void MoveSensorErrorScreen::drawCurrentHeight() +void MoveSensorErrorScreen::drawLastMeasurement() { - auto display = this->getDisplay(); - char currentHeightText[6]; - Control.getDisplayHeight(¤tHeightText[0], Control.getCurrentHeight()); + Control.getDisplayHeight(¤tHeightText[0], Control.getLastMeasurement()); if (this->lastTextWidth > 0) - display->fillRect((Config::DisplayWidth - this->lastTextWidth) / 2, this->currentHeightY, this->lastTextWidth, Metrics::LargeTextLineHeight, Config::ColorMoveBackground); + this->display->fillRect((Config::DisplayWidth - this->lastTextWidth) / 2, this->currentHeightY, this->lastTextWidth, Metrics::LargeTextLineHeight, Config::ColorErrorBackground); - display->setTextColor(Config::ColorMoveTarget); + this->display->setTextColor(Config::ColorMoveTarget); this->lastTextWidth = this->printCentered(¤tHeightText[0], this->currentHeightY + Metrics::LargeFontBaseline); } \ No newline at end of file diff --git a/src/lib/screen/move-sensorerror.h b/src/lib/screen/move-sensorerror.h index 4ef6311..8531bfa 100644 --- a/src/lib/screen/move-sensorerror.h +++ b/src/lib/screen/move-sensorerror.h @@ -1,6 +1,7 @@ #ifndef __screen_move_sensorerror #define __screen_move_sensorerror +#include "include/screenids.h" #include "../screen.h" #include "../Control.h" @@ -13,18 +14,20 @@ class MoveSensorErrorScreen : public BaseScreen { public: - MoveSensorErrorScreen(ScreenManager* screenManager) : BaseScreen(screenManager) { } + MoveSensorErrorScreen(ScreenManager* screenManager, Adafruit_GFX* display) : BaseScreen(screenManager, display) { } void onShow(); void onButton(Button button); void onTick(); + ScreenId screenId() { return ScreenId::MoveSensorError; }; + private: uint32_t lastRefresh; uint8_t currentHeightY; uint16_t lastTextWidth = 0; - void drawCurrentHeight(); + void drawLastMeasurement(); }; #endif diff --git a/src/lib/screen/move.cpp b/src/lib/screen/move.cpp index 3f62c5f..9fb8440 100644 --- a/src/lib/screen/move.cpp +++ b/src/lib/screen/move.cpp @@ -9,25 +9,28 @@ void MoveScreen::onShow() { - auto display = this->getDisplay(); auto startY = Metrics::LargeTextLineHeight; auto arrowY = startY + Metrics::LargeTextLineHeight + Metrics::LargeTextLineVArrowYOffset; auto arrowX = (Config::DisplayWidth - Metrics::VArrowWidth) / 2; auto stopY = Config::DisplayHeight - Metrics::LargeTextLineHeight; - display->fillScreen(Config::ColorMoveBackground); + this->display->fillScreen(Config::ColorMoveBackground); // Stop - display->setFont(Metrics::SmallFont); - display->setTextColor(Config::ColorMoveStop); + this->display->setFont(Metrics::SmallFont); + this->display->setTextSize(Metrics::SmallFontTextSize); + + this->display->setTextColor(Config::ColorMoveStop); this->printCentered("Press any button to", stopY - Metrics::SmallTextLineHeight); - display->setFont(Metrics::LargeFont); + this->display->setFont(Metrics::LargeFont); + this->display->setTextSize(Metrics::LargeFontTextSize); + this->printCentered("STOP", stopY + Metrics::LargeTextLineYOffset); char targetHeightText[6]; Control.getDisplayHeight(&targetHeightText[0], Control.getMoveTarget()); - display->setTextColor(Config::ColorMoveCurrent); + this->display->setTextColor(Config::ColorMoveCurrent); // Target and arrow if (Control.getMoveDirection() == MoveDirection::Up) @@ -53,32 +56,37 @@ void MoveScreen::onShow() void MoveScreen::onButton(Button button) { Control.moveStop(); - this->getScreenManager()->show(); + this->screenManager->show(); } void MoveScreen::onTick() { + bool isStabilizing = Control.getIsStabilizing(); + // Don't update every tick, monitoring the current height is more // important and the flicker would be unpleasant as well. - if (CurrentTime - this->lastRefresh >= Config::DisplayMoveRefreshRate) + if (this->lastIsStabilizing != isStabilizing || CurrentTime - this->lastRefresh >= Config::DisplayRefreshRate) { this->drawCurrentHeight(); this->lastRefresh = CurrentTime; + this->lastIsStabilizing = isStabilizing; } } void MoveScreen::drawCurrentHeight() { - auto display = this->getDisplay(); - char currentHeightText[6]; - Control.getDisplayHeight(¤tHeightText[0], Control.getCurrentHeight()); + + if (Control.getIsStabilizing()) + Control.getDisplayHeight(¤tHeightText[0], 0); + else + Control.getDisplayHeight(¤tHeightText[0], Control.getCurrentHeight()); if (this->lastTextWidth > 0) - display->fillRect((Config::DisplayWidth - this->lastTextWidth) / 2, this->currentHeightY, this->lastTextWidth, Metrics::LargeTextLineHeight, Config::ColorMoveBackground); + this->display->fillRect((Config::DisplayWidth - this->lastTextWidth) / 2, this->currentHeightY, this->lastTextWidth, Metrics::LargeTextLineHeight, Config::ColorMoveBackground); - display->setTextColor(Config::ColorMoveTarget); - this->lastTextWidth = this->printCentered(¤tHeightText[0], this->currentHeightY + Metrics::LargeFontBaseline); + this->display->setTextColor(Config::ColorMoveTarget); + this->lastTextWidth = this->printCentered(¤tHeightText[0], this->currentHeightY + Metrics::LargeTextLineYOffset); } \ No newline at end of file diff --git a/src/lib/screen/move.h b/src/lib/screen/move.h index 5acc83c..ba4f15b 100644 --- a/src/lib/screen/move.h +++ b/src/lib/screen/move.h @@ -1,6 +1,7 @@ #ifndef __screen_move #define __screen_move +#include "include/screenids.h" #include "../screen.h" #include "../Control.h" @@ -12,16 +13,19 @@ class MoveScreen : public BaseScreen { public: - MoveScreen(ScreenManager* screenManager) : BaseScreen(screenManager) { } + MoveScreen(ScreenManager* screenManager, Adafruit_GFX* display) : BaseScreen(screenManager, display) { } void onShow(); void onButton(Button button); void onTick(); + ScreenId screenId() { return ScreenId::Move; }; + private: uint32_t lastRefresh; uint8_t currentHeightY; uint16_t lastTextWidth = 0; + bool lastIsStabilizing = true; void drawCurrentHeight(); }; diff --git a/src/lib/settings.h b/src/lib/settings.h index 0715721..0318642 100644 --- a/src/lib/settings.h +++ b/src/lib/settings.h @@ -6,7 +6,9 @@ struct SettingsHeights { - uint8_t Offset; + uint16_t Offset; + uint16_t Minimum; + uint16_t Maximum; uint16_t Preset[2]; }; diff --git a/src/lib/vl53l0x.cpp b/src/lib/vl53l0x.cpp index 3aa438a..25f89cc 100644 --- a/src/lib/vl53l0x.cpp +++ b/src/lib/vl53l0x.cpp @@ -1,5 +1,6 @@ // Slightly modified version of https://github.com/pololu/vl53l0x-arduino -// which returns an error code if initialization fails. +// which returns an error code if initialization fails, and allows for +// asynchronous readings. // Most of the functionality of this library is based on the VL53L0X API // provided by ST (STSW-IMG005), and some of the explanatory comments are quoted @@ -866,6 +867,89 @@ uint16_t VL53L0X::readRangeSingleMillimeters(void) return readRangeContinuousMillimeters(); } + +// Asynchronous reading - call asyncStartReadSingle to begin a measurement, +// then call asyncReadSingle in the loop to determine if a measurement is +// available. This will return VL53L0XRangeNotReady if not ready, or VL53L0XRangeTimeout if a timeout occured. +void VL53L0X::asyncStartReadRangeSingleMillimeters() +{ + if (asyncReading()) + return; + + async_state = VL53L0XAsyncStateWaitingForStart; + writeReg(0x80, 0x01); + writeReg(0xFF, 0x01); + writeReg(0x00, 0x00); + writeReg(0x91, stop_variable); + writeReg(0x00, 0x01); + writeReg(0xFF, 0x00); + writeReg(0x80, 0x00); + + writeReg(SYSRANGE_START, 0x01); + + // "Wait until start bit has been cleared" + startTimeout(); +} + + +uint16_t VL53L0X::asyncReadRangeSingleMillimeters() +{ + if (async_state == VL53L0XAsyncStateWaitingForStart) + { + if (readReg(SYSRANGE_START) & 0x01) + { + if (checkTimeoutExpired()) + { + did_timeout = true; + async_state = VL53L0XAsyncStateNotStarted; + return VL53L0XRangeTimeout; + } + + return VL53L0XRangeNotReady; + } + else + { + startTimeout(); + async_state = VL53L0XAsyncStateWaitingForStatus; + } + } + + if (async_state == VL53L0XAsyncStateWaitingForStatus) + { + if ((readReg(RESULT_INTERRUPT_STATUS) & 0x07) == 0) + { + if (checkTimeoutExpired()) + { + did_timeout = true; + async_state = VL53L0XAsyncStateNotStarted; + return VL53L0XRangeTimeout; + } + + return VL53L0XRangeNotReady; + } + else + { + // assumptions: Linearity Corrective Gain is 1000 (default); + // fractional ranging is not enabled + uint16_t range = readReg16Bit(RESULT_RANGE_STATUS + 10); + writeReg(SYSTEM_INTERRUPT_CLEAR, 0x01); + + async_state = VL53L0XAsyncStateNotStarted; + return range; + } + } + + return VL53L0XRangeNotStarted; +} + + +// Indicates whether an async read has started, but not yet finished by +// calling asyncReadRangleSingleMillimeters. +bool VL53L0X::asyncReading() +{ + return (async_state != VL53L0XAsyncStateNotStarted); +} + // Did a timeout occur in one of the read functions since the last call to // timeoutOccurred()? bool VL53L0X::timeoutOccurred() diff --git a/src/lib/vl53l0x.h b/src/lib/vl53l0x.h index 57483de..c752775 100644 --- a/src/lib/vl53l0x.h +++ b/src/lib/vl53l0x.h @@ -9,14 +9,23 @@ enum class VL53L0XInitResult { - Success = 0, - InvalidIdentification = 1, - GetSpadInfoFailed = 2, - VHVCalibrationFailed = 3, - PhaseCalibrationFailed = 4, + Success, + InvalidIdentification, + GetSpadInfoFailed, + VHVCalibrationFailed, + PhaseCalibrationFailed, }; +static const uint16_t VL53L0XRangeNotStarted = 65533; +static const uint16_t VL53L0XRangeNotReady = 65534; +static const uint16_t VL53L0XRangeTimeout = 65535; + +static const uint8_t VL53L0XAsyncStateNotStarted = 0; +static const uint8_t VL53L0XAsyncStateWaitingForStart = 1; +static const uint8_t VL53L0XAsyncStateWaitingForStatus = 2; + + class VL53L0X { public: @@ -141,6 +150,10 @@ class VL53L0X uint16_t readRangeContinuousMillimeters(void); uint16_t readRangeSingleMillimeters(void); + void asyncStartReadRangeSingleMillimeters(); + uint16_t asyncReadRangeSingleMillimeters(); + bool asyncReading(); + inline void setTimeout(uint16_t timeout) { io_timeout = timeout; } inline uint16_t getTimeout(void) { return io_timeout; } bool timeoutOccurred(void); @@ -171,6 +184,8 @@ class VL53L0X uint8_t stop_variable; // read by init and used when starting measurement; is StopVariable field of VL53L0X_DevData_t structure in API uint32_t measurement_timing_budget_us; + uint8_t async_state = VL53L0XAsyncStateNotStarted; + bool getSpadInfo(uint8_t * count, bool * type_is_aperture); void getSequenceStepEnables(SequenceStepEnables * enables); diff --git a/src/main.cpp b/src/main.cpp index ed69d48..505e9d8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -4,6 +4,7 @@ #include #include "./include/config.h" +#include "./include/screenids.h" #include "./lib/debug.h" #include "./lib/settings.h" #include "./lib/screen.h" @@ -21,8 +22,8 @@ enum class InitSequenceStep { EEPROM = 0, - HeightSensorInit = 1, - HeightSensorTest = 2, + HeightSensorInit, + HeightSensorTest, Last = HeightSensorTest }; @@ -30,7 +31,7 @@ enum class InitSequenceStep // Forward declarations inline void setupHeightSensor(); -inline uint16_t testHeightSensor(); +inline void testHeightSensor(); void initSequenceStart(); void initSequenceSuccess(InitSequenceStep step); @@ -45,6 +46,7 @@ auto screenManager = ScreenManager(&display); Bounce buttons[3]; + /* Setup @@ -53,11 +55,11 @@ Bounce buttons[3]; void setup() { DebugInit(); - dln("Debug log active"); + dln("[ MAIN ] Debug log started"); - buttons[0].attach(Config::ButtonPinUp, INPUT_PULLUP); - buttons[1].attach(Config::ButtonPinMenu, INPUT_PULLUP); - buttons[2].attach(Config::ButtonPinDown, INPUT_PULLUP); + buttons[0].attach(Config::ButtonPinTop, INPUT_PULLUP); + buttons[1].attach(Config::ButtonPinMiddle, INPUT_PULLUP); + buttons[2].attach(Config::ButtonPinBottom, INPUT_PULLUP); display.init(Config::DisplayWidth, Config::DisplayHeight, SPI_MODE3); display.setRotation(Config::DisplayRotation); @@ -73,8 +75,10 @@ void setup() // Initialize VL53L0X sensor + CurrentTime = millis(); + setupHeightSensor(); - auto currentHeight = testHeightSensor(); + testHeightSensor(); initSequenceEnd(); @@ -84,7 +88,6 @@ void setup() if (initialized) { -// Control.getCurrentHeight() = currentHeight; Control.snapToPreset(); screenManager.show(); @@ -135,8 +138,26 @@ inline void setupHeightSensor() -inline uint16_t testHeightSensor() +inline void testHeightSensor() { + Control.stabilizeStart(); + + while (true) + { + if (Control.update() != ControlUpdateResult::SensorError) + { + initSequenceDisplayHeight(Control.getCurrentHeight()); + + if (Control.stabilized()) + break; + } + else + initSequenceDisplayHeight(0); + + CurrentTime = millis(); + } + + initSequenceSuccess(InitSequenceStep::HeightSensorTest); } @@ -158,11 +179,11 @@ void loop() break; case ControlUpdateResult::SensorError: - screenManager.show(); + screenManager.show(ScreenId::MoveSensorError); break; - case ControlUpdateResult::OverCurrent: - screenManager.show(); + case ControlUpdateResult::Overcurrent: + screenManager.show(ScreenId::MoveOvercurrent); break; default: @@ -174,21 +195,28 @@ void loop() buttons[1].update(); buttons[2].update(); + if (buttons[0].fell()) + { + dln("[ MAIN ] Button pressed: Top"); + screenManager.button(Button::Top); + } - if (buttons[0].rose()) - screenManager.button(Button::Up); + if (buttons[1].fell()) + { + dln("[ MAIN ] Button pressed: Middle"); + screenManager.button(Button::Middle); + } - if (buttons[1].rose()) - screenManager.button(Button::Menu); - - if (buttons[2].rose()) - screenManager.button(Button::Down); + if (buttons[2].fell()) + { + dln("[ MAIN ] Button pressed: Bottom"); + screenManager.button(Button::Bottom); + } screenManager.tick(); } - /* Helper functions for the status display during the initialization sequence