/** * @file main.cpp * * @brief Main source file for the Souko's ChainLube Mk1 ESP8266 project. * * This file includes necessary libraries, defines configuration options, and declares global variables * and function prototypes. It sets up essential components, initializes peripherals, and defines * callbacks for interrupt service routines (ISRs) and timers. The main setup function configures the * project, and the loop function handles the main execution loop, performing various tasks based on * the configured options. * * @author Marcel Peterkau * @date 09.01.2024 */ #include #include #ifdef FEATURE_ENABLE_OLED #include #endif #include #include #include #include #include "common.h" #include "sanitycheck.h" #include "lubeapp.h" #include "webui.h" #include "config.h" #include "globals.h" #include "debugger.h" #include "can.h" #include "gps.h" #include "dtc.h" #include "led_colors.h" #ifdef FEATURE_ENABLE_WIFI_CLIENT #include const char *ssid = QUOTE(WIFI_SSID_CLIENT); const char *password = QUOTE(WIFI_PASSWORD_CLIENT); const uint32_t connectTimeoutMs = 5000; ESP8266WiFiMulti wifiMulti; #endif uint32_t (*wheelSpeedcapture)() = nullptr; bool startSetupMode = false; volatile uint32_t wheel_pulse = 0; Adafruit_NeoPixel leds(1, GPIO_LED, NEO_RGB + NEO_KHZ800); // Function-Prototypes void IRAM_ATTR trigger_ISR(); void LED_Process(uint8_t override = false, uint32_t setColor = LED_DEFAULT_COLOR); #ifdef FEATURE_ENABLE_OLED U8X8_SSD1306_128X64_NONAME_HW_I2C u8x8(-1); void Display_Process(); #endif void Button_Process(); void toggleWiFiAP(bool shutdown = false); void SystemShutdown(bool restart = false); uint32_t Process_Impulse_WheelSpeed(); void EEPROMCyclicPDS_callback(); #ifdef FEATURE_ENABLE_WIFI_CLIENT void wifiMaintainConnectionTicker_callback(); Ticker WiFiMaintainConnectionTicker(wifiMaintainConnectionTicker_callback, 1000, 0, MILLIS); #endif Ticker EEPROMCyclicPDSTicker(EEPROMCyclicPDS_callback, 60000, 0, MILLIS); /** * @brief Initializes the ESP8266 project, configuring various components and setting up required services. * * This setup function is responsible for initializing the ESP8266 project, including setting the CPU frequency, * configuring WiFi settings, initializing DTC storage, handling WiFi client functionality (if enabled), * initializing the Serial communication, setting up an OLED display (if enabled), initializing EEPROM, * loading configuration and persistence data from EEPROM, initializing LEDs, setting up the chosen speed source * (CAN, GPS, Impulse), configuring GPIO pins, setting up Over-The-Air (OTA) updates, initializing the web user interface, * initializing global variables, starting cyclic EEPROM updates for Persistence Data Structure (PDS), and printing * initialization status messages to Serial. */ void setup() { // Set CPU frequency to 80MHz system_update_cpu_freq(SYS_CPU_80MHZ); // Generate a unique device name based on ESP chip ID snprintf(globals.DeviceName, 32, HOST_NAME, ESP.getChipId()); // Disable WiFi persistent storage WiFi.persistent(false); // Initialize and clear Diagnostic Trouble Code (DTC) storage ClearAllDTC(); #ifdef FEATURE_ENABLE_WIFI_CLIENT // Configure WiFi settings for client mode if enabled WiFi.mode(WIFI_STA); WiFi.setHostname(globals.DeviceName); wifiMulti.addAP(QUOTE(WIFI_SSID_CLIENT), QUOTE(WIFI_PASSWORD_CLIENT)); WiFiMaintainConnectionTicker.start(); #else // Disable WiFi if WiFi client feature is not enabled WiFi.mode(WIFI_OFF); #endif // Initialize Serial communication Serial.begin(115200); Serial.print("\n\nSouko's ChainLube Mk1\n"); Serial.print(globals.DeviceName); #ifdef FEATURE_ENABLE_OLED // Initialize OLED display if enabled u8x8.begin(); u8x8.setFont(u8x8_font_chroma48medium8_r); u8x8.clearDisplay(); u8x8.drawString(0, 0, "KTM ChainLube V1"); u8x8.refreshDisplay(); Serial.print("\nDisplay-Init done"); #endif // Initialize EEPROM, load configuration, and persistence data from EEPROM InitEEPROM(); GetConfig_EEPROM(); GetPersistence_EEPROM(); Serial.print("\nEE-Init done"); // Initialize LEDs leds.begin(); Serial.print("\nLED-Init done"); // Initialize based on the chosen speed source (CAN, GPS, Impulse) switch (LubeConfig.SpeedSource) { case SOURCE_CAN: Init_CAN(); wheelSpeedcapture = &Process_CAN_WheelSpeed; Serial.print("\nCAN-Init done"); break; case SOURCE_GPS: Init_GPS(); wheelSpeedcapture = &Process_GPS_WheelSpeed; Serial.print("\nGPS-Init done"); break; case SOURCE_IMPULSE: pinMode(GPIO_TRIGGER, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(GPIO_TRIGGER), trigger_ISR, FALLING); wheelSpeedcapture = &Process_Impulse_WheelSpeed; Serial.print("\nPulse-Input Init done"); break; default: break; } Serial.print("\nSource-Init done"); // Configure GPIO pins for button and pump control pinMode(GPIO_BUTTON, INPUT_PULLUP); pinMode(GPIO_PUMP, OUTPUT); // Set up OTA updates ArduinoOTA.setPort(8266); ArduinoOTA.setHostname(globals.DeviceName); ArduinoOTA.setPassword(QUOTE(ADMIN_PASSWORD)); #ifdef FEATURE_ENABLE_OLED // Set up OTA callbacks for OLED display if enabled ArduinoOTA.onStart([]() { u8x8.clearDisplay(); u8x8.drawString(0, 6, "OTA-Update"); u8x8.refreshDisplay(); }); ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { static bool refreshed = false; if (!refreshed) { u8x8.clearDisplay(); refreshed = true; u8x8.drawString(0, 6, "OTA Upload"); } uint32_t percent = progress / (total / 100); u8x8.setCursor(0, 7); u8x8.printf("%d %%", percent); u8x8.refreshDisplay(); }); ArduinoOTA.onEnd([]() { u8x8.clearDisplay(); u8x8.drawString(0, 6, "OTA-Restart"); u8x8.refreshDisplay(); }); #endif // Begin OTA updates ArduinoOTA.begin(); Serial.print("\nOTA-Init done"); // Initialize the web user interface initWebUI(); Serial.print("\nWebUI-Init done"); // Initialize global variables initGlobals(); Serial.print("\nglobals-Init done"); // Start cyclic EEPROM updates for Persistence Data Structure (PDS) EEPROMCyclicPDSTicker.start(); Serial.print("\nSetup Done\n"); } /** * @brief Main execution loop for the ESP8266 project, performing various tasks based on configuration. * * This loop function handles different tasks based on the configured source of speed data (impulse, CAN, time, GPS). * It calculates wheel distance, runs the lubrication application, updates the OLED display (if enabled), * processes CAN messages, handles button input, manages LED behavior, performs EEPROM-related tasks, handles * webserver operations, processes Diagnostic Trouble Codes (DTC), and manages debugging. Additionally, it * integrates functionalities such as Over-The-Air (OTA) updates, cyclic EEPROM updates for Persistence Data * Structure (PDS), WiFi connection maintenance, and system shutdown handling. */ void loop() { // Run lubrication application with the calculated wheel distance RunLubeApp(wheelSpeedcapture()); #ifdef FEATURE_ENABLE_OLED // Update OLED display if enabled Display_Process(); #endif // Process CAN messages if the speed source is not impulse if (LubeConfig.SpeedSource != SOURCE_IMPULSE) { CAN_Process(); } // Process button input, manage LED behavior, perform EEPROM tasks, handle webserver operations, // process Diagnostic Trouble Codes (DTC), and manage debugging Button_Process(); LED_Process(); EEPROM_Process(); Webserver_Process(); DTC_Process(); Debug_Process(); // Handle OTA updates and update cyclic EEPROM tasks for Persistence Data Structure (PDS) ArduinoOTA.handle(); EEPROMCyclicPDSTicker.update(); #ifdef FEATURE_ENABLE_WIFI_CLIENT // Update WiFi connection maintenance ticker if WiFi client feature is enabled WiFiMaintainConnectionTicker.update(); #endif // Perform system shutdown if the status is set to shutdown if (globals.systemStatus == sysStat_Shutdown) SystemShutdown(false); // Yield to allow other tasks to run yield(); } #ifdef FEATURE_ENABLE_WIFI_CLIENT /** * @brief Callback function for maintaining WiFi connection and handling connection failures. * * This callback function is used by a ticker to periodically check the WiFi connection status. * If the device is not connected to WiFi, it counts connection failures. If the number of failures * exceeds a defined threshold, the function triggers the initiation of an Access Point (AP) mode * using the `toggleWiFiAP` function. */ void wifiMaintainConnectionTicker_callback() { // Static variables to track WiFi connection failure count and maximum allowed failures static uint32_t WiFiFailCount = 0; const uint32_t WiFiFailMax = 20; // Check if the device is connected to WiFi if (wifiMulti.run(connectTimeoutMs) == WL_CONNECTED) { return; // Exit if connected } else { // Increment WiFi connection failure count if (WiFiFailCount < WiFiFailMax) { WiFiFailCount++; } else { // Trigger AP mode if the maximum failures are reached Debug_pushMessage("WiFi not connected! - Start AP\n"); toggleWiFiAP(); } } } #endif /** * @brief Callback function for cyclically storing Persistence Data Structure (PDS) to EEPROM. * * This callback function is invoked periodically to store the Persistence Data Structure (PDS) * to the EEPROM. It ensures that essential data is saved persistently, allowing the system to * recover its state after power cycles or resets. */ void EEPROMCyclicPDS_callback() { StorePersistence_EEPROM(); } /** * @brief Interrupt Service Routine (ISR) triggered by wheel speed sensor pulses. * * This ISR is called whenever a pulse is detected from the wheel speed sensor. It increments * the `wheel_pulse` variable, which is used to track the number of pulses received. */ void trigger_ISR() { wheel_pulse++; } /** * @brief Manages LED behavior based on the current system status and user overrides. * * This function handles LED behavior, including startup animations, confirmation animations for * normal and rain modes, indication for purge, error, shutdown, and normal operation. It supports * user overrides to set a specific LED color. The LED status is determined by the current system * status, and specific LED patterns are displayed accordingly. * * @param override Flag indicating whether to override the LED behavior (0: No override, 1: Override, 2: Resume previous state). * @param SetColor The color to set when overriding the LED behavior. */ void LED_Process(uint8_t override, uint32_t SetColor) { // Enumeration to represent LED status typedef enum { LED_Startup, LED_Normal, LED_Confirm_Normal, LED_Rain, LED_Confirm_Rain, LED_Purge, LED_Error, LED_Shutdown, LED_Override } tLED_Status; // Static variables to track LED status, system status, override color, and previous LED status static tSystem_Status oldSysStatus = sysStat_Startup; static tLED_Status LED_Status = LED_Startup; static uint32_t LED_override_color = 0; static tLED_Status LED_ResumeOverrideStatus = LED_Startup; // Variables for managing LED animation timing uint8_t color = 0; uint32_t timer = 0; uint32_t animtimer = 0; static uint32_t timestamp = 0; timer = millis(); // Handle LED overrides if (override == 1) { if (LED_Status != LED_Override) { LED_ResumeOverrideStatus = LED_Status; Debug_pushMessage("Override LED_Status\n"); } LED_Status = LED_Override; LED_override_color = SetColor; } if (override == 2) { if (LED_Status == LED_Override) { LED_Status = LED_ResumeOverrideStatus; Debug_pushMessage("Resume LED_Status\n"); } } // Update LED status when system status changes if (oldSysStatus != globals.systemStatus) { switch (globals.systemStatus) { case sysStat_Startup: LED_Status = LED_Startup; Debug_pushMessage("sysStat: Startup\n"); break; case sysStat_Normal: timestamp = timer + 3500; LED_Status = LED_Confirm_Normal; Debug_pushMessage("sysStat: Normal\n"); break; case sysStat_Rain: timestamp = timer + 3500; LED_Status = LED_Confirm_Rain; Debug_pushMessage("sysStat: Rain\n"); break; case sysStat_Purge: LED_Status = LED_Purge; Debug_pushMessage("sysStat: Purge\n"); break; case sysStat_Error: LED_Status = LED_Error; Debug_pushMessage("sysStat: Error\n"); break; case sysStat_Shutdown: LED_Status = LED_Shutdown; Debug_pushMessage("sysStat: Shutdown\n"); break; default: break; } oldSysStatus = globals.systemStatus; } // Handle different LED statuses switch (LED_Status) { case LED_Startup: leds.setBrightness(LubeConfig.LED_Max_Brightness); if (globals.TankPercentage < LubeConfig.TankRemindAtPercentage) leds.setPixelColor(0, LED_STARTUP_TANKWARN); else leds.setPixelColor(0, LED_STARTUP_NORMAL); break; case LED_Confirm_Normal: animtimer = timer % 500; color = map(animtimer / 2, 0, 250, 0, LubeConfig.LED_Max_Brightness); leds.setPixelColor(0, LED_NORMAL_COLOR); if (animtimer < 250) leds.setBrightness(color); else leds.setBrightness(LubeConfig.LED_Max_Brightness - color); if (timestamp < timer) { LED_Status = LED_Normal; Debug_pushMessage("LED_Status: Confirm -> Normal\n"); } break; case LED_Normal: leds.setBrightness(LubeConfig.LED_Min_Brightness); leds.setPixelColor(0, LED_NORMAL_COLOR); if (timer % 2000 > 1950 && LubeConfig.LED_Mode_Flash == true) leds.setBrightness(LubeConfig.LED_Max_Brightness); else if (timer % 2000 > 1500 && WiFi.getMode() != WIFI_OFF) leds.setPixelColor(0, LED_WIFI_BLINK); break; case LED_Confirm_Rain: animtimer = timer % 500; color = map(animtimer / 2, 0, 250, 0, LubeConfig.LED_Max_Brightness); leds.setPixelColor(0, LED_RAIN_COLOR); if (animtimer < 250) leds.setBrightness(color); else leds.setBrightness(LubeConfig.LED_Max_Brightness - color); if (timestamp < timer) { LED_Status = LED_Rain; Debug_pushMessage("LED_Status: Confirm -> Rain\n"); } break; case LED_Rain: leds.setBrightness(LubeConfig.LED_Min_Brightness); leds.setPixelColor(0, LED_RAIN_COLOR); if (timer % 2000 > 1950 && LubeConfig.LED_Mode_Flash == true) leds.setBrightness(LubeConfig.LED_Max_Brightness); else if (timer % 2000 > 1500 && WiFi.getMode() != WIFI_OFF) leds.setPixelColor(0, LED_WIFI_BLINK); break; case LED_Purge: timer = timer % 500; color = map(timer / 2, 0, 250, LubeConfig.LED_Min_Brightness, LubeConfig.LED_Max_Brightness); leds.setPixelColor(0, LED_PURGE_COLOR); if (timer < 250) leds.setBrightness(color); else leds.setBrightness(LubeConfig.LED_Max_Brightness - color); break; case LED_Error: leds.setBrightness(LubeConfig.LED_Max_Brightness); leds.setPixelColor(0, timer % 500 > 250 ? LED_ERROR_BLINK : 0); break; case LED_Shutdown: timer = timer % 600; leds.setPixelColor(0, LED_SHUTDOWN_BLINK); if (timer < 500) { color = map(timer, 0, 500, LubeConfig.LED_Max_Brightness, LubeConfig.LED_Min_Brightness); leds.setBrightness(color); } else { leds.setBrightness(LubeConfig.LED_Min_Brightness); } break; case LED_Override: leds.setBrightness(LubeConfig.LED_Max_Brightness); leds.setPixelColor(0, LED_override_color); break; default: break; } leds.show(); } #ifdef FEATURE_ENABLE_OLED /** * @brief Manages the display content based on the current system status and updates the OLED display. * * This function handles the content to be displayed on the OLED screen, taking into account the * current system status. It clears the display and prints relevant information such as system mode, * remaining lubrication distance, tank level, WiFi status, speed source, and IP address. Additionally, * it refreshes the OLED display with the updated content. */ void Display_Process() { // Static variable to track the previous system status static tSystem_Status oldSysStatus = sysStat_Startup; // Check if the system status has changed since the last update if (oldSysStatus != globals.systemStatus) { // Clear the display and print the system title when the status changes u8x8.clearDisplay(); u8x8.drawString(0, 0, "KTM ChainLube V1"); oldSysStatus = globals.systemStatus; } // Set the cursor position for displaying information on the OLED screen u8x8.setCursor(0, 1); // Calculate remaining lubrication distance based on system mode uint32_t DistRemain = globals.systemStatus == sysStat_Normal ? LubeConfig.DistancePerLube_Default : LubeConfig.DistancePerLube_Rain; DistRemain = DistRemain - (PersistenceData.TravelDistance_highRes_mm / 1000); // Display relevant information on the OLED screen based on system status u8x8.printf(PSTR("Mode: %10s\n"), globals.systemStatustxt); if (globals.systemStatus == sysStat_Error) { // Display the last Diagnostic Trouble Code (DTC) in case of an error u8x8.printf(PSTR("last DTC: %6d\n"), getlastDTC(false)); } else { // Display information such as next lubrication distance, tank level, WiFi status, speed source, and IP address u8x8.printf(PSTR("next Lube: %4dm\n"), DistRemain); u8x8.printf(PSTR("Tank: %8dml\n"), PersistenceData.tankRemain_microL / 1000); u8x8.printf(PSTR("WiFi: %10s\n"), (WiFi.getMode() == WIFI_AP ? "AP" : WiFi.getMode() == WIFI_OFF ? "OFF" : WiFi.getMode() == WIFI_STA ? "CLIENT" : "UNKNOWN")); u8x8.printf(PSTR("Source: %8s\n"), SpeedSourceString[LubeConfig.SpeedSource]); u8x8.printf("%s\n", WiFi.localIP().toString().c_str()); } // Refresh the OLED display with the updated content u8x8.refreshDisplay(); } #endif /** * @brief Processes the button input and performs corresponding actions based on button state and timing. * * This function handles the button input, detecting button presses and executing actions based on * predefined time delays. Actions include toggling WiFi, starting purge, toggling operating modes, * and displaying feedback through LEDs. The function utilizes an enumeration to track button actions * and manages the timing for different actions. */ void Button_Process() { // Time delays for different button actions #define BUTTON_ACTION_DELAY_TOGGLEMODE 500 #define BUTTON_ACTION_DELAY_PURGE 3500 #define BUTTON_ACTION_DELAY_WIFI 6500 #define BUTTON_ACTION_DELAY_NOTHING 9500 // Enumeration to represent button actions typedef enum buttonAction_e { BTN_INACTIVE, BTN_NOTHING, BTN_TOGGLEMODE, BTN_TOGGLEWIFI, BTN_STARTPURGE } buttonAction_t; // Static variables to track button state and timing static uint32_t buttonTimestamp = 0; static buttonAction_t buttonAction = BTN_INACTIVE; // Check if button is pressed (LOW) if (digitalRead(GPIO_BUTTON) == LOW) { // Update button timestamp on the first button press if (buttonTimestamp == 0) buttonTimestamp = millis(); // Check and execute actions based on predefined time delays if (buttonTimestamp + BUTTON_ACTION_DELAY_NOTHING < millis()) { LED_Process(1, COLOR_WARM_WHITE); buttonAction = BTN_NOTHING; } else if (buttonTimestamp + BUTTON_ACTION_DELAY_WIFI < millis()) { LED_Process(1, LED_WIFI_BLINK); buttonAction = BTN_TOGGLEWIFI; } else if (buttonTimestamp + BUTTON_ACTION_DELAY_PURGE < millis()) { LED_Process(1, LED_PURGE_COLOR); buttonAction = BTN_STARTPURGE; } else if (buttonTimestamp + BUTTON_ACTION_DELAY_TOGGLEMODE < millis()) { uint32_t color = globals.systemStatus == sysStat_Normal ? LED_RAIN_COLOR : LED_NORMAL_COLOR; LED_Process(1, color); buttonAction = BTN_TOGGLEMODE; } } else // Button is released { // Execute corresponding actions based on the detected button action if (buttonAction != BTN_INACTIVE) { switch (buttonAction) { case BTN_TOGGLEWIFI: toggleWiFiAP(); Debug_pushMessage("Starting WiFi AP\n"); break; case BTN_STARTPURGE: globals.systemStatus = sysStat_Purge; globals.purgePulses = LubeConfig.BleedingPulses; Debug_pushMessage("Starting Purge\n"); break; case BTN_TOGGLEMODE: switch (globals.systemStatus) { case sysStat_Normal: globals.systemStatus = sysStat_Rain; globals.resumeStatus = sysStat_Rain; break; case sysStat_Rain: globals.systemStatus = sysStat_Normal; globals.resumeStatus = sysStat_Normal; break; default: break; } Debug_pushMessage("Toggling Mode\n"); break; case BTN_NOTHING: default: Debug_pushMessage("Nothing or invalid\n"); break; } // Display feedback through LEDs LED_Process(2); } // Reset button state and timestamp buttonAction = BTN_INACTIVE; buttonTimestamp = 0; } } /** * @brief Toggles the WiFi functionality based on the current status. * * This function manages the WiFi state, either turning it off or starting it as an Access Point (AP), * depending on the current mode. If the WiFi is turned off, it can be started in AP mode with the * device name and password configured. Additionally, it may stop certain operations related to WiFi * maintenance or display debug messages based on the defined features. * * @param shutdown Flag indicating whether the system is in a shutdown state. */ void toggleWiFiAP(bool shutdown) { // Check if WiFi is currently active if (WiFi.getMode() != WIFI_OFF) { // Turn off WiFi WiFi.mode(WIFI_OFF); Debug_pushMessage("WiFi turned off\n"); // Stop WiFi maintenance connection ticker if enabled #ifdef FEATURE_ENABLE_WIFI_CLIENT WiFiMaintainConnectionTicker.stop(); #endif } else { // Start WiFi in Access Point (AP) mode WiFi.mode(WIFI_AP); WiFi.softAPConfig(IPAddress(WIFI_AP_IP_GW), IPAddress(WIFI_AP_IP_GW), IPAddress(255, 255, 255, 0)); WiFi.softAP(globals.DeviceName, QUOTE(WIFI_AP_PASSWORD)); // Stop WiFi maintenance connection ticker if enabled and display debug messages #ifdef FEATURE_ENABLE_WIFI_CLIENT WiFiMaintainConnectionTicker.stop(); Debug_pushMessage("WiFi AP started, stopped Maintain-Timer\n"); #else Debug_pushMessage("WiFi AP started\n"); #endif } } /** * @brief Performs necessary tasks before shutting down and optionally restarts the ESP. * * This function initiates a system shutdown, performing tasks such as storing configuration * and persistence data to EEPROM before shutting down. If a restart is requested, the ESP * will be restarted; otherwise, the system will enter an indefinite loop. * * @param restart Flag indicating whether to restart the ESP after shutdown (default: false). */ void SystemShutdown(bool restart) { static uint32_t shutdown_delay = 0; // Initialize shutdown delay on the first call if (shutdown_delay == 0) { shutdown_delay = millis() + SHUTDOWN_DELAY_MS; Serial.printf("Shutdown requested - Restarting in %d seconds\n", SHUTDOWN_DELAY_MS / 1000); } // Check if the shutdown delay has elapsed if (shutdown_delay < millis()) { Webserver_Shutdown(); // Store persistence data to EEPROM StorePersistence_EEPROM(); // Perform restart if requested, otherwise enter an indefinite loop if (restart) ESP.restart(); else while (1) ; } } /** * @brief Processes the impulses from the wheel speed sensor and converts them into traveled distance. * * This function takes the pulse count from the wheel speed sensor and converts it into distance * traveled in millimeters. The conversion is based on the configured parameters such as the number * of pulses per revolution and the distance traveled per revolution. * * @return The calculated distance traveled in millimeters. */ uint32_t Process_Impulse_WheelSpeed() { uint32_t add_milimeters = 0; // Calculate traveled Distance in mm if (LubeConfig.PulsePerRevolution != 0) add_milimeters = (wheel_pulse * (LubeConfig.DistancePerRevolution_mm / LubeConfig.PulsePerRevolution)); if (globals.measurementActive == true) globals.measuredPulses = globals.measuredPulses + wheel_pulse; wheel_pulse = 0; return add_milimeters; }