Stairs/src/main.cpp
2018-01-14 21:31:52 +01:00

584 lines
13 KiB
C++

/*
* Stairs lighting
* Copyright 2017 (c) Mark van Renswoude
*
* https://git.x2software.net/pub/Stairs
*/
#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <NTPClient.h>
#include <WiFiUdp.h>
#include <TimeLib.h>
extern "C" {
#include <user_interface.h>
}
#include "./config.h"
#include "./debug.h"
#include "./global.h"
#include "./components/PCA9685.h"
#include "./settings/connection.h"
#include "./server/static.h"
#include "./server/settings.h"
#include "./server/firmware.h"
#include "./server/api.h"
ADC_MODE(ADC_VCC);
// Forward declarations
void initWiFi();
void initMotionPins();
#ifdef SerialDebug
void wifiEvent(WiFiEvent_t event);
void updateDebugStatus();
#endif
void updateLED();
void updateNTPClient();
void checkTriggers();
void handleNotFound(AsyncWebServerRequest* request);
AsyncWebServer server(80);
PCA9685* pwmDriver;
WiFiUDP ntpUDP;
NTPClient* ntpClient = nullptr;
bool accessPoint = false;
bool stationMode = false;
bool forceAccessPoint = false;
uint32_t stationModeStart = 0;
uint32_t blinkOnTime = 0;
void setup()
{
_dinit();
currentTime = millis();
blinkOnTime = currentTime;
if (!SPIFFS.begin())
_dln("Setup :: failed to mount file system");
connectionSettings->read();
systemSettings->read();
stepsSettings->read();
timeTriggerSettings->read();
motionTriggerSettings->read();
pinMode(systemSettings->pinAPButton(), INPUT_PULLUP);
pinMode(systemSettings->pinLEDAP(), OUTPUT);
pinMode(systemSettings->pinLEDSTA(), OUTPUT);
initMotionPins();
_dln("Setup :: initializing PCA9685");
pwmDriver = new PCA9685();
pwmDriver->setAddress(systemSettings->pwmDriverAddress(), systemSettings->pinPWMDriverSDA(), systemSettings->pinPWMDriverSCL());
pwmDriver->setPWMFrequency(systemSettings->pwmDriverFrequency());
pwmDriver->setAll(0);
_dln("Setup :: initializing Stairs");
stairs = new Stairs();
stairs->init(pwmDriver);
_dln("Setup :: starting initialization sequence");
stairs->set(0, 255);
delay(300);
uint8_t stepCount = stepsSettings->count();
for (int step = 1; step < stepCount; step++)
{
stairs->set(step - 1, 0);
stairs->set(step, 255);
delay(300);
}
stairs->set(stepCount - 1, 0);
_dln("Setup :: initializing WiFi");
WiFi.persistent(false);
WiFi.mode(WIFI_OFF);
#ifdef SerialDebug
// onEvent is already deprecated, but since I'm only using it
// for debug purposes we'll see how long it lasts...
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
WiFi.onEvent(wifiEvent);
_d("WiFi :: MAC address: ");
_dln(WiFi.macAddress());
#pragma GCC diagnostic pop
#endif
initWiFi();
_dln("Setup :: registering routes");
registerStaticRoutes(&server);
registerSettingsRoutes(&server);
registerFirmwareRoutes(&server);
registerAPIRoutes(&server);
_dln("Setup :: starting HTTP server");
server.onNotFound(handleNotFound);
server.begin();
}
void loop()
{
if (shouldReboot || systemSettingsChanged)
{
_dln("Loop :: reboot requested, so long and thanks for all the fish!");
delay(100);
ESP.restart();
}
if (motionTriggerSettingsChanged)
{
initMotionPins();
motionTriggerSettingsChanged = false;
}
currentTime = millis();
#ifdef SerialDebug
updateDebugStatus();
#endif
if (connectionSettingsChanged)
{
_dln("Loop :: connection settings changed");
initWiFi();
connectionSettingsChanged = false;
}
if (stationModeStart > 0)
{
bool isConnected = WiFi.status() == WL_CONNECTED;
if (isConnected)
{
_d("WiFi :: connected, IP address: ");
_dln(WiFi.localIP());
stationModeStart = 0;
}
else if (stationMode && accessPoint &&
currentTime - stationModeStart >= StationModeTimeout)
{
_dln("WiFi :: unable to connect, switching off station mode, status:");
_dln(WiFi.status());
#ifdef SerialDebug
WiFi.printDiag(Serial);
#endif
// Connecting to access point is taking too long and is blocking
// the access point mode, stop trying
stationMode = false;
WiFi.disconnect();
WiFi.mode(WIFI_AP);
}
}
// TODO check AP button
updateLED();
updateNTPClient();
checkTriggers();
stairs->tick();
}
void initWiFi()
{
WiFi.disconnect();
WiFi.softAPdisconnect();
accessPoint = connectionSettings->flag(AccessPoint) || forceAccessPoint;
stationMode = connectionSettings->flag(StationMode) && connectionSettings->ssid() != nullptr;
WiFi.mode(accessPoint && stationMode ? WIFI_AP_STA :
accessPoint ? WIFI_AP :
stationMode ? WIFI_STA :
WIFI_OFF);
if (accessPoint)
{
_dln("WiFi :: starting access point");
String ssidString = DefaultAPSSIDPrefix + String(ESP.getChipId(), HEX);
if (WiFi.softAP((const char *)ssidString.c_str()))
{
_d("WiFi :: IP address: ");
_dln(WiFi.softAPIP());
}
else
_d("WiFi :: failed to start soft access point");
}
if (stationMode)
{
_d("WiFi :: starting station mode to: ");
_dln(connectionSettings->ssid());
stationModeStart = currentTime;
if (connectionSettings->hostname() != nullptr)
WiFi.hostname(connectionSettings->hostname());
if (WiFi.begin(connectionSettings->ssid(), connectionSettings->password()))
{
if (connectionSettings->flag(DHCP))
// I've had the same issue as described here with config(0, 0, 0):
// https://stackoverflow.com/questions/40069654/how-to-clear-static-ip-configuration-and-start-dhcp
wifi_station_dhcpc_start();
else
WiFi.config(connectionSettings->ip(), connectionSettings->gateway(), connectionSettings->subnetMask());
}
else
_d("WiFi :: failed to start station mode");
}
}
void initMotionPins()
{
if (!motionTriggerSettings->enabled())
return;
for (uint8_t i = 0; i < motionTriggerSettings->triggerCount(); i++)
{
MotionTrigger* trigger = motionTriggerSettings->trigger(i);
if (trigger->enabled)
pinMode(trigger->pin, INPUT);
}
}
#ifdef SerialDebug
void wifiEvent(WiFiEvent_t event)
{
switch (event)
{
case WIFI_EVENT_STAMODE_CONNECTED:
_dln("WiFi:: station mode: connected"); break;
case WIFI_EVENT_STAMODE_DISCONNECTED:
_dln("WiFi:: station mode: disconnected"); break;
case WIFI_EVENT_STAMODE_AUTHMODE_CHANGE:
_dln("WiFi:: station mode: authmode change"); break;
case WIFI_EVENT_STAMODE_GOT_IP:
_dln("WiFi:: station mode: got IP");
_dln(WiFi.localIP());
break;
case WIFI_EVENT_STAMODE_DHCP_TIMEOUT:
_dln("WiFi:: station mode: DHCP timeout"); break;
case WIFI_EVENT_SOFTAPMODE_STACONNECTED:
_dln("WiFi:: soft AP mode: station connected"); break;
case WIFI_EVENT_SOFTAPMODE_STADISCONNECTED:
_dln("WiFi:: soft AP mode: station disconnected"); break;
}
}
uint32_t debugStatusTime = 0;
void updateDebugStatus()
{
if (currentTime - debugStatusTime < 5000) return;
debugStatusTime = currentTime;
_d("Status :: available heap: ");
_dln(ESP.getFreeHeap());
if (ntpClient != nullptr)
{
_d("Status :: time: ");
uint32_t time = ntpClient->getEpochTime();
_d(day(time)); _d("-"); _d(month(time)); _d("-"); _d(year(time)); _d(" ");
_d(hour(time)); _d(":"); _d(minute(time)); _d(":"); _dln(second(time));
}
}
#endif
enum LEDState
{
Off,
BlinkLow,
BlinkHigh,
On
};
bool ledAP = false;
LEDState ledWiFi = Off;
void updateLED()
{
uint8_t value = (currentTime - blinkOnTime >= 1000) ? LOW : HIGH;
WiFiMode_t mode = WiFi.getMode();
if (mode == WIFI_AP_STA || mode == WIFI_AP)
{
if (!ledAP)
{
digitalWrite(systemSettings->pinLEDAP(), HIGH);
ledAP = true;
}
}
else
{
if (ledAP)
{
digitalWrite(systemSettings->pinLEDAP(), LOW);
ledAP = false;
}
}
if (mode == WIFI_AP_STA || mode == WIFI_STA)
{
wl_status_t status = WiFi.status();
if (status == WL_CONNECTED)
{
if (ledWiFi != On)
{
digitalWrite(systemSettings->pinLEDSTA(), HIGH);
ledWiFi = On;
}
}
else
{
LEDState expectedState = value == HIGH ? BlinkHigh : BlinkLow;
if (ledWiFi != expectedState)
{
digitalWrite(systemSettings->pinLEDSTA(), value);
ledWiFi = expectedState;
}
}
}
else
{
if (ledWiFi != Off)
{
digitalWrite(systemSettings->pinLEDSTA(), LOW);
ledWiFi = Off;
}
}
if (currentTime - blinkOnTime >= 2000)
blinkOnTime = currentTime;
}
void updateNTPClient()
{
if (ntpClient == nullptr && WiFi.status() == WL_CONNECTED)
{
_dln("NTP :: initializing NTP client");
// TODO make NTP address and refresh interval configurable
ntpClient = new NTPClient(ntpUDP, "nl.pool.ntp.org", 0, 5 * 60 * 1000);
ntpClient->begin();
}
if (ntpClient != nullptr)
ntpClient->update();
}
uint32_t lastTimeTriggerChecked = 0;
TimeTrigger* lastTimeTrigger = nullptr;
TimeTrigger* activeTimeTrigger = nullptr;
void updateTimeTrigger()
{
if (ntpClient == nullptr || !timeTriggerSettings->enabled())
{
activeTimeTrigger = nullptr;
return;
}
if (timeTriggerSettingsChanged)
{
// Time trigger settings changed, activeTimeTrigger pointer is considered
// invalid, force recheck
timeTriggerSettingsChanged = false;
}
else if (currentTime - lastTimeTriggerChecked < 10000)
return;
lastTimeTriggerChecked = currentTime;
_dln("Triggers:: updating time trigger");
uint32_t epochTime = ntpClient->getEpochTime();
if (epochTime == 0)
{
activeTimeTrigger = nullptr;
_dln("Triggers:: time not synchronised yet");
return;
}
// TODO apply timezone offset
tmElements_t time;
breakTime(epochTime, time);
activeTimeTrigger = timeTriggerSettings->getActiveTrigger(time);
#ifdef SerialDebug
_d("Triggers:: active time trigger: ");
if (activeTimeTrigger != nullptr)
_dln(activeTimeTrigger->time);
else
_dln("null");
#endif
}
uint32_t activeMotionStart = 0;
uint16_t activeMotionBrightness = 0;
MotionDirection activeMotionDirection = Nondirectional;
bool lastMotion = false;
void updateMotionTrigger()
{
if (!motionTriggerSettings->enabled() || !motionTriggerSettings->triggerCount())
{
activeMotionStart = 0;
return;
}
for (uint8_t i = 0; i < motionTriggerSettings->triggerCount(); i++)
{
MotionTrigger* trigger = motionTriggerSettings->trigger(i);
if (trigger->enabled && digitalRead(trigger->pin) == HIGH)
{
if (activeMotionStart == 0)
{
activeMotionDirection = trigger->direction;
activeMotionBrightness = trigger->brightness;
}
activeMotionStart = currentTime;
}
}
if (currentTime - activeMotionStart >= motionTriggerSettings->delay())
activeMotionStart = 0;
}
void checkTriggers()
{
if (!timeTriggerSettings->enabled() && activeTimeTrigger == nullptr &&
!motionTriggerSettings->enabled() && activeMotionStart == 0)
return;
updateTimeTrigger();
updateMotionTrigger();
bool inTimeTrigger = timeTriggerSettings->enabled() &&
activeTimeTrigger != nullptr &&
activeTimeTrigger->brightness;
bool timeTriggerChanged = activeTimeTrigger != lastTimeTrigger;
lastTimeTrigger = activeTimeTrigger;
bool inMotionTrigger = (activeMotionStart > 0) && (!inTimeTrigger || motionTriggerSettings->enabledDuringTimeTrigger());
bool motionChanged = (activeMotionStart > 0) != lastMotion;
lastMotion = (activeMotionStart > 0);
if (!motionChanged && !timeTriggerChanged)
return;
if (motionChanged)
{
if (inMotionTrigger)
{
_dln("Triggers :: start motion trigger");
if (activeMotionDirection == Nondirectional || motionTriggerSettings->transitionTime() == 0)
{
stairs->setAll(activeMotionBrightness, motionTriggerSettings->transitionTime(), 0);
}
else
{
// Start sweep
uint8_t stepsCount = stepsSettings->count();
uint16_t offsetIncrement = stepsCount > 0 ? (motionTriggerSettings->transitionTime() / stepsCount) * 1.5 : 0;
uint16_t offset = activeMotionDirection == TopDown ? 0 : (stepsCount - 1) * offsetIncrement;
for (uint8_t step = 0; step < stepsCount; step++)
{
stairs->set(step, activeMotionBrightness, motionTriggerSettings->transitionTime(), offset);
if (activeMotionDirection == TopDown)
offset += offsetIncrement;
else
offset -= offsetIncrement;
}
}
}
else
{
if (inTimeTrigger)
{
_dln("Triggers :: motion stopped, falling back to time trigger");
// Fall back to time trigger value
stairs->setAll(activeTimeTrigger->brightness, motionTriggerSettings->transitionTime(), 0);
}
else
{
_dln("Triggers :: motion stopped, turning off");
// No more motion, no active time trigger, turn off
stairs->setAll(0, motionTriggerSettings->transitionTime(), 0);
}
}
}
else if (timeTriggerChanged && !inMotionTrigger)
{
_dln("Triggers :: time trigger changed");
// Set to time trigger value
stairs->setAll(activeTimeTrigger->brightness, timeTriggerSettings->transitionTime(), 0);
}
}
void handleNotFound(AsyncWebServerRequest *request)
{
_d("HTTP :: not found: "); _dln(request->url());
request->send(404);
}