Created
December 24, 2022 04:32
-
-
Save philharlow/15892ba692e528348d1220a8a3a21597 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include "ESPHASDevice.h" | |
#include <Encoder.h> | |
#include <AccelStepper.h> | |
const String DEVICETYPE = "curtainopener"; // lowercase | |
const String VERSION = "v0.35"; | |
const String EEPROMVERSION = "v0.10"; | |
ESPHASDevice device; | |
EEPROMInt* currentPosition = device.AddSettingInt("currentPosition", 0); | |
EEPROMInt* openPosition = device.AddSettingInt("openPosition", 400); | |
EEPROMInt* stepsToEncoder = device.AddSettingInt("stepsToEncoder", 40); | |
EEPROMInt* maxSpeed = device.AddSettingInt("maxSpeed", 5000); | |
EEPROMInt* acceleration = device.AddSettingInt("acceleration", 3000); | |
EEPROMInt* minPulseWidth = device.AddSettingInt("minPulseWidth", 1); | |
EEPROMInt* quickUpDownThreshold = device.AddSettingInt("quickUpDownThreshold", 300); | |
EEPROMInt* programmingThreshold = device.AddSettingInt("programmingThreshold", 1000); | |
EEPROMInt* stallThreshold = device.AddSettingInt("stallThreshold", 5); | |
#define enablePin D0 | |
#define dirPin D6 | |
#define stepPin D7 | |
// Update me! | |
#define openButtonPin D4 | |
#define closeButtonPin D5 | |
#define ledPin D8 | |
Encoder myEnc(D1, D2); | |
AccelStepper stepper(1, stepPin, dirPin); | |
bool motorEnabled = false; | |
int motorCooldown = 0; | |
int targetPosition = 0; | |
int saveSettingsIn = 0; | |
int startingPositionOffset = 0; | |
int encoderPos = 0; | |
int lastEncoderPos = encoderPos; | |
bool inProgrammingMode = false; | |
int singleButtonHeldStartedAt = 0; | |
int bothButtonsHeldStartedAt = 0; | |
bool waitingForDepress = false; | |
int lastTugPosition = 0; | |
int unsetPosition = -100000; | |
int position1 = unsetPosition; | |
int position2 = unsetPosition; | |
int stalledTicks = 0; | |
//int connectingBlinkInterval = 3; | |
int programmingModeBlinkInterval = 5; | |
long ledCount = 0; | |
void updateLED(uint64_t now = 0) | |
{ | |
bool ledOn = false; | |
if (inProgrammingMode) | |
{ | |
ledOn = waitingForDepress || ledCount >= programmingModeBlinkInterval; | |
if (ledCount++ > programmingModeBlinkInterval * 2) | |
ledCount = 0; | |
} else { | |
ledOn = motorEnabled; | |
ledCount = 0; | |
} | |
analogWrite(ledPin, ledOn ? 100 : 0); | |
} | |
void sendStatus() { | |
int openPerc = 100 * clampf((float)targetPosition/(float)openPosition->Value, 0, 1); | |
device.publish(device.deviceName + "/open/status", openPerc > 0 ? "1" : "0"); | |
device.publish(device.deviceName + "/brightness/status", String(openPerc)); | |
} | |
void move(int delta, bool constrain = true) | |
{ | |
int32 position = currentPosition->Value + delta; | |
if (constrain) | |
position = clamp(position, 0, openPosition->Value); | |
if (currentPosition->Value == position || targetPosition == position) | |
return; | |
delta = position - currentPosition->Value; | |
if (motorEnabled == false) | |
stepper.moveTo(stepper.currentPosition() + delta * stepsToEncoder->Value); | |
else | |
stepper.setTarget(stepper.currentPosition() + delta * stepsToEncoder->Value); | |
targetPosition = position; | |
motorEnabled = true; | |
stalledTicks = 0; | |
} | |
void setTargetPosition(int position, bool constrain = true) | |
{ | |
if (constrain) | |
position = clamp(position, 0, openPosition->Value); | |
Serial.println("setTargetPosition position: " + String(position) + " currentPosition->Value: " + String(currentPosition->Value) + " targetPosition: " + String(targetPosition)); | |
if (currentPosition->Value == position || targetPosition == position) | |
return; | |
targetPosition = position; | |
motorEnabled = true; | |
stalledTicks = 0; | |
Serial.println("setTargetPosition: " + String(targetPosition)); | |
int delta = position - currentPosition->Value; | |
stepper.moveTo(stepper.currentPosition() + delta * stepsToEncoder->Value); | |
sendStatus(); | |
} | |
void onMQTTConnected() { | |
sendStatus(); | |
} | |
void onSettingsChanged() { | |
Serial.println("onSettingsChanged"); | |
stepper.setMinPulseWidth(minPulseWidth->Value); | |
stepper.setMaxSpeed(maxSpeed->Value); | |
stepper.setAcceleration(acceleration->Value); | |
// Dont move on startup | |
//setTargetPosition(currentPosition->Value); | |
} | |
void setup() { | |
Serial.begin(115200); | |
pinMode(enablePin, OUTPUT); | |
digitalWrite(enablePin, HIGH); | |
pinMode(dirPin, OUTPUT); | |
pinMode(stepPin, OUTPUT); | |
pinMode(openButtonPin, INPUT_PULLUP); | |
pinMode(closeButtonPin, INPUT_PULLUP); | |
pinMode(ledPin, OUTPUT); | |
updateLED(); | |
device.init(DEVICETYPE, VERSION, EEPROMVERSION); | |
device.subscribe(device.deviceName + "/open", handleOpen); | |
device.subscribe(device.deviceName + "/brightness", handleBrightness); | |
device.subscribe(device.deviceName + "/close", handleClose); | |
device.createTimer(100, true, stallCheck); | |
device.createTimer(100, true, updateLED); | |
device.onSettingsChanged = onSettingsChanged; | |
device.onMQTTConnected = onMQTTConnected; | |
delay(100); | |
bool openButtonPressed = digitalRead(openButtonPin) == LOW; | |
bool closeButtonPressed = digitalRead(closeButtonPin) == LOW; | |
if (openButtonPressed && closeButtonPressed) | |
device.resetSettings(); | |
Serial.println("starting up: open: " + String(openButtonPressed ? "true" : "false") + String(" closed: ") + String(closeButtonPressed ? "true" : "false")); | |
//device.subscribe(DEVICETYPE + "/" + device.deviceName + "/" + displayTopic, handleDisplay); | |
encoderPos = 0; | |
startingPositionOffset = currentPosition->Value; | |
targetPosition = currentPosition->Value; | |
lastTugPosition = currentPosition->Value; | |
Serial.println("starting up: startingPositionOffset: " + String(startingPositionOffset)); | |
//stepper.setEnablePin(enablePin); | |
onSettingsChanged(); | |
stepper.setPinsInverted(true,true); | |
} | |
void disableMotor() { | |
Serial.println("motor disabled"); | |
motorEnabled = false; | |
stalledTicks = 0; | |
targetPosition = currentPosition->Value; | |
lastTugPosition = currentPosition->Value; | |
motorCooldown = 10; | |
} | |
int clamp(int input, int minV, int maxV) | |
{ | |
return max(minV, min(maxV, input)); | |
} | |
float clampf(float input, float minV, float maxV) | |
{ | |
return max(minV, min(maxV, input)); | |
} | |
void handleOpen(String topic, String payload) { | |
payload.toLowerCase(); | |
bool boolVal = payload == "1" || payload == "on"; | |
float pos = boolVal ? 1 : 0; | |
setOpenPercentage(pos); | |
} | |
void handleBrightness(String topic, String payload) { | |
payload.toLowerCase(); | |
float floatVal = clampf(payload.toInt(), 0, 100) / 100.0f; | |
setOpenPercentage(floatVal); | |
} | |
void handleClose(String topic, String payload) { | |
setTargetPosition(0); | |
} | |
// every 250 ms | |
void stallCheck(uint64_t now) { | |
int moveThreshold = 2;//_max(3, abs(stepper.speed() * 0.002f)); | |
int32 delta = encoderPos - lastEncoderPos; | |
bool opening = targetPosition > currentPosition->Value; | |
bool moved = opening ? delta >= moveThreshold : delta <= -moveThreshold; | |
//Serial.println("delta > threshold: " + String(delta) + " > " + String(moveThreshold)); | |
if (inProgrammingMode == false) { | |
// motor is on | |
if (motorEnabled) { | |
if (moved) { | |
stalledTicks = 0; | |
//Serial.println("Moving!!"); | |
} else { | |
stalledTicks++; | |
Serial.println("Stalling!! " + String(stalledTicks)); | |
if (stalledTicks > stallThreshold->Value) { | |
Serial.println("Stall detected!!"); | |
disableMotor(); | |
motorCooldown = 50; | |
stepper.stop(); | |
} | |
} | |
} else { // motors is off | |
bool moved = abs(delta) >= moveThreshold; | |
if (moved && motorCooldown == 0) { | |
//Serial.println("moved encoderPos: " + String(encoderPos) + " currentPosition->Value" + String(currentPosition->Value)); | |
int tugAmount = currentPosition->Value - lastTugPosition; | |
int tugThreshold = 3; | |
if (abs(tugAmount) >= tugThreshold) { | |
if (tugAmount > 0) { | |
Serial.println("opening from tug"); | |
setOpenPercentage(1); | |
} else { | |
Serial.println("closing from tug"); | |
setOpenPercentage(0); | |
} | |
lastTugPosition = currentPosition->Value; | |
} | |
} | |
} | |
} | |
if (moved) | |
lastEncoderPos = encoderPos; | |
if (motorCooldown > 0) { | |
motorCooldown--; | |
if (motorCooldown == 0) | |
lastTugPosition = currentPosition->Value; | |
} | |
} | |
void loop() | |
{ | |
encoderPos = myEnc.read(); | |
currentPosition->Value = encoderPos + startingPositionOffset; | |
stepper.run(); | |
device.CanDoBlockingWork = motorEnabled == false; | |
device.loop(); | |
handleButtons(); | |
if (motorEnabled && stepper.distanceToGo() == 0) { | |
Serial.println("Reached end! currentPosition->Value: " + String(currentPosition->Value) + " stepper.currentPosition()/stepsToEncoder " + String(stepper.currentPosition() / stepsToEncoder->Value)); | |
disableMotor(); | |
} | |
if (inProgrammingMode) | |
saveSettingsIn = 0; | |
else if (motorEnabled) { | |
saveSettingsIn = 100000; | |
} | |
digitalWrite(enablePin, motorEnabled ? LOW :HIGH); | |
if (saveSettingsIn > 0) | |
{ | |
saveSettingsIn--; | |
if (saveSettingsIn == 0) { | |
sendStatus(); | |
device.SaveSettings(); | |
} | |
} | |
} | |
bool wasOpenButtonPressed = false; | |
bool wasCloseButtonPressed = false; | |
void handleButtons() { | |
unsigned long now = millis(); | |
bool openButtonPressed = digitalRead(openButtonPin) == LOW; | |
bool closeButtonPressed = digitalRead(closeButtonPin) == LOW; | |
int pressedCount = openButtonPressed + closeButtonPressed; | |
if (pressedCount == 1) { | |
bothButtonsHeldStartedAt = 0; | |
if (singleButtonHeldStartedAt == 0) { | |
singleButtonHeldStartedAt = now; | |
//Serial.println("down " + String(openButtonPressed ? "open" : "close")); | |
} | |
unsigned long delta = now - singleButtonHeldStartedAt; | |
unsigned long threshold = quickUpDownThreshold->Value; | |
//Serial.println("pressed single button for: " + String(delta) + " threshold: " + String(threshold)); | |
bool buttonHeld = delta >= threshold; | |
// Held, manually move | |
if (buttonHeld && waitingForDepress == false) { | |
int adjustedDelta = _max(delta - threshold, 0); | |
//Serial.println("delta: " + String(delta) + " adjustedDelta: " + String(adjustedDelta)); | |
adjustedDelta /= 50; | |
int moveAmount = clamp(adjustedDelta, 5, 50); | |
//Serial.println("held or programming. adjustedDelta: " + String(adjustedDelta) + " moveAmount: " + String(moveAmount)); | |
if (openButtonPressed) | |
move(moveAmount, inProgrammingMode == false); | |
if (closeButtonPressed) | |
move(-moveAmount, inProgrammingMode == false); | |
} | |
} | |
else if (pressedCount == 2) | |
{ | |
stepper.stop(); | |
singleButtonHeldStartedAt = 0; | |
if (bothButtonsHeldStartedAt == 0) { | |
bothButtonsHeldStartedAt = now; | |
} | |
unsigned long delta = now - bothButtonsHeldStartedAt; | |
if (delta > programmingThreshold->Value && waitingForDepress == false) { | |
waitingForDepress = true; | |
if (inProgrammingMode) { | |
Serial.println("programming mode tap"); | |
// save first pos | |
if (position1 == unsetPosition) | |
{ | |
Serial.print("Setting position 1 to: "); | |
Serial.println(currentPosition->Value); | |
position1 = encoderPos;// settings.currentPosition->Value; | |
} | |
else // second pos | |
{ | |
Serial.print("Setting position 2 to: "); | |
Serial.println(currentPosition->Value); | |
position2 = encoderPos;// settings.currentPosition->Value; | |
// settings.currentPosition->Value = encoderPos + startingPositionOffset; | |
// now closed, pos1 = top/open | |
if (position1 > position2) | |
{ | |
openPosition->Value = position1 - position2; | |
currentPosition->Value = 0; | |
} | |
// now open, pos2 = top/open | |
else | |
{ | |
openPosition->Value = position2 - position1; | |
currentPosition->Value = openPosition->Value; | |
} | |
startingPositionOffset = currentPosition->Value - encoderPos; | |
targetPosition = currentPosition->Value; | |
lastTugPosition = currentPosition->Value; | |
Serial.print("Saving new settings! openPosition: "); | |
Serial.println(openPosition->Value); | |
inProgrammingMode = false; | |
device.SaveSettings(); | |
} | |
} else { | |
inProgrammingMode = true; | |
position1 = unsetPosition; | |
position2 = unsetPosition; | |
Serial.println("starting programming mode"); | |
} | |
bothButtonsHeldStartedAt = 0; | |
} | |
} | |
else { | |
// On release, check for short press | |
if (inProgrammingMode == false && singleButtonHeldStartedAt > 0) { | |
unsigned long delta = now - singleButtonHeldStartedAt; | |
unsigned long threshold = quickUpDownThreshold->Value; | |
//Serial.println("pressed single button for: " + String(delta) + " threshold: " + String(threshold)); | |
bool buttonHeld = delta >= threshold; | |
// Held, manually move | |
if (buttonHeld == false && waitingForDepress == false) { | |
if (wasOpenButtonPressed) { | |
Serial.println("opening from button"); | |
setOpenPercentage(1); | |
} | |
if (wasCloseButtonPressed) { | |
Serial.println("closing from button"); | |
setOpenPercentage(0); | |
} | |
} | |
} | |
singleButtonHeldStartedAt = 0; | |
bothButtonsHeldStartedAt = 0; | |
waitingForDepress = false; | |
} | |
wasOpenButtonPressed = openButtonPressed; | |
wasCloseButtonPressed = closeButtonPressed; | |
} | |
void setOpenPercentage(float percentage) | |
{ | |
int newTarget = percentage * (float)openPosition->Value; | |
newTarget = clamp(newTarget, 0, openPosition->Value); | |
if (currentPosition->Value < 0 && newTarget == 0) | |
return; | |
if (currentPosition->Value > openPosition->Value && newTarget == openPosition->Value) | |
return; | |
setTargetPosition(newTarget); | |
//Serial.print("newTarget: "); | |
//Serial.println(newTarget); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment