/** * @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 "gps.h" #include "dtc.h" #include "led_colors.h" #include "obd2_kline.h" #include "can_obd2.h" #include "can_native.h" #include "buttoncontrol.h" #include "button_actions.h" #include "ledcontrol.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(); #ifdef FEATURE_ENABLE_OLED U8X8_SSD1306_128X64_NONAME_HW_I2C u8x8(-1); void Display_Process(); #endif 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(); Wire.begin(); #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 LEDControl_Init(GPIO_LED); Serial.print("\nLED-Init done"); // Initialize based on the chosen speed source (CAN, GPS, Impulse) switch (LubeConfig.SpeedSource) { case SOURCE_CAN: Init_CAN_Native(); wheelSpeedcapture = &Process_CAN_Native_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; case SOURCE_OBD2_KLINE: Init_OBD2_KLine(Serial); wheelSpeedcapture = &Process_OBD2_KLine_Speed; Serial.print("\nOBD2-KLine-Init done"); break; case SOURCE_OBD2_CAN: Init_CAN_OBD2(); wheelSpeedcapture = &Process_CAN_OBD2_Speed; Serial.print("\nOBD2-CAN-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); ButtonControl_Init(GPIO_BUTTON, buttonActions, buttonActionCount); // 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 if (wheelSpeedcapture != nullptr) RunLubeApp(wheelSpeedcapture()); #ifdef FEATURE_ENABLE_OLED // Update OLED display if enabled Display_Process(); #endif // Process button input, manage LED behavior, perform EEPROM tasks, handle webserver operations, // process Diagnostic Trouble Codes (DTC), and manage debugging ButtonControl_Update(); LEDControl_Update(); EEPROM_Process(); Webserver_Process(); DTC_Process(); Debug_Process(); if (globals.toggle_wifi == true) { globals.toggle_wifi = false; toggleWiFiAP(); } // 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. */ volatile uint32_t lastTriggerMicros = 0; void IRAM_ATTR trigger_ISR() { uint32_t now = micros(); if (now - lastTriggerMicros < 2500) return; lastTriggerMicros = now; globals.isr_debug++; wheel_pulse++; } #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 = 0; switch (globals.systemStatus) { case sysStat_Normal: DistRemain = LubeConfig.DistancePerLube_Default; break; case sysStat_Rain: DistRemain = LubeConfig.DistancePerLube_Rain; break; case sysStat_Wash: DistRemain = LubeConfig.WashMode_Interval; break; default: DistRemain = 0; break; } DistRemain = DistRemain - (PersistenceData.TravelDistance_highRes_mm / 1000); // Display relevant information on the OLED screen based on system status u8x8.printf(PSTR("Mode: %10s\n"), ToString(globals.systemStatus)); 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"), ToString(LubeConfig.SpeedSource)); u8x8.printf("%s\n", WiFi.localIP().toString().c_str()); } // Refresh the OLED display with the updated content u8x8.refreshDisplay(); } #endif /** * @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) { LEDControl_SetOverride(LED_WIFI_COLOR, LED_PATTERN_BLINK_FAST, 2500); // 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; }