From e4b89ea21890a8862f51171ff85e939eeee2f12b Mon Sep 17 00:00:00 2001 From: Tomas Kuchta Date: Sun, 18 Aug 2024 22:32:05 +0200 Subject: [PATCH 001/145] Add Power Measurement usermod - Implement functions to measure power consumption --- .../Power_Measurement/Power_Measurement.h | 576 +++ .../assets/example_schematic.kicad_sch | 3266 +++++++++++++++++ .../assets/img/example schematic.png | Bin 0 -> 53358 bytes .../assets/img/screenshot 1 - info.jpg | Bin 0 -> 48340 bytes .../assets/img/screenshot 2 - settings.png | Bin 0 -> 24762 bytes .../assets/img/screenshot 3 - settings.png | Bin 0 -> 35492 bytes usermods/Power_Measurement/readme.md | 94 + wled00/const.h | 1 + wled00/pin_manager.h | 1 + wled00/usermods_list.cpp | 8 + 10 files changed, 3946 insertions(+) create mode 100644 usermods/Power_Measurement/Power_Measurement.h create mode 100644 usermods/Power_Measurement/assets/example_schematic.kicad_sch create mode 100644 usermods/Power_Measurement/assets/img/example schematic.png create mode 100644 usermods/Power_Measurement/assets/img/screenshot 1 - info.jpg create mode 100644 usermods/Power_Measurement/assets/img/screenshot 2 - settings.png create mode 100644 usermods/Power_Measurement/assets/img/screenshot 3 - settings.png create mode 100644 usermods/Power_Measurement/readme.md diff --git a/usermods/Power_Measurement/Power_Measurement.h b/usermods/Power_Measurement/Power_Measurement.h new file mode 100644 index 0000000000..d54ac790c5 --- /dev/null +++ b/usermods/Power_Measurement/Power_Measurement.h @@ -0,0 +1,576 @@ +// Filename: Power_Measurement.h +// This code was cocreated by github copilot and created by Tomáš Kuchta +#pragma once + +#include "wled.h" +#include "esp_adc_cal.h" + +#ifndef CURRENT_PIN + #define CURRENT_PIN 1 +#endif + +#ifndef VOLTAGE_PIN + #define VOLTAGE_PIN 0 +#endif + +#define NUM_READINGS 10 +#define NUM_READINGS_CAL 100 +#define ADC_MAX_VALUE (pow(2, ADCResolution) - 1) // For 12-bit ADC, the max value is 4095 +#define UPDATE_INTERVAL_MAIN 100 +#define UPDATE_INTERVAL_MQTT 60000 + +class UsermodPower_Measurement : public Usermod { + private: + bool initDone = false; + unsigned long lastTime_slow = 0; + unsigned long lastTime_main = 0; + unsigned long lastTime_energy = 0; + unsigned long lastTime_mqtt = 0; + boolean enabled = true; + boolean calibration_enable = false; + boolean cal_adavnced = false; + + int Voltage_raw = 0; + float AverageVoltage_raw = 0; + int Voltage_raw_adj = 0; + int Voltage_calc = 0; + + int Current_raw = 0; + float AverageCurrent_raw = 0; + int Current_calc = 0; + + float voltageReadings_raw[NUM_READINGS]; + float currentReadings_raw[NUM_READINGS]; + int readIndex = 0; + float totalVoltage_raw = 0; + float totalCurrent_raw = 0; + + // Low-pass filter variables + float alpha = 0.1; + float filtered_Voltage_raw = 0; + float filtered_Current_raw = 0; + + unsigned long long wattmiliseconds = 0; //energy counter in watt milliseconds + + + // calibration variables + int Num_Readings_Cal = NUM_READINGS_CAL; + bool Cal_In_Progress = false; + bool Cal_Zero_Points = false; + bool Cal_calibrate_Measured_Voltage = false; + bool Cal_calibrate_Measured_Current = false; + float Cal_Measured_Voltage = 0; + float Cal_Measured_Current = 0; + + float Cal_min_Voltage_raw = 17; + float Cal_min_Current_calc = 718; + + float Cal_Voltage_raw_averaged = 0; + float Cal_Voltage_calc_averaged = 0; + float Cal_Current_calc_averaged = 0; + + int Cal_Current_at_x = 1000; + int Cal_Current_calc_at_x = 775; + float Cal_Voltage_Coefficient = 22.97; + + // averiging variables + float Cal_Voltage_raw_Readings_Avg[NUM_READINGS_CAL]; + float Cal_Voltage_calc_Readings_Avg[NUM_READINGS_CAL]; + float Cal_Current_calc_Readings_Avg[NUM_READINGS_CAL]; + int Cal_Read_Index = 0; + float Cal_Total_Voltage_raw = 0; + float Cal_Total_Voltage_calc = 0; + float Cal_Total_Current_calc = 0; + + int8_t VoltagePin = VOLTAGE_PIN; + int8_t CurrentPin = CURRENT_PIN; + + int Update_Interval_Mqtt = UPDATE_INTERVAL_MQTT; + int Update_Interval_Main = UPDATE_INTERVAL_MAIN; + + // String used more than once + static const char _name[] PROGMEM; + static const char _no_data[] PROGMEM; + + public: + int ADCResolution = 12; + int ADCAttenuation = ADC_6db; + + //For usage in other parts of the main code + float Voltage = 0; + float Current = 0; + float Power = 0; + unsigned long kilowatthours = 0; + + void setup() { + analogReadResolution(ADCResolution); + analogSetAttenuation(static_cast(ADCAttenuation)); // Set the ADC attenuation (ADC_ATTEN_DB_6 = 0 mV ~ 1300 mV) + + // Initialize all readings to 0: + for (int i = 0; i < NUM_READINGS; i++) { + voltageReadings_raw[i] = 0; + currentReadings_raw[i] = 0; + } + + Current_raw = 1800; + filtered_Current_raw = 1800; + + Cal_Zero_Points = false; + Cal_calibrate_Measured_Voltage = false; + Cal_calibrate_Measured_Current = false; + Cal_In_Progress = false; + Num_Readings_Cal = NUM_READINGS_CAL; + + + #ifdef WLED_DEBUG + esp_adc_cal_characteristics_t adc_chars; + esp_adc_cal_value_t val_type = esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_6, ADC_WIDTH_BIT_12, ESP_ADC_CAL_VAL_DEFAULT_VREF, &adc_chars); + + if (val_type == ESP_ADC_CAL_VAL_EFUSE_VREF) { + // eFuse Vref is available + DEBUG_PRINTLN(F("PM: Using eFuse Vref for ADC calibration_enable")); + } else if (val_type == ESP_ADC_CAL_VAL_EFUSE_TP) { + // Two Point calibration_enable is available + DEBUG_PRINTLN(F("PM: Using Two Point calibration_enable for ADC calibration_enable")); + } else { + // Default Vref is used + DEBUG_PRINTLN(F("PM: Using default Vref for ADC calibration_enable")); + } + #endif + + + if (enabled) { + pinAlocation(); + } + + initDone = true; + + } + + void loop() { + if (!enabled || strip.isUpdating()) return; + + unsigned long currentTime = millis(); + + #ifdef WLED_DEBUG + if (currentTime - lastTime_slow >= 1000) { + printDebugInfo(); + lastTime_slow = currentTime; + } + #endif + + #ifndef WLED_DISABLE_MQTT + if (currentTime - lastTime_mqtt >= Update_Interval_Mqtt) { + publishPowerMeasurements(); + lastTime_mqtt = currentTime; + } + #endif + + if (currentTime - lastTime_main >= Update_Interval_Main) { + updateReadings(); + + if (Cal_Zero_Points || Cal_calibrate_Measured_Voltage || Cal_calibrate_Measured_Current) calibration(); + + lastTime_main = currentTime; + } + + + } + + void pinAlocation() { + DEBUG_PRINTLN(F("Allocating power pins...")); + if (VoltagePin >= 0 && pinManager.allocatePin(VoltagePin, true, PinOwner::UM_Power_Measurement)) { + DEBUG_PRINT(F("Voltage pin allocated: ")); + DEBUG_PRINTLN(VoltagePin); + } else { + if (VoltagePin >= 0) { + DEBUG_PRINTLN(F("Voltage pin allocation failed.")); + } + VoltagePin = -1; // allocation failed, disable + } + + if (CurrentPin >= 0 && pinManager.allocatePin(CurrentPin, true, PinOwner::UM_Power_Measurement)) { + DEBUG_PRINT(F("Current pin allocated: ")); + DEBUG_PRINTLN(CurrentPin); + } else { + if (CurrentPin >= 0) { + DEBUG_PRINTLN(F("Current pin allocation failed.")); + } + CurrentPin = -1; // allocation failed, disable + } + } + + + void printDebugInfo() { + DEBUG_PRINT(F("Voltage raw: ")); + DEBUG_PRINTLN(Voltage_raw); + DEBUG_PRINTLN(AverageVoltage_raw); + DEBUG_PRINT(F("Calc: ")); + DEBUG_PRINTLN(Voltage_calc); + DEBUG_PRINT(F("Voltage: ")); + DEBUG_PRINTLN(Voltage); + + DEBUG_PRINT(F("Current raw: ")); + DEBUG_PRINTLN(Current_raw); + DEBUG_PRINTLN(AverageCurrent_raw); + DEBUG_PRINT(F("Calc: ")); + DEBUG_PRINTLN(Current_calc); + DEBUG_PRINT("Current: "); + DEBUG_PRINTLN(Current); + + DEBUG_PRINT("Power: "); + DEBUG_PRINTLN(Power); + + DEBUG_PRINT("Energy: "); + DEBUG_PRINTLN(kilowatthours); + DEBUG_PRINT("Energy Wms: "); + DEBUG_PRINTLN(wattmiliseconds); + } + + void updateReadings() { + // Measure the voltage and current and store them in the arrays for the moving average and convert via map function: + totalVoltage_raw -= voltageReadings_raw[readIndex]; + totalCurrent_raw -= currentReadings_raw[readIndex]; + + if (VoltagePin == -1) { + Voltage_raw = 0; + DEBUG_PRINTLN("Voltage pin not allocated"); + } else { + Voltage_raw = analogRead(VoltagePin); + } + + if (CurrentPin == -1) { + Current_raw = 0; + DEBUG_PRINTLN("Current pin not allocated"); + } else { + Current_raw = analogRead(CurrentPin); + } + + if (millis() > 1000) { // To avoid the initial spike in readings + filtered_Voltage_raw = (alpha * Voltage_raw) + ((1 - alpha) * filtered_Voltage_raw); + filtered_Current_raw = (alpha * Current_raw) + ((1 - alpha) * filtered_Current_raw); + } else { + filtered_Voltage_raw = Voltage_raw; + filtered_Current_raw = Current_raw; + } + + voltageReadings_raw[readIndex] = filtered_Voltage_raw; + currentReadings_raw[readIndex] = filtered_Current_raw; + + totalVoltage_raw += filtered_Voltage_raw; + totalCurrent_raw += filtered_Current_raw; + + AverageVoltage_raw = totalVoltage_raw / NUM_READINGS; + AverageCurrent_raw = totalCurrent_raw / NUM_READINGS; + + readIndex = (readIndex + 1) % NUM_READINGS; + + Voltage_raw_adj = map(AverageVoltage_raw, Cal_min_Voltage_raw, ADC_MAX_VALUE, 0, ADC_MAX_VALUE); + if (Voltage_raw_adj < 0) Voltage_raw_adj = 0; + Voltage_calc = readADC_Cal(Voltage_raw_adj); + Voltage = (Voltage_calc / 1000.0) * Cal_Voltage_Coefficient; + if (Voltage < 0.05) Voltage = 0; + Voltage = round(Voltage * 100.0) / 100.0; // Round to 2 decimal places + if (VoltagePin == -1) Voltage = 0; + + Current_calc = readADC_Cal(AverageCurrent_raw); + Current = (map(Current_calc, Cal_min_Current_calc, Cal_Current_calc_at_x, 0, Cal_Current_at_x)) / 1000.0; + if (Current > -0.1 && Current < 0.05) { + Current = 0; + } + Current = round(Current * 100.0) / 100.0; + if (CurrentPin == -1) Current = 0; + + // Calculate power + Power = Voltage * Current; + Power = round(Power * 100.0) / 100.0; + + // Calculate energy - dont do it when led is off + if (Power > 0) { + unsigned long elapsedTime = millis() - lastTime_energy; + wattmiliseconds += Power * elapsedTime; + } + lastTime_energy = millis(); + + if (wattmiliseconds >= 3600000000) { // 3,600,000 milliseconds = 1 hour + kilowatthours += wattmiliseconds / 3600000000; // Convert watt-milliseconds to kilowatt-hours (1 watt-millisecond = 1/3,600,000,000 kilowatt-hours) + wattmiliseconds = 0; + } + } + + void calibration() { + if (Num_Readings_Cal == NUM_READINGS_CAL) { + DEBUG_PRINTLN("calibration_enable started"); + Cal_In_Progress = true; + serializeConfig(); // To update the checkboxes in the config + } + if (Num_Readings_Cal > 0) { + Num_Readings_Cal--; + // Average the readings + Cal_Total_Voltage_raw -= Cal_Voltage_raw_Readings_Avg[Cal_Read_Index]; + Cal_Total_Voltage_calc -= Cal_Voltage_calc_Readings_Avg[Cal_Read_Index]; + Cal_Total_Current_calc -= Cal_Current_calc_Readings_Avg[Cal_Read_Index]; + + Cal_Voltage_raw_Readings_Avg[Cal_Read_Index] = Voltage_raw; + Cal_Voltage_calc_Readings_Avg[Cal_Read_Index] = Voltage_calc; + Cal_Current_calc_Readings_Avg[Cal_Read_Index] = Current_calc; + + Cal_Total_Voltage_raw += Voltage_raw; + Cal_Total_Voltage_calc += Voltage_calc; + Cal_Total_Current_calc += Current_calc; + + Cal_Read_Index = (Cal_Read_Index + 1) % NUM_READINGS_CAL; + + Cal_Voltage_raw_averaged = Cal_Total_Voltage_raw / NUM_READINGS_CAL; + Cal_Voltage_calc_averaged = Cal_Total_Voltage_calc / NUM_READINGS_CAL; + Cal_Current_calc_averaged = Cal_Total_Current_calc / NUM_READINGS_CAL; + } else { + + DEBUG_PRINTLN("calibration_enable Flags:"); + DEBUG_PRINTLN(Cal_In_Progress); + DEBUG_PRINTLN(Num_Readings_Cal); + DEBUG_PRINTLN(Cal_Zero_Points); + DEBUG_PRINTLN(Cal_calibrate_Measured_Voltage); + DEBUG_PRINTLN(Cal_calibrate_Measured_Current); + DEBUG_PRINTLN("the averaged values are:"); + DEBUG_PRINTLN(Cal_Voltage_raw_averaged); + DEBUG_PRINTLN(Cal_Voltage_calc_averaged); + DEBUG_PRINTLN(Cal_Current_calc_averaged); + DEBUG_PRINTLN("Inputed values are:"); + DEBUG_PRINTLN(Cal_Measured_Voltage); + DEBUG_PRINTLN(Cal_Measured_Current); + + Calibration_calculation(); + + Cal_In_Progress = false; + Cal_Zero_Points = false; + Cal_calibrate_Measured_Voltage = false; + Cal_calibrate_Measured_Current = false; + Num_Readings_Cal = NUM_READINGS_CAL; + serializeConfig(); // To update the checkboxes in the config + + DEBUG_PRINTLN("calibration_enable finished"); + } + } + + void Calibration_calculation() { + DEBUG_PRINTLN("Calculating calibration_enable values"); + + if (Cal_calibrate_Measured_Current) { + Cal_Current_at_x = Cal_Measured_Current * 1000; + Cal_Current_calc_at_x = Cal_Current_calc_averaged; + + } else if (Cal_calibrate_Measured_Voltage) { + Cal_Voltage_Coefficient = (Cal_Measured_Voltage * 1000) / Cal_Voltage_calc_averaged; + + } else if (Cal_Zero_Points) { + Cal_min_Voltage_raw = Cal_Voltage_raw_averaged; + Cal_min_Current_calc = Cal_Current_calc_averaged; + } else { + DEBUG_PRINTLN("No calibration_enable values selected - but that should not happen"); + } + + } + + void addToJsonInfo(JsonObject& root) { + if (!enabled)return; + + JsonObject user = root["u"]; + if (user.isNull())user = root.createNestedObject("u"); + + JsonArray Current_json = user.createNestedArray(FPSTR("Current")); + if (Current_raw == 0 || CurrentPin == -1) { + Current_json.add(F(_no_data)); + } else if (Current_raw >= (ADC_MAX_VALUE - 3)) { + Current_json.add(F("Overrange")); + } else { + Current_json.add(Current); + Current_json.add(F(" A")); + } + + JsonArray Voltage_json = user.createNestedArray(FPSTR("Voltage")); + if (Voltage_raw == 0 || VoltagePin == -1) { + Voltage_json.add(F(_no_data)); + } else if (Voltage_raw >= (ADC_MAX_VALUE - 3)) { + Voltage_json.add(F(_no_data)); + } else if (Voltage_raw >= (ADC_MAX_VALUE - 3)) { + Voltage_json.add(F("Overrange")); + } else { + Voltage_json.add(Voltage); + Voltage_json.add(F(" V")); + } + + if (calibration_enable) { + JsonArray Current_raw_json = user.createNestedArray(FPSTR("Current raw")); + Current_raw_json.add(Current_raw); + Current_raw_json.add(" -> " + String(Current_calc)); + + JsonArray Voltage_raw_json = user.createNestedArray(FPSTR("Voltage raw")); + Voltage_raw_json.add(Voltage_raw); + Voltage_raw_json.add(" -> " + String(Voltage_calc)); + } + + JsonArray Power_json = user.createNestedArray(FPSTR("Power")); + Power_json.add(Power); + Power_json.add(F(" W")); + + JsonArray Energy_json = user.createNestedArray(FPSTR("Energy")); + Energy_json.add(kilowatthours); + Energy_json.add(F(" kWh")); + } + + void addToConfig(JsonObject& root) { + JsonObject top = root.createNestedObject(FPSTR(_name)); + top[FPSTR("enabled")] = enabled; + + JsonObject power_pins = top.createNestedObject(FPSTR("power_pins")); + power_pins[FPSTR("Voltage Pin")] = VoltagePin; + power_pins[FPSTR("Current Pin")] = CurrentPin; + + JsonObject update = top.createNestedObject(FPSTR("update rate in ms")); + update[FPSTR("update rate of mqtt")] = Update_Interval_Mqtt; + update[FPSTR("update rate of main")] = Update_Interval_Main; + + JsonObject cal = top.createNestedObject(FPSTR("calibration")); + cal[FPSTR("calibration Mode")] = calibration_enable; + if (calibration_enable && !Cal_In_Progress) { + cal[FPSTR("Advanced")] = cal_adavnced; + + cal["Zero Points"] = Cal_Zero_Points; + cal["Measured Voltage"] = Cal_Measured_Voltage; + cal["Calibrate Voltage?"] = Cal_calibrate_Measured_Voltage; + cal["Measured Current"] = Cal_Measured_Current; + cal["Calibrate Current?"] = Cal_calibrate_Measured_Current; + } else if (Cal_In_Progress) { + cal[FPSTR("calibration_enable is in progress please wait")] = "Non-Essential Data Entry Zone: Just for Kicks and Giggles"; + } + + if (calibration_enable && cal_adavnced && !Cal_In_Progress) { + cal[FPSTR("Number of samples")] = Num_Readings_Cal; + cal[FPSTR("Zero Point of Voltage")] = Cal_min_Voltage_raw; + cal[FPSTR("Zero Point of Current")] = Cal_min_Current_calc; + cal[FPSTR("Voltage Coefficient")] = Cal_Voltage_Coefficient; + cal[FPSTR("Current at X (mV at ADC)")] = Cal_Current_calc_at_x; + cal[FPSTR("Current at X (mA)")] = Cal_Current_at_x; + } + } + + bool readFromConfig(JsonObject& root) { + int8_t tmpVoltagePin = VoltagePin; + int8_t tmpCurrentPin = CurrentPin; + + JsonObject top = root[FPSTR(_name)]; + if (top.isNull()) { + DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); + return false; + } + + enabled = top[FPSTR("Enabled")] | enabled; + + tmpVoltagePin = top[FPSTR("power_pins")][FPSTR("Voltage Pin")] | tmpVoltagePin; + tmpCurrentPin = top[FPSTR("power_pins")][FPSTR("Current Pin")] | tmpCurrentPin; + + Update_Interval_Mqtt = top[FPSTR("update rate in ms")][FPSTR("update rate of mqtt")] | Update_Interval_Mqtt; + Update_Interval_Main = top[FPSTR("update rate in ms")][FPSTR("update rate of main")] | Update_Interval_Main; + + JsonObject cal = top[FPSTR("calibration")]; + calibration_enable = cal[FPSTR("calibration Mode")] | calibration_enable; + + if (calibration_enable && !Cal_In_Progress) { + cal_adavnced = cal[FPSTR("Advanced")] | cal_adavnced; + + Cal_Zero_Points = cal["Zero Points"] | Cal_Zero_Points; + Cal_Measured_Voltage = cal["Measured Voltage"] | Cal_Measured_Voltage; + Cal_calibrate_Measured_Voltage = cal["Calibrate Voltage?"] | Cal_calibrate_Measured_Voltage; + Cal_Measured_Current = cal["Measured Current"] | Cal_Measured_Current; + Cal_calibrate_Measured_Current = cal["Calibrate Current?"] | Cal_calibrate_Measured_Current; + } + + if (calibration_enable && cal_adavnced && !Cal_In_Progress) { + Num_Readings_Cal = cal[FPSTR("Number of samples")] | Num_Readings_Cal; + Cal_min_Voltage_raw = cal[FPSTR("Zero Point of Voltage")] | Cal_min_Voltage_raw; + Cal_min_Current_calc = cal[FPSTR("Zero Point of Current")] | Cal_min_Current_calc; + Cal_Voltage_Coefficient = cal[FPSTR("Voltage Coefficient")] | Cal_Voltage_Coefficient; + Cal_Current_calc_at_x = cal[FPSTR("Current at X (mV at ADC)")] | Cal_Current_calc_at_x; + Cal_Current_at_x = cal[FPSTR("Current at X (mA)")] | Cal_Current_at_x; + } + + if (!initDone) { + // first run: reading from cfg.json + VoltagePin = tmpVoltagePin; + CurrentPin = tmpCurrentPin; + DEBUG_PRINTLN(F(" config loaded.")); + } else { + DEBUG_PRINTLN(F(" config (re)loaded.")); + // changing paramters from settings page + if (tmpVoltagePin != VoltagePin || tmpCurrentPin != CurrentPin) { + DEBUG_PRINTLN(F("Re-init Power pins.")); + // deallocate pin and release memory + pinManager.deallocatePin(VoltagePin, PinOwner::UM_Power_Measurement); + VoltagePin = tmpVoltagePin; + pinManager.deallocatePin(CurrentPin, PinOwner::UM_Power_Measurement); + CurrentPin = tmpCurrentPin; + // initialise + pinAlocation(); + } + } + + return true; + } + + #ifndef WLED_DISABLE_MQTT + void onMqttConnect(bool sessionPresent) { + publishPowerMeasurements(); + } + + void publishPowerMeasurements() { + if (WLED_MQTT_CONNECTED) { + char subuf[64]; + char payload[32]; + + // Publish Voltage + strcpy(subuf, mqttDeviceTopic); + strcat_P(subuf, PSTR("/power_measurement/voltage")); + dtostrf(Voltage, 6, 2, payload); // Convert float to string + mqtt->publish(subuf, 0, true, payload); + + // Publish Current + strcpy(subuf, mqttDeviceTopic); + strcat_P(subuf, PSTR("/power_measurement/current")); + dtostrf(Current, 6, 2, payload); // Convert float to string + mqtt->publish(subuf, 0, true, payload); + + // Publish Power + strcpy(subuf, mqttDeviceTopic); + strcat_P(subuf, PSTR("/power_measurement/power")); + dtostrf(Power, 6, 2, payload); // Convert float to string + mqtt->publish(subuf, 0, true, payload); + + // Publish kilowatthours + strcpy(subuf, mqttDeviceTopic); + strcat_P(subuf, PSTR("/power_measurement/kilowatthours")); + ultoa(kilowatthours, payload, 10); // Convert unsigned long to string + mqtt->publish(subuf, 0, true, payload); + } + } + #endif + + uint16_t getId() override { + return USERMOD_ID_POWER_MEASUREMENT; + } + + uint32_t readADC_Cal(int ADC_Raw) { + esp_adc_cal_characteristics_t adc_chars; + esp_adc_cal_value_t val_type = esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_6, ADC_WIDTH_BIT_12, ESP_ADC_CAL_VAL_DEFAULT_VREF, &adc_chars); + if (val_type == ESP_ADC_CAL_VAL_EFUSE_VREF) { + // Handle error if calibration_enable value is not available + DEBUG_PRINTF("Error: eFuse Vref not available"); + return 0; + } + return (esp_adc_cal_raw_to_voltage(ADC_Raw, &adc_chars)); + } +}; + +// String used more than once +const char UsermodPower_Measurement::_name[] PROGMEM = "Power Measurement"; +const char UsermodPower_Measurement::_no_data[] PROGMEM = "No data"; \ No newline at end of file diff --git a/usermods/Power_Measurement/assets/example_schematic.kicad_sch b/usermods/Power_Measurement/assets/example_schematic.kicad_sch new file mode 100644 index 0000000000..7b0c9bb933 --- /dev/null +++ b/usermods/Power_Measurement/assets/example_schematic.kicad_sch @@ -0,0 +1,3266 @@ +(kicad_sch + (version 20231120) + (generator "eeschema") + (generator_version "8.0") + (uuid "2360a543-140e-4488-b7ed-7d45263c2314") + (paper "A4") + (lib_symbols + (symbol "Device:C" + (pin_numbers hide) + (pin_names + (offset 0.254) + ) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (property "Reference" "C" + (at 0.635 2.54 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Value" "C" + (at 0.635 -2.54 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Footprint" "" + (at 0.9652 -3.81 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "~" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Unpolarized capacitor" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "ki_keywords" "cap capacitor" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "ki_fp_filters" "C_*" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (symbol "C_0_1" + (polyline + (pts + (xy -2.032 -0.762) (xy 2.032 -0.762) + ) + (stroke + (width 0.508) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy -2.032 0.762) (xy 2.032 0.762) + ) + (stroke + (width 0.508) + (type default) + ) + (fill + (type none) + ) + ) + ) + (symbol "C_1_1" + (pin passive line + (at 0 3.81 270) + (length 2.794) + (name "~" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin passive line + (at 0 -3.81 90) + (length 2.794) + (name "~" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "2" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + ) + ) + (symbol "Device:C_Polarized" + (pin_numbers hide) + (pin_names + (offset 0.254) + ) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (property "Reference" "C" + (at 0.635 2.54 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Value" "C_Polarized" + (at 0.635 -2.54 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Footprint" "" + (at 0.9652 -3.81 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "~" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Polarized capacitor" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "ki_keywords" "cap capacitor" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "ki_fp_filters" "CP_*" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (symbol "C_Polarized_0_1" + (rectangle + (start -2.286 0.508) + (end 2.286 1.016) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy -1.778 2.286) (xy -0.762 2.286) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy -1.27 2.794) (xy -1.27 1.778) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (rectangle + (start 2.286 -0.508) + (end -2.286 -1.016) + (stroke + (width 0) + (type default) + ) + (fill + (type outline) + ) + ) + ) + (symbol "C_Polarized_1_1" + (pin passive line + (at 0 3.81 270) + (length 2.794) + (name "~" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin passive line + (at 0 -3.81 90) + (length 2.794) + (name "~" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "2" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + ) + ) + (symbol "Device:Fuse" + (pin_numbers hide) + (pin_names + (offset 0) + ) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (property "Reference" "F" + (at 2.032 0 90) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "Fuse" + (at -1.905 0 90) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at -1.778 0 90) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "~" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Fuse" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "ki_keywords" "fuse" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "ki_fp_filters" "*Fuse*" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (symbol "Fuse_0_1" + (rectangle + (start -0.762 -2.54) + (end 0.762 2.54) + (stroke + (width 0.254) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy 0 2.54) (xy 0 -2.54) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + ) + (symbol "Fuse_1_1" + (pin passive line + (at 0 3.81 270) + (length 1.27) + (name "~" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin passive line + (at 0 -3.81 90) + (length 1.27) + (name "~" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "2" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + ) + ) + (symbol "Device:R_Small" + (pin_numbers hide) + (pin_names + (offset 0.254) hide) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (property "Reference" "R" + (at 0.762 0.508 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Value" "R_Small" + (at 0.762 -1.016 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Footprint" "" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "~" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Resistor, small symbol" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "ki_keywords" "R resistor" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "ki_fp_filters" "R_*" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (symbol "R_Small_0_1" + (rectangle + (start -0.762 1.778) + (end 0.762 -1.778) + (stroke + (width 0.2032) + (type default) + ) + (fill + (type none) + ) + ) + ) + (symbol "R_Small_1_1" + (pin passive line + (at 0 2.54 270) + (length 0.762) + (name "~" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin passive line + (at 0 -2.54 90) + (length 0.762) + (name "~" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "2" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + ) + ) + (symbol "Sensor_Current:ACS722xLCTR-10AB" + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (property "Reference" "U" + (at 2.54 11.43 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Value" "ACS722xLCTR-10AB" + (at 2.54 8.89 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Footprint" "Package_SO:SOIC-8_3.9x4.9mm_P1.27mm" + (at 2.54 -8.89 0) + (effects + (font + (size 1.27 1.27) + (italic yes) + ) + (justify left) + (hide yes) + ) + ) + (property "Datasheet" "http://www.allegromicro.com/~/media/Files/Datasheets/ACS722-Datasheet.ashx?la=en" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "±10A Bidirectional Hall-Effect Current Sensor, +3.3V supply, 132mV/A, SOIC-8" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "ki_keywords" "hall effect current monitor sensor isolated" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "ki_fp_filters" "SOIC*3.9x4.9mm*P1.27mm*" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (symbol "ACS722xLCTR-10AB_0_1" + (rectangle + (start -7.62 7.62) + (end 7.62 -7.62) + (stroke + (width 0.254) + (type default) + ) + (fill + (type background) + ) + ) + ) + (symbol "ACS722xLCTR-10AB_1_1" + (pin passive line + (at -10.16 5.08 0) + (length 2.54) + (name "IP+" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin passive line + (at -10.16 5.08 0) + (length 2.54) hide + (name "IP+" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "2" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin passive line + (at -10.16 -2.54 0) + (length 2.54) + (name "IP-" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "3" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin passive line + (at -10.16 -2.54 0) + (length 2.54) hide + (name "IP-" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "4" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin power_in line + (at 0 -10.16 90) + (length 2.54) + (name "GND" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "5" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin input line + (at 10.16 -2.54 180) + (length 2.54) + (name "BW_SEL" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "6" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin output line + (at 10.16 5.08 180) + (length 2.54) + (name "VIOUT" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "7" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin power_in line + (at 0 10.16 270) + (length 2.54) + (name "VCC" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "8" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + ) + ) + (symbol "power:+12V" + (power) + (pin_numbers hide) + (pin_names + (offset 0) hide) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (property "Reference" "#PWR" + (at 0 -3.81 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Value" "+12V" + (at 0 3.556 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Power symbol creates a global label with name \"+12V\"" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "ki_keywords" "global power" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (symbol "+12V_0_1" + (polyline + (pts + (xy -0.762 1.27) (xy 0 2.54) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy 0 0) (xy 0 2.54) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy 0 2.54) (xy 0.762 1.27) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + ) + (symbol "+12V_1_1" + (pin power_in line + (at 0 0 90) + (length 0) + (name "~" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + ) + ) + (symbol "power:+3.3V" + (power) + (pin_numbers hide) + (pin_names + (offset 0) hide) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (property "Reference" "#PWR" + (at 0 -3.81 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Value" "+3.3V" + (at 0 3.556 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Power symbol creates a global label with name \"+3.3V\"" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "ki_keywords" "global power" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (symbol "+3.3V_0_1" + (polyline + (pts + (xy -0.762 1.27) (xy 0 2.54) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy 0 0) (xy 0 2.54) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy 0 2.54) (xy 0.762 1.27) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + ) + (symbol "+3.3V_1_1" + (pin power_in line + (at 0 0 90) + (length 0) + (name "~" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + ) + ) + (symbol "power:GND" + (power) + (pin_numbers hide) + (pin_names + (offset 0) hide) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (property "Reference" "#PWR" + (at 0 -6.35 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Value" "GND" + (at 0 -3.81 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Power symbol creates a global label with name \"GND\" , ground" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "ki_keywords" "global power" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (symbol "GND_0_1" + (polyline + (pts + (xy 0 0) (xy 0 -1.27) (xy 1.27 -1.27) (xy 0 -2.54) (xy -1.27 -1.27) (xy 0 -1.27) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + ) + (symbol "GND_1_1" + (pin power_in line + (at 0 0 270) + (length 0) + (name "~" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + ) + ) + ) + (junction + (at 153.67 107.95) + (diameter 0) + (color 0 0 0 0) + (uuid "1b07dc7b-9356-4ae8-9e9e-b57b58329148") + ) + (junction + (at 113.03 76.2) + (diameter 0) + (color 0 0 0 0) + (uuid "2d154cf4-f5b7-4323-b3e9-9eac8537ff79") + ) + (junction + (at 193.04 76.2) + (diameter 0) + (color 0 0 0 0) + (uuid "2db59dc4-ddf7-4b94-a277-fb8c5b964aea") + ) + (junction + (at 125.73 76.2) + (diameter 0) + (color 0 0 0 0) + (uuid "3c475897-b6ee-4b26-aabb-a108d9c0d018") + ) + (junction + (at 113.03 87.63) + (diameter 0) + (color 0 0 0 0) + (uuid "c840daac-a559-40e2-bec1-819a61bd16f0") + ) + (junction + (at 139.7 107.95) + (diameter 0) + (color 0 0 0 0) + (uuid "cab8a5f1-5511-4c6e-a5cc-7be5a16b3808") + ) + (junction + (at 99.06 87.63) + (diameter 0) + (color 0 0 0 0) + (uuid "ec34c1bf-1f80-4e09-b75a-207e2d51eee0") + ) + (wire + (pts + (xy 91.44 76.2) (xy 95.25 76.2) + ) + (stroke + (width 0) + (type default) + ) + (uuid "09e68bce-a76f-468d-bd1f-3498704202a5") + ) + (wire + (pts + (xy 193.04 76.2) (xy 193.04 78.74) + ) + (stroke + (width 0) + (type default) + ) + (uuid "0e106dab-edbd-4402-baed-7cfb8c61d0fb") + ) + (wire + (pts + (xy 125.73 76.2) (xy 153.67 76.2) + ) + (stroke + (width 0) + (type default) + ) + (uuid "382618b9-c8aa-4d10-94fb-aa43deac4ea5") + ) + (wire + (pts + (xy 113.03 93.98) (xy 113.03 95.25) + ) + (stroke + (width 0) + (type default) + ) + (uuid "4bcdf674-c19f-4d9b-a412-c93407b26cea") + ) + (wire + (pts + (xy 139.7 111.76) (xy 139.7 107.95) + ) + (stroke + (width 0) + (type default) + ) + (uuid "4c39f15a-8351-403a-9d89-fe42188e85a2") + ) + (wire + (pts + (xy 125.73 87.63) (xy 125.73 86.36) + ) + (stroke + (width 0) + (type default) + ) + (uuid "4e53bfda-b832-4d7f-9baf-b0b01d5fdcb0") + ) + (wire + (pts + (xy 113.03 87.63) (xy 113.03 88.9) + ) + (stroke + (width 0) + (type default) + ) + (uuid "5241d529-f06d-413f-b170-da5727d7a0cf") + ) + (wire + (pts + (xy 193.04 76.2) (xy 200.66 76.2) + ) + (stroke + (width 0) + (type default) + ) + (uuid "548cd7f1-1ecc-42d5-9753-2b0cd4503ddb") + ) + (wire + (pts + (xy 193.04 87.63) (xy 193.04 86.36) + ) + (stroke + (width 0) + (type default) + ) + (uuid "633d110a-0f0e-41e8-b4ff-02cc1efb32dc") + ) + (wire + (pts + (xy 99.06 88.9) (xy 99.06 87.63) + ) + (stroke + (width 0) + (type default) + ) + (uuid "65593ed6-3469-4ce3-bb4d-c23dc0bac992") + ) + (wire + (pts + (xy 168.91 86.36) (xy 172.72 86.36) + ) + (stroke + (width 0) + (type default) + ) + (uuid "6e8528fd-746e-42ef-944b-bba142fa3f7f") + ) + (wire + (pts + (xy 144.78 86.36) (xy 144.78 83.82) + ) + (stroke + (width 0) + (type default) + ) + (uuid "6f12ed2d-ab77-4e6f-aa71-48b62d210b29") + ) + (wire + (pts + (xy 91.44 69.85) (xy 91.44 76.2) + ) + (stroke + (width 0) + (type default) + ) + (uuid "73e3453d-01b8-4011-ba4e-cbee25c77cd8") + ) + (wire + (pts + (xy 161.29 76.2) (xy 193.04 76.2) + ) + (stroke + (width 0) + (type default) + ) + (uuid "8fd82abc-2ff7-4ef7-b5f5-a611d22054ec") + ) + (wire + (pts + (xy 139.7 119.38) (xy 139.7 120.65) + ) + (stroke + (width 0) + (type default) + ) + (uuid "90109a20-1e33-4e88-b4cd-9b90e3d7275a") + ) + (wire + (pts + (xy 144.78 86.36) (xy 148.59 86.36) + ) + (stroke + (width 0) + (type default) + ) + (uuid "9068dd4f-f645-4baf-9b81-194e52f99ac6") + ) + (wire + (pts + (xy 125.73 76.2) (xy 125.73 78.74) + ) + (stroke + (width 0) + (type default) + ) + (uuid "970ce8ae-e94c-4e2f-bccc-11ad86d40bd2") + ) + (wire + (pts + (xy 138.43 107.95) (xy 139.7 107.95) + ) + (stroke + (width 0) + (type default) + ) + (uuid "973de61e-2303-4b5d-a962-023dd5f0e210") + ) + (wire + (pts + (xy 139.7 107.95) (xy 153.67 107.95) + ) + (stroke + (width 0) + (type default) + ) + (uuid "a43648d4-9f86-4160-9f51-0bdc2feee276") + ) + (wire + (pts + (xy 153.67 120.65) (xy 153.67 119.38) + ) + (stroke + (width 0) + (type default) + ) + (uuid "a46f7b49-f088-4aaf-8f04-e7699744187b") + ) + (wire + (pts + (xy 113.03 76.2) (xy 125.73 76.2) + ) + (stroke + (width 0) + (type default) + ) + (uuid "acb41acf-8594-442e-9d0a-dac74b40272f") + ) + (wire + (pts + (xy 99.06 87.63) (xy 113.03 87.63) + ) + (stroke + (width 0) + (type default) + ) + (uuid "c31c1aea-00f9-48f8-813e-f98f2c48866f") + ) + (wire + (pts + (xy 153.67 114.3) (xy 153.67 107.95) + ) + (stroke + (width 0) + (type default) + ) + (uuid "d703656b-3d51-44da-8626-4f4187e70bef") + ) + (wire + (pts + (xy 113.03 76.2) (xy 113.03 81.28) + ) + (stroke + (width 0) + (type default) + ) + (uuid "de954c79-1743-46bc-8ed2-bf76ba626004") + ) + (wire + (pts + (xy 99.06 96.52) (xy 99.06 97.79) + ) + (stroke + (width 0) + (type default) + ) + (uuid "e2283c3f-7214-4471-9452-f46ed59fad2d") + ) + (wire + (pts + (xy 95.25 87.63) (xy 99.06 87.63) + ) + (stroke + (width 0) + (type default) + ) + (uuid "e2dab855-68c4-4b49-a858-2d5e5b0d7830") + ) + (wire + (pts + (xy 113.03 86.36) (xy 113.03 87.63) + ) + (stroke + (width 0) + (type default) + ) + (uuid "e527980f-78d5-4f87-9f2f-cefb29677c5f") + ) + (wire + (pts + (xy 153.67 96.52) (xy 153.67 99.06) + ) + (stroke + (width 0) + (type default) + ) + (uuid "e68c62bd-c95a-4c05-bb15-b7259fbb2f1b") + ) + (wire + (pts + (xy 153.67 104.14) (xy 153.67 107.95) + ) + (stroke + (width 0) + (type default) + ) + (uuid "f2d91e7a-679e-42c3-9e11-69d10d26f58f") + ) + (wire + (pts + (xy 102.87 76.2) (xy 113.03 76.2) + ) + (stroke + (width 0) + (type default) + ) + (uuid "f370ca2a-777b-4e89-b9fc-7a1dbf365c8b") + ) + (wire + (pts + (xy 172.72 86.36) (xy 172.72 90.17) + ) + (stroke + (width 0) + (type default) + ) + (uuid "f540e002-e775-4d10-b433-96e7a62da590") + ) + (wire + (pts + (xy 161.29 96.52) (xy 161.29 99.06) + ) + (stroke + (width 0) + (type default) + ) + (uuid "f67391a0-35e9-4361-b34d-fe2d8426bffd") + ) + (text "0.33V - 2.97V\nZero Current Output Voltage = 1.65\n1.32V 10A swing" + (exclude_from_sim no) + (at 157.48 72.136 0) + (effects + (font + (size 1.27 1.27) + ) + ) + (uuid "67a60731-3184-4129-a037-edab08d612f7") + ) + (global_label "IO0X-Voltage" + (shape input) + (at 95.25 87.63 180) + (fields_autoplaced yes) + (effects + (font + (size 1.27 1.27) + ) + (justify right) + ) + (uuid "4cd9524b-ddbd-409c-b13a-b8f411a39577") + (property "Intersheetrefs" "${INTERSHEET_REFS}" + (at 79.323 87.63 0) + (effects + (font + (size 1.27 1.27) + ) + (justify right) + (hide yes) + ) + ) + ) + (global_label "VIN_Measured" + (shape output) + (at 200.66 76.2 0) + (fields_autoplaced yes) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + (uuid "534fa94a-2362-42d7-a892-5ec26944b9f7") + (property "Intersheetrefs" "${INTERSHEET_REFS}" + (at 216.5266 76.2 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + (hide yes) + ) + ) + ) + (global_label "IO0Y-Current" + (shape input) + (at 138.43 107.95 180) + (fields_autoplaced yes) + (effects + (font + (size 1.27 1.27) + ) + (justify right) + ) + (uuid "551f3e47-8f86-4a64-81ac-10348a5ca32d") + (property "Intersheetrefs" "${INTERSHEET_REFS}" + (at 122.6843 107.95 0) + (effects + (font + (size 1.27 1.27) + ) + (justify right) + (hide yes) + ) + ) + ) + (symbol + (lib_id "power:GND") + (at 139.7 120.65 0) + (mirror y) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "025dd409-0965-4b1b-b4b7-a70978a388b5") + (property "Reference" "#PWR05" + (at 139.7 127 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Value" "GND" + (at 139.7 124.714 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 139.7 120.65 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 139.7 120.65 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Power symbol creates a global label with name \"GND\" , ground" + (at 139.7 120.65 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "b7b23158-8ed1-4860-b766-6b49aea9a474") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "#PWR05") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "power:GND") + (at 125.73 87.63 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "0dd6af27-53f3-4b81-b860-78e0e2a7f4c6") + (property "Reference" "#PWR04" + (at 125.73 93.98 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Value" "GND" + (at 125.73 92.71 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 125.73 87.63 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 125.73 87.63 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Power symbol creates a global label with name \"GND\" , ground" + (at 125.73 87.63 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "2cb6c609-62a5-4196-9689-ca66eed822b5") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "#PWR04") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "power:GND") + (at 193.04 87.63 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "15b563e7-f8f8-4f8b-a72e-7a81c4e86445") + (property "Reference" "#PWR010" + (at 193.04 93.98 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Value" "GND" + (at 193.04 92.71 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 193.04 87.63 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 193.04 87.63 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Power symbol creates a global label with name \"GND\" , ground" + (at 193.04 87.63 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "e5bd921a-8aac-4bcc-b564-5c8680cfd9f7") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "#PWR010") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "power:+12V") + (at 91.44 69.85 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "407df456-dee1-42cc-a267-d98fa7533233") + (property "Reference" "#PWR01" + (at 91.44 73.66 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Value" "+VIN" + (at 91.186 64.77 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 91.44 69.85 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 91.44 69.85 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Power symbol creates a global label with name \"+12V\"" + (at 91.44 69.85 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "aaaa8249-1eb5-42b2-8084-a2c6a598cc0c") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "#PWR01") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "Sensor_Current:ACS722xLCTR-10AB") + (at 158.75 86.36 90) + (mirror x) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (fields_autoplaced yes) + (uuid "56afd411-3977-437b-bb3a-8f28c710835e") + (property "Reference" "U1" + (at 177.8 80.0414 90) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "ACS722xLCTR-10AB" + (at 177.8 82.5814 90) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "Package_SO:SOIC-8_3.9x4.9mm_P1.27mm" + (at 167.64 88.9 0) + (effects + (font + (size 1.27 1.27) + (italic yes) + ) + (justify left) + (hide yes) + ) + ) + (property "Datasheet" "http://www.allegromicro.com/~/media/Files/Datasheets/ACS722-Datasheet.ashx?la=en" + (at 158.75 86.36 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "±10A Bidirectional Hall-Effect Current Sensor, +3.3V supply, 132mV/A, SOIC-8" + (at 158.75 86.36 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "2" + (uuid "72f645c4-4abf-4ed6-8444-41711e1da047") + ) + (pin "5" + (uuid "b2582dca-89d3-426c-abbb-c3fc19df7eab") + ) + (pin "8" + (uuid "2a6be634-5f04-46c7-8438-214e2044f43d") + ) + (pin "6" + (uuid "e8f38fe2-ecef-4bd0-8f8c-99f4b004dbb6") + ) + (pin "4" + (uuid "97ba3573-4081-4ca9-96e6-5cdb0ea88803") + ) + (pin "7" + (uuid "142838f0-c367-4e18-bf7b-ee3e7e9d7db5") + ) + (pin "3" + (uuid "b75ee556-6002-41b0-96db-16a7d7beea40") + ) + (pin "1" + (uuid "cde11bc5-dfa3-4dd4-9658-9eb7c8fed8a7") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "U1") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "Device:C") + (at 125.73 82.55 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "5fad63ef-f4d6-456d-b079-2b3217b553f2") + (property "Reference" "C2" + (at 127 80.01 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Value" "100nF" + (at 127.254 85.09 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Footprint" "Capacitor_SMD:C_0805_2012Metric_Pad1.18x1.45mm_HandSolder" + (at 126.6952 86.36 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "~" + (at 125.73 82.55 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Unpolarized capacitor" + (at 125.73 82.55 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MANUFACTURER" "" + (at 125.73 82.55 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MAXIMUM_PACKAGE_HEIGHT" "" + (at 125.73 82.55 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "PARTREV" "" + (at 125.73 82.55 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "STANDARD" "" + (at 125.73 82.55 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "2" + (uuid "2cbac835-c714-454e-a097-f07e36ea31d4") + ) + (pin "1" + (uuid "d6ff9025-85f6-4ead-83e6-35d7daee59a5") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "C2") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "power:+3.3V") + (at 144.78 83.82 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "6017f11f-a4ef-46fa-bf62-b72b94c8bdfc") + (property "Reference" "#PWR06" + (at 144.78 87.63 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Value" "+3.3V" + (at 144.78 80.01 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 144.78 83.82 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 144.78 83.82 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Power symbol creates a global label with name \"+3.3V\"" + (at 144.78 83.82 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "0147def6-8cae-4008-9c8a-fb5fa5d131c6") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "#PWR06") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "Device:C_Polarized") + (at 193.04 82.55 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (fields_autoplaced yes) + (uuid "82226e50-0f54-4abb-ad36-90f7a425fbd1") + (property "Reference" "C4" + (at 196.85 80.3909 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Value" "1000uF" + (at 196.85 82.9309 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Footprint" "Capacitor_THT:CP_Radial_D10.0mm_P5.00mm" + (at 194.0052 86.36 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "~" + (at 193.04 82.55 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Polarized capacitor" + (at 193.04 82.55 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MANUFACTURER" "" + (at 193.04 82.55 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MAXIMUM_PACKAGE_HEIGHT" "" + (at 193.04 82.55 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "PARTREV" "" + (at 193.04 82.55 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "STANDARD" "" + (at 193.04 82.55 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "2" + (uuid "7ba46b09-0b39-4f1d-8c36-abf2cdc780df") + ) + (pin "1" + (uuid "683c3db6-7c90-44ef-ab5f-0091b7cee3da") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "C4") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "Device:Fuse") + (at 99.06 76.2 90) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "89446d9b-f672-45df-9d3a-68373b75dc56") + (property "Reference" "F1" + (at 99.06 74.168 90) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "10A" + (at 99.06 78.486 90) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "Fuse:Fuseholder_Littelfuse_Nano2_154x" + (at 99.06 77.978 90) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "~" + (at 99.06 76.2 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Fuse" + (at 99.06 76.2 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Can be found at" "https://www.digikey.cz/cs/products/detail/littelfuse-inc/0154010-DR/552684" + (at 99.06 76.2 90) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MANUFACTURER" "" + (at 99.06 76.2 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MAXIMUM_PACKAGE_HEIGHT" "" + (at 99.06 76.2 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "PARTREV" "" + (at 99.06 76.2 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "STANDARD" "" + (at 99.06 76.2 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "81156747-e392-4f5d-891c-de789d1cc7df") + ) + (pin "2" + (uuid "94ced624-93d2-41f8-ba92-b05615e69e68") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "F1") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "power:GND") + (at 99.06 97.79 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "90a71c55-8ccd-4b79-ad25-d3ab6fc7ddc7") + (property "Reference" "#PWR02" + (at 99.06 104.14 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Value" "GND" + (at 99.06 101.854 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 99.06 97.79 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 99.06 97.79 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Power symbol creates a global label with name \"GND\" , ground" + (at 99.06 97.79 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "d04aa806-3c19-4632-afa5-705e3b63bf07") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "#PWR02") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "Device:R_Small") + (at 113.03 91.44 0) + (mirror y) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (fields_autoplaced yes) + (uuid "916b1f8d-6642-488e-af1d-84205ae4c834") + (property "Reference" "R2" + (at 115.57 90.1699 0) + (effects + (font + (size 1.27 1.27) + ) + (justify right) + ) + ) + (property "Value" "10k" + (at 115.57 92.7099 0) + (effects + (font + (size 1.27 1.27) + ) + (justify right) + ) + ) + (property "Footprint" "Resistor_SMD:R_0805_2012Metric_Pad1.20x1.40mm_HandSolder" + (at 113.03 91.44 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "~" + (at 113.03 91.44 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Resistor, small symbol" + (at 113.03 91.44 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MANUFACTURER" "" + (at 113.03 91.44 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MAXIMUM_PACKAGE_HEIGHT" "" + (at 113.03 91.44 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "PARTREV" "" + (at 113.03 91.44 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "STANDARD" "" + (at 113.03 91.44 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "80da57cb-1acf-41ff-accf-438dbaa07ce8") + ) + (pin "2" + (uuid "d76cf884-9331-435b-a0fb-7c592b71d183") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "R2") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "Device:R_Small") + (at 153.67 116.84 0) + (mirror y) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "a2f78b9f-6eea-4b57-9350-7a56c8bf81fc") + (property "Reference" "R4" + (at 151.638 115.316 90) + (effects + (font + (size 1.27 1.27) + ) + (justify right) + ) + ) + (property "Value" "39k" + (at 155.702 114.808 90) + (effects + (font + (size 1.27 1.27) + ) + (justify right) + ) + ) + (property "Footprint" "Resistor_SMD:R_0805_2012Metric_Pad1.20x1.40mm_HandSolder" + (at 153.67 116.84 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "~" + (at 153.67 116.84 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Resistor, small symbol" + (at 153.67 116.84 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MANUFACTURER" "" + (at 153.67 116.84 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MAXIMUM_PACKAGE_HEIGHT" "" + (at 153.67 116.84 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "PARTREV" "" + (at 153.67 116.84 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "STANDARD" "" + (at 153.67 116.84 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "4663be56-5d7b-4dde-981e-26590e75cb77") + ) + (pin "2" + (uuid "5ebf0633-3a93-4867-9db1-76976ac19143") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "R4") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "Device:R_Small") + (at 113.03 83.82 0) + (mirror y) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (fields_autoplaced yes) + (uuid "b3f1d506-940d-4f50-933d-aeebc3a5b136") + (property "Reference" "R1" + (at 115.57 82.5499 0) + (effects + (font + (size 1.27 1.27) + ) + (justify right) + ) + ) + (property "Value" "220k" + (at 115.57 85.0899 0) + (effects + (font + (size 1.27 1.27) + ) + (justify right) + ) + ) + (property "Footprint" "Resistor_SMD:R_0805_2012Metric_Pad1.20x1.40mm_HandSolder" + (at 113.03 83.82 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "~" + (at 113.03 83.82 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Resistor, small symbol" + (at 113.03 83.82 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MANUFACTURER" "" + (at 113.03 83.82 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MAXIMUM_PACKAGE_HEIGHT" "" + (at 113.03 83.82 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "PARTREV" "" + (at 113.03 83.82 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "STANDARD" "" + (at 113.03 83.82 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "70c05b2c-4e80-403a-9e92-8542d45d1206") + ) + (pin "2" + (uuid "c3cfea87-c544-4c8e-9eab-e26c8713801d") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "R1") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "power:GND") + (at 172.72 90.17 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "b404e6ac-da9a-4b38-9fcc-469610d9c102") + (property "Reference" "#PWR09" + (at 172.72 96.52 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Value" "GND" + (at 172.974 93.98 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 172.72 90.17 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 172.72 90.17 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Power symbol creates a global label with name \"GND\" , ground" + (at 172.72 90.17 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "3c7ac4e1-3263-4e11-b848-2ee2b98850b3") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "#PWR09") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "power:+3.3V") + (at 161.29 99.06 180) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "d6678757-2ebb-4128-8030-6d8841bb6c75") + (property "Reference" "#PWR08" + (at 161.29 95.25 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Value" "+3.3V" + (at 161.29 102.87 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 161.29 99.06 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 161.29 99.06 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Power symbol creates a global label with name \"+3.3V\"" + (at 161.29 99.06 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "b2fbe9f3-3222-4ae1-b583-970abdfc56ca") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "#PWR08") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "Device:R_Small") + (at 153.67 101.6 0) + (mirror y) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "db6a3718-e8ff-44ae-aa13-b57f45e4bd09") + (property "Reference" "R3" + (at 151.384 100.076 90) + (effects + (font + (size 1.27 1.27) + ) + (justify right) + ) + ) + (property "Value" "51k" + (at 155.956 99.568 90) + (effects + (font + (size 1.27 1.27) + ) + (justify right) + ) + ) + (property "Footprint" "Resistor_SMD:R_0805_2012Metric_Pad1.20x1.40mm_HandSolder" + (at 153.67 101.6 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "~" + (at 153.67 101.6 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Resistor, small symbol" + (at 153.67 101.6 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MANUFACTURER" "" + (at 153.67 101.6 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MAXIMUM_PACKAGE_HEIGHT" "" + (at 153.67 101.6 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "PARTREV" "" + (at 153.67 101.6 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "STANDARD" "" + (at 153.67 101.6 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "6829eee2-48c3-428c-b395-aded09ed3da1") + ) + (pin "2" + (uuid "3f91b68a-7552-416e-b2ab-0e04c521fffd") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "R3") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "power:GND") + (at 113.03 95.25 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "dc1fb61f-7794-44b1-9c3f-9d8160f73445") + (property "Reference" "#PWR03" + (at 113.03 101.6 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Value" "GND" + (at 113.03 99.314 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 113.03 95.25 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 113.03 95.25 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Power symbol creates a global label with name \"GND\" , ground" + (at 113.03 95.25 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "7a1a9ab3-e4f2-4afb-ade6-51fe3c60c5d4") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "#PWR03") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "Device:C") + (at 139.7 115.57 0) + (mirror y) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "df3697c9-ae53-41d5-a282-976508338da5") + (property "Reference" "C3" + (at 143.764 113.03 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Value" "100nF" + (at 146.812 118.364 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Footprint" "Capacitor_SMD:C_0805_2012Metric_Pad1.18x1.45mm_HandSolder" + (at 138.7348 119.38 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "~" + (at 139.7 115.57 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Unpolarized capacitor" + (at 139.7 115.57 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MANUFACTURER" "" + (at 139.7 115.57 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MAXIMUM_PACKAGE_HEIGHT" "" + (at 139.7 115.57 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "PARTREV" "" + (at 139.7 115.57 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "STANDARD" "" + (at 139.7 115.57 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "2" + (uuid "234d1c0f-c5de-4dc7-b16b-da5dc27fab1c") + ) + (pin "1" + (uuid "2b679a4b-9e23-47bd-a24d-6eded824d69c") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "C3") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "Device:C") + (at 99.06 92.71 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "e78fc402-ce31-495a-81f4-a4788b0342b0") + (property "Reference" "C1" + (at 100.33 90.17 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Value" "100nF" + (at 100.584 95.25 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Footprint" "Capacitor_SMD:C_0805_2012Metric_Pad1.18x1.45mm_HandSolder" + (at 100.0252 96.52 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "~" + (at 99.06 92.71 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Unpolarized capacitor" + (at 99.06 92.71 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MANUFACTURER" "" + (at 99.06 92.71 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MAXIMUM_PACKAGE_HEIGHT" "" + (at 99.06 92.71 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "PARTREV" "" + (at 99.06 92.71 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "STANDARD" "" + (at 99.06 92.71 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "2" + (uuid "30c9c460-abed-45f7-a241-ed63555d80fc") + ) + (pin "1" + (uuid "f986288e-0ac9-4da5-9bcb-ea41a75dd84f") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "C1") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "power:GND") + (at 153.67 120.65 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "eb2dd32b-7175-4175-be49-b823cf64d88f") + (property "Reference" "#PWR07" + (at 153.67 127 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Value" "GND" + (at 153.67 124.714 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 153.67 120.65 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 153.67 120.65 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Power symbol creates a global label with name \"GND\" , ground" + (at 153.67 120.65 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "ea5a2d61-5227-4a80-af48-dc2dd1529c05") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "#PWR07") + (unit 1) + ) + ) + ) + ) + (sheet_instances + (path "/" + (page "1") + ) + ) +) diff --git a/usermods/Power_Measurement/assets/img/example schematic.png b/usermods/Power_Measurement/assets/img/example schematic.png new file mode 100644 index 0000000000000000000000000000000000000000..2a25116fbd2faab79222618dbe0d58fcd9a5e372 GIT binary patch literal 53358 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 CieuRT literal 0 HcmV?d00001 diff --git a/usermods/Power_Measurement/assets/img/screenshot 1 - info.jpg b/usermods/Power_Measurement/assets/img/screenshot 1 - info.jpg new file mode 100644 index 0000000000000000000000000000000000000000..278d572463d64d8a3555800090a85f93a3c7c883 GIT binary patch literal 48340 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%0DcYFVo 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 Djb;n* literal 0 HcmV?d00001 diff --git a/usermods/Power_Measurement/assets/img/screenshot 2 - settings.png b/usermods/Power_Measurement/assets/img/screenshot 2 - settings.png new file mode 100644 index 0000000000000000000000000000000000000000..bb9a59d08d23589651194f560e655aa461e79b3e GIT binary patch literal 24762 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 literal 0 HcmV?d00001 diff --git a/usermods/Power_Measurement/assets/img/screenshot 3 - settings.png b/usermods/Power_Measurement/assets/img/screenshot 3 - settings.png new file mode 100644 index 0000000000000000000000000000000000000000..96c05e067edfa7f006bf99710edb306e72726208 GIT binary patch literal 35492 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 KsE~af_`d+&RW*D7 literal 0 HcmV?d00001 diff --git a/usermods/Power_Measurement/readme.md b/usermods/Power_Measurement/readme.md new file mode 100644 index 0000000000..4df846179c --- /dev/null +++ b/usermods/Power_Measurement/readme.md @@ -0,0 +1,94 @@ +# Voltage and Current Measurement using ESP's Internal ADC + +This usermod is a proof of concept that measures current and voltage using a voltage divider and a current sensor. It leverages the ESP's internal ADC to read the voltage and current, calculate power and energy consumption, and optionally publish these measurements via MQTT. + +## Features + +- **Voltage and Current Measurement**: Reads voltage and current using ADC pins of the ESP. +- **Power and Energy Calculation**: Calculates power (in watts) and energy consumption (in kilowatt-hours). +- **Calibration Support**: Offers calibration for more accurate measurements. +- **MQTT Publishing**: Publishes voltage, current, power, and energy measurements to an MQTT broker. +- **Debug Information**: Provides debug output via serial for monitoring raw and processed data. + +## Dependencies + +- **ESP32 ADC Calibration Library**: Requires `esp_adc_cal.h` for ADC calibration, which is a standard ESP-IDF library. + +## Configuration + +### Pins + +- `VOLTAGE_PIN`: ADC pin for voltage measurement (default: `0`) +- `CURRENT_PIN`: ADC pin for current measurement (default: `1`) + +### Constants + +- `NUM_READINGS`: Number of readings for moving average (default: `10`) +- `NUM_READINGS_CAL`: Number of readings for calibration (default: `100`) +- `UPDATE_INTERVAL_MAIN`: Main update interval in milliseconds (default: `100`) +- `UPDATE_INTERVAL_MQTT`: MQTT update interval in milliseconds (default: `60000`) + +## Installation + +Add `-D USERMOD_CURRENT_MEASUREMENT` to `build_flags` in `platformio_override.ini`. + +Or copy the example `platformio_override.ini` to the root directory. This file should be placed in the same directory as `platformio.ini`. + +## Hardware Example + +![Example Schematic](./assets/img/example%20schematic.png "Example Schematic") + +## Define Your Options + +- `USERMOD_POWER_MEASUREMENT`: Enable the usermod + +All parameters and calibration variables can be configured at runtime via the Usermods settings page. + +## Calibration + +### Calibration Steps + +1. Enable the `Calibration mode` checkbox. +2. Connect the controller via USB. +3. Disconnect the power supply (Vin) from the LED strip. +4. Select the option to `Calibrate Zero Points`. +5. Reconnect the power supply to the LED strip and set it to white and full brightness. +6. Measure the voltage and current and enter the values into the `Measured Voltage` and `Measured Current` fields. +7. Check the checkboxes for `Calibrate Voltage` and `Calibrate Current`. + +### Advanced + +![Advanced Calibration](./assets/img/screenshot%203%20-%20settings.png "Advanced Calibration") + +## MQTT + +If MQTT is enabled, the module will periodically publish the voltage, current, power, and energy measurements to the configured MQTT broker. + +## Debugging + +Enable `WLED_DEBUG` to print detailed debug information to the serial output, including raw and calculated values for voltage, current, power, and energy. + +## Screenshots + +Info screen | Settings page +:-----------------------------------------------------------------------:|:-------------------------------------------------------------------------------: +![Info screen](./assets/img/screenshot%201%20-%20info.jpg "Info screen") | ![Settings page](./assets/img/screenshot%202%20-%20settings.png "Settings page") + +## To-Do + +- [ ] Pin manager doesn't work properly. +- [ ] Implement a brightness limiter based on current. +- [ ] Make the code use less flash memory. + +## Changelog + +19.8.2024 +- Initial PR + +## License + +This code was created by Tomáš Kuchta. + +## Contributions + +- Tomáš Kuchta (Initial idea) \ No newline at end of file diff --git a/wled00/const.h b/wled00/const.h index 388b64c820..913af2fd64 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -152,6 +152,7 @@ #define USERMOD_ID_INTERNAL_TEMPERATURE 42 //Usermod "usermod_internal_temperature.h" #define USERMOD_ID_LDR_DUSK_DAWN 43 //Usermod "usermod_LDR_Dusk_Dawn_v2.h" #define USERMOD_ID_STAIRWAY_WIPE 44 //Usermod "stairway-wipe-usermod-v2.h" +#define USERMOD_ID_POWER_MEASUREMENT 45 //Usermod "Power_measurement.h" //Access point behavior #define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot diff --git a/wled00/pin_manager.h b/wled00/pin_manager.h index 39f2c6ec60..c54c6ae9cd 100644 --- a/wled00/pin_manager.h +++ b/wled00/pin_manager.h @@ -62,6 +62,7 @@ enum struct PinOwner : uint8_t { UM_SdCard = USERMOD_ID_SD_CARD, // 0x25 // Usermod "usermod_sd_card.h" UM_PWM_OUTPUTS = USERMOD_ID_PWM_OUTPUTS, // 0x26 // Usermod "usermod_pwm_outputs.h" UM_LDR_DUSK_DAWN = USERMOD_ID_LDR_DUSK_DAWN // 0x2B // Usermod "usermod_LDR_Dusk_Dawn_v2.h" + UM_Power_Measurement = USERMOD_ID_POWER_MEASUREMENT // 0x2C // Usermod "Power_measurement.h" }; static_assert(0u == static_cast(PinOwner::None), "PinOwner::None must be zero, so default array initialization works as expected"); diff --git a/wled00/usermods_list.cpp b/wled00/usermods_list.cpp index db016f5508..f04be9b383 100644 --- a/wled00/usermods_list.cpp +++ b/wled00/usermods_list.cpp @@ -181,6 +181,10 @@ #include "../usermods/Internal_Temperature_v2/usermod_internal_temperature.h" #endif +#ifdef USERMOD_POWER_MEASUREMENT + #include "../usermods/Power_Measurement/Power_Measurement.h" +#endif + #if defined(WLED_USE_SD_MMC) || defined(WLED_USE_SD_SPI) // This include of SD.h and SD_MMC.h must happen here, else they won't be // resolved correctly (when included in mod's header only) @@ -387,4 +391,8 @@ void registerUsermods() #ifdef USERMOD_STAIRCASE_WIPE usermods.add(new StairwayWipeUsermod()); #endif + + #ifdef USERMOD_POWER_MEASUREMENT + usermods.add(new UsermodPower_Measurement()); + #endif } From 4c0bd3ad882b1ec89fb4ec5a7cd88c8d41c35e2e Mon Sep 17 00:00:00 2001 From: Tomas Kuchta Date: Sun, 18 Aug 2024 22:32:05 +0200 Subject: [PATCH 002/145] Add Power Measurement usermod - Implement functions to measure power consumption --- .../Power_Measurement/Power_Measurement.h | 576 +++ .../assets/example_schematic.kicad_sch | 3266 +++++++++++++++++ .../assets/img/example schematic.png | Bin 0 -> 53358 bytes .../assets/img/screenshot 1 - info.jpg | Bin 0 -> 48340 bytes .../assets/img/screenshot 2 - settings.png | Bin 0 -> 24762 bytes .../assets/img/screenshot 3 - settings.png | Bin 0 -> 35492 bytes usermods/Power_Measurement/readme.md | 94 + wled00/const.h | 1 + wled00/pin_manager.h | 1 + wled00/usermods_list.cpp | 8 + 10 files changed, 3946 insertions(+) create mode 100644 usermods/Power_Measurement/Power_Measurement.h create mode 100644 usermods/Power_Measurement/assets/example_schematic.kicad_sch create mode 100644 usermods/Power_Measurement/assets/img/example schematic.png create mode 100644 usermods/Power_Measurement/assets/img/screenshot 1 - info.jpg create mode 100644 usermods/Power_Measurement/assets/img/screenshot 2 - settings.png create mode 100644 usermods/Power_Measurement/assets/img/screenshot 3 - settings.png create mode 100644 usermods/Power_Measurement/readme.md diff --git a/usermods/Power_Measurement/Power_Measurement.h b/usermods/Power_Measurement/Power_Measurement.h new file mode 100644 index 0000000000..d54ac790c5 --- /dev/null +++ b/usermods/Power_Measurement/Power_Measurement.h @@ -0,0 +1,576 @@ +// Filename: Power_Measurement.h +// This code was cocreated by github copilot and created by Tomáš Kuchta +#pragma once + +#include "wled.h" +#include "esp_adc_cal.h" + +#ifndef CURRENT_PIN + #define CURRENT_PIN 1 +#endif + +#ifndef VOLTAGE_PIN + #define VOLTAGE_PIN 0 +#endif + +#define NUM_READINGS 10 +#define NUM_READINGS_CAL 100 +#define ADC_MAX_VALUE (pow(2, ADCResolution) - 1) // For 12-bit ADC, the max value is 4095 +#define UPDATE_INTERVAL_MAIN 100 +#define UPDATE_INTERVAL_MQTT 60000 + +class UsermodPower_Measurement : public Usermod { + private: + bool initDone = false; + unsigned long lastTime_slow = 0; + unsigned long lastTime_main = 0; + unsigned long lastTime_energy = 0; + unsigned long lastTime_mqtt = 0; + boolean enabled = true; + boolean calibration_enable = false; + boolean cal_adavnced = false; + + int Voltage_raw = 0; + float AverageVoltage_raw = 0; + int Voltage_raw_adj = 0; + int Voltage_calc = 0; + + int Current_raw = 0; + float AverageCurrent_raw = 0; + int Current_calc = 0; + + float voltageReadings_raw[NUM_READINGS]; + float currentReadings_raw[NUM_READINGS]; + int readIndex = 0; + float totalVoltage_raw = 0; + float totalCurrent_raw = 0; + + // Low-pass filter variables + float alpha = 0.1; + float filtered_Voltage_raw = 0; + float filtered_Current_raw = 0; + + unsigned long long wattmiliseconds = 0; //energy counter in watt milliseconds + + + // calibration variables + int Num_Readings_Cal = NUM_READINGS_CAL; + bool Cal_In_Progress = false; + bool Cal_Zero_Points = false; + bool Cal_calibrate_Measured_Voltage = false; + bool Cal_calibrate_Measured_Current = false; + float Cal_Measured_Voltage = 0; + float Cal_Measured_Current = 0; + + float Cal_min_Voltage_raw = 17; + float Cal_min_Current_calc = 718; + + float Cal_Voltage_raw_averaged = 0; + float Cal_Voltage_calc_averaged = 0; + float Cal_Current_calc_averaged = 0; + + int Cal_Current_at_x = 1000; + int Cal_Current_calc_at_x = 775; + float Cal_Voltage_Coefficient = 22.97; + + // averiging variables + float Cal_Voltage_raw_Readings_Avg[NUM_READINGS_CAL]; + float Cal_Voltage_calc_Readings_Avg[NUM_READINGS_CAL]; + float Cal_Current_calc_Readings_Avg[NUM_READINGS_CAL]; + int Cal_Read_Index = 0; + float Cal_Total_Voltage_raw = 0; + float Cal_Total_Voltage_calc = 0; + float Cal_Total_Current_calc = 0; + + int8_t VoltagePin = VOLTAGE_PIN; + int8_t CurrentPin = CURRENT_PIN; + + int Update_Interval_Mqtt = UPDATE_INTERVAL_MQTT; + int Update_Interval_Main = UPDATE_INTERVAL_MAIN; + + // String used more than once + static const char _name[] PROGMEM; + static const char _no_data[] PROGMEM; + + public: + int ADCResolution = 12; + int ADCAttenuation = ADC_6db; + + //For usage in other parts of the main code + float Voltage = 0; + float Current = 0; + float Power = 0; + unsigned long kilowatthours = 0; + + void setup() { + analogReadResolution(ADCResolution); + analogSetAttenuation(static_cast(ADCAttenuation)); // Set the ADC attenuation (ADC_ATTEN_DB_6 = 0 mV ~ 1300 mV) + + // Initialize all readings to 0: + for (int i = 0; i < NUM_READINGS; i++) { + voltageReadings_raw[i] = 0; + currentReadings_raw[i] = 0; + } + + Current_raw = 1800; + filtered_Current_raw = 1800; + + Cal_Zero_Points = false; + Cal_calibrate_Measured_Voltage = false; + Cal_calibrate_Measured_Current = false; + Cal_In_Progress = false; + Num_Readings_Cal = NUM_READINGS_CAL; + + + #ifdef WLED_DEBUG + esp_adc_cal_characteristics_t adc_chars; + esp_adc_cal_value_t val_type = esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_6, ADC_WIDTH_BIT_12, ESP_ADC_CAL_VAL_DEFAULT_VREF, &adc_chars); + + if (val_type == ESP_ADC_CAL_VAL_EFUSE_VREF) { + // eFuse Vref is available + DEBUG_PRINTLN(F("PM: Using eFuse Vref for ADC calibration_enable")); + } else if (val_type == ESP_ADC_CAL_VAL_EFUSE_TP) { + // Two Point calibration_enable is available + DEBUG_PRINTLN(F("PM: Using Two Point calibration_enable for ADC calibration_enable")); + } else { + // Default Vref is used + DEBUG_PRINTLN(F("PM: Using default Vref for ADC calibration_enable")); + } + #endif + + + if (enabled) { + pinAlocation(); + } + + initDone = true; + + } + + void loop() { + if (!enabled || strip.isUpdating()) return; + + unsigned long currentTime = millis(); + + #ifdef WLED_DEBUG + if (currentTime - lastTime_slow >= 1000) { + printDebugInfo(); + lastTime_slow = currentTime; + } + #endif + + #ifndef WLED_DISABLE_MQTT + if (currentTime - lastTime_mqtt >= Update_Interval_Mqtt) { + publishPowerMeasurements(); + lastTime_mqtt = currentTime; + } + #endif + + if (currentTime - lastTime_main >= Update_Interval_Main) { + updateReadings(); + + if (Cal_Zero_Points || Cal_calibrate_Measured_Voltage || Cal_calibrate_Measured_Current) calibration(); + + lastTime_main = currentTime; + } + + + } + + void pinAlocation() { + DEBUG_PRINTLN(F("Allocating power pins...")); + if (VoltagePin >= 0 && pinManager.allocatePin(VoltagePin, true, PinOwner::UM_Power_Measurement)) { + DEBUG_PRINT(F("Voltage pin allocated: ")); + DEBUG_PRINTLN(VoltagePin); + } else { + if (VoltagePin >= 0) { + DEBUG_PRINTLN(F("Voltage pin allocation failed.")); + } + VoltagePin = -1; // allocation failed, disable + } + + if (CurrentPin >= 0 && pinManager.allocatePin(CurrentPin, true, PinOwner::UM_Power_Measurement)) { + DEBUG_PRINT(F("Current pin allocated: ")); + DEBUG_PRINTLN(CurrentPin); + } else { + if (CurrentPin >= 0) { + DEBUG_PRINTLN(F("Current pin allocation failed.")); + } + CurrentPin = -1; // allocation failed, disable + } + } + + + void printDebugInfo() { + DEBUG_PRINT(F("Voltage raw: ")); + DEBUG_PRINTLN(Voltage_raw); + DEBUG_PRINTLN(AverageVoltage_raw); + DEBUG_PRINT(F("Calc: ")); + DEBUG_PRINTLN(Voltage_calc); + DEBUG_PRINT(F("Voltage: ")); + DEBUG_PRINTLN(Voltage); + + DEBUG_PRINT(F("Current raw: ")); + DEBUG_PRINTLN(Current_raw); + DEBUG_PRINTLN(AverageCurrent_raw); + DEBUG_PRINT(F("Calc: ")); + DEBUG_PRINTLN(Current_calc); + DEBUG_PRINT("Current: "); + DEBUG_PRINTLN(Current); + + DEBUG_PRINT("Power: "); + DEBUG_PRINTLN(Power); + + DEBUG_PRINT("Energy: "); + DEBUG_PRINTLN(kilowatthours); + DEBUG_PRINT("Energy Wms: "); + DEBUG_PRINTLN(wattmiliseconds); + } + + void updateReadings() { + // Measure the voltage and current and store them in the arrays for the moving average and convert via map function: + totalVoltage_raw -= voltageReadings_raw[readIndex]; + totalCurrent_raw -= currentReadings_raw[readIndex]; + + if (VoltagePin == -1) { + Voltage_raw = 0; + DEBUG_PRINTLN("Voltage pin not allocated"); + } else { + Voltage_raw = analogRead(VoltagePin); + } + + if (CurrentPin == -1) { + Current_raw = 0; + DEBUG_PRINTLN("Current pin not allocated"); + } else { + Current_raw = analogRead(CurrentPin); + } + + if (millis() > 1000) { // To avoid the initial spike in readings + filtered_Voltage_raw = (alpha * Voltage_raw) + ((1 - alpha) * filtered_Voltage_raw); + filtered_Current_raw = (alpha * Current_raw) + ((1 - alpha) * filtered_Current_raw); + } else { + filtered_Voltage_raw = Voltage_raw; + filtered_Current_raw = Current_raw; + } + + voltageReadings_raw[readIndex] = filtered_Voltage_raw; + currentReadings_raw[readIndex] = filtered_Current_raw; + + totalVoltage_raw += filtered_Voltage_raw; + totalCurrent_raw += filtered_Current_raw; + + AverageVoltage_raw = totalVoltage_raw / NUM_READINGS; + AverageCurrent_raw = totalCurrent_raw / NUM_READINGS; + + readIndex = (readIndex + 1) % NUM_READINGS; + + Voltage_raw_adj = map(AverageVoltage_raw, Cal_min_Voltage_raw, ADC_MAX_VALUE, 0, ADC_MAX_VALUE); + if (Voltage_raw_adj < 0) Voltage_raw_adj = 0; + Voltage_calc = readADC_Cal(Voltage_raw_adj); + Voltage = (Voltage_calc / 1000.0) * Cal_Voltage_Coefficient; + if (Voltage < 0.05) Voltage = 0; + Voltage = round(Voltage * 100.0) / 100.0; // Round to 2 decimal places + if (VoltagePin == -1) Voltage = 0; + + Current_calc = readADC_Cal(AverageCurrent_raw); + Current = (map(Current_calc, Cal_min_Current_calc, Cal_Current_calc_at_x, 0, Cal_Current_at_x)) / 1000.0; + if (Current > -0.1 && Current < 0.05) { + Current = 0; + } + Current = round(Current * 100.0) / 100.0; + if (CurrentPin == -1) Current = 0; + + // Calculate power + Power = Voltage * Current; + Power = round(Power * 100.0) / 100.0; + + // Calculate energy - dont do it when led is off + if (Power > 0) { + unsigned long elapsedTime = millis() - lastTime_energy; + wattmiliseconds += Power * elapsedTime; + } + lastTime_energy = millis(); + + if (wattmiliseconds >= 3600000000) { // 3,600,000 milliseconds = 1 hour + kilowatthours += wattmiliseconds / 3600000000; // Convert watt-milliseconds to kilowatt-hours (1 watt-millisecond = 1/3,600,000,000 kilowatt-hours) + wattmiliseconds = 0; + } + } + + void calibration() { + if (Num_Readings_Cal == NUM_READINGS_CAL) { + DEBUG_PRINTLN("calibration_enable started"); + Cal_In_Progress = true; + serializeConfig(); // To update the checkboxes in the config + } + if (Num_Readings_Cal > 0) { + Num_Readings_Cal--; + // Average the readings + Cal_Total_Voltage_raw -= Cal_Voltage_raw_Readings_Avg[Cal_Read_Index]; + Cal_Total_Voltage_calc -= Cal_Voltage_calc_Readings_Avg[Cal_Read_Index]; + Cal_Total_Current_calc -= Cal_Current_calc_Readings_Avg[Cal_Read_Index]; + + Cal_Voltage_raw_Readings_Avg[Cal_Read_Index] = Voltage_raw; + Cal_Voltage_calc_Readings_Avg[Cal_Read_Index] = Voltage_calc; + Cal_Current_calc_Readings_Avg[Cal_Read_Index] = Current_calc; + + Cal_Total_Voltage_raw += Voltage_raw; + Cal_Total_Voltage_calc += Voltage_calc; + Cal_Total_Current_calc += Current_calc; + + Cal_Read_Index = (Cal_Read_Index + 1) % NUM_READINGS_CAL; + + Cal_Voltage_raw_averaged = Cal_Total_Voltage_raw / NUM_READINGS_CAL; + Cal_Voltage_calc_averaged = Cal_Total_Voltage_calc / NUM_READINGS_CAL; + Cal_Current_calc_averaged = Cal_Total_Current_calc / NUM_READINGS_CAL; + } else { + + DEBUG_PRINTLN("calibration_enable Flags:"); + DEBUG_PRINTLN(Cal_In_Progress); + DEBUG_PRINTLN(Num_Readings_Cal); + DEBUG_PRINTLN(Cal_Zero_Points); + DEBUG_PRINTLN(Cal_calibrate_Measured_Voltage); + DEBUG_PRINTLN(Cal_calibrate_Measured_Current); + DEBUG_PRINTLN("the averaged values are:"); + DEBUG_PRINTLN(Cal_Voltage_raw_averaged); + DEBUG_PRINTLN(Cal_Voltage_calc_averaged); + DEBUG_PRINTLN(Cal_Current_calc_averaged); + DEBUG_PRINTLN("Inputed values are:"); + DEBUG_PRINTLN(Cal_Measured_Voltage); + DEBUG_PRINTLN(Cal_Measured_Current); + + Calibration_calculation(); + + Cal_In_Progress = false; + Cal_Zero_Points = false; + Cal_calibrate_Measured_Voltage = false; + Cal_calibrate_Measured_Current = false; + Num_Readings_Cal = NUM_READINGS_CAL; + serializeConfig(); // To update the checkboxes in the config + + DEBUG_PRINTLN("calibration_enable finished"); + } + } + + void Calibration_calculation() { + DEBUG_PRINTLN("Calculating calibration_enable values"); + + if (Cal_calibrate_Measured_Current) { + Cal_Current_at_x = Cal_Measured_Current * 1000; + Cal_Current_calc_at_x = Cal_Current_calc_averaged; + + } else if (Cal_calibrate_Measured_Voltage) { + Cal_Voltage_Coefficient = (Cal_Measured_Voltage * 1000) / Cal_Voltage_calc_averaged; + + } else if (Cal_Zero_Points) { + Cal_min_Voltage_raw = Cal_Voltage_raw_averaged; + Cal_min_Current_calc = Cal_Current_calc_averaged; + } else { + DEBUG_PRINTLN("No calibration_enable values selected - but that should not happen"); + } + + } + + void addToJsonInfo(JsonObject& root) { + if (!enabled)return; + + JsonObject user = root["u"]; + if (user.isNull())user = root.createNestedObject("u"); + + JsonArray Current_json = user.createNestedArray(FPSTR("Current")); + if (Current_raw == 0 || CurrentPin == -1) { + Current_json.add(F(_no_data)); + } else if (Current_raw >= (ADC_MAX_VALUE - 3)) { + Current_json.add(F("Overrange")); + } else { + Current_json.add(Current); + Current_json.add(F(" A")); + } + + JsonArray Voltage_json = user.createNestedArray(FPSTR("Voltage")); + if (Voltage_raw == 0 || VoltagePin == -1) { + Voltage_json.add(F(_no_data)); + } else if (Voltage_raw >= (ADC_MAX_VALUE - 3)) { + Voltage_json.add(F(_no_data)); + } else if (Voltage_raw >= (ADC_MAX_VALUE - 3)) { + Voltage_json.add(F("Overrange")); + } else { + Voltage_json.add(Voltage); + Voltage_json.add(F(" V")); + } + + if (calibration_enable) { + JsonArray Current_raw_json = user.createNestedArray(FPSTR("Current raw")); + Current_raw_json.add(Current_raw); + Current_raw_json.add(" -> " + String(Current_calc)); + + JsonArray Voltage_raw_json = user.createNestedArray(FPSTR("Voltage raw")); + Voltage_raw_json.add(Voltage_raw); + Voltage_raw_json.add(" -> " + String(Voltage_calc)); + } + + JsonArray Power_json = user.createNestedArray(FPSTR("Power")); + Power_json.add(Power); + Power_json.add(F(" W")); + + JsonArray Energy_json = user.createNestedArray(FPSTR("Energy")); + Energy_json.add(kilowatthours); + Energy_json.add(F(" kWh")); + } + + void addToConfig(JsonObject& root) { + JsonObject top = root.createNestedObject(FPSTR(_name)); + top[FPSTR("enabled")] = enabled; + + JsonObject power_pins = top.createNestedObject(FPSTR("power_pins")); + power_pins[FPSTR("Voltage Pin")] = VoltagePin; + power_pins[FPSTR("Current Pin")] = CurrentPin; + + JsonObject update = top.createNestedObject(FPSTR("update rate in ms")); + update[FPSTR("update rate of mqtt")] = Update_Interval_Mqtt; + update[FPSTR("update rate of main")] = Update_Interval_Main; + + JsonObject cal = top.createNestedObject(FPSTR("calibration")); + cal[FPSTR("calibration Mode")] = calibration_enable; + if (calibration_enable && !Cal_In_Progress) { + cal[FPSTR("Advanced")] = cal_adavnced; + + cal["Zero Points"] = Cal_Zero_Points; + cal["Measured Voltage"] = Cal_Measured_Voltage; + cal["Calibrate Voltage?"] = Cal_calibrate_Measured_Voltage; + cal["Measured Current"] = Cal_Measured_Current; + cal["Calibrate Current?"] = Cal_calibrate_Measured_Current; + } else if (Cal_In_Progress) { + cal[FPSTR("calibration_enable is in progress please wait")] = "Non-Essential Data Entry Zone: Just for Kicks and Giggles"; + } + + if (calibration_enable && cal_adavnced && !Cal_In_Progress) { + cal[FPSTR("Number of samples")] = Num_Readings_Cal; + cal[FPSTR("Zero Point of Voltage")] = Cal_min_Voltage_raw; + cal[FPSTR("Zero Point of Current")] = Cal_min_Current_calc; + cal[FPSTR("Voltage Coefficient")] = Cal_Voltage_Coefficient; + cal[FPSTR("Current at X (mV at ADC)")] = Cal_Current_calc_at_x; + cal[FPSTR("Current at X (mA)")] = Cal_Current_at_x; + } + } + + bool readFromConfig(JsonObject& root) { + int8_t tmpVoltagePin = VoltagePin; + int8_t tmpCurrentPin = CurrentPin; + + JsonObject top = root[FPSTR(_name)]; + if (top.isNull()) { + DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); + return false; + } + + enabled = top[FPSTR("Enabled")] | enabled; + + tmpVoltagePin = top[FPSTR("power_pins")][FPSTR("Voltage Pin")] | tmpVoltagePin; + tmpCurrentPin = top[FPSTR("power_pins")][FPSTR("Current Pin")] | tmpCurrentPin; + + Update_Interval_Mqtt = top[FPSTR("update rate in ms")][FPSTR("update rate of mqtt")] | Update_Interval_Mqtt; + Update_Interval_Main = top[FPSTR("update rate in ms")][FPSTR("update rate of main")] | Update_Interval_Main; + + JsonObject cal = top[FPSTR("calibration")]; + calibration_enable = cal[FPSTR("calibration Mode")] | calibration_enable; + + if (calibration_enable && !Cal_In_Progress) { + cal_adavnced = cal[FPSTR("Advanced")] | cal_adavnced; + + Cal_Zero_Points = cal["Zero Points"] | Cal_Zero_Points; + Cal_Measured_Voltage = cal["Measured Voltage"] | Cal_Measured_Voltage; + Cal_calibrate_Measured_Voltage = cal["Calibrate Voltage?"] | Cal_calibrate_Measured_Voltage; + Cal_Measured_Current = cal["Measured Current"] | Cal_Measured_Current; + Cal_calibrate_Measured_Current = cal["Calibrate Current?"] | Cal_calibrate_Measured_Current; + } + + if (calibration_enable && cal_adavnced && !Cal_In_Progress) { + Num_Readings_Cal = cal[FPSTR("Number of samples")] | Num_Readings_Cal; + Cal_min_Voltage_raw = cal[FPSTR("Zero Point of Voltage")] | Cal_min_Voltage_raw; + Cal_min_Current_calc = cal[FPSTR("Zero Point of Current")] | Cal_min_Current_calc; + Cal_Voltage_Coefficient = cal[FPSTR("Voltage Coefficient")] | Cal_Voltage_Coefficient; + Cal_Current_calc_at_x = cal[FPSTR("Current at X (mV at ADC)")] | Cal_Current_calc_at_x; + Cal_Current_at_x = cal[FPSTR("Current at X (mA)")] | Cal_Current_at_x; + } + + if (!initDone) { + // first run: reading from cfg.json + VoltagePin = tmpVoltagePin; + CurrentPin = tmpCurrentPin; + DEBUG_PRINTLN(F(" config loaded.")); + } else { + DEBUG_PRINTLN(F(" config (re)loaded.")); + // changing paramters from settings page + if (tmpVoltagePin != VoltagePin || tmpCurrentPin != CurrentPin) { + DEBUG_PRINTLN(F("Re-init Power pins.")); + // deallocate pin and release memory + pinManager.deallocatePin(VoltagePin, PinOwner::UM_Power_Measurement); + VoltagePin = tmpVoltagePin; + pinManager.deallocatePin(CurrentPin, PinOwner::UM_Power_Measurement); + CurrentPin = tmpCurrentPin; + // initialise + pinAlocation(); + } + } + + return true; + } + + #ifndef WLED_DISABLE_MQTT + void onMqttConnect(bool sessionPresent) { + publishPowerMeasurements(); + } + + void publishPowerMeasurements() { + if (WLED_MQTT_CONNECTED) { + char subuf[64]; + char payload[32]; + + // Publish Voltage + strcpy(subuf, mqttDeviceTopic); + strcat_P(subuf, PSTR("/power_measurement/voltage")); + dtostrf(Voltage, 6, 2, payload); // Convert float to string + mqtt->publish(subuf, 0, true, payload); + + // Publish Current + strcpy(subuf, mqttDeviceTopic); + strcat_P(subuf, PSTR("/power_measurement/current")); + dtostrf(Current, 6, 2, payload); // Convert float to string + mqtt->publish(subuf, 0, true, payload); + + // Publish Power + strcpy(subuf, mqttDeviceTopic); + strcat_P(subuf, PSTR("/power_measurement/power")); + dtostrf(Power, 6, 2, payload); // Convert float to string + mqtt->publish(subuf, 0, true, payload); + + // Publish kilowatthours + strcpy(subuf, mqttDeviceTopic); + strcat_P(subuf, PSTR("/power_measurement/kilowatthours")); + ultoa(kilowatthours, payload, 10); // Convert unsigned long to string + mqtt->publish(subuf, 0, true, payload); + } + } + #endif + + uint16_t getId() override { + return USERMOD_ID_POWER_MEASUREMENT; + } + + uint32_t readADC_Cal(int ADC_Raw) { + esp_adc_cal_characteristics_t adc_chars; + esp_adc_cal_value_t val_type = esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_6, ADC_WIDTH_BIT_12, ESP_ADC_CAL_VAL_DEFAULT_VREF, &adc_chars); + if (val_type == ESP_ADC_CAL_VAL_EFUSE_VREF) { + // Handle error if calibration_enable value is not available + DEBUG_PRINTF("Error: eFuse Vref not available"); + return 0; + } + return (esp_adc_cal_raw_to_voltage(ADC_Raw, &adc_chars)); + } +}; + +// String used more than once +const char UsermodPower_Measurement::_name[] PROGMEM = "Power Measurement"; +const char UsermodPower_Measurement::_no_data[] PROGMEM = "No data"; \ No newline at end of file diff --git a/usermods/Power_Measurement/assets/example_schematic.kicad_sch b/usermods/Power_Measurement/assets/example_schematic.kicad_sch new file mode 100644 index 0000000000..7b0c9bb933 --- /dev/null +++ b/usermods/Power_Measurement/assets/example_schematic.kicad_sch @@ -0,0 +1,3266 @@ +(kicad_sch + (version 20231120) + (generator "eeschema") + (generator_version "8.0") + (uuid "2360a543-140e-4488-b7ed-7d45263c2314") + (paper "A4") + (lib_symbols + (symbol "Device:C" + (pin_numbers hide) + (pin_names + (offset 0.254) + ) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (property "Reference" "C" + (at 0.635 2.54 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Value" "C" + (at 0.635 -2.54 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Footprint" "" + (at 0.9652 -3.81 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "~" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Unpolarized capacitor" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "ki_keywords" "cap capacitor" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "ki_fp_filters" "C_*" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (symbol "C_0_1" + (polyline + (pts + (xy -2.032 -0.762) (xy 2.032 -0.762) + ) + (stroke + (width 0.508) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy -2.032 0.762) (xy 2.032 0.762) + ) + (stroke + (width 0.508) + (type default) + ) + (fill + (type none) + ) + ) + ) + (symbol "C_1_1" + (pin passive line + (at 0 3.81 270) + (length 2.794) + (name "~" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin passive line + (at 0 -3.81 90) + (length 2.794) + (name "~" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "2" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + ) + ) + (symbol "Device:C_Polarized" + (pin_numbers hide) + (pin_names + (offset 0.254) + ) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (property "Reference" "C" + (at 0.635 2.54 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Value" "C_Polarized" + (at 0.635 -2.54 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Footprint" "" + (at 0.9652 -3.81 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "~" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Polarized capacitor" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "ki_keywords" "cap capacitor" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "ki_fp_filters" "CP_*" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (symbol "C_Polarized_0_1" + (rectangle + (start -2.286 0.508) + (end 2.286 1.016) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy -1.778 2.286) (xy -0.762 2.286) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy -1.27 2.794) (xy -1.27 1.778) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (rectangle + (start 2.286 -0.508) + (end -2.286 -1.016) + (stroke + (width 0) + (type default) + ) + (fill + (type outline) + ) + ) + ) + (symbol "C_Polarized_1_1" + (pin passive line + (at 0 3.81 270) + (length 2.794) + (name "~" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin passive line + (at 0 -3.81 90) + (length 2.794) + (name "~" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "2" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + ) + ) + (symbol "Device:Fuse" + (pin_numbers hide) + (pin_names + (offset 0) + ) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (property "Reference" "F" + (at 2.032 0 90) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "Fuse" + (at -1.905 0 90) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at -1.778 0 90) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "~" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Fuse" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "ki_keywords" "fuse" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "ki_fp_filters" "*Fuse*" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (symbol "Fuse_0_1" + (rectangle + (start -0.762 -2.54) + (end 0.762 2.54) + (stroke + (width 0.254) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy 0 2.54) (xy 0 -2.54) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + ) + (symbol "Fuse_1_1" + (pin passive line + (at 0 3.81 270) + (length 1.27) + (name "~" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin passive line + (at 0 -3.81 90) + (length 1.27) + (name "~" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "2" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + ) + ) + (symbol "Device:R_Small" + (pin_numbers hide) + (pin_names + (offset 0.254) hide) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (property "Reference" "R" + (at 0.762 0.508 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Value" "R_Small" + (at 0.762 -1.016 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Footprint" "" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "~" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Resistor, small symbol" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "ki_keywords" "R resistor" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "ki_fp_filters" "R_*" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (symbol "R_Small_0_1" + (rectangle + (start -0.762 1.778) + (end 0.762 -1.778) + (stroke + (width 0.2032) + (type default) + ) + (fill + (type none) + ) + ) + ) + (symbol "R_Small_1_1" + (pin passive line + (at 0 2.54 270) + (length 0.762) + (name "~" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin passive line + (at 0 -2.54 90) + (length 0.762) + (name "~" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "2" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + ) + ) + (symbol "Sensor_Current:ACS722xLCTR-10AB" + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (property "Reference" "U" + (at 2.54 11.43 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Value" "ACS722xLCTR-10AB" + (at 2.54 8.89 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Footprint" "Package_SO:SOIC-8_3.9x4.9mm_P1.27mm" + (at 2.54 -8.89 0) + (effects + (font + (size 1.27 1.27) + (italic yes) + ) + (justify left) + (hide yes) + ) + ) + (property "Datasheet" "http://www.allegromicro.com/~/media/Files/Datasheets/ACS722-Datasheet.ashx?la=en" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "±10A Bidirectional Hall-Effect Current Sensor, +3.3V supply, 132mV/A, SOIC-8" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "ki_keywords" "hall effect current monitor sensor isolated" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "ki_fp_filters" "SOIC*3.9x4.9mm*P1.27mm*" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (symbol "ACS722xLCTR-10AB_0_1" + (rectangle + (start -7.62 7.62) + (end 7.62 -7.62) + (stroke + (width 0.254) + (type default) + ) + (fill + (type background) + ) + ) + ) + (symbol "ACS722xLCTR-10AB_1_1" + (pin passive line + (at -10.16 5.08 0) + (length 2.54) + (name "IP+" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin passive line + (at -10.16 5.08 0) + (length 2.54) hide + (name "IP+" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "2" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin passive line + (at -10.16 -2.54 0) + (length 2.54) + (name "IP-" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "3" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin passive line + (at -10.16 -2.54 0) + (length 2.54) hide + (name "IP-" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "4" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin power_in line + (at 0 -10.16 90) + (length 2.54) + (name "GND" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "5" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin input line + (at 10.16 -2.54 180) + (length 2.54) + (name "BW_SEL" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "6" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin output line + (at 10.16 5.08 180) + (length 2.54) + (name "VIOUT" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "7" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin power_in line + (at 0 10.16 270) + (length 2.54) + (name "VCC" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "8" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + ) + ) + (symbol "power:+12V" + (power) + (pin_numbers hide) + (pin_names + (offset 0) hide) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (property "Reference" "#PWR" + (at 0 -3.81 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Value" "+12V" + (at 0 3.556 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Power symbol creates a global label with name \"+12V\"" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "ki_keywords" "global power" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (symbol "+12V_0_1" + (polyline + (pts + (xy -0.762 1.27) (xy 0 2.54) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy 0 0) (xy 0 2.54) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy 0 2.54) (xy 0.762 1.27) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + ) + (symbol "+12V_1_1" + (pin power_in line + (at 0 0 90) + (length 0) + (name "~" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + ) + ) + (symbol "power:+3.3V" + (power) + (pin_numbers hide) + (pin_names + (offset 0) hide) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (property "Reference" "#PWR" + (at 0 -3.81 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Value" "+3.3V" + (at 0 3.556 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Power symbol creates a global label with name \"+3.3V\"" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "ki_keywords" "global power" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (symbol "+3.3V_0_1" + (polyline + (pts + (xy -0.762 1.27) (xy 0 2.54) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy 0 0) (xy 0 2.54) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy 0 2.54) (xy 0.762 1.27) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + ) + (symbol "+3.3V_1_1" + (pin power_in line + (at 0 0 90) + (length 0) + (name "~" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + ) + ) + (symbol "power:GND" + (power) + (pin_numbers hide) + (pin_names + (offset 0) hide) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (property "Reference" "#PWR" + (at 0 -6.35 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Value" "GND" + (at 0 -3.81 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Power symbol creates a global label with name \"GND\" , ground" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "ki_keywords" "global power" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (symbol "GND_0_1" + (polyline + (pts + (xy 0 0) (xy 0 -1.27) (xy 1.27 -1.27) (xy 0 -2.54) (xy -1.27 -1.27) (xy 0 -1.27) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + ) + (symbol "GND_1_1" + (pin power_in line + (at 0 0 270) + (length 0) + (name "~" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + ) + ) + ) + (junction + (at 153.67 107.95) + (diameter 0) + (color 0 0 0 0) + (uuid "1b07dc7b-9356-4ae8-9e9e-b57b58329148") + ) + (junction + (at 113.03 76.2) + (diameter 0) + (color 0 0 0 0) + (uuid "2d154cf4-f5b7-4323-b3e9-9eac8537ff79") + ) + (junction + (at 193.04 76.2) + (diameter 0) + (color 0 0 0 0) + (uuid "2db59dc4-ddf7-4b94-a277-fb8c5b964aea") + ) + (junction + (at 125.73 76.2) + (diameter 0) + (color 0 0 0 0) + (uuid "3c475897-b6ee-4b26-aabb-a108d9c0d018") + ) + (junction + (at 113.03 87.63) + (diameter 0) + (color 0 0 0 0) + (uuid "c840daac-a559-40e2-bec1-819a61bd16f0") + ) + (junction + (at 139.7 107.95) + (diameter 0) + (color 0 0 0 0) + (uuid "cab8a5f1-5511-4c6e-a5cc-7be5a16b3808") + ) + (junction + (at 99.06 87.63) + (diameter 0) + (color 0 0 0 0) + (uuid "ec34c1bf-1f80-4e09-b75a-207e2d51eee0") + ) + (wire + (pts + (xy 91.44 76.2) (xy 95.25 76.2) + ) + (stroke + (width 0) + (type default) + ) + (uuid "09e68bce-a76f-468d-bd1f-3498704202a5") + ) + (wire + (pts + (xy 193.04 76.2) (xy 193.04 78.74) + ) + (stroke + (width 0) + (type default) + ) + (uuid "0e106dab-edbd-4402-baed-7cfb8c61d0fb") + ) + (wire + (pts + (xy 125.73 76.2) (xy 153.67 76.2) + ) + (stroke + (width 0) + (type default) + ) + (uuid "382618b9-c8aa-4d10-94fb-aa43deac4ea5") + ) + (wire + (pts + (xy 113.03 93.98) (xy 113.03 95.25) + ) + (stroke + (width 0) + (type default) + ) + (uuid "4bcdf674-c19f-4d9b-a412-c93407b26cea") + ) + (wire + (pts + (xy 139.7 111.76) (xy 139.7 107.95) + ) + (stroke + (width 0) + (type default) + ) + (uuid "4c39f15a-8351-403a-9d89-fe42188e85a2") + ) + (wire + (pts + (xy 125.73 87.63) (xy 125.73 86.36) + ) + (stroke + (width 0) + (type default) + ) + (uuid "4e53bfda-b832-4d7f-9baf-b0b01d5fdcb0") + ) + (wire + (pts + (xy 113.03 87.63) (xy 113.03 88.9) + ) + (stroke + (width 0) + (type default) + ) + (uuid "5241d529-f06d-413f-b170-da5727d7a0cf") + ) + (wire + (pts + (xy 193.04 76.2) (xy 200.66 76.2) + ) + (stroke + (width 0) + (type default) + ) + (uuid "548cd7f1-1ecc-42d5-9753-2b0cd4503ddb") + ) + (wire + (pts + (xy 193.04 87.63) (xy 193.04 86.36) + ) + (stroke + (width 0) + (type default) + ) + (uuid "633d110a-0f0e-41e8-b4ff-02cc1efb32dc") + ) + (wire + (pts + (xy 99.06 88.9) (xy 99.06 87.63) + ) + (stroke + (width 0) + (type default) + ) + (uuid "65593ed6-3469-4ce3-bb4d-c23dc0bac992") + ) + (wire + (pts + (xy 168.91 86.36) (xy 172.72 86.36) + ) + (stroke + (width 0) + (type default) + ) + (uuid "6e8528fd-746e-42ef-944b-bba142fa3f7f") + ) + (wire + (pts + (xy 144.78 86.36) (xy 144.78 83.82) + ) + (stroke + (width 0) + (type default) + ) + (uuid "6f12ed2d-ab77-4e6f-aa71-48b62d210b29") + ) + (wire + (pts + (xy 91.44 69.85) (xy 91.44 76.2) + ) + (stroke + (width 0) + (type default) + ) + (uuid "73e3453d-01b8-4011-ba4e-cbee25c77cd8") + ) + (wire + (pts + (xy 161.29 76.2) (xy 193.04 76.2) + ) + (stroke + (width 0) + (type default) + ) + (uuid "8fd82abc-2ff7-4ef7-b5f5-a611d22054ec") + ) + (wire + (pts + (xy 139.7 119.38) (xy 139.7 120.65) + ) + (stroke + (width 0) + (type default) + ) + (uuid "90109a20-1e33-4e88-b4cd-9b90e3d7275a") + ) + (wire + (pts + (xy 144.78 86.36) (xy 148.59 86.36) + ) + (stroke + (width 0) + (type default) + ) + (uuid "9068dd4f-f645-4baf-9b81-194e52f99ac6") + ) + (wire + (pts + (xy 125.73 76.2) (xy 125.73 78.74) + ) + (stroke + (width 0) + (type default) + ) + (uuid "970ce8ae-e94c-4e2f-bccc-11ad86d40bd2") + ) + (wire + (pts + (xy 138.43 107.95) (xy 139.7 107.95) + ) + (stroke + (width 0) + (type default) + ) + (uuid "973de61e-2303-4b5d-a962-023dd5f0e210") + ) + (wire + (pts + (xy 139.7 107.95) (xy 153.67 107.95) + ) + (stroke + (width 0) + (type default) + ) + (uuid "a43648d4-9f86-4160-9f51-0bdc2feee276") + ) + (wire + (pts + (xy 153.67 120.65) (xy 153.67 119.38) + ) + (stroke + (width 0) + (type default) + ) + (uuid "a46f7b49-f088-4aaf-8f04-e7699744187b") + ) + (wire + (pts + (xy 113.03 76.2) (xy 125.73 76.2) + ) + (stroke + (width 0) + (type default) + ) + (uuid "acb41acf-8594-442e-9d0a-dac74b40272f") + ) + (wire + (pts + (xy 99.06 87.63) (xy 113.03 87.63) + ) + (stroke + (width 0) + (type default) + ) + (uuid "c31c1aea-00f9-48f8-813e-f98f2c48866f") + ) + (wire + (pts + (xy 153.67 114.3) (xy 153.67 107.95) + ) + (stroke + (width 0) + (type default) + ) + (uuid "d703656b-3d51-44da-8626-4f4187e70bef") + ) + (wire + (pts + (xy 113.03 76.2) (xy 113.03 81.28) + ) + (stroke + (width 0) + (type default) + ) + (uuid "de954c79-1743-46bc-8ed2-bf76ba626004") + ) + (wire + (pts + (xy 99.06 96.52) (xy 99.06 97.79) + ) + (stroke + (width 0) + (type default) + ) + (uuid "e2283c3f-7214-4471-9452-f46ed59fad2d") + ) + (wire + (pts + (xy 95.25 87.63) (xy 99.06 87.63) + ) + (stroke + (width 0) + (type default) + ) + (uuid "e2dab855-68c4-4b49-a858-2d5e5b0d7830") + ) + (wire + (pts + (xy 113.03 86.36) (xy 113.03 87.63) + ) + (stroke + (width 0) + (type default) + ) + (uuid "e527980f-78d5-4f87-9f2f-cefb29677c5f") + ) + (wire + (pts + (xy 153.67 96.52) (xy 153.67 99.06) + ) + (stroke + (width 0) + (type default) + ) + (uuid "e68c62bd-c95a-4c05-bb15-b7259fbb2f1b") + ) + (wire + (pts + (xy 153.67 104.14) (xy 153.67 107.95) + ) + (stroke + (width 0) + (type default) + ) + (uuid "f2d91e7a-679e-42c3-9e11-69d10d26f58f") + ) + (wire + (pts + (xy 102.87 76.2) (xy 113.03 76.2) + ) + (stroke + (width 0) + (type default) + ) + (uuid "f370ca2a-777b-4e89-b9fc-7a1dbf365c8b") + ) + (wire + (pts + (xy 172.72 86.36) (xy 172.72 90.17) + ) + (stroke + (width 0) + (type default) + ) + (uuid "f540e002-e775-4d10-b433-96e7a62da590") + ) + (wire + (pts + (xy 161.29 96.52) (xy 161.29 99.06) + ) + (stroke + (width 0) + (type default) + ) + (uuid "f67391a0-35e9-4361-b34d-fe2d8426bffd") + ) + (text "0.33V - 2.97V\nZero Current Output Voltage = 1.65\n1.32V 10A swing" + (exclude_from_sim no) + (at 157.48 72.136 0) + (effects + (font + (size 1.27 1.27) + ) + ) + (uuid "67a60731-3184-4129-a037-edab08d612f7") + ) + (global_label "IO0X-Voltage" + (shape input) + (at 95.25 87.63 180) + (fields_autoplaced yes) + (effects + (font + (size 1.27 1.27) + ) + (justify right) + ) + (uuid "4cd9524b-ddbd-409c-b13a-b8f411a39577") + (property "Intersheetrefs" "${INTERSHEET_REFS}" + (at 79.323 87.63 0) + (effects + (font + (size 1.27 1.27) + ) + (justify right) + (hide yes) + ) + ) + ) + (global_label "VIN_Measured" + (shape output) + (at 200.66 76.2 0) + (fields_autoplaced yes) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + (uuid "534fa94a-2362-42d7-a892-5ec26944b9f7") + (property "Intersheetrefs" "${INTERSHEET_REFS}" + (at 216.5266 76.2 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + (hide yes) + ) + ) + ) + (global_label "IO0Y-Current" + (shape input) + (at 138.43 107.95 180) + (fields_autoplaced yes) + (effects + (font + (size 1.27 1.27) + ) + (justify right) + ) + (uuid "551f3e47-8f86-4a64-81ac-10348a5ca32d") + (property "Intersheetrefs" "${INTERSHEET_REFS}" + (at 122.6843 107.95 0) + (effects + (font + (size 1.27 1.27) + ) + (justify right) + (hide yes) + ) + ) + ) + (symbol + (lib_id "power:GND") + (at 139.7 120.65 0) + (mirror y) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "025dd409-0965-4b1b-b4b7-a70978a388b5") + (property "Reference" "#PWR05" + (at 139.7 127 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Value" "GND" + (at 139.7 124.714 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 139.7 120.65 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 139.7 120.65 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Power symbol creates a global label with name \"GND\" , ground" + (at 139.7 120.65 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "b7b23158-8ed1-4860-b766-6b49aea9a474") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "#PWR05") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "power:GND") + (at 125.73 87.63 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "0dd6af27-53f3-4b81-b860-78e0e2a7f4c6") + (property "Reference" "#PWR04" + (at 125.73 93.98 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Value" "GND" + (at 125.73 92.71 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 125.73 87.63 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 125.73 87.63 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Power symbol creates a global label with name \"GND\" , ground" + (at 125.73 87.63 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "2cb6c609-62a5-4196-9689-ca66eed822b5") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "#PWR04") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "power:GND") + (at 193.04 87.63 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "15b563e7-f8f8-4f8b-a72e-7a81c4e86445") + (property "Reference" "#PWR010" + (at 193.04 93.98 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Value" "GND" + (at 193.04 92.71 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 193.04 87.63 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 193.04 87.63 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Power symbol creates a global label with name \"GND\" , ground" + (at 193.04 87.63 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "e5bd921a-8aac-4bcc-b564-5c8680cfd9f7") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "#PWR010") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "power:+12V") + (at 91.44 69.85 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "407df456-dee1-42cc-a267-d98fa7533233") + (property "Reference" "#PWR01" + (at 91.44 73.66 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Value" "+VIN" + (at 91.186 64.77 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 91.44 69.85 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 91.44 69.85 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Power symbol creates a global label with name \"+12V\"" + (at 91.44 69.85 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "aaaa8249-1eb5-42b2-8084-a2c6a598cc0c") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "#PWR01") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "Sensor_Current:ACS722xLCTR-10AB") + (at 158.75 86.36 90) + (mirror x) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (fields_autoplaced yes) + (uuid "56afd411-3977-437b-bb3a-8f28c710835e") + (property "Reference" "U1" + (at 177.8 80.0414 90) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "ACS722xLCTR-10AB" + (at 177.8 82.5814 90) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "Package_SO:SOIC-8_3.9x4.9mm_P1.27mm" + (at 167.64 88.9 0) + (effects + (font + (size 1.27 1.27) + (italic yes) + ) + (justify left) + (hide yes) + ) + ) + (property "Datasheet" "http://www.allegromicro.com/~/media/Files/Datasheets/ACS722-Datasheet.ashx?la=en" + (at 158.75 86.36 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "±10A Bidirectional Hall-Effect Current Sensor, +3.3V supply, 132mV/A, SOIC-8" + (at 158.75 86.36 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "2" + (uuid "72f645c4-4abf-4ed6-8444-41711e1da047") + ) + (pin "5" + (uuid "b2582dca-89d3-426c-abbb-c3fc19df7eab") + ) + (pin "8" + (uuid "2a6be634-5f04-46c7-8438-214e2044f43d") + ) + (pin "6" + (uuid "e8f38fe2-ecef-4bd0-8f8c-99f4b004dbb6") + ) + (pin "4" + (uuid "97ba3573-4081-4ca9-96e6-5cdb0ea88803") + ) + (pin "7" + (uuid "142838f0-c367-4e18-bf7b-ee3e7e9d7db5") + ) + (pin "3" + (uuid "b75ee556-6002-41b0-96db-16a7d7beea40") + ) + (pin "1" + (uuid "cde11bc5-dfa3-4dd4-9658-9eb7c8fed8a7") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "U1") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "Device:C") + (at 125.73 82.55 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "5fad63ef-f4d6-456d-b079-2b3217b553f2") + (property "Reference" "C2" + (at 127 80.01 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Value" "100nF" + (at 127.254 85.09 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Footprint" "Capacitor_SMD:C_0805_2012Metric_Pad1.18x1.45mm_HandSolder" + (at 126.6952 86.36 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "~" + (at 125.73 82.55 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Unpolarized capacitor" + (at 125.73 82.55 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MANUFACTURER" "" + (at 125.73 82.55 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MAXIMUM_PACKAGE_HEIGHT" "" + (at 125.73 82.55 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "PARTREV" "" + (at 125.73 82.55 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "STANDARD" "" + (at 125.73 82.55 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "2" + (uuid "2cbac835-c714-454e-a097-f07e36ea31d4") + ) + (pin "1" + (uuid "d6ff9025-85f6-4ead-83e6-35d7daee59a5") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "C2") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "power:+3.3V") + (at 144.78 83.82 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "6017f11f-a4ef-46fa-bf62-b72b94c8bdfc") + (property "Reference" "#PWR06" + (at 144.78 87.63 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Value" "+3.3V" + (at 144.78 80.01 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 144.78 83.82 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 144.78 83.82 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Power symbol creates a global label with name \"+3.3V\"" + (at 144.78 83.82 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "0147def6-8cae-4008-9c8a-fb5fa5d131c6") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "#PWR06") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "Device:C_Polarized") + (at 193.04 82.55 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (fields_autoplaced yes) + (uuid "82226e50-0f54-4abb-ad36-90f7a425fbd1") + (property "Reference" "C4" + (at 196.85 80.3909 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Value" "1000uF" + (at 196.85 82.9309 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Footprint" "Capacitor_THT:CP_Radial_D10.0mm_P5.00mm" + (at 194.0052 86.36 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "~" + (at 193.04 82.55 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Polarized capacitor" + (at 193.04 82.55 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MANUFACTURER" "" + (at 193.04 82.55 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MAXIMUM_PACKAGE_HEIGHT" "" + (at 193.04 82.55 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "PARTREV" "" + (at 193.04 82.55 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "STANDARD" "" + (at 193.04 82.55 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "2" + (uuid "7ba46b09-0b39-4f1d-8c36-abf2cdc780df") + ) + (pin "1" + (uuid "683c3db6-7c90-44ef-ab5f-0091b7cee3da") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "C4") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "Device:Fuse") + (at 99.06 76.2 90) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "89446d9b-f672-45df-9d3a-68373b75dc56") + (property "Reference" "F1" + (at 99.06 74.168 90) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "10A" + (at 99.06 78.486 90) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "Fuse:Fuseholder_Littelfuse_Nano2_154x" + (at 99.06 77.978 90) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "~" + (at 99.06 76.2 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Fuse" + (at 99.06 76.2 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Can be found at" "https://www.digikey.cz/cs/products/detail/littelfuse-inc/0154010-DR/552684" + (at 99.06 76.2 90) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MANUFACTURER" "" + (at 99.06 76.2 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MAXIMUM_PACKAGE_HEIGHT" "" + (at 99.06 76.2 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "PARTREV" "" + (at 99.06 76.2 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "STANDARD" "" + (at 99.06 76.2 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "81156747-e392-4f5d-891c-de789d1cc7df") + ) + (pin "2" + (uuid "94ced624-93d2-41f8-ba92-b05615e69e68") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "F1") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "power:GND") + (at 99.06 97.79 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "90a71c55-8ccd-4b79-ad25-d3ab6fc7ddc7") + (property "Reference" "#PWR02" + (at 99.06 104.14 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Value" "GND" + (at 99.06 101.854 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 99.06 97.79 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 99.06 97.79 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Power symbol creates a global label with name \"GND\" , ground" + (at 99.06 97.79 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "d04aa806-3c19-4632-afa5-705e3b63bf07") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "#PWR02") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "Device:R_Small") + (at 113.03 91.44 0) + (mirror y) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (fields_autoplaced yes) + (uuid "916b1f8d-6642-488e-af1d-84205ae4c834") + (property "Reference" "R2" + (at 115.57 90.1699 0) + (effects + (font + (size 1.27 1.27) + ) + (justify right) + ) + ) + (property "Value" "10k" + (at 115.57 92.7099 0) + (effects + (font + (size 1.27 1.27) + ) + (justify right) + ) + ) + (property "Footprint" "Resistor_SMD:R_0805_2012Metric_Pad1.20x1.40mm_HandSolder" + (at 113.03 91.44 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "~" + (at 113.03 91.44 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Resistor, small symbol" + (at 113.03 91.44 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MANUFACTURER" "" + (at 113.03 91.44 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MAXIMUM_PACKAGE_HEIGHT" "" + (at 113.03 91.44 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "PARTREV" "" + (at 113.03 91.44 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "STANDARD" "" + (at 113.03 91.44 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "80da57cb-1acf-41ff-accf-438dbaa07ce8") + ) + (pin "2" + (uuid "d76cf884-9331-435b-a0fb-7c592b71d183") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "R2") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "Device:R_Small") + (at 153.67 116.84 0) + (mirror y) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "a2f78b9f-6eea-4b57-9350-7a56c8bf81fc") + (property "Reference" "R4" + (at 151.638 115.316 90) + (effects + (font + (size 1.27 1.27) + ) + (justify right) + ) + ) + (property "Value" "39k" + (at 155.702 114.808 90) + (effects + (font + (size 1.27 1.27) + ) + (justify right) + ) + ) + (property "Footprint" "Resistor_SMD:R_0805_2012Metric_Pad1.20x1.40mm_HandSolder" + (at 153.67 116.84 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "~" + (at 153.67 116.84 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Resistor, small symbol" + (at 153.67 116.84 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MANUFACTURER" "" + (at 153.67 116.84 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MAXIMUM_PACKAGE_HEIGHT" "" + (at 153.67 116.84 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "PARTREV" "" + (at 153.67 116.84 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "STANDARD" "" + (at 153.67 116.84 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "4663be56-5d7b-4dde-981e-26590e75cb77") + ) + (pin "2" + (uuid "5ebf0633-3a93-4867-9db1-76976ac19143") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "R4") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "Device:R_Small") + (at 113.03 83.82 0) + (mirror y) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (fields_autoplaced yes) + (uuid "b3f1d506-940d-4f50-933d-aeebc3a5b136") + (property "Reference" "R1" + (at 115.57 82.5499 0) + (effects + (font + (size 1.27 1.27) + ) + (justify right) + ) + ) + (property "Value" "220k" + (at 115.57 85.0899 0) + (effects + (font + (size 1.27 1.27) + ) + (justify right) + ) + ) + (property "Footprint" "Resistor_SMD:R_0805_2012Metric_Pad1.20x1.40mm_HandSolder" + (at 113.03 83.82 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "~" + (at 113.03 83.82 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Resistor, small symbol" + (at 113.03 83.82 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MANUFACTURER" "" + (at 113.03 83.82 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MAXIMUM_PACKAGE_HEIGHT" "" + (at 113.03 83.82 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "PARTREV" "" + (at 113.03 83.82 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "STANDARD" "" + (at 113.03 83.82 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "70c05b2c-4e80-403a-9e92-8542d45d1206") + ) + (pin "2" + (uuid "c3cfea87-c544-4c8e-9eab-e26c8713801d") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "R1") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "power:GND") + (at 172.72 90.17 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "b404e6ac-da9a-4b38-9fcc-469610d9c102") + (property "Reference" "#PWR09" + (at 172.72 96.52 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Value" "GND" + (at 172.974 93.98 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 172.72 90.17 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 172.72 90.17 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Power symbol creates a global label with name \"GND\" , ground" + (at 172.72 90.17 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "3c7ac4e1-3263-4e11-b848-2ee2b98850b3") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "#PWR09") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "power:+3.3V") + (at 161.29 99.06 180) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "d6678757-2ebb-4128-8030-6d8841bb6c75") + (property "Reference" "#PWR08" + (at 161.29 95.25 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Value" "+3.3V" + (at 161.29 102.87 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 161.29 99.06 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 161.29 99.06 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Power symbol creates a global label with name \"+3.3V\"" + (at 161.29 99.06 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "b2fbe9f3-3222-4ae1-b583-970abdfc56ca") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "#PWR08") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "Device:R_Small") + (at 153.67 101.6 0) + (mirror y) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "db6a3718-e8ff-44ae-aa13-b57f45e4bd09") + (property "Reference" "R3" + (at 151.384 100.076 90) + (effects + (font + (size 1.27 1.27) + ) + (justify right) + ) + ) + (property "Value" "51k" + (at 155.956 99.568 90) + (effects + (font + (size 1.27 1.27) + ) + (justify right) + ) + ) + (property "Footprint" "Resistor_SMD:R_0805_2012Metric_Pad1.20x1.40mm_HandSolder" + (at 153.67 101.6 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "~" + (at 153.67 101.6 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Resistor, small symbol" + (at 153.67 101.6 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MANUFACTURER" "" + (at 153.67 101.6 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MAXIMUM_PACKAGE_HEIGHT" "" + (at 153.67 101.6 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "PARTREV" "" + (at 153.67 101.6 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "STANDARD" "" + (at 153.67 101.6 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "6829eee2-48c3-428c-b395-aded09ed3da1") + ) + (pin "2" + (uuid "3f91b68a-7552-416e-b2ab-0e04c521fffd") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "R3") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "power:GND") + (at 113.03 95.25 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "dc1fb61f-7794-44b1-9c3f-9d8160f73445") + (property "Reference" "#PWR03" + (at 113.03 101.6 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Value" "GND" + (at 113.03 99.314 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 113.03 95.25 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 113.03 95.25 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Power symbol creates a global label with name \"GND\" , ground" + (at 113.03 95.25 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "7a1a9ab3-e4f2-4afb-ade6-51fe3c60c5d4") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "#PWR03") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "Device:C") + (at 139.7 115.57 0) + (mirror y) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "df3697c9-ae53-41d5-a282-976508338da5") + (property "Reference" "C3" + (at 143.764 113.03 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Value" "100nF" + (at 146.812 118.364 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Footprint" "Capacitor_SMD:C_0805_2012Metric_Pad1.18x1.45mm_HandSolder" + (at 138.7348 119.38 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "~" + (at 139.7 115.57 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Unpolarized capacitor" + (at 139.7 115.57 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MANUFACTURER" "" + (at 139.7 115.57 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MAXIMUM_PACKAGE_HEIGHT" "" + (at 139.7 115.57 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "PARTREV" "" + (at 139.7 115.57 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "STANDARD" "" + (at 139.7 115.57 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "2" + (uuid "234d1c0f-c5de-4dc7-b16b-da5dc27fab1c") + ) + (pin "1" + (uuid "2b679a4b-9e23-47bd-a24d-6eded824d69c") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "C3") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "Device:C") + (at 99.06 92.71 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "e78fc402-ce31-495a-81f4-a4788b0342b0") + (property "Reference" "C1" + (at 100.33 90.17 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Value" "100nF" + (at 100.584 95.25 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Footprint" "Capacitor_SMD:C_0805_2012Metric_Pad1.18x1.45mm_HandSolder" + (at 100.0252 96.52 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "~" + (at 99.06 92.71 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Unpolarized capacitor" + (at 99.06 92.71 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MANUFACTURER" "" + (at 99.06 92.71 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MAXIMUM_PACKAGE_HEIGHT" "" + (at 99.06 92.71 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "PARTREV" "" + (at 99.06 92.71 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "STANDARD" "" + (at 99.06 92.71 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "2" + (uuid "30c9c460-abed-45f7-a241-ed63555d80fc") + ) + (pin "1" + (uuid "f986288e-0ac9-4da5-9bcb-ea41a75dd84f") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "C1") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "power:GND") + (at 153.67 120.65 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "eb2dd32b-7175-4175-be49-b823cf64d88f") + (property "Reference" "#PWR07" + (at 153.67 127 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Value" "GND" + (at 153.67 124.714 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 153.67 120.65 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 153.67 120.65 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Power symbol creates a global label with name \"GND\" , ground" + (at 153.67 120.65 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "ea5a2d61-5227-4a80-af48-dc2dd1529c05") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "#PWR07") + (unit 1) + ) + ) + ) + ) + (sheet_instances + (path "/" + (page "1") + ) + ) +) diff --git a/usermods/Power_Measurement/assets/img/example schematic.png b/usermods/Power_Measurement/assets/img/example schematic.png new file mode 100644 index 0000000000000000000000000000000000000000..2a25116fbd2faab79222618dbe0d58fcd9a5e372 GIT binary patch literal 53358 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 CieuRT literal 0 HcmV?d00001 diff --git a/usermods/Power_Measurement/assets/img/screenshot 1 - info.jpg b/usermods/Power_Measurement/assets/img/screenshot 1 - info.jpg new file mode 100644 index 0000000000000000000000000000000000000000..278d572463d64d8a3555800090a85f93a3c7c883 GIT binary patch literal 48340 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%0DcYFVo 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 Djb;n* literal 0 HcmV?d00001 diff --git a/usermods/Power_Measurement/assets/img/screenshot 2 - settings.png b/usermods/Power_Measurement/assets/img/screenshot 2 - settings.png new file mode 100644 index 0000000000000000000000000000000000000000..bb9a59d08d23589651194f560e655aa461e79b3e GIT binary patch literal 24762 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 literal 0 HcmV?d00001 diff --git a/usermods/Power_Measurement/assets/img/screenshot 3 - settings.png b/usermods/Power_Measurement/assets/img/screenshot 3 - settings.png new file mode 100644 index 0000000000000000000000000000000000000000..96c05e067edfa7f006bf99710edb306e72726208 GIT binary patch literal 35492 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 KsE~af_`d+&RW*D7 literal 0 HcmV?d00001 diff --git a/usermods/Power_Measurement/readme.md b/usermods/Power_Measurement/readme.md new file mode 100644 index 0000000000..4df846179c --- /dev/null +++ b/usermods/Power_Measurement/readme.md @@ -0,0 +1,94 @@ +# Voltage and Current Measurement using ESP's Internal ADC + +This usermod is a proof of concept that measures current and voltage using a voltage divider and a current sensor. It leverages the ESP's internal ADC to read the voltage and current, calculate power and energy consumption, and optionally publish these measurements via MQTT. + +## Features + +- **Voltage and Current Measurement**: Reads voltage and current using ADC pins of the ESP. +- **Power and Energy Calculation**: Calculates power (in watts) and energy consumption (in kilowatt-hours). +- **Calibration Support**: Offers calibration for more accurate measurements. +- **MQTT Publishing**: Publishes voltage, current, power, and energy measurements to an MQTT broker. +- **Debug Information**: Provides debug output via serial for monitoring raw and processed data. + +## Dependencies + +- **ESP32 ADC Calibration Library**: Requires `esp_adc_cal.h` for ADC calibration, which is a standard ESP-IDF library. + +## Configuration + +### Pins + +- `VOLTAGE_PIN`: ADC pin for voltage measurement (default: `0`) +- `CURRENT_PIN`: ADC pin for current measurement (default: `1`) + +### Constants + +- `NUM_READINGS`: Number of readings for moving average (default: `10`) +- `NUM_READINGS_CAL`: Number of readings for calibration (default: `100`) +- `UPDATE_INTERVAL_MAIN`: Main update interval in milliseconds (default: `100`) +- `UPDATE_INTERVAL_MQTT`: MQTT update interval in milliseconds (default: `60000`) + +## Installation + +Add `-D USERMOD_CURRENT_MEASUREMENT` to `build_flags` in `platformio_override.ini`. + +Or copy the example `platformio_override.ini` to the root directory. This file should be placed in the same directory as `platformio.ini`. + +## Hardware Example + +![Example Schematic](./assets/img/example%20schematic.png "Example Schematic") + +## Define Your Options + +- `USERMOD_POWER_MEASUREMENT`: Enable the usermod + +All parameters and calibration variables can be configured at runtime via the Usermods settings page. + +## Calibration + +### Calibration Steps + +1. Enable the `Calibration mode` checkbox. +2. Connect the controller via USB. +3. Disconnect the power supply (Vin) from the LED strip. +4. Select the option to `Calibrate Zero Points`. +5. Reconnect the power supply to the LED strip and set it to white and full brightness. +6. Measure the voltage and current and enter the values into the `Measured Voltage` and `Measured Current` fields. +7. Check the checkboxes for `Calibrate Voltage` and `Calibrate Current`. + +### Advanced + +![Advanced Calibration](./assets/img/screenshot%203%20-%20settings.png "Advanced Calibration") + +## MQTT + +If MQTT is enabled, the module will periodically publish the voltage, current, power, and energy measurements to the configured MQTT broker. + +## Debugging + +Enable `WLED_DEBUG` to print detailed debug information to the serial output, including raw and calculated values for voltage, current, power, and energy. + +## Screenshots + +Info screen | Settings page +:-----------------------------------------------------------------------:|:-------------------------------------------------------------------------------: +![Info screen](./assets/img/screenshot%201%20-%20info.jpg "Info screen") | ![Settings page](./assets/img/screenshot%202%20-%20settings.png "Settings page") + +## To-Do + +- [ ] Pin manager doesn't work properly. +- [ ] Implement a brightness limiter based on current. +- [ ] Make the code use less flash memory. + +## Changelog + +19.8.2024 +- Initial PR + +## License + +This code was created by Tomáš Kuchta. + +## Contributions + +- Tomáš Kuchta (Initial idea) \ No newline at end of file diff --git a/wled00/const.h b/wled00/const.h index d5cd049d5e..4eabffd43a 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -202,6 +202,7 @@ #define USERMOD_ID_LD2410 52 //Usermod "usermod_ld2410.h" #define USERMOD_ID_POV_DISPLAY 53 //Usermod "usermod_pov_display.h" #define USERMOD_ID_PIXELS_DICE_TRAY 54 //Usermod "pixels_dice_tray.h" +#define USERMOD_ID_POWER_MEASUREMENT 55 //Usermod "Power_measurement.h" //Access point behavior #define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot diff --git a/wled00/pin_manager.h b/wled00/pin_manager.h index 2fc0038803..342944568e 100644 --- a/wled00/pin_manager.h +++ b/wled00/pin_manager.h @@ -65,6 +65,7 @@ enum struct PinOwner : uint8_t { UM_MAX17048 = USERMOD_ID_MAX17048, // 0x2F // Usermod "usermod_max17048.h" UM_BME68X = USERMOD_ID_BME68X, // 0x31 // Usermod "usermod_bme68x.h -- Uses "standard" HW_I2C pins UM_PIXELS_DICE_TRAY = USERMOD_ID_PIXELS_DICE_TRAY, // 0x35 // Usermod "pixels_dice_tray.h" -- Needs compile time specified 6 pins for display including SPI. + UM_Power_Measurement = USERMOD_ID_POWER_MEASUREMENT, // 0x32 // Usermod "Power_measurement.h" }; static_assert(0u == static_cast(PinOwner::None), "PinOwner::None must be zero, so default array initialization works as expected"); diff --git a/wled00/usermods_list.cpp b/wled00/usermods_list.cpp index 25d9ee9ab9..5bd6983d7b 100644 --- a/wled00/usermods_list.cpp +++ b/wled00/usermods_list.cpp @@ -182,6 +182,10 @@ #include "../usermods/Internal_Temperature_v2/usermod_internal_temperature.h" #endif +#ifdef USERMOD_POWER_MEASUREMENT + #include "../usermods/Power_Measurement/Power_Measurement.h" +#endif + #if defined(WLED_USE_SD_MMC) || defined(WLED_USE_SD_SPI) // This include of SD.h and SD_MMC.h must happen here, else they won't be // resolved correctly (when included in mod's header only) @@ -470,4 +474,8 @@ void registerUsermods() #ifdef USERMOD_POV_DISPLAY usermods.add(new PovDisplayUsermod()); #endif + + #ifdef USERMOD_POWER_MEASUREMENT + usermods.add(new UsermodPower_Measurement()); + #endif } From e5a426419c78cdd51f6ebdc4555a3576e14a0256 Mon Sep 17 00:00:00 2001 From: Maximilian Mewes Date: Mon, 19 Aug 2024 22:07:08 +0200 Subject: [PATCH 003/145] Improve mqtt support, add battery percentage and voltage --- usermods/Battery/usermod_v2_Battery.h | 78 ++++++++++++++++++++++++--- 1 file changed, 71 insertions(+), 7 deletions(-) diff --git a/usermods/Battery/usermod_v2_Battery.h b/usermods/Battery/usermod_v2_Battery.h index f240d55f57..e22717db48 100644 --- a/usermods/Battery/usermod_v2_Battery.h +++ b/usermods/Battery/usermod_v2_Battery.h @@ -115,6 +115,58 @@ class UsermodBattery : public Usermod #endif } +#ifndef WLED_DISABLE_MQTT + void addMqttSensor(const String &name, const String &type, const String &topic, const String &deviceClass, const String &unitOfMeasurement = "", const bool &isDiagnostic = false) + { + // String t = String(F("homeassistant/sensor/")) + mqttClientID + F("/") + name + F("/config"); + + StaticJsonDocument<600> doc; + char uid[128], json_str[1024], buf[128]; + + doc[F("name")] = name; + doc[F("stat_t")] = topic; + sprintf_P(uid, PSTR("%s_%s_sensor"), name, escapedMac.c_str()); + doc[F("uniq_id")] = uid; + doc[F("dev_cla")] = deviceClass; + // doc[F("exp_aft")] = 1800; + + if(type == "binary_sensor") { + doc[F("pl_on")] = "on"; + doc[F("pl_off")] = "off"; + } + + if(unitOfMeasurement != "") + doc[F("unit_of_measurement")] = unitOfMeasurement; + + if(isDiagnostic) + doc[F("entity_category")] = "diagnostic"; + + + JsonObject device = doc.createNestedObject(F("device")); // attach the sensor to the same device + device[F("name")] = serverDescription; + device[F("ids")] = String(F("wled-sensor-")) + mqttClientID; + device[F("mf")] = F(WLED_BRAND); + device[F("mdl")] = F(WLED_PRODUCT_NAME); + device[F("sw")] = versionString; + + sprintf_P(buf, PSTR("homeassistant/%s/%s/%s/config"), type, mqttClientID, uid); + DEBUG_PRINTLN(buf); + size_t payload_size = serializeJson(doc, json_str); + DEBUG_PRINTLN(json_str); + + mqtt->publish(buf, 0, true, json_str, payload_size); + } + + void publishMqtt(const char* topic, const char* state) + { + if (WLED_MQTT_CONNECTED) { + char buf[128]; + snprintf_P(buf, 127, PSTR("%s/%s"), mqttDeviceTopic, topic); + mqtt->publish(buf, 0, false, state); + } + } +#endif + public: //Functions called by WLED @@ -223,13 +275,8 @@ class UsermodBattery : public Usermod turnOff(); #ifndef WLED_DISABLE_MQTT - // SmartHome stuff - // still don't know much about MQTT and/or HA - if (WLED_MQTT_CONNECTED) { - char buf[64]; // buffer for snprintf() - snprintf_P(buf, 63, PSTR("%s/voltage"), mqttDeviceTopic); - mqtt->publish(buf, 0, false, String(bat->getVoltage()).c_str()); - } + publishMqtt("battery", String(bat->getLevel(), 0).c_str()); + publishMqtt("voltage", String(bat->getVoltage()).c_str()); #endif } @@ -513,6 +560,23 @@ class UsermodBattery : public Usermod return !battery[FPSTR(_readInterval)].isNull(); } +#ifndef WLED_DISABLE_MQTT + void onMqttConnect(bool sessionPresent) + { + // Home Assistant Autodiscovery + + // battery percentage + char mqttBatteryTopic[128]; + snprintf_P(mqttBatteryTopic, 127, PSTR("%s/battery"), mqttDeviceTopic); + this->addMqttSensor(F("Battery"), "sensor", mqttBatteryTopic, "battery", "%", true); + + // voltage + char mqttVoltageTopic[128]; + snprintf_P(mqttVoltageTopic, 127, PSTR("%s/voltage"), mqttDeviceTopic); + this->addMqttSensor(F("Voltage"), "sensor", mqttVoltageTopic, "voltage", "V", true); + } +#endif + /** * TBD: Generate a preset sample for low power indication * a button on the config page would be cool, currently not possible From b8f15333d857f92b9346c16f75eab300882f87e2 Mon Sep 17 00:00:00 2001 From: Maximilian Mewes Date: Mon, 19 Aug 2024 22:12:21 +0200 Subject: [PATCH 004/145] update `readme.md` --- usermods/Battery/readme.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/usermods/Battery/readme.md b/usermods/Battery/readme.md index 84a6f50542..c3d3d8bf47 100644 --- a/usermods/Battery/readme.md +++ b/usermods/Battery/readme.md @@ -131,6 +131,11 @@ Specification from: [Molicel INR18650-M35A, 3500mAh 10A Lithium-ion battery, 3.6 ## 📝 Change Log +2024-08-19 + +- Improved MQTT support +- Added battery percentage & battery voltage as MQTT topic + 2024-05-11 - Documentation updated From cc24119a590e3256ad2e81841bfa3cf76ed00bfc Mon Sep 17 00:00:00 2001 From: Maximilian Mewes Date: Mon, 19 Aug 2024 22:22:46 +0200 Subject: [PATCH 005/145] remove unnecessary comments --- usermods/Battery/usermod_v2_Battery.h | 43 ++------------------------- 1 file changed, 3 insertions(+), 40 deletions(-) diff --git a/usermods/Battery/usermod_v2_Battery.h b/usermods/Battery/usermod_v2_Battery.h index e22717db48..c9d3b639ec 100644 --- a/usermods/Battery/usermod_v2_Battery.h +++ b/usermods/Battery/usermod_v2_Battery.h @@ -117,9 +117,7 @@ class UsermodBattery : public Usermod #ifndef WLED_DISABLE_MQTT void addMqttSensor(const String &name, const String &type, const String &topic, const String &deviceClass, const String &unitOfMeasurement = "", const bool &isDiagnostic = false) - { - // String t = String(F("homeassistant/sensor/")) + mqttClientID + F("/") + name + F("/config"); - + { StaticJsonDocument<600> doc; char uid[128], json_str[1024], buf[128]; @@ -128,11 +126,11 @@ class UsermodBattery : public Usermod sprintf_P(uid, PSTR("%s_%s_sensor"), name, escapedMac.c_str()); doc[F("uniq_id")] = uid; doc[F("dev_cla")] = deviceClass; - // doc[F("exp_aft")] = 1800; if(type == "binary_sensor") { doc[F("pl_on")] = "on"; doc[F("pl_off")] = "off"; + doc[F("exp_aft")] = 1800; } if(unitOfMeasurement != "") @@ -141,7 +139,6 @@ class UsermodBattery : public Usermod if(isDiagnostic) doc[F("entity_category")] = "diagnostic"; - JsonObject device = doc.createNestedObject(F("device")); // attach the sensor to the same device device[F("name")] = serverDescription; device[F("ids")] = String(F("wled-sensor-")) + mqttClientID; @@ -525,7 +522,6 @@ class UsermodBattery : public Usermod #ifdef ARDUINO_ARCH_ESP32 newBatteryPin = battery[F("pin")] | newBatteryPin; #endif - // calculateTimeLeftEnabled = battery[F("time-left")] | calculateTimeLeftEnabled; setMinBatteryVoltage(battery[F("min-voltage")] | bat->getMinVoltage()); setMaxBatteryVoltage(battery[F("max-voltage")] | bat->getMaxVoltage()); setCalibration(battery[F("calibration")] | bat->getCalibration()); @@ -575,40 +571,7 @@ class UsermodBattery : public Usermod snprintf_P(mqttVoltageTopic, 127, PSTR("%s/voltage"), mqttDeviceTopic); this->addMqttSensor(F("Voltage"), "sensor", mqttVoltageTopic, "voltage", "V", true); } -#endif - - /** - * TBD: Generate a preset sample for low power indication - * a button on the config page would be cool, currently not possible - */ - void generateExamplePreset() - { - // StaticJsonDocument<300> j; - // JsonObject preset = j.createNestedObject(); - // preset["mainseg"] = 0; - // JsonArray seg = preset.createNestedArray("seg"); - // JsonObject seg0 = seg.createNestedObject(); - // seg0["id"] = 0; - // seg0["start"] = 0; - // seg0["stop"] = 60; - // seg0["grp"] = 0; - // seg0["spc"] = 0; - // seg0["on"] = true; - // seg0["bri"] = 255; - - // JsonArray col0 = seg0.createNestedArray("col"); - // JsonArray col00 = col0.createNestedArray(); - // col00.add(255); - // col00.add(0); - // col00.add(0); - - // seg0["fx"] = 1; - // seg0["sx"] = 128; - // seg0["ix"] = 128; - - // savePreset(199, "Low power Indicator", preset); - } - +#endif /* * From 2d6365dc6a6a0bdb6606312478e8439eff6fa7b4 Mon Sep 17 00:00:00 2001 From: Maximilian Mewes Date: Tue, 20 Aug 2024 12:37:01 +0200 Subject: [PATCH 006/145] Add HA-discovery as config option --- usermods/Battery/usermod_v2_Battery.h | 61 +++++++++++++++++++++------ 1 file changed, 48 insertions(+), 13 deletions(-) diff --git a/usermods/Battery/usermod_v2_Battery.h b/usermods/Battery/usermod_v2_Battery.h index c9d3b639ec..136d3a71a4 100644 --- a/usermods/Battery/usermod_v2_Battery.h +++ b/usermods/Battery/usermod_v2_Battery.h @@ -50,6 +50,7 @@ class UsermodBattery : public Usermod // bool initDone = false; bool initializing = true; + bool HomeAssistantDiscovery = false; // strings to reduce flash memory usage (used more than twice) static const char _name[]; @@ -59,6 +60,7 @@ class UsermodBattery : public Usermod static const char _preset[]; static const char _duration[]; static const char _init[]; + static const char _haDiscovery[]; /** * Helper for rounding floating point values @@ -69,6 +71,17 @@ class UsermodBattery : public Usermod return (float)(nx / 100); } + /** + * Helper for converting a string to lowercase + */ + String stringToLower(String str) + { + for(int i = 0; i < str.length(); i++) + if(str[i] >= 'A' && str[i] <= 'Z') + str[i] += 32; + return str; + } + /** * Turn off all leds */ @@ -123,14 +136,14 @@ class UsermodBattery : public Usermod doc[F("name")] = name; doc[F("stat_t")] = topic; - sprintf_P(uid, PSTR("%s_%s_sensor"), name, escapedMac.c_str()); + sprintf_P(uid, PSTR("%s_%s_%s"), escapedMac.c_str(), stringToLower(name).c_str(), type); doc[F("uniq_id")] = uid; doc[F("dev_cla")] = deviceClass; + doc[F("exp_aft")] = 1800; if(type == "binary_sensor") { doc[F("pl_on")] = "on"; doc[F("pl_off")] = "off"; - doc[F("exp_aft")] = 1800; } if(unitOfMeasurement != "") @@ -332,6 +345,7 @@ class UsermodBattery : public Usermod battery[F("calibration")] = bat->getCalibration(); battery[F("voltage-multiplier")] = bat->getVoltageMultiplier(); battery[FPSTR(_readInterval)] = readingInterval; + battery[FPSTR(_haDiscovery)] = HomeAssistantDiscovery; JsonObject ao = battery.createNestedObject(F("auto-off")); // auto off section ao[FPSTR(_enabled)] = autoOffEnabled; @@ -351,8 +365,8 @@ class UsermodBattery : public Usermod getJsonValue(battery[F("max-voltage")], cfg.maxVoltage); getJsonValue(battery[F("calibration")], cfg.calibration); getJsonValue(battery[F("voltage-multiplier")], cfg.voltageMultiplier); - setReadingInterval(battery[FPSTR(_readInterval)] | readingInterval); + setHomeAssistantDiscovery(battery[FPSTR(_haDiscovery)] | HomeAssistantDiscovery); JsonObject ao = battery[F("auto-off")]; setAutoOffEnabled(ao[FPSTR(_enabled)] | autoOffEnabled); @@ -464,17 +478,18 @@ class UsermodBattery : public Usermod void appendConfigData() { // Total: 462 Bytes - oappend(SET_F("td=addDropdown('Battery', 'type');")); // 35 Bytes - oappend(SET_F("addOption(td, 'Unkown', '0');")); // 30 Bytes - oappend(SET_F("addOption(td, 'LiPo', '1');")); // 28 Bytes - oappend(SET_F("addOption(td, 'LiOn', '2');")); // 28 Bytes + oappend(SET_F("td=addDropdown('Battery','type');")); // 34 Bytes + oappend(SET_F("addOption(td,'Unkown','0');")); // 28 Bytes + oappend(SET_F("addOption(td,'LiPo','1');")); // 26 Bytes + oappend(SET_F("addOption(td,'LiOn','2');")); // 26 Bytes oappend(SET_F("addInfo('Battery:type',1,'requires reboot');")); // 81 Bytes - oappend(SET_F("addInfo('Battery:min-voltage', 1, 'v');")); // 40 Bytes - oappend(SET_F("addInfo('Battery:max-voltage', 1, 'v');")); // 40 Bytes - oappend(SET_F("addInfo('Battery:interval', 1, 'ms');")); // 38 Bytes - oappend(SET_F("addInfo('Battery:auto-off:threshold', 1, '%');")); // 47 Bytes - oappend(SET_F("addInfo('Battery:indicator:threshold', 1, '%');")); // 48 Bytes - oappend(SET_F("addInfo('Battery:indicator:duration', 1, 's');")); // 47 Bytes + oappend(SET_F("addInfo('Battery:min-voltage',1,'v');")); // 38 Bytes + oappend(SET_F("addInfo('Battery:max-voltage',1,'v');")); // 38 Bytes + oappend(SET_F("addInfo('Battery:interval',1,'ms');")); // 36 Bytes + oappend(SET_F("addInfo('Battery:HA-discovery',1,'');")); // 38 Bytes + oappend(SET_F("addInfo('Battery:auto-off:threshold',1,'%');")); // 45 Bytes + oappend(SET_F("addInfo('Battery:indicator:threshold',1,'%');")); // 46 Bytes + oappend(SET_F("addInfo('Battery:indicator:duration',1,'s');")); // 45 Bytes // this option list would exeed the oappend() buffer // a list of all presets to select one from @@ -527,6 +542,7 @@ class UsermodBattery : public Usermod setCalibration(battery[F("calibration")] | bat->getCalibration()); setVoltageMultiplier(battery[F("voltage-multiplier")] | bat->getVoltageMultiplier()); setReadingInterval(battery[FPSTR(_readInterval)] | readingInterval); + setHomeAssistantDiscovery(battery[FPSTR(_haDiscovery)] | HomeAssistantDiscovery); getUsermodConfigFromJsonObject(battery); @@ -560,6 +576,8 @@ class UsermodBattery : public Usermod void onMqttConnect(bool sessionPresent) { // Home Assistant Autodiscovery + if (!HomeAssistantDiscovery) + return; // battery percentage char mqttBatteryTopic[128]; @@ -812,6 +830,22 @@ class UsermodBattery : public Usermod { return lowPowerIndicationDone; } + + /** + * Set Home Assistant auto discovery + */ + void setHomeAssistantDiscovery(bool enable) + { + HomeAssistantDiscovery = enable; + } + + /** + * Get Home Assistant auto discovery + */ + bool getHomeAssistantDiscovery() + { + return HomeAssistantDiscovery; + } }; // strings to reduce flash memory usage (used more than twice) @@ -822,3 +856,4 @@ const char UsermodBattery::_threshold[] PROGMEM = "threshold"; const char UsermodBattery::_preset[] PROGMEM = "preset"; const char UsermodBattery::_duration[] PROGMEM = "duration"; const char UsermodBattery::_init[] PROGMEM = "init"; +const char UsermodBattery::_haDiscovery[] PROGMEM = "HA-discovery"; From e7babc071ddfe84b6b7da0c5666cb2b842a396bd Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 20 Aug 2024 20:15:17 +0200 Subject: [PATCH 007/145] replaced PWM LUT with calculation --- wled00/bus_manager.cpp | 43 ++++++++++-------------------------------- 1 file changed, 10 insertions(+), 33 deletions(-) diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index d0e32b2116..2ae624fee5 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -497,45 +497,22 @@ uint32_t BusPwm::getPixelColor(uint16_t pix) { return RGBW32(_data[0], _data[0], _data[0], _data[0]); } -#ifndef ESP8266 -static const uint16_t cieLUT[256] = { - 0, 2, 4, 5, 7, 9, 11, 13, 15, 16, - 18, 20, 22, 24, 26, 27, 29, 31, 33, 35, - 34, 36, 37, 39, 41, 43, 45, 47, 49, 52, - 54, 56, 59, 61, 64, 67, 69, 72, 75, 78, - 81, 84, 87, 90, 94, 97, 100, 104, 108, 111, - 115, 119, 123, 127, 131, 136, 140, 144, 149, 154, - 158, 163, 168, 173, 178, 183, 189, 194, 200, 205, - 211, 217, 223, 229, 235, 241, 247, 254, 261, 267, - 274, 281, 288, 295, 302, 310, 317, 325, 333, 341, - 349, 357, 365, 373, 382, 391, 399, 408, 417, 426, - 436, 445, 455, 464, 474, 484, 494, 505, 515, 526, - 536, 547, 558, 569, 580, 592, 603, 615, 627, 639, - 651, 663, 676, 689, 701, 714, 727, 741, 754, 768, - 781, 795, 809, 824, 838, 853, 867, 882, 897, 913, - 928, 943, 959, 975, 991, 1008, 1024, 1041, 1058, 1075, - 1092, 1109, 1127, 1144, 1162, 1180, 1199, 1217, 1236, 1255, - 1274, 1293, 1312, 1332, 1352, 1372, 1392, 1412, 1433, 1454, - 1475, 1496, 1517, 1539, 1561, 1583, 1605, 1628, 1650, 1673, - 1696, 1719, 1743, 1767, 1791, 1815, 1839, 1864, 1888, 1913, - 1939, 1964, 1990, 2016, 2042, 2068, 2095, 2121, 2148, 2176, - 2203, 2231, 2259, 2287, 2315, 2344, 2373, 2402, 2431, 2461, - 2491, 2521, 2551, 2581, 2612, 2643, 2675, 2706, 2738, 2770, - 2802, 2835, 2867, 2900, 2934, 2967, 3001, 3035, 3069, 3104, - 3138, 3174, 3209, 3244, 3280, 3316, 3353, 3389, 3426, 3463, - 3501, 3539, 3576, 3615, 3653, 3692, 3731, 3770, 3810, 3850, - 3890, 3930, 3971, 4012, 4053, 4095 -}; -#endif - void BusPwm::show() { if (!_valid) return; unsigned numPins = NUM_PWM_PINS(_type); unsigned maxBri = (1<<_depth) - 1; #ifdef ESP8266 unsigned pwmBri = (unsigned)(roundf(powf((float)_bri / 255.0f, 1.7f) * (float)maxBri)); // using gamma 1.7 to extrapolate PWM duty cycle - #else - unsigned pwmBri = cieLUT[_bri] >> (12 - _depth); // use CIE LUT + #else // use CIE brightness formula + unsigned pwmBri = (unsigned)_bri * 100; + if(pwmBri < 2040) pwmBri = ((pwmBri << _depth) + 115043) / 230087; //adding '0.5' before division for correct rounding + else { + pwmBri += 4080; + float temp = (float)pwmBri / 29580; + temp = temp * temp * temp * (1<<_depth) - 1; + pwmBri = (unsigned)temp; + } + Serial.println(pwmBri); #endif for (unsigned i = 0; i < numPins; i++) { unsigned scaled = (_data[i] * pwmBri) / 255; From 1cc47b02cf7d828c9424773478f3d1f2b9805cf5 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Wed, 21 Aug 2024 08:06:32 +0200 Subject: [PATCH 008/145] use CIE brightness also for ESP8266 --- wled00/bus_manager.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index 2ae624fee5..24cb7993da 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -501,9 +501,7 @@ void BusPwm::show() { if (!_valid) return; unsigned numPins = NUM_PWM_PINS(_type); unsigned maxBri = (1<<_depth) - 1; - #ifdef ESP8266 - unsigned pwmBri = (unsigned)(roundf(powf((float)_bri / 255.0f, 1.7f) * (float)maxBri)); // using gamma 1.7 to extrapolate PWM duty cycle - #else // use CIE brightness formula + // use CIE brightness formula unsigned pwmBri = (unsigned)_bri * 100; if(pwmBri < 2040) pwmBri = ((pwmBri << _depth) + 115043) / 230087; //adding '0.5' before division for correct rounding else { @@ -512,8 +510,6 @@ void BusPwm::show() { temp = temp * temp * temp * (1<<_depth) - 1; pwmBri = (unsigned)temp; } - Serial.println(pwmBri); - #endif for (unsigned i = 0; i < numPins; i++) { unsigned scaled = (_data[i] * pwmBri) / 255; if (_reversed) scaled = maxBri - scaled; From 0bbd6b7c4b677249b584f90abbacde6f44f0ea85 Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Thu, 22 Aug 2024 17:08:51 +0200 Subject: [PATCH 009/145] Minor optimisation - disable JSON live - WS error string - button irelevant check --- wled00/button.cpp | 2 +- wled00/wled.h | 4 ++-- wled00/ws.cpp | 8 ++++++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/wled00/button.cpp b/wled00/button.cpp index 23d7b8a90f..8b366e055c 100644 --- a/wled00/button.cpp +++ b/wled00/button.cpp @@ -308,7 +308,7 @@ void handleButton() buttonLongPressed[b] = true; } - } else if (!isButtonPressed(b) && buttonPressedBefore[b]) { //released + } else if (buttonPressedBefore[b]) { //released long dur = now - buttonPressedTime[b]; // released after rising-edge short press action diff --git a/wled00/wled.h b/wled00/wled.h index b9e675edca..7ef41d9612 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -36,7 +36,7 @@ #undef WLED_ENABLE_ADALIGHT // disable has priority over enable #endif //#define WLED_ENABLE_DMX // uses 3.5kb (use LEDPIN other than 2) -#define WLED_ENABLE_JSONLIVE // peek LED output via /json/live (WS binary peek is always enabled) +//#define WLED_ENABLE_JSONLIVE // peek LED output via /json/live (WS binary peek is always enabled) #ifndef WLED_DISABLE_LOXONE #define WLED_ENABLE_LOXONE // uses 1.2kb #endif @@ -331,7 +331,7 @@ typedef class WiFiOptions { struct { uint8_t selectedWiFi : 4; // max 16 SSIDs uint8_t apChannel : 4; - bool apHide : 1; + uint8_t apHide : 3; uint8_t apBehavior : 3; bool noWifiSleep : 1; bool force802_3g : 1; diff --git a/wled00/ws.cpp b/wled00/ws.cpp index d0bac144dd..3dec548f4c 100644 --- a/wled00/ws.cpp +++ b/wled00/ws.cpp @@ -96,6 +96,8 @@ void wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventTyp //pong message was received (in response to a ping request maybe) DEBUG_PRINTLN(F("WS pong.")); + } else { + DEBUG_PRINTLN(F("WS unknown event.")); } } @@ -104,10 +106,11 @@ void sendDataWs(AsyncWebSocketClient * client) if (!ws.count()) return; if (!requestJSONBufferLock(12)) { + const char* error = PSTR("{\"error\":3}"); if (client) { - client->text(F("{\"error\":3}")); // ERR_NOBUF + client->text(FPSTR(error)); // ERR_NOBUF } else { - ws.textAll(F("{\"error\":3}")); // ERR_NOBUF + ws.textAll(FPSTR(error)); // ERR_NOBUF } return; } @@ -120,6 +123,7 @@ void sendDataWs(AsyncWebSocketClient * client) size_t len = measureJson(*pDoc); DEBUG_PRINTF_P(PSTR("JSON buffer size: %u for WS request (%u).\n"), pDoc->memoryUsage(), len); + // the following may no longer be necessary as heap management has been fixed by @willmmiles in AWS size_t heap1 = ESP.getFreeHeap(); DEBUG_PRINT(F("heap ")); DEBUG_PRINTLN(ESP.getFreeHeap()); #ifdef ESP8266 From 6f3267aee98095bab3381956fa1949f5c09cb2db Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Thu, 22 Aug 2024 17:15:12 +0200 Subject: [PATCH 010/145] Dynamic bus config - provide LED types from BusManager for settings Credit: @netmindz for the idea. --- wled00/bus_manager.cpp | 157 +++++++++++++++---------- wled00/bus_manager.h | 214 +++++++++++++++++----------------- wled00/cfg.cpp | 7 +- wled00/data/settings_leds.htm | 149 ++++++++++++----------- wled00/set.cpp | 4 +- wled00/xml.cpp | 2 + 6 files changed, 279 insertions(+), 254 deletions(-) diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index d0e32b2116..aef6b2ee9b 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -48,38 +48,25 @@ uint8_t realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, byte #define W(c) (byte((c) >> 24)) -void ColorOrderMap::add(uint16_t start, uint16_t len, uint8_t colorOrder) { - if (_count >= WLED_MAX_COLOR_ORDER_MAPPINGS) { - return; - } - if (len == 0) { - return; - } - // upper nibble contains W swap information - if ((colorOrder & 0x0F) > COL_ORDER_MAX) { - return; - } - _mappings[_count].start = start; - _mappings[_count].len = len; - _mappings[_count].colorOrder = colorOrder; - _count++; +bool ColorOrderMap::add(uint16_t start, uint16_t len, uint8_t colorOrder) { + if (count() >= WLED_MAX_COLOR_ORDER_MAPPINGS || len == 0 || (colorOrder & 0x0F) > COL_ORDER_MAX) return false; // upper nibble contains W swap information + _mappings.push_back({start,len,colorOrder}); + return true; } uint8_t IRAM_ATTR ColorOrderMap::getPixelColorOrder(uint16_t pix, uint8_t defaultColorOrder) const { - if (_count > 0) { - // upper nibble contains W swap information - // when ColorOrderMap's upper nibble contains value >0 then swap information is used from it, otherwise global swap is used - for (unsigned i = 0; i < _count; i++) { - if (pix >= _mappings[i].start && pix < (_mappings[i].start + _mappings[i].len)) { - return _mappings[i].colorOrder | ((_mappings[i].colorOrder >> 4) ? 0 : (defaultColorOrder & 0xF0)); - } + // upper nibble contains W swap information + // when ColorOrderMap's upper nibble contains value >0 then swap information is used from it, otherwise global swap is used + for (unsigned i = 0; i < count(); i++) { + if (pix >= _mappings[i].start && pix < (_mappings[i].start + _mappings[i].len)) { + return _mappings[i].colorOrder | ((_mappings[i].colorOrder >> 4) ? 0 : (defaultColorOrder & 0xF0)); } } return defaultColorOrder; } -uint32_t Bus::autoWhiteCalc(uint32_t c) { +uint32_t Bus::autoWhiteCalc(uint32_t c) const { unsigned aWM = _autoWhiteMode; if (_gAWM < AW_GLOBAL_DISABLED) aWM = _gAWM; if (aWM == RGBW_MODE_MANUAL_ONLY) return c; @@ -95,7 +82,7 @@ uint32_t Bus::autoWhiteCalc(uint32_t c) { return RGBW32(r, g, b, w); } -uint8_t *Bus::allocData(size_t size) { +uint8_t *Bus::allocateData(size_t size) { if (_data) free(_data); // should not happen, but for safety return _data = (uint8_t *)(size>0 ? calloc(size, sizeof(uint8_t)) : nullptr); } @@ -123,7 +110,7 @@ BusDigital::BusDigital(BusConfig &bc, uint8_t nr, const ColorOrderMap &com) } _iType = PolyBus::getI(bc.type, _pins, nr); if (_iType == I_NONE) return; - if (bc.doubleBuffer && !allocData(bc.count * Bus::getNumberOfChannels(bc.type))) return; + if (bc.doubleBuffer && !allocateData(bc.count * Bus::getNumberOfChannels(bc.type))) return; //_buffering = bc.doubleBuffer; uint16_t lenToCreate = bc.count; if (bc.type == TYPE_WS2812_1CH_X3) lenToCreate = NUM_ICS_WS2812_1CH_3X(bc.count); // only needs a third of "RGB" LEDs for NeoPixelBus @@ -150,7 +137,7 @@ BusDigital::BusDigital(BusConfig &bc, uint8_t nr, const ColorOrderMap &com) //I am NOT to be held liable for burned down garages or houses! // To disable brightness limiter we either set output max current to 0 or single LED current to 0 -uint8_t BusDigital::estimateCurrentAndLimitBri() { +uint8_t BusDigital::estimateCurrentAndLimitBri(void) { bool useWackyWS2815PowerModel = false; byte actualMilliampsPerLed = _milliAmpsPerLed; @@ -202,7 +189,7 @@ uint8_t BusDigital::estimateCurrentAndLimitBri() { return newBri; } -void BusDigital::show() { +void BusDigital::show(void) { _milliAmpsTotal = 0; if (!_valid) return; @@ -263,7 +250,7 @@ void BusDigital::show() { if (newBri < _bri) PolyBus::setBrightness(_busPtr, _iType, _bri); } -bool BusDigital::canShow() { +bool BusDigital::canShow(void) const { if (!_valid) return true; return PolyBus::canShow(_busPtr, _iType); } @@ -319,7 +306,7 @@ void IRAM_ATTR BusDigital::setPixelColor(uint16_t pix, uint32_t c) { } // returns original color if global buffering is enabled, else returns lossly restored color from bus -uint32_t IRAM_ATTR BusDigital::getPixelColor(uint16_t pix) { +uint32_t IRAM_ATTR BusDigital::getPixelColor(uint16_t pix) const { if (!_valid) return 0; if (_data) { size_t offset = pix * getNumberOfChannels(); @@ -349,9 +336,9 @@ uint32_t IRAM_ATTR BusDigital::getPixelColor(uint16_t pix) { } } -uint8_t BusDigital::getPins(uint8_t* pinArray) { +uint8_t BusDigital::getPins(uint8_t* pinArray) const { unsigned numPins = IS_2PIN(_type) ? 2 : 1; - for (unsigned i = 0; i < numPins; i++) pinArray[i] = _pins[i]; + if (pinArray) for (unsigned i = 0; i < numPins; i++) pinArray[i] = _pins[i]; return numPins; } @@ -361,12 +348,12 @@ void BusDigital::setColorOrder(uint8_t colorOrder) { _colorOrder = colorOrder; } -void BusDigital::reinit() { +void BusDigital::reinit(void) { if (!_valid) return; PolyBus::begin(_busPtr, _iType, _pins); } -void BusDigital::cleanup() { +void BusDigital::cleanup(void) { DEBUG_PRINTLN(F("Digital Cleanup.")); PolyBus::cleanup(_busPtr, _iType); _iType = I_NONE; @@ -477,7 +464,7 @@ void BusPwm::setPixelColor(uint16_t pix, uint32_t c) { } //does no index check -uint32_t BusPwm::getPixelColor(uint16_t pix) { +uint32_t BusPwm::getPixelColor(uint16_t pix) const { if (!_valid) return 0; // TODO getting the reverse from CCT is involved (a quick approximation when CCT blending is ste to 0 implemented) switch (_type) { @@ -528,7 +515,7 @@ static const uint16_t cieLUT[256] = { }; #endif -void BusPwm::show() { +void BusPwm::show(void) { if (!_valid) return; unsigned numPins = NUM_PWM_PINS(_type); unsigned maxBri = (1<<_depth) - 1; @@ -548,16 +535,14 @@ void BusPwm::show() { } } -uint8_t BusPwm::getPins(uint8_t* pinArray) { +uint8_t BusPwm::getPins(uint8_t* pinArray) const { if (!_valid) return 0; unsigned numPins = NUM_PWM_PINS(_type); - for (unsigned i = 0; i < numPins; i++) { - pinArray[i] = _pins[i]; - } + if (pinArray) for (unsigned i = 0; i < numPins; i++) pinArray[i] = _pins[i]; return numPins; } -void BusPwm::deallocatePins() { +void BusPwm::deallocatePins(void) { unsigned numPins = NUM_PWM_PINS(_type); for (unsigned i = 0; i < numPins; i++) { pinManager.deallocatePin(_pins[i], PinOwner::BusPwm); @@ -601,19 +586,19 @@ void BusOnOff::setPixelColor(uint16_t pix, uint32_t c) { _data[0] = bool(r|g|b|w) && bool(_bri) ? 0xFF : 0; } -uint32_t BusOnOff::getPixelColor(uint16_t pix) { +uint32_t BusOnOff::getPixelColor(uint16_t pix) const { if (!_valid) return 0; return RGBW32(_data[0], _data[0], _data[0], _data[0]); } -void BusOnOff::show() { +void BusOnOff::show(void) { if (!_valid) return; digitalWrite(_pin, _reversed ? !(bool)_data[0] : (bool)_data[0]); } -uint8_t BusOnOff::getPins(uint8_t* pinArray) { +uint8_t BusOnOff::getPins(uint8_t* pinArray) const { if (!_valid) return 0; - pinArray[0] = _pin; + if (pinArray) pinArray[0] = _pin; return 1; } @@ -642,7 +627,7 @@ BusNetwork::BusNetwork(BusConfig &bc) } _UDPchannels = _rgbw ? 4 : 3; _client = IPAddress(bc.pins[0],bc.pins[1],bc.pins[2],bc.pins[3]); - _valid = (allocData(_len * _UDPchannels) != nullptr); + _valid = (allocateData(_len * _UDPchannels) != nullptr); DEBUG_PRINTF_P(PSTR("%successfully inited virtual strip with type %u and IP %u.%u.%u.%u\n"), _valid?"S":"Uns", bc.type, bc.pins[0], bc.pins[1], bc.pins[2], bc.pins[3]); } @@ -657,27 +642,25 @@ void BusNetwork::setPixelColor(uint16_t pix, uint32_t c) { if (_rgbw) _data[offset+3] = W(c); } -uint32_t BusNetwork::getPixelColor(uint16_t pix) { +uint32_t BusNetwork::getPixelColor(uint16_t pix) const { if (!_valid || pix >= _len) return 0; unsigned offset = pix * _UDPchannels; return RGBW32(_data[offset], _data[offset+1], _data[offset+2], (_rgbw ? _data[offset+3] : 0)); } -void BusNetwork::show() { +void BusNetwork::show(void) { if (!_valid || !canShow()) return; _broadcastLock = true; realtimeBroadcast(_UDPtype, _client, _len, _data, _bri, _rgbw); _broadcastLock = false; } -uint8_t BusNetwork::getPins(uint8_t* pinArray) { - for (unsigned i = 0; i < 4; i++) { - pinArray[i] = _client[i]; - } +uint8_t BusNetwork::getPins(uint8_t* pinArray) const { + if (pinArray) for (unsigned i = 0; i < 4; i++) pinArray[i] = _client[i]; return 4; } -void BusNetwork::cleanup() { +void BusNetwork::cleanup(void) { _type = I_NONE; _valid = false; freeData(); @@ -724,13 +707,67 @@ int BusManager::add(BusConfig &bc) { return numBusses++; } +// idea by @netmindz https://github.com/Aircoookie/WLED/pull/4056 +String BusManager::getLEDTypesJSONString(void) { + struct LEDType { + uint8_t id; + const char *type; + const char *name; + } types[] = { + {TYPE_WS2812_RGB, "D", PSTR("WS281x")}, + {TYPE_SK6812_RGBW, "D", PSTR("SK6812/WS2814 RGBW")}, + {TYPE_TM1814, "D", PSTR("TM1814")}, + {TYPE_WS2811_400KHZ, "D", PSTR("400kHz")}, + {TYPE_TM1829, "D", PSTR("TM1829")}, + {TYPE_UCS8903, "D", PSTR("UCS8903")}, + {TYPE_APA106, "D", PSTR("APA106/PL9823")}, + {TYPE_TM1914, "D", PSTR("TM1914")}, + {TYPE_FW1906, "D", PSTR("FW1906 GRBCW")}, + {TYPE_UCS8904, "D", PSTR("UCS8904 RGBW")}, + {TYPE_WS2805, "D", PSTR("WS2805 RGBCW")}, + {TYPE_SM16825, "D", PSTR("SM16825 RGBCW")}, + {TYPE_WS2812_1CH_X3, "D", PSTR("WS2811 White")}, + //{TYPE_WS2812_2CH_X3, "D", PSTR("WS2811 CCT")}, + //{TYPE_WS2812_WWA, "D", PSTR("WS2811 WWA")}, + {TYPE_WS2801, "2P", PSTR("WS2801")}, + {TYPE_APA102, "2P", PSTR("APA102")}, + {TYPE_LPD8806, "2P", PSTR("LPD8806")}, + {TYPE_LPD6803, "2P", PSTR("LPD6803")}, + {TYPE_P9813, "2P", PSTR("PP9813")}, + {TYPE_ONOFF, "", PSTR("On/Off")}, + {TYPE_ANALOG_1CH, "A", PSTR("PWM White")}, + {TYPE_ANALOG_2CH, "AA", PSTR("PWM CCT")}, + {TYPE_ANALOG_3CH, "AAA", PSTR("PWM RGB")}, + {TYPE_ANALOG_4CH, "AAAA", PSTR("PWM RGBW")}, + {TYPE_ANALOG_5CH, "AAAAA", PSTR("PWM RGB+CCT")}, + //{TYPE_ANALOG_6CH, "AAAAAA", PSTR("PWM RGB+DCCT")}, + {TYPE_NET_DDP_RGB, "V", PSTR("DDP RGB (network)")}, + {TYPE_NET_ARTNET_RGB, "V", PSTR("Art-Net RGB (network)")}, + {TYPE_NET_DDP_RGBW, "V", PSTR("DDP RGBW (network)")}, + {TYPE_NET_ARTNET_RGBW, "V", PSTR("Art-Net RGBW (network)")} + }; + String json = "["; + for (const auto &type : types) { + String id = String(type.id); + json += "{i:" + id + + F(",w:") + String((int)Bus::hasWhite(type.id)) + + F(",c:") + String((int)Bus::hasCCT(type.id)) + + F(",s:") + String((int)Bus::is16bit(type.id)) + + F(",t:\"") + FPSTR(type.type) + + F("\",n:\"") + FPSTR(type.name) + F("\"},"); + } + json.setCharAt(json.length()-1, ']'); // replace last comma with bracket + return json; +} + + void BusManager::useParallelOutput(void) { _parallelOutputs = 8; // hardcoded since we use NPB I2S x8 methods PolyBus::setParallelI2S1Output(); } //do not call this method from system context (network callback) -void BusManager::removeAll() { +void BusManager::removeAll(void) { DEBUG_PRINTLN(F("Removing all.")); //prevents crashes due to deleting busses while in use. while (!canAllShow()) yield(); @@ -744,7 +781,7 @@ void BusManager::removeAll() { // #2478 // If enabled, RMT idle level is set to HIGH when off // to prevent leakage current when using an N-channel MOSFET to toggle LED power -void BusManager::esp32RMTInvertIdle() { +void BusManager::esp32RMTInvertIdle(void) { bool idle_out; unsigned rmt = 0; for (unsigned u = 0; u < numBusses(); u++) { @@ -775,7 +812,7 @@ void BusManager::esp32RMTInvertIdle() { } #endif -void BusManager::on() { +void BusManager::on(void) { #ifdef ESP8266 //Fix for turning off onboard LED breaking bus if (pinManager.getPinOwner(LED_BUILTIN) == PinOwner::BusDigital) { @@ -796,7 +833,7 @@ void BusManager::on() { #endif } -void BusManager::off() { +void BusManager::off(void) { #ifdef ESP8266 // turn off built-in LED if strip is turned off // this will break digital bus so will need to be re-initialised on On @@ -811,7 +848,7 @@ void BusManager::off() { #endif } -void BusManager::show() { +void BusManager::show(void) { _milliAmpsUsed = 0; for (unsigned i = 0; i < numBusses; i++) { busses[i]->show(); @@ -852,13 +889,13 @@ void BusManager::setSegmentCCT(int16_t cct, bool allowWBCorrection) { uint32_t BusManager::getPixelColor(uint16_t pix) { for (unsigned i = 0; i < numBusses; i++) { unsigned bstart = busses[i]->getStart(); - if (pix < bstart || pix >= bstart + busses[i]->getLength()) continue; + if (!busses[i]->containsPixel(pix)) continue; return busses[i]->getPixelColor(pix - bstart); } return 0; } -bool BusManager::canAllShow() { +bool BusManager::canAllShow(void) { for (unsigned i = 0; i < numBusses; i++) { if (!busses[i]->canShow()) return false; } @@ -871,7 +908,7 @@ Bus* BusManager::getBus(uint8_t busNr) { } //semi-duplicate of strip.getLengthTotal() (though that just returns strip._length, calculated in finalizeInit()) -uint16_t BusManager::getTotalLength() { +uint16_t BusManager::getTotalLength(void) { unsigned len = 0; for (unsigned i=0; igetLength(); return len; diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index 5e516d2e16..8d23f11272 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -6,6 +6,8 @@ */ #include "const.h" +#include +#include //colors.cpp uint16_t approximateKelvinFromRGB(uint32_t rgb); @@ -73,34 +75,31 @@ struct BusConfig { // Defines an LED Strip and its color ordering. -struct ColorOrderMapEntry { +typedef struct { uint16_t start; uint16_t len; uint8_t colorOrder; -}; +} ColorOrderMapEntry; struct ColorOrderMap { - void add(uint16_t start, uint16_t len, uint8_t colorOrder); + bool add(uint16_t start, uint16_t len, uint8_t colorOrder); - uint8_t count() const { return _count; } + inline uint8_t count() const { return _mappings.size(); } void reset() { - _count = 0; - memset(_mappings, 0, sizeof(_mappings)); + _mappings.clear(); + _mappings.shrink_to_fit(); } const ColorOrderMapEntry* get(uint8_t n) const { - if (n > _count) { - return nullptr; - } + if (n >= count()) return nullptr; return &(_mappings[n]); } uint8_t getPixelColorOrder(uint16_t pix, uint8_t defaultColorOrder) const; private: - uint8_t _count; - ColorOrderMapEntry _mappings[WLED_MAX_COLOR_ORDER_MAPPINGS]; + std::vector _mappings; }; @@ -122,59 +121,61 @@ class Bus { virtual ~Bus() {} //throw the bus under the bus - virtual void show() = 0; - virtual bool canShow() { return true; } - virtual void setStatusPixel(uint32_t c) {} + virtual void show(void) = 0; + virtual bool canShow(void) const { return true; } + virtual void setStatusPixel(uint32_t c) {} virtual void setPixelColor(uint16_t pix, uint32_t c) = 0; - virtual uint32_t getPixelColor(uint16_t pix) { return 0; } - virtual void setBrightness(uint8_t b) { _bri = b; }; - virtual uint8_t getPins(uint8_t* pinArray) { return 0; } - virtual uint16_t getLength() { return isOk() ? _len : 0; } - virtual void setColorOrder(uint8_t co) {} - virtual uint8_t getColorOrder() { return COL_ORDER_RGB; } - virtual uint8_t skippedLeds() { return 0; } - virtual uint16_t getFrequency() { return 0U; } - virtual uint16_t getLEDCurrent() { return 0; } - virtual uint16_t getUsedCurrent() { return 0; } - virtual uint16_t getMaxCurrent() { return 0; } - virtual uint8_t getNumberOfChannels() { return hasWhite(_type) + 3*hasRGB(_type) + hasCCT(_type); } - static inline uint8_t getNumberOfChannels(uint8_t type) { return hasWhite(type) + 3*hasRGB(type) + hasCCT(type); } - inline void setReversed(bool reversed) { _reversed = reversed; } - inline uint16_t getStart() { return _start; } - inline void setStart(uint16_t start) { _start = start; } - inline uint8_t getType() { return _type; } - inline bool isOk() { return _valid; } - inline bool isReversed() { return _reversed; } - inline bool isOffRefreshRequired() { return _needsRefresh; } - bool containsPixel(uint16_t pix) { return pix >= _start && pix < _start+_len; } - - virtual bool hasRGB(void) { return Bus::hasRGB(_type); } - static bool hasRGB(uint8_t type) { - if ((type >= TYPE_WS2812_1CH && type <= TYPE_WS2812_WWA) || type == TYPE_ANALOG_1CH || type == TYPE_ANALOG_2CH || type == TYPE_ONOFF) return false; - return true; + virtual void setBrightness(uint8_t b) { _bri = b; }; + inline void setStart(uint16_t start) { _start = start; } + virtual void setColorOrder(uint8_t co) {} + virtual bool hasRGB(void) const { return Bus::hasRGB(_type); } + virtual bool hasWhite(void) const { return Bus::hasWhite(_type); } + virtual bool hasCCT(void) const { return Bus::hasCCT(_type); } + virtual bool is16bit(void) const { return Bus::is16bit(_type); } + virtual uint32_t getPixelColor(uint16_t pix) const { return 0; } + virtual uint8_t getPins(uint8_t* pinArray = nullptr) const { return 0; } + virtual uint16_t getLength(void) const { return isOk() ? _len : 0; } + virtual uint8_t getColorOrder(void) const { return COL_ORDER_RGB; } + virtual uint8_t skippedLeds(void) const { return 0; } + virtual uint16_t getFrequency(void) const { return 0U; } + virtual uint16_t getLEDCurrent(void) const { return 0; } + virtual uint16_t getUsedCurrent(void) const { return 0; } + virtual uint16_t getMaxCurrent(void) const { return 0; } + virtual uint8_t getNumberOfChannels(void) const { return hasWhite(_type) + 3*hasRGB(_type) + hasCCT(_type); } + + inline void setReversed(bool reversed) { _reversed = reversed; } + inline void setAutoWhiteMode(uint8_t m) { if (m < 5) _autoWhiteMode = m; } + inline uint8_t getAutoWhiteMode(void) const { return _autoWhiteMode; } + inline uint16_t getStart(void) const { return _start; } + inline uint8_t getType(void) const { return _type; } + inline bool isOk(void) const { return _valid; } + inline bool isReversed(void) const { return _reversed; } + inline bool isOffRefreshRequired(void) const { return _needsRefresh; } + inline bool containsPixel(uint16_t pix) const { return pix >= _start && pix < _start + _len; } + + static inline uint8_t getNumberOfChannels(uint8_t type) { return hasWhite(type) + 3*hasRGB(type) + hasCCT(type); } + static constexpr bool hasRGB(uint8_t type) { + return !((type >= TYPE_WS2812_1CH && type <= TYPE_WS2812_WWA) || type == TYPE_ANALOG_1CH || type == TYPE_ANALOG_2CH || type == TYPE_ONOFF); } - virtual bool hasWhite(void) { return Bus::hasWhite(_type); } - static bool hasWhite(uint8_t type) { - if ((type >= TYPE_WS2812_1CH && type <= TYPE_WS2812_WWA) || - type == TYPE_SK6812_RGBW || type == TYPE_TM1814 || type == TYPE_UCS8904 || - type == TYPE_FW1906 || type == TYPE_WS2805 || type == TYPE_SM16825) return true; // digital types with white channel - if (type > TYPE_ONOFF && type <= TYPE_ANALOG_5CH && type != TYPE_ANALOG_3CH) return true; // analog types with white channel - if (type == TYPE_NET_DDP_RGBW || type == TYPE_NET_ARTNET_RGBW) return true; // network types with white channel - return false; + static constexpr bool hasWhite(uint8_t type) { + return (type >= TYPE_WS2812_1CH && type <= TYPE_WS2812_WWA) || + type == TYPE_SK6812_RGBW || type == TYPE_TM1814 || type == TYPE_UCS8904 || + type == TYPE_FW1906 || type == TYPE_WS2805 || type == TYPE_SM16825 || // digital types with white channel + (type > TYPE_ONOFF && type <= TYPE_ANALOG_5CH && type != TYPE_ANALOG_3CH) || // analog types with white channel + type == TYPE_NET_DDP_RGBW || type == TYPE_NET_ARTNET_RGBW; // network types with white channel } - virtual bool hasCCT(void) { return Bus::hasCCT(_type); } - static bool hasCCT(uint8_t type) { - if (type == TYPE_WS2812_2CH_X3 || type == TYPE_WS2812_WWA || - type == TYPE_ANALOG_2CH || type == TYPE_ANALOG_5CH || - type == TYPE_FW1906 || type == TYPE_WS2805 || - type == TYPE_SM16825) return true; - return false; + static constexpr bool hasCCT(uint8_t type) { + return type == TYPE_WS2812_2CH_X3 || type == TYPE_WS2812_WWA || + type == TYPE_ANALOG_2CH || type == TYPE_ANALOG_5CH || + type == TYPE_FW1906 || type == TYPE_WS2805 || + type == TYPE_SM16825; } - static inline int16_t getCCT() { return _cct; } - static void setCCT(int16_t cct) { - _cct = cct; - } - static inline uint8_t getCCTBlend() { return _cctBlend; } + static constexpr bool is16bit(uint8_t type) { return type == TYPE_UCS8903 || type == TYPE_UCS8904 || type == TYPE_SM16825; } + static inline int16_t getCCT(void) { return _cct; } + static inline void setGlobalAWMode(uint8_t m) { if (m < 5) _gAWM = m; else _gAWM = AW_GLOBAL_DISABLED; } + static inline uint8_t getGlobalAWMode(void) { return _gAWM; } + static void setCCT(int16_t cct) { _cct = cct; } + static inline uint8_t getCCTBlend(void) { return _cctBlend; } static void setCCTBlend(uint8_t b) { if (b > 100) b = 100; _cctBlend = (b * 127) / 100; @@ -203,10 +204,6 @@ class Bus { ww = (w * ww) / 255; //brightness scaling cw = (w * cw) / 255; } - inline void setAutoWhiteMode(uint8_t m) { if (m < 5) _autoWhiteMode = m; } - inline uint8_t getAutoWhiteMode() { return _autoWhiteMode; } - inline static void setGlobalAWMode(uint8_t m) { if (m < 5) _gAWM = m; else _gAWM = AW_GLOBAL_DISABLED; } - inline static uint8_t getGlobalAWMode() { return _gAWM; } protected: uint8_t _type; @@ -231,8 +228,8 @@ class Bus { // 127 - additive CCT blending (CCT 127 => 100% warm, 100% cold) static uint8_t _cctBlend; - uint32_t autoWhiteCalc(uint32_t c); - uint8_t *allocData(size_t size = 1); + uint32_t autoWhiteCalc(uint32_t c) const; + uint8_t *allocateData(size_t size = 1); void freeData() { if (_data != nullptr) free(_data); _data = nullptr; } }; @@ -242,23 +239,22 @@ class BusDigital : public Bus { BusDigital(BusConfig &bc, uint8_t nr, const ColorOrderMap &com); ~BusDigital() { cleanup(); } - void show() override; - bool canShow() override; + void show(void) override; + bool canShow(void) const override; void setBrightness(uint8_t b) override; void setStatusPixel(uint32_t c) override; void setPixelColor(uint16_t pix, uint32_t c) override; void setColorOrder(uint8_t colorOrder) override; - uint32_t getPixelColor(uint16_t pix) override; - uint8_t getColorOrder() override { return _colorOrder; } - uint8_t getPins(uint8_t* pinArray) override; - uint8_t skippedLeds() override { return _skip; } - uint16_t getFrequency() override { return _frequencykHz; } - uint8_t estimateCurrentAndLimitBri(); - uint16_t getLEDCurrent() override { return _milliAmpsPerLed; } - uint16_t getUsedCurrent() override { return _milliAmpsTotal; } - uint16_t getMaxCurrent() override { return _milliAmpsMax; } - void reinit(); - void cleanup(); + uint32_t getPixelColor(uint16_t pix) const override; + uint8_t getColorOrder(void) const override { return _colorOrder; } + uint8_t getPins(uint8_t* pinArray) const override; + uint8_t skippedLeds(void) const override { return _skip; } + uint16_t getFrequency(void) const override { return _frequencykHz; } + uint16_t getLEDCurrent(void) const override { return _milliAmpsPerLed; } + uint16_t getUsedCurrent(void) const override { return _milliAmpsTotal; } + uint16_t getMaxCurrent(void) const override { return _milliAmpsMax; } + void reinit(void); + void cleanup(void); private: uint8_t _skip; @@ -273,7 +269,7 @@ class BusDigital : public Bus { static uint16_t _milliAmpsTotal; // is overwitten/recalculated on each show() - inline uint32_t restoreColorLossy(uint32_t c, uint8_t restoreBri) { + inline uint32_t restoreColorLossy(uint32_t c, uint8_t restoreBri) const { if (restoreBri < 255) { uint8_t* chan = (uint8_t*) &c; for (uint_fast8_t i=0; i<4; i++) { @@ -283,6 +279,8 @@ class BusDigital : public Bus { } return c; } + + uint8_t estimateCurrentAndLimitBri(void); }; @@ -292,11 +290,11 @@ class BusPwm : public Bus { ~BusPwm() { cleanup(); } void setPixelColor(uint16_t pix, uint32_t c) override; - uint32_t getPixelColor(uint16_t pix) override; //does no index check - uint8_t getPins(uint8_t* pinArray) override; - uint16_t getFrequency() override { return _frequency; } - void show() override; - void cleanup() { deallocatePins(); } + uint32_t getPixelColor(uint16_t pix) const override; //does no index check + uint8_t getPins(uint8_t* pinArray) const override; + uint16_t getFrequency(void) const override { return _frequency; } + void show(void) override; + void cleanup(void) { deallocatePins(); } private: uint8_t _pins[5]; @@ -307,7 +305,7 @@ class BusPwm : public Bus { uint8_t _depth; uint16_t _frequency; - void deallocatePins(); + void deallocatePins(void); }; @@ -317,10 +315,10 @@ class BusOnOff : public Bus { ~BusOnOff() { cleanup(); } void setPixelColor(uint16_t pix, uint32_t c) override; - uint32_t getPixelColor(uint16_t pix) override; - uint8_t getPins(uint8_t* pinArray) override; - void show() override; - void cleanup() { pinManager.deallocatePin(_pin, PinOwner::BusOnOff); } + uint32_t getPixelColor(uint16_t pix) const override; + uint8_t getPins(uint8_t* pinArray) const override; + void show(void) override; + void cleanup(void) { pinManager.deallocatePin(_pin, PinOwner::BusOnOff); } private: uint8_t _pin; @@ -333,14 +331,14 @@ class BusNetwork : public Bus { BusNetwork(BusConfig &bc); ~BusNetwork() { cleanup(); } - bool hasRGB() override { return true; } - bool hasWhite() override { return _rgbw; } - bool canShow() override { return !_broadcastLock; } // this should be a return value from UDP routine if it is still sending data out + bool hasRGB(void) const override { return true; } + bool hasWhite(void) const override { return _rgbw; } + bool canShow(void) const override { return !_broadcastLock; } // this should be a return value from UDP routine if it is still sending data out void setPixelColor(uint16_t pix, uint32_t c) override; - uint32_t getPixelColor(uint16_t pix) override; - uint8_t getPins(uint8_t* pinArray) override; - void show() override; - void cleanup(); + uint32_t getPixelColor(uint16_t pix) const override; + uint8_t getPins(uint8_t* pinArray) const override; + void show(void) override; + void cleanup(void); private: IPAddress _client; @@ -365,31 +363,31 @@ class BusManager { static void useParallelOutput(void); // workaround for inaccessible PolyBus //do not call this method from system context (network callback) - static void removeAll(); + static void removeAll(void); static void on(void); static void off(void); - static void show(); - static bool canAllShow(); + static void show(void); + static bool canAllShow(void); static void setStatusPixel(uint32_t c); static void setPixelColor(uint16_t pix, uint32_t c); static void setBrightness(uint8_t b); // for setSegmentCCT(), cct can only be in [-1,255] range; allowWBCorrection will convert it to K // WARNING: setSegmentCCT() is a misleading name!!! much better would be setGlobalCCT() or just setCCT() static void setSegmentCCT(int16_t cct, bool allowWBCorrection = false); - static void setMilliampsMax(uint16_t max) { _milliAmpsMax = max;} + static inline void setMilliampsMax(uint16_t max) { _milliAmpsMax = max;} static uint32_t getPixelColor(uint16_t pix); - static inline int16_t getSegmentCCT() { return Bus::getCCT(); } + static inline int16_t getSegmentCCT(void) { return Bus::getCCT(); } static Bus* getBus(uint8_t busNr); //semi-duplicate of strip.getLengthTotal() (though that just returns strip._length, calculated in finalizeInit()) - static uint16_t getTotalLength(); - static uint8_t getNumBusses() { return numBusses; } + static uint16_t getTotalLength(void); + static inline uint8_t getNumBusses(void) { return numBusses; } + static String getLEDTypesJSONString(void); - static void updateColorOrderMap(const ColorOrderMap &com) { memcpy(&colorOrderMap, &com, sizeof(ColorOrderMap)); } - static const ColorOrderMap& getColorOrderMap() { return colorOrderMap; } + static inline ColorOrderMap& getColorOrderMap(void) { return colorOrderMap; } private: static uint8_t numBusses; @@ -400,9 +398,9 @@ class BusManager { static uint8_t _parallelOutputs; #ifdef ESP32_DATA_IDLE_HIGH - static void esp32RMTInvertIdle(); + static void esp32RMTInvertIdle(void); #endif - static uint8_t getNumVirtualBusses() { + static uint8_t getNumVirtualBusses(void) { int j = 0; for (int i=0; igetType() >= TYPE_NET_DDP_RGB && busses[i]->getType() < 96) j++; return j; diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index 89076efab6..76ff4d20eb 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -244,17 +244,12 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { // read color order map configuration JsonArray hw_com = hw[F("com")]; if (!hw_com.isNull()) { - ColorOrderMap com = {}; - unsigned s = 0; for (JsonObject entry : hw_com) { - if (s > WLED_MAX_COLOR_ORDER_MAPPINGS) break; uint16_t start = entry["start"] | 0; uint16_t len = entry["len"] | 0; uint8_t colorOrder = (int)entry[F("order")]; - com.add(start, len, colorOrder); - s++; + if (!BusManager::getColorOrderMap().add(start, len, colorOrder)) break; } - BusManager::updateColorOrderMap(com); } // read multiple button configuration diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index b7d2d18a7d..54c16b9d96 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -7,6 +7,7 @@ ', options).replace(/<[\/]*script>/g, ''); + let js = await minifyHtml('', options); + return js.replace(/<[\/]*script>/g, ''); } else if (type == "html-minify") { return await minifyHtml(str, options); } @@ -252,6 +253,12 @@ writeChunks( str .replace("%%", "%") }, + { + file: "common.js", + name: "JS_common", + method: "gzip", + filter: "js-minify", + }, { file: "settings.htm", name: "PAGE_settings", diff --git a/wled00/data/common.js b/wled00/data/common.js new file mode 100644 index 0000000000..9378ef07a8 --- /dev/null +++ b/wled00/data/common.js @@ -0,0 +1,118 @@ +var d=document; +var loc = false, locip, locproto = "http:"; + +function H(pg="") { window.open("https://kno.wled.ge/"+pg); } +function GH() { window.open("https://github.com/Aircoookie/WLED"); } +function gId(c) { return d.getElementById(c); } // getElementById +function cE(e) { return d.createElement(e); } // createElement +function gEBCN(c) { return d.getElementsByClassName(c); } // getElementsByClassName +function gN(s) { return d.getElementsByName(s)[0]; } // getElementsByName +function isE(o) { return Object.keys(o).length === 0; } // isEmpty +function isO(i) { return (i && typeof i === 'object' && !Array.isArray(i)); } // isObject +function isN(n) { return !isNaN(parseFloat(n)) && isFinite(n); } // isNumber +// https://stackoverflow.com/questions/3885817/how-do-i-check-that-a-number-is-float-or-integer +function isF(n) { return n === +n && n !== (n|0); } // isFloat +function isI(n) { return n === +n && n === (n|0); } // isInteger +function toggle(el) { gId(el).classList.toggle("hide"); gId('No'+el).classList.toggle("hide"); } +function tooltip(cont=null) { + d.querySelectorAll((cont?cont+" ":"")+"[title]").forEach((element)=>{ + element.addEventListener("mouseover", ()=>{ + // save title + element.setAttribute("data-title", element.getAttribute("title")); + const tooltip = d.createElement("span"); + tooltip.className = "tooltip"; + tooltip.textContent = element.getAttribute("title"); + + // prevent default title popup + element.removeAttribute("title"); + + let { top, left, width } = element.getBoundingClientRect(); + + d.body.appendChild(tooltip); + + const { offsetHeight, offsetWidth } = tooltip; + + const offset = element.classList.contains("sliderwrap") ? 4 : 10; + top -= offsetHeight + offset; + left += (width - offsetWidth) / 2; + + tooltip.style.top = top + "px"; + tooltip.style.left = left + "px"; + tooltip.classList.add("visible"); + }); + + element.addEventListener("mouseout", ()=>{ + d.querySelectorAll('.tooltip').forEach((tooltip)=>{ + tooltip.classList.remove("visible"); + d.body.removeChild(tooltip); + }); + // restore title + element.setAttribute("title", element.getAttribute("data-title")); + }); + }); +}; +// https://www.educative.io/edpresso/how-to-dynamically-load-a-js-file-in-javascript +function loadJS(FILE_URL, async = true, preGetV = undefined, postGetV = undefined) { + let scE = d.createElement("script"); + scE.setAttribute("src", FILE_URL); + scE.setAttribute("type", "text/javascript"); + scE.setAttribute("async", async); + d.body.appendChild(scE); + // success event + scE.addEventListener("load", () => { + //console.log("File loaded"); + if (preGetV) preGetV(); + GetV(); + if (postGetV) postGetV(); + }); + // error event + scE.addEventListener("error", (ev) => { + console.log("Error on loading file", ev); + alert("Loading of configuration script failed.\nIncomplete page data!"); + }); +} +function getLoc() { + let l = window.location; + if (l.protocol == "file:") { + loc = true; + locip = localStorage.getItem('locIp'); + if (!locip) { + locip = prompt("File Mode. Please enter WLED IP!"); + localStorage.setItem('locIp', locip); + } + } else { + // detect reverse proxy + let path = l.pathname; + let paths = path.slice(1,path.endsWith('/')?-1:undefined).split("/"); + if (paths.length > 1) paths.pop(); // remove subpage (or "settings") + if (paths.length > 0 && paths[paths.length-1]=="settings") paths.pop(); // remove "settings" + if (paths.length > 1) { + locproto = l.protocol; + loc = true; + locip = l.hostname + (l.port ? ":" + l.port : "") + "/" + paths.join('/'); + } + } +} +function getURL(path) { return (loc ? locproto + "//" + locip : "") + path; } +function B() { window.open(getURL("/settings"),"_self"); } +var timeout; +function showToast(text, error = false) { + var x = gId("toast"); + if (!x) return; + x.innerHTML = text; + x.className = error ? "error":"show"; + clearTimeout(timeout); + x.style.animation = 'none'; + timeout = setTimeout(function(){ x.className = x.className.replace("show", ""); }, 2900); +} +function uploadFile(fileObj, name) { + var req = new XMLHttpRequest(); + req.addEventListener('load', function(){showToast(this.responseText,this.status >= 400)}); + req.addEventListener('error', function(e){showToast(e.stack,true);}); + req.open("POST", "/upload"); + var formData = new FormData(); + formData.append("data", fileObj.files[0], name); + req.send(formData); + fileObj.value = ''; + return false; +} diff --git a/wled00/data/cpal/cpal.htm b/wled00/data/cpal/cpal.htm index a4b9135924..b58c0987ab 100644 --- a/wled00/data/cpal/cpal.htm +++ b/wled00/data/cpal/cpal.htm @@ -608,8 +608,8 @@

} function generatePaletteDivs() { - const palettesDiv = d.getElementById("palettes"); - const staticPalettesDiv = d.getElementById("staticPalettes"); + const palettesDiv = gId("palettes"); + const staticPalettesDiv = gId("staticPalettes"); const paletteDivs = Array.from(palettesDiv.children).filter((child) => { return child.id.match(/^palette\d$/); // match only elements with id starting with "palette" followed by a single digit }); @@ -620,25 +620,25 @@

for (let i = 0; i < paletteArray.length; i++) { const palette = paletteArray[i]; - const paletteDiv = d.createElement("div"); + const paletteDiv = cE("div"); paletteDiv.id = `palette${i}`; paletteDiv.classList.add("palette"); const thisKey = Object.keys(palette)[0]; paletteDiv.dataset.colarray = JSON.stringify(palette[thisKey]); - const gradientDiv = d.createElement("div"); + const gradientDiv = cE("div"); gradientDiv.id = `paletteGradient${i}` - const buttonsDiv = d.createElement("div"); + const buttonsDiv = cE("div"); buttonsDiv.id = `buttonsDiv${i}`; buttonsDiv.classList.add("buttonsDiv") - const sendSpan = d.createElement("span"); + const sendSpan = cE("span"); sendSpan.id = `sendSpan${i}`; sendSpan.onclick = function() {initiateUpload(i)}; sendSpan.setAttribute('title', `Send current editor to slot ${i}`); // perhaps Save instead of Send? sendSpan.innerHTML = svgSave; sendSpan.classList.add("sendSpan") - const editSpan = d.createElement("span"); + const editSpan = cE("span"); editSpan.id = `editSpan${i}`; editSpan.onclick = function() {loadForEdit(i)}; editSpan.setAttribute('title', `Copy slot ${i} palette to editor`); diff --git a/wled00/data/pxmagic/pxmagic.htm b/wled00/data/pxmagic/pxmagic.htm index d59f924cf2..8ec11f454a 100644 --- a/wled00/data/pxmagic/pxmagic.htm +++ b/wled00/data/pxmagic/pxmagic.htm @@ -882,10 +882,8 @@ hostnameLabel(); })(); - function gId(id) { - return d.getElementById(id); - } - + function gId(e) {return d.getElementById(e);} + function cE(e) {return d.createElement(e);} function hostnameLabel() { const link = gId("wledEdit"); link.href = WLED_URL + "/edit"; @@ -1675,7 +1673,7 @@ } function createCanvas(width, height) { - const canvas = d.createElement("canvas"); + const canvas = cE("canvas"); canvas.width = width; canvas.height = height; @@ -1719,7 +1717,7 @@ const blob = new Blob([text], { type: mimeType }); const url = URL.createObjectURL(blob); - const anchorElement = d.createElement("a"); + const anchorElement = cE("a"); anchorElement.href = url; anchorElement.download = `${filename}.${fileExtension}`; @@ -1790,7 +1788,7 @@ hideElement = "preview" ) { const hide = gId(hideElement); - const toast = d.createElement("div"); + const toast = cE("div"); const wait = 100; toast.style.animation = "fadeIn"; @@ -1799,14 +1797,14 @@ toast.classList.add("toast", type); - const body = d.createElement("span"); + const body = cE("span"); body.classList.add("toast-body"); body.textContent = message; toast.appendChild(body); - const progress = d.createElement("div"); + const progress = cE("div"); progress.classList.add("toast-progress"); progress.style.animation = "progress"; @@ -1831,7 +1829,7 @@ function carousel(id, images, delay = 3000) { let index = 0; - const carousel = d.createElement("div"); + const carousel = cE("div"); carousel.classList.add("carousel"); images.forEach((canvas, i) => { @@ -1959,7 +1957,7 @@ let errorElement = parent.querySelector(".error-message"); if (!errorElement) { - errorElement = d.createElement("div"); + errorElement = cE("div"); errorElement.classList.add("error-message"); parent.appendChild(errorElement); } diff --git a/wled00/data/settings.htm b/wled00/data/settings.htm index 52b64006b5..82c7782140 100644 --- a/wled00/data/settings.htm +++ b/wled00/data/settings.htm @@ -4,53 +4,12 @@ WLED Settings +
-
+

Imma firin ma lazer (if it has DMX support)

diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index 206d4a8c75..54ba9d8ba5 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -4,20 +4,12 @@ LED Settings +
-
+

LED & Hardware setup

@@ -861,7 +800,7 @@

Hardware setup

 ✕
Apply IR change to main segment only:
- + IR info

Relay GPIO:  ✕
diff --git a/wled00/data/settings_sec.htm b/wled00/data/settings_sec.htm index ff8231ccbf..ce9bd8aa32 100644 --- a/wled00/data/settings_sec.htm +++ b/wled00/data/settings_sec.htm @@ -4,55 +4,9 @@ Misc Settings +
-
+


diff --git a/wled00/data/settings_um.htm b/wled00/data/settings_um.htm index e2fbd5eb74..c2f0ffbf2e 100644 --- a/wled00/data/settings_um.htm +++ b/wled00/data/settings_um.htm @@ -4,75 +4,55 @@ Usermod Settings +
-
+

WiFi setup

diff --git a/wled00/wled_server.cpp b/wled00/wled_server.cpp index 291f6f5fcb..9d4e4c85b9 100644 --- a/wled00/wled_server.cpp +++ b/wled00/wled_server.cpp @@ -18,6 +18,7 @@ static const char s_unlock_ota [] PROGMEM = "Please unlock OTA in security setti static const char s_unlock_cfg [] PROGMEM = "Please unlock settings using PIN code!"; static const char s_notimplemented[] PROGMEM = "Not implemented"; static const char s_accessdenied[] PROGMEM = "Access Denied"; +static const char _common_js[] PROGMEM = "/common.js"; //Is this an IP? static bool isIp(String str) { @@ -237,6 +238,10 @@ void initServer() handleStaticContent(request, "", 200, FPSTR(CONTENT_TYPE_HTML), PAGE_liveview, PAGE_liveview_length); }); + server.on(_common_js, HTTP_GET, [](AsyncWebServerRequest *request) { + handleStaticContent(request, FPSTR(_common_js), 200, FPSTR(CONTENT_TYPE_JAVASCRIPT), JS_common, JS_common_length); + }); + //settings page server.on(F("/settings"), HTTP_GET, [](AsyncWebServerRequest *request){ serveSettings(request); @@ -511,6 +516,10 @@ void serveJsonError(AsyncWebServerRequest* request, uint16_t code, uint16_t erro void serveSettingsJS(AsyncWebServerRequest* request) { + if (request->url().indexOf(FPSTR(_common_js)) > 0) { + handleStaticContent(request, FPSTR(_common_js), 200, FPSTR(CONTENT_TYPE_JAVASCRIPT), JS_common, JS_common_length); + return; + } char buf[SETTINGS_STACK_BUF_SIZE+37]; buf[0] = 0; byte subPage = request->arg(F("p")).toInt(); From 88fb8605681a6417e27b7a09a40ae846c8f4d909 Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Tue, 17 Sep 2024 16:34:38 +0200 Subject: [PATCH 076/145] SAVE_RAM bugfix introduced by #4137 --- wled00/wled.h | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/wled00/wled.h b/wled00/wled.h index 33dea8b03e..31a6128580 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -641,17 +641,19 @@ typedef class Receive { bool SegmentOptions : 1; bool SegmentBounds : 1; bool Direct : 1; - uint8_t reserved : 2; + bool Palette : 1; + uint8_t reserved : 1; }; }; Receive(int i) { Options = i; } - Receive(bool b, bool c, bool e, bool sO, bool sB) { - Brightness = b; - Color = c; - Effects = e; - SegmentOptions = sO; - SegmentBounds = sB; - }; + Receive(bool b, bool c, bool e, bool sO, bool sB, bool p) + : Brightness(b) + , Color(c) + , Effects(e) + , SegmentOptions(sO) + , SegmentBounds(sB) + , Palette(p) + {}; } __attribute__ ((aligned(1), packed)) receive_notification_t; typedef class Send { public: @@ -673,7 +675,7 @@ typedef class Send { Hue = h; } } __attribute__ ((aligned(1), packed)) send_notification_t; -WLED_GLOBAL receive_notification_t receiveN _INIT(0b00100111); +WLED_GLOBAL receive_notification_t receiveN _INIT(0b01100111); WLED_GLOBAL send_notification_t notifyG _INIT(0b00001111); #define receiveNotificationBrightness receiveN.Brightness #define receiveNotificationColor receiveN.Color From 72455ccde1f35c390e86a1038d2c2fd63d5e86fb Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Tue, 17 Sep 2024 19:47:24 +0200 Subject: [PATCH 077/145] Missing "not" --- wled00/data/settings_sync.htm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/data/settings_sync.htm b/wled00/data/settings_sync.htm index bf5ce39794..34b9fc6cdb 100644 --- a/wled00/data/settings_sync.htm +++ b/wled00/data/settings_sync.htm @@ -206,7 +206,7 @@

Philips Hue

Serial

- This firmware build does support Serial interface.
+ This firmware build does not support Serial interface.
Baud rate: From d4268ba070bc33d52193aafa7f7e1efe904d39dd Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sat, 7 Sep 2024 19:51:23 -0400 Subject: [PATCH 078/145] handleFileRead: Skip duplicate FS check Since we validate the file existence ourselves, no need to have AsyncWebServer do it again. --- wled00/file.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/file.cpp b/wled00/file.cpp index 69e1e692cd..bc34672023 100644 --- a/wled00/file.cpp +++ b/wled00/file.cpp @@ -433,7 +433,7 @@ bool handleFileRead(AsyncWebServerRequest* request, String path){ } #endif if(WLED_FS.exists(path) || WLED_FS.exists(path + ".gz")) { - request->send(WLED_FS, path, String(), request->hasArg(F("download"))); + request->send(request->beginResponse(WLED_FS, path, {}, request->hasArg(F("download")), {})); return true; } return false; From 1346eb4f76805fc6210e31949e33a06fa44d813a Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sun, 8 Sep 2024 15:55:50 -0400 Subject: [PATCH 079/145] tools: Add all_xml fetch script Useful for checking that I haven't broken anything. --- tools/all_xml.sh | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 tools/all_xml.sh diff --git a/tools/all_xml.sh b/tools/all_xml.sh new file mode 100644 index 0000000000..68ed07bbda --- /dev/null +++ b/tools/all_xml.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# Pull all settings pages for comparison +HOST=$1 +TGT_PATH=$2 +CURL_ARGS="--compressed" + +# Replicate one target many times +function replicate() { + for i in {0..10} + do + echo -n " http://${HOST}/settings.js?p=$i -o ${TGT_PATH}/$i.xml" + done +} +read -a TARGETS <<< $(replicate) + +mkdir -p ${TGT_PATH} +curl ${CURL_ARGS} ${TARGETS[@]} From 32f9616b6e078e53da033f1054e747e04e8b30c3 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Thu, 5 Sep 2024 21:13:55 -0400 Subject: [PATCH 080/145] Remove oappend Remove the large stack buffer as we're just going to copy it in to a heap buffer anyways. Later we can refine the length estimation or use a rope-style dynamic data structure like DynamicBufferList. --- wled00/fcn_declare.h | 14 +- wled00/mqtt.cpp | 34 +- wled00/set.cpp | 6 +- wled00/um_manager.cpp | 2 +- wled00/util.cpp | 69 +--- wled00/wled.h | 4 - wled00/wled_server.cpp | 18 +- wled00/xml.cpp | 792 ++++++++++++++++++++--------------------- 8 files changed, 452 insertions(+), 487 deletions(-) diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index a95064a2a6..6ce30facfc 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -302,7 +302,7 @@ class Usermod { virtual bool handleButton(uint8_t b) { return false; } // button overrides are possible here virtual bool getUMData(um_data_t **data) { if (data) *data = nullptr; return false; }; // usermod data exchange [see examples for audio effects] virtual void connected() {} // called when WiFi is (re)connected - virtual void appendConfigData() {} // helper function called from usermod settings page to add metadata for entry fields + virtual void appendConfigData(Print&) {} // helper function called from usermod settings page to add metadata for entry fields virtual void addToJsonState(JsonObject& obj) {} // add JSON objects for WLED state virtual void addToJsonInfo(JsonObject& obj) {} // add JSON objects for UI Info page virtual void readFromJsonState(JsonObject& obj) {} // process JSON messages received from web server @@ -328,7 +328,7 @@ class UsermodManager { bool getUMData(um_data_t **um_data, uint8_t mod_id = USERMOD_ID_RESERVED); // USERMOD_ID_RESERVED will poll all usermods void setup(); void connected(); - void appendConfigData(); + void appendConfigData(Print&); void addToJsonState(JsonObject& obj); void addToJsonInfo(JsonObject& obj); void readFromJsonState(JsonObject& obj); @@ -362,10 +362,8 @@ void parseNumber(const char* str, byte* val, byte minv=0, byte maxv=255); bool getVal(JsonVariant elem, byte* val, byte minv=0, byte maxv=255); bool getBoolVal(JsonVariant elem, bool dflt); bool updateVal(const char* req, const char* key, byte* val, byte minv=0, byte maxv=255); -bool oappend(const char* txt); // append new c string to temp buffer efficiently -bool oappendi(int i); // append new number to temp buffer efficiently -void sappend(char stype, const char* key, int val); -void sappends(char stype, const char* key, char* val); +void sappend(Print& dest, char stype, const char* key, int val); +void sappends(Print& dest, char stype, const char* key, char* val); void prepareHostname(char* hostname); bool isAsterisksOnly(const char* str, byte maxLen); bool requestJSONBufferLock(uint8_t module=255); @@ -444,7 +442,7 @@ void wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventTyp void sendDataWs(AsyncWebSocketClient * client = nullptr); //xml.cpp -void XML_response(AsyncWebServerRequest *request, char* dest = nullptr); -void getSettingsJS(byte subPage, char* dest); +void XML_response(Print& dest); +void getSettingsJS(byte subPage, Print& dest); #endif diff --git a/wled00/mqtt.cpp b/wled00/mqtt.cpp index 833e6eb7d4..6694be07dc 100644 --- a/wled00/mqtt.cpp +++ b/wled00/mqtt.cpp @@ -124,6 +124,32 @@ static void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProp payloadStr = nullptr; } +// Print adapter for flat buffers +namespace { +class bufferPrint : public Print { + char* _buf; + size_t _size, _offset; + public: + + bufferPrint(char* buf, size_t size) : _buf(buf), _size(size), _offset(0) {}; + + size_t write(const uint8_t *buffer, size_t size) { + size = std::min(size, _size - _offset); + memcpy(_buf + _offset, buffer, size); + _offset += size; + return size; + } + + size_t write(uint8_t c) { + return this->write(&c, 1); + } + + char* data() const { return _buf; } + size_t size() const { return _offset; } + size_t capacity() const { return _size; } +}; +}; // anonymous namespace + void publishMqtt() { @@ -148,11 +174,13 @@ void publishMqtt() strcat_P(subuf, PSTR("/status")); mqtt->publish(subuf, 0, true, "online"); // retain message for a LWT - char apires[1024]; // allocating 1024 bytes from stack can be risky - XML_response(nullptr, apires); + // TODO: use a DynamicBufferList. Requires a list-read-capable MQTT client API. + DynamicBuffer buf(1024); + bufferPrint pbuf(buf.data(), buf.size()); + XML_response(pbuf); strlcpy(subuf, mqttDeviceTopic, 33); strcat_P(subuf, PSTR("/v")); - mqtt->publish(subuf, 0, retainMqttMsg, apires); // optionally retain message (#2263) + mqtt->publish(subuf, 0, retainMqttMsg, buf.data(), pbuf.size()); // optionally retain message (#2263) #endif } diff --git a/wled00/set.cpp b/wled00/set.cpp index 812bcc52f3..05b5b31811 100644 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -1191,7 +1191,11 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) // internal call, does not send XML response pos = req.indexOf(F("IN")); - if (pos < 1) XML_response(request); + if (pos < 1) { + auto response = request->beginResponseStream("text/xml"); + XML_response(*response); + request->send(response); + } return true; } diff --git a/wled00/um_manager.cpp b/wled00/um_manager.cpp index 2db29c3cda..3970e7af40 100644 --- a/wled00/um_manager.cpp +++ b/wled00/um_manager.cpp @@ -8,7 +8,7 @@ void UsermodManager::setup() { for (unsigned i = 0; i < numMods; i++ void UsermodManager::connected() { for (unsigned i = 0; i < numMods; i++) ums[i]->connected(); } void UsermodManager::loop() { for (unsigned i = 0; i < numMods; i++) ums[i]->loop(); } void UsermodManager::handleOverlayDraw() { for (unsigned i = 0; i < numMods; i++) ums[i]->handleOverlayDraw(); } -void UsermodManager::appendConfigData() { for (unsigned i = 0; i < numMods; i++) ums[i]->appendConfigData(); } +void UsermodManager::appendConfigData(Print& dest) { for (unsigned i = 0; i < numMods; i++) ums[i]->appendConfigData(dest); } bool UsermodManager::handleButton(uint8_t b) { bool overrideIO = false; for (unsigned i = 0; i < numMods; i++) { diff --git a/wled00/util.cpp b/wled00/util.cpp index 99a75bdd30..00506ea978 100644 --- a/wled00/util.cpp +++ b/wled00/util.cpp @@ -89,88 +89,43 @@ bool updateVal(const char* req, const char* key, byte* val, byte minv, byte maxv //append a numeric setting to string buffer -void sappend(char stype, const char* key, int val) +void sappend(Print& dest, char stype, const char* key, int val) { - char ds[] = "d.Sf."; - + const __FlashStringHelper* type_str; switch(stype) { case 'c': //checkbox - oappend(ds); - oappend(key); - oappend(".checked="); - oappendi(val); - oappend(";"); + type_str = F(".checked="); break; case 'v': //numeric - oappend(ds); - oappend(key); - oappend(".value="); - oappendi(val); - oappend(";"); + type_str = F(".value="); break; case 'i': //selectedIndex - oappend(ds); - oappend(key); - oappend(SET_F(".selectedIndex=")); - oappendi(val); - oappend(";"); + type_str = F(".selectedIndex="); break; + default: + return; //??? } + + dest.printf_P(PSTR("d.Sf.%s%s%d;"), key, type_str, val); } //append a string setting to buffer -void sappends(char stype, const char* key, char* val) +void sappends(Print& dest, char stype, const char* key, char* val) { switch(stype) { case 's': {//string (we can interpret val as char*) - String buf = val; - //convert "%" to "%%" to make EspAsyncWebServer happy - //buf.replace("%","%%"); - oappend("d.Sf."); - oappend(key); - oappend(".value=\""); - oappend(buf.c_str()); - oappend("\";"); + dest.printf_P(PSTR("d.Sf.%s.value=\"%s\";"),key,val); break;} case 'm': //message - oappend(SET_F("d.getElementsByClassName")); - oappend(key); - oappend(SET_F(".innerHTML=\"")); - oappend(val); - oappend("\";"); + dest.printf_P(PSTR("d.getElementsByClassName%s.innerHTML=\"%s\";"), key, val); break; } } -bool oappendi(int i) -{ - char s[12]; // 32bit signed number can have 10 digits plus - sign - sprintf(s, "%d", i); - return oappend(s); -} - - -bool oappend(const char* txt) -{ - unsigned len = strlen(txt); - if ((obuf == nullptr) || (olen + len >= SETTINGS_STACK_BUF_SIZE)) { // sanity checks -#ifdef WLED_DEBUG - DEBUG_PRINT(F("oappend() buffer overflow. Cannot append ")); - DEBUG_PRINT(len); DEBUG_PRINT(F(" bytes \t\"")); - DEBUG_PRINT(txt); DEBUG_PRINTLN(F("\"")); -#endif - return false; // buffer full - } - strcpy(obuf + olen, txt); - olen += len; - return true; -} - - void prepareHostname(char* hostname) { sprintf_P(hostname, PSTR("wled-%*s"), 6, escapedMac.c_str() + 6); diff --git a/wled00/wled.h b/wled00/wled.h index 31a6128580..052f29b29f 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -839,10 +839,6 @@ WLED_GLOBAL time_t sunrise _INIT(0); WLED_GLOBAL time_t sunset _INIT(0); WLED_GLOBAL Toki toki _INIT(Toki()); -// Temp buffer -WLED_GLOBAL char* obuf; -WLED_GLOBAL uint16_t olen _INIT(0); - // General filesystem WLED_GLOBAL size_t fsBytesUsed _INIT(0); WLED_GLOBAL size_t fsBytesTotal _INIT(0); diff --git a/wled00/wled_server.cpp b/wled00/wled_server.cpp index 9d4e4c85b9..8fdcb1ebec 100644 --- a/wled00/wled_server.cpp +++ b/wled00/wled_server.cpp @@ -520,27 +520,23 @@ void serveSettingsJS(AsyncWebServerRequest* request) handleStaticContent(request, FPSTR(_common_js), 200, FPSTR(CONTENT_TYPE_JAVASCRIPT), JS_common, JS_common_length); return; } - char buf[SETTINGS_STACK_BUF_SIZE+37]; - buf[0] = 0; byte subPage = request->arg(F("p")).toInt(); if (subPage > 10) { - strcpy_P(buf, PSTR("alert('Settings for this request are not implemented.');")); - request->send(501, FPSTR(CONTENT_TYPE_JAVASCRIPT), buf); + request->send_P(501, FPSTR(CONTENT_TYPE_JAVASCRIPT), PSTR("alert('Settings for this request are not implemented.');")); return; } if (subPage > 0 && !correctPIN && strlen(settingsPIN)>0) { - strcpy_P(buf, PSTR("alert('PIN incorrect.');")); - request->send(401, FPSTR(CONTENT_TYPE_JAVASCRIPT), buf); + request->send_P(401, FPSTR(CONTENT_TYPE_JAVASCRIPT), PSTR("alert('PIN incorrect.');")); return; } - strcat_P(buf,PSTR("function GetV(){var d=document;")); - getSettingsJS(subPage, buf+strlen(buf)); // this may overflow by 35bytes!!! - strcat_P(buf,PSTR("}")); - AsyncWebServerResponse *response; - response = request->beginResponse(200, FPSTR(CONTENT_TYPE_JAVASCRIPT), buf); + AsyncResponseStream *response = request->beginResponseStream(FPSTR(CONTENT_TYPE_JAVASCRIPT)); response->addHeader(F("Cache-Control"), F("no-store")); response->addHeader(F("Expires"), F("0")); + + response->print(F("function GetV(){var d=document;")); + getSettingsJS(subPage, *response); + response->print(F("}")); request->send(response); } diff --git a/wled00/xml.cpp b/wled00/xml.cpp index 71d66d0022..2d63d61f3f 100644 --- a/wled00/xml.cpp +++ b/wled00/xml.cpp @@ -6,85 +6,80 @@ */ //build XML response to HTTP /win API request -void XML_response(AsyncWebServerRequest *request, char* dest) +void XML_response(Print& dest) { - char sbuf[(dest == nullptr)?1024:1]; //allocate local buffer if none passed - obuf = (dest == nullptr)? sbuf:dest; - - olen = 0; - oappend(SET_F("")); - oappendi((nightlightActive && nightlightMode > NL_MODE_SET) ? briT : bri); - oappend(SET_F("")); + dest.print(F("")); + dest.print((nightlightActive && nightlightMode > NL_MODE_SET) ? briT : bri); + dest.print(F("")); for (int i = 0; i < 3; i++) { - oappend(""); - oappendi(col[i]); - oappend(""); + dest.print(""); + dest.print(col[i]); + dest.print(""); } for (int i = 0; i < 3; i++) { - oappend(""); - oappendi(colSec[i]); - oappend(""); + dest.print(""); + dest.print(colSec[i]); + dest.print(""); } - oappend(SET_F("")); - oappendi(notifyDirect); - oappend(SET_F("")); - oappendi(receiveGroups!=0); - oappend(SET_F("")); - oappendi(nightlightActive); - oappend(SET_F("")); - oappendi(nightlightMode > NL_MODE_SET); - oappend(SET_F("")); - oappendi(nightlightDelayMins); - oappend(SET_F("")); - oappendi(nightlightTargetBri); - oappend(SET_F("")); - oappendi(effectCurrent); - oappend(SET_F("")); - oappendi(effectSpeed); - oappend(SET_F("")); - oappendi(effectIntensity); - oappend(SET_F("")); - oappendi(effectPalette); - oappend(SET_F("")); + dest.print(F("")); + dest.print(notifyDirect); + dest.print(F("")); + dest.print(receiveGroups!=0); + dest.print(F("")); + dest.print(nightlightActive); + dest.print(F("")); + dest.print(nightlightMode > NL_MODE_SET); + dest.print(F("")); + dest.print(nightlightDelayMins); + dest.print(F("")); + dest.print(nightlightTargetBri); + dest.print(F("")); + dest.print(effectCurrent); + dest.print(F("")); + dest.print(effectSpeed); + dest.print(F("")); + dest.print(effectIntensity); + dest.print(F("")); + dest.print(effectPalette); + dest.print(F("")); if (strip.hasWhiteChannel()) { - oappendi(col[3]); + dest.print(col[3]); } else { - oappend("-1"); + dest.print("-1"); } - oappend(SET_F("")); - oappendi(colSec[3]); - oappend(SET_F("")); - oappendi(currentPreset); - oappend(SET_F("")); - oappendi(currentPlaylist >= 0); - oappend(SET_F("")); - oappend(serverDescription); + dest.print(F("")); + dest.print(colSec[3]); + dest.print(F("")); + dest.print(currentPreset); + dest.print(F("")); + dest.print(currentPlaylist >= 0); + dest.print(F("")); + dest.print(serverDescription); if (realtimeMode) { - oappend(SET_F(" (live)")); + dest.print(F(" (live)")); } - oappend(SET_F("")); - oappendi(strip.getFirstSelectedSegId()); - oappend(SET_F("")); - if (request != nullptr) request->send(200, "text/xml", obuf); + dest.print(F("")); + dest.print(strip.getFirstSelectedSegId()); + dest.print(F("")); } -void extractPin(JsonObject &obj, const char *key) { +static void extractPin(Print& dest, JsonObject &obj, const char *key) { if (obj[key].is()) { JsonArray pins = obj[key].as(); for (JsonVariant pv : pins) { - if (pv.as() > -1) { oappend(","); oappendi(pv.as()); } + if (pv.as() > -1) { dest.print(","); dest.print(pv.as()); } } } else { - if (obj[key].as() > -1) { oappend(","); oappendi(obj[key].as()); } + if (obj[key].as() > -1) { dest.print(","); dest.print(obj[key].as()); } } } -// oappend used pins by scanning JsonObject (1 level deep) -void fillUMPins(JsonObject &mods) +// dest.print used pins by scanning JsonObject (1 level deep) +void fillUMPins(Print& dest, JsonObject &mods) { for (JsonPair kv : mods) { // kv.key() is usermod name or subobject key @@ -93,7 +88,7 @@ void fillUMPins(JsonObject &mods) if (!obj.isNull()) { // element is an JsonObject if (!obj["pin"].isNull()) { - extractPin(obj, "pin"); + extractPin(dest, obj, "pin"); } else { // scan keys (just one level deep as is possible with usermods) for (JsonPair so : obj) { @@ -102,7 +97,7 @@ void fillUMPins(JsonObject &mods) // we found a key containing "pin" substring if (strlen(strstr(key, "pin")) == 3) { // and it is at the end, we found another pin - extractPin(obj, key); + extractPin(dest, obj, key); continue; } } @@ -110,7 +105,7 @@ void fillUMPins(JsonObject &mods) JsonObject subObj = obj[so.key()]; if (!subObj["pin"].isNull()) { // get pins from subobject - extractPin(subObj, "pin"); + extractPin(dest, subObj, "pin"); } } } @@ -118,101 +113,99 @@ void fillUMPins(JsonObject &mods) } } -void appendGPIOinfo() { +void appendGPIOinfo(Print& dest) { char nS[8]; // add usermod pins as d.um_p array - oappend(SET_F("d.um_p=[-1")); // has to have 1 element + dest.print(F("d.um_p=[-1")); // has to have 1 element if (i2c_sda > -1 && i2c_scl > -1) { - oappend(","); oappend(itoa(i2c_sda,nS,10)); - oappend(","); oappend(itoa(i2c_scl,nS,10)); + dest.print(","); dest.print(itoa(i2c_sda,nS,10)); + dest.print(","); dest.print(itoa(i2c_scl,nS,10)); } if (spi_mosi > -1 && spi_sclk > -1) { - oappend(","); oappend(itoa(spi_mosi,nS,10)); - oappend(","); oappend(itoa(spi_sclk,nS,10)); + dest.print(","); dest.print(itoa(spi_mosi,nS,10)); + dest.print(","); dest.print(itoa(spi_sclk,nS,10)); } // usermod pin reservations will become unnecessary when settings pages will read cfg.json directly if (requestJSONBufferLock(6)) { // if we can't allocate JSON buffer ignore usermod pins JsonObject mods = pDoc->createNestedObject(F("um")); usermods.addToConfig(mods); - if (!mods.isNull()) fillUMPins(mods); + if (!mods.isNull()) fillUMPins(dest, mods); releaseJSONBufferLock(); } - oappend(SET_F("];")); + dest.print(F("];")); // add reserved (unusable) pins - oappend(SET_F("d.rsvd=[")); + dest.print(SET_F("d.rsvd=[")); for (unsigned i = 0; i < WLED_NUM_PINS; i++) { if (!pinManager.isPinOk(i, false)) { // include readonly pins - oappendi(i); oappend(","); + dest.print(i); dest.print(","); } } #ifdef WLED_ENABLE_DMX - oappend(SET_F("2,")); // DMX hardcoded pin + dest.print(SET_F("2,")); // DMX hardcoded pin #endif #if defined(WLED_DEBUG) && !defined(WLED_DEBUG_HOST) - oappend(itoa(hardwareTX,nS,10)); oappend(","); // debug output (TX) pin + dest.print(itoa(hardwareTX,nS,10)); dest.print(","); // debug output (TX) pin #endif //Note: Using pin 3 (RX) disables Adalight / Serial JSON #ifdef WLED_USE_ETHERNET if (ethernetType != WLED_ETH_NONE && ethernetType < WLED_NUM_ETH_TYPES) { - for (unsigned p=0; p=0) { oappend(itoa(ethernetBoards[ethernetType].eth_power,nS,10)); oappend(","); } - if (ethernetBoards[ethernetType].eth_mdc>=0) { oappend(itoa(ethernetBoards[ethernetType].eth_mdc,nS,10)); oappend(","); } - if (ethernetBoards[ethernetType].eth_mdio>=0) { oappend(itoa(ethernetBoards[ethernetType].eth_mdio,nS,10)); oappend(","); } + for (unsigned p=0; p=0) { dest.print(itoa(ethernetBoards[ethernetType].eth_power,nS,10)); dest.print(","); } + if (ethernetBoards[ethernetType].eth_mdc>=0) { dest.print(itoa(ethernetBoards[ethernetType].eth_mdc,nS,10)); dest.print(","); } + if (ethernetBoards[ethernetType].eth_mdio>=0) { dest.print(itoa(ethernetBoards[ethernetType].eth_mdio,nS,10)); dest.print(","); } switch (ethernetBoards[ethernetType].eth_clk_mode) { case ETH_CLOCK_GPIO0_IN: case ETH_CLOCK_GPIO0_OUT: - oappend(SET_F("0")); + dest.print(SET_F("0")); break; case ETH_CLOCK_GPIO16_OUT: - oappend(SET_F("16")); + dest.print(SET_F("16")); break; case ETH_CLOCK_GPIO17_OUT: - oappend(SET_F("17")); + dest.print(SET_F("17")); break; } } #endif - oappend(SET_F("];")); // rsvd + dest.print(SET_F("];")); // rsvd // add info for read-only GPIO - oappend(SET_F("d.ro_gpio=[")); + dest.print(SET_F("d.ro_gpio=[")); bool firstPin = true; for (unsigned i = 0; i < WLED_NUM_PINS; i++) { if (pinManager.isReadOnlyPin(i)) { // No comma before the first pin - if (!firstPin) oappend(SET_F(",")); - oappendi(i); + if (!firstPin) dest.print(SET_F(",")); + dest.print(i); firstPin = false; } } - oappend(SET_F("];")); + dest.print(SET_F("];")); // add info about max. # of pins - oappend(SET_F("d.max_gpio=")); - oappendi(WLED_NUM_PINS); - oappend(SET_F(";")); + dest.print(SET_F("d.max_gpio=")); + dest.print(WLED_NUM_PINS); + dest.print(SET_F(";")); } //get values for settings form in javascript -void getSettingsJS(byte subPage, char* dest) +void getSettingsJS(byte subPage, Print& dest) { //0: menu 1: wifi 2: leds 3: ui 4: sync 5: time 6: sec DEBUG_PRINTF_P(PSTR("settings resp %u\n"), (unsigned)subPage); - obuf = dest; - olen = 0; if (subPage <0 || subPage >10) return; if (subPage == SUBPAGE_MENU) { #ifdef WLED_DISABLE_2D // include only if 2D is not compiled in - oappend(PSTR("gId('2dbtn').style.display='none';")); + dest.print(F("gId('2dbtn').style.display='none';")); #endif #ifdef WLED_ENABLE_DMX // include only if DMX is enabled - oappend(PSTR("gId('dmxbtn').style.display='';")); + dest.print(F("gId('dmxbtn').style.display='';")); #endif } @@ -220,65 +213,65 @@ void getSettingsJS(byte subPage, char* dest) { char nS[10]; size_t l; - oappend(SET_F("resetWiFi(")); - oappend(itoa(WLED_MAX_WIFI_COUNT,nS,10)); - oappend(SET_F(");")); + dest.print(F("resetWiFi(")); + dest.print(WLED_MAX_WIFI_COUNT); + dest.print(F(");")); for (size_t n = 0; n < multiWiFi.size(); n++) { l = strlen(multiWiFi[n].clientPass); char fpass[l+1]; //fill password field with *** fpass[l] = 0; memset(fpass,'*',l); - oappend(SET_F("addWiFi(\"")); - oappend(multiWiFi[n].clientSSID); - oappend(SET_F("\",\"")); - oappend(fpass); - oappend(SET_F("\",0x")); - oappend(itoa(multiWiFi[n].staticIP,nS,16)); - oappend(SET_F(",0x")); - oappend(itoa(multiWiFi[n].staticGW,nS,16)); - oappend(SET_F(",0x")); - oappend(itoa(multiWiFi[n].staticSN,nS,16)); - oappend(SET_F(");")); + dest.print(F("addWiFi(\"")); + dest.print(multiWiFi[n].clientSSID); + dest.print(F("\",\"")); + dest.print(fpass); + dest.print(F("\",0x")); + dest.print(itoa(multiWiFi[n].staticIP,nS,16)); + dest.print(F(",0x")); + dest.print(itoa(multiWiFi[n].staticGW,nS,16)); + dest.print(F(",0x")); + dest.print(itoa(multiWiFi[n].staticSN,nS,16)); + dest.print(F(");")); } - sappend('v',SET_F("D0"),dnsAddress[0]); - sappend('v',SET_F("D1"),dnsAddress[1]); - sappend('v',SET_F("D2"),dnsAddress[2]); - sappend('v',SET_F("D3"),dnsAddress[3]); + sappend(dest,'v',SET_F("D0"),dnsAddress[0]); + sappend(dest,'v',SET_F("D1"),dnsAddress[1]); + sappend(dest,'v',SET_F("D2"),dnsAddress[2]); + sappend(dest,'v',SET_F("D3"),dnsAddress[3]); - sappends('s',SET_F("CM"),cmDNS); - sappend('i',SET_F("AB"),apBehavior); - sappends('s',SET_F("AS"),apSSID); - sappend('c',SET_F("AH"),apHide); + sappends(dest,'s',SET_F("CM"),cmDNS); + sappend(dest,'i',SET_F("AB"),apBehavior); + sappends(dest,'s',SET_F("AS"),apSSID); + sappend(dest,'c',SET_F("AH"),apHide); l = strlen(apPass); char fapass[l+1]; //fill password field with *** fapass[l] = 0; memset(fapass,'*',l); - sappends('s',SET_F("AP"),fapass); + sappends(dest,'s',SET_F("AP"),fapass); - sappend('v',SET_F("AC"),apChannel); + sappend(dest,'v',SET_F("AC"),apChannel); #ifdef ARDUINO_ARCH_ESP32 - sappend('v',SET_F("TX"),txPower); + sappend(dest,'v',SET_F("TX"),txPower); #else - oappend(SET_F("gId('tx').style.display='none';")); + dest.print(F("gId('tx').style.display='none';")); #endif - sappend('c',SET_F("FG"),force802_3g); - sappend('c',SET_F("WS"),noWifiSleep); + sappend(dest,'c',SET_F("FG"),force802_3g); + sappend(dest,'c',SET_F("WS"),noWifiSleep); #ifndef WLED_DISABLE_ESPNOW - sappend('c',SET_F("RE"),enableESPNow); - sappends('s',SET_F("RMAC"),linked_remote); + sappend(dest,'c',SET_F("RE"),enableESPNow); + sappends(dest,'s',SET_F("RMAC"),linked_remote); #else //hide remote settings if not compiled - oappend(SET_F("toggle('ESPNOW');")); // hide ESP-NOW setting + dest.print(F("toggle('ESPNOW');")); // hide ESP-NOW setting #endif #ifdef WLED_USE_ETHERNET - sappend('v',SET_F("ETH"),ethernetType); + sappend(dest,'v',SET_F("ETH"),ethernetType); #else //hide ethernet setting if not compiled in - oappend(SET_F("gId('ethd').style.display='none';")); + dest.print(F("gId('ethd').style.display='none';")); #endif if (Network.isConnected()) //is connected @@ -290,10 +283,10 @@ void getSettingsJS(byte subPage, char* dest) #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) if (Network.isEthernet()) strcat_P(s ,SET_F(" (Ethernet)")); #endif - sappends('m',SET_F("(\"sip\")[0]"),s); + sappends(dest,'m',SET_F("(\"sip\")[0]"),s); } else { - sappends('m',SET_F("(\"sip\")[0]"),(char*)F("Not connected")); + sappends(dest,'m',SET_F("(\"sip\")[0]"),(char*)F("Not connected")); } if (WiFi.softAPIP()[0] != 0) //is active @@ -301,19 +294,19 @@ void getSettingsJS(byte subPage, char* dest) char s[16]; IPAddress apIP = WiFi.softAPIP(); sprintf(s, "%d.%d.%d.%d", apIP[0], apIP[1], apIP[2], apIP[3]); - sappends('m',SET_F("(\"sip\")[1]"),s); + sappends(dest,'m',SET_F("(\"sip\")[1]"),s); } else { - sappends('m',SET_F("(\"sip\")[1]"),(char*)F("Not active")); + sappends(dest,'m',SET_F("(\"sip\")[1]"),(char*)F("Not active")); } #ifndef WLED_DISABLE_ESPNOW if (strlen(last_signal_src) > 0) { //Have seen an ESP-NOW Remote - sappends('m',SET_F("(\"rlid\")[0]"),last_signal_src); + sappends(dest,'m',SET_F("(\"rlid\")[0]"),last_signal_src); } else if (!enableESPNow) { - sappends('m',SET_F("(\"rlid\")[0]"),(char*)F("(Enable ESP-NOW to listen)")); + sappends(dest,'m',SET_F("(\"rlid\")[0]"),(char*)F("(Enable ESP-NOW to listen)")); } else { - sappends('m',SET_F("(\"rlid\")[0]"),(char*)F("None")); + sappends(dest,'m',SET_F("(\"rlid\")[0]"),(char*)F("None")); } #endif } @@ -322,30 +315,30 @@ void getSettingsJS(byte subPage, char* dest) { char nS[32]; - appendGPIOinfo(); + appendGPIOinfo(dest); - oappend(SET_F("d.ledTypes=")); oappend(BusManager::getLEDTypesJSONString().c_str()); oappend(";"); + dest.print(SET_F("d.ledTypes=")); dest.print(BusManager::getLEDTypesJSONString().c_str()); dest.print(";"); // set limits - oappend(SET_F("bLimits(")); - oappend(itoa(WLED_MAX_BUSSES,nS,10)); oappend(","); - oappend(itoa(WLED_MIN_VIRTUAL_BUSSES,nS,10)); oappend(","); - oappend(itoa(MAX_LEDS_PER_BUS,nS,10)); oappend(","); - oappend(itoa(MAX_LED_MEMORY,nS,10)); oappend(","); - oappend(itoa(MAX_LEDS,nS,10)); oappend(","); - oappend(itoa(WLED_MAX_COLOR_ORDER_MAPPINGS,nS,10)); oappend(","); - oappend(itoa(WLED_MAX_DIGITAL_CHANNELS,nS,10)); oappend(","); - oappend(itoa(WLED_MAX_ANALOG_CHANNELS,nS,10)); - oappend(SET_F(");")); - - sappend('c',SET_F("MS"),strip.autoSegments); - sappend('c',SET_F("CCT"),strip.correctWB); - sappend('c',SET_F("IC"),cctICused); - sappend('c',SET_F("CR"),strip.cctFromRgb); - sappend('v',SET_F("CB"),strip.cctBlending); - sappend('v',SET_F("FR"),strip.getTargetFps()); - sappend('v',SET_F("AW"),Bus::getGlobalAWMode()); - sappend('c',SET_F("LD"),useGlobalLedBuffer); + dest.print(F("bLimits(")); + dest.print(itoa(WLED_MAX_BUSSES,nS,10)); dest.print(","); + dest.print(itoa(WLED_MIN_VIRTUAL_BUSSES,nS,10)); dest.print(","); + dest.print(itoa(MAX_LEDS_PER_BUS,nS,10)); dest.print(","); + dest.print(itoa(MAX_LED_MEMORY,nS,10)); dest.print(","); + dest.print(itoa(MAX_LEDS,nS,10)); dest.print(","); + dest.print(itoa(WLED_MAX_COLOR_ORDER_MAPPINGS,nS,10)); dest.print(","); + dest.print(itoa(WLED_MAX_DIGITAL_CHANNELS,nS,10)); dest.print(","); + dest.print(itoa(WLED_MAX_ANALOG_CHANNELS,nS,10)); + dest.print(F(");")); + + sappend(dest,'c',SET_F("MS"),strip.autoSegments); + sappend(dest,'c',SET_F("CCT"),strip.correctWB); + sappend(dest,'c',SET_F("IC"),cctICused); + sappend(dest,'c',SET_F("CR"),strip.cctFromRgb); + sappend(dest,'v',SET_F("CB"),strip.cctBlending); + sappend(dest,'v',SET_F("FR"),strip.getTargetFps()); + sappend(dest,'v',SET_F("AW"),Bus::getGlobalAWMode()); + sappend(dest,'c',SET_F("LD"),useGlobalLedBuffer); unsigned sumMa = 0; for (int s = 0; s < BusManager::getNumBusses(); s++) { @@ -365,22 +358,22 @@ void getSettingsJS(byte subPage, char* dest) char sp[4] = "SP"; sp[2] = offset+s; sp[3] = 0; //bus clock speed char la[4] = "LA"; la[2] = offset+s; la[3] = 0; //LED current char ma[4] = "MA"; ma[2] = offset+s; ma[3] = 0; //max per-port PSU current - oappend(SET_F("addLEDs(1);")); + dest.print(F("addLEDs(1);")); uint8_t pins[5]; int nPins = bus->getPins(pins); for (int i = 0; i < nPins; i++) { lp[1] = offset+i; - if (pinManager.isPinOk(pins[i]) || bus->isVirtual()) sappend('v',lp,pins[i]); + if (pinManager.isPinOk(pins[i]) || bus->isVirtual()) sappend(dest,'v',lp,pins[i]); } - sappend('v',lc,bus->getLength()); - sappend('v',lt,bus->getType()); - sappend('v',co,bus->getColorOrder() & 0x0F); - sappend('v',ls,bus->getStart()); - sappend('c',cv,bus->isReversed()); - sappend('v',sl,bus->skippedLeds()); - sappend('c',rf,bus->isOffRefreshRequired()); - sappend('v',aw,bus->getAutoWhiteMode()); - sappend('v',wo,bus->getColorOrder() >> 4); + sappend(dest,'v',lc,bus->getLength()); + sappend(dest,'v',lt,bus->getType()); + sappend(dest,'v',co,bus->getColorOrder() & 0x0F); + sappend(dest,'v',ls,bus->getStart()); + sappend(dest,'c',cv,bus->isReversed()); + sappend(dest,'v',sl,bus->skippedLeds()); + sappend(dest,'c',rf,bus->isOffRefreshRequired()); + sappend(dest,'v',aw,bus->getAutoWhiteMode()); + sappend(dest,'v',wo,bus->getColorOrder() >> 4); unsigned speed = bus->getFrequency(); if (bus->isPWM()) { switch (speed) { @@ -401,158 +394,158 @@ void getSettingsJS(byte subPage, char* dest) case 20000 : speed = 4; break; } } - sappend('v',sp,speed); - sappend('v',la,bus->getLEDCurrent()); - sappend('v',ma,bus->getMaxCurrent()); + sappend(dest,'v',sp,speed); + sappend(dest,'v',la,bus->getLEDCurrent()); + sappend(dest,'v',ma,bus->getMaxCurrent()); sumMa += bus->getMaxCurrent(); } - sappend('v',SET_F("MA"),BusManager::ablMilliampsMax() ? BusManager::ablMilliampsMax() : sumMa); - sappend('c',SET_F("ABL"),BusManager::ablMilliampsMax() || sumMa > 0); - sappend('c',SET_F("PPL"),!BusManager::ablMilliampsMax() && sumMa > 0); + sappend(dest,'v',SET_F("MA"),BusManager::ablMilliampsMax() ? BusManager::ablMilliampsMax() : sumMa); + sappend(dest,'c',SET_F("ABL"),BusManager::ablMilliampsMax() || sumMa > 0); + sappend(dest,'c',SET_F("PPL"),!BusManager::ablMilliampsMax() && sumMa > 0); - oappend(SET_F("resetCOM(")); - oappend(itoa(WLED_MAX_COLOR_ORDER_MAPPINGS,nS,10)); - oappend(SET_F(");")); + dest.print(F("resetCOM(")); + dest.print(itoa(WLED_MAX_COLOR_ORDER_MAPPINGS,nS,10)); + dest.print(F(");")); const ColorOrderMap& com = BusManager::getColorOrderMap(); for (int s = 0; s < com.count(); s++) { const ColorOrderMapEntry* entry = com.get(s); if (entry == nullptr) break; - oappend(SET_F("addCOM(")); - oappend(itoa(entry->start,nS,10)); oappend(","); - oappend(itoa(entry->len,nS,10)); oappend(","); - oappend(itoa(entry->colorOrder,nS,10)); oappend(");"); + dest.print(F("addCOM(")); + dest.print(itoa(entry->start,nS,10)); dest.print(","); + dest.print(itoa(entry->len,nS,10)); dest.print(","); + dest.print(itoa(entry->colorOrder,nS,10)); dest.print(");"); } - sappend('v',SET_F("CA"),briS); - - sappend('c',SET_F("BO"),turnOnAtBoot); - sappend('v',SET_F("BP"),bootPreset); - - sappend('c',SET_F("GB"),gammaCorrectBri); - sappend('c',SET_F("GC"),gammaCorrectCol); - dtostrf(gammaCorrectVal,3,1,nS); sappends('s',SET_F("GV"),nS); - sappend('c',SET_F("TF"),fadeTransition); - sappend('c',SET_F("EB"),modeBlending); - sappend('v',SET_F("TD"),transitionDelayDefault); - sappend('c',SET_F("PF"),strip.paletteFade); - sappend('v',SET_F("TP"),randomPaletteChangeTime); - sappend('c',SET_F("TH"),useHarmonicRandomPalette); - sappend('v',SET_F("BF"),briMultiplier); - sappend('v',SET_F("TB"),nightlightTargetBri); - sappend('v',SET_F("TL"),nightlightDelayMinsDefault); - sappend('v',SET_F("TW"),nightlightMode); - sappend('i',SET_F("PB"),strip.paletteBlend); - sappend('v',SET_F("RL"),rlyPin); - sappend('c',SET_F("RM"),rlyMde); - sappend('c',SET_F("RO"),rlyOpenDrain); + sappend(dest,'v',SET_F("CA"),briS); + + sappend(dest,'c',SET_F("BO"),turnOnAtBoot); + sappend(dest,'v',SET_F("BP"),bootPreset); + + sappend(dest,'c',SET_F("GB"),gammaCorrectBri); + sappend(dest,'c',SET_F("GC"),gammaCorrectCol); + dtostrf(gammaCorrectVal,3,1,nS); sappends(dest,'s',SET_F("GV"),nS); + sappend(dest,'c',SET_F("TF"),fadeTransition); + sappend(dest,'c',SET_F("EB"),modeBlending); + sappend(dest,'v',SET_F("TD"),transitionDelayDefault); + sappend(dest,'c',SET_F("PF"),strip.paletteFade); + sappend(dest,'v',SET_F("TP"),randomPaletteChangeTime); + sappend(dest,'c',SET_F("TH"),useHarmonicRandomPalette); + sappend(dest,'v',SET_F("BF"),briMultiplier); + sappend(dest,'v',SET_F("TB"),nightlightTargetBri); + sappend(dest,'v',SET_F("TL"),nightlightDelayMinsDefault); + sappend(dest,'v',SET_F("TW"),nightlightMode); + sappend(dest,'i',SET_F("PB"),strip.paletteBlend); + sappend(dest,'v',SET_F("RL"),rlyPin); + sappend(dest,'c',SET_F("RM"),rlyMde); + sappend(dest,'c',SET_F("RO"),rlyOpenDrain); for (int i = 0; i < WLED_MAX_BUTTONS; i++) { - oappend(SET_F("addBtn(")); - oappend(itoa(i,nS,10)); oappend(","); - oappend(itoa(btnPin[i],nS,10)); oappend(","); - oappend(itoa(buttonType[i],nS,10)); - oappend(SET_F(");")); + dest.print(F("addBtn(")); + dest.print(itoa(i,nS,10)); dest.print(","); + dest.print(itoa(btnPin[i],nS,10)); dest.print(","); + dest.print(itoa(buttonType[i],nS,10)); + dest.print(F(");")); } - sappend('c',SET_F("IP"),disablePullUp); - sappend('v',SET_F("TT"),touchThreshold); + sappend(dest,'c',SET_F("IP"),disablePullUp); + sappend(dest,'v',SET_F("TT"),touchThreshold); #ifndef WLED_DISABLE_INFRARED - sappend('v',SET_F("IR"),irPin); - sappend('v',SET_F("IT"),irEnabled); + sappend(dest,'v',SET_F("IR"),irPin); + sappend(dest,'v',SET_F("IT"),irEnabled); #endif - sappend('c',SET_F("MSO"),!irApplyToAllSelected); + sappend(dest,'c',SET_F("MSO"),!irApplyToAllSelected); } if (subPage == SUBPAGE_UI) { - sappends('s',SET_F("DS"),serverDescription); - sappend('c',SET_F("SU"),simplifiedUI); + sappends(dest,'s',SET_F("DS"),serverDescription); + sappend(dest,'c',SET_F("SU"),simplifiedUI); } if (subPage == SUBPAGE_SYNC) { [[maybe_unused]] char nS[32]; - sappend('v',SET_F("UP"),udpPort); - sappend('v',SET_F("U2"),udpPort2); + sappend(dest,'v',SET_F("UP"),udpPort); + sappend(dest,'v',SET_F("U2"),udpPort2); #ifndef WLED_DISABLE_ESPNOW - if (enableESPNow) sappend('c',SET_F("EN"),useESPNowSync); - else oappend(SET_F("toggle('ESPNOW');")); // hide ESP-NOW setting + if (enableESPNow) sappend(dest,'c',SET_F("EN"),useESPNowSync); + else dest.print(F("toggle('ESPNOW');")); // hide ESP-NOW setting #else - oappend(SET_F("toggle('ESPNOW');")); // hide ESP-NOW setting + dest.print(F("toggle('ESPNOW');")); // hide ESP-NOW setting #endif - sappend('v',SET_F("GS"),syncGroups); - sappend('v',SET_F("GR"),receiveGroups); - - sappend('c',SET_F("RB"),receiveNotificationBrightness); - sappend('c',SET_F("RC"),receiveNotificationColor); - sappend('c',SET_F("RX"),receiveNotificationEffects); - sappend('c',SET_F("RP"),receiveNotificationPalette); - sappend('c',SET_F("SO"),receiveSegmentOptions); - sappend('c',SET_F("SG"),receiveSegmentBounds); - sappend('c',SET_F("SS"),sendNotifications); - sappend('c',SET_F("SD"),notifyDirect); - sappend('c',SET_F("SB"),notifyButton); - sappend('c',SET_F("SH"),notifyHue); - sappend('v',SET_F("UR"),udpNumRetries); - - sappend('c',SET_F("NL"),nodeListEnabled); - sappend('c',SET_F("NB"),nodeBroadcastEnabled); - - sappend('c',SET_F("RD"),receiveDirect); - sappend('c',SET_F("MO"),useMainSegmentOnly); - sappend('c',SET_F("RLM"),realtimeRespectLedMaps); - sappend('v',SET_F("EP"),e131Port); - sappend('c',SET_F("ES"),e131SkipOutOfSequence); - sappend('c',SET_F("EM"),e131Multicast); - sappend('v',SET_F("EU"),e131Universe); - sappend('v',SET_F("DA"),DMXAddress); - sappend('v',SET_F("XX"),DMXSegmentSpacing); - sappend('v',SET_F("PY"),e131Priority); - sappend('v',SET_F("DM"),DMXMode); - sappend('v',SET_F("ET"),realtimeTimeoutMs); - sappend('c',SET_F("FB"),arlsForceMaxBri); - sappend('c',SET_F("RG"),arlsDisableGammaCorrection); - sappend('v',SET_F("WO"),arlsOffset); + sappend(dest,'v',SET_F("GS"),syncGroups); + sappend(dest,'v',SET_F("GR"),receiveGroups); + + sappend(dest,'c',SET_F("RB"),receiveNotificationBrightness); + sappend(dest,'c',SET_F("RC"),receiveNotificationColor); + sappend(dest,'c',SET_F("RX"),receiveNotificationEffects); + sappend(dest,'c',SET_F("RP"),receiveNotificationPalette); + sappend(dest,'c',SET_F("SO"),receiveSegmentOptions); + sappend(dest,'c',SET_F("SG"),receiveSegmentBounds); + sappend(dest,'c',SET_F("SS"),sendNotifications); + sappend(dest,'c',SET_F("SD"),notifyDirect); + sappend(dest,'c',SET_F("SB"),notifyButton); + sappend(dest,'c',SET_F("SH"),notifyHue); + sappend(dest,'v',SET_F("UR"),udpNumRetries); + + sappend(dest,'c',SET_F("NL"),nodeListEnabled); + sappend(dest,'c',SET_F("NB"),nodeBroadcastEnabled); + + sappend(dest,'c',SET_F("RD"),receiveDirect); + sappend(dest,'c',SET_F("MO"),useMainSegmentOnly); + sappend(dest,'c',SET_F("RLM"),realtimeRespectLedMaps); + sappend(dest,'v',SET_F("EP"),e131Port); + sappend(dest,'c',SET_F("ES"),e131SkipOutOfSequence); + sappend(dest,'c',SET_F("EM"),e131Multicast); + sappend(dest,'v',SET_F("EU"),e131Universe); + sappend(dest,'v',SET_F("DA"),DMXAddress); + sappend(dest,'v',SET_F("XX"),DMXSegmentSpacing); + sappend(dest,'v',SET_F("PY"),e131Priority); + sappend(dest,'v',SET_F("DM"),DMXMode); + sappend(dest,'v',SET_F("ET"),realtimeTimeoutMs); + sappend(dest,'c',SET_F("FB"),arlsForceMaxBri); + sappend(dest,'c',SET_F("RG"),arlsDisableGammaCorrection); + sappend(dest,'v',SET_F("WO"),arlsOffset); #ifndef WLED_DISABLE_ALEXA - sappend('c',SET_F("AL"),alexaEnabled); - sappends('s',SET_F("AI"),alexaInvocationName); - sappend('c',SET_F("SA"),notifyAlexa); - sappend('v',SET_F("AP"),alexaNumPresets); + sappend(dest,'c',SET_F("AL"),alexaEnabled); + sappends(dest,'s',SET_F("AI"),alexaInvocationName); + sappend(dest,'c',SET_F("SA"),notifyAlexa); + sappend(dest,'v',SET_F("AP"),alexaNumPresets); #else - oappend(SET_F("toggle('Alexa');")); // hide Alexa settings + dest.print(F("toggle('Alexa');")); // hide Alexa settings #endif #ifndef WLED_DISABLE_MQTT - sappend('c',SET_F("MQ"),mqttEnabled); - sappends('s',SET_F("MS"),mqttServer); - sappend('v',SET_F("MQPORT"),mqttPort); - sappends('s',SET_F("MQUSER"),mqttUser); + sappend(dest,'c',SET_F("MQ"),mqttEnabled); + sappends(dest,'s',SET_F("MS"),mqttServer); + sappend(dest,'v',SET_F("MQPORT"),mqttPort); + sappends(dest,'s',SET_F("MQUSER"),mqttUser); byte l = strlen(mqttPass); char fpass[l+1]; //fill password field with *** fpass[l] = 0; memset(fpass,'*',l); - sappends('s',SET_F("MQPASS"),fpass); - sappends('s',SET_F("MQCID"),mqttClientID); - sappends('s',"MD",mqttDeviceTopic); - sappends('s',SET_F("MG"),mqttGroupTopic); - sappend('c',SET_F("BM"),buttonPublishMqtt); - sappend('c',SET_F("RT"),retainMqttMsg); - oappend(SET_F("d.Sf.MD.maxlength=")); oappend(itoa(MQTT_MAX_TOPIC_LEN,nS,10)); oappend(SET_F(";")); - oappend(SET_F("d.Sf.MG.maxlength=")); oappend(itoa(MQTT_MAX_TOPIC_LEN,nS,10)); oappend(SET_F(";")); - oappend(SET_F("d.Sf.MS.maxlength=")); oappend(itoa(MQTT_MAX_SERVER_LEN,nS,10)); oappend(SET_F(";")); + sappends(dest,'s',SET_F("MQPASS"),fpass); + sappends(dest,'s',SET_F("MQCID"),mqttClientID); + sappends(dest,'s',"MD",mqttDeviceTopic); + sappends(dest,'s',SET_F("MG"),mqttGroupTopic); + sappend(dest,'c',SET_F("BM"),buttonPublishMqtt); + sappend(dest,'c',SET_F("RT"),retainMqttMsg); + dest.print(F("d.Sf.MD.maxlength=")); dest.print(itoa(MQTT_MAX_TOPIC_LEN,nS,10)); dest.print(F(";")); + dest.print(F("d.Sf.MG.maxlength=")); dest.print(itoa(MQTT_MAX_TOPIC_LEN,nS,10)); dest.print(F(";")); + dest.print(F("d.Sf.MS.maxlength=")); dest.print(itoa(MQTT_MAX_SERVER_LEN,nS,10)); dest.print(F(";")); #else - oappend(SET_F("toggle('MQTT');")); // hide MQTT settings + dest.print(F("toggle('MQTT');")); // hide MQTT settings #endif #ifndef WLED_DISABLE_HUESYNC - sappend('v',SET_F("H0"),hueIP[0]); - sappend('v',SET_F("H1"),hueIP[1]); - sappend('v',SET_F("H2"),hueIP[2]); - sappend('v',SET_F("H3"),hueIP[3]); - sappend('v',SET_F("HL"),huePollLightId); - sappend('v',SET_F("HI"),huePollIntervalMs); - sappend('c',SET_F("HP"),huePollingEnabled); - sappend('c',SET_F("HO"),hueApplyOnOff); - sappend('c',SET_F("HB"),hueApplyBri); - sappend('c',SET_F("HC"),hueApplyColor); + sappend(dest,'v',SET_F("H0"),hueIP[0]); + sappend(dest,'v',SET_F("H1"),hueIP[1]); + sappend(dest,'v',SET_F("H2"),hueIP[2]); + sappend(dest,'v',SET_F("H3"),hueIP[3]); + sappend(dest,'v',SET_F("HL"),huePollLightId); + sappend(dest,'v',SET_F("HI"),huePollIntervalMs); + sappend(dest,'c',SET_F("HP"),huePollingEnabled); + sappend(dest,'c',SET_F("HO"),hueApplyOnOff); + sappend(dest,'c',SET_F("HB"),hueApplyBri); + sappend(dest,'c',SET_F("HC"),hueApplyColor); char hueErrorString[25]; switch (hueError) { @@ -566,61 +559,61 @@ void getSettingsJS(byte subPage, char* dest) default: sprintf_P(hueErrorString,PSTR("Bridge Error %i"),hueError); } - sappends('m',SET_F("(\"sip\")[0]"),hueErrorString); + sappends(dest,'m',SET_F("(\"sip\")[0]"),hueErrorString); #else - oappend(SET_F("toggle('Hue');")); // hide Hue Sync settings + dest.print(F("toggle('Hue');")); // hide Hue Sync settings #endif - sappend('v',SET_F("BD"),serialBaud); + sappend(dest,'v',SET_F("BD"),serialBaud); #ifndef WLED_ENABLE_ADALIGHT - oappend(SET_F("toggle('Serial);")); + dest.print(SET_F("toggle('Serial);")); #endif } if (subPage == SUBPAGE_TIME) { - sappend('c',SET_F("NT"),ntpEnabled); - sappends('s',SET_F("NS"),ntpServerName); - sappend('c',SET_F("CF"),!useAMPM); - sappend('i',SET_F("TZ"),currentTimezone); - sappend('v',SET_F("UO"),utcOffsetSecs); + sappend(dest,'c',SET_F("NT"),ntpEnabled); + sappends(dest,'s',SET_F("NS"),ntpServerName); + sappend(dest,'c',SET_F("CF"),!useAMPM); + sappend(dest,'i',SET_F("TZ"),currentTimezone); + sappend(dest,'v',SET_F("UO"),utcOffsetSecs); char tm[32]; dtostrf(longitude,4,2,tm); - sappends('s',SET_F("LN"),tm); + sappends(dest,'s',SET_F("LN"),tm); dtostrf(latitude,4,2,tm); - sappends('s',SET_F("LT"),tm); + sappends(dest,'s',SET_F("LT"),tm); getTimeString(tm); - sappends('m',SET_F("(\"times\")[0]"),tm); + sappends(dest,'m',SET_F("(\"times\")[0]"),tm); if ((int)(longitude*10.0f) || (int)(latitude*10.0f)) { sprintf_P(tm, PSTR("Sunrise: %02d:%02d Sunset: %02d:%02d"), hour(sunrise), minute(sunrise), hour(sunset), minute(sunset)); - sappends('m',SET_F("(\"times\")[1]"),tm); + sappends(dest,'m',SET_F("(\"times\")[1]"),tm); } - sappend('c',SET_F("OL"),overlayCurrent); - sappend('v',SET_F("O1"),overlayMin); - sappend('v',SET_F("O2"),overlayMax); - sappend('v',SET_F("OM"),analogClock12pixel); - sappend('c',SET_F("OS"),analogClockSecondsTrail); - sappend('c',SET_F("O5"),analogClock5MinuteMarks); - sappend('c',SET_F("OB"),analogClockSolidBlack); - - sappend('c',SET_F("CE"),countdownMode); - sappend('v',SET_F("CY"),countdownYear); - sappend('v',SET_F("CI"),countdownMonth); - sappend('v',SET_F("CD"),countdownDay); - sappend('v',SET_F("CH"),countdownHour); - sappend('v',SET_F("CM"),countdownMin); - sappend('v',SET_F("CS"),countdownSec); - - sappend('v',SET_F("A0"),macroAlexaOn); - sappend('v',SET_F("A1"),macroAlexaOff); - sappend('v',SET_F("MC"),macroCountdown); - sappend('v',SET_F("MN"),macroNl); + sappend(dest,'c',SET_F("OL"),overlayCurrent); + sappend(dest,'v',SET_F("O1"),overlayMin); + sappend(dest,'v',SET_F("O2"),overlayMax); + sappend(dest,'v',SET_F("OM"),analogClock12pixel); + sappend(dest,'c',SET_F("OS"),analogClockSecondsTrail); + sappend(dest,'c',SET_F("O5"),analogClock5MinuteMarks); + sappend(dest,'c',SET_F("OB"),analogClockSolidBlack); + + sappend(dest,'c',SET_F("CE"),countdownMode); + sappend(dest,'v',SET_F("CY"),countdownYear); + sappend(dest,'v',SET_F("CI"),countdownMonth); + sappend(dest,'v',SET_F("CD"),countdownDay); + sappend(dest,'v',SET_F("CH"),countdownHour); + sappend(dest,'v',SET_F("CM"),countdownMin); + sappend(dest,'v',SET_F("CS"),countdownSec); + + sappend(dest,'v',SET_F("A0"),macroAlexaOn); + sappend(dest,'v',SET_F("A1"),macroAlexaOff); + sappend(dest,'v',SET_F("MC"),macroCountdown); + sappend(dest,'v',SET_F("MN"),macroNl); for (unsigned i=0; i> 4) & 0x0F); - k[0] = 'P'; sappend('v',k,timerMonth[i] & 0x0F); - k[0] = 'D'; sappend('v',k,timerDay[i]); - k[0] = 'E'; sappend('v',k,timerDayEnd[i]); + k[0] = 'M'; sappend(dest,'v',k,(timerMonth[i] >> 4) & 0x0F); + k[0] = 'P'; sappend(dest,'v',k,timerMonth[i] & 0x0F); + k[0] = 'D'; sappend(dest,'v',k,timerDay[i]); + k[0] = 'E'; sappend(dest,'v',k,timerDayEnd[i]); } } } @@ -647,121 +640,116 @@ void getSettingsJS(byte subPage, char* dest) char fpass[l+1]; //fill PIN field with 0000 fpass[l] = 0; memset(fpass,'0',l); - sappends('s',SET_F("PIN"),fpass); - sappend('c',SET_F("NO"),otaLock); - sappend('c',SET_F("OW"),wifiLock); - sappend('c',SET_F("AO"),aOtaEnabled); - sappends('m',SET_F("(\"sip\")[0]"),(char*)F("WLED ")); - olen -= 2; //delete "; - oappend(versionString); - oappend(SET_F(" (build ")); - oappendi(VERSION); - oappend(SET_F(")\";")); - oappend(SET_F("sd=\"")); - oappend(serverDescription); - oappend(SET_F("\";")); + sappends(dest,'s',SET_F("PIN"),fpass); + sappend(dest,'c',SET_F("NO"),otaLock); + sappend(dest,'c',SET_F("OW"),wifiLock); + sappend(dest,'c',SET_F("AO"),aOtaEnabled); + char tmp_buf[128]; + snprintf_P(tmp_buf,sizeof(tmp_buf),PSTR("WLED %s (build %d)"),versionString,VERSION); + sappends(dest,'m',SET_F("(\"sip\")[0]"),tmp_buf); + dest.print(F("sd=\"")); + dest.print(serverDescription); + dest.print(F("\";")); } #ifdef WLED_ENABLE_DMX // include only if DMX is enabled if (subPage == SUBPAGE_DMX) { - sappend('v',SET_F("PU"),e131ProxyUniverse); - - sappend('v',SET_F("CN"),DMXChannels); - sappend('v',SET_F("CG"),DMXGap); - sappend('v',SET_F("CS"),DMXStart); - sappend('v',SET_F("SL"),DMXStartLED); - - sappend('i',SET_F("CH1"),DMXFixtureMap[0]); - sappend('i',SET_F("CH2"),DMXFixtureMap[1]); - sappend('i',SET_F("CH3"),DMXFixtureMap[2]); - sappend('i',SET_F("CH4"),DMXFixtureMap[3]); - sappend('i',SET_F("CH5"),DMXFixtureMap[4]); - sappend('i',SET_F("CH6"),DMXFixtureMap[5]); - sappend('i',SET_F("CH7"),DMXFixtureMap[6]); - sappend('i',SET_F("CH8"),DMXFixtureMap[7]); - sappend('i',SET_F("CH9"),DMXFixtureMap[8]); - sappend('i',SET_F("CH10"),DMXFixtureMap[9]); - sappend('i',SET_F("CH11"),DMXFixtureMap[10]); - sappend('i',SET_F("CH12"),DMXFixtureMap[11]); - sappend('i',SET_F("CH13"),DMXFixtureMap[12]); - sappend('i',SET_F("CH14"),DMXFixtureMap[13]); - sappend('i',SET_F("CH15"),DMXFixtureMap[14]); + sappend(dest,'v',SET_F("PU"),e131ProxyUniverse); + + sappend(dest,'v',SET_F("CN"),DMXChannels); + sappend(dest,'v',SET_F("CG"),DMXGap); + sappend(dest,'v',SET_F("CS"),DMXStart); + sappend(dest,'v',SET_F("SL"),DMXStartLED); + + sappend(dest,'i',SET_F("CH1"),DMXFixtureMap[0]); + sappend(dest,'i',SET_F("CH2"),DMXFixtureMap[1]); + sappend(dest,'i',SET_F("CH3"),DMXFixtureMap[2]); + sappend(dest,'i',SET_F("CH4"),DMXFixtureMap[3]); + sappend(dest,'i',SET_F("CH5"),DMXFixtureMap[4]); + sappend(dest,'i',SET_F("CH6"),DMXFixtureMap[5]); + sappend(dest,'i',SET_F("CH7"),DMXFixtureMap[6]); + sappend(dest,'i',SET_F("CH8"),DMXFixtureMap[7]); + sappend(dest,'i',SET_F("CH9"),DMXFixtureMap[8]); + sappend(dest,'i',SET_F("CH10"),DMXFixtureMap[9]); + sappend(dest,'i',SET_F("CH11"),DMXFixtureMap[10]); + sappend(dest,'i',SET_F("CH12"),DMXFixtureMap[11]); + sappend(dest,'i',SET_F("CH13"),DMXFixtureMap[12]); + sappend(dest,'i',SET_F("CH14"),DMXFixtureMap[13]); + sappend(dest,'i',SET_F("CH15"),DMXFixtureMap[14]); } #endif if (subPage == SUBPAGE_UM) //usermods { - appendGPIOinfo(); - oappend(SET_F("numM=")); - oappendi(usermods.getModCount()); - oappend(";"); - sappend('v',SET_F("SDA"),i2c_sda); - sappend('v',SET_F("SCL"),i2c_scl); - sappend('v',SET_F("MOSI"),spi_mosi); - sappend('v',SET_F("MISO"),spi_miso); - sappend('v',SET_F("SCLK"),spi_sclk); - oappend(SET_F("addInfo('SDA','")); oappendi(HW_PIN_SDA); oappend(SET_F("');")); - oappend(SET_F("addInfo('SCL','")); oappendi(HW_PIN_SCL); oappend(SET_F("');")); - oappend(SET_F("addInfo('MOSI','")); oappendi(HW_PIN_DATASPI); oappend(SET_F("');")); - oappend(SET_F("addInfo('MISO','")); oappendi(HW_PIN_MISOSPI); oappend(SET_F("');")); - oappend(SET_F("addInfo('SCLK','")); oappendi(HW_PIN_CLOCKSPI); oappend(SET_F("');")); - usermods.appendConfigData(); + appendGPIOinfo(dest); + dest.print(F("numM=")); + dest.print(usermods.getModCount()); + dest.print(";"); + sappend(dest,'v',SET_F("SDA"),i2c_sda); + sappend(dest,'v',SET_F("SCL"),i2c_scl); + sappend(dest,'v',SET_F("MOSI"),spi_mosi); + sappend(dest,'v',SET_F("MISO"),spi_miso); + sappend(dest,'v',SET_F("SCLK"),spi_sclk); + dest.print(F("addInfo('SDA','")); dest.print(HW_PIN_SDA); dest.print(F("');")); + dest.print(F("addInfo('SCL','")); dest.print(HW_PIN_SCL); dest.print(F("');")); + dest.print(F("addInfo('MOSI','")); dest.print(HW_PIN_DATASPI); dest.print(F("');")); + dest.print(F("addInfo('MISO','")); dest.print(HW_PIN_MISOSPI); dest.print(F("');")); + dest.print(F("addInfo('SCLK','")); dest.print(HW_PIN_CLOCKSPI); dest.print(F("');")); + usermods.appendConfigData(dest); } if (subPage == SUBPAGE_UPDATE) // update { - sappends('m',SET_F("(\"sip\")[0]"),(char*)F("WLED ")); - olen -= 2; //delete "; - oappend(versionString); - oappend(SET_F("
")); - oappend(releaseString); - oappend(SET_F("
(")); + char tmp_buf[128]; + snprintf_P(tmp_buf,sizeof(tmp_buf),PSTR("WLED %s
%s
(%s build %d)"), + versionString, + releaseString, #if defined(ARDUINO_ARCH_ESP32) - oappend(ESP.getChipModel()); + ESP.getChipModel(), #else - oappend("esp8266"); + F("esp8266"), #endif - oappend(SET_F(" build ")); - oappendi(VERSION); - oappend(SET_F(")\";")); + VERSION); + + sappends(dest,'m',SET_F("(\"sip\")[0]"),tmp_buf); } if (subPage == SUBPAGE_2D) // 2D matrices { - sappend('v',SET_F("SOMP"),strip.isMatrix); + sappend(dest,'v',SET_F("SOMP"),strip.isMatrix); #ifndef WLED_DISABLE_2D - oappend(SET_F("maxPanels=")); oappendi(WLED_MAX_PANELS); oappend(SET_F(";")); - oappend(SET_F("resetPanels();")); + dest.print(F("maxPanels=")); dest.print(WLED_MAX_PANELS); dest.print(F(";")); + dest.print(F("resetPanels();")); if (strip.isMatrix) { if(strip.panels>0){ - sappend('v',SET_F("PW"),strip.panel[0].width); //Set generator Width and Height to first panel size for convenience - sappend('v',SET_F("PH"),strip.panel[0].height); + sappend(dest,'v',SET_F("PW"),strip.panel[0].width); //Set generator Width and Height to first panel size for convenience + sappend(dest,'v',SET_F("PH"),strip.panel[0].height); } - sappend('v',SET_F("MPC"),strip.panels); + sappend(dest,'v',SET_F("MPC"),strip.panels); // panels for (unsigned i=0; i Date: Tue, 17 Sep 2024 18:26:46 -0400 Subject: [PATCH 081/145] Usermod: Implement shim for oappend Use a static Print* to transform old oappend calls to print calls. --- wled00/fcn_declare.h | 12 +++++++++++- wled00/um_manager.cpp | 11 +++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 6ce30facfc..8c90e2be18 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -302,7 +302,7 @@ class Usermod { virtual bool handleButton(uint8_t b) { return false; } // button overrides are possible here virtual bool getUMData(um_data_t **data) { if (data) *data = nullptr; return false; }; // usermod data exchange [see examples for audio effects] virtual void connected() {} // called when WiFi is (re)connected - virtual void appendConfigData(Print&) {} // helper function called from usermod settings page to add metadata for entry fields + virtual void appendConfigData(Print&); // helper function called from usermod settings page to add metadata for entry fields virtual void addToJsonState(JsonObject& obj) {} // add JSON objects for WLED state virtual void addToJsonInfo(JsonObject& obj) {} // add JSON objects for UI Info page virtual void readFromJsonState(JsonObject& obj) {} // process JSON messages received from web server @@ -314,6 +314,16 @@ class Usermod { virtual void onUpdateBegin(bool) {} // fired prior to and after unsuccessful firmware update virtual void onStateChange(uint8_t mode) {} // fired upon WLED state change virtual uint16_t getId() {return USERMOD_ID_UNSPECIFIED;} + + // API shims + private: + static Print* oappend_shim; + // old form of appendConfigData; called by default appendConfigData(Print&) with oappend_shim set up + // private so it is not accidentally invoked except via Usermod::appendConfigData(Print&) + virtual void appendConfigData() {} + protected: + // Shim for oappend(), which used to exist in utils.cpp + template static inline void oappend(const T& t) { oappend_shim->print(t); }; }; class UsermodManager { diff --git a/wled00/um_manager.cpp b/wled00/um_manager.cpp index 3970e7af40..5307d26f6b 100644 --- a/wled00/um_manager.cpp +++ b/wled00/um_manager.cpp @@ -68,3 +68,14 @@ bool UsermodManager::add(Usermod* um) ums[numMods++] = um; return true; } + + +/* Usermod v2 interface shim for oappend */ +Print* Usermod::oappend_shim = nullptr; + +void Usermod::appendConfigData(Print& p) { + assert(!oappend_shim); + oappend_shim = &p; + this->appendConfigData(); + oappend_shim = nullptr; +} From 4ef583c8445e74eea3f6c0bc5805563728351f17 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sat, 7 Sep 2024 19:52:30 -0400 Subject: [PATCH 082/145] xml: Print optimization Reduce the total number of calls by using printf_P and skipping atoi(). --- wled00/xml.cpp | 171 +++++++++++++++---------------------------------- 1 file changed, 50 insertions(+), 121 deletions(-) diff --git a/wled00/xml.cpp b/wled00/xml.cpp index 2d63d61f3f..5ed1109c9a 100644 --- a/wled00/xml.cpp +++ b/wled00/xml.cpp @@ -8,63 +8,22 @@ //build XML response to HTTP /win API request void XML_response(Print& dest) { - dest.print(F("")); - dest.print((nightlightActive && nightlightMode > NL_MODE_SET) ? briT : bri); - dest.print(F("")); - + dest.printf_P(PSTR("%d"), (nightlightActive && nightlightMode > NL_MODE_SET) ? briT : bri); for (int i = 0; i < 3; i++) { - dest.print(""); - dest.print(col[i]); - dest.print(""); + dest.printf_P(PSTR("%d"), col[i]); } for (int i = 0; i < 3; i++) { - dest.print(""); - dest.print(colSec[i]); - dest.print(""); - } - dest.print(F("")); - dest.print(notifyDirect); - dest.print(F("")); - dest.print(receiveGroups!=0); - dest.print(F("")); - dest.print(nightlightActive); - dest.print(F("")); - dest.print(nightlightMode > NL_MODE_SET); - dest.print(F("")); - dest.print(nightlightDelayMins); - dest.print(F("")); - dest.print(nightlightTargetBri); - dest.print(F("")); - dest.print(effectCurrent); - dest.print(F("")); - dest.print(effectSpeed); - dest.print(F("")); - dest.print(effectIntensity); - dest.print(F("")); - dest.print(effectPalette); - dest.print(F("")); - if (strip.hasWhiteChannel()) { - dest.print(col[3]); - } else { - dest.print("-1"); - } - dest.print(F("")); - dest.print(colSec[3]); - dest.print(F("")); - dest.print(currentPreset); - dest.print(F("")); - dest.print(currentPlaylist >= 0); - dest.print(F("")); - dest.print(serverDescription); - if (realtimeMode) - { - dest.print(F(" (live)")); + dest.printf_P(PSTR("%d"), colSec[i]); } - dest.print(F("")); - dest.print(strip.getFirstSelectedSegId()); - dest.print(F("")); + dest.printf_P(PSTR("%d%d%d%d%d%d%d%d%d%d%d%d%d%d%s%s%d"), + notifyDirect, receiveGroups!=0, nightlightActive, nightlightMode > NL_MODE_SET, nightlightDelayMins, + nightlightTargetBri, effectCurrent, effectSpeed, effectIntensity, effectPalette, + strip.hasWhiteChannel() ? col[3] : -1, colSec[3], currentPreset, currentPlaylist >= 0, + serverDescription, realtimeMode ? PSTR(" (live)") : "", + strip.getFirstSelectedSegId() + ); } static void extractPin(Print& dest, JsonObject &obj, const char *key) { @@ -114,17 +73,12 @@ void fillUMPins(Print& dest, JsonObject &mods) } void appendGPIOinfo(Print& dest) { - char nS[8]; - - // add usermod pins as d.um_p array dest.print(F("d.um_p=[-1")); // has to have 1 element if (i2c_sda > -1 && i2c_scl > -1) { - dest.print(","); dest.print(itoa(i2c_sda,nS,10)); - dest.print(","); dest.print(itoa(i2c_scl,nS,10)); + dest.printf_P(PSTR(",%d,%d"), i2c_sda, i2c_scl); } if (spi_mosi > -1 && spi_sclk > -1) { - dest.print(","); dest.print(itoa(spi_mosi,nS,10)); - dest.print(","); dest.print(itoa(spi_sclk,nS,10)); + dest.printf_P(PSTR(",%d,%d"), spi_mosi, spi_sclk); } // usermod pin reservations will become unnecessary when settings pages will read cfg.json directly if (requestJSONBufferLock(6)) { @@ -147,16 +101,16 @@ void appendGPIOinfo(Print& dest) { dest.print(SET_F("2,")); // DMX hardcoded pin #endif #if defined(WLED_DEBUG) && !defined(WLED_DEBUG_HOST) - dest.print(itoa(hardwareTX,nS,10)); dest.print(","); // debug output (TX) pin + dest.printf_P(PSTR(",%d"),hardwareTX); // debug output (TX) pin #endif //Note: Using pin 3 (RX) disables Adalight / Serial JSON #ifdef WLED_USE_ETHERNET if (ethernetType != WLED_ETH_NONE && ethernetType < WLED_NUM_ETH_TYPES) { - for (unsigned p=0; p=0) { dest.print(itoa(ethernetBoards[ethernetType].eth_power,nS,10)); dest.print(","); } - if (ethernetBoards[ethernetType].eth_mdc>=0) { dest.print(itoa(ethernetBoards[ethernetType].eth_mdc,nS,10)); dest.print(","); } - if (ethernetBoards[ethernetType].eth_mdio>=0) { dest.print(itoa(ethernetBoards[ethernetType].eth_mdio,nS,10)); dest.print(","); } - switch (ethernetBoards[ethernetType].eth_clk_mode) { + for (unsigned p=0; p=0) { dest.printf(",%d", ethernetBoards[ethernetType].eth_power); } + if (ethernetBoards[ethernetType].eth_mdc>=0) { dest.printf(",%d", ethernetBoards[ethernetType].eth_mdc); } + if (ethernetBoards[ethernetType].eth_mdio>=0) { dest.printf(",%d", ethernetBoards[ethernetType].eth_mdio); } + switch (ethernetBoards[ethernetType].eth_clk_mode) { case ETH_CLOCK_GPIO0_IN: case ETH_CLOCK_GPIO0_OUT: dest.print(SET_F("0")); @@ -211,27 +165,19 @@ void getSettingsJS(byte subPage, Print& dest) if (subPage == SUBPAGE_WIFI) { - char nS[10]; size_t l; - dest.print(F("resetWiFi(")); - dest.print(WLED_MAX_WIFI_COUNT); - dest.print(F(");")); + dest.printf_P(PSTR("resetWiFi(%d);"), WLED_MAX_WIFI_COUNT); for (size_t n = 0; n < multiWiFi.size(); n++) { l = strlen(multiWiFi[n].clientPass); char fpass[l+1]; //fill password field with *** fpass[l] = 0; memset(fpass,'*',l); - dest.print(F("addWiFi(\"")); - dest.print(multiWiFi[n].clientSSID); - dest.print(F("\",\"")); - dest.print(fpass); - dest.print(F("\",0x")); - dest.print(itoa(multiWiFi[n].staticIP,nS,16)); - dest.print(F(",0x")); - dest.print(itoa(multiWiFi[n].staticGW,nS,16)); - dest.print(F(",0x")); - dest.print(itoa(multiWiFi[n].staticSN,nS,16)); - dest.print(F(");")); + dest.printf_P(PSTR("addWiFi(\"%s\",\",%s\",0x%X,0x%X,0x%X);"), + multiWiFi[n].clientSSID, + fpass, + (uint32_t) multiWiFi[n].staticIP, // explicit cast required as this is a struct + (uint32_t) multiWiFi[n].staticGW, + (uint32_t) multiWiFi[n].staticSN); } sappend(dest,'v',SET_F("D0"),dnsAddress[0]); @@ -320,16 +266,16 @@ void getSettingsJS(byte subPage, Print& dest) dest.print(SET_F("d.ledTypes=")); dest.print(BusManager::getLEDTypesJSONString().c_str()); dest.print(";"); // set limits - dest.print(F("bLimits(")); - dest.print(itoa(WLED_MAX_BUSSES,nS,10)); dest.print(","); - dest.print(itoa(WLED_MIN_VIRTUAL_BUSSES,nS,10)); dest.print(","); - dest.print(itoa(MAX_LEDS_PER_BUS,nS,10)); dest.print(","); - dest.print(itoa(MAX_LED_MEMORY,nS,10)); dest.print(","); - dest.print(itoa(MAX_LEDS,nS,10)); dest.print(","); - dest.print(itoa(WLED_MAX_COLOR_ORDER_MAPPINGS,nS,10)); dest.print(","); - dest.print(itoa(WLED_MAX_DIGITAL_CHANNELS,nS,10)); dest.print(","); - dest.print(itoa(WLED_MAX_ANALOG_CHANNELS,nS,10)); - dest.print(F(");")); + dest.printf_P(PSTR("bLimits(%d,%d,%d,%d,%d,%d,%d,%d);"), + WLED_MAX_BUSSES, + WLED_MIN_VIRTUAL_BUSSES, + MAX_LEDS_PER_BUS, + MAX_LED_MEMORY, + MAX_LEDS, + WLED_MAX_COLOR_ORDER_MAPPINGS, + WLED_MAX_DIGITAL_CHANNELS, + WLED_MAX_ANALOG_CHANNELS + ); sappend(dest,'c',SET_F("MS"),strip.autoSegments); sappend(dest,'c',SET_F("CCT"),strip.correctWB); @@ -403,17 +349,12 @@ void getSettingsJS(byte subPage, Print& dest) sappend(dest,'c',SET_F("ABL"),BusManager::ablMilliampsMax() || sumMa > 0); sappend(dest,'c',SET_F("PPL"),!BusManager::ablMilliampsMax() && sumMa > 0); - dest.print(F("resetCOM(")); - dest.print(itoa(WLED_MAX_COLOR_ORDER_MAPPINGS,nS,10)); - dest.print(F(");")); + dest.printf_P(PSTR("resetCOM(%d);"), WLED_MAX_COLOR_ORDER_MAPPINGS); const ColorOrderMap& com = BusManager::getColorOrderMap(); for (int s = 0; s < com.count(); s++) { const ColorOrderMapEntry* entry = com.get(s); if (entry == nullptr) break; - dest.print(F("addCOM(")); - dest.print(itoa(entry->start,nS,10)); dest.print(","); - dest.print(itoa(entry->len,nS,10)); dest.print(","); - dest.print(itoa(entry->colorOrder,nS,10)); dest.print(");"); + dest.printf_P(PSTR("addCOM(%d,%d,%d);"), entry->start, entry->len, entry->colorOrder); } sappend(dest,'v',SET_F("CA"),briS); @@ -439,11 +380,7 @@ void getSettingsJS(byte subPage, Print& dest) sappend(dest,'c',SET_F("RM"),rlyMde); sappend(dest,'c',SET_F("RO"),rlyOpenDrain); for (int i = 0; i < WLED_MAX_BUTTONS; i++) { - dest.print(F("addBtn(")); - dest.print(itoa(i,nS,10)); dest.print(","); - dest.print(itoa(btnPin[i],nS,10)); dest.print(","); - dest.print(itoa(buttonType[i],nS,10)); - dest.print(F(");")); + dest.printf_P(PSTR("addBtn(%d,%d,%d);"), i, btnPin[i], buttonType[i]); } sappend(dest,'c',SET_F("IP"),disablePullUp); sappend(dest,'v',SET_F("TT"),touchThreshold); @@ -528,9 +465,8 @@ void getSettingsJS(byte subPage, Print& dest) sappends(dest,'s',SET_F("MG"),mqttGroupTopic); sappend(dest,'c',SET_F("BM"),buttonPublishMqtt); sappend(dest,'c',SET_F("RT"),retainMqttMsg); - dest.print(F("d.Sf.MD.maxlength=")); dest.print(itoa(MQTT_MAX_TOPIC_LEN,nS,10)); dest.print(F(";")); - dest.print(F("d.Sf.MG.maxlength=")); dest.print(itoa(MQTT_MAX_TOPIC_LEN,nS,10)); dest.print(F(";")); - dest.print(F("d.Sf.MS.maxlength=")); dest.print(itoa(MQTT_MAX_SERVER_LEN,nS,10)); dest.print(F(";")); + dest.printf_P(PSTR("d.Sf.MD.maxlength=%d;d.Sf.MG.maxlength=%d;d.Sf.MS.maxlength=%d;"), + MQTT_MAX_TOPIC_LEN, MQTT_MAX_TOPIC_LEN, MQTT_MAX_SERVER_LEN); #else dest.print(F("toggle('MQTT');")); // hide MQTT settings #endif @@ -608,12 +544,7 @@ void getSettingsJS(byte subPage, Print& dest) sappend(dest,'v',SET_F("MC"),macroCountdown); sappend(dest,'v',SET_F("MN"),macroNl); for (unsigned i=0; i Date: Mon, 9 Sep 2024 20:00:23 -0400 Subject: [PATCH 083/145] Replace sappend and sappends Use named functions to describe what's being printed. --- wled00/fcn_declare.h | 7 +- wled00/util.cpp | 49 ++--- wled00/xml.cpp | 440 +++++++++++++++++++++---------------------- 3 files changed, 242 insertions(+), 254 deletions(-) diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 8c90e2be18..a36f2dc264 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -372,8 +372,11 @@ void parseNumber(const char* str, byte* val, byte minv=0, byte maxv=255); bool getVal(JsonVariant elem, byte* val, byte minv=0, byte maxv=255); bool getBoolVal(JsonVariant elem, bool dflt); bool updateVal(const char* req, const char* key, byte* val, byte minv=0, byte maxv=255); -void sappend(Print& dest, char stype, const char* key, int val); -void sappends(Print& dest, char stype, const char* key, char* val); +size_t printSetCheckbox(Print& dest, const char* key, int val); +size_t printSetValue(Print& dest, const char* key, int val); +size_t printSetValue(Print& dest, const char* key, const char* val); +size_t printSetIndex(Print& dest, const char* key, int index); +size_t printSetMessage(Print& dest, const char* key, const char* val); void prepareHostname(char* hostname); bool isAsterisksOnly(const char* str, byte maxLen); bool requestJSONBufferLock(uint8_t module=255); diff --git a/wled00/util.cpp b/wled00/util.cpp index 00506ea978..660877d186 100644 --- a/wled00/util.cpp +++ b/wled00/util.cpp @@ -87,45 +87,30 @@ bool updateVal(const char* req, const char* key, byte* val, byte minv, byte maxv return true; } +static size_t printSetInt(Print& dest, const char* key, const char* selector, int value) { + return dest.printf_P(PSTR("d.Sf.%s.%s=%d;"), key, selector, value); +} -//append a numeric setting to string buffer -void sappend(Print& dest, char stype, const char* key, int val) -{ - const __FlashStringHelper* type_str; - switch(stype) - { - case 'c': //checkbox - type_str = F(".checked="); - break; - case 'v': //numeric - type_str = F(".value="); - break; - case 'i': //selectedIndex - type_str = F(".selectedIndex="); - break; - default: - return; //??? - } - - dest.printf_P(PSTR("d.Sf.%s%s%d;"), key, type_str, val); +size_t printSetCheckbox(Print& dest, const char* key, int val) { + return printSetInt(dest, key, PSTR("checked"), val); +} +size_t printSetValue(Print& dest, const char* key, int val) { + return printSetInt(dest, key, PSTR("value"), val); +} +size_t printSetIndex(Print& dest, const char* key, int index) { + return printSetInt(dest, key, PSTR("selectedIndex"), index); } +size_t printSetValue(Print& dest, const char* key, const char* val) { + return dest.printf_P(PSTR("d.Sf.%s.value=\"%s\";"),key,val); +} -//append a string setting to buffer -void sappends(Print& dest, char stype, const char* key, char* val) -{ - switch(stype) - { - case 's': {//string (we can interpret val as char*) - dest.printf_P(PSTR("d.Sf.%s.value=\"%s\";"),key,val); - break;} - case 'm': //message - dest.printf_P(PSTR("d.getElementsByClassName%s.innerHTML=\"%s\";"), key, val); - break; - } +size_t printSetMessage(Print& dest, const char* key, const char* val) { + return dest.printf_P(PSTR("d.getElementsByClassName%s.innerHTML=\"%s\";"), key, val); } + void prepareHostname(char* hostname) { sprintf_P(hostname, PSTR("wled-%*s"), 6, escapedMac.c_str() + 6); diff --git a/wled00/xml.cpp b/wled00/xml.cpp index 5ed1109c9a..e8858066c1 100644 --- a/wled00/xml.cpp +++ b/wled00/xml.cpp @@ -180,41 +180,41 @@ void getSettingsJS(byte subPage, Print& dest) (uint32_t) multiWiFi[n].staticSN); } - sappend(dest,'v',SET_F("D0"),dnsAddress[0]); - sappend(dest,'v',SET_F("D1"),dnsAddress[1]); - sappend(dest,'v',SET_F("D2"),dnsAddress[2]); - sappend(dest,'v',SET_F("D3"),dnsAddress[3]); + printSetValue(dest,PSTR("D0"),dnsAddress[0]); + printSetValue(dest,PSTR("D1"),dnsAddress[1]); + printSetValue(dest,PSTR("D2"),dnsAddress[2]); + printSetValue(dest,PSTR("D3"),dnsAddress[3]); - sappends(dest,'s',SET_F("CM"),cmDNS); - sappend(dest,'i',SET_F("AB"),apBehavior); - sappends(dest,'s',SET_F("AS"),apSSID); - sappend(dest,'c',SET_F("AH"),apHide); + printSetValue(dest,PSTR("CM"),cmDNS); + printSetIndex(dest,PSTR("AB"),apBehavior); + printSetValue(dest,PSTR("AS"),apSSID); + printSetCheckbox(dest,PSTR("AH"),apHide); l = strlen(apPass); char fapass[l+1]; //fill password field with *** fapass[l] = 0; memset(fapass,'*',l); - sappends(dest,'s',SET_F("AP"),fapass); + printSetValue(dest,PSTR("AP"),fapass); - sappend(dest,'v',SET_F("AC"),apChannel); + printSetValue(dest,PSTR("AC"),apChannel); #ifdef ARDUINO_ARCH_ESP32 - sappend(dest,'v',SET_F("TX"),txPower); + printSetValue(dest,PSTR("TX"),txPower); #else dest.print(F("gId('tx').style.display='none';")); #endif - sappend(dest,'c',SET_F("FG"),force802_3g); - sappend(dest,'c',SET_F("WS"),noWifiSleep); + printSetCheckbox(dest,PSTR("FG"),force802_3g); + printSetCheckbox(dest,PSTR("WS"),noWifiSleep); #ifndef WLED_DISABLE_ESPNOW - sappend(dest,'c',SET_F("RE"),enableESPNow); - sappends(dest,'s',SET_F("RMAC"),linked_remote); + printSetCheckbox(dest,PSTR("RE"),enableESPNow); + printSetValue(dest,PSTR("RMAC"),linked_remote); #else //hide remote settings if not compiled dest.print(F("toggle('ESPNOW');")); // hide ESP-NOW setting #endif #ifdef WLED_USE_ETHERNET - sappend(dest,'v',SET_F("ETH"),ethernetType); + printSetValue(dest,PSTR("ETH"),ethernetType); #else //hide ethernet setting if not compiled in dest.print(F("gId('ethd').style.display='none';")); @@ -229,10 +229,10 @@ void getSettingsJS(byte subPage, Print& dest) #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) if (Network.isEthernet()) strcat_P(s ,SET_F(" (Ethernet)")); #endif - sappends(dest,'m',SET_F("(\"sip\")[0]"),s); + printSetMessage(dest,PSTR("(\"sip\")[0]"),s); } else { - sappends(dest,'m',SET_F("(\"sip\")[0]"),(char*)F("Not connected")); + printSetMessage(dest,PSTR("(\"sip\")[0]"),(char*)F("Not connected")); } if (WiFi.softAPIP()[0] != 0) //is active @@ -240,19 +240,19 @@ void getSettingsJS(byte subPage, Print& dest) char s[16]; IPAddress apIP = WiFi.softAPIP(); sprintf(s, "%d.%d.%d.%d", apIP[0], apIP[1], apIP[2], apIP[3]); - sappends(dest,'m',SET_F("(\"sip\")[1]"),s); + printSetMessage(dest,PSTR("(\"sip\")[1]"),s); } else { - sappends(dest,'m',SET_F("(\"sip\")[1]"),(char*)F("Not active")); + printSetMessage(dest,PSTR("(\"sip\")[1]"),(char*)F("Not active")); } #ifndef WLED_DISABLE_ESPNOW if (strlen(last_signal_src) > 0) { //Have seen an ESP-NOW Remote - sappends(dest,'m',SET_F("(\"rlid\")[0]"),last_signal_src); + printSetMessage(dest,PSTR("(\"rlid\")[0]"),last_signal_src); } else if (!enableESPNow) { - sappends(dest,'m',SET_F("(\"rlid\")[0]"),(char*)F("(Enable ESP-NOW to listen)")); + printSetMessage(dest,PSTR("(\"rlid\")[0]"),(char*)F("(Enable ESP-NOW to listen)")); } else { - sappends(dest,'m',SET_F("(\"rlid\")[0]"),(char*)F("None")); + printSetMessage(dest,PSTR("(\"rlid\")[0]"),(char*)F("None")); } #endif } @@ -277,14 +277,14 @@ void getSettingsJS(byte subPage, Print& dest) WLED_MAX_ANALOG_CHANNELS ); - sappend(dest,'c',SET_F("MS"),strip.autoSegments); - sappend(dest,'c',SET_F("CCT"),strip.correctWB); - sappend(dest,'c',SET_F("IC"),cctICused); - sappend(dest,'c',SET_F("CR"),strip.cctFromRgb); - sappend(dest,'v',SET_F("CB"),strip.cctBlending); - sappend(dest,'v',SET_F("FR"),strip.getTargetFps()); - sappend(dest,'v',SET_F("AW"),Bus::getGlobalAWMode()); - sappend(dest,'c',SET_F("LD"),useGlobalLedBuffer); + printSetCheckbox(dest,PSTR("MS"),strip.autoSegments); + printSetCheckbox(dest,PSTR("CCT"),strip.correctWB); + printSetCheckbox(dest,PSTR("IC"),cctICused); + printSetCheckbox(dest,PSTR("CR"),strip.cctFromRgb); + printSetValue(dest,PSTR("CB"),strip.cctBlending); + printSetValue(dest,PSTR("FR"),strip.getTargetFps()); + printSetValue(dest,PSTR("AW"),Bus::getGlobalAWMode()); + printSetCheckbox(dest,PSTR("LD"),useGlobalLedBuffer); unsigned sumMa = 0; for (int s = 0; s < BusManager::getNumBusses(); s++) { @@ -309,17 +309,17 @@ void getSettingsJS(byte subPage, Print& dest) int nPins = bus->getPins(pins); for (int i = 0; i < nPins; i++) { lp[1] = offset+i; - if (pinManager.isPinOk(pins[i]) || bus->isVirtual()) sappend(dest,'v',lp,pins[i]); + if (pinManager.isPinOk(pins[i]) || bus->isVirtual()) printSetValue(dest,lp,pins[i]); } - sappend(dest,'v',lc,bus->getLength()); - sappend(dest,'v',lt,bus->getType()); - sappend(dest,'v',co,bus->getColorOrder() & 0x0F); - sappend(dest,'v',ls,bus->getStart()); - sappend(dest,'c',cv,bus->isReversed()); - sappend(dest,'v',sl,bus->skippedLeds()); - sappend(dest,'c',rf,bus->isOffRefreshRequired()); - sappend(dest,'v',aw,bus->getAutoWhiteMode()); - sappend(dest,'v',wo,bus->getColorOrder() >> 4); + printSetValue(dest,lc,bus->getLength()); + printSetValue(dest,lt,bus->getType()); + printSetValue(dest,co,bus->getColorOrder() & 0x0F); + printSetValue(dest,ls,bus->getStart()); + printSetCheckbox(dest,cv,bus->isReversed()); + printSetValue(dest,sl,bus->skippedLeds()); + printSetCheckbox(dest,rf,bus->isOffRefreshRequired()); + printSetValue(dest,aw,bus->getAutoWhiteMode()); + printSetValue(dest,wo,bus->getColorOrder() >> 4); unsigned speed = bus->getFrequency(); if (bus->isPWM()) { switch (speed) { @@ -340,14 +340,14 @@ void getSettingsJS(byte subPage, Print& dest) case 20000 : speed = 4; break; } } - sappend(dest,'v',sp,speed); - sappend(dest,'v',la,bus->getLEDCurrent()); - sappend(dest,'v',ma,bus->getMaxCurrent()); + printSetValue(dest,sp,speed); + printSetValue(dest,la,bus->getLEDCurrent()); + printSetValue(dest,ma,bus->getMaxCurrent()); sumMa += bus->getMaxCurrent(); } - sappend(dest,'v',SET_F("MA"),BusManager::ablMilliampsMax() ? BusManager::ablMilliampsMax() : sumMa); - sappend(dest,'c',SET_F("ABL"),BusManager::ablMilliampsMax() || sumMa > 0); - sappend(dest,'c',SET_F("PPL"),!BusManager::ablMilliampsMax() && sumMa > 0); + printSetValue(dest,PSTR("MA"),BusManager::ablMilliampsMax() ? BusManager::ablMilliampsMax() : sumMa); + printSetCheckbox(dest,PSTR("ABL"),BusManager::ablMilliampsMax() || sumMa > 0); + printSetCheckbox(dest,PSTR("PPL"),!BusManager::ablMilliampsMax() && sumMa > 0); dest.printf_P(PSTR("resetCOM(%d);"), WLED_MAX_COLOR_ORDER_MAPPINGS); const ColorOrderMap& com = BusManager::getColorOrderMap(); @@ -357,114 +357,114 @@ void getSettingsJS(byte subPage, Print& dest) dest.printf_P(PSTR("addCOM(%d,%d,%d);"), entry->start, entry->len, entry->colorOrder); } - sappend(dest,'v',SET_F("CA"),briS); - - sappend(dest,'c',SET_F("BO"),turnOnAtBoot); - sappend(dest,'v',SET_F("BP"),bootPreset); - - sappend(dest,'c',SET_F("GB"),gammaCorrectBri); - sappend(dest,'c',SET_F("GC"),gammaCorrectCol); - dtostrf(gammaCorrectVal,3,1,nS); sappends(dest,'s',SET_F("GV"),nS); - sappend(dest,'c',SET_F("TF"),fadeTransition); - sappend(dest,'c',SET_F("EB"),modeBlending); - sappend(dest,'v',SET_F("TD"),transitionDelayDefault); - sappend(dest,'c',SET_F("PF"),strip.paletteFade); - sappend(dest,'v',SET_F("TP"),randomPaletteChangeTime); - sappend(dest,'c',SET_F("TH"),useHarmonicRandomPalette); - sappend(dest,'v',SET_F("BF"),briMultiplier); - sappend(dest,'v',SET_F("TB"),nightlightTargetBri); - sappend(dest,'v',SET_F("TL"),nightlightDelayMinsDefault); - sappend(dest,'v',SET_F("TW"),nightlightMode); - sappend(dest,'i',SET_F("PB"),strip.paletteBlend); - sappend(dest,'v',SET_F("RL"),rlyPin); - sappend(dest,'c',SET_F("RM"),rlyMde); - sappend(dest,'c',SET_F("RO"),rlyOpenDrain); + printSetValue(dest,PSTR("CA"),briS); + + printSetCheckbox(dest,PSTR("BO"),turnOnAtBoot); + printSetValue(dest,PSTR("BP"),bootPreset); + + printSetCheckbox(dest,PSTR("GB"),gammaCorrectBri); + printSetCheckbox(dest,PSTR("GC"),gammaCorrectCol); + dtostrf(gammaCorrectVal,3,1,nS); printSetValue(dest,PSTR("GV"),nS); + printSetCheckbox(dest,PSTR("TF"),fadeTransition); + printSetCheckbox(dest,PSTR("EB"),modeBlending); + printSetValue(dest,PSTR("TD"),transitionDelayDefault); + printSetCheckbox(dest,PSTR("PF"),strip.paletteFade); + printSetValue(dest,PSTR("TP"),randomPaletteChangeTime); + printSetCheckbox(dest,PSTR("TH"),useHarmonicRandomPalette); + printSetValue(dest,PSTR("BF"),briMultiplier); + printSetValue(dest,PSTR("TB"),nightlightTargetBri); + printSetValue(dest,PSTR("TL"),nightlightDelayMinsDefault); + printSetValue(dest,PSTR("TW"),nightlightMode); + printSetIndex(dest,PSTR("PB"),strip.paletteBlend); + printSetValue(dest,PSTR("RL"),rlyPin); + printSetCheckbox(dest,PSTR("RM"),rlyMde); + printSetCheckbox(dest,PSTR("RO"),rlyOpenDrain); for (int i = 0; i < WLED_MAX_BUTTONS; i++) { dest.printf_P(PSTR("addBtn(%d,%d,%d);"), i, btnPin[i], buttonType[i]); } - sappend(dest,'c',SET_F("IP"),disablePullUp); - sappend(dest,'v',SET_F("TT"),touchThreshold); + printSetCheckbox(dest,PSTR("IP"),disablePullUp); + printSetValue(dest,PSTR("TT"),touchThreshold); #ifndef WLED_DISABLE_INFRARED - sappend(dest,'v',SET_F("IR"),irPin); - sappend(dest,'v',SET_F("IT"),irEnabled); + printSetValue(dest,PSTR("IR"),irPin); + printSetValue(dest,PSTR("IT"),irEnabled); #endif - sappend(dest,'c',SET_F("MSO"),!irApplyToAllSelected); + printSetCheckbox(dest,PSTR("MSO"),!irApplyToAllSelected); } if (subPage == SUBPAGE_UI) { - sappends(dest,'s',SET_F("DS"),serverDescription); - sappend(dest,'c',SET_F("SU"),simplifiedUI); + printSetValue(dest,PSTR("DS"),serverDescription); + printSetCheckbox(dest,PSTR("SU"),simplifiedUI); } if (subPage == SUBPAGE_SYNC) { [[maybe_unused]] char nS[32]; - sappend(dest,'v',SET_F("UP"),udpPort); - sappend(dest,'v',SET_F("U2"),udpPort2); + printSetValue(dest,PSTR("UP"),udpPort); + printSetValue(dest,PSTR("U2"),udpPort2); #ifndef WLED_DISABLE_ESPNOW - if (enableESPNow) sappend(dest,'c',SET_F("EN"),useESPNowSync); + if (enableESPNow) printSetCheckbox(dest,PSTR("EN"),useESPNowSync); else dest.print(F("toggle('ESPNOW');")); // hide ESP-NOW setting #else dest.print(F("toggle('ESPNOW');")); // hide ESP-NOW setting #endif - sappend(dest,'v',SET_F("GS"),syncGroups); - sappend(dest,'v',SET_F("GR"),receiveGroups); - - sappend(dest,'c',SET_F("RB"),receiveNotificationBrightness); - sappend(dest,'c',SET_F("RC"),receiveNotificationColor); - sappend(dest,'c',SET_F("RX"),receiveNotificationEffects); - sappend(dest,'c',SET_F("RP"),receiveNotificationPalette); - sappend(dest,'c',SET_F("SO"),receiveSegmentOptions); - sappend(dest,'c',SET_F("SG"),receiveSegmentBounds); - sappend(dest,'c',SET_F("SS"),sendNotifications); - sappend(dest,'c',SET_F("SD"),notifyDirect); - sappend(dest,'c',SET_F("SB"),notifyButton); - sappend(dest,'c',SET_F("SH"),notifyHue); - sappend(dest,'v',SET_F("UR"),udpNumRetries); - - sappend(dest,'c',SET_F("NL"),nodeListEnabled); - sappend(dest,'c',SET_F("NB"),nodeBroadcastEnabled); - - sappend(dest,'c',SET_F("RD"),receiveDirect); - sappend(dest,'c',SET_F("MO"),useMainSegmentOnly); - sappend(dest,'c',SET_F("RLM"),realtimeRespectLedMaps); - sappend(dest,'v',SET_F("EP"),e131Port); - sappend(dest,'c',SET_F("ES"),e131SkipOutOfSequence); - sappend(dest,'c',SET_F("EM"),e131Multicast); - sappend(dest,'v',SET_F("EU"),e131Universe); - sappend(dest,'v',SET_F("DA"),DMXAddress); - sappend(dest,'v',SET_F("XX"),DMXSegmentSpacing); - sappend(dest,'v',SET_F("PY"),e131Priority); - sappend(dest,'v',SET_F("DM"),DMXMode); - sappend(dest,'v',SET_F("ET"),realtimeTimeoutMs); - sappend(dest,'c',SET_F("FB"),arlsForceMaxBri); - sappend(dest,'c',SET_F("RG"),arlsDisableGammaCorrection); - sappend(dest,'v',SET_F("WO"),arlsOffset); + printSetValue(dest,PSTR("GS"),syncGroups); + printSetValue(dest,PSTR("GR"),receiveGroups); + + printSetCheckbox(dest,PSTR("RB"),receiveNotificationBrightness); + printSetCheckbox(dest,PSTR("RC"),receiveNotificationColor); + printSetCheckbox(dest,PSTR("RX"),receiveNotificationEffects); + printSetCheckbox(dest,PSTR("RP"),receiveNotificationPalette); + printSetCheckbox(dest,PSTR("SO"),receiveSegmentOptions); + printSetCheckbox(dest,PSTR("SG"),receiveSegmentBounds); + printSetCheckbox(dest,PSTR("SS"),sendNotifications); + printSetCheckbox(dest,PSTR("SD"),notifyDirect); + printSetCheckbox(dest,PSTR("SB"),notifyButton); + printSetCheckbox(dest,PSTR("SH"),notifyHue); + printSetValue(dest,PSTR("UR"),udpNumRetries); + + printSetCheckbox(dest,PSTR("NL"),nodeListEnabled); + printSetCheckbox(dest,PSTR("NB"),nodeBroadcastEnabled); + + printSetCheckbox(dest,PSTR("RD"),receiveDirect); + printSetCheckbox(dest,PSTR("MO"),useMainSegmentOnly); + printSetCheckbox(dest,PSTR("RLM"),realtimeRespectLedMaps); + printSetValue(dest,PSTR("EP"),e131Port); + printSetCheckbox(dest,PSTR("ES"),e131SkipOutOfSequence); + printSetCheckbox(dest,PSTR("EM"),e131Multicast); + printSetValue(dest,PSTR("EU"),e131Universe); + printSetValue(dest,PSTR("DA"),DMXAddress); + printSetValue(dest,PSTR("XX"),DMXSegmentSpacing); + printSetValue(dest,PSTR("PY"),e131Priority); + printSetValue(dest,PSTR("DM"),DMXMode); + printSetValue(dest,PSTR("ET"),realtimeTimeoutMs); + printSetCheckbox(dest,PSTR("FB"),arlsForceMaxBri); + printSetCheckbox(dest,PSTR("RG"),arlsDisableGammaCorrection); + printSetValue(dest,PSTR("WO"),arlsOffset); #ifndef WLED_DISABLE_ALEXA - sappend(dest,'c',SET_F("AL"),alexaEnabled); - sappends(dest,'s',SET_F("AI"),alexaInvocationName); - sappend(dest,'c',SET_F("SA"),notifyAlexa); - sappend(dest,'v',SET_F("AP"),alexaNumPresets); + printSetCheckbox(dest,PSTR("AL"),alexaEnabled); + printSetValue(dest,PSTR("AI"),alexaInvocationName); + printSetCheckbox(dest,PSTR("SA"),notifyAlexa); + printSetValue(dest,PSTR("AP"),alexaNumPresets); #else dest.print(F("toggle('Alexa');")); // hide Alexa settings #endif #ifndef WLED_DISABLE_MQTT - sappend(dest,'c',SET_F("MQ"),mqttEnabled); - sappends(dest,'s',SET_F("MS"),mqttServer); - sappend(dest,'v',SET_F("MQPORT"),mqttPort); - sappends(dest,'s',SET_F("MQUSER"),mqttUser); + printSetCheckbox(dest,PSTR("MQ"),mqttEnabled); + printSetValue(dest,PSTR("MS"),mqttServer); + printSetValue(dest,PSTR("MQPORT"),mqttPort); + printSetValue(dest,PSTR("MQUSER"),mqttUser); byte l = strlen(mqttPass); char fpass[l+1]; //fill password field with *** fpass[l] = 0; memset(fpass,'*',l); - sappends(dest,'s',SET_F("MQPASS"),fpass); - sappends(dest,'s',SET_F("MQCID"),mqttClientID); - sappends(dest,'s',"MD",mqttDeviceTopic); - sappends(dest,'s',SET_F("MG"),mqttGroupTopic); - sappend(dest,'c',SET_F("BM"),buttonPublishMqtt); - sappend(dest,'c',SET_F("RT"),retainMqttMsg); + printSetValue(dest,PSTR("MQPASS"),fpass); + printSetValue(dest,PSTR("MQCID"),mqttClientID); + printSetValue(dest,PSTR("MD"),mqttDeviceTopic); + printSetValue(dest,PSTR("MG"),mqttGroupTopic); + printSetCheckbox(dest,PSTR("BM"),buttonPublishMqtt); + printSetCheckbox(dest,PSTR("RT"),retainMqttMsg); dest.printf_P(PSTR("d.Sf.MD.maxlength=%d;d.Sf.MG.maxlength=%d;d.Sf.MS.maxlength=%d;"), MQTT_MAX_TOPIC_LEN, MQTT_MAX_TOPIC_LEN, MQTT_MAX_SERVER_LEN); #else @@ -472,16 +472,16 @@ void getSettingsJS(byte subPage, Print& dest) #endif #ifndef WLED_DISABLE_HUESYNC - sappend(dest,'v',SET_F("H0"),hueIP[0]); - sappend(dest,'v',SET_F("H1"),hueIP[1]); - sappend(dest,'v',SET_F("H2"),hueIP[2]); - sappend(dest,'v',SET_F("H3"),hueIP[3]); - sappend(dest,'v',SET_F("HL"),huePollLightId); - sappend(dest,'v',SET_F("HI"),huePollIntervalMs); - sappend(dest,'c',SET_F("HP"),huePollingEnabled); - sappend(dest,'c',SET_F("HO"),hueApplyOnOff); - sappend(dest,'c',SET_F("HB"),hueApplyBri); - sappend(dest,'c',SET_F("HC"),hueApplyColor); + printSetValue(dest,PSTR("H0"),hueIP[0]); + printSetValue(dest,PSTR("H1"),hueIP[1]); + printSetValue(dest,PSTR("H2"),hueIP[2]); + printSetValue(dest,PSTR("H3"),hueIP[3]); + printSetValue(dest,PSTR("HL"),huePollLightId); + printSetValue(dest,PSTR("HI"),huePollIntervalMs); + printSetCheckbox(dest,PSTR("HP"),huePollingEnabled); + printSetCheckbox(dest,PSTR("HO"),hueApplyOnOff); + printSetCheckbox(dest,PSTR("HB"),hueApplyBri); + printSetCheckbox(dest,PSTR("HC"),hueApplyColor); char hueErrorString[25]; switch (hueError) { @@ -495,11 +495,11 @@ void getSettingsJS(byte subPage, Print& dest) default: sprintf_P(hueErrorString,PSTR("Bridge Error %i"),hueError); } - sappends(dest,'m',SET_F("(\"sip\")[0]"),hueErrorString); + printSetMessage(dest,PSTR("(\"sip\")[0]"),hueErrorString); #else dest.print(F("toggle('Hue');")); // hide Hue Sync settings #endif - sappend(dest,'v',SET_F("BD"),serialBaud); + printSetValue(dest,PSTR("BD"),serialBaud); #ifndef WLED_ENABLE_ADALIGHT dest.print(SET_F("toggle('Serial);")); #endif @@ -507,42 +507,42 @@ void getSettingsJS(byte subPage, Print& dest) if (subPage == SUBPAGE_TIME) { - sappend(dest,'c',SET_F("NT"),ntpEnabled); - sappends(dest,'s',SET_F("NS"),ntpServerName); - sappend(dest,'c',SET_F("CF"),!useAMPM); - sappend(dest,'i',SET_F("TZ"),currentTimezone); - sappend(dest,'v',SET_F("UO"),utcOffsetSecs); + printSetCheckbox(dest,PSTR("NT"),ntpEnabled); + printSetValue(dest,PSTR("NS"),ntpServerName); + printSetCheckbox(dest,PSTR("CF"),!useAMPM); + printSetIndex(dest,PSTR("TZ"),currentTimezone); + printSetValue(dest,PSTR("UO"),utcOffsetSecs); char tm[32]; dtostrf(longitude,4,2,tm); - sappends(dest,'s',SET_F("LN"),tm); + printSetValue(dest,PSTR("LN"),tm); dtostrf(latitude,4,2,tm); - sappends(dest,'s',SET_F("LT"),tm); + printSetValue(dest,PSTR("LT"),tm); getTimeString(tm); - sappends(dest,'m',SET_F("(\"times\")[0]"),tm); + printSetMessage(dest,PSTR("(\"times\")[0]"),tm); if ((int)(longitude*10.0f) || (int)(latitude*10.0f)) { sprintf_P(tm, PSTR("Sunrise: %02d:%02d Sunset: %02d:%02d"), hour(sunrise), minute(sunrise), hour(sunset), minute(sunset)); - sappends(dest,'m',SET_F("(\"times\")[1]"),tm); + printSetMessage(dest,PSTR("(\"times\")[1]"),tm); } - sappend(dest,'c',SET_F("OL"),overlayCurrent); - sappend(dest,'v',SET_F("O1"),overlayMin); - sappend(dest,'v',SET_F("O2"),overlayMax); - sappend(dest,'v',SET_F("OM"),analogClock12pixel); - sappend(dest,'c',SET_F("OS"),analogClockSecondsTrail); - sappend(dest,'c',SET_F("O5"),analogClock5MinuteMarks); - sappend(dest,'c',SET_F("OB"),analogClockSolidBlack); - - sappend(dest,'c',SET_F("CE"),countdownMode); - sappend(dest,'v',SET_F("CY"),countdownYear); - sappend(dest,'v',SET_F("CI"),countdownMonth); - sappend(dest,'v',SET_F("CD"),countdownDay); - sappend(dest,'v',SET_F("CH"),countdownHour); - sappend(dest,'v',SET_F("CM"),countdownMin); - sappend(dest,'v',SET_F("CS"),countdownSec); - - sappend(dest,'v',SET_F("A0"),macroAlexaOn); - sappend(dest,'v',SET_F("A1"),macroAlexaOff); - sappend(dest,'v',SET_F("MC"),macroCountdown); - sappend(dest,'v',SET_F("MN"),macroNl); + printSetCheckbox(dest,PSTR("OL"),overlayCurrent); + printSetValue(dest,PSTR("O1"),overlayMin); + printSetValue(dest,PSTR("O2"),overlayMax); + printSetValue(dest,PSTR("OM"),analogClock12pixel); + printSetCheckbox(dest,PSTR("OS"),analogClockSecondsTrail); + printSetCheckbox(dest,PSTR("O5"),analogClock5MinuteMarks); + printSetCheckbox(dest,PSTR("OB"),analogClockSolidBlack); + + printSetCheckbox(dest,PSTR("CE"),countdownMode); + printSetValue(dest,PSTR("CY"),countdownYear); + printSetValue(dest,PSTR("CI"),countdownMonth); + printSetValue(dest,PSTR("CD"),countdownDay); + printSetValue(dest,PSTR("CH"),countdownHour); + printSetValue(dest,PSTR("CM"),countdownMin); + printSetValue(dest,PSTR("CS"),countdownSec); + + printSetValue(dest,PSTR("A0"),macroAlexaOn); + printSetValue(dest,PSTR("A1"),macroAlexaOff); + printSetValue(dest,PSTR("MC"),macroCountdown); + printSetValue(dest,PSTR("MN"),macroNl); for (unsigned i=0; i> 4) & 0x0F); - k[0] = 'P'; sappend(dest,'v',k,timerMonth[i] & 0x0F); - k[0] = 'D'; sappend(dest,'v',k,timerDay[i]); - k[0] = 'E'; sappend(dest,'v',k,timerDayEnd[i]); + k[0] = 'M'; printSetValue(dest,k,(timerMonth[i] >> 4) & 0x0F); + k[0] = 'P'; printSetValue(dest,k,timerMonth[i] & 0x0F); + k[0] = 'D'; printSetValue(dest,k,timerDay[i]); + k[0] = 'E'; printSetValue(dest,k,timerDayEnd[i]); } } } @@ -571,41 +571,41 @@ void getSettingsJS(byte subPage, Print& dest) char fpass[l+1]; //fill PIN field with 0000 fpass[l] = 0; memset(fpass,'0',l); - sappends(dest,'s',SET_F("PIN"),fpass); - sappend(dest,'c',SET_F("NO"),otaLock); - sappend(dest,'c',SET_F("OW"),wifiLock); - sappend(dest,'c',SET_F("AO"),aOtaEnabled); + printSetValue(dest,PSTR("PIN"),fpass); + printSetCheckbox(dest,PSTR("NO"),otaLock); + printSetCheckbox(dest,PSTR("OW"),wifiLock); + printSetCheckbox(dest,PSTR("AO"),aOtaEnabled); char tmp_buf[128]; snprintf_P(tmp_buf,sizeof(tmp_buf),PSTR("WLED %s (build %d)"),versionString,VERSION); - sappends(dest,'m',SET_F("(\"sip\")[0]"),tmp_buf); + printSetMessage(dest,PSTR("(\"sip\")[0]"),tmp_buf); dest.printf_P(PSTR("sd=\"%s\";"), serverDescription); } #ifdef WLED_ENABLE_DMX // include only if DMX is enabled if (subPage == SUBPAGE_DMX) { - sappend(dest,'v',SET_F("PU"),e131ProxyUniverse); - - sappend(dest,'v',SET_F("CN"),DMXChannels); - sappend(dest,'v',SET_F("CG"),DMXGap); - sappend(dest,'v',SET_F("CS"),DMXStart); - sappend(dest,'v',SET_F("SL"),DMXStartLED); - - sappend(dest,'i',SET_F("CH1"),DMXFixtureMap[0]); - sappend(dest,'i',SET_F("CH2"),DMXFixtureMap[1]); - sappend(dest,'i',SET_F("CH3"),DMXFixtureMap[2]); - sappend(dest,'i',SET_F("CH4"),DMXFixtureMap[3]); - sappend(dest,'i',SET_F("CH5"),DMXFixtureMap[4]); - sappend(dest,'i',SET_F("CH6"),DMXFixtureMap[5]); - sappend(dest,'i',SET_F("CH7"),DMXFixtureMap[6]); - sappend(dest,'i',SET_F("CH8"),DMXFixtureMap[7]); - sappend(dest,'i',SET_F("CH9"),DMXFixtureMap[8]); - sappend(dest,'i',SET_F("CH10"),DMXFixtureMap[9]); - sappend(dest,'i',SET_F("CH11"),DMXFixtureMap[10]); - sappend(dest,'i',SET_F("CH12"),DMXFixtureMap[11]); - sappend(dest,'i',SET_F("CH13"),DMXFixtureMap[12]); - sappend(dest,'i',SET_F("CH14"),DMXFixtureMap[13]); - sappend(dest,'i',SET_F("CH15"),DMXFixtureMap[14]); + printSetValue(dest,PSTR("PU"),e131ProxyUniverse); + + printSetValue(dest,PSTR("CN"),DMXChannels); + printSetValue(dest,PSTR("CG"),DMXGap); + printSetValue(dest,PSTR("CS"),DMXStart); + printSetValue(dest,PSTR("SL"),DMXStartLED); + + printSetIndex(dest,PSTR("CH1"),DMXFixtureMap[0]); + printSetIndex(dest,PSTR("CH2"),DMXFixtureMap[1]); + printSetIndex(dest,PSTR("CH3"),DMXFixtureMap[2]); + printSetIndex(dest,PSTR("CH4"),DMXFixtureMap[3]); + printSetIndex(dest,PSTR("CH5"),DMXFixtureMap[4]); + printSetIndex(dest,PSTR("CH6"),DMXFixtureMap[5]); + printSetIndex(dest,PSTR("CH7"),DMXFixtureMap[6]); + printSetIndex(dest,PSTR("CH8"),DMXFixtureMap[7]); + printSetIndex(dest,PSTR("CH9"),DMXFixtureMap[8]); + printSetIndex(dest,PSTR("CH10"),DMXFixtureMap[9]); + printSetIndex(dest,PSTR("CH11"),DMXFixtureMap[10]); + printSetIndex(dest,PSTR("CH12"),DMXFixtureMap[11]); + printSetIndex(dest,PSTR("CH13"),DMXFixtureMap[12]); + printSetIndex(dest,PSTR("CH14"),DMXFixtureMap[13]); + printSetIndex(dest,PSTR("CH15"),DMXFixtureMap[14]); } #endif @@ -613,11 +613,11 @@ void getSettingsJS(byte subPage, Print& dest) { appendGPIOinfo(dest); dest.printf_P(PSTR("numM=%d;"), usermods.getModCount()); - sappend(dest,'v',SET_F("SDA"),i2c_sda); - sappend(dest,'v',SET_F("SCL"),i2c_scl); - sappend(dest,'v',SET_F("MOSI"),spi_mosi); - sappend(dest,'v',SET_F("MISO"),spi_miso); - sappend(dest,'v',SET_F("SCLK"),spi_sclk); + printSetValue(dest,PSTR("SDA"),i2c_sda); + printSetValue(dest,PSTR("SCL"),i2c_scl); + printSetValue(dest,PSTR("MOSI"),spi_mosi); + printSetValue(dest,PSTR("MISO"),spi_miso); + printSetValue(dest,PSTR("SCLK"),spi_sclk); dest.printf_P(PSTR("addInfo('SDA','%d');" "addInfo('SCL','%d');" "addInfo('MOSI','%d');" @@ -641,21 +641,21 @@ void getSettingsJS(byte subPage, Print& dest) #endif VERSION); - sappends(dest,'m',SET_F("(\"sip\")[0]"),tmp_buf); + printSetMessage(dest,PSTR("(\"sip\")[0]"),tmp_buf); } if (subPage == SUBPAGE_2D) // 2D matrices { - sappend(dest,'v',SET_F("SOMP"),strip.isMatrix); + printSetValue(dest,PSTR("SOMP"),strip.isMatrix); #ifndef WLED_DISABLE_2D dest.print(F("maxPanels=")); dest.print(WLED_MAX_PANELS); dest.print(F(";")); dest.print(F("resetPanels();")); if (strip.isMatrix) { if(strip.panels>0){ - sappend(dest,'v',SET_F("PW"),strip.panel[0].width); //Set generator Width and Height to first panel size for convenience - sappend(dest,'v',SET_F("PH"),strip.panel[0].height); + printSetValue(dest,PSTR("PW"),strip.panel[0].width); //Set generator Width and Height to first panel size for convenience + printSetValue(dest,PSTR("PH"),strip.panel[0].height); } - sappend(dest,'v',SET_F("MPC"),strip.panels); + printSetValue(dest,PSTR("MPC"),strip.panels); // panels for (unsigned i=0; i Date: Thu, 12 Sep 2024 20:39:13 +0200 Subject: [PATCH 084/145] New names --- wled00/fcn_declare.h | 10 +- wled00/util.cpp | 20 +- wled00/xml.cpp | 442 +++++++++++++++++++++---------------------- 3 files changed, 236 insertions(+), 236 deletions(-) diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index a36f2dc264..29db8ea765 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -372,11 +372,11 @@ void parseNumber(const char* str, byte* val, byte minv=0, byte maxv=255); bool getVal(JsonVariant elem, byte* val, byte minv=0, byte maxv=255); bool getBoolVal(JsonVariant elem, bool dflt); bool updateVal(const char* req, const char* key, byte* val, byte minv=0, byte maxv=255); -size_t printSetCheckbox(Print& dest, const char* key, int val); -size_t printSetValue(Print& dest, const char* key, int val); -size_t printSetValue(Print& dest, const char* key, const char* val); -size_t printSetIndex(Print& dest, const char* key, int index); -size_t printSetMessage(Print& dest, const char* key, const char* val); +size_t printSetFormCheckbox(Print& dest, const char* key, int val); +size_t printSetFormValue(Print& dest, const char* key, int val); +size_t printSetFormValue(Print& dest, const char* key, const char* val); +size_t printSetFormIndex(Print& dest, const char* key, int index); +size_t printSetClassElementHTML(Print& dest, const char* key, const int index, const char* val); void prepareHostname(char* hostname); bool isAsterisksOnly(const char* str, byte maxLen); bool requestJSONBufferLock(uint8_t module=255); diff --git a/wled00/util.cpp b/wled00/util.cpp index 660877d186..07190e37cc 100644 --- a/wled00/util.cpp +++ b/wled00/util.cpp @@ -87,26 +87,26 @@ bool updateVal(const char* req, const char* key, byte* val, byte minv, byte maxv return true; } -static size_t printSetInt(Print& dest, const char* key, const char* selector, int value) { +static size_t printSetFormInput(Print& dest, const char* key, const char* selector, int value) { return dest.printf_P(PSTR("d.Sf.%s.%s=%d;"), key, selector, value); } -size_t printSetCheckbox(Print& dest, const char* key, int val) { - return printSetInt(dest, key, PSTR("checked"), val); +size_t printSetFormCheckbox(Print& dest, const char* key, int val) { + return printSetFormInput(dest, key, PSTR("checked"), val); } -size_t printSetValue(Print& dest, const char* key, int val) { - return printSetInt(dest, key, PSTR("value"), val); +size_t printSetFormValue(Print& dest, const char* key, int val) { + return printSetFormInput(dest, key, PSTR("value"), val); } -size_t printSetIndex(Print& dest, const char* key, int index) { - return printSetInt(dest, key, PSTR("selectedIndex"), index); +size_t printSetFormIndex(Print& dest, const char* key, int index) { + return printSetFormInput(dest, key, PSTR("selectedIndex"), index); } -size_t printSetValue(Print& dest, const char* key, const char* val) { +size_t printSetFormValue(Print& dest, const char* key, const char* val) { return dest.printf_P(PSTR("d.Sf.%s.value=\"%s\";"),key,val); } -size_t printSetMessage(Print& dest, const char* key, const char* val) { - return dest.printf_P(PSTR("d.getElementsByClassName%s.innerHTML=\"%s\";"), key, val); +size_t printSetClassElementHTML(Print& dest, const char* key, const int index, const char* val) { + return dest.printf_P(PSTR("d.getElementsByClassName(\"%s\")[%d].innerHTML=\"%s\";"), key, index, val); } diff --git a/wled00/xml.cpp b/wled00/xml.cpp index e8858066c1..8cbd51cde9 100644 --- a/wled00/xml.cpp +++ b/wled00/xml.cpp @@ -180,41 +180,41 @@ void getSettingsJS(byte subPage, Print& dest) (uint32_t) multiWiFi[n].staticSN); } - printSetValue(dest,PSTR("D0"),dnsAddress[0]); - printSetValue(dest,PSTR("D1"),dnsAddress[1]); - printSetValue(dest,PSTR("D2"),dnsAddress[2]); - printSetValue(dest,PSTR("D3"),dnsAddress[3]); + printSetFormValue(dest,PSTR("D0"),dnsAddress[0]); + printSetFormValue(dest,PSTR("D1"),dnsAddress[1]); + printSetFormValue(dest,PSTR("D2"),dnsAddress[2]); + printSetFormValue(dest,PSTR("D3"),dnsAddress[3]); - printSetValue(dest,PSTR("CM"),cmDNS); - printSetIndex(dest,PSTR("AB"),apBehavior); - printSetValue(dest,PSTR("AS"),apSSID); - printSetCheckbox(dest,PSTR("AH"),apHide); + printSetFormValue(dest,PSTR("CM"),cmDNS); + printSetFormIndex(dest,PSTR("AB"),apBehavior); + printSetFormValue(dest,PSTR("AS"),apSSID); + printSetFormCheckbox(dest,PSTR("AH"),apHide); l = strlen(apPass); char fapass[l+1]; //fill password field with *** fapass[l] = 0; memset(fapass,'*',l); - printSetValue(dest,PSTR("AP"),fapass); + printSetFormValue(dest,PSTR("AP"),fapass); - printSetValue(dest,PSTR("AC"),apChannel); + printSetFormValue(dest,PSTR("AC"),apChannel); #ifdef ARDUINO_ARCH_ESP32 - printSetValue(dest,PSTR("TX"),txPower); + printSetFormValue(dest,PSTR("TX"),txPower); #else dest.print(F("gId('tx').style.display='none';")); #endif - printSetCheckbox(dest,PSTR("FG"),force802_3g); - printSetCheckbox(dest,PSTR("WS"),noWifiSleep); + printSetFormCheckbox(dest,PSTR("FG"),force802_3g); + printSetFormCheckbox(dest,PSTR("WS"),noWifiSleep); #ifndef WLED_DISABLE_ESPNOW - printSetCheckbox(dest,PSTR("RE"),enableESPNow); - printSetValue(dest,PSTR("RMAC"),linked_remote); + printSetFormCheckbox(dest,PSTR("RE"),enableESPNow); + printSetFormValue(dest,PSTR("RMAC"),linked_remote); #else //hide remote settings if not compiled dest.print(F("toggle('ESPNOW');")); // hide ESP-NOW setting #endif #ifdef WLED_USE_ETHERNET - printSetValue(dest,PSTR("ETH"),ethernetType); + printSetFormValue(dest,PSTR("ETH"),ethernetType); #else //hide ethernet setting if not compiled in dest.print(F("gId('ethd').style.display='none';")); @@ -229,10 +229,10 @@ void getSettingsJS(byte subPage, Print& dest) #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) if (Network.isEthernet()) strcat_P(s ,SET_F(" (Ethernet)")); #endif - printSetMessage(dest,PSTR("(\"sip\")[0]"),s); + printSetClassElementHTML(dest,PSTR("sip"),0,s); } else { - printSetMessage(dest,PSTR("(\"sip\")[0]"),(char*)F("Not connected")); + printSetClassElementHTML(dest,PSTR("sip"),0,(char*)F("Not connected")); } if (WiFi.softAPIP()[0] != 0) //is active @@ -240,19 +240,19 @@ void getSettingsJS(byte subPage, Print& dest) char s[16]; IPAddress apIP = WiFi.softAPIP(); sprintf(s, "%d.%d.%d.%d", apIP[0], apIP[1], apIP[2], apIP[3]); - printSetMessage(dest,PSTR("(\"sip\")[1]"),s); + printSetClassElementHTML(dest,PSTR("sip"),1,s); } else { - printSetMessage(dest,PSTR("(\"sip\")[1]"),(char*)F("Not active")); + printSetClassElementHTML(dest,PSTR("sip"),1,(char*)F("Not active")); } #ifndef WLED_DISABLE_ESPNOW if (strlen(last_signal_src) > 0) { //Have seen an ESP-NOW Remote - printSetMessage(dest,PSTR("(\"rlid\")[0]"),last_signal_src); + printSetClassElementHTML(dest,PSTR("rlid"),0,last_signal_src); } else if (!enableESPNow) { - printSetMessage(dest,PSTR("(\"rlid\")[0]"),(char*)F("(Enable ESP-NOW to listen)")); + printSetClassElementHTML(dest,PSTR("rlid"),0,(char*)F("(Enable ESP-NOW to listen)")); } else { - printSetMessage(dest,PSTR("(\"rlid\")[0]"),(char*)F("None")); + printSetClassElementHTML(dest,PSTR("rlid"),0,(char*)F("None")); } #endif } @@ -277,14 +277,14 @@ void getSettingsJS(byte subPage, Print& dest) WLED_MAX_ANALOG_CHANNELS ); - printSetCheckbox(dest,PSTR("MS"),strip.autoSegments); - printSetCheckbox(dest,PSTR("CCT"),strip.correctWB); - printSetCheckbox(dest,PSTR("IC"),cctICused); - printSetCheckbox(dest,PSTR("CR"),strip.cctFromRgb); - printSetValue(dest,PSTR("CB"),strip.cctBlending); - printSetValue(dest,PSTR("FR"),strip.getTargetFps()); - printSetValue(dest,PSTR("AW"),Bus::getGlobalAWMode()); - printSetCheckbox(dest,PSTR("LD"),useGlobalLedBuffer); + printSetFormCheckbox(dest,PSTR("MS"),strip.autoSegments); + printSetFormCheckbox(dest,PSTR("CCT"),strip.correctWB); + printSetFormCheckbox(dest,PSTR("IC"),cctICused); + printSetFormCheckbox(dest,PSTR("CR"),strip.cctFromRgb); + printSetFormValue(dest,PSTR("CB"),strip.cctBlending); + printSetFormValue(dest,PSTR("FR"),strip.getTargetFps()); + printSetFormValue(dest,PSTR("AW"),Bus::getGlobalAWMode()); + printSetFormCheckbox(dest,PSTR("LD"),useGlobalLedBuffer); unsigned sumMa = 0; for (int s = 0; s < BusManager::getNumBusses(); s++) { @@ -309,17 +309,17 @@ void getSettingsJS(byte subPage, Print& dest) int nPins = bus->getPins(pins); for (int i = 0; i < nPins; i++) { lp[1] = offset+i; - if (pinManager.isPinOk(pins[i]) || bus->isVirtual()) printSetValue(dest,lp,pins[i]); + if (pinManager.isPinOk(pins[i]) || bus->isVirtual()) printSetFormValue(dest,lp,pins[i]); } - printSetValue(dest,lc,bus->getLength()); - printSetValue(dest,lt,bus->getType()); - printSetValue(dest,co,bus->getColorOrder() & 0x0F); - printSetValue(dest,ls,bus->getStart()); - printSetCheckbox(dest,cv,bus->isReversed()); - printSetValue(dest,sl,bus->skippedLeds()); - printSetCheckbox(dest,rf,bus->isOffRefreshRequired()); - printSetValue(dest,aw,bus->getAutoWhiteMode()); - printSetValue(dest,wo,bus->getColorOrder() >> 4); + printSetFormValue(dest,lc,bus->getLength()); + printSetFormValue(dest,lt,bus->getType()); + printSetFormValue(dest,co,bus->getColorOrder() & 0x0F); + printSetFormValue(dest,ls,bus->getStart()); + printSetFormCheckbox(dest,cv,bus->isReversed()); + printSetFormValue(dest,sl,bus->skippedLeds()); + printSetFormCheckbox(dest,rf,bus->isOffRefreshRequired()); + printSetFormValue(dest,aw,bus->getAutoWhiteMode()); + printSetFormValue(dest,wo,bus->getColorOrder() >> 4); unsigned speed = bus->getFrequency(); if (bus->isPWM()) { switch (speed) { @@ -340,14 +340,14 @@ void getSettingsJS(byte subPage, Print& dest) case 20000 : speed = 4; break; } } - printSetValue(dest,sp,speed); - printSetValue(dest,la,bus->getLEDCurrent()); - printSetValue(dest,ma,bus->getMaxCurrent()); + printSetFormValue(dest,sp,speed); + printSetFormValue(dest,la,bus->getLEDCurrent()); + printSetFormValue(dest,ma,bus->getMaxCurrent()); sumMa += bus->getMaxCurrent(); } - printSetValue(dest,PSTR("MA"),BusManager::ablMilliampsMax() ? BusManager::ablMilliampsMax() : sumMa); - printSetCheckbox(dest,PSTR("ABL"),BusManager::ablMilliampsMax() || sumMa > 0); - printSetCheckbox(dest,PSTR("PPL"),!BusManager::ablMilliampsMax() && sumMa > 0); + printSetFormValue(dest,PSTR("MA"),BusManager::ablMilliampsMax() ? BusManager::ablMilliampsMax() : sumMa); + printSetFormCheckbox(dest,PSTR("ABL"),BusManager::ablMilliampsMax() || sumMa > 0); + printSetFormCheckbox(dest,PSTR("PPL"),!BusManager::ablMilliampsMax() && sumMa > 0); dest.printf_P(PSTR("resetCOM(%d);"), WLED_MAX_COLOR_ORDER_MAPPINGS); const ColorOrderMap& com = BusManager::getColorOrderMap(); @@ -357,114 +357,114 @@ void getSettingsJS(byte subPage, Print& dest) dest.printf_P(PSTR("addCOM(%d,%d,%d);"), entry->start, entry->len, entry->colorOrder); } - printSetValue(dest,PSTR("CA"),briS); - - printSetCheckbox(dest,PSTR("BO"),turnOnAtBoot); - printSetValue(dest,PSTR("BP"),bootPreset); - - printSetCheckbox(dest,PSTR("GB"),gammaCorrectBri); - printSetCheckbox(dest,PSTR("GC"),gammaCorrectCol); - dtostrf(gammaCorrectVal,3,1,nS); printSetValue(dest,PSTR("GV"),nS); - printSetCheckbox(dest,PSTR("TF"),fadeTransition); - printSetCheckbox(dest,PSTR("EB"),modeBlending); - printSetValue(dest,PSTR("TD"),transitionDelayDefault); - printSetCheckbox(dest,PSTR("PF"),strip.paletteFade); - printSetValue(dest,PSTR("TP"),randomPaletteChangeTime); - printSetCheckbox(dest,PSTR("TH"),useHarmonicRandomPalette); - printSetValue(dest,PSTR("BF"),briMultiplier); - printSetValue(dest,PSTR("TB"),nightlightTargetBri); - printSetValue(dest,PSTR("TL"),nightlightDelayMinsDefault); - printSetValue(dest,PSTR("TW"),nightlightMode); - printSetIndex(dest,PSTR("PB"),strip.paletteBlend); - printSetValue(dest,PSTR("RL"),rlyPin); - printSetCheckbox(dest,PSTR("RM"),rlyMde); - printSetCheckbox(dest,PSTR("RO"),rlyOpenDrain); + printSetFormValue(dest,PSTR("CA"),briS); + + printSetFormCheckbox(dest,PSTR("BO"),turnOnAtBoot); + printSetFormValue(dest,PSTR("BP"),bootPreset); + + printSetFormCheckbox(dest,PSTR("GB"),gammaCorrectBri); + printSetFormCheckbox(dest,PSTR("GC"),gammaCorrectCol); + dtostrf(gammaCorrectVal,3,1,nS); printSetFormValue(dest,PSTR("GV"),nS); + printSetFormCheckbox(dest,PSTR("TF"),fadeTransition); + printSetFormCheckbox(dest,PSTR("EB"),modeBlending); + printSetFormValue(dest,PSTR("TD"),transitionDelayDefault); + printSetFormCheckbox(dest,PSTR("PF"),strip.paletteFade); + printSetFormValue(dest,PSTR("TP"),randomPaletteChangeTime); + printSetFormCheckbox(dest,PSTR("TH"),useHarmonicRandomPalette); + printSetFormValue(dest,PSTR("BF"),briMultiplier); + printSetFormValue(dest,PSTR("TB"),nightlightTargetBri); + printSetFormValue(dest,PSTR("TL"),nightlightDelayMinsDefault); + printSetFormValue(dest,PSTR("TW"),nightlightMode); + printSetFormIndex(dest,PSTR("PB"),strip.paletteBlend); + printSetFormValue(dest,PSTR("RL"),rlyPin); + printSetFormCheckbox(dest,PSTR("RM"),rlyMde); + printSetFormCheckbox(dest,PSTR("RO"),rlyOpenDrain); for (int i = 0; i < WLED_MAX_BUTTONS; i++) { dest.printf_P(PSTR("addBtn(%d,%d,%d);"), i, btnPin[i], buttonType[i]); } - printSetCheckbox(dest,PSTR("IP"),disablePullUp); - printSetValue(dest,PSTR("TT"),touchThreshold); + printSetFormCheckbox(dest,PSTR("IP"),disablePullUp); + printSetFormValue(dest,PSTR("TT"),touchThreshold); #ifndef WLED_DISABLE_INFRARED - printSetValue(dest,PSTR("IR"),irPin); - printSetValue(dest,PSTR("IT"),irEnabled); + printSetFormValue(dest,PSTR("IR"),irPin); + printSetFormValue(dest,PSTR("IT"),irEnabled); #endif - printSetCheckbox(dest,PSTR("MSO"),!irApplyToAllSelected); + printSetFormCheckbox(dest,PSTR("MSO"),!irApplyToAllSelected); } if (subPage == SUBPAGE_UI) { - printSetValue(dest,PSTR("DS"),serverDescription); - printSetCheckbox(dest,PSTR("SU"),simplifiedUI); + printSetFormValue(dest,PSTR("DS"),serverDescription); + printSetFormCheckbox(dest,PSTR("SU"),simplifiedUI); } if (subPage == SUBPAGE_SYNC) { [[maybe_unused]] char nS[32]; - printSetValue(dest,PSTR("UP"),udpPort); - printSetValue(dest,PSTR("U2"),udpPort2); + printSetFormValue(dest,PSTR("UP"),udpPort); + printSetFormValue(dest,PSTR("U2"),udpPort2); #ifndef WLED_DISABLE_ESPNOW - if (enableESPNow) printSetCheckbox(dest,PSTR("EN"),useESPNowSync); + if (enableESPNow) printSetFormCheckbox(dest,PSTR("EN"),useESPNowSync); else dest.print(F("toggle('ESPNOW');")); // hide ESP-NOW setting #else dest.print(F("toggle('ESPNOW');")); // hide ESP-NOW setting #endif - printSetValue(dest,PSTR("GS"),syncGroups); - printSetValue(dest,PSTR("GR"),receiveGroups); - - printSetCheckbox(dest,PSTR("RB"),receiveNotificationBrightness); - printSetCheckbox(dest,PSTR("RC"),receiveNotificationColor); - printSetCheckbox(dest,PSTR("RX"),receiveNotificationEffects); - printSetCheckbox(dest,PSTR("RP"),receiveNotificationPalette); - printSetCheckbox(dest,PSTR("SO"),receiveSegmentOptions); - printSetCheckbox(dest,PSTR("SG"),receiveSegmentBounds); - printSetCheckbox(dest,PSTR("SS"),sendNotifications); - printSetCheckbox(dest,PSTR("SD"),notifyDirect); - printSetCheckbox(dest,PSTR("SB"),notifyButton); - printSetCheckbox(dest,PSTR("SH"),notifyHue); - printSetValue(dest,PSTR("UR"),udpNumRetries); - - printSetCheckbox(dest,PSTR("NL"),nodeListEnabled); - printSetCheckbox(dest,PSTR("NB"),nodeBroadcastEnabled); - - printSetCheckbox(dest,PSTR("RD"),receiveDirect); - printSetCheckbox(dest,PSTR("MO"),useMainSegmentOnly); - printSetCheckbox(dest,PSTR("RLM"),realtimeRespectLedMaps); - printSetValue(dest,PSTR("EP"),e131Port); - printSetCheckbox(dest,PSTR("ES"),e131SkipOutOfSequence); - printSetCheckbox(dest,PSTR("EM"),e131Multicast); - printSetValue(dest,PSTR("EU"),e131Universe); - printSetValue(dest,PSTR("DA"),DMXAddress); - printSetValue(dest,PSTR("XX"),DMXSegmentSpacing); - printSetValue(dest,PSTR("PY"),e131Priority); - printSetValue(dest,PSTR("DM"),DMXMode); - printSetValue(dest,PSTR("ET"),realtimeTimeoutMs); - printSetCheckbox(dest,PSTR("FB"),arlsForceMaxBri); - printSetCheckbox(dest,PSTR("RG"),arlsDisableGammaCorrection); - printSetValue(dest,PSTR("WO"),arlsOffset); + printSetFormValue(dest,PSTR("GS"),syncGroups); + printSetFormValue(dest,PSTR("GR"),receiveGroups); + + printSetFormCheckbox(dest,PSTR("RB"),receiveNotificationBrightness); + printSetFormCheckbox(dest,PSTR("RC"),receiveNotificationColor); + printSetFormCheckbox(dest,PSTR("RX"),receiveNotificationEffects); + printSetFormCheckbox(dest,PSTR("RP"),receiveNotificationPalette); + printSetFormCheckbox(dest,PSTR("SO"),receiveSegmentOptions); + printSetFormCheckbox(dest,PSTR("SG"),receiveSegmentBounds); + printSetFormCheckbox(dest,PSTR("SS"),sendNotifications); + printSetFormCheckbox(dest,PSTR("SD"),notifyDirect); + printSetFormCheckbox(dest,PSTR("SB"),notifyButton); + printSetFormCheckbox(dest,PSTR("SH"),notifyHue); + printSetFormValue(dest,PSTR("UR"),udpNumRetries); + + printSetFormCheckbox(dest,PSTR("NL"),nodeListEnabled); + printSetFormCheckbox(dest,PSTR("NB"),nodeBroadcastEnabled); + + printSetFormCheckbox(dest,PSTR("RD"),receiveDirect); + printSetFormCheckbox(dest,PSTR("MO"),useMainSegmentOnly); + printSetFormCheckbox(dest,PSTR("RLM"),realtimeRespectLedMaps); + printSetFormValue(dest,PSTR("EP"),e131Port); + printSetFormCheckbox(dest,PSTR("ES"),e131SkipOutOfSequence); + printSetFormCheckbox(dest,PSTR("EM"),e131Multicast); + printSetFormValue(dest,PSTR("EU"),e131Universe); + printSetFormValue(dest,PSTR("DA"),DMXAddress); + printSetFormValue(dest,PSTR("XX"),DMXSegmentSpacing); + printSetFormValue(dest,PSTR("PY"),e131Priority); + printSetFormValue(dest,PSTR("DM"),DMXMode); + printSetFormValue(dest,PSTR("ET"),realtimeTimeoutMs); + printSetFormCheckbox(dest,PSTR("FB"),arlsForceMaxBri); + printSetFormCheckbox(dest,PSTR("RG"),arlsDisableGammaCorrection); + printSetFormValue(dest,PSTR("WO"),arlsOffset); #ifndef WLED_DISABLE_ALEXA - printSetCheckbox(dest,PSTR("AL"),alexaEnabled); - printSetValue(dest,PSTR("AI"),alexaInvocationName); - printSetCheckbox(dest,PSTR("SA"),notifyAlexa); - printSetValue(dest,PSTR("AP"),alexaNumPresets); + printSetFormCheckbox(dest,PSTR("AL"),alexaEnabled); + printSetFormValue(dest,PSTR("AI"),alexaInvocationName); + printSetFormCheckbox(dest,PSTR("SA"),notifyAlexa); + printSetFormValue(dest,PSTR("AP"),alexaNumPresets); #else dest.print(F("toggle('Alexa');")); // hide Alexa settings #endif #ifndef WLED_DISABLE_MQTT - printSetCheckbox(dest,PSTR("MQ"),mqttEnabled); - printSetValue(dest,PSTR("MS"),mqttServer); - printSetValue(dest,PSTR("MQPORT"),mqttPort); - printSetValue(dest,PSTR("MQUSER"),mqttUser); + printSetFormCheckbox(dest,PSTR("MQ"),mqttEnabled); + printSetFormValue(dest,PSTR("MS"),mqttServer); + printSetFormValue(dest,PSTR("MQPORT"),mqttPort); + printSetFormValue(dest,PSTR("MQUSER"),mqttUser); byte l = strlen(mqttPass); char fpass[l+1]; //fill password field with *** fpass[l] = 0; memset(fpass,'*',l); - printSetValue(dest,PSTR("MQPASS"),fpass); - printSetValue(dest,PSTR("MQCID"),mqttClientID); - printSetValue(dest,PSTR("MD"),mqttDeviceTopic); - printSetValue(dest,PSTR("MG"),mqttGroupTopic); - printSetCheckbox(dest,PSTR("BM"),buttonPublishMqtt); - printSetCheckbox(dest,PSTR("RT"),retainMqttMsg); + printSetFormValue(dest,PSTR("MQPASS"),fpass); + printSetFormValue(dest,PSTR("MQCID"),mqttClientID); + printSetFormValue(dest,PSTR("MD"),mqttDeviceTopic); + printSetFormValue(dest,PSTR("MG"),mqttGroupTopic); + printSetFormCheckbox(dest,PSTR("BM"),buttonPublishMqtt); + printSetFormCheckbox(dest,PSTR("RT"),retainMqttMsg); dest.printf_P(PSTR("d.Sf.MD.maxlength=%d;d.Sf.MG.maxlength=%d;d.Sf.MS.maxlength=%d;"), MQTT_MAX_TOPIC_LEN, MQTT_MAX_TOPIC_LEN, MQTT_MAX_SERVER_LEN); #else @@ -472,16 +472,16 @@ void getSettingsJS(byte subPage, Print& dest) #endif #ifndef WLED_DISABLE_HUESYNC - printSetValue(dest,PSTR("H0"),hueIP[0]); - printSetValue(dest,PSTR("H1"),hueIP[1]); - printSetValue(dest,PSTR("H2"),hueIP[2]); - printSetValue(dest,PSTR("H3"),hueIP[3]); - printSetValue(dest,PSTR("HL"),huePollLightId); - printSetValue(dest,PSTR("HI"),huePollIntervalMs); - printSetCheckbox(dest,PSTR("HP"),huePollingEnabled); - printSetCheckbox(dest,PSTR("HO"),hueApplyOnOff); - printSetCheckbox(dest,PSTR("HB"),hueApplyBri); - printSetCheckbox(dest,PSTR("HC"),hueApplyColor); + printSetFormValue(dest,PSTR("H0"),hueIP[0]); + printSetFormValue(dest,PSTR("H1"),hueIP[1]); + printSetFormValue(dest,PSTR("H2"),hueIP[2]); + printSetFormValue(dest,PSTR("H3"),hueIP[3]); + printSetFormValue(dest,PSTR("HL"),huePollLightId); + printSetFormValue(dest,PSTR("HI"),huePollIntervalMs); + printSetFormCheckbox(dest,PSTR("HP"),huePollingEnabled); + printSetFormCheckbox(dest,PSTR("HO"),hueApplyOnOff); + printSetFormCheckbox(dest,PSTR("HB"),hueApplyBri); + printSetFormCheckbox(dest,PSTR("HC"),hueApplyColor); char hueErrorString[25]; switch (hueError) { @@ -495,11 +495,11 @@ void getSettingsJS(byte subPage, Print& dest) default: sprintf_P(hueErrorString,PSTR("Bridge Error %i"),hueError); } - printSetMessage(dest,PSTR("(\"sip\")[0]"),hueErrorString); + printSetClassElementHTML(dest,PSTR("sip"),0,hueErrorString); #else dest.print(F("toggle('Hue');")); // hide Hue Sync settings #endif - printSetValue(dest,PSTR("BD"),serialBaud); + printSetFormValue(dest,PSTR("BD"),serialBaud); #ifndef WLED_ENABLE_ADALIGHT dest.print(SET_F("toggle('Serial);")); #endif @@ -507,42 +507,42 @@ void getSettingsJS(byte subPage, Print& dest) if (subPage == SUBPAGE_TIME) { - printSetCheckbox(dest,PSTR("NT"),ntpEnabled); - printSetValue(dest,PSTR("NS"),ntpServerName); - printSetCheckbox(dest,PSTR("CF"),!useAMPM); - printSetIndex(dest,PSTR("TZ"),currentTimezone); - printSetValue(dest,PSTR("UO"),utcOffsetSecs); + printSetFormCheckbox(dest,PSTR("NT"),ntpEnabled); + printSetFormValue(dest,PSTR("NS"),ntpServerName); + printSetFormCheckbox(dest,PSTR("CF"),!useAMPM); + printSetFormIndex(dest,PSTR("TZ"),currentTimezone); + printSetFormValue(dest,PSTR("UO"),utcOffsetSecs); char tm[32]; dtostrf(longitude,4,2,tm); - printSetValue(dest,PSTR("LN"),tm); + printSetFormValue(dest,PSTR("LN"),tm); dtostrf(latitude,4,2,tm); - printSetValue(dest,PSTR("LT"),tm); + printSetFormValue(dest,PSTR("LT"),tm); getTimeString(tm); - printSetMessage(dest,PSTR("(\"times\")[0]"),tm); + printSetClassElementHTML(dest,PSTR("times"),0,tm); if ((int)(longitude*10.0f) || (int)(latitude*10.0f)) { sprintf_P(tm, PSTR("Sunrise: %02d:%02d Sunset: %02d:%02d"), hour(sunrise), minute(sunrise), hour(sunset), minute(sunset)); - printSetMessage(dest,PSTR("(\"times\")[1]"),tm); + printSetClassElementHTML(dest,PSTR("times"),1,tm); } - printSetCheckbox(dest,PSTR("OL"),overlayCurrent); - printSetValue(dest,PSTR("O1"),overlayMin); - printSetValue(dest,PSTR("O2"),overlayMax); - printSetValue(dest,PSTR("OM"),analogClock12pixel); - printSetCheckbox(dest,PSTR("OS"),analogClockSecondsTrail); - printSetCheckbox(dest,PSTR("O5"),analogClock5MinuteMarks); - printSetCheckbox(dest,PSTR("OB"),analogClockSolidBlack); - - printSetCheckbox(dest,PSTR("CE"),countdownMode); - printSetValue(dest,PSTR("CY"),countdownYear); - printSetValue(dest,PSTR("CI"),countdownMonth); - printSetValue(dest,PSTR("CD"),countdownDay); - printSetValue(dest,PSTR("CH"),countdownHour); - printSetValue(dest,PSTR("CM"),countdownMin); - printSetValue(dest,PSTR("CS"),countdownSec); - - printSetValue(dest,PSTR("A0"),macroAlexaOn); - printSetValue(dest,PSTR("A1"),macroAlexaOff); - printSetValue(dest,PSTR("MC"),macroCountdown); - printSetValue(dest,PSTR("MN"),macroNl); + printSetFormCheckbox(dest,PSTR("OL"),overlayCurrent); + printSetFormValue(dest,PSTR("O1"),overlayMin); + printSetFormValue(dest,PSTR("O2"),overlayMax); + printSetFormValue(dest,PSTR("OM"),analogClock12pixel); + printSetFormCheckbox(dest,PSTR("OS"),analogClockSecondsTrail); + printSetFormCheckbox(dest,PSTR("O5"),analogClock5MinuteMarks); + printSetFormCheckbox(dest,PSTR("OB"),analogClockSolidBlack); + + printSetFormCheckbox(dest,PSTR("CE"),countdownMode); + printSetFormValue(dest,PSTR("CY"),countdownYear); + printSetFormValue(dest,PSTR("CI"),countdownMonth); + printSetFormValue(dest,PSTR("CD"),countdownDay); + printSetFormValue(dest,PSTR("CH"),countdownHour); + printSetFormValue(dest,PSTR("CM"),countdownMin); + printSetFormValue(dest,PSTR("CS"),countdownSec); + + printSetFormValue(dest,PSTR("A0"),macroAlexaOn); + printSetFormValue(dest,PSTR("A1"),macroAlexaOff); + printSetFormValue(dest,PSTR("MC"),macroCountdown); + printSetFormValue(dest,PSTR("MN"),macroNl); for (unsigned i=0; i> 4) & 0x0F); - k[0] = 'P'; printSetValue(dest,k,timerMonth[i] & 0x0F); - k[0] = 'D'; printSetValue(dest,k,timerDay[i]); - k[0] = 'E'; printSetValue(dest,k,timerDayEnd[i]); + k[0] = 'M'; printSetFormValue(dest,k,(timerMonth[i] >> 4) & 0x0F); + k[0] = 'P'; printSetFormValue(dest,k,timerMonth[i] & 0x0F); + k[0] = 'D'; printSetFormValue(dest,k,timerDay[i]); + k[0] = 'E'; printSetFormValue(dest,k,timerDayEnd[i]); } } } @@ -571,41 +571,41 @@ void getSettingsJS(byte subPage, Print& dest) char fpass[l+1]; //fill PIN field with 0000 fpass[l] = 0; memset(fpass,'0',l); - printSetValue(dest,PSTR("PIN"),fpass); - printSetCheckbox(dest,PSTR("NO"),otaLock); - printSetCheckbox(dest,PSTR("OW"),wifiLock); - printSetCheckbox(dest,PSTR("AO"),aOtaEnabled); + printSetFormValue(dest,PSTR("PIN"),fpass); + printSetFormCheckbox(dest,PSTR("NO"),otaLock); + printSetFormCheckbox(dest,PSTR("OW"),wifiLock); + printSetFormCheckbox(dest,PSTR("AO"),aOtaEnabled); char tmp_buf[128]; snprintf_P(tmp_buf,sizeof(tmp_buf),PSTR("WLED %s (build %d)"),versionString,VERSION); - printSetMessage(dest,PSTR("(\"sip\")[0]"),tmp_buf); + printSetClassElementHTML(dest,PSTR("sip"),0,tmp_buf); dest.printf_P(PSTR("sd=\"%s\";"), serverDescription); } #ifdef WLED_ENABLE_DMX // include only if DMX is enabled if (subPage == SUBPAGE_DMX) { - printSetValue(dest,PSTR("PU"),e131ProxyUniverse); - - printSetValue(dest,PSTR("CN"),DMXChannels); - printSetValue(dest,PSTR("CG"),DMXGap); - printSetValue(dest,PSTR("CS"),DMXStart); - printSetValue(dest,PSTR("SL"),DMXStartLED); - - printSetIndex(dest,PSTR("CH1"),DMXFixtureMap[0]); - printSetIndex(dest,PSTR("CH2"),DMXFixtureMap[1]); - printSetIndex(dest,PSTR("CH3"),DMXFixtureMap[2]); - printSetIndex(dest,PSTR("CH4"),DMXFixtureMap[3]); - printSetIndex(dest,PSTR("CH5"),DMXFixtureMap[4]); - printSetIndex(dest,PSTR("CH6"),DMXFixtureMap[5]); - printSetIndex(dest,PSTR("CH7"),DMXFixtureMap[6]); - printSetIndex(dest,PSTR("CH8"),DMXFixtureMap[7]); - printSetIndex(dest,PSTR("CH9"),DMXFixtureMap[8]); - printSetIndex(dest,PSTR("CH10"),DMXFixtureMap[9]); - printSetIndex(dest,PSTR("CH11"),DMXFixtureMap[10]); - printSetIndex(dest,PSTR("CH12"),DMXFixtureMap[11]); - printSetIndex(dest,PSTR("CH13"),DMXFixtureMap[12]); - printSetIndex(dest,PSTR("CH14"),DMXFixtureMap[13]); - printSetIndex(dest,PSTR("CH15"),DMXFixtureMap[14]); + printSetFormValue(dest,PSTR("PU"),e131ProxyUniverse); + + printSetFormValue(dest,PSTR("CN"),DMXChannels); + printSetFormValue(dest,PSTR("CG"),DMXGap); + printSetFormValue(dest,PSTR("CS"),DMXStart); + printSetFormValue(dest,PSTR("SL"),DMXStartLED); + + printSetFormIndex(dest,PSTR("CH1"),DMXFixtureMap[0]); + printSetFormIndex(dest,PSTR("CH2"),DMXFixtureMap[1]); + printSetFormIndex(dest,PSTR("CH3"),DMXFixtureMap[2]); + printSetFormIndex(dest,PSTR("CH4"),DMXFixtureMap[3]); + printSetFormIndex(dest,PSTR("CH5"),DMXFixtureMap[4]); + printSetFormIndex(dest,PSTR("CH6"),DMXFixtureMap[5]); + printSetFormIndex(dest,PSTR("CH7"),DMXFixtureMap[6]); + printSetFormIndex(dest,PSTR("CH8"),DMXFixtureMap[7]); + printSetFormIndex(dest,PSTR("CH9"),DMXFixtureMap[8]); + printSetFormIndex(dest,PSTR("CH10"),DMXFixtureMap[9]); + printSetFormIndex(dest,PSTR("CH11"),DMXFixtureMap[10]); + printSetFormIndex(dest,PSTR("CH12"),DMXFixtureMap[11]); + printSetFormIndex(dest,PSTR("CH13"),DMXFixtureMap[12]); + printSetFormIndex(dest,PSTR("CH14"),DMXFixtureMap[13]); + printSetFormIndex(dest,PSTR("CH15"),DMXFixtureMap[14]); } #endif @@ -613,11 +613,11 @@ void getSettingsJS(byte subPage, Print& dest) { appendGPIOinfo(dest); dest.printf_P(PSTR("numM=%d;"), usermods.getModCount()); - printSetValue(dest,PSTR("SDA"),i2c_sda); - printSetValue(dest,PSTR("SCL"),i2c_scl); - printSetValue(dest,PSTR("MOSI"),spi_mosi); - printSetValue(dest,PSTR("MISO"),spi_miso); - printSetValue(dest,PSTR("SCLK"),spi_sclk); + printSetFormValue(dest,PSTR("SDA"),i2c_sda); + printSetFormValue(dest,PSTR("SCL"),i2c_scl); + printSetFormValue(dest,PSTR("MOSI"),spi_mosi); + printSetFormValue(dest,PSTR("MISO"),spi_miso); + printSetFormValue(dest,PSTR("SCLK"),spi_sclk); dest.printf_P(PSTR("addInfo('SDA','%d');" "addInfo('SCL','%d');" "addInfo('MOSI','%d');" @@ -641,21 +641,21 @@ void getSettingsJS(byte subPage, Print& dest) #endif VERSION); - printSetMessage(dest,PSTR("(\"sip\")[0]"),tmp_buf); + printSetClassElementHTML(dest,PSTR("sip"),0,tmp_buf); } if (subPage == SUBPAGE_2D) // 2D matrices { - printSetValue(dest,PSTR("SOMP"),strip.isMatrix); + printSetFormValue(dest,PSTR("SOMP"),strip.isMatrix); #ifndef WLED_DISABLE_2D - dest.print(F("maxPanels=")); dest.print(WLED_MAX_PANELS); dest.print(F(";")); + dest.printf_P(PSTR("maxPanels=%d;"),WLED_MAX_PANELS); dest.print(F("resetPanels();")); if (strip.isMatrix) { if(strip.panels>0){ - printSetValue(dest,PSTR("PW"),strip.panel[0].width); //Set generator Width and Height to first panel size for convenience - printSetValue(dest,PSTR("PH"),strip.panel[0].height); + printSetFormValue(dest,PSTR("PW"),strip.panel[0].width); //Set generator Width and Height to first panel size for convenience + printSetFormValue(dest,PSTR("PH"),strip.panel[0].height); } - printSetValue(dest,PSTR("MPC"),strip.panels); + printSetFormValue(dest,PSTR("MPC"),strip.panels); // panels for (unsigned i=0; i Date: Wed, 18 Sep 2024 19:19:40 -0400 Subject: [PATCH 085/145] Rename destination for getSettingsJS Use a name that makes it a bit clearer what the output is. The new name is applied consistently through most uses. Usermods are not yet updated. --- wled00/fcn_declare.h | 12 +- wled00/um_manager.cpp | 4 +- wled00/util.cpp | 24 +- wled00/xml.cpp | 574 +++++++++++++++++++++--------------------- 4 files changed, 307 insertions(+), 307 deletions(-) diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 29db8ea765..5712c7f8d2 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -302,7 +302,7 @@ class Usermod { virtual bool handleButton(uint8_t b) { return false; } // button overrides are possible here virtual bool getUMData(um_data_t **data) { if (data) *data = nullptr; return false; }; // usermod data exchange [see examples for audio effects] virtual void connected() {} // called when WiFi is (re)connected - virtual void appendConfigData(Print&); // helper function called from usermod settings page to add metadata for entry fields + virtual void appendConfigData(Print& settingsScript); // helper function called from usermod settings page to add metadata for entry fields virtual void addToJsonState(JsonObject& obj) {} // add JSON objects for WLED state virtual void addToJsonInfo(JsonObject& obj) {} // add JSON objects for UI Info page virtual void readFromJsonState(JsonObject& obj) {} // process JSON messages received from web server @@ -372,11 +372,11 @@ void parseNumber(const char* str, byte* val, byte minv=0, byte maxv=255); bool getVal(JsonVariant elem, byte* val, byte minv=0, byte maxv=255); bool getBoolVal(JsonVariant elem, bool dflt); bool updateVal(const char* req, const char* key, byte* val, byte minv=0, byte maxv=255); -size_t printSetFormCheckbox(Print& dest, const char* key, int val); -size_t printSetFormValue(Print& dest, const char* key, int val); -size_t printSetFormValue(Print& dest, const char* key, const char* val); -size_t printSetFormIndex(Print& dest, const char* key, int index); -size_t printSetClassElementHTML(Print& dest, const char* key, const int index, const char* val); +size_t printSetFormCheckbox(Print& settingsScript, const char* key, int val); +size_t printSetFormValue(Print& settingsScript, const char* key, int val); +size_t printSetFormValue(Print& settingsScript, const char* key, const char* val); +size_t printSetFormIndex(Print& settingsScript, const char* key, int index); +size_t printSetClassElementHTML(Print& settingsScript, const char* key, const int index, const char* val); void prepareHostname(char* hostname); bool isAsterisksOnly(const char* str, byte maxLen); bool requestJSONBufferLock(uint8_t module=255); diff --git a/wled00/um_manager.cpp b/wled00/um_manager.cpp index 5307d26f6b..ff3b627894 100644 --- a/wled00/um_manager.cpp +++ b/wled00/um_manager.cpp @@ -73,9 +73,9 @@ bool UsermodManager::add(Usermod* um) /* Usermod v2 interface shim for oappend */ Print* Usermod::oappend_shim = nullptr; -void Usermod::appendConfigData(Print& p) { +void Usermod::appendConfigData(Print& settingsScript) { assert(!oappend_shim); - oappend_shim = &p; + oappend_shim = &settingsScript; this->appendConfigData(); oappend_shim = nullptr; } diff --git a/wled00/util.cpp b/wled00/util.cpp index 07190e37cc..0b78a46469 100644 --- a/wled00/util.cpp +++ b/wled00/util.cpp @@ -87,26 +87,26 @@ bool updateVal(const char* req, const char* key, byte* val, byte minv, byte maxv return true; } -static size_t printSetFormInput(Print& dest, const char* key, const char* selector, int value) { - return dest.printf_P(PSTR("d.Sf.%s.%s=%d;"), key, selector, value); +static size_t printSetFormInput(Print& settingsScript, const char* key, const char* selector, int value) { + return settingsScript.printf_P(PSTR("d.Sf.%s.%s=%d;"), key, selector, value); } -size_t printSetFormCheckbox(Print& dest, const char* key, int val) { - return printSetFormInput(dest, key, PSTR("checked"), val); +size_t printSetFormCheckbox(Print& settingsScript, const char* key, int val) { + return printSetFormInput(settingsScript, key, PSTR("checked"), val); } -size_t printSetFormValue(Print& dest, const char* key, int val) { - return printSetFormInput(dest, key, PSTR("value"), val); +size_t printSetFormValue(Print& settingsScript, const char* key, int val) { + return printSetFormInput(settingsScript, key, PSTR("value"), val); } -size_t printSetFormIndex(Print& dest, const char* key, int index) { - return printSetFormInput(dest, key, PSTR("selectedIndex"), index); +size_t printSetFormIndex(Print& settingsScript, const char* key, int index) { + return printSetFormInput(settingsScript, key, PSTR("selectedIndex"), index); } -size_t printSetFormValue(Print& dest, const char* key, const char* val) { - return dest.printf_P(PSTR("d.Sf.%s.value=\"%s\";"),key,val); +size_t printSetFormValue(Print& settingsScript, const char* key, const char* val) { + return settingsScript.printf_P(PSTR("d.Sf.%s.value=\"%s\";"),key,val); } -size_t printSetClassElementHTML(Print& dest, const char* key, const int index, const char* val) { - return dest.printf_P(PSTR("d.getElementsByClassName(\"%s\")[%d].innerHTML=\"%s\";"), key, index, val); +size_t printSetClassElementHTML(Print& settingsScript, const char* key, const int index, const char* val) { + return settingsScript.printf_P(PSTR("d.getElementsByClassName(\"%s\")[%d].innerHTML=\"%s\";"), key, index, val); } diff --git a/wled00/xml.cpp b/wled00/xml.cpp index 8cbd51cde9..5619a0ee33 100644 --- a/wled00/xml.cpp +++ b/wled00/xml.cpp @@ -26,19 +26,19 @@ void XML_response(Print& dest) ); } -static void extractPin(Print& dest, JsonObject &obj, const char *key) { +static void extractPin(Print& settingsScript, JsonObject &obj, const char *key) { if (obj[key].is()) { JsonArray pins = obj[key].as(); for (JsonVariant pv : pins) { - if (pv.as() > -1) { dest.print(","); dest.print(pv.as()); } + if (pv.as() > -1) { settingsScript.print(","); settingsScript.print(pv.as()); } } } else { - if (obj[key].as() > -1) { dest.print(","); dest.print(obj[key].as()); } + if (obj[key].as() > -1) { settingsScript.print(","); settingsScript.print(obj[key].as()); } } } -// dest.print used pins by scanning JsonObject (1 level deep) -void fillUMPins(Print& dest, JsonObject &mods) +// print used pins by scanning JsonObject (1 level deep) +static void fillUMPins(Print& settingsScript, JsonObject &mods) { for (JsonPair kv : mods) { // kv.key() is usermod name or subobject key @@ -47,7 +47,7 @@ void fillUMPins(Print& dest, JsonObject &mods) if (!obj.isNull()) { // element is an JsonObject if (!obj["pin"].isNull()) { - extractPin(dest, obj, "pin"); + extractPin(settingsScript, obj, "pin"); } else { // scan keys (just one level deep as is possible with usermods) for (JsonPair so : obj) { @@ -56,7 +56,7 @@ void fillUMPins(Print& dest, JsonObject &mods) // we found a key containing "pin" substring if (strlen(strstr(key, "pin")) == 3) { // and it is at the end, we found another pin - extractPin(dest, obj, key); + extractPin(settingsScript, obj, key); continue; } } @@ -64,7 +64,7 @@ void fillUMPins(Print& dest, JsonObject &mods) JsonObject subObj = obj[so.key()]; if (!subObj["pin"].isNull()) { // get pins from subobject - extractPin(dest, subObj, "pin"); + extractPin(settingsScript, subObj, "pin"); } } } @@ -72,81 +72,81 @@ void fillUMPins(Print& dest, JsonObject &mods) } } -void appendGPIOinfo(Print& dest) { - dest.print(F("d.um_p=[-1")); // has to have 1 element +void appendGPIOinfo(Print& settingsScript) { + settingsScript.print(F("d.um_p=[-1")); // has to have 1 element if (i2c_sda > -1 && i2c_scl > -1) { - dest.printf_P(PSTR(",%d,%d"), i2c_sda, i2c_scl); + settingsScript.printf_P(PSTR(",%d,%d"), i2c_sda, i2c_scl); } if (spi_mosi > -1 && spi_sclk > -1) { - dest.printf_P(PSTR(",%d,%d"), spi_mosi, spi_sclk); + settingsScript.printf_P(PSTR(",%d,%d"), spi_mosi, spi_sclk); } // usermod pin reservations will become unnecessary when settings pages will read cfg.json directly if (requestJSONBufferLock(6)) { // if we can't allocate JSON buffer ignore usermod pins JsonObject mods = pDoc->createNestedObject(F("um")); usermods.addToConfig(mods); - if (!mods.isNull()) fillUMPins(dest, mods); + if (!mods.isNull()) fillUMPins(settingsScript, mods); releaseJSONBufferLock(); } - dest.print(F("];")); + settingsScript.print(F("];")); // add reserved (unusable) pins - dest.print(SET_F("d.rsvd=[")); + settingsScript.print(SET_F("d.rsvd=[")); for (unsigned i = 0; i < WLED_NUM_PINS; i++) { if (!pinManager.isPinOk(i, false)) { // include readonly pins - dest.print(i); dest.print(","); + settingsScript.print(i); settingsScript.print(","); } } #ifdef WLED_ENABLE_DMX - dest.print(SET_F("2,")); // DMX hardcoded pin + settingsScript.print(SET_F("2,")); // DMX hardcoded pin #endif #if defined(WLED_DEBUG) && !defined(WLED_DEBUG_HOST) - dest.printf_P(PSTR(",%d"),hardwareTX); // debug output (TX) pin + settingsScript.printf_P(PSTR(",%d"),hardwareTX); // debug output (TX) pin #endif //Note: Using pin 3 (RX) disables Adalight / Serial JSON #ifdef WLED_USE_ETHERNET if (ethernetType != WLED_ETH_NONE && ethernetType < WLED_NUM_ETH_TYPES) { - for (unsigned p=0; p=0) { dest.printf(",%d", ethernetBoards[ethernetType].eth_power); } - if (ethernetBoards[ethernetType].eth_mdc>=0) { dest.printf(",%d", ethernetBoards[ethernetType].eth_mdc); } - if (ethernetBoards[ethernetType].eth_mdio>=0) { dest.printf(",%d", ethernetBoards[ethernetType].eth_mdio); } + for (unsigned p=0; p=0) { settingsScript.printf(",%d", ethernetBoards[ethernetType].eth_power); } + if (ethernetBoards[ethernetType].eth_mdc>=0) { settingsScript.printf(",%d", ethernetBoards[ethernetType].eth_mdc); } + if (ethernetBoards[ethernetType].eth_mdio>=0) { settingsScript.printf(",%d", ethernetBoards[ethernetType].eth_mdio); } switch (ethernetBoards[ethernetType].eth_clk_mode) { case ETH_CLOCK_GPIO0_IN: case ETH_CLOCK_GPIO0_OUT: - dest.print(SET_F("0")); + settingsScript.print(SET_F("0")); break; case ETH_CLOCK_GPIO16_OUT: - dest.print(SET_F("16")); + settingsScript.print(SET_F("16")); break; case ETH_CLOCK_GPIO17_OUT: - dest.print(SET_F("17")); + settingsScript.print(SET_F("17")); break; } } #endif - dest.print(SET_F("];")); // rsvd + settingsScript.print(SET_F("];")); // rsvd // add info for read-only GPIO - dest.print(SET_F("d.ro_gpio=[")); + settingsScript.print(SET_F("d.ro_gpio=[")); bool firstPin = true; for (unsigned i = 0; i < WLED_NUM_PINS; i++) { if (pinManager.isReadOnlyPin(i)) { // No comma before the first pin - if (!firstPin) dest.print(SET_F(",")); - dest.print(i); + if (!firstPin) settingsScript.print(SET_F(",")); + settingsScript.print(i); firstPin = false; } } - dest.print(SET_F("];")); + settingsScript.print(SET_F("];")); // add info about max. # of pins - dest.print(SET_F("d.max_gpio=")); - dest.print(WLED_NUM_PINS); - dest.print(SET_F(";")); + settingsScript.print(SET_F("d.max_gpio=")); + settingsScript.print(WLED_NUM_PINS); + settingsScript.print(SET_F(";")); } //get values for settings form in javascript -void getSettingsJS(byte subPage, Print& dest) +void getSettingsJS(byte subPage, Print& settingsScript) { //0: menu 1: wifi 2: leds 3: ui 4: sync 5: time 6: sec DEBUG_PRINTF_P(PSTR("settings resp %u\n"), (unsigned)subPage); @@ -156,23 +156,23 @@ void getSettingsJS(byte subPage, Print& dest) if (subPage == SUBPAGE_MENU) { #ifdef WLED_DISABLE_2D // include only if 2D is not compiled in - dest.print(F("gId('2dbtn').style.display='none';")); + settingsScript.print(F("gId('2dbtn').style.display='none';")); #endif #ifdef WLED_ENABLE_DMX // include only if DMX is enabled - dest.print(F("gId('dmxbtn').style.display='';")); + settingsScript.print(F("gId('dmxbtn').style.display='';")); #endif } if (subPage == SUBPAGE_WIFI) { size_t l; - dest.printf_P(PSTR("resetWiFi(%d);"), WLED_MAX_WIFI_COUNT); + settingsScript.printf_P(PSTR("resetWiFi(%d);"), WLED_MAX_WIFI_COUNT); for (size_t n = 0; n < multiWiFi.size(); n++) { l = strlen(multiWiFi[n].clientPass); char fpass[l+1]; //fill password field with *** fpass[l] = 0; memset(fpass,'*',l); - dest.printf_P(PSTR("addWiFi(\"%s\",\",%s\",0x%X,0x%X,0x%X);"), + settingsScript.printf_P(PSTR("addWiFi(\"%s\",\",%s\",0x%X,0x%X,0x%X);"), multiWiFi[n].clientSSID, fpass, (uint32_t) multiWiFi[n].staticIP, // explicit cast required as this is a struct @@ -180,44 +180,44 @@ void getSettingsJS(byte subPage, Print& dest) (uint32_t) multiWiFi[n].staticSN); } - printSetFormValue(dest,PSTR("D0"),dnsAddress[0]); - printSetFormValue(dest,PSTR("D1"),dnsAddress[1]); - printSetFormValue(dest,PSTR("D2"),dnsAddress[2]); - printSetFormValue(dest,PSTR("D3"),dnsAddress[3]); + printSetFormValue(settingsScript,PSTR("D0"),dnsAddress[0]); + printSetFormValue(settingsScript,PSTR("D1"),dnsAddress[1]); + printSetFormValue(settingsScript,PSTR("D2"),dnsAddress[2]); + printSetFormValue(settingsScript,PSTR("D3"),dnsAddress[3]); - printSetFormValue(dest,PSTR("CM"),cmDNS); - printSetFormIndex(dest,PSTR("AB"),apBehavior); - printSetFormValue(dest,PSTR("AS"),apSSID); - printSetFormCheckbox(dest,PSTR("AH"),apHide); + printSetFormValue(settingsScript,PSTR("CM"),cmDNS); + printSetFormIndex(settingsScript,PSTR("AB"),apBehavior); + printSetFormValue(settingsScript,PSTR("AS"),apSSID); + printSetFormCheckbox(settingsScript,PSTR("AH"),apHide); l = strlen(apPass); char fapass[l+1]; //fill password field with *** fapass[l] = 0; memset(fapass,'*',l); - printSetFormValue(dest,PSTR("AP"),fapass); + printSetFormValue(settingsScript,PSTR("AP"),fapass); - printSetFormValue(dest,PSTR("AC"),apChannel); + printSetFormValue(settingsScript,PSTR("AC"),apChannel); #ifdef ARDUINO_ARCH_ESP32 - printSetFormValue(dest,PSTR("TX"),txPower); + printSetFormValue(settingsScript,PSTR("TX"),txPower); #else - dest.print(F("gId('tx').style.display='none';")); + settingsScript.print(F("gId('tx').style.display='none';")); #endif - printSetFormCheckbox(dest,PSTR("FG"),force802_3g); - printSetFormCheckbox(dest,PSTR("WS"),noWifiSleep); + printSetFormCheckbox(settingsScript,PSTR("FG"),force802_3g); + printSetFormCheckbox(settingsScript,PSTR("WS"),noWifiSleep); #ifndef WLED_DISABLE_ESPNOW - printSetFormCheckbox(dest,PSTR("RE"),enableESPNow); - printSetFormValue(dest,PSTR("RMAC"),linked_remote); + printSetFormCheckbox(settingsScript,PSTR("RE"),enableESPNow); + printSetFormValue(settingsScript,PSTR("RMAC"),linked_remote); #else //hide remote settings if not compiled - dest.print(F("toggle('ESPNOW');")); // hide ESP-NOW setting + settingsScript.print(F("toggle('ESPNOW');")); // hide ESP-NOW setting #endif #ifdef WLED_USE_ETHERNET - printSetFormValue(dest,PSTR("ETH"),ethernetType); + printSetFormValue(settingsScript,PSTR("ETH"),ethernetType); #else //hide ethernet setting if not compiled in - dest.print(F("gId('ethd').style.display='none';")); + settingsScript.print(F("gId('ethd').style.display='none';")); #endif if (Network.isConnected()) //is connected @@ -229,10 +229,10 @@ void getSettingsJS(byte subPage, Print& dest) #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) if (Network.isEthernet()) strcat_P(s ,SET_F(" (Ethernet)")); #endif - printSetClassElementHTML(dest,PSTR("sip"),0,s); + printSetClassElementHTML(settingsScript,PSTR("sip"),0,s); } else { - printSetClassElementHTML(dest,PSTR("sip"),0,(char*)F("Not connected")); + printSetClassElementHTML(settingsScript,PSTR("sip"),0,(char*)F("Not connected")); } if (WiFi.softAPIP()[0] != 0) //is active @@ -240,19 +240,19 @@ void getSettingsJS(byte subPage, Print& dest) char s[16]; IPAddress apIP = WiFi.softAPIP(); sprintf(s, "%d.%d.%d.%d", apIP[0], apIP[1], apIP[2], apIP[3]); - printSetClassElementHTML(dest,PSTR("sip"),1,s); + printSetClassElementHTML(settingsScript,PSTR("sip"),1,s); } else { - printSetClassElementHTML(dest,PSTR("sip"),1,(char*)F("Not active")); + printSetClassElementHTML(settingsScript,PSTR("sip"),1,(char*)F("Not active")); } #ifndef WLED_DISABLE_ESPNOW if (strlen(last_signal_src) > 0) { //Have seen an ESP-NOW Remote - printSetClassElementHTML(dest,PSTR("rlid"),0,last_signal_src); + printSetClassElementHTML(settingsScript,PSTR("rlid"),0,last_signal_src); } else if (!enableESPNow) { - printSetClassElementHTML(dest,PSTR("rlid"),0,(char*)F("(Enable ESP-NOW to listen)")); + printSetClassElementHTML(settingsScript,PSTR("rlid"),0,(char*)F("(Enable ESP-NOW to listen)")); } else { - printSetClassElementHTML(dest,PSTR("rlid"),0,(char*)F("None")); + printSetClassElementHTML(settingsScript,PSTR("rlid"),0,(char*)F("None")); } #endif } @@ -261,12 +261,12 @@ void getSettingsJS(byte subPage, Print& dest) { char nS[32]; - appendGPIOinfo(dest); + appendGPIOinfo(settingsScript); - dest.print(SET_F("d.ledTypes=")); dest.print(BusManager::getLEDTypesJSONString().c_str()); dest.print(";"); + settingsScript.print(SET_F("d.ledTypes=")); settingsScript.print(BusManager::getLEDTypesJSONString().c_str()); settingsScript.print(";"); // set limits - dest.printf_P(PSTR("bLimits(%d,%d,%d,%d,%d,%d,%d,%d);"), + settingsScript.printf_P(PSTR("bLimits(%d,%d,%d,%d,%d,%d,%d,%d);"), WLED_MAX_BUSSES, WLED_MIN_VIRTUAL_BUSSES, MAX_LEDS_PER_BUS, @@ -277,14 +277,14 @@ void getSettingsJS(byte subPage, Print& dest) WLED_MAX_ANALOG_CHANNELS ); - printSetFormCheckbox(dest,PSTR("MS"),strip.autoSegments); - printSetFormCheckbox(dest,PSTR("CCT"),strip.correctWB); - printSetFormCheckbox(dest,PSTR("IC"),cctICused); - printSetFormCheckbox(dest,PSTR("CR"),strip.cctFromRgb); - printSetFormValue(dest,PSTR("CB"),strip.cctBlending); - printSetFormValue(dest,PSTR("FR"),strip.getTargetFps()); - printSetFormValue(dest,PSTR("AW"),Bus::getGlobalAWMode()); - printSetFormCheckbox(dest,PSTR("LD"),useGlobalLedBuffer); + printSetFormCheckbox(settingsScript,PSTR("MS"),strip.autoSegments); + printSetFormCheckbox(settingsScript,PSTR("CCT"),strip.correctWB); + printSetFormCheckbox(settingsScript,PSTR("IC"),cctICused); + printSetFormCheckbox(settingsScript,PSTR("CR"),strip.cctFromRgb); + printSetFormValue(settingsScript,PSTR("CB"),strip.cctBlending); + printSetFormValue(settingsScript,PSTR("FR"),strip.getTargetFps()); + printSetFormValue(settingsScript,PSTR("AW"),Bus::getGlobalAWMode()); + printSetFormCheckbox(settingsScript,PSTR("LD"),useGlobalLedBuffer); unsigned sumMa = 0; for (int s = 0; s < BusManager::getNumBusses(); s++) { @@ -304,22 +304,22 @@ void getSettingsJS(byte subPage, Print& dest) char sp[4] = "SP"; sp[2] = offset+s; sp[3] = 0; //bus clock speed char la[4] = "LA"; la[2] = offset+s; la[3] = 0; //LED current char ma[4] = "MA"; ma[2] = offset+s; ma[3] = 0; //max per-port PSU current - dest.print(F("addLEDs(1);")); + settingsScript.print(F("addLEDs(1);")); uint8_t pins[5]; int nPins = bus->getPins(pins); for (int i = 0; i < nPins; i++) { lp[1] = offset+i; - if (pinManager.isPinOk(pins[i]) || bus->isVirtual()) printSetFormValue(dest,lp,pins[i]); + if (pinManager.isPinOk(pins[i]) || bus->isVirtual()) printSetFormValue(settingsScript,lp,pins[i]); } - printSetFormValue(dest,lc,bus->getLength()); - printSetFormValue(dest,lt,bus->getType()); - printSetFormValue(dest,co,bus->getColorOrder() & 0x0F); - printSetFormValue(dest,ls,bus->getStart()); - printSetFormCheckbox(dest,cv,bus->isReversed()); - printSetFormValue(dest,sl,bus->skippedLeds()); - printSetFormCheckbox(dest,rf,bus->isOffRefreshRequired()); - printSetFormValue(dest,aw,bus->getAutoWhiteMode()); - printSetFormValue(dest,wo,bus->getColorOrder() >> 4); + printSetFormValue(settingsScript,lc,bus->getLength()); + printSetFormValue(settingsScript,lt,bus->getType()); + printSetFormValue(settingsScript,co,bus->getColorOrder() & 0x0F); + printSetFormValue(settingsScript,ls,bus->getStart()); + printSetFormCheckbox(settingsScript,cv,bus->isReversed()); + printSetFormValue(settingsScript,sl,bus->skippedLeds()); + printSetFormCheckbox(settingsScript,rf,bus->isOffRefreshRequired()); + printSetFormValue(settingsScript,aw,bus->getAutoWhiteMode()); + printSetFormValue(settingsScript,wo,bus->getColorOrder() >> 4); unsigned speed = bus->getFrequency(); if (bus->isPWM()) { switch (speed) { @@ -340,148 +340,148 @@ void getSettingsJS(byte subPage, Print& dest) case 20000 : speed = 4; break; } } - printSetFormValue(dest,sp,speed); - printSetFormValue(dest,la,bus->getLEDCurrent()); - printSetFormValue(dest,ma,bus->getMaxCurrent()); + printSetFormValue(settingsScript,sp,speed); + printSetFormValue(settingsScript,la,bus->getLEDCurrent()); + printSetFormValue(settingsScript,ma,bus->getMaxCurrent()); sumMa += bus->getMaxCurrent(); } - printSetFormValue(dest,PSTR("MA"),BusManager::ablMilliampsMax() ? BusManager::ablMilliampsMax() : sumMa); - printSetFormCheckbox(dest,PSTR("ABL"),BusManager::ablMilliampsMax() || sumMa > 0); - printSetFormCheckbox(dest,PSTR("PPL"),!BusManager::ablMilliampsMax() && sumMa > 0); + printSetFormValue(settingsScript,PSTR("MA"),BusManager::ablMilliampsMax() ? BusManager::ablMilliampsMax() : sumMa); + printSetFormCheckbox(settingsScript,PSTR("ABL"),BusManager::ablMilliampsMax() || sumMa > 0); + printSetFormCheckbox(settingsScript,PSTR("PPL"),!BusManager::ablMilliampsMax() && sumMa > 0); - dest.printf_P(PSTR("resetCOM(%d);"), WLED_MAX_COLOR_ORDER_MAPPINGS); + settingsScript.printf_P(PSTR("resetCOM(%d);"), WLED_MAX_COLOR_ORDER_MAPPINGS); const ColorOrderMap& com = BusManager::getColorOrderMap(); for (int s = 0; s < com.count(); s++) { const ColorOrderMapEntry* entry = com.get(s); if (entry == nullptr) break; - dest.printf_P(PSTR("addCOM(%d,%d,%d);"), entry->start, entry->len, entry->colorOrder); + settingsScript.printf_P(PSTR("addCOM(%d,%d,%d);"), entry->start, entry->len, entry->colorOrder); } - printSetFormValue(dest,PSTR("CA"),briS); - - printSetFormCheckbox(dest,PSTR("BO"),turnOnAtBoot); - printSetFormValue(dest,PSTR("BP"),bootPreset); - - printSetFormCheckbox(dest,PSTR("GB"),gammaCorrectBri); - printSetFormCheckbox(dest,PSTR("GC"),gammaCorrectCol); - dtostrf(gammaCorrectVal,3,1,nS); printSetFormValue(dest,PSTR("GV"),nS); - printSetFormCheckbox(dest,PSTR("TF"),fadeTransition); - printSetFormCheckbox(dest,PSTR("EB"),modeBlending); - printSetFormValue(dest,PSTR("TD"),transitionDelayDefault); - printSetFormCheckbox(dest,PSTR("PF"),strip.paletteFade); - printSetFormValue(dest,PSTR("TP"),randomPaletteChangeTime); - printSetFormCheckbox(dest,PSTR("TH"),useHarmonicRandomPalette); - printSetFormValue(dest,PSTR("BF"),briMultiplier); - printSetFormValue(dest,PSTR("TB"),nightlightTargetBri); - printSetFormValue(dest,PSTR("TL"),nightlightDelayMinsDefault); - printSetFormValue(dest,PSTR("TW"),nightlightMode); - printSetFormIndex(dest,PSTR("PB"),strip.paletteBlend); - printSetFormValue(dest,PSTR("RL"),rlyPin); - printSetFormCheckbox(dest,PSTR("RM"),rlyMde); - printSetFormCheckbox(dest,PSTR("RO"),rlyOpenDrain); + printSetFormValue(settingsScript,PSTR("CA"),briS); + + printSetFormCheckbox(settingsScript,PSTR("BO"),turnOnAtBoot); + printSetFormValue(settingsScript,PSTR("BP"),bootPreset); + + printSetFormCheckbox(settingsScript,PSTR("GB"),gammaCorrectBri); + printSetFormCheckbox(settingsScript,PSTR("GC"),gammaCorrectCol); + dtostrf(gammaCorrectVal,3,1,nS); printSetFormValue(settingsScript,PSTR("GV"),nS); + printSetFormCheckbox(settingsScript,PSTR("TF"),fadeTransition); + printSetFormCheckbox(settingsScript,PSTR("EB"),modeBlending); + printSetFormValue(settingsScript,PSTR("TD"),transitionDelayDefault); + printSetFormCheckbox(settingsScript,PSTR("PF"),strip.paletteFade); + printSetFormValue(settingsScript,PSTR("TP"),randomPaletteChangeTime); + printSetFormCheckbox(settingsScript,PSTR("TH"),useHarmonicRandomPalette); + printSetFormValue(settingsScript,PSTR("BF"),briMultiplier); + printSetFormValue(settingsScript,PSTR("TB"),nightlightTargetBri); + printSetFormValue(settingsScript,PSTR("TL"),nightlightDelayMinsDefault); + printSetFormValue(settingsScript,PSTR("TW"),nightlightMode); + printSetFormIndex(settingsScript,PSTR("PB"),strip.paletteBlend); + printSetFormValue(settingsScript,PSTR("RL"),rlyPin); + printSetFormCheckbox(settingsScript,PSTR("RM"),rlyMde); + printSetFormCheckbox(settingsScript,PSTR("RO"),rlyOpenDrain); for (int i = 0; i < WLED_MAX_BUTTONS; i++) { - dest.printf_P(PSTR("addBtn(%d,%d,%d);"), i, btnPin[i], buttonType[i]); + settingsScript.printf_P(PSTR("addBtn(%d,%d,%d);"), i, btnPin[i], buttonType[i]); } - printSetFormCheckbox(dest,PSTR("IP"),disablePullUp); - printSetFormValue(dest,PSTR("TT"),touchThreshold); + printSetFormCheckbox(settingsScript,PSTR("IP"),disablePullUp); + printSetFormValue(settingsScript,PSTR("TT"),touchThreshold); #ifndef WLED_DISABLE_INFRARED - printSetFormValue(dest,PSTR("IR"),irPin); - printSetFormValue(dest,PSTR("IT"),irEnabled); + printSetFormValue(settingsScript,PSTR("IR"),irPin); + printSetFormValue(settingsScript,PSTR("IT"),irEnabled); #endif - printSetFormCheckbox(dest,PSTR("MSO"),!irApplyToAllSelected); + printSetFormCheckbox(settingsScript,PSTR("MSO"),!irApplyToAllSelected); } if (subPage == SUBPAGE_UI) { - printSetFormValue(dest,PSTR("DS"),serverDescription); - printSetFormCheckbox(dest,PSTR("SU"),simplifiedUI); + printSetFormValue(settingsScript,PSTR("DS"),serverDescription); + printSetFormCheckbox(settingsScript,PSTR("SU"),simplifiedUI); } if (subPage == SUBPAGE_SYNC) { [[maybe_unused]] char nS[32]; - printSetFormValue(dest,PSTR("UP"),udpPort); - printSetFormValue(dest,PSTR("U2"),udpPort2); + printSetFormValue(settingsScript,PSTR("UP"),udpPort); + printSetFormValue(settingsScript,PSTR("U2"),udpPort2); #ifndef WLED_DISABLE_ESPNOW - if (enableESPNow) printSetFormCheckbox(dest,PSTR("EN"),useESPNowSync); - else dest.print(F("toggle('ESPNOW');")); // hide ESP-NOW setting + if (enableESPNow) printSetFormCheckbox(settingsScript,PSTR("EN"),useESPNowSync); + else settingsScript.print(F("toggle('ESPNOW');")); // hide ESP-NOW setting #else - dest.print(F("toggle('ESPNOW');")); // hide ESP-NOW setting + settingsScript.print(F("toggle('ESPNOW');")); // hide ESP-NOW setting #endif - printSetFormValue(dest,PSTR("GS"),syncGroups); - printSetFormValue(dest,PSTR("GR"),receiveGroups); - - printSetFormCheckbox(dest,PSTR("RB"),receiveNotificationBrightness); - printSetFormCheckbox(dest,PSTR("RC"),receiveNotificationColor); - printSetFormCheckbox(dest,PSTR("RX"),receiveNotificationEffects); - printSetFormCheckbox(dest,PSTR("RP"),receiveNotificationPalette); - printSetFormCheckbox(dest,PSTR("SO"),receiveSegmentOptions); - printSetFormCheckbox(dest,PSTR("SG"),receiveSegmentBounds); - printSetFormCheckbox(dest,PSTR("SS"),sendNotifications); - printSetFormCheckbox(dest,PSTR("SD"),notifyDirect); - printSetFormCheckbox(dest,PSTR("SB"),notifyButton); - printSetFormCheckbox(dest,PSTR("SH"),notifyHue); - printSetFormValue(dest,PSTR("UR"),udpNumRetries); - - printSetFormCheckbox(dest,PSTR("NL"),nodeListEnabled); - printSetFormCheckbox(dest,PSTR("NB"),nodeBroadcastEnabled); - - printSetFormCheckbox(dest,PSTR("RD"),receiveDirect); - printSetFormCheckbox(dest,PSTR("MO"),useMainSegmentOnly); - printSetFormCheckbox(dest,PSTR("RLM"),realtimeRespectLedMaps); - printSetFormValue(dest,PSTR("EP"),e131Port); - printSetFormCheckbox(dest,PSTR("ES"),e131SkipOutOfSequence); - printSetFormCheckbox(dest,PSTR("EM"),e131Multicast); - printSetFormValue(dest,PSTR("EU"),e131Universe); - printSetFormValue(dest,PSTR("DA"),DMXAddress); - printSetFormValue(dest,PSTR("XX"),DMXSegmentSpacing); - printSetFormValue(dest,PSTR("PY"),e131Priority); - printSetFormValue(dest,PSTR("DM"),DMXMode); - printSetFormValue(dest,PSTR("ET"),realtimeTimeoutMs); - printSetFormCheckbox(dest,PSTR("FB"),arlsForceMaxBri); - printSetFormCheckbox(dest,PSTR("RG"),arlsDisableGammaCorrection); - printSetFormValue(dest,PSTR("WO"),arlsOffset); + printSetFormValue(settingsScript,PSTR("GS"),syncGroups); + printSetFormValue(settingsScript,PSTR("GR"),receiveGroups); + + printSetFormCheckbox(settingsScript,PSTR("RB"),receiveNotificationBrightness); + printSetFormCheckbox(settingsScript,PSTR("RC"),receiveNotificationColor); + printSetFormCheckbox(settingsScript,PSTR("RX"),receiveNotificationEffects); + printSetFormCheckbox(settingsScript,PSTR("RP"),receiveNotificationPalette); + printSetFormCheckbox(settingsScript,PSTR("SO"),receiveSegmentOptions); + printSetFormCheckbox(settingsScript,PSTR("SG"),receiveSegmentBounds); + printSetFormCheckbox(settingsScript,PSTR("SS"),sendNotifications); + printSetFormCheckbox(settingsScript,PSTR("SD"),notifyDirect); + printSetFormCheckbox(settingsScript,PSTR("SB"),notifyButton); + printSetFormCheckbox(settingsScript,PSTR("SH"),notifyHue); + printSetFormValue(settingsScript,PSTR("UR"),udpNumRetries); + + printSetFormCheckbox(settingsScript,PSTR("NL"),nodeListEnabled); + printSetFormCheckbox(settingsScript,PSTR("NB"),nodeBroadcastEnabled); + + printSetFormCheckbox(settingsScript,PSTR("RD"),receiveDirect); + printSetFormCheckbox(settingsScript,PSTR("MO"),useMainSegmentOnly); + printSetFormCheckbox(settingsScript,PSTR("RLM"),realtimeRespectLedMaps); + printSetFormValue(settingsScript,PSTR("EP"),e131Port); + printSetFormCheckbox(settingsScript,PSTR("ES"),e131SkipOutOfSequence); + printSetFormCheckbox(settingsScript,PSTR("EM"),e131Multicast); + printSetFormValue(settingsScript,PSTR("EU"),e131Universe); + printSetFormValue(settingsScript,PSTR("DA"),DMXAddress); + printSetFormValue(settingsScript,PSTR("XX"),DMXSegmentSpacing); + printSetFormValue(settingsScript,PSTR("PY"),e131Priority); + printSetFormValue(settingsScript,PSTR("DM"),DMXMode); + printSetFormValue(settingsScript,PSTR("ET"),realtimeTimeoutMs); + printSetFormCheckbox(settingsScript,PSTR("FB"),arlsForceMaxBri); + printSetFormCheckbox(settingsScript,PSTR("RG"),arlsDisableGammaCorrection); + printSetFormValue(settingsScript,PSTR("WO"),arlsOffset); #ifndef WLED_DISABLE_ALEXA - printSetFormCheckbox(dest,PSTR("AL"),alexaEnabled); - printSetFormValue(dest,PSTR("AI"),alexaInvocationName); - printSetFormCheckbox(dest,PSTR("SA"),notifyAlexa); - printSetFormValue(dest,PSTR("AP"),alexaNumPresets); + printSetFormCheckbox(settingsScript,PSTR("AL"),alexaEnabled); + printSetFormValue(settingsScript,PSTR("AI"),alexaInvocationName); + printSetFormCheckbox(settingsScript,PSTR("SA"),notifyAlexa); + printSetFormValue(settingsScript,PSTR("AP"),alexaNumPresets); #else - dest.print(F("toggle('Alexa');")); // hide Alexa settings + settingsScript.print(F("toggle('Alexa');")); // hide Alexa settings #endif #ifndef WLED_DISABLE_MQTT - printSetFormCheckbox(dest,PSTR("MQ"),mqttEnabled); - printSetFormValue(dest,PSTR("MS"),mqttServer); - printSetFormValue(dest,PSTR("MQPORT"),mqttPort); - printSetFormValue(dest,PSTR("MQUSER"),mqttUser); + printSetFormCheckbox(settingsScript,PSTR("MQ"),mqttEnabled); + printSetFormValue(settingsScript,PSTR("MS"),mqttServer); + printSetFormValue(settingsScript,PSTR("MQPORT"),mqttPort); + printSetFormValue(settingsScript,PSTR("MQUSER"),mqttUser); byte l = strlen(mqttPass); char fpass[l+1]; //fill password field with *** fpass[l] = 0; memset(fpass,'*',l); - printSetFormValue(dest,PSTR("MQPASS"),fpass); - printSetFormValue(dest,PSTR("MQCID"),mqttClientID); - printSetFormValue(dest,PSTR("MD"),mqttDeviceTopic); - printSetFormValue(dest,PSTR("MG"),mqttGroupTopic); - printSetFormCheckbox(dest,PSTR("BM"),buttonPublishMqtt); - printSetFormCheckbox(dest,PSTR("RT"),retainMqttMsg); - dest.printf_P(PSTR("d.Sf.MD.maxlength=%d;d.Sf.MG.maxlength=%d;d.Sf.MS.maxlength=%d;"), + printSetFormValue(settingsScript,PSTR("MQPASS"),fpass); + printSetFormValue(settingsScript,PSTR("MQCID"),mqttClientID); + printSetFormValue(settingsScript,PSTR("MD"),mqttDeviceTopic); + printSetFormValue(settingsScript,PSTR("MG"),mqttGroupTopic); + printSetFormCheckbox(settingsScript,PSTR("BM"),buttonPublishMqtt); + printSetFormCheckbox(settingsScript,PSTR("RT"),retainMqttMsg); + settingsScript.printf_P(PSTR("d.Sf.MD.maxlength=%d;d.Sf.MG.maxlength=%d;d.Sf.MS.maxlength=%d;"), MQTT_MAX_TOPIC_LEN, MQTT_MAX_TOPIC_LEN, MQTT_MAX_SERVER_LEN); #else - dest.print(F("toggle('MQTT');")); // hide MQTT settings + settingsScript.print(F("toggle('MQTT');")); // hide MQTT settings #endif #ifndef WLED_DISABLE_HUESYNC - printSetFormValue(dest,PSTR("H0"),hueIP[0]); - printSetFormValue(dest,PSTR("H1"),hueIP[1]); - printSetFormValue(dest,PSTR("H2"),hueIP[2]); - printSetFormValue(dest,PSTR("H3"),hueIP[3]); - printSetFormValue(dest,PSTR("HL"),huePollLightId); - printSetFormValue(dest,PSTR("HI"),huePollIntervalMs); - printSetFormCheckbox(dest,PSTR("HP"),huePollingEnabled); - printSetFormCheckbox(dest,PSTR("HO"),hueApplyOnOff); - printSetFormCheckbox(dest,PSTR("HB"),hueApplyBri); - printSetFormCheckbox(dest,PSTR("HC"),hueApplyColor); + printSetFormValue(settingsScript,PSTR("H0"),hueIP[0]); + printSetFormValue(settingsScript,PSTR("H1"),hueIP[1]); + printSetFormValue(settingsScript,PSTR("H2"),hueIP[2]); + printSetFormValue(settingsScript,PSTR("H3"),hueIP[3]); + printSetFormValue(settingsScript,PSTR("HL"),huePollLightId); + printSetFormValue(settingsScript,PSTR("HI"),huePollIntervalMs); + printSetFormCheckbox(settingsScript,PSTR("HP"),huePollingEnabled); + printSetFormCheckbox(settingsScript,PSTR("HO"),hueApplyOnOff); + printSetFormCheckbox(settingsScript,PSTR("HB"),hueApplyBri); + printSetFormCheckbox(settingsScript,PSTR("HC"),hueApplyColor); char hueErrorString[25]; switch (hueError) { @@ -495,56 +495,56 @@ void getSettingsJS(byte subPage, Print& dest) default: sprintf_P(hueErrorString,PSTR("Bridge Error %i"),hueError); } - printSetClassElementHTML(dest,PSTR("sip"),0,hueErrorString); + printSetClassElementHTML(settingsScript,PSTR("sip"),0,hueErrorString); #else - dest.print(F("toggle('Hue');")); // hide Hue Sync settings + settingsScript.print(F("toggle('Hue');")); // hide Hue Sync settings #endif - printSetFormValue(dest,PSTR("BD"),serialBaud); + printSetFormValue(settingsScript,PSTR("BD"),serialBaud); #ifndef WLED_ENABLE_ADALIGHT - dest.print(SET_F("toggle('Serial);")); + settingsScript.print(SET_F("toggle('Serial);")); #endif } if (subPage == SUBPAGE_TIME) { - printSetFormCheckbox(dest,PSTR("NT"),ntpEnabled); - printSetFormValue(dest,PSTR("NS"),ntpServerName); - printSetFormCheckbox(dest,PSTR("CF"),!useAMPM); - printSetFormIndex(dest,PSTR("TZ"),currentTimezone); - printSetFormValue(dest,PSTR("UO"),utcOffsetSecs); + printSetFormCheckbox(settingsScript,PSTR("NT"),ntpEnabled); + printSetFormValue(settingsScript,PSTR("NS"),ntpServerName); + printSetFormCheckbox(settingsScript,PSTR("CF"),!useAMPM); + printSetFormIndex(settingsScript,PSTR("TZ"),currentTimezone); + printSetFormValue(settingsScript,PSTR("UO"),utcOffsetSecs); char tm[32]; dtostrf(longitude,4,2,tm); - printSetFormValue(dest,PSTR("LN"),tm); + printSetFormValue(settingsScript,PSTR("LN"),tm); dtostrf(latitude,4,2,tm); - printSetFormValue(dest,PSTR("LT"),tm); + printSetFormValue(settingsScript,PSTR("LT"),tm); getTimeString(tm); - printSetClassElementHTML(dest,PSTR("times"),0,tm); + printSetClassElementHTML(settingsScript,PSTR("times"),0,tm); if ((int)(longitude*10.0f) || (int)(latitude*10.0f)) { sprintf_P(tm, PSTR("Sunrise: %02d:%02d Sunset: %02d:%02d"), hour(sunrise), minute(sunrise), hour(sunset), minute(sunset)); - printSetClassElementHTML(dest,PSTR("times"),1,tm); + printSetClassElementHTML(settingsScript,PSTR("times"),1,tm); } - printSetFormCheckbox(dest,PSTR("OL"),overlayCurrent); - printSetFormValue(dest,PSTR("O1"),overlayMin); - printSetFormValue(dest,PSTR("O2"),overlayMax); - printSetFormValue(dest,PSTR("OM"),analogClock12pixel); - printSetFormCheckbox(dest,PSTR("OS"),analogClockSecondsTrail); - printSetFormCheckbox(dest,PSTR("O5"),analogClock5MinuteMarks); - printSetFormCheckbox(dest,PSTR("OB"),analogClockSolidBlack); - - printSetFormCheckbox(dest,PSTR("CE"),countdownMode); - printSetFormValue(dest,PSTR("CY"),countdownYear); - printSetFormValue(dest,PSTR("CI"),countdownMonth); - printSetFormValue(dest,PSTR("CD"),countdownDay); - printSetFormValue(dest,PSTR("CH"),countdownHour); - printSetFormValue(dest,PSTR("CM"),countdownMin); - printSetFormValue(dest,PSTR("CS"),countdownSec); - - printSetFormValue(dest,PSTR("A0"),macroAlexaOn); - printSetFormValue(dest,PSTR("A1"),macroAlexaOff); - printSetFormValue(dest,PSTR("MC"),macroCountdown); - printSetFormValue(dest,PSTR("MN"),macroNl); + printSetFormCheckbox(settingsScript,PSTR("OL"),overlayCurrent); + printSetFormValue(settingsScript,PSTR("O1"),overlayMin); + printSetFormValue(settingsScript,PSTR("O2"),overlayMax); + printSetFormValue(settingsScript,PSTR("OM"),analogClock12pixel); + printSetFormCheckbox(settingsScript,PSTR("OS"),analogClockSecondsTrail); + printSetFormCheckbox(settingsScript,PSTR("O5"),analogClock5MinuteMarks); + printSetFormCheckbox(settingsScript,PSTR("OB"),analogClockSolidBlack); + + printSetFormCheckbox(settingsScript,PSTR("CE"),countdownMode); + printSetFormValue(settingsScript,PSTR("CY"),countdownYear); + printSetFormValue(settingsScript,PSTR("CI"),countdownMonth); + printSetFormValue(settingsScript,PSTR("CD"),countdownDay); + printSetFormValue(settingsScript,PSTR("CH"),countdownHour); + printSetFormValue(settingsScript,PSTR("CM"),countdownMin); + printSetFormValue(settingsScript,PSTR("CS"),countdownSec); + + printSetFormValue(settingsScript,PSTR("A0"),macroAlexaOn); + printSetFormValue(settingsScript,PSTR("A1"),macroAlexaOff); + printSetFormValue(settingsScript,PSTR("MC"),macroCountdown); + printSetFormValue(settingsScript,PSTR("MN"),macroNl); for (unsigned i=0; i> 4) & 0x0F); - k[0] = 'P'; printSetFormValue(dest,k,timerMonth[i] & 0x0F); - k[0] = 'D'; printSetFormValue(dest,k,timerDay[i]); - k[0] = 'E'; printSetFormValue(dest,k,timerDayEnd[i]); + k[0] = 'M'; printSetFormValue(settingsScript,k,(timerMonth[i] >> 4) & 0x0F); + k[0] = 'P'; printSetFormValue(settingsScript,k,timerMonth[i] & 0x0F); + k[0] = 'D'; printSetFormValue(settingsScript,k,timerDay[i]); + k[0] = 'E'; printSetFormValue(settingsScript,k,timerDayEnd[i]); } } } @@ -571,61 +571,61 @@ void getSettingsJS(byte subPage, Print& dest) char fpass[l+1]; //fill PIN field with 0000 fpass[l] = 0; memset(fpass,'0',l); - printSetFormValue(dest,PSTR("PIN"),fpass); - printSetFormCheckbox(dest,PSTR("NO"),otaLock); - printSetFormCheckbox(dest,PSTR("OW"),wifiLock); - printSetFormCheckbox(dest,PSTR("AO"),aOtaEnabled); + printSetFormValue(settingsScript,PSTR("PIN"),fpass); + printSetFormCheckbox(settingsScript,PSTR("NO"),otaLock); + printSetFormCheckbox(settingsScript,PSTR("OW"),wifiLock); + printSetFormCheckbox(settingsScript,PSTR("AO"),aOtaEnabled); char tmp_buf[128]; snprintf_P(tmp_buf,sizeof(tmp_buf),PSTR("WLED %s (build %d)"),versionString,VERSION); - printSetClassElementHTML(dest,PSTR("sip"),0,tmp_buf); - dest.printf_P(PSTR("sd=\"%s\";"), serverDescription); + printSetClassElementHTML(settingsScript,PSTR("sip"),0,tmp_buf); + settingsScript.printf_P(PSTR("sd=\"%s\";"), serverDescription); } #ifdef WLED_ENABLE_DMX // include only if DMX is enabled if (subPage == SUBPAGE_DMX) { - printSetFormValue(dest,PSTR("PU"),e131ProxyUniverse); - - printSetFormValue(dest,PSTR("CN"),DMXChannels); - printSetFormValue(dest,PSTR("CG"),DMXGap); - printSetFormValue(dest,PSTR("CS"),DMXStart); - printSetFormValue(dest,PSTR("SL"),DMXStartLED); - - printSetFormIndex(dest,PSTR("CH1"),DMXFixtureMap[0]); - printSetFormIndex(dest,PSTR("CH2"),DMXFixtureMap[1]); - printSetFormIndex(dest,PSTR("CH3"),DMXFixtureMap[2]); - printSetFormIndex(dest,PSTR("CH4"),DMXFixtureMap[3]); - printSetFormIndex(dest,PSTR("CH5"),DMXFixtureMap[4]); - printSetFormIndex(dest,PSTR("CH6"),DMXFixtureMap[5]); - printSetFormIndex(dest,PSTR("CH7"),DMXFixtureMap[6]); - printSetFormIndex(dest,PSTR("CH8"),DMXFixtureMap[7]); - printSetFormIndex(dest,PSTR("CH9"),DMXFixtureMap[8]); - printSetFormIndex(dest,PSTR("CH10"),DMXFixtureMap[9]); - printSetFormIndex(dest,PSTR("CH11"),DMXFixtureMap[10]); - printSetFormIndex(dest,PSTR("CH12"),DMXFixtureMap[11]); - printSetFormIndex(dest,PSTR("CH13"),DMXFixtureMap[12]); - printSetFormIndex(dest,PSTR("CH14"),DMXFixtureMap[13]); - printSetFormIndex(dest,PSTR("CH15"),DMXFixtureMap[14]); + printSetFormValue(settingsScript,PSTR("PU"),e131ProxyUniverse); + + printSetFormValue(settingsScript,PSTR("CN"),DMXChannels); + printSetFormValue(settingsScript,PSTR("CG"),DMXGap); + printSetFormValue(settingsScript,PSTR("CS"),DMXStart); + printSetFormValue(settingsScript,PSTR("SL"),DMXStartLED); + + printSetFormIndex(settingsScript,PSTR("CH1"),DMXFixtureMap[0]); + printSetFormIndex(settingsScript,PSTR("CH2"),DMXFixtureMap[1]); + printSetFormIndex(settingsScript,PSTR("CH3"),DMXFixtureMap[2]); + printSetFormIndex(settingsScript,PSTR("CH4"),DMXFixtureMap[3]); + printSetFormIndex(settingsScript,PSTR("CH5"),DMXFixtureMap[4]); + printSetFormIndex(settingsScript,PSTR("CH6"),DMXFixtureMap[5]); + printSetFormIndex(settingsScript,PSTR("CH7"),DMXFixtureMap[6]); + printSetFormIndex(settingsScript,PSTR("CH8"),DMXFixtureMap[7]); + printSetFormIndex(settingsScript,PSTR("CH9"),DMXFixtureMap[8]); + printSetFormIndex(settingsScript,PSTR("CH10"),DMXFixtureMap[9]); + printSetFormIndex(settingsScript,PSTR("CH11"),DMXFixtureMap[10]); + printSetFormIndex(settingsScript,PSTR("CH12"),DMXFixtureMap[11]); + printSetFormIndex(settingsScript,PSTR("CH13"),DMXFixtureMap[12]); + printSetFormIndex(settingsScript,PSTR("CH14"),DMXFixtureMap[13]); + printSetFormIndex(settingsScript,PSTR("CH15"),DMXFixtureMap[14]); } #endif if (subPage == SUBPAGE_UM) //usermods { - appendGPIOinfo(dest); - dest.printf_P(PSTR("numM=%d;"), usermods.getModCount()); - printSetFormValue(dest,PSTR("SDA"),i2c_sda); - printSetFormValue(dest,PSTR("SCL"),i2c_scl); - printSetFormValue(dest,PSTR("MOSI"),spi_mosi); - printSetFormValue(dest,PSTR("MISO"),spi_miso); - printSetFormValue(dest,PSTR("SCLK"),spi_sclk); - dest.printf_P(PSTR("addInfo('SDA','%d');" + appendGPIOinfo(settingsScript); + settingsScript.printf_P(PSTR("numM=%d;"), usermods.getModCount()); + printSetFormValue(settingsScript,PSTR("SDA"),i2c_sda); + printSetFormValue(settingsScript,PSTR("SCL"),i2c_scl); + printSetFormValue(settingsScript,PSTR("MOSI"),spi_mosi); + printSetFormValue(settingsScript,PSTR("MISO"),spi_miso); + printSetFormValue(settingsScript,PSTR("SCLK"),spi_sclk); + settingsScript.printf_P(PSTR("addInfo('SDA','%d');" "addInfo('SCL','%d');" "addInfo('MOSI','%d');" "addInfo('MISO','%d');" "addInfo('SCLK','%d');"), HW_PIN_SDA, HW_PIN_SCL, HW_PIN_DATASPI, HW_PIN_MISOSPI, HW_PIN_CLOCKSPI ); - usermods.appendConfigData(dest); + usermods.appendConfigData(settingsScript); } if (subPage == SUBPAGE_UPDATE) // update @@ -641,44 +641,44 @@ void getSettingsJS(byte subPage, Print& dest) #endif VERSION); - printSetClassElementHTML(dest,PSTR("sip"),0,tmp_buf); + printSetClassElementHTML(settingsScript,PSTR("sip"),0,tmp_buf); } if (subPage == SUBPAGE_2D) // 2D matrices { - printSetFormValue(dest,PSTR("SOMP"),strip.isMatrix); + printSetFormValue(settingsScript,PSTR("SOMP"),strip.isMatrix); #ifndef WLED_DISABLE_2D - dest.printf_P(PSTR("maxPanels=%d;"),WLED_MAX_PANELS); - dest.print(F("resetPanels();")); + settingsScript.printf_P(PSTR("maxPanels=%d;"),WLED_MAX_PANELS); + settingsScript.print(F("resetPanels();")); if (strip.isMatrix) { if(strip.panels>0){ - printSetFormValue(dest,PSTR("PW"),strip.panel[0].width); //Set generator Width and Height to first panel size for convenience - printSetFormValue(dest,PSTR("PH"),strip.panel[0].height); + printSetFormValue(settingsScript,PSTR("PW"),strip.panel[0].width); //Set generator Width and Height to first panel size for convenience + printSetFormValue(settingsScript,PSTR("PH"),strip.panel[0].height); } - printSetFormValue(dest,PSTR("MPC"),strip.panels); + printSetFormValue(settingsScript,PSTR("MPC"),strip.panels); // panels for (unsigned i=0; i Date: Thu, 19 Sep 2024 21:44:11 +0200 Subject: [PATCH 086/145] Static PinManager & UsermodManager - saves a few bytes of flash --- .../Animated_Staircase/Animated_Staircase.h | 10 +- usermods/Animated_Staircase/README.md | 2 +- usermods/Battery/usermod_v2_Battery.h | 4 +- usermods/EXAMPLE_v2/usermod_v2_example.h | 2 +- .../Fix_unreachable_netservices_v2/readme.md | 8 +- .../usermod_LDR_Dusk_Dawn_v2.h | 6 +- usermods/PIR_sensor_switch/readme.md | 2 +- .../usermod_PIR_sensor_switch.h | 4 +- usermods/PWM_fan/usermod_PWM_fan.h | 16 +- usermods/SN_Photoresistor/usermods_list.cpp | 2 +- usermods/ST7789_display/ST7789_display.h | 6 +- usermods/Temperature/usermod_temperature.h | 10 +- usermods/audioreactive/audio_reactive.old.h | 2071 +++++++++++++++++ usermods/audioreactive/audio_source.h | 20 +- usermods/mpu6050_imu/readme.md | 2 +- usermods/mpu6050_imu/usermod_gyro_surge.h | 2 +- usermods/mpu6050_imu/usermod_mpu6050_imu.h | 4 +- usermods/mqtt_switch_v2/README.md | 2 +- usermods/multi_relay/readme.md | 8 +- usermods/multi_relay/usermod_multi_relay.h | 4 +- usermods/pixels_dice_tray/pixels_dice_tray.h | 6 +- usermods/pwm_outputs/usermod_pwm_outputs.h | 10 +- usermods/quinled-an-penta/quinled-an-penta.h | 18 +- .../rgb-rotary-encoder/rgb-rotary-encoder.h | 10 +- usermods/sd_card/usermod_sd_card.h | 10 +- .../usermod_seven_segment_reloaded.h | 2 +- .../usermod_v2_auto_save.h | 2 +- .../usermod_v2_four_line_display_ALT.h | 10 +- .../usermod_v2_rotary_encoder_ui_ALT.h | 14 +- wled00/FX.cpp | 8 +- wled00/FX_fcn.cpp | 8 +- wled00/bus_manager.cpp | 26 +- wled00/bus_manager.h | 2 +- wled00/button.cpp | 2 +- wled00/cfg.cpp | 28 +- wled00/fcn_declare.h | 44 +- wled00/json.cpp | 6 +- wled00/led.cpp | 2 +- wled00/mqtt.cpp | 6 +- wled00/overlay.cpp | 2 +- wled00/pin_manager.cpp | 109 +- wled00/pin_manager.h | 83 +- wled00/set.cpp | 34 +- wled00/udp.cpp | 2 +- wled00/um_manager.cpp | 3 + wled00/usermods_list.cpp | 112 +- wled00/wled.cpp | 22 +- wled00/wled_server.cpp | 4 +- wled00/xml.cpp | 12 +- 49 files changed, 2399 insertions(+), 383 deletions(-) create mode 100644 usermods/audioreactive/audio_reactive.old.h diff --git a/usermods/Animated_Staircase/Animated_Staircase.h b/usermods/Animated_Staircase/Animated_Staircase.h index 8953756d35..d1ec9bb7f6 100644 --- a/usermods/Animated_Staircase/Animated_Staircase.h +++ b/usermods/Animated_Staircase/Animated_Staircase.h @@ -332,7 +332,7 @@ class Animated_Staircase : public Usermod { }; // NOTE: this *WILL* return TRUE if all the pins are set to -1. // this is *BY DESIGN*. - if (!pinManager.allocateMultiplePins(pins, 4, PinOwner::UM_AnimatedStaircase)) { + if (!PinManager::allocateMultiplePins(pins, 4, PinOwner::UM_AnimatedStaircase)) { topPIRorTriggerPin = -1; topEchoPin = -1; bottomPIRorTriggerPin = -1; @@ -513,10 +513,10 @@ class Animated_Staircase : public Usermod { (oldBottomAPin != bottomPIRorTriggerPin) || (oldBottomBPin != bottomEchoPin)) { changed = true; - pinManager.deallocatePin(oldTopAPin, PinOwner::UM_AnimatedStaircase); - pinManager.deallocatePin(oldTopBPin, PinOwner::UM_AnimatedStaircase); - pinManager.deallocatePin(oldBottomAPin, PinOwner::UM_AnimatedStaircase); - pinManager.deallocatePin(oldBottomBPin, PinOwner::UM_AnimatedStaircase); + PinManager::deallocatePin(oldTopAPin, PinOwner::UM_AnimatedStaircase); + PinManager::deallocatePin(oldTopBPin, PinOwner::UM_AnimatedStaircase); + PinManager::deallocatePin(oldBottomAPin, PinOwner::UM_AnimatedStaircase); + PinManager::deallocatePin(oldBottomBPin, PinOwner::UM_AnimatedStaircase); } if (changed) setup(); } diff --git a/usermods/Animated_Staircase/README.md b/usermods/Animated_Staircase/README.md index 320b744a55..2ad66b5aef 100644 --- a/usermods/Animated_Staircase/README.md +++ b/usermods/Animated_Staircase/README.md @@ -18,7 +18,7 @@ Before compiling, you have to make the following modifications: Edit `usermods_list.cpp`: 1. Open `wled00/usermods_list.cpp` 2. add `#include "../usermods/Animated_Staircase/Animated_Staircase.h"` to the top of the file -3. add `usermods.add(new Animated_Staircase());` to the end of the `void registerUsermods()` function. +3. add `UsermodManager::add(new Animated_Staircase());` to the end of the `void registerUsermods()` function. You can configure usermod using the Usermods settings page. Please enter GPIO pins for PIR or ultrasonic sensors (trigger and echo). diff --git a/usermods/Battery/usermod_v2_Battery.h b/usermods/Battery/usermod_v2_Battery.h index 136d3a71a4..e91de850c2 100644 --- a/usermods/Battery/usermod_v2_Battery.h +++ b/usermods/Battery/usermod_v2_Battery.h @@ -200,7 +200,7 @@ class UsermodBattery : public Usermod bool success = false; DEBUG_PRINTLN(F("Allocating battery pin...")); if (batteryPin >= 0 && digitalPinToAnalogChannel(batteryPin) >= 0) - if (pinManager.allocatePin(batteryPin, false, PinOwner::UM_Battery)) { + if (PinManager::allocatePin(batteryPin, false, PinOwner::UM_Battery)) { DEBUG_PRINTLN(F("Battery pin allocation succeeded.")); success = true; } @@ -561,7 +561,7 @@ class UsermodBattery : public Usermod if (newBatteryPin != batteryPin) { // deallocate pin - pinManager.deallocatePin(batteryPin, PinOwner::UM_Battery); + PinManager::deallocatePin(batteryPin, PinOwner::UM_Battery); batteryPin = newBatteryPin; // initialise setup(); diff --git a/usermods/EXAMPLE_v2/usermod_v2_example.h b/usermods/EXAMPLE_v2/usermod_v2_example.h index 32374fde2a..3d562b5857 100644 --- a/usermods/EXAMPLE_v2/usermod_v2_example.h +++ b/usermods/EXAMPLE_v2/usermod_v2_example.h @@ -71,7 +71,7 @@ class MyExampleUsermod : public Usermod { // #endif // in setup() // #ifdef USERMOD_EXAMPLE - // UM = (MyExampleUsermod*) usermods.lookup(USERMOD_ID_EXAMPLE); + // UM = (MyExampleUsermod*) UsermodManager::lookup(USERMOD_ID_EXAMPLE); // #endif // somewhere in loop() or other member method // #ifdef USERMOD_EXAMPLE diff --git a/usermods/Fix_unreachable_netservices_v2/readme.md b/usermods/Fix_unreachable_netservices_v2/readme.md index 006eaf9f94..07d64bc673 100644 --- a/usermods/Fix_unreachable_netservices_v2/readme.md +++ b/usermods/Fix_unreachable_netservices_v2/readme.md @@ -59,10 +59,10 @@ void registerUsermods() * || || || * \/ \/ \/ */ - //usermods.add(new MyExampleUsermod()); - //usermods.add(new UsermodTemperature()); - //usermods.add(new UsermodRenameMe()); - usermods.add(new FixUnreachableNetServices()); + //UsermodManager::add(new MyExampleUsermod()); + //UsermodManager::add(new UsermodTemperature()); + //UsermodManager::add(new UsermodRenameMe()); + UsermodManager::add(new FixUnreachableNetServices()); } ``` diff --git a/usermods/LDR_Dusk_Dawn_v2/usermod_LDR_Dusk_Dawn_v2.h b/usermods/LDR_Dusk_Dawn_v2/usermod_LDR_Dusk_Dawn_v2.h index 393fc22327..03f4c078a4 100644 --- a/usermods/LDR_Dusk_Dawn_v2/usermod_LDR_Dusk_Dawn_v2.h +++ b/usermods/LDR_Dusk_Dawn_v2/usermod_LDR_Dusk_Dawn_v2.h @@ -30,7 +30,7 @@ class LDR_Dusk_Dawn_v2 : public Usermod { void setup() { // register ldrPin if ((ldrPin >= 0) && (digitalPinToAnalogChannel(ldrPin) >= 0)) { - if(!pinManager.allocatePin(ldrPin, false, PinOwner::UM_LDR_DUSK_DAWN)) ldrEnabled = false; // pin already in use -> disable usermod + if(!PinManager::allocatePin(ldrPin, false, PinOwner::UM_LDR_DUSK_DAWN)) ldrEnabled = false; // pin already in use -> disable usermod else pinMode(ldrPin, INPUT); // alloc success -> configure pin for input } else ldrEnabled = false; // invalid pin -> disable usermod initDone = true; @@ -110,7 +110,7 @@ class LDR_Dusk_Dawn_v2 : public Usermod { if (initDone && (ldrPin != oldLdrPin)) { // pin changed - un-register previous pin, register new pin - if (oldLdrPin >= 0) pinManager.deallocatePin(oldLdrPin, PinOwner::UM_LDR_DUSK_DAWN); + if (oldLdrPin >= 0) PinManager::deallocatePin(oldLdrPin, PinOwner::UM_LDR_DUSK_DAWN); setup(); // setup new pin } return configComplete; @@ -139,7 +139,7 @@ class LDR_Dusk_Dawn_v2 : public Usermod { //LDR_Off_Count.add(ldrOffCount); //bool pinValid = ((ldrPin >= 0) && (digitalPinToAnalogChannel(ldrPin) >= 0)); - //if (pinManager.getPinOwner(ldrPin) != PinOwner::UM_LDR_DUSK_DAWN) pinValid = false; + //if (PinManager::getPinOwner(ldrPin) != PinOwner::UM_LDR_DUSK_DAWN) pinValid = false; //JsonArray LDR_valid = user.createNestedArray(F("LDR pin")); //LDR_valid.add(ldrPin); //LDR_valid.add(pinValid ? F(" OK"): F(" invalid")); diff --git a/usermods/PIR_sensor_switch/readme.md b/usermods/PIR_sensor_switch/readme.md index 4dfdb07bd3..fac5419f00 100644 --- a/usermods/PIR_sensor_switch/readme.md +++ b/usermods/PIR_sensor_switch/readme.md @@ -52,7 +52,7 @@ class MyUsermod : public Usermod { void togglePIRSensor() { #ifdef USERMOD_PIR_SENSOR_SWITCH - PIRsensorSwitch *PIRsensor = (PIRsensorSwitch::*) usermods.lookup(USERMOD_ID_PIRSWITCH); + PIRsensorSwitch *PIRsensor = (PIRsensorSwitch::*) UsermodManager::lookup(USERMOD_ID_PIRSWITCH); if (PIRsensor != nullptr) { PIRsensor->EnablePIRsensor(!PIRsensor->PIRsensorEnabled()); } diff --git a/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h b/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h index 7a67dd7497..29070cf84e 100644 --- a/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h +++ b/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h @@ -375,7 +375,7 @@ void PIRsensorSwitch::setup() sensorPinState[i] = LOW; if (PIRsensorPin[i] < 0) continue; // pin retrieved from cfg.json (readFromConfig()) prior to running setup() - if (pinManager.allocatePin(PIRsensorPin[i], false, PinOwner::UM_PIR)) { + if (PinManager::allocatePin(PIRsensorPin[i], false, PinOwner::UM_PIR)) { // PIR Sensor mode INPUT_PULLDOWN #ifdef ESP8266 pinMode(PIRsensorPin[i], PIRsensorPin[i]==16 ? INPUT_PULLDOWN_16 : INPUT_PULLUP); // ESP8266 has INPUT_PULLDOWN on GPIO16 only @@ -564,7 +564,7 @@ bool PIRsensorSwitch::readFromConfig(JsonObject &root) DEBUG_PRINTLN(F(" config loaded.")); } else { for (int i = 0; i < PIR_SENSOR_MAX_SENSORS; i++) - if (oldPin[i] >= 0) pinManager.deallocatePin(oldPin[i], PinOwner::UM_PIR); + if (oldPin[i] >= 0) PinManager::deallocatePin(oldPin[i], PinOwner::UM_PIR); setup(); DEBUG_PRINTLN(F(" config (re)loaded.")); } diff --git a/usermods/PWM_fan/usermod_PWM_fan.h b/usermods/PWM_fan/usermod_PWM_fan.h index 1b78cfd4cc..c3ef24fe41 100644 --- a/usermods/PWM_fan/usermod_PWM_fan.h +++ b/usermods/PWM_fan/usermod_PWM_fan.h @@ -75,7 +75,7 @@ class PWMFanUsermod : public Usermod { static const char _lock[]; void initTacho(void) { - if (tachoPin < 0 || !pinManager.allocatePin(tachoPin, false, PinOwner::UM_Unspecified)){ + if (tachoPin < 0 || !PinManager::allocatePin(tachoPin, false, PinOwner::UM_Unspecified)){ tachoPin = -1; return; } @@ -88,7 +88,7 @@ class PWMFanUsermod : public Usermod { void deinitTacho(void) { if (tachoPin < 0) return; detachInterrupt(digitalPinToInterrupt(tachoPin)); - pinManager.deallocatePin(tachoPin, PinOwner::UM_Unspecified); + PinManager::deallocatePin(tachoPin, PinOwner::UM_Unspecified); tachoPin = -1; } @@ -111,7 +111,7 @@ class PWMFanUsermod : public Usermod { // https://randomnerdtutorials.com/esp32-pwm-arduino-ide/ void initPWMfan(void) { - if (pwmPin < 0 || !pinManager.allocatePin(pwmPin, true, PinOwner::UM_Unspecified)) { + if (pwmPin < 0 || !PinManager::allocatePin(pwmPin, true, PinOwner::UM_Unspecified)) { enabled = false; pwmPin = -1; return; @@ -121,7 +121,7 @@ class PWMFanUsermod : public Usermod { analogWriteRange(255); analogWriteFreq(WLED_PWM_FREQ); #else - pwmChannel = pinManager.allocateLedc(1); + pwmChannel = PinManager::allocateLedc(1); if (pwmChannel == 255) { //no more free LEDC channels deinitPWMfan(); return; } @@ -136,9 +136,9 @@ class PWMFanUsermod : public Usermod { void deinitPWMfan(void) { if (pwmPin < 0) return; - pinManager.deallocatePin(pwmPin, PinOwner::UM_Unspecified); + PinManager::deallocatePin(pwmPin, PinOwner::UM_Unspecified); #ifdef ARDUINO_ARCH_ESP32 - pinManager.deallocateLedc(pwmChannel, 1); + PinManager::deallocateLedc(pwmChannel, 1); #endif pwmPin = -1; } @@ -191,9 +191,9 @@ class PWMFanUsermod : public Usermod { void setup() override { #ifdef USERMOD_DALLASTEMPERATURE // This Usermod requires Temperature usermod - tempUM = (UsermodTemperature*) usermods.lookup(USERMOD_ID_TEMPERATURE); + tempUM = (UsermodTemperature*) UsermodManager::lookup(USERMOD_ID_TEMPERATURE); #elif defined(USERMOD_SHT) - tempUM = (ShtUsermod*) usermods.lookup(USERMOD_ID_SHT); + tempUM = (ShtUsermod*) UsermodManager::lookup(USERMOD_ID_SHT); #endif initTacho(); initPWMfan(); diff --git a/usermods/SN_Photoresistor/usermods_list.cpp b/usermods/SN_Photoresistor/usermods_list.cpp index 649e197392..a2c6ca165f 100644 --- a/usermods/SN_Photoresistor/usermods_list.cpp +++ b/usermods/SN_Photoresistor/usermods_list.cpp @@ -9,6 +9,6 @@ void registerUsermods() { #ifdef USERMOD_SN_PHOTORESISTOR - usermods.add(new Usermod_SN_Photoresistor()); + UsermodManager::add(new Usermod_SN_Photoresistor()); #endif } \ No newline at end of file diff --git a/usermods/ST7789_display/ST7789_display.h b/usermods/ST7789_display/ST7789_display.h index 59f6d9271d..0dbada382f 100644 --- a/usermods/ST7789_display/ST7789_display.h +++ b/usermods/ST7789_display/ST7789_display.h @@ -138,10 +138,10 @@ class St7789DisplayUsermod : public Usermod { void setup() override { PinManagerPinType spiPins[] = { { spi_mosi, true }, { spi_miso, false}, { spi_sclk, true } }; - if (!pinManager.allocateMultiplePins(spiPins, 3, PinOwner::HW_SPI)) { enabled = false; return; } + if (!PinManager::allocateMultiplePins(spiPins, 3, PinOwner::HW_SPI)) { enabled = false; return; } PinManagerPinType displayPins[] = { { TFT_CS, true}, { TFT_DC, true}, { TFT_RST, true }, { TFT_BL, true } }; - if (!pinManager.allocateMultiplePins(displayPins, sizeof(displayPins)/sizeof(PinManagerPinType), PinOwner::UM_FourLineDisplay)) { - pinManager.deallocateMultiplePins(spiPins, 3, PinOwner::HW_SPI); + if (!PinManager::allocateMultiplePins(displayPins, sizeof(displayPins)/sizeof(PinManagerPinType), PinOwner::UM_FourLineDisplay)) { + PinManager::deallocateMultiplePins(spiPins, 3, PinOwner::HW_SPI); enabled = false; return; } diff --git a/usermods/Temperature/usermod_temperature.h b/usermods/Temperature/usermod_temperature.h index d7a9d82a47..ad755eaeec 100644 --- a/usermods/Temperature/usermod_temperature.h +++ b/usermods/Temperature/usermod_temperature.h @@ -73,7 +73,7 @@ class UsermodTemperature : public Usermod { void publishHomeAssistantAutodiscovery(); #endif - static UsermodTemperature* _instance; // to overcome nonstatic getTemperatureC() method and avoid usermods.lookup(USERMOD_ID_TEMPERATURE); + static UsermodTemperature* _instance; // to overcome nonstatic getTemperatureC() method and avoid UsermodManager::lookup(USERMOD_ID_TEMPERATURE); public: @@ -223,14 +223,14 @@ void UsermodTemperature::setup() { // config says we are enabled DEBUG_PRINTLN(F("Allocating temperature pin...")); // pin retrieved from cfg.json (readFromConfig()) prior to running setup() - if (temperaturePin >= 0 && pinManager.allocatePin(temperaturePin, true, PinOwner::UM_Temperature)) { + if (temperaturePin >= 0 && PinManager::allocatePin(temperaturePin, true, PinOwner::UM_Temperature)) { oneWire = new OneWire(temperaturePin); if (oneWire->reset()) { while (!findSensor() && retries--) { delay(25); // try to find sensor } } - if (parasite && pinManager.allocatePin(parasitePin, true, PinOwner::UM_Temperature)) { + if (parasite && PinManager::allocatePin(parasitePin, true, PinOwner::UM_Temperature)) { pinMode(parasitePin, OUTPUT); digitalWrite(parasitePin, LOW); // deactivate power (close MOSFET) } else { @@ -423,9 +423,9 @@ bool UsermodTemperature::readFromConfig(JsonObject &root) { DEBUG_PRINTLN(F("Re-init temperature.")); // deallocate pin and release memory delete oneWire; - pinManager.deallocatePin(temperaturePin, PinOwner::UM_Temperature); + PinManager::deallocatePin(temperaturePin, PinOwner::UM_Temperature); temperaturePin = newTemperaturePin; - pinManager.deallocatePin(parasitePin, PinOwner::UM_Temperature); + PinManager::deallocatePin(parasitePin, PinOwner::UM_Temperature); // initialise setup(); } diff --git a/usermods/audioreactive/audio_reactive.old.h b/usermods/audioreactive/audio_reactive.old.h new file mode 100644 index 0000000000..4f2e04c089 --- /dev/null +++ b/usermods/audioreactive/audio_reactive.old.h @@ -0,0 +1,2071 @@ +#pragma once + +#include "wled.h" + +#ifdef ARDUINO_ARCH_ESP32 + +#include +#include + +#ifdef WLED_ENABLE_DMX + #error This audio reactive usermod is not compatible with DMX Out. +#endif + +#endif + +#if defined(ARDUINO_ARCH_ESP32) && (defined(WLED_DEBUG) || defined(SR_DEBUG)) +#include +#endif + +/* + * Usermods allow you to add own functionality to WLED more easily + * See: https://github.com/Aircoookie/WLED/wiki/Add-own-functionality + * + * This is an audioreactive v2 usermod. + * .... + */ + +#if !defined(FFTTASK_PRIORITY) +#define FFTTASK_PRIORITY 1 // standard: looptask prio +//#define FFTTASK_PRIORITY 2 // above looptask, below asyc_tcp +//#define FFTTASK_PRIORITY 4 // above asyc_tcp +#endif + +// Comment/Uncomment to toggle usb serial debugging +// #define MIC_LOGGER // MIC sampling & sound input debugging (serial plotter) +// #define FFT_SAMPLING_LOG // FFT result debugging +// #define SR_DEBUG // generic SR DEBUG messages + +#ifdef SR_DEBUG + #define DEBUGSR_PRINT(x) DEBUGOUT.print(x) + #define DEBUGSR_PRINTLN(x) DEBUGOUT.println(x) + #define DEBUGSR_PRINTF(x...) DEBUGOUT.printf(x) +#else + #define DEBUGSR_PRINT(x) + #define DEBUGSR_PRINTLN(x) + #define DEBUGSR_PRINTF(x...) +#endif + +#if defined(MIC_LOGGER) || defined(FFT_SAMPLING_LOG) + #define PLOT_PRINT(x) DEBUGOUT.print(x) + #define PLOT_PRINTLN(x) DEBUGOUT.println(x) + #define PLOT_PRINTF(x...) DEBUGOUT.printf(x) +#else + #define PLOT_PRINT(x) + #define PLOT_PRINTLN(x) + #define PLOT_PRINTF(x...) +#endif + +#define MAX_PALETTES 3 + +static volatile bool disableSoundProcessing = false; // if true, sound processing (FFT, filters, AGC) will be suspended. "volatile" as its shared between tasks. +static uint8_t audioSyncEnabled = 0; // bit field: bit 0 - send, bit 1 - receive (config value) +static bool udpSyncConnected = false; // UDP connection status -> true if connected to multicast group + +#define NUM_GEQ_CHANNELS 16 // number of frequency channels. Don't change !! + +// audioreactive variables +#ifdef ARDUINO_ARCH_ESP32 +static float micDataReal = 0.0f; // MicIn data with full 24bit resolution - lowest 8bit after decimal point +static float multAgc = 1.0f; // sample * multAgc = sampleAgc. Our AGC multiplier +static float sampleAvg = 0.0f; // Smoothed Average sample - sampleAvg < 1 means "quiet" (simple noise gate) +static float sampleAgc = 0.0f; // Smoothed AGC sample +static uint8_t soundAgc = 0; // Automagic gain control: 0 - none, 1 - normal, 2 - vivid, 3 - lazy (config value) +#endif +//static float volumeSmth = 0.0f; // either sampleAvg or sampleAgc depending on soundAgc; smoothed sample +static float FFT_MajorPeak = 1.0f; // FFT: strongest (peak) frequency +static float FFT_Magnitude = 0.0f; // FFT: volume (magnitude) of peak frequency +static bool samplePeak = false; // Boolean flag for peak - used in effects. Responding routine may reset this flag. Auto-reset after WS2812FX::getMinShowDelay() +static bool udpSamplePeak = false; // Boolean flag for peak. Set at the same time as samplePeak, but reset by transmitAudioData +static unsigned long timeOfPeak = 0; // time of last sample peak detection. +static uint8_t fftResult[NUM_GEQ_CHANNELS]= {0};// Our calculated freq. channel result table to be used by effects + +// TODO: probably best not used by receive nodes +//static float agcSensitivity = 128; // AGC sensitivity estimation, based on agc gain (multAgc). calculated by getSensitivity(). range 0..255 + +// user settable parameters for limitSoundDynamics() +#ifdef UM_AUDIOREACTIVE_DYNAMICS_LIMITER_OFF +static bool limiterOn = false; // bool: enable / disable dynamics limiter +#else +static bool limiterOn = true; +#endif +static uint16_t attackTime = 80; // int: attack time in milliseconds. Default 0.08sec +static uint16_t decayTime = 1400; // int: decay time in milliseconds. Default 1.40sec + +// peak detection +#ifdef ARDUINO_ARCH_ESP32 +static void detectSamplePeak(void); // peak detection function (needs scaled FFT results in vReal[]) - no used for 8266 receive-only mode +#endif +static void autoResetPeak(void); // peak auto-reset function +static uint8_t maxVol = 31; // (was 10) Reasonable value for constant volume for 'peak detector', as it won't always trigger (deprecated) +static uint8_t binNum = 8; // Used to select the bin for FFT based beat detection (deprecated) + +#ifdef ARDUINO_ARCH_ESP32 + +// use audio source class (ESP32 specific) +#include "audio_source.h" +constexpr i2s_port_t I2S_PORT = I2S_NUM_0; // I2S port to use (do not change !) +constexpr int BLOCK_SIZE = 128; // I2S buffer size (samples) + +// globals +static uint8_t inputLevel = 128; // UI slider value +#ifndef SR_SQUELCH + uint8_t soundSquelch = 10; // squelch value for volume reactive routines (config value) +#else + uint8_t soundSquelch = SR_SQUELCH; // squelch value for volume reactive routines (config value) +#endif +#ifndef SR_GAIN + uint8_t sampleGain = 60; // sample gain (config value) +#else + uint8_t sampleGain = SR_GAIN; // sample gain (config value) +#endif +// user settable options for FFTResult scaling +static uint8_t FFTScalingMode = 3; // 0 none; 1 optimized logarithmic; 2 optimized linear; 3 optimized square root + +// +// AGC presets +// Note: in C++, "const" implies "static" - no need to explicitly declare everything as "static const" +// +#define AGC_NUM_PRESETS 3 // AGC presets: normal, vivid, lazy +const double agcSampleDecay[AGC_NUM_PRESETS] = { 0.9994f, 0.9985f, 0.9997f}; // decay factor for sampleMax, in case the current sample is below sampleMax +const float agcZoneLow[AGC_NUM_PRESETS] = { 32, 28, 36}; // low volume emergency zone +const float agcZoneHigh[AGC_NUM_PRESETS] = { 240, 240, 248}; // high volume emergency zone +const float agcZoneStop[AGC_NUM_PRESETS] = { 336, 448, 304}; // disable AGC integrator if we get above this level +const float agcTarget0[AGC_NUM_PRESETS] = { 112, 144, 164}; // first AGC setPoint -> between 40% and 65% +const float agcTarget0Up[AGC_NUM_PRESETS] = { 88, 64, 116}; // setpoint switching value (a poor man's bang-bang) +const float agcTarget1[AGC_NUM_PRESETS] = { 220, 224, 216}; // second AGC setPoint -> around 85% +const double agcFollowFast[AGC_NUM_PRESETS] = { 1/192.f, 1/128.f, 1/256.f}; // quickly follow setpoint - ~0.15 sec +const double agcFollowSlow[AGC_NUM_PRESETS] = {1/6144.f,1/4096.f,1/8192.f}; // slowly follow setpoint - ~2-15 secs +const double agcControlKp[AGC_NUM_PRESETS] = { 0.6f, 1.5f, 0.65f}; // AGC - PI control, proportional gain parameter +const double agcControlKi[AGC_NUM_PRESETS] = { 1.7f, 1.85f, 1.2f}; // AGC - PI control, integral gain parameter +const float agcSampleSmooth[AGC_NUM_PRESETS] = { 1/12.f, 1/6.f, 1/16.f}; // smoothing factor for sampleAgc (use rawSampleAgc if you want the non-smoothed value) +// AGC presets end + +static AudioSource *audioSource = nullptr; +static bool useBandPassFilter = false; // if true, enables a bandpass filter 80Hz-16Khz to remove noise. Applies before FFT. + +//////////////////// +// Begin FFT Code // +//////////////////// + +// some prototypes, to ensure consistent interfaces +static float mapf(float x, float in_min, float in_max, float out_min, float out_max); // map function for float +static float fftAddAvg(int from, int to); // average of several FFT result bins +static void FFTcode(void * parameter); // audio processing task: read samples, run FFT, fill GEQ channels from FFT results +static void runMicFilter(uint16_t numSamples, float *sampleBuffer); // pre-filtering of raw samples (band-pass) +static void postProcessFFTResults(bool noiseGateOpen, int numberOfChannels); // post-processing and post-amp of GEQ channels + +static TaskHandle_t FFT_Task = nullptr; + +// Table of multiplication factors so that we can even out the frequency response. +static float fftResultPink[NUM_GEQ_CHANNELS] = { 1.70f, 1.71f, 1.73f, 1.78f, 1.68f, 1.56f, 1.55f, 1.63f, 1.79f, 1.62f, 1.80f, 2.06f, 2.47f, 3.35f, 6.83f, 9.55f }; + +// globals and FFT Output variables shared with animations +#if defined(WLED_DEBUG) || defined(SR_DEBUG) +static uint64_t fftTime = 0; +static uint64_t sampleTime = 0; +#endif + +// FFT Task variables (filtering and post-processing) +static float fftCalc[NUM_GEQ_CHANNELS] = {0.0f}; // Try and normalize fftBin values to a max of 4096, so that 4096/16 = 256. +static float fftAvg[NUM_GEQ_CHANNELS] = {0.0f}; // Calculated frequency channel results, with smoothing (used if dynamics limiter is ON) +#ifdef SR_DEBUG +static float fftResultMax[NUM_GEQ_CHANNELS] = {0.0f}; // A table used for testing to determine how our post-processing is working. +#endif + +// audio source parameters and constant +#ifdef ARDUINO_ARCH_ESP32C3 +constexpr SRate_t SAMPLE_RATE = 16000; // 16kHz - use if FFTtask takes more than 20ms. Physical sample time -> 32ms +#define FFT_MIN_CYCLE 30 // Use with 16Khz sampling +#else +constexpr SRate_t SAMPLE_RATE = 22050; // Base sample rate in Hz - 22Khz is a standard rate. Physical sample time -> 23ms +//constexpr SRate_t SAMPLE_RATE = 16000; // 16kHz - use if FFTtask takes more than 20ms. Physical sample time -> 32ms +//constexpr SRate_t SAMPLE_RATE = 20480; // Base sample rate in Hz - 20Khz is experimental. Physical sample time -> 25ms +//constexpr SRate_t SAMPLE_RATE = 10240; // Base sample rate in Hz - previous default. Physical sample time -> 50ms +#define FFT_MIN_CYCLE 21 // minimum time before FFT task is repeated. Use with 22Khz sampling +//#define FFT_MIN_CYCLE 30 // Use with 16Khz sampling +//#define FFT_MIN_CYCLE 23 // minimum time before FFT task is repeated. Use with 20Khz sampling +//#define FFT_MIN_CYCLE 46 // minimum time before FFT task is repeated. Use with 10Khz sampling +#endif + +// FFT Constants +constexpr uint16_t samplesFFT = 512; // Samples in an FFT batch - This value MUST ALWAYS be a power of 2 +constexpr uint16_t samplesFFT_2 = 256; // meaningfull part of FFT results - only the "lower half" contains useful information. +// the following are observed values, supported by a bit of "educated guessing" +//#define FFT_DOWNSCALE 0.65f // 20kHz - downscaling factor for FFT results - "Flat-Top" window @20Khz, old freq channels +#define FFT_DOWNSCALE 0.46f // downscaling factor for FFT results - for "Flat-Top" window @22Khz, new freq channels +#define LOG_256 5.54517744f // log(256) + +// These are the input and output vectors. Input vectors receive computed results from FFT. +static float vReal[samplesFFT] = {0.0f}; // FFT sample inputs / freq output - these are our raw result bins +static float vImag[samplesFFT] = {0.0f}; // imaginary parts + +// Create FFT object +// lib_deps += https://github.com/kosme/arduinoFFT#develop @ 2.0.1 +// these options actually cause slow-downs on all esp32 processors, don't use them. +// #define FFT_SPEED_OVER_PRECISION // enables use of reciprocals (1/x etc) - not faster on ESP32 +// #define FFT_SQRT_APPROXIMATION // enables "quake3" style inverse sqrt - slower on ESP32 +// Below options are forcing ArduinoFFT to use sqrtf() instead of sqrt() +#define sqrt(x) sqrtf(x) // little hack that reduces FFT time by 10-50% on ESP32 +#define sqrt_internal sqrtf // see https://github.com/kosme/arduinoFFT/pull/83 + +#include + +/* Create FFT object with weighing factor storage */ +static ArduinoFFT FFT = ArduinoFFT( vReal, vImag, samplesFFT, SAMPLE_RATE, true); + +// Helper functions + +// float version of map() +static float mapf(float x, float in_min, float in_max, float out_min, float out_max){ + return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; +} + +// compute average of several FFT result bins +static float fftAddAvg(int from, int to) { + float result = 0.0f; + for (int i = from; i <= to; i++) { + result += vReal[i]; + } + return result / float(to - from + 1); +} + +// +// FFT main task +// +void FFTcode(void * parameter) +{ + DEBUGSR_PRINT("FFT started on core: "); DEBUGSR_PRINTLN(xPortGetCoreID()); + + // see https://www.freertos.org/vtaskdelayuntil.html + const TickType_t xFrequency = FFT_MIN_CYCLE * portTICK_PERIOD_MS; + + TickType_t xLastWakeTime = xTaskGetTickCount(); + for(;;) { + delay(1); // DO NOT DELETE THIS LINE! It is needed to give the IDLE(0) task enough time and to keep the watchdog happy. + // taskYIELD(), yield(), vTaskDelay() and esp_task_wdt_feed() didn't seem to work. + + // Don't run FFT computing code if we're in Receive mode or in realtime mode + if (disableSoundProcessing || (audioSyncEnabled & 0x02)) { + vTaskDelayUntil( &xLastWakeTime, xFrequency); // release CPU, and let I2S fill its buffers + continue; + } + +#if defined(WLED_DEBUG) || defined(SR_DEBUG) + uint64_t start = esp_timer_get_time(); + bool haveDoneFFT = false; // indicates if second measurement (FFT time) is valid +#endif + + // get a fresh batch of samples from I2S + if (audioSource) audioSource->getSamples(vReal, samplesFFT); + +#if defined(WLED_DEBUG) || defined(SR_DEBUG) + if (start < esp_timer_get_time()) { // filter out overflows + uint64_t sampleTimeInMillis = (esp_timer_get_time() - start +5ULL) / 10ULL; // "+5" to ensure proper rounding + sampleTime = (sampleTimeInMillis*3 + sampleTime*7)/10; // smooth + } + start = esp_timer_get_time(); // start measuring FFT time +#endif + + xLastWakeTime = xTaskGetTickCount(); // update "last unblocked time" for vTaskDelay + + // band pass filter - can reduce noise floor by a factor of 50 + // downside: frequencies below 100Hz will be ignored + if (useBandPassFilter) runMicFilter(samplesFFT, vReal); + + // find highest sample in the batch + float maxSample = 0.0f; // max sample from FFT batch + for (int i=0; i < samplesFFT; i++) { + // set imaginary parts to 0 + vImag[i] = 0; + // pick our our current mic sample - we take the max value from all samples that go into FFT + if ((vReal[i] <= (INT16_MAX - 1024)) && (vReal[i] >= (INT16_MIN + 1024))) //skip extreme values - normally these are artefacts + if (fabsf((float)vReal[i]) > maxSample) maxSample = fabsf((float)vReal[i]); + } + // release highest sample to volume reactive effects early - not strictly necessary here - could also be done at the end of the function + // early release allows the filters (getSample() and agcAvg()) to work with fresh values - we will have matching gain and noise gate values when we want to process the FFT results. + micDataReal = maxSample; + +#ifdef SR_DEBUG + if (true) { // this allows measure FFT runtimes, as it disables the "only when needed" optimization +#else + if (sampleAvg > 0.25f) { // noise gate open means that FFT results will be used. Don't run FFT if results are not needed. +#endif + + // run FFT (takes 3-5ms on ESP32, ~12ms on ESP32-S2) + FFT.dcRemoval(); // remove DC offset + FFT.windowing( FFTWindow::Flat_top, FFTDirection::Forward); // Weigh data using "Flat Top" function - better amplitude accuracy + //FFT.windowing(FFTWindow::Blackman_Harris, FFTDirection::Forward); // Weigh data using "Blackman- Harris" window - sharp peaks due to excellent sideband rejection + FFT.compute( FFTDirection::Forward ); // Compute FFT + FFT.complexToMagnitude(); // Compute magnitudes + vReal[0] = 0.0f; // The remaining DC offset on the signal produces a strong spike on position 0 that should be eliminated to avoid issues. + + FFT.majorPeak(&FFT_MajorPeak, &FFT_Magnitude); // let the effects know which freq was most dominant + FFT_MajorPeak = constrain(FFT_MajorPeak, 1.0f, 11025.0f); // restrict value to range expected by effects + +#if defined(WLED_DEBUG) || defined(SR_DEBUG) + haveDoneFFT = true; +#endif + + } else { // noise gate closed - only clear results as FFT was skipped. MIC samples are still valid when we do this. + memset(vReal, 0, sizeof(vReal)); + FFT_MajorPeak = 1.0f; + FFT_Magnitude = 0.001f; + } + + for (int i = 0; i < samplesFFT; i++) { + float t = fabsf(vReal[i]); // just to be sure - values in fft bins should be positive any way + vReal[i] = t / 16.0f; // Reduce magnitude. Want end result to be scaled linear and ~4096 max. + } // for() + + // mapping of FFT result bins to frequency channels + if (fabsf(sampleAvg) > 0.5f) { // noise gate open +#if 0 + /* This FFT post processing is a DIY endeavour. What we really need is someone with sound engineering expertise to do a great job here AND most importantly, that the animations look GREAT as a result. + * + * Andrew's updated mapping of 256 bins down to the 16 result bins with Sample Freq = 10240, samplesFFT = 512 and some overlap. + * Based on testing, the lowest/Start frequency is 60 Hz (with bin 3) and a highest/End frequency of 5120 Hz in bin 255. + * Now, Take the 60Hz and multiply by 1.320367784 to get the next frequency and so on until the end. Then determine the bins. + * End frequency = Start frequency * multiplier ^ 16 + * Multiplier = (End frequency/ Start frequency) ^ 1/16 + * Multiplier = 1.320367784 + */ // Range + fftCalc[ 0] = fftAddAvg(2,4); // 60 - 100 + fftCalc[ 1] = fftAddAvg(4,5); // 80 - 120 + fftCalc[ 2] = fftAddAvg(5,7); // 100 - 160 + fftCalc[ 3] = fftAddAvg(7,9); // 140 - 200 + fftCalc[ 4] = fftAddAvg(9,12); // 180 - 260 + fftCalc[ 5] = fftAddAvg(12,16); // 240 - 340 + fftCalc[ 6] = fftAddAvg(16,21); // 320 - 440 + fftCalc[ 7] = fftAddAvg(21,29); // 420 - 600 + fftCalc[ 8] = fftAddAvg(29,37); // 580 - 760 + fftCalc[ 9] = fftAddAvg(37,48); // 740 - 980 + fftCalc[10] = fftAddAvg(48,64); // 960 - 1300 + fftCalc[11] = fftAddAvg(64,84); // 1280 - 1700 + fftCalc[12] = fftAddAvg(84,111); // 1680 - 2240 + fftCalc[13] = fftAddAvg(111,147); // 2220 - 2960 + fftCalc[14] = fftAddAvg(147,194); // 2940 - 3900 + fftCalc[15] = fftAddAvg(194,250); // 3880 - 5000 // avoid the last 5 bins, which are usually inaccurate +#else + /* new mapping, optimized for 22050 Hz by softhack007 */ + // bins frequency range + if (useBandPassFilter) { + // skip frequencies below 100hz + fftCalc[ 0] = 0.8f * fftAddAvg(3,4); + fftCalc[ 1] = 0.9f * fftAddAvg(4,5); + fftCalc[ 2] = fftAddAvg(5,6); + fftCalc[ 3] = fftAddAvg(6,7); + // don't use the last bins from 206 to 255. + fftCalc[15] = fftAddAvg(165,205) * 0.75f; // 40 7106 - 8828 high -- with some damping + } else { + fftCalc[ 0] = fftAddAvg(1,2); // 1 43 - 86 sub-bass + fftCalc[ 1] = fftAddAvg(2,3); // 1 86 - 129 bass + fftCalc[ 2] = fftAddAvg(3,5); // 2 129 - 216 bass + fftCalc[ 3] = fftAddAvg(5,7); // 2 216 - 301 bass + midrange + // don't use the last bins from 216 to 255. They are usually contaminated by aliasing (aka noise) + fftCalc[15] = fftAddAvg(165,215) * 0.70f; // 50 7106 - 9259 high -- with some damping + } + fftCalc[ 4] = fftAddAvg(7,10); // 3 301 - 430 midrange + fftCalc[ 5] = fftAddAvg(10,13); // 3 430 - 560 midrange + fftCalc[ 6] = fftAddAvg(13,19); // 5 560 - 818 midrange + fftCalc[ 7] = fftAddAvg(19,26); // 7 818 - 1120 midrange -- 1Khz should always be the center ! + fftCalc[ 8] = fftAddAvg(26,33); // 7 1120 - 1421 midrange + fftCalc[ 9] = fftAddAvg(33,44); // 9 1421 - 1895 midrange + fftCalc[10] = fftAddAvg(44,56); // 12 1895 - 2412 midrange + high mid + fftCalc[11] = fftAddAvg(56,70); // 14 2412 - 3015 high mid + fftCalc[12] = fftAddAvg(70,86); // 16 3015 - 3704 high mid + fftCalc[13] = fftAddAvg(86,104); // 18 3704 - 4479 high mid + fftCalc[14] = fftAddAvg(104,165) * 0.88f; // 61 4479 - 7106 high mid + high -- with slight damping +#endif + } else { // noise gate closed - just decay old values + for (int i=0; i < NUM_GEQ_CHANNELS; i++) { + fftCalc[i] *= 0.85f; // decay to zero + if (fftCalc[i] < 4.0f) fftCalc[i] = 0.0f; + } + } + + // post-processing of frequency channels (pink noise adjustment, AGC, smoothing, scaling) + postProcessFFTResults((fabsf(sampleAvg) > 0.25f)? true : false , NUM_GEQ_CHANNELS); + +#if defined(WLED_DEBUG) || defined(SR_DEBUG) + if (haveDoneFFT && (start < esp_timer_get_time())) { // filter out overflows + uint64_t fftTimeInMillis = ((esp_timer_get_time() - start) +5ULL) / 10ULL; // "+5" to ensure proper rounding + fftTime = (fftTimeInMillis*3 + fftTime*7)/10; // smooth + } +#endif + // run peak detection + autoResetPeak(); + detectSamplePeak(); + + #if !defined(I2S_GRAB_ADC1_COMPLETELY) + if ((audioSource == nullptr) || (audioSource->getType() != AudioSource::Type_I2SAdc)) // the "delay trick" does not help for analog ADC + #endif + vTaskDelayUntil( &xLastWakeTime, xFrequency); // release CPU, and let I2S fill its buffers + + } // for(;;)ever +} // FFTcode() task end + + +/////////////////////////// +// Pre / Postprocessing // +/////////////////////////// + +static void runMicFilter(uint16_t numSamples, float *sampleBuffer) // pre-filtering of raw samples (band-pass) +{ + // low frequency cutoff parameter - see https://dsp.stackexchange.com/questions/40462/exponential-moving-average-cut-off-frequency + //constexpr float alpha = 0.04f; // 150Hz + //constexpr float alpha = 0.03f; // 110Hz + constexpr float alpha = 0.0225f; // 80hz + //constexpr float alpha = 0.01693f;// 60hz + // high frequency cutoff parameter + //constexpr float beta1 = 0.75f; // 11Khz + //constexpr float beta1 = 0.82f; // 15Khz + //constexpr float beta1 = 0.8285f; // 18Khz + constexpr float beta1 = 0.85f; // 20Khz + + constexpr float beta2 = (1.0f - beta1) / 2.0f; + static float last_vals[2] = { 0.0f }; // FIR high freq cutoff filter + static float lowfilt = 0.0f; // IIR low frequency cutoff filter + + for (int i=0; i < numSamples; i++) { + // FIR lowpass, to remove high frequency noise + float highFilteredSample; + if (i < (numSamples-1)) highFilteredSample = beta1*sampleBuffer[i] + beta2*last_vals[0] + beta2*sampleBuffer[i+1]; // smooth out spikes + else highFilteredSample = beta1*sampleBuffer[i] + beta2*last_vals[0] + beta2*last_vals[1]; // special handling for last sample in array + last_vals[1] = last_vals[0]; + last_vals[0] = sampleBuffer[i]; + sampleBuffer[i] = highFilteredSample; + // IIR highpass, to remove low frequency noise + lowfilt += alpha * (sampleBuffer[i] - lowfilt); + sampleBuffer[i] = sampleBuffer[i] - lowfilt; + } +} + +static void postProcessFFTResults(bool noiseGateOpen, int numberOfChannels) // post-processing and post-amp of GEQ channels +{ + for (int i=0; i < numberOfChannels; i++) { + + if (noiseGateOpen) { // noise gate open + // Adjustment for frequency curves. + fftCalc[i] *= fftResultPink[i]; + if (FFTScalingMode > 0) fftCalc[i] *= FFT_DOWNSCALE; // adjustment related to FFT windowing function + // Manual linear adjustment of gain using sampleGain adjustment for different input types. + fftCalc[i] *= soundAgc ? multAgc : ((float)sampleGain/40.0f * (float)inputLevel/128.0f + 1.0f/16.0f); //apply gain, with inputLevel adjustment + if(fftCalc[i] < 0) fftCalc[i] = 0.0f; + } + + // smooth results - rise fast, fall slower + if (fftCalc[i] > fftAvg[i]) fftAvg[i] = fftCalc[i]*0.75f + 0.25f*fftAvg[i]; // rise fast; will need approx 2 cycles (50ms) for converging against fftCalc[i] + else { // fall slow + if (decayTime < 1000) fftAvg[i] = fftCalc[i]*0.22f + 0.78f*fftAvg[i]; // approx 5 cycles (225ms) for falling to zero + else if (decayTime < 2000) fftAvg[i] = fftCalc[i]*0.17f + 0.83f*fftAvg[i]; // default - approx 9 cycles (225ms) for falling to zero + else if (decayTime < 3000) fftAvg[i] = fftCalc[i]*0.14f + 0.86f*fftAvg[i]; // approx 14 cycles (350ms) for falling to zero + else fftAvg[i] = fftCalc[i]*0.1f + 0.9f*fftAvg[i]; // approx 20 cycles (500ms) for falling to zero + } + // constrain internal vars - just to be sure + fftCalc[i] = constrain(fftCalc[i], 0.0f, 1023.0f); + fftAvg[i] = constrain(fftAvg[i], 0.0f, 1023.0f); + + float currentResult; + if(limiterOn == true) + currentResult = fftAvg[i]; + else + currentResult = fftCalc[i]; + + switch (FFTScalingMode) { + case 1: + // Logarithmic scaling + currentResult *= 0.42f; // 42 is the answer ;-) + currentResult -= 8.0f; // this skips the lowest row, giving some room for peaks + if (currentResult > 1.0f) currentResult = logf(currentResult); // log to base "e", which is the fastest log() function + else currentResult = 0.0f; // special handling, because log(1) = 0; log(0) = undefined + currentResult *= 0.85f + (float(i)/18.0f); // extra up-scaling for high frequencies + currentResult = mapf(currentResult, 0.0f, LOG_256, 0.0f, 255.0f); // map [log(1) ... log(255)] to [0 ... 255] + break; + case 2: + // Linear scaling + currentResult *= 0.30f; // needs a bit more damping, get stay below 255 + currentResult -= 4.0f; // giving a bit more room for peaks (WLEDMM uses -2) + if (currentResult < 1.0f) currentResult = 0.0f; + currentResult *= 0.85f + (float(i)/1.8f); // extra up-scaling for high frequencies + break; + case 3: + // square root scaling + currentResult *= 0.38f; + currentResult -= 6.0f; + if (currentResult > 1.0f) currentResult = sqrtf(currentResult); + else currentResult = 0.0f; // special handling, because sqrt(0) = undefined + currentResult *= 0.85f + (float(i)/4.5f); // extra up-scaling for high frequencies + currentResult = mapf(currentResult, 0.0f, 16.0f, 0.0f, 255.0f); // map [sqrt(1) ... sqrt(256)] to [0 ... 255] + break; + + case 0: + default: + // no scaling - leave freq bins as-is + currentResult -= 4; // just a bit more room for peaks (WLEDMM uses -2) + break; + } + + // Now, let's dump it all into fftResult. Need to do this, otherwise other routines might grab fftResult values prematurely. + if (soundAgc > 0) { // apply extra "GEQ Gain" if set by user + float post_gain = (float)inputLevel/128.0f; + if (post_gain < 1.0f) post_gain = ((post_gain -1.0f) * 0.8f) +1.0f; + currentResult *= post_gain; + } + fftResult[i] = constrain((int)currentResult, 0, 255); + } +} +//////////////////// +// Peak detection // +//////////////////// + +// peak detection is called from FFT task when vReal[] contains valid FFT results +static void detectSamplePeak(void) { + bool havePeak = false; + // softhack007: this code continuously triggers while amplitude in the selected bin is above a certain threshold. So it does not detect peaks - it detects high activity in a frequency bin. + // Poor man's beat detection by seeing if sample > Average + some value. + // This goes through ALL of the 255 bins - but ignores stupid settings + // Then we got a peak, else we don't. The peak has to time out on its own in order to support UDP sound sync. + if ((sampleAvg > 1) && (maxVol > 0) && (binNum > 4) && (vReal[binNum] > maxVol) && ((millis() - timeOfPeak) > 100)) { + havePeak = true; + } + + if (havePeak) { + samplePeak = true; + timeOfPeak = millis(); + udpSamplePeak = true; + } +} + +#endif + +static void autoResetPeak(void) { + uint16_t MinShowDelay = MAX(50, WS2812FX::getMinShowDelay()); // Fixes private class variable compiler error. Unsure if this is the correct way of fixing the root problem. -THATDONFC + if (millis() - timeOfPeak > MinShowDelay) { // Auto-reset of samplePeak after a complete frame has passed. + samplePeak = false; + if (audioSyncEnabled == 0) udpSamplePeak = false; // this is normally reset by transmitAudioData + } +} + + +//////////////////// +// usermod class // +//////////////////// + +//class name. Use something descriptive and leave the ": public Usermod" part :) +class AudioReactive : public Usermod { + + private: +#ifdef ARDUINO_ARCH_ESP32 + + #ifndef AUDIOPIN + int8_t audioPin = -1; + #else + int8_t audioPin = AUDIOPIN; + #endif + #ifndef SR_DMTYPE // I2S mic type + uint8_t dmType = 1; // 0=none/disabled/analog; 1=generic I2S + #define SR_DMTYPE 1 // default type = I2S + #else + uint8_t dmType = SR_DMTYPE; + #endif + #ifndef I2S_SDPIN // aka DOUT + int8_t i2ssdPin = 32; + #else + int8_t i2ssdPin = I2S_SDPIN; + #endif + #ifndef I2S_WSPIN // aka LRCL + int8_t i2swsPin = 15; + #else + int8_t i2swsPin = I2S_WSPIN; + #endif + #ifndef I2S_CKPIN // aka BCLK + int8_t i2sckPin = 14; /*PDM: set to I2S_PIN_NO_CHANGE*/ + #else + int8_t i2sckPin = I2S_CKPIN; + #endif + #ifndef MCLK_PIN + int8_t mclkPin = I2S_PIN_NO_CHANGE; /* ESP32: only -1, 0, 1, 3 allowed*/ + #else + int8_t mclkPin = MCLK_PIN; + #endif +#endif + + // new "V2" audiosync struct - 44 Bytes + struct __attribute__ ((packed)) audioSyncPacket { // "packed" ensures that there are no additional gaps + char header[6]; // 06 Bytes offset 0 + uint8_t reserved1[2]; // 02 Bytes, offset 6 - gap required by the compiler - not used yet + float sampleRaw; // 04 Bytes offset 8 - either "sampleRaw" or "rawSampleAgc" depending on soundAgc setting + float sampleSmth; // 04 Bytes offset 12 - either "sampleAvg" or "sampleAgc" depending on soundAgc setting + uint8_t samplePeak; // 01 Bytes offset 16 - 0 no peak; >=1 peak detected. In future, this will also provide peak Magnitude + uint8_t reserved2; // 01 Bytes offset 17 - for future extensions - not used yet + uint8_t fftResult[16]; // 16 Bytes offset 18 + uint16_t reserved3; // 02 Bytes, offset 34 - gap required by the compiler - not used yet + float FFT_Magnitude; // 04 Bytes offset 36 + float FFT_MajorPeak; // 04 Bytes offset 40 + }; + + // old "V1" audiosync struct - 83 Bytes payload, 88 bytes total (with padding added by compiler) - for backwards compatibility + struct audioSyncPacket_v1 { + char header[6]; // 06 Bytes + uint8_t myVals[32]; // 32 Bytes + int sampleAgc; // 04 Bytes + int sampleRaw; // 04 Bytes + float sampleAvg; // 04 Bytes + bool samplePeak; // 01 Bytes + uint8_t fftResult[16]; // 16 Bytes + double FFT_Magnitude; // 08 Bytes + double FFT_MajorPeak; // 08 Bytes + }; + + constexpr static unsigned UDPSOUND_MAX_PACKET = MAX(sizeof(audioSyncPacket), sizeof(audioSyncPacket_v1)); + + // set your config variables to their boot default value (this can also be done in readFromConfig() or a constructor if you prefer) + #ifdef UM_AUDIOREACTIVE_ENABLE + bool enabled = true; + #else + bool enabled = false; + #endif + + bool initDone = false; + bool addPalettes = false; + int8_t palettes = 0; + + // variables for UDP sound sync + WiFiUDP fftUdp; // UDP object for sound sync (from WiFi UDP, not Async UDP!) + unsigned long lastTime = 0; // last time of running UDP Microphone Sync + const uint16_t delayMs = 10; // I don't want to sample too often and overload WLED + uint16_t audioSyncPort= 11988;// default port for UDP sound sync + + bool updateIsRunning = false; // true during OTA. + +#ifdef ARDUINO_ARCH_ESP32 + // used for AGC + int last_soundAgc = -1; // used to detect AGC mode change (for resetting AGC internal error buffers) + float control_integrated = 0.0f; // persistent across calls to agcAvg(); "integrator control" = accumulated error + // variables used by getSample() and agcAvg() + int16_t micIn = 0; // Current sample starts with negative values and large values, which is why it's 16 bit signed + float sampleMax = 0.0f; // Max sample over a few seconds. Needed for AGC controller. + float micLev = 0.0f; // Used to convert returned value to have '0' as minimum. A leveller + float expAdjF = 0.0f; // Used for exponential filter. + float sampleReal = 0.0f; // "sampleRaw" as float, to provide bits that are lost otherwise (before amplification by sampleGain or inputLevel). Needed for AGC. + int16_t sampleRaw = 0; // Current sample. Must only be updated ONCE!!! (amplified mic value by sampleGain and inputLevel) + int16_t rawSampleAgc = 0; // not smoothed AGC sample +#endif + + // variables used in effects + float volumeSmth = 0.0f; // either sampleAvg or sampleAgc depending on soundAgc; smoothed sample + int16_t volumeRaw = 0; // either sampleRaw or rawSampleAgc depending on soundAgc + float my_magnitude =0.0f; // FFT_Magnitude, scaled by multAgc + + // used to feed "Info" Page + unsigned long last_UDPTime = 0; // time of last valid UDP sound sync datapacket + int receivedFormat = 0; // last received UDP sound sync format - 0=none, 1=v1 (0.13.x), 2=v2 (0.14.x) + float maxSample5sec = 0.0f; // max sample (after AGC) in last 5 seconds + unsigned long sampleMaxTimer = 0; // last time maxSample5sec was reset + #define CYCLE_SAMPLEMAX 3500 // time window for merasuring + + // strings to reduce flash memory usage (used more than twice) + static const char _name[]; + static const char _enabled[]; + static const char _config[]; + static const char _dynamics[]; + static const char _frequency[]; + static const char _inputLvl[]; +#if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) + static const char _analogmic[]; +#endif + static const char _digitalmic[]; + static const char _addPalettes[]; + static const char UDP_SYNC_HEADER[]; + static const char UDP_SYNC_HEADER_v1[]; + + // private methods + void removeAudioPalettes(void); + void createAudioPalettes(void); + CRGB getCRGBForBand(int x, int pal); + void fillAudioPalettes(void); + + //////////////////// + // Debug support // + //////////////////// + void logAudio() + { + if (disableSoundProcessing && (!udpSyncConnected || ((audioSyncEnabled & 0x02) == 0))) return; // no audio availeable + #ifdef MIC_LOGGER + // Debugging functions for audio input and sound processing. Comment out the values you want to see + PLOT_PRINT("micReal:"); PLOT_PRINT(micDataReal); PLOT_PRINT("\t"); + PLOT_PRINT("volumeSmth:"); PLOT_PRINT(volumeSmth); PLOT_PRINT("\t"); + //PLOT_PRINT("volumeRaw:"); PLOT_PRINT(volumeRaw); PLOT_PRINT("\t"); + PLOT_PRINT("DC_Level:"); PLOT_PRINT(micLev); PLOT_PRINT("\t"); + //PLOT_PRINT("sampleAgc:"); PLOT_PRINT(sampleAgc); PLOT_PRINT("\t"); + //PLOT_PRINT("sampleAvg:"); PLOT_PRINT(sampleAvg); PLOT_PRINT("\t"); + //PLOT_PRINT("sampleReal:"); PLOT_PRINT(sampleReal); PLOT_PRINT("\t"); + #ifdef ARDUINO_ARCH_ESP32 + //PLOT_PRINT("micIn:"); PLOT_PRINT(micIn); PLOT_PRINT("\t"); + //PLOT_PRINT("sample:"); PLOT_PRINT(sample); PLOT_PRINT("\t"); + //PLOT_PRINT("sampleMax:"); PLOT_PRINT(sampleMax); PLOT_PRINT("\t"); + //PLOT_PRINT("samplePeak:"); PLOT_PRINT((samplePeak!=0) ? 128:0); PLOT_PRINT("\t"); + //PLOT_PRINT("multAgc:"); PLOT_PRINT(multAgc, 4); PLOT_PRINT("\t"); + #endif + PLOT_PRINTLN(); + #endif + + #ifdef FFT_SAMPLING_LOG + #if 0 + for(int i=0; i maxVal) maxVal = fftResult[i]; + if(fftResult[i] < minVal) minVal = fftResult[i]; + } + for(int i = 0; i < NUM_GEQ_CHANNELS; i++) { + PLOT_PRINT(i); PLOT_PRINT(":"); + PLOT_PRINTF("%04ld ", map(fftResult[i], 0, (scaleValuesFromCurrentMaxVal ? maxVal : defaultScalingFromHighValue), (mapValuesToPlotterSpace*i*scalingToHighValue)+0, (mapValuesToPlotterSpace*i*scalingToHighValue)+scalingToHighValue-1)); + } + if(printMaxVal) { + PLOT_PRINTF("maxVal:%04d ", maxVal + (mapValuesToPlotterSpace ? 16*256 : 0)); + } + if(printMinVal) { + PLOT_PRINTF("%04d:minVal ", minVal); // printed with value first, then label, so negative values can be seen in Serial Monitor but don't throw off y axis in Serial Plotter + } + if(mapValuesToPlotterSpace) + PLOT_PRINTF("max:%04d ", (printMaxVal ? 17 : 16)*256); // print line above the maximum value we expect to see on the plotter to avoid autoscaling y axis + else { + PLOT_PRINTF("max:%04d ", 256); + } + PLOT_PRINTLN(); + #endif // FFT_SAMPLING_LOG + } // logAudio() + + +#ifdef ARDUINO_ARCH_ESP32 + ////////////////////// + // Audio Processing // + ////////////////////// + + /* + * A "PI controller" multiplier to automatically adjust sound sensitivity. + * + * A few tricks are implemented so that sampleAgc does't only utilize 0% and 100%: + * 0. don't amplify anything below squelch (but keep previous gain) + * 1. gain input = maximum signal observed in the last 5-10 seconds + * 2. we use two setpoints, one at ~60%, and one at ~80% of the maximum signal + * 3. the amplification depends on signal level: + * a) normal zone - very slow adjustment + * b) emergency zone (<10% or >90%) - very fast adjustment + */ + void agcAvg(unsigned long the_time) + { + const int AGC_preset = (soundAgc > 0)? (soundAgc-1): 0; // make sure the _compiler_ knows this value will not change while we are inside the function + + float lastMultAgc = multAgc; // last multiplier used + float multAgcTemp = multAgc; // new multiplier + float tmpAgc = sampleReal * multAgc; // what-if amplified signal + + float control_error; // "control error" input for PI control + + if (last_soundAgc != soundAgc) control_integrated = 0.0f; // new preset - reset integrator + + // For PI controller, we need to have a constant "frequency" + // so let's make sure that the control loop is not running at insane speed + static unsigned long last_time = 0; + unsigned long time_now = millis(); + if ((the_time > 0) && (the_time < time_now)) time_now = the_time; // allow caller to override my clock + + if (time_now - last_time > 2) { + last_time = time_now; + + if ((fabsf(sampleReal) < 2.0f) || (sampleMax < 1.0f)) { + // MIC signal is "squelched" - deliver silence + tmpAgc = 0; + // we need to "spin down" the intgrated error buffer + if (fabs(control_integrated) < 0.01f) control_integrated = 0.0f; + else control_integrated *= 0.91f; + } else { + // compute new setpoint + if (tmpAgc <= agcTarget0Up[AGC_preset]) + multAgcTemp = agcTarget0[AGC_preset] / sampleMax; // Make the multiplier so that sampleMax * multiplier = first setpoint + else + multAgcTemp = agcTarget1[AGC_preset] / sampleMax; // Make the multiplier so that sampleMax * multiplier = second setpoint + } + // limit amplification + if (multAgcTemp > 32.0f) multAgcTemp = 32.0f; + if (multAgcTemp < 1.0f/64.0f) multAgcTemp = 1.0f/64.0f; + + // compute error terms + control_error = multAgcTemp - lastMultAgc; + + if (((multAgcTemp > 0.085f) && (multAgcTemp < 6.5f)) //integrator anti-windup by clamping + && (multAgc*sampleMax < agcZoneStop[AGC_preset])) //integrator ceiling (>140% of max) + control_integrated += control_error * 0.002f * 0.25f; // 2ms = integration time; 0.25 for damping + else + control_integrated *= 0.9f; // spin down that beasty integrator + + // apply PI Control + tmpAgc = sampleReal * lastMultAgc; // check "zone" of the signal using previous gain + if ((tmpAgc > agcZoneHigh[AGC_preset]) || (tmpAgc < soundSquelch + agcZoneLow[AGC_preset])) { // upper/lower energy zone + multAgcTemp = lastMultAgc + agcFollowFast[AGC_preset] * agcControlKp[AGC_preset] * control_error; + multAgcTemp += agcFollowFast[AGC_preset] * agcControlKi[AGC_preset] * control_integrated; + } else { // "normal zone" + multAgcTemp = lastMultAgc + agcFollowSlow[AGC_preset] * agcControlKp[AGC_preset] * control_error; + multAgcTemp += agcFollowSlow[AGC_preset] * agcControlKi[AGC_preset] * control_integrated; + } + + // limit amplification again - PI controller sometimes "overshoots" + //multAgcTemp = constrain(multAgcTemp, 0.015625f, 32.0f); // 1/64 < multAgcTemp < 32 + if (multAgcTemp > 32.0f) multAgcTemp = 32.0f; + if (multAgcTemp < 1.0f/64.0f) multAgcTemp = 1.0f/64.0f; + } + + // NOW finally amplify the signal + tmpAgc = sampleReal * multAgcTemp; // apply gain to signal + if (fabsf(sampleReal) < 2.0f) tmpAgc = 0.0f; // apply squelch threshold + //tmpAgc = constrain(tmpAgc, 0, 255); + if (tmpAgc > 255) tmpAgc = 255.0f; // limit to 8bit + if (tmpAgc < 1) tmpAgc = 0.0f; // just to be sure + + // update global vars ONCE - multAgc, sampleAGC, rawSampleAgc + multAgc = multAgcTemp; + rawSampleAgc = 0.8f * tmpAgc + 0.2f * (float)rawSampleAgc; + // update smoothed AGC sample + if (fabsf(tmpAgc) < 1.0f) + sampleAgc = 0.5f * tmpAgc + 0.5f * sampleAgc; // fast path to zero + else + sampleAgc += agcSampleSmooth[AGC_preset] * (tmpAgc - sampleAgc); // smooth path + + sampleAgc = fabsf(sampleAgc); // // make sure we have a positive value + last_soundAgc = soundAgc; + } // agcAvg() + + // post-processing and filtering of MIC sample (micDataReal) from FFTcode() + void getSample() + { + float sampleAdj; // Gain adjusted sample value + float tmpSample; // An interim sample variable used for calculations. + const float weighting = 0.2f; // Exponential filter weighting. Will be adjustable in a future release. + const int AGC_preset = (soundAgc > 0)? (soundAgc-1): 0; // make sure the _compiler_ knows this value will not change while we are inside the function + + #ifdef WLED_DISABLE_SOUND + micIn = inoise8(millis(), millis()); // Simulated analog read + micDataReal = micIn; + #else + #ifdef ARDUINO_ARCH_ESP32 + micIn = int(micDataReal); // micDataSm = ((micData * 3) + micData)/4; + #else + // this is the minimal code for reading analog mic input on 8266. + // warning!! Absolutely experimental code. Audio on 8266 is still not working. Expects a million follow-on problems. + static unsigned long lastAnalogTime = 0; + static float lastAnalogValue = 0.0f; + if (millis() - lastAnalogTime > 20) { + micDataReal = analogRead(A0); // read one sample with 10bit resolution. This is a dirty hack, supporting volumereactive effects only. + lastAnalogTime = millis(); + lastAnalogValue = micDataReal; + yield(); + } else micDataReal = lastAnalogValue; + micIn = int(micDataReal); + #endif + #endif + + micLev += (micDataReal-micLev) / 12288.0f; + if (micIn < micLev) micLev = ((micLev * 31.0f) + micDataReal) / 32.0f; // align micLev to lowest input signal + + micIn -= micLev; // Let's center it to 0 now + // Using an exponential filter to smooth out the signal. We'll add controls for this in a future release. + float micInNoDC = fabsf(micDataReal - micLev); + expAdjF = (weighting * micInNoDC + (1.0f-weighting) * expAdjF); + expAdjF = fabsf(expAdjF); // Now (!) take the absolute value + + expAdjF = (expAdjF <= soundSquelch) ? 0.0f : expAdjF; // simple noise gate + if ((soundSquelch == 0) && (expAdjF < 0.25f)) expAdjF = 0.0f; // do something meaningfull when "squelch = 0" + + tmpSample = expAdjF; + micIn = abs(micIn); // And get the absolute value of each sample + + sampleAdj = tmpSample * sampleGain * inputLevel / 5120.0f /* /40 /128 */ + tmpSample / 16.0f; // Adjust the gain. with inputLevel adjustment + sampleReal = tmpSample; + + sampleAdj = fmax(fmin(sampleAdj, 255.0f), 0.0f); // Question: why are we limiting the value to 8 bits ??? + sampleRaw = (int16_t)sampleAdj; // ONLY update sample ONCE!!!! + + // keep "peak" sample, but decay value if current sample is below peak + if ((sampleMax < sampleReal) && (sampleReal > 0.5f)) { + sampleMax += 0.5f * (sampleReal - sampleMax); // new peak - with some filtering + // another simple way to detect samplePeak - cannot detect beats, but reacts on peak volume + if (((binNum < 12) || ((maxVol < 1))) && (millis() - timeOfPeak > 80) && (sampleAvg > 1)) { + samplePeak = true; + timeOfPeak = millis(); + udpSamplePeak = true; + } + } else { + if ((multAgc*sampleMax > agcZoneStop[AGC_preset]) && (soundAgc > 0)) + sampleMax += 0.5f * (sampleReal - sampleMax); // over AGC Zone - get back quickly + else + sampleMax *= agcSampleDecay[AGC_preset]; // signal to zero --> 5-8sec + } + if (sampleMax < 0.5f) sampleMax = 0.0f; + + sampleAvg = ((sampleAvg * 15.0f) + sampleAdj) / 16.0f; // Smooth it out over the last 16 samples. + sampleAvg = fabsf(sampleAvg); // make sure we have a positive value + } // getSample() + +#endif + + /* Limits the dynamics of volumeSmth (= sampleAvg or sampleAgc). + * does not affect FFTResult[] or volumeRaw ( = sample or rawSampleAgc) + */ + // effects: Gravimeter, Gravcenter, Gravcentric, Noisefire, Plasmoid, Freqpixels, Freqwave, Gravfreq, (2D Swirl, 2D Waverly) + void limitSampleDynamics(void) { + const float bigChange = 196.0f; // just a representative number - a large, expected sample value + static unsigned long last_time = 0; + static float last_volumeSmth = 0.0f; + + if (limiterOn == false) return; + + long delta_time = millis() - last_time; + delta_time = constrain(delta_time , 1, 1000); // below 1ms -> 1ms; above 1sec -> sily lil hick-up + float deltaSample = volumeSmth - last_volumeSmth; + + if (attackTime > 0) { // user has defined attack time > 0 + float maxAttack = bigChange * float(delta_time) / float(attackTime); + if (deltaSample > maxAttack) deltaSample = maxAttack; + } + if (decayTime > 0) { // user has defined decay time > 0 + float maxDecay = - bigChange * float(delta_time) / float(decayTime); + if (deltaSample < maxDecay) deltaSample = maxDecay; + } + + volumeSmth = last_volumeSmth + deltaSample; + + last_volumeSmth = volumeSmth; + last_time = millis(); + } + + + ////////////////////// + // UDP Sound Sync // + ////////////////////// + + // try to establish UDP sound sync connection + void connectUDPSoundSync(void) { + // This function tries to establish a UDP sync connection if needed + // necessary as we also want to transmit in "AP Mode", but the standard "connected()" callback only reacts on STA connection + static unsigned long last_connection_attempt = 0; + + if ((audioSyncPort <= 0) || ((audioSyncEnabled & 0x03) == 0)) return; // Sound Sync not enabled + if (udpSyncConnected) return; // already connected + if (!(apActive || interfacesInited)) return; // neither AP nor other connections availeable + if (millis() - last_connection_attempt < 15000) return; // only try once in 15 seconds + if (updateIsRunning) return; + + // if we arrive here, we need a UDP connection but don't have one + last_connection_attempt = millis(); + connected(); // try to start UDP + } + +#ifdef ARDUINO_ARCH_ESP32 + void transmitAudioData() + { + //DEBUGSR_PRINTLN("Transmitting UDP Mic Packet"); + + audioSyncPacket transmitData; + memset(reinterpret_cast(&transmitData), 0, sizeof(transmitData)); // make sure that the packet - including "invisible" padding bytes added by the compiler - is fully initialized + + strncpy_P(transmitData.header, PSTR(UDP_SYNC_HEADER), 6); + // transmit samples that were not modified by limitSampleDynamics() + transmitData.sampleRaw = (soundAgc) ? rawSampleAgc: sampleRaw; + transmitData.sampleSmth = (soundAgc) ? sampleAgc : sampleAvg; + transmitData.samplePeak = udpSamplePeak ? 1:0; + udpSamplePeak = false; // Reset udpSamplePeak after we've transmitted it + + for (int i = 0; i < NUM_GEQ_CHANNELS; i++) { + transmitData.fftResult[i] = (uint8_t)constrain(fftResult[i], 0, 254); + } + + transmitData.FFT_Magnitude = my_magnitude; + transmitData.FFT_MajorPeak = FFT_MajorPeak; + +#ifndef WLED_DISABLE_ESPNOW + if (useESPNowSync && statusESPNow == ESP_NOW_STATE_ON) { + EspNowPartialPacket buffer = {{'W','L','E','D'}, 0, 1, {0}}; + //DEBUGSR_PRINTLN(F("ESP-NOW Sending audio packet.")); + size_t packetSize = sizeof(EspNowPartialPacket) - sizeof(EspNowPartialPacket::data) + sizeof(transmitData); + memcpy(buffer.data, &transmitData, sizeof(transmitData)); + quickEspNow.send(ESPNOW_BROADCAST_ADDRESS, reinterpret_cast(&buffer), packetSize); + } +#endif + + if (udpSyncConnected && fftUdp.beginMulticastPacket() != 0) { // beginMulticastPacket returns 0 in case of error + fftUdp.write(reinterpret_cast(&transmitData), sizeof(transmitData)); + fftUdp.endPacket(); + } + return; + } // transmitAudioData() + +#endif + + static inline bool isValidUdpSyncVersion(const char *header) { + return strncmp_P(header, UDP_SYNC_HEADER, 6) == 0; + } + static inline bool isValidUdpSyncVersion_v1(const char *header) { + return strncmp_P(header, UDP_SYNC_HEADER_v1, 6) == 0; + } + + void decodeAudioData(int packetSize, uint8_t *fftBuff) { + audioSyncPacket receivedPacket; + memset(&receivedPacket, 0, sizeof(receivedPacket)); // start clean + memcpy(&receivedPacket, fftBuff, min((unsigned)packetSize, (unsigned)sizeof(receivedPacket))); // don't violate alignment - thanks @willmmiles# + + // update samples for effects + volumeSmth = fmaxf(receivedPacket.sampleSmth, 0.0f); + volumeRaw = fmaxf(receivedPacket.sampleRaw, 0.0f); +#ifdef ARDUINO_ARCH_ESP32 + // update internal samples + sampleRaw = volumeRaw; + sampleAvg = volumeSmth; + rawSampleAgc = volumeRaw; + sampleAgc = volumeSmth; + multAgc = 1.0f; +#endif + // Only change samplePeak IF it's currently false. + // If it's true already, then the animation still needs to respond. + autoResetPeak(); + if (!samplePeak) { + samplePeak = receivedPacket.samplePeak > 0; + if (samplePeak) timeOfPeak = millis(); + } + //These values are only computed by ESP32 + for (int i = 0; i < NUM_GEQ_CHANNELS; i++) fftResult[i] = receivedPacket.fftResult[i]; + my_magnitude = fmaxf(receivedPacket.FFT_Magnitude, 0.0f); + FFT_Magnitude = my_magnitude; + FFT_MajorPeak = constrain(receivedPacket.FFT_MajorPeak, 1.0f, 11025.0f); // restrict value to range expected by effects + } + + void decodeAudioData_v1(int packetSize, uint8_t *fftBuff) { + audioSyncPacket_v1 *receivedPacket = reinterpret_cast(fftBuff); + // update samples for effects + volumeSmth = fmaxf(receivedPacket->sampleAgc, 0.0f); + volumeRaw = volumeSmth; // V1 format does not have "raw" AGC sample +#ifdef ARDUINO_ARCH_ESP32 + // update internal samples + sampleRaw = fmaxf(receivedPacket->sampleRaw, 0.0f); + sampleAvg = fmaxf(receivedPacket->sampleAvg, 0.0f);; + sampleAgc = volumeSmth; + rawSampleAgc = volumeRaw; + multAgc = 1.0f; +#endif + // Only change samplePeak IF it's currently false. + // If it's true already, then the animation still needs to respond. + autoResetPeak(); + if (!samplePeak) { + samplePeak = receivedPacket->samplePeak > 0; + if (samplePeak) timeOfPeak = millis(); + } + //These values are only available on the ESP32 + for (int i = 0; i < NUM_GEQ_CHANNELS; i++) fftResult[i] = receivedPacket->fftResult[i]; + my_magnitude = fmaxf(receivedPacket->FFT_Magnitude, 0.0f); + FFT_Magnitude = my_magnitude; + FFT_MajorPeak = constrain(receivedPacket->FFT_MajorPeak, 1.0f, 11025.0f); // restrict value to range expected by effects + } + + bool receiveAudioData() // check & process new data. return TRUE in case that new audio data was received. + { + if (!udpSyncConnected) return false; + bool haveFreshData = false; + + size_t packetSize = fftUdp.parsePacket(); +#ifdef ARDUINO_ARCH_ESP32 + if ((packetSize > 0) && ((packetSize < 5) || (packetSize > UDPSOUND_MAX_PACKET))) fftUdp.flush(); // discard invalid packets (too small or too big) - only works on esp32 +#endif + if ((packetSize > 5) && (packetSize <= UDPSOUND_MAX_PACKET)) { + //DEBUGSR_PRINTLN("Received UDP Sync Packet"); + uint8_t fftBuff[UDPSOUND_MAX_PACKET+1] = { 0 }; // fixed-size buffer for receiving (stack), to avoid heap fragmentation caused by variable sized arrays + fftUdp.read(fftBuff, packetSize); + + // VERIFY THAT THIS IS A COMPATIBLE PACKET + if (packetSize == sizeof(audioSyncPacket) && (isValidUdpSyncVersion((const char *)fftBuff))) { + decodeAudioData(packetSize, fftBuff); + //DEBUGSR_PRINTLN("Finished parsing UDP Sync Packet v2"); + haveFreshData = true; + receivedFormat = 2; + } else { + if (packetSize == sizeof(audioSyncPacket_v1) && (isValidUdpSyncVersion_v1((const char *)fftBuff))) { + decodeAudioData_v1(packetSize, fftBuff); + //DEBUGSR_PRINTLN("Finished parsing UDP Sync Packet v1"); + haveFreshData = true; + receivedFormat = 1; + } else receivedFormat = 0; // unknown format + } + } + return haveFreshData; + } + + + ////////////////////// + // usermod functions// + ////////////////////// + + public: + //Functions called by WLED or other usermods + + /* + * setup() is called once at boot. WiFi is not yet connected at this point. + * You can use it to initialize variables, sensors or similar. + * It is called *AFTER* readFromConfig() + */ + void setup() override + { + disableSoundProcessing = true; // just to be sure + if (!initDone) { + // usermod exchangeable data + // we will assign all usermod exportable data here as pointers to original variables or arrays and allocate memory for pointers + um_data = new um_data_t; + um_data->u_size = 8; + um_data->u_type = new um_types_t[um_data->u_size]; + um_data->u_data = new void*[um_data->u_size]; + um_data->u_data[0] = &volumeSmth; //*used (New) + um_data->u_type[0] = UMT_FLOAT; + um_data->u_data[1] = &volumeRaw; // used (New) + um_data->u_type[1] = UMT_UINT16; + um_data->u_data[2] = fftResult; //*used (Blurz, DJ Light, Noisemove, GEQ_base, 2D Funky Plank, Akemi) + um_data->u_type[2] = UMT_BYTE_ARR; + um_data->u_data[3] = &samplePeak; //*used (Puddlepeak, Ripplepeak, Waterfall) + um_data->u_type[3] = UMT_BYTE; + um_data->u_data[4] = &FFT_MajorPeak; //*used (Ripplepeak, Freqmap, Freqmatrix, Freqpixels, Freqwave, Gravfreq, Rocktaves, Waterfall) + um_data->u_type[4] = UMT_FLOAT; + um_data->u_data[5] = &my_magnitude; // used (New) + um_data->u_type[5] = UMT_FLOAT; + um_data->u_data[6] = &maxVol; // assigned in effect function from UI element!!! (Puddlepeak, Ripplepeak, Waterfall) + um_data->u_type[6] = UMT_BYTE; + um_data->u_data[7] = &binNum; // assigned in effect function from UI element!!! (Puddlepeak, Ripplepeak, Waterfall) + um_data->u_type[7] = UMT_BYTE; + } + + +#ifdef ARDUINO_ARCH_ESP32 + + // Reset I2S peripheral for good measure + i2s_driver_uninstall(I2S_NUM_0); // E (696) I2S: i2s_driver_uninstall(2006): I2S port 0 has not installed + #if !defined(CONFIG_IDF_TARGET_ESP32C3) + delay(100); + periph_module_reset(PERIPH_I2S0_MODULE); // not possible on -C3 + #endif + delay(100); // Give that poor microphone some time to setup. + + useBandPassFilter = false; + + #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) + if ((i2sckPin == I2S_PIN_NO_CHANGE) && (i2ssdPin >= 0) && (i2swsPin >= 0) && ((dmType == 1) || (dmType == 4)) ) dmType = 5; // dummy user support: SCK == -1 --means--> PDM microphone + #endif + + switch (dmType) { + #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3) + // stub cases for not-yet-supported I2S modes on other ESP32 chips + case 0: //ADC analog + #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) + case 5: //PDM Microphone + #endif + #endif + case 1: + DEBUGSR_PRINT(F("AR: Generic I2S Microphone - ")); DEBUGSR_PRINTLN(F(I2S_MIC_CHANNEL_TEXT)); + audioSource = new I2SSource(SAMPLE_RATE, BLOCK_SIZE); + delay(100); + if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin); + break; + case 2: + DEBUGSR_PRINTLN(F("AR: ES7243 Microphone (right channel only).")); + audioSource = new ES7243(SAMPLE_RATE, BLOCK_SIZE); + delay(100); + if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); + break; + case 3: + DEBUGSR_PRINT(F("AR: SPH0645 Microphone - ")); DEBUGSR_PRINTLN(F(I2S_MIC_CHANNEL_TEXT)); + audioSource = new SPH0654(SAMPLE_RATE, BLOCK_SIZE); + delay(100); + audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin); + break; + case 4: + DEBUGSR_PRINT(F("AR: Generic I2S Microphone with Master Clock - ")); DEBUGSR_PRINTLN(F(I2S_MIC_CHANNEL_TEXT)); + audioSource = new I2SSource(SAMPLE_RATE, BLOCK_SIZE, 1.0f/24.0f); + delay(100); + if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); + break; + #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) + case 5: + DEBUGSR_PRINT(F("AR: I2S PDM Microphone - ")); DEBUGSR_PRINTLN(F(I2S_PDM_MIC_CHANNEL_TEXT)); + audioSource = new I2SSource(SAMPLE_RATE, BLOCK_SIZE, 1.0f/4.0f); + useBandPassFilter = true; // this reduces the noise floor on SPM1423 from 5% Vpp (~380) down to 0.05% Vpp (~5) + delay(100); + if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin); + break; + #endif + case 6: + DEBUGSR_PRINTLN(F("AR: ES8388 Source")); + audioSource = new ES8388Source(SAMPLE_RATE, BLOCK_SIZE); + delay(100); + if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); + break; + + #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) + // ADC over I2S is only possible on "classic" ESP32 + case 0: + default: + DEBUGSR_PRINTLN(F("AR: Analog Microphone (left channel only).")); + audioSource = new I2SAdcSource(SAMPLE_RATE, BLOCK_SIZE); + delay(100); + useBandPassFilter = true; // PDM bandpass filter seems to help for bad quality analog + if (audioSource) audioSource->initialize(audioPin); + break; + #endif + } + delay(250); // give microphone enough time to initialise + + if (!audioSource) enabled = false; // audio failed to initialise +#endif + if (enabled) onUpdateBegin(false); // create FFT task, and initialize network + if (enabled) disableSoundProcessing = false; // all good - enable audio processing +#ifdef ARDUINO_ARCH_ESP32 + if (FFT_Task == nullptr) enabled = false; // FFT task creation failed + if ((!audioSource) || (!audioSource->isInitialized())) { + // audio source failed to initialize. Still stay "enabled", as there might be input arriving via UDP Sound Sync + #ifdef WLED_DEBUG + DEBUG_PRINTLN(F("AR: Failed to initialize sound input driver. Please check input PIN settings.")); + #else + DEBUGSR_PRINTLN(F("AR: Failed to initialize sound input driver. Please check input PIN settings.")); + #endif + disableSoundProcessing = true; + } +#endif + if (enabled) connectUDPSoundSync(); + if (enabled && addPalettes) createAudioPalettes(); + initDone = true; + } + + + /* + * connected() is called every time the WiFi is (re)connected + * Use it to initialize network interfaces + */ + void connected() override + { + if (udpSyncConnected) { // clean-up: if open, close old UDP sync connection + udpSyncConnected = false; + fftUdp.stop(); + } + + if (audioSyncPort > 0 && (audioSyncEnabled & 0x03)) { + #ifdef ARDUINO_ARCH_ESP32 + udpSyncConnected = fftUdp.beginMulticast(IPAddress(239, 0, 0, 1), audioSyncPort); + #else + udpSyncConnected = fftUdp.beginMulticast(WiFi.localIP(), IPAddress(239, 0, 0, 1), audioSyncPort); + #endif + } + } + + + /* + * loop() is called continuously. Here you can check for events, read sensors, etc. + * + * Tips: + * 1. You can use "if (WLED_CONNECTED)" to check for a successful network connection. + * Additionally, "if (WLED_MQTT_CONNECTED)" is available to check for a connection to an MQTT broker. + * + * 2. Try to avoid using the delay() function. NEVER use delays longer than 10 milliseconds. + * Instead, use a timer check as shown here. + */ + void loop() override + { + static unsigned long lastUMRun = millis(); + + if (!enabled) { + disableSoundProcessing = true; // keep processing suspended (FFT task) + lastUMRun = millis(); // update time keeping + return; + } + // We cannot wait indefinitely before processing audio data + if (WS2812FX::isUpdating() && (millis() - lastUMRun < 2)) return; // be nice, but not too nice + + // suspend local sound processing when "real time mode" is active (E131, UDP, ADALIGHT, ARTNET) + if ( (realtimeOverride == REALTIME_OVERRIDE_NONE) // please add other overrides here if needed + &&( (realtimeMode == REALTIME_MODE_GENERIC) + ||(realtimeMode == REALTIME_MODE_E131) + ||(realtimeMode == REALTIME_MODE_UDP) + ||(realtimeMode == REALTIME_MODE_ADALIGHT) + ||(realtimeMode == REALTIME_MODE_ARTNET) ) ) // please add other modes here if needed + { + #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_DEBUG) + if ((disableSoundProcessing == false) && (audioSyncEnabled == 0)) { // we just switched to "disabled" + DEBUG_PRINTLN(F("[AR userLoop] realtime mode active - audio processing suspended.")); + DEBUG_PRINTF_P(PSTR(" RealtimeMode = %d; RealtimeOverride = %d\n"), int(realtimeMode), int(realtimeOverride)); + } + #endif + disableSoundProcessing = true; + } else { + #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_DEBUG) + if ((disableSoundProcessing == true) && (audioSyncEnabled == 0) && audioSource->isInitialized()) { // we just switched to "enabled" + DEBUG_PRINTLN(F("[AR userLoop] realtime mode ended - audio processing resumed.")); + DEBUG_PRINTF_P(PSTR(" RealtimeMode = %d; RealtimeOverride = %d\n"), int(realtimeMode), int(realtimeOverride)); + } + #endif + if ((disableSoundProcessing == true) && (audioSyncEnabled == 0)) lastUMRun = millis(); // just left "realtime mode" - update timekeeping + disableSoundProcessing = false; + } + + if (audioSyncEnabled & 0x02) disableSoundProcessing = true; // make sure everything is disabled IF in audio Receive mode + if (audioSyncEnabled & 0x01) disableSoundProcessing = false; // keep running audio IF we're in audio Transmit mode +#ifdef ARDUINO_ARCH_ESP32 + if (!audioSource->isInitialized()) disableSoundProcessing = true; // no audio source + + + // Only run the sampling code IF we're not in Receive mode or realtime mode + if (!(audioSyncEnabled & 0x02) && !disableSoundProcessing) { + if (soundAgc > AGC_NUM_PRESETS) soundAgc = 0; // make sure that AGC preset is valid (to avoid array bounds violation) + + unsigned long t_now = millis(); // remember current time + int userloopDelay = int(t_now - lastUMRun); + if (lastUMRun == 0) userloopDelay=0; // startup - don't have valid data from last run. + + #ifdef WLED_DEBUG + // complain when audio userloop has been delayed for long time. Currently we need userloop running between 500 and 1500 times per second. + // softhack007 disabled temporarily - avoid serial console spam with MANY leds and low FPS + //if ((userloopDelay > 65) && !disableSoundProcessing && (audioSyncEnabled == 0)) { + // DEBUG_PRINTF_P(PSTR("[AR userLoop] hiccup detected -> was inactive for last %d millis!\n"), userloopDelay); + //} + #endif + + // run filters, and repeat in case of loop delays (hick-up compensation) + if (userloopDelay <2) userloopDelay = 0; // minor glitch, no problem + if (userloopDelay >200) userloopDelay = 200; // limit number of filter re-runs + do { + getSample(); // run microphone sampling filters + agcAvg(t_now - userloopDelay); // Calculated the PI adjusted value as sampleAvg + userloopDelay -= 2; // advance "simulated time" by 2ms + } while (userloopDelay > 0); + lastUMRun = t_now; // update time keeping + + // update samples for effects (raw, smooth) + volumeSmth = (soundAgc) ? sampleAgc : sampleAvg; + volumeRaw = (soundAgc) ? rawSampleAgc: sampleRaw; + // update FFTMagnitude, taking into account AGC amplification + my_magnitude = FFT_Magnitude; // / 16.0f, 8.0f, 4.0f done in effects + if (soundAgc) my_magnitude *= multAgc; + if (volumeSmth < 1 ) my_magnitude = 0.001f; // noise gate closed - mute + + limitSampleDynamics(); + } // if (!disableSoundProcessing) +#endif + + autoResetPeak(); // auto-reset sample peak after strip minShowDelay + if (!udpSyncConnected) udpSamplePeak = false; // reset UDP samplePeak while UDP is unconnected + + connectUDPSoundSync(); // ensure we have a connection - if needed + + // UDP Microphone Sync - receive mode + if ((audioSyncEnabled & 0x02) && udpSyncConnected) { + // Only run the audio listener code if we're in Receive mode + static float syncVolumeSmth = 0; + bool have_new_sample = false; + if (millis() - lastTime > delayMs) { + have_new_sample = receiveAudioData(); + if (have_new_sample) last_UDPTime = millis(); +#ifdef ARDUINO_ARCH_ESP32 + else fftUdp.flush(); // Flush udp input buffers if we haven't read it - avoids hickups in receive mode. Does not work on 8266. +#endif + lastTime = millis(); + } + if (have_new_sample) syncVolumeSmth = volumeSmth; // remember received sample + else volumeSmth = syncVolumeSmth; // restore originally received sample for next run of dynamics limiter + limitSampleDynamics(); // run dynamics limiter on received volumeSmth, to hide jumps and hickups + } + + #if defined(MIC_LOGGER) || defined(MIC_SAMPLING_LOG) || defined(FFT_SAMPLING_LOG) + static unsigned long lastMicLoggerTime = 0; + if (millis()-lastMicLoggerTime > 20) { + lastMicLoggerTime = millis(); + logAudio(); + } + #endif + + // Info Page: keep max sample from last 5 seconds +#ifdef ARDUINO_ARCH_ESP32 + if ((millis() - sampleMaxTimer) > CYCLE_SAMPLEMAX) { + sampleMaxTimer = millis(); + maxSample5sec = (0.15f * maxSample5sec) + 0.85f *((soundAgc) ? sampleAgc : sampleAvg); // reset, and start with some smoothing + if (sampleAvg < 1) maxSample5sec = 0; // noise gate + } else { + if ((sampleAvg >= 1)) maxSample5sec = fmaxf(maxSample5sec, (soundAgc) ? rawSampleAgc : sampleRaw); // follow maximum volume + } +#else // similar functionality for 8266 receive only - use VolumeSmth instead of raw sample data + if ((millis() - sampleMaxTimer) > CYCLE_SAMPLEMAX) { + sampleMaxTimer = millis(); + maxSample5sec = (0.15 * maxSample5sec) + 0.85 * volumeSmth; // reset, and start with some smoothing + if (volumeSmth < 1.0f) maxSample5sec = 0; // noise gate + if (maxSample5sec < 0.0f) maxSample5sec = 0; // avoid negative values + } else { + if (volumeSmth >= 1.0f) maxSample5sec = fmaxf(maxSample5sec, volumeRaw); // follow maximum volume + } +#endif + +#ifdef ARDUINO_ARCH_ESP32 + //UDP Microphone Sync - transmit mode + if ((audioSyncEnabled & 0x01) && (millis() - lastTime > 20)) { + // Only run the transmit code IF we're in Transmit mode + transmitAudioData(); + lastTime = millis(); + } +#endif + + fillAudioPalettes(); + } + + + bool getUMData(um_data_t **data) override + { + if (!data || !enabled) return false; // no pointer provided by caller or not enabled -> exit + *data = um_data; + return true; + } + +#ifdef ARDUINO_ARCH_ESP32 + void onUpdateBegin(bool init) override + { +#ifdef WLED_DEBUG + fftTime = sampleTime = 0; +#endif + // gracefully suspend FFT task (if running) + disableSoundProcessing = true; + + // reset sound data + micDataReal = 0.0f; + volumeRaw = 0; volumeSmth = 0.0f; + sampleAgc = 0.0f; sampleAvg = 0.0f; + sampleRaw = 0; rawSampleAgc = 0.0f; + my_magnitude = 0.0f; FFT_Magnitude = 0.0f; FFT_MajorPeak = 1.0f; + multAgc = 1.0f; + // reset FFT data + memset(fftCalc, 0, sizeof(fftCalc)); + memset(fftAvg, 0, sizeof(fftAvg)); + memset(fftResult, 0, sizeof(fftResult)); + for(int i=(init?0:1); i don't process audio + updateIsRunning = init; + } +#endif + +#ifdef ARDUINO_ARCH_ESP32 + /** + * handleButton() can be used to override default button behaviour. Returning true + * will prevent button working in a default way. + */ + bool handleButton(uint8_t b) override { + yield(); + // crude way of determining if audio input is analog + // better would be for AudioSource to implement getType() + if (enabled + && dmType == 0 && audioPin>=0 + && (buttonType[b] == BTN_TYPE_ANALOG || buttonType[b] == BTN_TYPE_ANALOG_INVERTED) + ) { + return true; + } + return false; + } + +#endif + //////////////////////////// + // Settings and Info Page // + //////////////////////////// + + /* + * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. + * Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI. + * Below it is shown how this could be used for e.g. a light sensor + */ + void addToJsonInfo(JsonObject& root) override + { +#ifdef ARDUINO_ARCH_ESP32 + char myStringBuffer[16]; // buffer for snprintf() - not used yet on 8266 +#endif + JsonObject user = root["u"]; + if (user.isNull()) user = root.createNestedObject("u"); + + JsonArray infoArr = user.createNestedArray(FPSTR(_name)); + + String uiDomString = F(""); + infoArr.add(uiDomString); + + if (enabled) { +#ifdef ARDUINO_ARCH_ESP32 + // Input Level Slider + if (disableSoundProcessing == false) { // only show slider when audio processing is running + if (soundAgc > 0) { + infoArr = user.createNestedArray(F("GEQ Input Level")); // if AGC is on, this slider only affects fftResult[] frequencies + } else { + infoArr = user.createNestedArray(F("Audio Input Level")); + } + uiDomString = F("
"); // + infoArr.add(uiDomString); + } +#endif + // The following can be used for troubleshooting user errors and is so not enclosed in #ifdef WLED_DEBUG + + // current Audio input + infoArr = user.createNestedArray(F("Audio Source")); + if (audioSyncEnabled & 0x02) { + // UDP sound sync - receive mode + infoArr.add(F("UDP sound sync")); + if (udpSyncConnected) { + if (millis() - last_UDPTime < 2500) + infoArr.add(F(" - receiving")); + else + infoArr.add(F(" - idle")); + } else { + infoArr.add(F(" - no connection")); + } +#ifndef ARDUINO_ARCH_ESP32 // substitute for 8266 + } else { + infoArr.add(F("sound sync Off")); + } +#else // ESP32 only + } else { + // Analog or I2S digital input + if (audioSource && (audioSource->isInitialized())) { + // audio source successfully configured + if (audioSource->getType() == AudioSource::Type_I2SAdc) { + infoArr.add(F("ADC analog")); + } else { + infoArr.add(F("I2S digital")); + } + // input level or "silence" + if (maxSample5sec > 1.0f) { + float my_usage = 100.0f * (maxSample5sec / 255.0f); + snprintf_P(myStringBuffer, 15, PSTR(" - peak %3d%%"), int(my_usage)); + infoArr.add(myStringBuffer); + } else { + infoArr.add(F(" - quiet")); + } + } else { + // error during audio source setup + infoArr.add(F("not initialized")); + infoArr.add(F(" - check pin settings")); + } + } + + // Sound processing (FFT and input filters) + infoArr = user.createNestedArray(F("Sound Processing")); + if (audioSource && (disableSoundProcessing == false)) { + infoArr.add(F("running")); + } else { + infoArr.add(F("suspended")); + } + + // AGC or manual Gain + if ((soundAgc==0) && (disableSoundProcessing == false) && !(audioSyncEnabled & 0x02)) { + infoArr = user.createNestedArray(F("Manual Gain")); + float myGain = ((float)sampleGain/40.0f * (float)inputLevel/128.0f) + 1.0f/16.0f; // non-AGC gain from presets + infoArr.add(roundf(myGain*100.0f) / 100.0f); + infoArr.add("x"); + } + if (soundAgc && (disableSoundProcessing == false) && !(audioSyncEnabled & 0x02)) { + infoArr = user.createNestedArray(F("AGC Gain")); + infoArr.add(roundf(multAgc*100.0f) / 100.0f); + infoArr.add("x"); + } +#endif + // UDP Sound Sync status + infoArr = user.createNestedArray(F("UDP Sound Sync")); + if (audioSyncEnabled) { + if (audioSyncEnabled & 0x01) { + infoArr.add(F("send mode")); + if ((udpSyncConnected) && (millis() - lastTime < 2500)) infoArr.add(F(" v2")); + } else if (audioSyncEnabled & 0x02) { + infoArr.add(F("receive mode")); + } + } else + infoArr.add("off"); + if (audioSyncEnabled && !udpSyncConnected) infoArr.add(" (unconnected)"); + if (audioSyncEnabled && udpSyncConnected && (millis() - last_UDPTime < 2500)) { + if (receivedFormat == 1) infoArr.add(F(" v1")); + if (receivedFormat == 2) infoArr.add(F(" v2")); + } + + #if defined(WLED_DEBUG) || defined(SR_DEBUG) + #ifdef ARDUINO_ARCH_ESP32 + infoArr = user.createNestedArray(F("Sampling time")); + infoArr.add(float(sampleTime)/100.0f); + infoArr.add(" ms"); + + infoArr = user.createNestedArray(F("FFT time")); + infoArr.add(float(fftTime)/100.0f); + if ((fftTime/100) >= FFT_MIN_CYCLE) // FFT time over budget -> I2S buffer will overflow + infoArr.add("! ms"); + else if ((fftTime/80 + sampleTime/80) >= FFT_MIN_CYCLE) // FFT time >75% of budget -> risk of instability + infoArr.add(" ms!"); + else + infoArr.add(" ms"); + + DEBUGSR_PRINTF("AR Sampling time: %5.2f ms\n", float(sampleTime)/100.0f); + DEBUGSR_PRINTF("AR FFT time : %5.2f ms\n", float(fftTime)/100.0f); + #endif + #endif + } + } + + + /* + * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + */ + void addToJsonState(JsonObject& root) override + { + if (!initDone) return; // prevent crash on boot applyPreset() + JsonObject usermod = root[FPSTR(_name)]; + if (usermod.isNull()) { + usermod = root.createNestedObject(FPSTR(_name)); + } + usermod["on"] = enabled; + } + + + /* + * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + */ + void readFromJsonState(JsonObject& root) override + { + if (!initDone) return; // prevent crash on boot applyPreset() + bool prevEnabled = enabled; + JsonObject usermod = root[FPSTR(_name)]; + if (!usermod.isNull()) { + if (usermod[FPSTR(_enabled)].is()) { + enabled = usermod[FPSTR(_enabled)].as(); + if (prevEnabled != enabled) onUpdateBegin(!enabled); + if (addPalettes) { + // add/remove custom/audioreactive palettes + if (prevEnabled && !enabled) removeAudioPalettes(); + if (!prevEnabled && enabled) createAudioPalettes(); + } + } +#ifdef ARDUINO_ARCH_ESP32 + if (usermod[FPSTR(_inputLvl)].is()) { + inputLevel = min(255,max(0,usermod[FPSTR(_inputLvl)].as())); + } +#endif + } + if (root.containsKey(F("rmcpal")) && root[F("rmcpal")].as()) { + // handle removal of custom palettes from JSON call so we don't break things + removeAudioPalettes(); + } + } + + void onStateChange(uint8_t callMode) override { + if (initDone && enabled && addPalettes && palettes==0 && WS2812FX::customPalettes.size()<10) { + // if palettes were removed during JSON call re-add them + createAudioPalettes(); + } + } + + /* + * addToConfig() can be used to add custom persistent settings to the cfg.json file in the "um" (usermod) object. + * It will be called by WLED when settings are actually saved (for example, LED settings are saved) + * If you want to force saving the current state, use serializeConfig() in your loop(). + * + * CAUTION: serializeConfig() will initiate a filesystem write operation. + * It might cause the LEDs to stutter and will cause flash wear if called too often. + * Use it sparingly and always in the loop, never in network callbacks! + * + * addToConfig() will make your settings editable through the Usermod Settings page automatically. + * + * Usermod Settings Overview: + * - Numeric values are treated as floats in the browser. + * - If the numeric value entered into the browser contains a decimal point, it will be parsed as a C float + * before being returned to the Usermod. The float data type has only 6-7 decimal digits of precision, and + * doubles are not supported, numbers will be rounded to the nearest float value when being parsed. + * The range accepted by the input field is +/- 1.175494351e-38 to +/- 3.402823466e+38. + * - If the numeric value entered into the browser doesn't contain a decimal point, it will be parsed as a + * C int32_t (range: -2147483648 to 2147483647) before being returned to the usermod. + * Overflows or underflows are truncated to the max/min value for an int32_t, and again truncated to the type + * used in the Usermod when reading the value from ArduinoJson. + * - Pin values can be treated differently from an integer value by using the key name "pin" + * - "pin" can contain a single or array of integer values + * - On the Usermod Settings page there is simple checking for pin conflicts and warnings for special pins + * - Red color indicates a conflict. Yellow color indicates a pin with a warning (e.g. an input-only pin) + * - Tip: use int8_t to store the pin value in the Usermod, so a -1 value (pin not set) can be used + * + * See usermod_v2_auto_save.h for an example that saves Flash space by reusing ArduinoJson key name strings + * + * If you need a dedicated settings page with custom layout for your Usermod, that takes a lot more work. + * You will have to add the setting to the HTML, xml.cpp and set.cpp manually. + * See the WLED Soundreactive fork (code and wiki) for reference. https://github.com/atuline/WLED + * + * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings! + */ + void addToConfig(JsonObject& root) override + { + JsonObject top = root.createNestedObject(FPSTR(_name)); + top[FPSTR(_enabled)] = enabled; + top[FPSTR(_addPalettes)] = addPalettes; + +#ifdef ARDUINO_ARCH_ESP32 + #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) + JsonObject amic = top.createNestedObject(FPSTR(_analogmic)); + amic["pin"] = audioPin; + #endif + + JsonObject dmic = top.createNestedObject(FPSTR(_digitalmic)); + dmic["type"] = dmType; + JsonArray pinArray = dmic.createNestedArray("pin"); + pinArray.add(i2ssdPin); + pinArray.add(i2swsPin); + pinArray.add(i2sckPin); + pinArray.add(mclkPin); + + JsonObject cfg = top.createNestedObject(FPSTR(_config)); + cfg[F("squelch")] = soundSquelch; + cfg[F("gain")] = sampleGain; + cfg[F("AGC")] = soundAgc; + + JsonObject freqScale = top.createNestedObject(FPSTR(_frequency)); + freqScale[F("scale")] = FFTScalingMode; +#endif + + JsonObject dynLim = top.createNestedObject(FPSTR(_dynamics)); + dynLim[F("limiter")] = limiterOn; + dynLim[F("rise")] = attackTime; + dynLim[F("fall")] = decayTime; + + JsonObject sync = top.createNestedObject("sync"); + sync["port"] = audioSyncPort; + sync["mode"] = audioSyncEnabled; + } + + + /* + * readFromConfig() can be used to read back the custom settings you added with addToConfig(). + * This is called by WLED when settings are loaded (currently this only happens immediately after boot, or after saving on the Usermod Settings page) + * + * readFromConfig() is called BEFORE setup(). This means you can use your persistent values in setup() (e.g. pin assignments, buffer sizes), + * but also that if you want to write persistent values to a dynamic buffer, you'd need to allocate it here instead of in setup. + * If you don't know what that is, don't fret. It most likely doesn't affect your use case :) + * + * Return true in case the config values returned from Usermod Settings were complete, or false if you'd like WLED to save your defaults to disk (so any missing values are editable in Usermod Settings) + * + * getJsonValue() returns false if the value is missing, or copies the value into the variable provided and returns true if the value is present + * The configComplete variable is true only if the "exampleUsermod" object and all values are present. If any values are missing, WLED will know to call addToConfig() to save them + * + * This function is guaranteed to be called on boot, but could also be called every time settings are updated + */ + bool readFromConfig(JsonObject& root) override + { + JsonObject top = root[FPSTR(_name)]; + bool configComplete = !top.isNull(); + bool oldEnabled = enabled; + bool oldAddPalettes = addPalettes; + + configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled); + configComplete &= getJsonValue(top[FPSTR(_addPalettes)], addPalettes); + +#ifdef ARDUINO_ARCH_ESP32 + #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) + configComplete &= getJsonValue(top[FPSTR(_analogmic)]["pin"], audioPin); + #else + audioPin = -1; // MCU does not support analog mic + #endif + + configComplete &= getJsonValue(top[FPSTR(_digitalmic)]["type"], dmType); + #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3) + if (dmType == 0) dmType = SR_DMTYPE; // MCU does not support analog + #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) + if (dmType == 5) dmType = SR_DMTYPE; // MCU does not support PDM + #endif + #endif + + configComplete &= getJsonValue(top[FPSTR(_digitalmic)]["pin"][0], i2ssdPin); + configComplete &= getJsonValue(top[FPSTR(_digitalmic)]["pin"][1], i2swsPin); + configComplete &= getJsonValue(top[FPSTR(_digitalmic)]["pin"][2], i2sckPin); + configComplete &= getJsonValue(top[FPSTR(_digitalmic)]["pin"][3], mclkPin); + + configComplete &= getJsonValue(top[FPSTR(_config)][F("squelch")], soundSquelch); + configComplete &= getJsonValue(top[FPSTR(_config)][F("gain")], sampleGain); + configComplete &= getJsonValue(top[FPSTR(_config)][F("AGC")], soundAgc); + + configComplete &= getJsonValue(top[FPSTR(_frequency)][F("scale")], FFTScalingMode); + + configComplete &= getJsonValue(top[FPSTR(_dynamics)][F("limiter")], limiterOn); + configComplete &= getJsonValue(top[FPSTR(_dynamics)][F("rise")], attackTime); + configComplete &= getJsonValue(top[FPSTR(_dynamics)][F("fall")], decayTime); +#endif + configComplete &= getJsonValue(top["sync"]["port"], audioSyncPort); + configComplete &= getJsonValue(top["sync"]["mode"], audioSyncEnabled); + + if (initDone) { + // add/remove custom/audioreactive palettes + if ((oldAddPalettes && !addPalettes) || (oldAddPalettes && !enabled)) removeAudioPalettes(); + if ((addPalettes && !oldAddPalettes && enabled) || (addPalettes && !oldEnabled && enabled)) createAudioPalettes(); + } // else setup() will create palettes + return configComplete; + } + + + void appendConfigData() override + { +#ifdef ARDUINO_ARCH_ESP32 + oappend(SET_F("dd=addDropdown('AudioReactive','digitalmic:type');")); + #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) + oappend(SET_F("addOption(dd,'Generic Analog',0);")); + #endif + oappend(SET_F("addOption(dd,'Generic I2S',1);")); + oappend(SET_F("addOption(dd,'ES7243',2);")); + oappend(SET_F("addOption(dd,'SPH0654',3);")); + oappend(SET_F("addOption(dd,'Generic I2S with Mclk',4);")); + #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) + oappend(SET_F("addOption(dd,'Generic I2S PDM',5);")); + #endif + oappend(SET_F("addOption(dd,'ES8388',6);")); + + oappend(SET_F("dd=addDropdown('AudioReactive','config:AGC');")); + oappend(SET_F("addOption(dd,'Off',0);")); + oappend(SET_F("addOption(dd,'Normal',1);")); + oappend(SET_F("addOption(dd,'Vivid',2);")); + oappend(SET_F("addOption(dd,'Lazy',3);")); + + oappend(SET_F("dd=addDropdown('AudioReactive','dynamics:limiter');")); + oappend(SET_F("addOption(dd,'Off',0);")); + oappend(SET_F("addOption(dd,'On',1);")); + oappend(SET_F("addInfo('AudioReactive:dynamics:limiter',0,' On ');")); // 0 is field type, 1 is actual field + oappend(SET_F("addInfo('AudioReactive:dynamics:rise',1,'ms (♪ effects only)');")); + oappend(SET_F("addInfo('AudioReactive:dynamics:fall',1,'ms (♪ effects only)');")); + + oappend(SET_F("dd=addDropdown('AudioReactive','frequency:scale');")); + oappend(SET_F("addOption(dd,'None',0);")); + oappend(SET_F("addOption(dd,'Linear (Amplitude)',2);")); + oappend(SET_F("addOption(dd,'Square Root (Energy)',3);")); + oappend(SET_F("addOption(dd,'Logarithmic (Loudness)',1);")); +#endif + + oappend(SET_F("dd=addDropdown('AudioReactive','sync:mode');")); + oappend(SET_F("addOption(dd,'Off',0);")); +#ifdef ARDUINO_ARCH_ESP32 + oappend(SET_F("addOption(dd,'Send',1);")); +#endif + oappend(SET_F("addOption(dd,'Receive',2);")); +#ifdef ARDUINO_ARCH_ESP32 + oappend(SET_F("addInfo('AudioReactive:digitalmic:type',1,'requires reboot!');")); // 0 is field type, 1 is actual field + oappend(SET_F("addInfo('AudioReactive:digitalmic:pin[]',0,'sd/data/dout','I2S SD');")); + oappend(SET_F("addInfo('AudioReactive:digitalmic:pin[]',1,'ws/clk/lrck','I2S WS');")); + oappend(SET_F("addInfo('AudioReactive:digitalmic:pin[]',2,'sck/bclk','I2S SCK');")); + #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) + oappend(SET_F("addInfo('AudioReactive:digitalmic:pin[]',3,'only use -1, 0, 1 or 3','I2S MCLK');")); + #else + oappend(SET_F("addInfo('AudioReactive:digitalmic:pin[]',3,'master clock','I2S MCLK');")); + #endif +#endif + } + + + /* + * handleOverlayDraw() is called just before every show() (LED strip update frame) after effects have set the colors. + * Use this to blank out some LEDs or set them to a different color regardless of the set effect mode. + * Commonly used for custom clocks (Cronixie, 7 segment) + */ + //void handleOverlayDraw() override + //{ + //WS2812FX::setPixelColor(0, RGBW32(0,0,0,0)) // set the first pixel to black + //} + + + /* + * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). + * This could be used in the future for the system to determine whether your usermod is installed. + */ + uint16_t getId() override + { + return USERMOD_ID_AUDIOREACTIVE; + } +}; + +void AudioReactive::removeAudioPalettes(void) { + DEBUG_PRINTLN(F("Removing audio palettes.")); + while (palettes>0) { + WS2812FX::customPalettes.pop_back(); + DEBUG_PRINTLN(palettes); + palettes--; + } + DEBUG_PRINT(F("Total # of palettes: ")); DEBUG_PRINTLN(WS2812FX::customPalettes.size()); +} + +void AudioReactive::createAudioPalettes(void) { + DEBUG_PRINT(F("Total # of palettes: ")); DEBUG_PRINTLN(WS2812FX::customPalettes.size()); + if (palettes) return; + DEBUG_PRINTLN(F("Adding audio palettes.")); + for (int i=0; i= palettes) lastCustPalette -= palettes; + for (int pal=0; pal= 0) { - irqBound = pinManager.allocatePin(config.interruptPin, false, PinOwner::UM_IMU); + irqBound = PinManager::allocatePin(config.interruptPin, false, PinOwner::UM_IMU); if (!irqBound) { DEBUG_PRINTLN(F("MPU6050: IRQ pin already in use.")); return; } pinMode(config.interruptPin, INPUT); }; @@ -408,7 +408,7 @@ class MPU6050Driver : public Usermod { // Previously loaded and config changed if (irqBound && ((old_cfg.interruptPin != config.interruptPin) || !config.enabled)) { detachInterrupt(old_cfg.interruptPin); - pinManager.deallocatePin(old_cfg.interruptPin, PinOwner::UM_IMU); + PinManager::deallocatePin(old_cfg.interruptPin, PinOwner::UM_IMU); irqBound = false; } diff --git a/usermods/mqtt_switch_v2/README.md b/usermods/mqtt_switch_v2/README.md index 4cb7ef0e84..382f72d0e8 100644 --- a/usermods/mqtt_switch_v2/README.md +++ b/usermods/mqtt_switch_v2/README.md @@ -19,7 +19,7 @@ Example `usermods_list.cpp`: void registerUsermods() { - usermods.add(new UsermodMqttSwitch()); + UsermodManager::add(new UsermodMqttSwitch()); } ``` diff --git a/usermods/multi_relay/readme.md b/usermods/multi_relay/readme.md index 24dd394b82..eaa069ae76 100644 --- a/usermods/multi_relay/readme.md +++ b/usermods/multi_relay/readme.md @@ -41,7 +41,7 @@ When a relay is switched, a message is published: ## Usermod installation -1. Register the usermod by adding `#include "../usermods/multi_relay/usermod_multi_relay.h"` at the top and `usermods.add(new MultiRelay());` at the bottom of `usermods_list.cpp`. +1. Register the usermod by adding `#include "../usermods/multi_relay/usermod_multi_relay.h"` at the top and `UsermodManager::add(new MultiRelay());` at the bottom of `usermods_list.cpp`. or 2. Use `#define USERMOD_MULTI_RELAY` in wled.h or `-D USERMOD_MULTI_RELAY` in your platformio.ini @@ -90,9 +90,9 @@ void registerUsermods() * || || || * \/ \/ \/ */ - //usermods.add(new MyExampleUsermod()); - //usermods.add(new UsermodTemperature()); - usermods.add(new MultiRelay()); + //UsermodManager::add(new MyExampleUsermod()); + //UsermodManager::add(new UsermodTemperature()); + UsermodManager::add(new MultiRelay()); } ``` diff --git a/usermods/multi_relay/usermod_multi_relay.h b/usermods/multi_relay/usermod_multi_relay.h index efb3c8ae19..33a6cf85e2 100644 --- a/usermods/multi_relay/usermod_multi_relay.h +++ b/usermods/multi_relay/usermod_multi_relay.h @@ -516,7 +516,7 @@ void MultiRelay::setup() { if (!_relay[i].external) _relay[i].state = !offMode; state |= (uint8_t)(_relay[i].invert ? !_relay[i].state : _relay[i].state) << pin; } else if (_relay[i].pin<100 && _relay[i].pin>=0) { - if (pinManager.allocatePin(_relay[i].pin,true, PinOwner::UM_MultiRelay)) { + if (PinManager::allocatePin(_relay[i].pin,true, PinOwner::UM_MultiRelay)) { if (!_relay[i].external) _relay[i].state = !offMode; switchRelay(i, _relay[i].state); _relay[i].active = false; @@ -817,7 +817,7 @@ bool MultiRelay::readFromConfig(JsonObject &root) { // deallocate all pins 1st for (int i=0; i=0 && oldPin[i]<100) { - pinManager.deallocatePin(oldPin[i], PinOwner::UM_MultiRelay); + PinManager::deallocatePin(oldPin[i], PinOwner::UM_MultiRelay); } // allocate new pins setup(); diff --git a/usermods/pixels_dice_tray/pixels_dice_tray.h b/usermods/pixels_dice_tray/pixels_dice_tray.h index 238af314ea..a1e45ba33b 100644 --- a/usermods/pixels_dice_tray/pixels_dice_tray.h +++ b/usermods/pixels_dice_tray/pixels_dice_tray.h @@ -112,15 +112,15 @@ class PixelsDiceTrayUsermod : public Usermod { SetSPIPinsFromMacros(); PinManagerPinType spiPins[] = { {spi_mosi, true}, {spi_miso, false}, {spi_sclk, true}}; - if (!pinManager.allocateMultiplePins(spiPins, 3, PinOwner::HW_SPI)) { + if (!PinManager::allocateMultiplePins(spiPins, 3, PinOwner::HW_SPI)) { enabled = false; } else { PinManagerPinType displayPins[] = { {TFT_CS, true}, {TFT_DC, true}, {TFT_RST, true}, {TFT_BL, true}}; - if (!pinManager.allocateMultiplePins( + if (!PinManager::allocateMultiplePins( displayPins, sizeof(displayPins) / sizeof(PinManagerPinType), PinOwner::UM_FourLineDisplay)) { - pinManager.deallocateMultiplePins(spiPins, 3, PinOwner::HW_SPI); + PinManager::deallocateMultiplePins(spiPins, 3, PinOwner::HW_SPI); enabled = false; } } diff --git a/usermods/pwm_outputs/usermod_pwm_outputs.h b/usermods/pwm_outputs/usermod_pwm_outputs.h index 1880308c45..09232f043a 100644 --- a/usermods/pwm_outputs/usermod_pwm_outputs.h +++ b/usermods/pwm_outputs/usermod_pwm_outputs.h @@ -29,13 +29,13 @@ class PwmOutput { return; DEBUG_PRINTF("pwm_output[%d]: setup to freq %d\n", pin_, freq_); - if (!pinManager.allocatePin(pin_, true, PinOwner::UM_PWM_OUTPUTS)) + if (!PinManager::allocatePin(pin_, true, PinOwner::UM_PWM_OUTPUTS)) return; - channel_ = pinManager.allocateLedc(1); + channel_ = PinManager::allocateLedc(1); if (channel_ == 255) { DEBUG_PRINTF("pwm_output[%d]: failed to quire ledc\n", pin_); - pinManager.deallocatePin(pin_, PinOwner::UM_PWM_OUTPUTS); + PinManager::deallocatePin(pin_, PinOwner::UM_PWM_OUTPUTS); return; } @@ -49,9 +49,9 @@ class PwmOutput { DEBUG_PRINTF("pwm_output[%d]: close\n", pin_); if (!enabled_) return; - pinManager.deallocatePin(pin_, PinOwner::UM_PWM_OUTPUTS); + PinManager::deallocatePin(pin_, PinOwner::UM_PWM_OUTPUTS); if (channel_ != 255) - pinManager.deallocateLedc(channel_, 1); + PinManager::deallocateLedc(channel_, 1); channel_ = 255; duty_ = 0.0f; enabled_ = false; diff --git a/usermods/quinled-an-penta/quinled-an-penta.h b/usermods/quinled-an-penta/quinled-an-penta.h index 10b7843344..e446720398 100644 --- a/usermods/quinled-an-penta/quinled-an-penta.h +++ b/usermods/quinled-an-penta/quinled-an-penta.h @@ -129,7 +129,7 @@ class QuinLEDAnPentaUsermod : public Usermod void initOledDisplay() { PinManagerPinType pins[5] = { { oledSpiClk, true }, { oledSpiData, true }, { oledSpiCs, true }, { oledSpiDc, true }, { oledSpiRst, true } }; - if (!pinManager.allocateMultiplePins(pins, 5, PinOwner::UM_QuinLEDAnPenta)) { + if (!PinManager::allocateMultiplePins(pins, 5, PinOwner::UM_QuinLEDAnPenta)) { DEBUG_PRINTF("[%s] OLED pin allocation failed!\n", _name); oledEnabled = oledInitDone = false; return; @@ -164,11 +164,11 @@ class QuinLEDAnPentaUsermod : public Usermod oledDisplay->clear(); } - pinManager.deallocatePin(oledSpiClk, PinOwner::UM_QuinLEDAnPenta); - pinManager.deallocatePin(oledSpiData, PinOwner::UM_QuinLEDAnPenta); - pinManager.deallocatePin(oledSpiCs, PinOwner::UM_QuinLEDAnPenta); - pinManager.deallocatePin(oledSpiDc, PinOwner::UM_QuinLEDAnPenta); - pinManager.deallocatePin(oledSpiRst, PinOwner::UM_QuinLEDAnPenta); + PinManager::deallocatePin(oledSpiClk, PinOwner::UM_QuinLEDAnPenta); + PinManager::deallocatePin(oledSpiData, PinOwner::UM_QuinLEDAnPenta); + PinManager::deallocatePin(oledSpiCs, PinOwner::UM_QuinLEDAnPenta); + PinManager::deallocatePin(oledSpiDc, PinOwner::UM_QuinLEDAnPenta); + PinManager::deallocatePin(oledSpiRst, PinOwner::UM_QuinLEDAnPenta); delete oledDisplay; @@ -184,7 +184,7 @@ class QuinLEDAnPentaUsermod : public Usermod void initSht30TempHumiditySensor() { PinManagerPinType pins[2] = { { shtSda, true }, { shtScl, true } }; - if (!pinManager.allocateMultiplePins(pins, 2, PinOwner::UM_QuinLEDAnPenta)) { + if (!PinManager::allocateMultiplePins(pins, 2, PinOwner::UM_QuinLEDAnPenta)) { DEBUG_PRINTF("[%s] SHT30 pin allocation failed!\n", _name); shtEnabled = shtInitDone = false; return; @@ -212,8 +212,8 @@ class QuinLEDAnPentaUsermod : public Usermod sht30TempHumidSensor->reset(); } - pinManager.deallocatePin(shtSda, PinOwner::UM_QuinLEDAnPenta); - pinManager.deallocatePin(shtScl, PinOwner::UM_QuinLEDAnPenta); + PinManager::deallocatePin(shtSda, PinOwner::UM_QuinLEDAnPenta); + PinManager::deallocatePin(shtScl, PinOwner::UM_QuinLEDAnPenta); delete sht30TempHumidSensor; diff --git a/usermods/rgb-rotary-encoder/rgb-rotary-encoder.h b/usermods/rgb-rotary-encoder/rgb-rotary-encoder.h index e57641bf9b..00fc227252 100644 --- a/usermods/rgb-rotary-encoder/rgb-rotary-encoder.h +++ b/usermods/rgb-rotary-encoder/rgb-rotary-encoder.h @@ -40,7 +40,7 @@ class RgbRotaryEncoderUsermod : public Usermod void initRotaryEncoder() { PinManagerPinType pins[2] = { { eaIo, false }, { ebIo, false } }; - if (!pinManager.allocateMultiplePins(pins, 2, PinOwner::UM_RGBRotaryEncoder)) { + if (!PinManager::allocateMultiplePins(pins, 2, PinOwner::UM_RGBRotaryEncoder)) { eaIo = -1; ebIo = -1; cleanup(); @@ -108,11 +108,11 @@ class RgbRotaryEncoderUsermod : public Usermod { // Only deallocate pins if we allocated them ;) if (eaIo != -1) { - pinManager.deallocatePin(eaIo, PinOwner::UM_RGBRotaryEncoder); + PinManager::deallocatePin(eaIo, PinOwner::UM_RGBRotaryEncoder); eaIo = -1; } if (ebIo != -1) { - pinManager.deallocatePin(ebIo, PinOwner::UM_RGBRotaryEncoder); + PinManager::deallocatePin(ebIo, PinOwner::UM_RGBRotaryEncoder); ebIo = -1; } @@ -303,8 +303,8 @@ class RgbRotaryEncoderUsermod : public Usermod } if (eaIo != oldEaIo || ebIo != oldEbIo || stepsPerClick != oldStepsPerClick || incrementPerClick != oldIncrementPerClick) { - pinManager.deallocatePin(oldEaIo, PinOwner::UM_RGBRotaryEncoder); - pinManager.deallocatePin(oldEbIo, PinOwner::UM_RGBRotaryEncoder); + PinManager::deallocatePin(oldEaIo, PinOwner::UM_RGBRotaryEncoder); + PinManager::deallocatePin(oldEbIo, PinOwner::UM_RGBRotaryEncoder); delete rotaryEncoder; initRotaryEncoder(); diff --git a/usermods/sd_card/usermod_sd_card.h b/usermods/sd_card/usermod_sd_card.h index 5dac79159c..da1999d9b5 100644 --- a/usermods/sd_card/usermod_sd_card.h +++ b/usermods/sd_card/usermod_sd_card.h @@ -45,7 +45,7 @@ class UsermodSdCard : public Usermod { { configPinPico, true } }; - if (!pinManager.allocateMultiplePins(pins, 4, PinOwner::UM_SdCard)) { + if (!PinManager::allocateMultiplePins(pins, 4, PinOwner::UM_SdCard)) { DEBUG_PRINTF("[%s] SD (SPI) pin allocation failed!\n", _name); sdInitDone = false; return; @@ -75,10 +75,10 @@ class UsermodSdCard : public Usermod { SD_ADAPTER.end(); DEBUG_PRINTF("[%s] deallocate pins!\n", _name); - pinManager.deallocatePin(configPinSourceSelect, PinOwner::UM_SdCard); - pinManager.deallocatePin(configPinSourceClock, PinOwner::UM_SdCard); - pinManager.deallocatePin(configPinPoci, PinOwner::UM_SdCard); - pinManager.deallocatePin(configPinPico, PinOwner::UM_SdCard); + PinManager::deallocatePin(configPinSourceSelect, PinOwner::UM_SdCard); + PinManager::deallocatePin(configPinSourceClock, PinOwner::UM_SdCard); + PinManager::deallocatePin(configPinPoci, PinOwner::UM_SdCard); + PinManager::deallocatePin(configPinPico, PinOwner::UM_SdCard); sdInitDone = false; } diff --git a/usermods/seven_segment_display_reloaded/usermod_seven_segment_reloaded.h b/usermods/seven_segment_display_reloaded/usermod_seven_segment_reloaded.h index 111df29672..1436f8fc4c 100644 --- a/usermods/seven_segment_display_reloaded/usermod_seven_segment_reloaded.h +++ b/usermods/seven_segment_display_reloaded/usermod_seven_segment_reloaded.h @@ -385,7 +385,7 @@ class UsermodSSDR : public Usermod { _setAllFalse(); #ifdef USERMOD_SN_PHOTORESISTOR - ptr = (Usermod_SN_Photoresistor*) usermods.lookup(USERMOD_ID_SN_PHOTORESISTOR); + ptr = (Usermod_SN_Photoresistor*) UsermodManager::lookup(USERMOD_ID_SN_PHOTORESISTOR); #endif DEBUG_PRINTLN(F("Setup done")); } diff --git a/usermods/usermod_v2_auto_save/usermod_v2_auto_save.h b/usermods/usermod_v2_auto_save/usermod_v2_auto_save.h index 52ff3cc1db..a257413b42 100644 --- a/usermods/usermod_v2_auto_save/usermod_v2_auto_save.h +++ b/usermods/usermod_v2_auto_save/usermod_v2_auto_save.h @@ -103,7 +103,7 @@ class AutoSaveUsermod : public Usermod { #ifdef USERMOD_FOUR_LINE_DISPLAY // This Usermod has enhanced functionality if // FourLineDisplayUsermod is available. - display = (FourLineDisplayUsermod*) usermods.lookup(USERMOD_ID_FOUR_LINE_DISP); + display = (FourLineDisplayUsermod*) UsermodManager::lookup(USERMOD_ID_FOUR_LINE_DISP); #endif initDone = true; if (enabled && applyAutoSaveOnBoot) applyPreset(autoSavePreset); diff --git a/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h b/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h index 008647fa7b..dfab7e6ffb 100644 --- a/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h +++ b/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h @@ -543,7 +543,7 @@ void FourLineDisplayUsermod::setup() { type = NONE; } else { PinManagerPinType cspins[3] = { { ioPin[0], true }, { ioPin[1], true }, { ioPin[2], true } }; - if (!pinManager.allocateMultiplePins(cspins, 3, PinOwner::UM_FourLineDisplay)) { type = NONE; } + if (!PinManager::allocateMultiplePins(cspins, 3, PinOwner::UM_FourLineDisplay)) { type = NONE; } } } else { if (i2c_scl<0 || i2c_sda<0) { type=NONE; } @@ -569,7 +569,7 @@ void FourLineDisplayUsermod::setup() { if (nullptr == u8x8) { DEBUG_PRINTLN(F("Display init failed.")); if (isSPI) { - pinManager.deallocateMultiplePins((const uint8_t*)ioPin, 3, PinOwner::UM_FourLineDisplay); + PinManager::deallocateMultiplePins((const uint8_t*)ioPin, 3, PinOwner::UM_FourLineDisplay); } type = NONE; return; @@ -1307,7 +1307,7 @@ bool FourLineDisplayUsermod::readFromConfig(JsonObject& root) { bool isSPI = (type == SSD1306_SPI || type == SSD1306_SPI64 || type == SSD1309_SPI64); bool newSPI = (newType == SSD1306_SPI || newType == SSD1306_SPI64 || newType == SSD1309_SPI64); if (isSPI) { - if (pinsChanged || !newSPI) pinManager.deallocateMultiplePins((const uint8_t*)oldPin, 3, PinOwner::UM_FourLineDisplay); + if (pinsChanged || !newSPI) PinManager::deallocateMultiplePins((const uint8_t*)oldPin, 3, PinOwner::UM_FourLineDisplay); if (!newSPI) { // was SPI but is no longer SPI if (i2c_scl<0 || i2c_sda<0) { newType=NONE; } @@ -1315,7 +1315,7 @@ bool FourLineDisplayUsermod::readFromConfig(JsonObject& root) { // still SPI but pins changed PinManagerPinType cspins[3] = { { ioPin[0], true }, { ioPin[1], true }, { ioPin[2], true } }; if (ioPin[0]<0 || ioPin[1]<0 || ioPin[1]<0) { newType=NONE; } - else if (!pinManager.allocateMultiplePins(cspins, 3, PinOwner::UM_FourLineDisplay)) { newType=NONE; } + else if (!PinManager::allocateMultiplePins(cspins, 3, PinOwner::UM_FourLineDisplay)) { newType=NONE; } } } else if (newSPI) { // was I2C but is now SPI @@ -1324,7 +1324,7 @@ bool FourLineDisplayUsermod::readFromConfig(JsonObject& root) { } else { PinManagerPinType pins[3] = { { ioPin[0], true }, { ioPin[1], true }, { ioPin[2], true } }; if (ioPin[0]<0 || ioPin[1]<0 || ioPin[1]<0) { newType=NONE; } - else if (!pinManager.allocateMultiplePins(pins, 3, PinOwner::UM_FourLineDisplay)) { newType=NONE; } + else if (!PinManager::allocateMultiplePins(pins, 3, PinOwner::UM_FourLineDisplay)) { newType=NONE; } } } else { // just I2C type changed diff --git a/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h b/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h index 5756fbb695..55715b7c76 100644 --- a/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h +++ b/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h @@ -489,7 +489,7 @@ void RotaryEncoderUIUsermod::setup() enabled = false; return; } else { - if (pinIRQ >= 0 && pinManager.allocatePin(pinIRQ, false, PinOwner::UM_RotaryEncoderUI)) { + if (pinIRQ >= 0 && PinManager::allocatePin(pinIRQ, false, PinOwner::UM_RotaryEncoderUI)) { pinMode(pinIRQ, INPUT_PULLUP); attachInterrupt(pinIRQ, i2cReadingISR, FALLING); // RISING, FALLING, CHANGE, ONLOW, ONHIGH DEBUG_PRINTLN(F("Interrupt attached.")); @@ -502,7 +502,7 @@ void RotaryEncoderUIUsermod::setup() } } else { PinManagerPinType pins[3] = { { pinA, false }, { pinB, false }, { pinC, false } }; - if (pinA<0 || pinB<0 || !pinManager.allocateMultiplePins(pins, 3, PinOwner::UM_RotaryEncoderUI)) { + if (pinA<0 || pinB<0 || !PinManager::allocateMultiplePins(pins, 3, PinOwner::UM_RotaryEncoderUI)) { pinA = pinB = pinC = -1; enabled = false; return; @@ -525,7 +525,7 @@ void RotaryEncoderUIUsermod::setup() #ifdef USERMOD_FOUR_LINE_DISPLAY // This Usermod uses FourLineDisplayUsermod for the best experience. // But it's optional. But you want it. - display = (FourLineDisplayUsermod*) usermods.lookup(USERMOD_ID_FOUR_LINE_DISP); + display = (FourLineDisplayUsermod*) UsermodManager::lookup(USERMOD_ID_FOUR_LINE_DISP); if (display != nullptr) { display->setMarkLine(1, 0); } @@ -1138,14 +1138,14 @@ bool RotaryEncoderUIUsermod::readFromConfig(JsonObject &root) { if (oldPcf8574) { if (pinIRQ >= 0) { detachInterrupt(pinIRQ); - pinManager.deallocatePin(pinIRQ, PinOwner::UM_RotaryEncoderUI); + PinManager::deallocatePin(pinIRQ, PinOwner::UM_RotaryEncoderUI); DEBUG_PRINTLN(F("Deallocated old IRQ pin.")); } pinIRQ = newIRQpin<100 ? newIRQpin : -1; // ignore PCF8574 pins } else { - pinManager.deallocatePin(pinA, PinOwner::UM_RotaryEncoderUI); - pinManager.deallocatePin(pinB, PinOwner::UM_RotaryEncoderUI); - pinManager.deallocatePin(pinC, PinOwner::UM_RotaryEncoderUI); + PinManager::deallocatePin(pinA, PinOwner::UM_RotaryEncoderUI); + PinManager::deallocatePin(pinB, PinOwner::UM_RotaryEncoderUI); + PinManager::deallocatePin(pinC, PinOwner::UM_RotaryEncoderUI); DEBUG_PRINTLN(F("Deallocated old pins.")); } pinA = newDTpin; diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 0084b09e0f..ad843f0f95 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -75,7 +75,7 @@ int8_t tristate_square8(uint8_t x, uint8_t pulsewidth, uint8_t attdec) { static um_data_t* getAudioData() { um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + if (!UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { // add support for no audio um_data = simulateSound(SEGMENT.soundSim); } @@ -6298,7 +6298,7 @@ static const char _data_FX_MODE_2DPLASMAROTOZOOM[] PROGMEM = "Rotozoomer@!,Scale uint8_t *fftResult = nullptr; float *fftBin = nullptr; um_data_t *um_data; - if (usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + if (UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { volumeSmth = *(float*) um_data->u_data[0]; volumeRaw = *(float*) um_data->u_data[1]; fftResult = (uint8_t*) um_data->u_data[2]; @@ -6911,7 +6911,7 @@ uint16_t mode_pixels(void) { // Pixels. By Andrew Tuline. uint8_t *myVals = reinterpret_cast(SEGENV.data); // Used to store a pile of samples because WLED frame rate and WLED sample rate are not synchronized. Frame rate is too low. um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + if (!UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { um_data = simulateSound(SEGMENT.soundSim); } float volumeSmth = *(float*) um_data->u_data[0]; @@ -7494,7 +7494,7 @@ uint16_t mode_2DAkemi(void) { const float normalFactor = 0.4f; um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + if (!UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { um_data = simulateSound(SEGMENT.soundSim); } uint8_t *fftResult = (uint8_t*)um_data->u_data[2]; diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 0c4ec65703..0f197e80d4 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -183,11 +183,7 @@ void IRAM_ATTR_YN Segment::deallocateData() { if ((Segment::getUsedSegmentData() > 0) && (_dataLen > 0)) { // check that we don't have a dangling / inconsistent data pointer free(data); } else { - DEBUG_PRINT(F("---- Released data ")); - DEBUG_PRINTF_P(PSTR("(%p): "), this); - DEBUG_PRINT(F("inconsistent UsedSegmentData ")); - DEBUG_PRINTF_P(PSTR("(%d/%d)"), _dataLen, Segment::getUsedSegmentData()); - DEBUG_PRINTLN(F(", cowardly refusing to free nothing.")); + DEBUG_PRINTF_P(PSTR("---- Released data (%p): inconsistent UsedSegmentData (%d/%d), cowardly refusing to free nothing.\n"), this, _dataLen, Segment::getUsedSegmentData()); } data = nullptr; Segment::addUsedSegmentData(_dataLen <= Segment::getUsedSegmentData() ? -_dataLen : -Segment::getUsedSegmentData()); @@ -1251,7 +1247,7 @@ void WS2812FX::finalizeInit() { // When booting without config (1st boot) we need to make sure GPIOs defined for LED output don't clash with hardware // i.e. DEBUG (GPIO1), DMX (2), SPI RAM/FLASH (16&17 on ESP32-WROVER/PICO), read/only pins, etc. // Pin should not be already allocated, read/only or defined for current bus - while (pinManager.isPinAllocated(defPin[j]) || !pinManager.isPinOk(defPin[j],true)) { + while (PinManager::isPinAllocated(defPin[j]) || !PinManager::isPinOk(defPin[j],true)) { if (validPin) { DEBUG_PRINTLN(F("Some of the provided pins cannot be used to configure this LED output.")); defPin[j] = 1; // start with GPIO1 and work upwards diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index b20095d4c9..3766975f12 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -130,11 +130,11 @@ BusDigital::BusDigital(BusConfig &bc, uint8_t nr, const ColorOrderMap &com) , _colorOrderMap(com) { if (!isDigital(bc.type) || !bc.count) return; - if (!pinManager.allocatePin(bc.pins[0], true, PinOwner::BusDigital)) return; + if (!PinManager::allocatePin(bc.pins[0], true, PinOwner::BusDigital)) return; _frequencykHz = 0U; _pins[0] = bc.pins[0]; if (is2Pin(bc.type)) { - if (!pinManager.allocatePin(bc.pins[1], true, PinOwner::BusDigital)) { + if (!PinManager::allocatePin(bc.pins[1], true, PinOwner::BusDigital)) { cleanup(); return; } @@ -422,8 +422,8 @@ void BusDigital::cleanup() { _valid = false; _busPtr = nullptr; if (_data != nullptr) freeData(); - pinManager.deallocatePin(_pins[1], PinOwner::BusDigital); - pinManager.deallocatePin(_pins[0], PinOwner::BusDigital); + PinManager::deallocatePin(_pins[1], PinOwner::BusDigital); + PinManager::deallocatePin(_pins[0], PinOwner::BusDigital); } @@ -464,16 +464,16 @@ BusPwm::BusPwm(BusConfig &bc) managed_pin_type pins[numPins]; for (unsigned i = 0; i < numPins; i++) pins[i] = {(int8_t)bc.pins[i], true}; - if (!pinManager.allocateMultiplePins(pins, numPins, PinOwner::BusPwm)) return; + if (!PinManager::allocateMultiplePins(pins, numPins, PinOwner::BusPwm)) return; #ifdef ESP8266 analogWriteRange((1<<_depth)-1); analogWriteFreq(_frequency); #else // for 2 pin PWM CCT strip pinManager will make sure both LEDC channels are in the same speed group and sharing the same timer - _ledcStart = pinManager.allocateLedc(numPins); + _ledcStart = PinManager::allocateLedc(numPins); if (_ledcStart == 255) { //no more free LEDC channels - pinManager.deallocateMultiplePins(pins, numPins, PinOwner::BusPwm); + PinManager::deallocateMultiplePins(pins, numPins, PinOwner::BusPwm); return; } // if _needsRefresh is true (UI hack) we are using dithering (credit @dedehai & @zalatnaicsongor) @@ -640,8 +640,8 @@ std::vector BusPwm::getLEDTypes() { void BusPwm::deallocatePins() { unsigned numPins = getPins(); for (unsigned i = 0; i < numPins; i++) { - pinManager.deallocatePin(_pins[i], PinOwner::BusPwm); - if (!pinManager.isPinOk(_pins[i])) continue; + PinManager::deallocatePin(_pins[i], PinOwner::BusPwm); + if (!PinManager::isPinOk(_pins[i])) continue; #ifdef ESP8266 digitalWrite(_pins[i], LOW); //turn off PWM interrupt #else @@ -649,7 +649,7 @@ void BusPwm::deallocatePins() { #endif } #ifdef ARDUINO_ARCH_ESP32 - pinManager.deallocateLedc(_ledcStart, numPins); + PinManager::deallocateLedc(_ledcStart, numPins); #endif } @@ -661,7 +661,7 @@ BusOnOff::BusOnOff(BusConfig &bc) if (!Bus::isOnOff(bc.type)) return; uint8_t currentPin = bc.pins[0]; - if (!pinManager.allocatePin(currentPin, true, PinOwner::BusOnOff)) { + if (!PinManager::allocatePin(currentPin, true, PinOwner::BusOnOff)) { return; } _pin = currentPin; //store only after allocatePin() succeeds @@ -904,7 +904,7 @@ void BusManager::esp32RMTInvertIdle() { void BusManager::on() { #ifdef ESP8266 //Fix for turning off onboard LED breaking bus - if (pinManager.getPinOwner(LED_BUILTIN) == PinOwner::BusDigital) { + if (PinManager::getPinOwner(LED_BUILTIN) == PinOwner::BusDigital) { for (unsigned i = 0; i < numBusses; i++) { uint8_t pins[2] = {255,255}; if (busses[i]->isDigital() && busses[i]->getPins(pins)) { @@ -926,7 +926,7 @@ void BusManager::off() { #ifdef ESP8266 // turn off built-in LED if strip is turned off // this will break digital bus so will need to be re-initialised on On - if (pinManager.getPinOwner(LED_BUILTIN) == PinOwner::BusDigital) { + if (PinManager::getPinOwner(LED_BUILTIN) == PinOwner::BusDigital) { for (unsigned i = 0; i < numBusses; i++) if (busses[i]->isOffRefreshRequired()) return; pinMode(LED_BUILTIN, OUTPUT); digitalWrite(LED_BUILTIN, HIGH); diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index 24f10f0a77..40fe61f40a 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -280,7 +280,7 @@ class BusOnOff : public Bus { uint32_t getPixelColor(uint16_t pix) const override; uint8_t getPins(uint8_t* pinArray) const override; void show() override; - void cleanup() { pinManager.deallocatePin(_pin, PinOwner::BusOnOff); } + void cleanup() { PinManager::deallocatePin(_pin, PinOwner::BusOnOff); } static std::vector getLEDTypes(); diff --git a/wled00/button.cpp b/wled00/button.cpp index b5a4e9436d..f02ed3d6d8 100644 --- a/wled00/button.cpp +++ b/wled00/button.cpp @@ -267,7 +267,7 @@ void handleButton() if (btnPin[b]<0 || buttonType[b] == BTN_TYPE_NONE) continue; #endif - if (usermods.handleButton(b)) continue; // did usermod handle buttons + if (UsermodManager::handleButton(b)) continue; // did usermod handle buttons if (buttonType[b] == BTN_TYPE_ANALOG || buttonType[b] == BTN_TYPE_ANALOG_INVERTED) { // button is not a button but a potentiometer if (now - lastAnalogRead > ANALOG_BTN_READ_CYCLE) { diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index f99aa8cd5f..3f6cfbacb6 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -261,12 +261,12 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { JsonArray hw_btn_ins = btn_obj["ins"]; if (!hw_btn_ins.isNull()) { // deallocate existing button pins - for (unsigned b = 0; b < WLED_MAX_BUTTONS; b++) pinManager.deallocatePin(btnPin[b], PinOwner::Button); // does nothing if trying to deallocate a pin with PinOwner != Button + for (unsigned b = 0; b < WLED_MAX_BUTTONS; b++) PinManager::deallocatePin(btnPin[b], PinOwner::Button); // does nothing if trying to deallocate a pin with PinOwner != Button unsigned s = 0; for (JsonObject btn : hw_btn_ins) { CJSON(buttonType[s], btn["type"]); int8_t pin = btn["pin"][0] | -1; - if (pin > -1 && pinManager.allocatePin(pin, false, PinOwner::Button)) { + if (pin > -1 && PinManager::allocatePin(pin, false, PinOwner::Button)) { btnPin[s] = pin; #ifdef ARDUINO_ARCH_ESP32 // ESP32 only: check that analog button pin is a valid ADC gpio @@ -275,7 +275,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { // not an ADC analog pin DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for analog button #%d is not an analog pin!\n"), btnPin[s], s); btnPin[s] = -1; - pinManager.deallocatePin(pin,PinOwner::Button); + PinManager::deallocatePin(pin,PinOwner::Button); } else { analogReadResolution(12); // see #4040 } @@ -286,7 +286,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { // not a touch pin DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for touch button #%d is not a touch pin!\n"), btnPin[s], s); btnPin[s] = -1; - pinManager.deallocatePin(pin,PinOwner::Button); + PinManager::deallocatePin(pin,PinOwner::Button); } //if touch pin, enable the touch interrupt on ESP32 S2 & S3 #ifdef SOC_TOUCH_VERSION_2 // ESP32 S2 and S3 have a function to check touch state but need to attach an interrupt to do so @@ -331,7 +331,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { if (fromFS) { // relies upon only being called once with fromFS == true, which is currently true. for (size_t s = 0; s < WLED_MAX_BUTTONS; s++) { - if (buttonType[s] == BTN_TYPE_NONE || btnPin[s] < 0 || !pinManager.allocatePin(btnPin[s], false, PinOwner::Button)) { + if (buttonType[s] == BTN_TYPE_NONE || btnPin[s] < 0 || !PinManager::allocatePin(btnPin[s], false, PinOwner::Button)) { btnPin[s] = -1; buttonType[s] = BTN_TYPE_NONE; } @@ -358,8 +358,8 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { #ifndef WLED_DISABLE_INFRARED int hw_ir_pin = hw["ir"]["pin"] | -2; // 4 if (hw_ir_pin > -2) { - pinManager.deallocatePin(irPin, PinOwner::IR); - if (pinManager.allocatePin(hw_ir_pin, false, PinOwner::IR)) { + PinManager::deallocatePin(irPin, PinOwner::IR); + if (PinManager::allocatePin(hw_ir_pin, false, PinOwner::IR)) { irPin = hw_ir_pin; } else { irPin = -1; @@ -374,8 +374,8 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { rlyOpenDrain = relay[F("odrain")] | rlyOpenDrain; int hw_relay_pin = relay["pin"] | -2; if (hw_relay_pin > -2) { - pinManager.deallocatePin(rlyPin, PinOwner::Relay); - if (pinManager.allocatePin(hw_relay_pin,true, PinOwner::Relay)) { + PinManager::deallocatePin(rlyPin, PinOwner::Relay); + if (PinManager::allocatePin(hw_relay_pin,true, PinOwner::Relay)) { rlyPin = hw_relay_pin; pinMode(rlyPin, rlyOpenDrain ? OUTPUT_OPEN_DRAIN : OUTPUT); } else { @@ -394,7 +394,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { CJSON(i2c_sda, hw_if_i2c[0]); CJSON(i2c_scl, hw_if_i2c[1]); PinManagerPinType i2c[2] = { { i2c_sda, true }, { i2c_scl, true } }; - if (i2c_scl >= 0 && i2c_sda >= 0 && pinManager.allocateMultiplePins(i2c, 2, PinOwner::HW_I2C)) { + if (i2c_scl >= 0 && i2c_sda >= 0 && PinManager::allocateMultiplePins(i2c, 2, PinOwner::HW_I2C)) { #ifdef ESP32 if (!Wire.setPins(i2c_sda, i2c_scl)) { i2c_scl = i2c_sda = -1; } // this will fail if Wire is initialised (Wire.begin() called prior) else Wire.begin(); @@ -410,7 +410,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { CJSON(spi_sclk, hw_if_spi[1]); CJSON(spi_miso, hw_if_spi[2]); PinManagerPinType spi[3] = { { spi_mosi, true }, { spi_miso, true }, { spi_sclk, true } }; - if (spi_mosi >= 0 && spi_sclk >= 0 && pinManager.allocateMultiplePins(spi, 3, PinOwner::HW_SPI)) { + if (spi_mosi >= 0 && spi_sclk >= 0 && PinManager::allocateMultiplePins(spi, 3, PinOwner::HW_SPI)) { #ifdef ESP32 SPI.begin(spi_sclk, spi_miso, spi_mosi); // SPI global uses VSPI on ESP32 and FSPI on C3, S3 #else @@ -664,7 +664,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { DEBUG_PRINTLN(F("Starting usermod config.")); JsonObject usermods_settings = doc["um"]; if (!usermods_settings.isNull()) { - needsSave = !usermods.readFromConfig(usermods_settings); + needsSave = !UsermodManager::readFromConfig(usermods_settings); } if (fromFS) return needsSave; @@ -700,7 +700,7 @@ void deserializeConfigFromFS() { // save default values to /cfg.json // call readFromConfig() with an empty object so that usermods can initialize to defaults prior to saving JsonObject empty = JsonObject(); - usermods.readFromConfig(empty); + UsermodManager::readFromConfig(empty); serializeConfig(); // init Ethernet (in case default type is set at compile time) #ifdef WLED_USE_ETHERNET @@ -1121,7 +1121,7 @@ void serializeConfig() { #endif JsonObject usermods_settings = root.createNestedObject("um"); - usermods.addToConfig(usermods_settings); + UsermodManager::addToConfig(usermods_settings); File f = WLED_FS.open(FPSTR(s_cfg_json), "w"); if (f) serializeJson(root, f); diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index a95064a2a6..8903d1f273 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -318,34 +318,34 @@ class Usermod { class UsermodManager { private: - Usermod* ums[WLED_MAX_USERMODS]; - byte numMods = 0; + static Usermod* ums[WLED_MAX_USERMODS]; + static byte numMods; public: - void loop(); - void handleOverlayDraw(); - bool handleButton(uint8_t b); - bool getUMData(um_data_t **um_data, uint8_t mod_id = USERMOD_ID_RESERVED); // USERMOD_ID_RESERVED will poll all usermods - void setup(); - void connected(); - void appendConfigData(); - void addToJsonState(JsonObject& obj); - void addToJsonInfo(JsonObject& obj); - void readFromJsonState(JsonObject& obj); - void addToConfig(JsonObject& obj); - bool readFromConfig(JsonObject& obj); + static void loop(); + static void handleOverlayDraw(); + static bool handleButton(uint8_t b); + static bool getUMData(um_data_t **um_data, uint8_t mod_id = USERMOD_ID_RESERVED); // USERMOD_ID_RESERVED will poll all usermods + static void setup(); + static void connected(); + static void appendConfigData(); + static void addToJsonState(JsonObject& obj); + static void addToJsonInfo(JsonObject& obj); + static void readFromJsonState(JsonObject& obj); + static void addToConfig(JsonObject& obj); + static bool readFromConfig(JsonObject& obj); #ifndef WLED_DISABLE_MQTT - void onMqttConnect(bool sessionPresent); - bool onMqttMessage(char* topic, char* payload); + static void onMqttConnect(bool sessionPresent); + static bool onMqttMessage(char* topic, char* payload); #endif #ifndef WLED_DISABLE_ESPNOW - bool onEspNowMessage(uint8_t* sender, uint8_t* payload, uint8_t len); + static bool onEspNowMessage(uint8_t* sender, uint8_t* payload, uint8_t len); #endif - void onUpdateBegin(bool); - void onStateChange(uint8_t); - bool add(Usermod* um); - Usermod* lookup(uint16_t mod_id); - byte getModCount() {return numMods;}; + static void onUpdateBegin(bool); + static void onStateChange(uint8_t); + static bool add(Usermod* um); + static Usermod* lookup(uint16_t mod_id); + static inline byte getModCount() {return numMods;}; }; //usermods_list.cpp diff --git a/wled00/json.cpp b/wled00/json.cpp index 596bd780e1..0df7294c85 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -436,7 +436,7 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId) } strip.resume(); - usermods.readFromJsonState(root); + UsermodManager::readFromJsonState(root); loadLedmap = root[F("ledmap")] | loadLedmap; @@ -592,7 +592,7 @@ void serializeState(JsonObject root, bool forPreset, bool includeBri, bool segme root[F("pl")] = currentPlaylist; root[F("ledmap")] = currentLedmap; - usermods.addToJsonState(root); + UsermodManager::addToJsonState(root); JsonObject nl = root.createNestedObject("nl"); nl["on"] = nightlightActive; @@ -784,7 +784,7 @@ void serializeInfo(JsonObject root) getTimeString(time); root[F("time")] = time; - usermods.addToJsonInfo(root); + UsermodManager::addToJsonInfo(root); uint16_t os = 0; #ifdef WLED_DEBUG diff --git a/wled00/led.cpp b/wled00/led.cpp index ba6ed25504..9de0495b45 100644 --- a/wled00/led.cpp +++ b/wled00/led.cpp @@ -131,7 +131,7 @@ void stateUpdated(byte callMode) { if (bri == nightlightTargetBri && callMode != CALL_MODE_NO_NOTIFY && nightlightMode != NL_MODE_SUN) nightlightActive = false; // notify usermods of state change - usermods.onStateChange(callMode); + UsermodManager::onStateChange(callMode); if (fadeTransition) { if (strip.getTransition() == 0) { diff --git a/wled00/mqtt.cpp b/wled00/mqtt.cpp index 833e6eb7d4..6c523c3ebf 100644 --- a/wled00/mqtt.cpp +++ b/wled00/mqtt.cpp @@ -45,7 +45,7 @@ static void onMqttConnect(bool sessionPresent) mqtt->subscribe(subuf, 0); } - usermods.onMqttConnect(sessionPresent); + UsermodManager::onMqttConnect(sessionPresent); DEBUG_PRINTLN(F("MQTT ready")); publishMqtt(); @@ -89,7 +89,7 @@ static void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProp topic += topicPrefixLen; } else { // Non-Wled Topic used here. Probably a usermod subscribed to this topic. - usermods.onMqttMessage(topic, payloadStr); + UsermodManager::onMqttMessage(topic, payloadStr); delete[] payloadStr; payloadStr = nullptr; return; @@ -115,7 +115,7 @@ static void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProp } } else if (strlen(topic) != 0) { // non standard topic, check with usermods - usermods.onMqttMessage(topic, payloadStr); + UsermodManager::onMqttMessage(topic, payloadStr); } else { // topmost topic (just wled/MAC) parseMQTTBriPayload(payloadStr); diff --git a/wled00/overlay.cpp b/wled00/overlay.cpp index 239cff528b..fcd0a40c2c 100644 --- a/wled00/overlay.cpp +++ b/wled00/overlay.cpp @@ -88,7 +88,7 @@ void _overlayAnalogCountdown() } void handleOverlayDraw() { - usermods.handleOverlayDraw(); + UsermodManager::handleOverlayDraw(); if (analogClockSolidBlack) { const Segment* segments = strip.getSegments(); for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { diff --git a/wled00/pin_manager.cpp b/wled00/pin_manager.cpp index be2a4f9770..793b5440c8 100644 --- a/wled00/pin_manager.cpp +++ b/wled00/pin_manager.cpp @@ -13,34 +13,16 @@ #endif #endif -#ifdef WLED_DEBUG -static void DebugPrintOwnerTag(PinOwner tag) -{ - uint32_t q = static_cast(tag); - if (q) { - DEBUG_PRINTF_P(PSTR("0x%02x (%d)"), q, q); - } else { - DEBUG_PRINT(F("(no owner)")); - } -} -#endif /// Actual allocation/deallocation routines -bool PinManagerClass::deallocatePin(byte gpio, PinOwner tag) +bool PinManager::deallocatePin(byte gpio, PinOwner tag) { if (gpio == 0xFF) return true; // explicitly allow clients to free -1 as a no-op if (!isPinOk(gpio, false)) return false; // but return false for any other invalid pin // if a non-zero ownerTag, only allow de-allocation if the owner's tag is provided if ((ownerTag[gpio] != PinOwner::None) && (ownerTag[gpio] != tag)) { - #ifdef WLED_DEBUG - DEBUG_PRINT(F("PIN DEALLOC: IO ")); - DEBUG_PRINT(gpio); - DEBUG_PRINT(F(" allocated by ")); - DebugPrintOwnerTag(ownerTag[gpio]); - DEBUG_PRINT(F(", but attempted de-allocation by ")); - DebugPrintOwnerTag(tag); - #endif + DEBUG_PRINTF_P(PSTR("PIN DEALLOC: FAIL GPIO %d allocated by 0x%02X, but attempted de-allocation by 0x%02X.\n"), gpio, static_cast(ownerTag[gpio]), static_cast(tag)); return false; } @@ -50,7 +32,7 @@ bool PinManagerClass::deallocatePin(byte gpio, PinOwner tag) } // support function for deallocating multiple pins -bool PinManagerClass::deallocateMultiplePins(const uint8_t *pinArray, byte arrayElementCount, PinOwner tag) +bool PinManager::deallocateMultiplePins(const uint8_t *pinArray, byte arrayElementCount, PinOwner tag) { bool shouldFail = false; DEBUG_PRINTLN(F("MULTIPIN DEALLOC")); @@ -66,14 +48,7 @@ bool PinManagerClass::deallocateMultiplePins(const uint8_t *pinArray, byte array // if the current pin is allocated by selected owner it is possible to release it continue; } - #ifdef WLED_DEBUG - DEBUG_PRINT(F("PIN DEALLOC: IO ")); - DEBUG_PRINT(gpio); - DEBUG_PRINT(F(" allocated by ")); - DebugPrintOwnerTag(ownerTag[gpio]); - DEBUG_PRINT(F(", but attempted de-allocation by ")); - DebugPrintOwnerTag(tag); - #endif + DEBUG_PRINTF_P(PSTR("PIN DEALLOC: FAIL GPIO %d allocated by 0x%02X, but attempted de-allocation by 0x%02X.\n"), gpio, static_cast(ownerTag[gpio]), static_cast(tag)); shouldFail = true; } if (shouldFail) { @@ -97,14 +72,14 @@ bool PinManagerClass::deallocateMultiplePins(const uint8_t *pinArray, byte array return true; } -bool PinManagerClass::deallocateMultiplePins(const managed_pin_type * mptArray, byte arrayElementCount, PinOwner tag) +bool PinManager::deallocateMultiplePins(const managed_pin_type * mptArray, byte arrayElementCount, PinOwner tag) { uint8_t pins[arrayElementCount]; for (int i=0; i(ownerTag[gpio])); shouldFail = true; } } @@ -158,64 +122,45 @@ bool PinManagerClass::allocateMultiplePins(const managed_pin_type * mptArray, by bitWrite(pinAlloc, gpio, true); ownerTag[gpio] = tag; - #ifdef WLED_DEBUG - DEBUG_PRINT(F("PIN ALLOC: Pin ")); - DEBUG_PRINT(gpio); - DEBUG_PRINT(F(" allocated by ")); - DebugPrintOwnerTag(tag); - DEBUG_PRINTLN(F("")); - #endif + DEBUG_PRINTF_P(PSTR("PIN ALLOC: Pin %d allocated by 0x%02X.\n"), gpio, static_cast(tag)); } + DEBUG_PRINTF_P(PSTR("PIN ALLOC: 0x%014llX.\n"), (unsigned long long)pinAlloc); return true; } -bool PinManagerClass::allocatePin(byte gpio, bool output, PinOwner tag) +bool PinManager::allocatePin(byte gpio, bool output, PinOwner tag) { // HW I2C & SPI pins have to be allocated using allocateMultiplePins variant since there is always SCL/SDA pair if (!isPinOk(gpio, output) || (gpio >= WLED_NUM_PINS) || tag==PinOwner::HW_I2C || tag==PinOwner::HW_SPI) { #ifdef WLED_DEBUG if (gpio < 255) { // 255 (-1) is the "not defined GPIO" if (!isPinOk(gpio, output)) { - DEBUG_PRINT(F("PIN ALLOC: FAIL for owner ")); - DebugPrintOwnerTag(tag); - DEBUG_PRINT(F(": GPIO ")); DEBUG_PRINT(gpio); + DEBUG_PRINTF_P(PSTR("PIN ALLOC: FAIL for owner 0x%02X: GPIO %d "), static_cast(tag), gpio); if (output) DEBUG_PRINTLN(F(" cannot be used for i/o on this MCU.")); else DEBUG_PRINTLN(F(" cannot be used as input on this MCU.")); } else { - DEBUG_PRINT(F("PIN ALLOC: FAIL: GPIO ")); DEBUG_PRINT(gpio); - DEBUG_PRINTLN(F(" - HW I2C & SPI pins have to be allocated using allocateMultiplePins()")); + DEBUG_PRINTF_P(PSTR("PIN ALLOC: FAIL GPIO %d - HW I2C & SPI pins have to be allocated using allocateMultiplePins.\n"), gpio); } } #endif return false; } if (isPinAllocated(gpio)) { - #ifdef WLED_DEBUG - DEBUG_PRINT(F("PIN ALLOC: Pin ")); - DEBUG_PRINT(gpio); - DEBUG_PRINT(F(" already allocated by ")); - DebugPrintOwnerTag(ownerTag[gpio]); - DEBUG_PRINTLN(F("")); - #endif + DEBUG_PRINTF_P(PSTR("PIN ALLOC: FAIL Pin %d already allocated by 0x%02X.\n"), gpio, static_cast(ownerTag[gpio])); return false; } bitWrite(pinAlloc, gpio, true); ownerTag[gpio] = tag; - #ifdef WLED_DEBUG - DEBUG_PRINT(F("PIN ALLOC: Pin ")); - DEBUG_PRINT(gpio); - DEBUG_PRINT(F(" successfully allocated by ")); - DebugPrintOwnerTag(tag); - DEBUG_PRINTLN(F("")); - #endif + DEBUG_PRINTF_P(PSTR("PIN ALLOC: Pin %d successfully allocated by 0x%02X.\n"), gpio, static_cast(ownerTag[gpio])); + DEBUG_PRINTF_P(PSTR("PIN ALLOC: 0x%014llX.\n"), (unsigned long long)pinAlloc); return true; } // if tag is set to PinOwner::None, checks for ANY owner of the pin. // if tag is set to any other value, checks if that tag is the current owner of the pin. -bool PinManagerClass::isPinAllocated(byte gpio, PinOwner tag) const +bool PinManager::isPinAllocated(byte gpio, PinOwner tag) { if (!isPinOk(gpio, false)) return true; if ((tag != PinOwner::None) && (ownerTag[gpio] != tag)) return false; @@ -239,7 +184,7 @@ bool PinManagerClass::isPinAllocated(byte gpio, PinOwner tag) const */ // Check if supplied GPIO is ok to use -bool PinManagerClass::isPinOk(byte gpio, bool output) const +bool PinManager::isPinOk(byte gpio, bool output) { if (gpio >= WLED_NUM_PINS) return false; // catch error case, to avoid array out-of-bounds access #ifdef ARDUINO_ARCH_ESP32 @@ -279,7 +224,7 @@ bool PinManagerClass::isPinOk(byte gpio, bool output) const return false; } -bool PinManagerClass::isReadOnlyPin(byte gpio) +bool PinManager::isReadOnlyPin(byte gpio) { #ifdef ARDUINO_ARCH_ESP32 if (gpio < WLED_NUM_PINS) return (digitalPinIsValid(gpio) && !digitalPinCanOutput(gpio)); @@ -287,14 +232,14 @@ bool PinManagerClass::isReadOnlyPin(byte gpio) return false; } -PinOwner PinManagerClass::getPinOwner(byte gpio) const +PinOwner PinManager::getPinOwner(byte gpio) { if (!isPinOk(gpio, false)) return PinOwner::None; return ownerTag[gpio]; } #ifdef ARDUINO_ARCH_ESP32 -byte PinManagerClass::allocateLedc(byte channels) +byte PinManager::allocateLedc(byte channels) { if (channels > WLED_MAX_ANALOG_CHANNELS || channels == 0) return 255; unsigned ca = 0; @@ -321,7 +266,7 @@ byte PinManagerClass::allocateLedc(byte channels) return 255; //not enough consecutive free LEDC channels } -void PinManagerClass::deallocateLedc(byte pos, byte channels) +void PinManager::deallocateLedc(byte pos, byte channels) { for (unsigned j = pos; j < pos + channels && j < WLED_MAX_ANALOG_CHANNELS; j++) { bitWrite(ledcAlloc, j, false); @@ -329,4 +274,12 @@ void PinManagerClass::deallocateLedc(byte pos, byte channels) } #endif -PinManagerClass pinManager = PinManagerClass(); +#ifdef ESP8266 +uint32_t PinManager::pinAlloc = 0UL; +#else +uint64_t PinManager::pinAlloc = 0ULL; +uint16_t PinManager::ledcAlloc = 0; +#endif +uint8_t PinManager::i2cAllocCount = 0; +uint8_t PinManager::spiAllocCount = 0; +PinOwner PinManager::ownerTag[WLED_NUM_PINS] = { PinOwner::None }; diff --git a/wled00/pin_manager.h b/wled00/pin_manager.h index a64900c891..73a4a36564 100644 --- a/wled00/pin_manager.h +++ b/wled00/pin_manager.h @@ -70,61 +70,54 @@ enum struct PinOwner : uint8_t { }; static_assert(0u == static_cast(PinOwner::None), "PinOwner::None must be zero, so default array initialization works as expected"); -class PinManagerClass { +class PinManager { private: - struct { #ifdef ESP8266 - #define WLED_NUM_PINS (GPIO_PIN_COUNT+1) // somehow they forgot GPIO 16 (0-16==17) - uint32_t pinAlloc : 24; // 24bit, 1 bit per pin, we use first 17bits + #define WLED_NUM_PINS (GPIO_PIN_COUNT+1) // somehow they forgot GPIO 16 (0-16==17) + static uint32_t pinAlloc; // 1 bit per pin, we use first 17bits #else - #define WLED_NUM_PINS (GPIO_PIN_COUNT) - uint64_t pinAlloc : 56; // 56 bits, 1 bit per pin, we use 50 bits on ESP32-S3 - uint16_t ledcAlloc : 16; // up to 16 LEDC channels (WLED_MAX_ANALOG_CHANNELS) + #define WLED_NUM_PINS (GPIO_PIN_COUNT) + static uint64_t pinAlloc; // 1 bit per pin, we use 50 bits on ESP32-S3 + static uint16_t ledcAlloc; // up to 16 LEDC channels (WLED_MAX_ANALOG_CHANNELS) #endif - uint8_t i2cAllocCount : 4; // allow multiple allocation of I2C bus pins but keep track of allocations - uint8_t spiAllocCount : 4; // allow multiple allocation of SPI bus pins but keep track of allocations - } __attribute__ ((packed)); - PinOwner ownerTag[WLED_NUM_PINS] = { PinOwner::None }; + static uint8_t i2cAllocCount; // allow multiple allocation of I2C bus pins but keep track of allocations + static uint8_t spiAllocCount; // allow multiple allocation of SPI bus pins but keep track of allocations + static PinOwner ownerTag[WLED_NUM_PINS]; public: - PinManagerClass() : pinAlloc(0ULL), i2cAllocCount(0), spiAllocCount(0) { - #ifdef ARDUINO_ARCH_ESP32 - ledcAlloc = 0; - #endif - } - // De-allocates a single pin - bool deallocatePin(byte gpio, PinOwner tag); - // De-allocates multiple pins but only if all can be deallocated (PinOwner has to be specified) - bool deallocateMultiplePins(const uint8_t *pinArray, byte arrayElementCount, PinOwner tag); - bool deallocateMultiplePins(const managed_pin_type *pinArray, byte arrayElementCount, PinOwner tag); - // Allocates a single pin, with an owner tag. - // De-allocation requires the same owner tag (or override) - bool allocatePin(byte gpio, bool output, PinOwner tag); - // Allocates all the pins, or allocates none of the pins, with owner tag. - // Provided to simplify error condition handling in clients - // using more than one pin, such as I2C, SPI, rotary encoders, - // ethernet, etc.. - bool allocateMultiplePins(const managed_pin_type * mptArray, byte arrayElementCount, PinOwner tag ); + // De-allocates a single pin + static bool deallocatePin(byte gpio, PinOwner tag); + // De-allocates multiple pins but only if all can be deallocated (PinOwner has to be specified) + static bool deallocateMultiplePins(const uint8_t *pinArray, byte arrayElementCount, PinOwner tag); + static bool deallocateMultiplePins(const managed_pin_type *pinArray, byte arrayElementCount, PinOwner tag); + // Allocates a single pin, with an owner tag. + // De-allocation requires the same owner tag (or override) + static bool allocatePin(byte gpio, bool output, PinOwner tag); + // Allocates all the pins, or allocates none of the pins, with owner tag. + // Provided to simplify error condition handling in clients + // using more than one pin, such as I2C, SPI, rotary encoders, + // ethernet, etc.. + static bool allocateMultiplePins(const managed_pin_type * mptArray, byte arrayElementCount, PinOwner tag ); - [[deprecated("Replaced by three-parameter allocatePin(gpio, output, ownerTag), for improved debugging")]] - inline bool allocatePin(byte gpio, bool output = true) { return allocatePin(gpio, output, PinOwner::None); } - [[deprecated("Replaced by two-parameter deallocatePin(gpio, ownerTag), for improved debugging")]] - inline void deallocatePin(byte gpio) { deallocatePin(gpio, PinOwner::None); } + [[deprecated("Replaced by three-parameter allocatePin(gpio, output, ownerTag), for improved debugging")]] + static inline bool allocatePin(byte gpio, bool output = true) { return allocatePin(gpio, output, PinOwner::None); } + [[deprecated("Replaced by two-parameter deallocatePin(gpio, ownerTag), for improved debugging")]] + static inline void deallocatePin(byte gpio) { deallocatePin(gpio, PinOwner::None); } - // will return true for reserved pins - bool isPinAllocated(byte gpio, PinOwner tag = PinOwner::None) const; - // will return false for reserved pins - bool isPinOk(byte gpio, bool output = true) const; - - static bool isReadOnlyPin(byte gpio); + // will return true for reserved pins + static bool isPinAllocated(byte gpio, PinOwner tag = PinOwner::None); + // will return false for reserved pins + static bool isPinOk(byte gpio, bool output = true); + + static bool isReadOnlyPin(byte gpio); - PinOwner getPinOwner(byte gpio) const; + static PinOwner getPinOwner(byte gpio); - #ifdef ARDUINO_ARCH_ESP32 - byte allocateLedc(byte channels); - void deallocateLedc(byte pos, byte channels); - #endif + #ifdef ARDUINO_ARCH_ESP32 + static byte allocateLedc(byte channels); + static void deallocateLedc(byte pos, byte channels); + #endif }; -extern PinManagerClass pinManager; +//extern PinManager pinManager; #endif diff --git a/wled00/set.cpp b/wled00/set.cpp index 812bcc52f3..96eb3ed13b 100644 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -104,18 +104,18 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) { int t = 0; - if (rlyPin>=0 && pinManager.isPinAllocated(rlyPin, PinOwner::Relay)) { - pinManager.deallocatePin(rlyPin, PinOwner::Relay); + if (rlyPin>=0 && PinManager::isPinAllocated(rlyPin, PinOwner::Relay)) { + PinManager::deallocatePin(rlyPin, PinOwner::Relay); } #ifndef WLED_DISABLE_INFRARED - if (irPin>=0 && pinManager.isPinAllocated(irPin, PinOwner::IR)) { + if (irPin>=0 && PinManager::isPinAllocated(irPin, PinOwner::IR)) { deInitIR(); - pinManager.deallocatePin(irPin, PinOwner::IR); + PinManager::deallocatePin(irPin, PinOwner::IR); } #endif for (unsigned s=0; s=0 && pinManager.isPinAllocated(btnPin[s], PinOwner::Button)) { - pinManager.deallocatePin(btnPin[s], PinOwner::Button); + if (btnPin[s]>=0 && PinManager::isPinAllocated(btnPin[s], PinOwner::Button)) { + PinManager::deallocatePin(btnPin[s], PinOwner::Button); #ifdef SOC_TOUCH_VERSION_2 // ESP32 S2 and S3 have a function to check touch state, detach interrupt if (digitalPinToTouchChannel(btnPin[s]) >= 0) // if touch capable pin touchDetachInterrupt(btnPin[s]); // if not assigned previously, this will do nothing @@ -233,7 +233,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) // update other pins #ifndef WLED_DISABLE_INFRARED int hw_ir_pin = request->arg(F("IR")).toInt(); - if (pinManager.allocatePin(hw_ir_pin,false, PinOwner::IR)) { + if (PinManager::allocatePin(hw_ir_pin,false, PinOwner::IR)) { irPin = hw_ir_pin; } else { irPin = -1; @@ -244,7 +244,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) irApplyToAllSelected = !request->hasArg(F("MSO")); int hw_rly_pin = request->arg(F("RL")).toInt(); - if (pinManager.allocatePin(hw_rly_pin,true, PinOwner::Relay)) { + if (PinManager::allocatePin(hw_rly_pin,true, PinOwner::Relay)) { rlyPin = hw_rly_pin; } else { rlyPin = -1; @@ -259,7 +259,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) char bt[4] = "BT"; bt[2] = offset+i; bt[3] = 0; // button pin (use A,B,C,... if WLED_MAX_BUTTONS>10) char be[4] = "BE"; be[2] = offset+i; be[3] = 0; // button type (use A,B,C,... if WLED_MAX_BUTTONS>10) int hw_btn_pin = request->arg(bt).toInt(); - if (hw_btn_pin >= 0 && pinManager.allocatePin(hw_btn_pin,false,PinOwner::Button)) { + if (hw_btn_pin >= 0 && PinManager::allocatePin(hw_btn_pin,false,PinOwner::Button)) { btnPin[i] = hw_btn_pin; buttonType[i] = request->arg(be).toInt(); #ifdef ARDUINO_ARCH_ESP32 @@ -270,7 +270,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) // not an ADC analog pin DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for analog button #%d is not an analog pin!\n"), btnPin[i], i); btnPin[i] = -1; - pinManager.deallocatePin(hw_btn_pin,PinOwner::Button); + PinManager::deallocatePin(hw_btn_pin,PinOwner::Button); } else { analogReadResolution(12); // see #4040 } @@ -282,7 +282,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) // not a touch pin DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for touch button #%d is not an touch pin!\n"), btnPin[i], i); btnPin[i] = -1; - pinManager.deallocatePin(hw_btn_pin,PinOwner::Button); + PinManager::deallocatePin(hw_btn_pin,PinOwner::Button); } #ifdef SOC_TOUCH_VERSION_2 // ESP32 S2 and S3 have a fucntion to check touch state but need to attach an interrupt to do so else @@ -631,10 +631,10 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) if (i2c_sda != hw_sda_pin || i2c_scl != hw_scl_pin) { // only if pins changed uint8_t old_i2c[2] = { static_cast(i2c_scl), static_cast(i2c_sda) }; - pinManager.deallocateMultiplePins(old_i2c, 2, PinOwner::HW_I2C); // just in case deallocation of old pins + PinManager::deallocateMultiplePins(old_i2c, 2, PinOwner::HW_I2C); // just in case deallocation of old pins PinManagerPinType i2c[2] = { { hw_sda_pin, true }, { hw_scl_pin, true } }; - if (hw_sda_pin >= 0 && hw_scl_pin >= 0 && pinManager.allocateMultiplePins(i2c, 2, PinOwner::HW_I2C)) { + if (hw_sda_pin >= 0 && hw_scl_pin >= 0 && PinManager::allocateMultiplePins(i2c, 2, PinOwner::HW_I2C)) { i2c_sda = hw_sda_pin; i2c_scl = hw_scl_pin; // no bus re-initialisation as usermods do not get any notification @@ -658,9 +658,9 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) if (spi_mosi != hw_mosi_pin || spi_miso != hw_miso_pin || spi_sclk != hw_sclk_pin) { // only if pins changed uint8_t old_spi[3] = { static_cast(spi_mosi), static_cast(spi_miso), static_cast(spi_sclk) }; - pinManager.deallocateMultiplePins(old_spi, 3, PinOwner::HW_SPI); // just in case deallocation of old pins + PinManager::deallocateMultiplePins(old_spi, 3, PinOwner::HW_SPI); // just in case deallocation of old pins PinManagerPinType spi[3] = { { hw_mosi_pin, true }, { hw_miso_pin, true }, { hw_sclk_pin, true } }; - if (hw_mosi_pin >= 0 && hw_sclk_pin >= 0 && pinManager.allocateMultiplePins(spi, 3, PinOwner::HW_SPI)) { + if (hw_mosi_pin >= 0 && hw_sclk_pin >= 0 && PinManager::allocateMultiplePins(spi, 3, PinOwner::HW_SPI)) { spi_mosi = hw_mosi_pin; spi_miso = hw_miso_pin; spi_sclk = hw_sclk_pin; @@ -750,8 +750,8 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) DEBUG_PRINTF_P(PSTR(" = %s\n"), value.c_str()); } } - usermods.readFromConfig(um); // force change of usermod parameters - DEBUG_PRINTLN(F("Done re-init usermods.")); + UsermodManager::readFromConfig(um); // force change of usermod parameters + DEBUG_PRINTLN(F("Done re-init UsermodManager::")); releaseJSONBufferLock(); } diff --git a/wled00/udp.cpp b/wled00/udp.cpp index 8cf733dffc..09e1440efa 100644 --- a/wled00/udp.cpp +++ b/wled00/udp.cpp @@ -976,7 +976,7 @@ void espNowReceiveCB(uint8_t* address, uint8_t* data, uint8_t len, signed int rs #ifndef WLED_DISABLE_ESPNOW // usermods hook can override processing - if (usermods.onEspNowMessage(address, data, len)) return; + if (UsermodManager::onEspNowMessage(address, data, len)) return; #endif // handle WiZ Mote data diff --git a/wled00/um_manager.cpp b/wled00/um_manager.cpp index 2db29c3cda..d4ed8135fe 100644 --- a/wled00/um_manager.cpp +++ b/wled00/um_manager.cpp @@ -68,3 +68,6 @@ bool UsermodManager::add(Usermod* um) ums[numMods++] = um; return true; } + +Usermod* UsermodManager::ums[WLED_MAX_USERMODS] = {nullptr}; +byte UsermodManager::numMods = 0; diff --git a/wled00/usermods_list.cpp b/wled00/usermods_list.cpp index 25d9ee9ab9..36bd122a51 100644 --- a/wled00/usermods_list.cpp +++ b/wled00/usermods_list.cpp @@ -249,225 +249,225 @@ void registerUsermods() * || || || * \/ \/ \/ */ - //usermods.add(new MyExampleUsermod()); + //UsermodManager::add(new MyExampleUsermod()); #ifdef USERMOD_BATTERY - usermods.add(new UsermodBattery()); + UsermodManager::add(new UsermodBattery()); #endif #ifdef USERMOD_DALLASTEMPERATURE - usermods.add(new UsermodTemperature()); + UsermodManager::add(new UsermodTemperature()); #endif #ifdef USERMOD_SN_PHOTORESISTOR - usermods.add(new Usermod_SN_Photoresistor()); + UsermodManager::add(new Usermod_SN_Photoresistor()); #endif #ifdef USERMOD_PWM_FAN - usermods.add(new PWMFanUsermod()); + UsermodManager::add(new PWMFanUsermod()); #endif #ifdef USERMOD_BUZZER - usermods.add(new BuzzerUsermod()); + UsermodManager::add(new BuzzerUsermod()); #endif #ifdef USERMOD_BH1750 - usermods.add(new Usermod_BH1750()); + UsermodManager::add(new Usermod_BH1750()); #endif #ifdef USERMOD_BME280 - usermods.add(new UsermodBME280()); + UsermodManager::add(new UsermodBME280()); #endif #ifdef USERMOD_BME68X - usermods.add(new UsermodBME68X()); + UsermodManager::add(new UsermodBME68X()); #endif #ifdef USERMOD_SENSORSTOMQTT - usermods.add(new UserMod_SensorsToMQTT()); + UsermodManager::add(new UserMod_SensorsToMQTT()); #endif #ifdef USERMOD_PIRSWITCH - usermods.add(new PIRsensorSwitch()); + UsermodManager::add(new PIRsensorSwitch()); #endif #ifdef USERMOD_FOUR_LINE_DISPLAY - usermods.add(new FourLineDisplayUsermod()); + UsermodManager::add(new FourLineDisplayUsermod()); #endif #ifdef USERMOD_ROTARY_ENCODER_UI - usermods.add(new RotaryEncoderUIUsermod()); // can use USERMOD_FOUR_LINE_DISPLAY + UsermodManager::add(new RotaryEncoderUIUsermod()); // can use USERMOD_FOUR_LINE_DISPLAY #endif #ifdef USERMOD_AUTO_SAVE - usermods.add(new AutoSaveUsermod()); // can use USERMOD_FOUR_LINE_DISPLAY + UsermodManager::add(new AutoSaveUsermod()); // can use USERMOD_FOUR_LINE_DISPLAY #endif #ifdef USERMOD_DHT - usermods.add(new UsermodDHT()); + UsermodManager::add(new UsermodDHT()); #endif #ifdef USERMOD_VL53L0X_GESTURES - usermods.add(new UsermodVL53L0XGestures()); + UsermodManager::add(new UsermodVL53L0XGestures()); #endif #ifdef USERMOD_ANIMATED_STAIRCASE - usermods.add(new Animated_Staircase()); + UsermodManager::add(new Animated_Staircase()); #endif #ifdef USERMOD_MULTI_RELAY - usermods.add(new MultiRelay()); + UsermodManager::add(new MultiRelay()); #endif #ifdef USERMOD_RTC - usermods.add(new RTCUsermod()); + UsermodManager::add(new RTCUsermod()); #endif #ifdef USERMOD_ELEKSTUBE_IPS - usermods.add(new ElekstubeIPSUsermod()); + UsermodManager::add(new ElekstubeIPSUsermod()); #endif #ifdef USERMOD_ROTARY_ENCODER_BRIGHTNESS_COLOR - usermods.add(new RotaryEncoderBrightnessColor()); + UsermodManager::add(new RotaryEncoderBrightnessColor()); #endif #ifdef RGB_ROTARY_ENCODER - usermods.add(new RgbRotaryEncoderUsermod()); + UsermodManager::add(new RgbRotaryEncoderUsermod()); #endif #ifdef USERMOD_ST7789_DISPLAY - usermods.add(new St7789DisplayUsermod()); + UsermodManager::add(new St7789DisplayUsermod()); #endif #ifdef USERMOD_PIXELS_DICE_TRAY - usermods.add(new PixelsDiceTrayUsermod()); + UsermodManager::add(new PixelsDiceTrayUsermod()); #endif #ifdef USERMOD_SEVEN_SEGMENT - usermods.add(new SevenSegmentDisplay()); + UsermodManager::add(new SevenSegmentDisplay()); #endif #ifdef USERMOD_SSDR - usermods.add(new UsermodSSDR()); + UsermodManager::add(new UsermodSSDR()); #endif #ifdef USERMOD_CRONIXIE - usermods.add(new UsermodCronixie()); + UsermodManager::add(new UsermodCronixie()); #endif #ifdef QUINLED_AN_PENTA - usermods.add(new QuinLEDAnPentaUsermod()); + UsermodManager::add(new QuinLEDAnPentaUsermod()); #endif #ifdef USERMOD_WIZLIGHTS - usermods.add(new WizLightsUsermod()); + UsermodManager::add(new WizLightsUsermod()); #endif #ifdef USERMOD_WIREGUARD - usermods.add(new WireguardUsermod()); + UsermodManager::add(new WireguardUsermod()); #endif #ifdef USERMOD_WORDCLOCK - usermods.add(new WordClockUsermod()); + UsermodManager::add(new WordClockUsermod()); #endif #ifdef USERMOD_MY9291 - usermods.add(new MY9291Usermod()); + UsermodManager::add(new MY9291Usermod()); #endif #ifdef USERMOD_SI7021_MQTT_HA - usermods.add(new Si7021_MQTT_HA()); + UsermodManager::add(new Si7021_MQTT_HA()); #endif #ifdef USERMOD_SMARTNEST - usermods.add(new Smartnest()); + UsermodManager::add(new Smartnest()); #endif #ifdef USERMOD_AUDIOREACTIVE - usermods.add(new AudioReactive()); + UsermodManager::add(new AudioReactive()); #endif #ifdef USERMOD_ANALOG_CLOCK - usermods.add(new AnalogClockUsermod()); + UsermodManager::add(new AnalogClockUsermod()); #endif #ifdef USERMOD_PING_PONG_CLOCK - usermods.add(new PingPongClockUsermod()); + UsermodManager::add(new PingPongClockUsermod()); #endif #ifdef USERMOD_ADS1115 - usermods.add(new ADS1115Usermod()); + UsermodManager::add(new ADS1115Usermod()); #endif #ifdef USERMOD_KLIPPER_PERCENTAGE - usermods.add(new klipper_percentage()); + UsermodManager::add(new klipper_percentage()); #endif #ifdef USERMOD_BOBLIGHT - usermods.add(new BobLightUsermod()); + UsermodManager::add(new BobLightUsermod()); #endif #ifdef SD_ADAPTER - usermods.add(new UsermodSdCard()); + UsermodManager::add(new UsermodSdCard()); #endif #ifdef USERMOD_PWM_OUTPUTS - usermods.add(new PwmOutputsUsermod()); + UsermodManager::add(new PwmOutputsUsermod()); #endif #ifdef USERMOD_SHT - usermods.add(new ShtUsermod()); + UsermodManager::add(new ShtUsermod()); #endif #ifdef USERMOD_ANIMARTRIX - usermods.add(new AnimartrixUsermod("Animartrix", false)); + UsermodManager::add(new AnimartrixUsermod("Animartrix", false)); #endif #ifdef USERMOD_INTERNAL_TEMPERATURE - usermods.add(new InternalTemperatureUsermod()); + UsermodManager::add(new InternalTemperatureUsermod()); #endif #ifdef USERMOD_HTTP_PULL_LIGHT_CONTROL - usermods.add(new HttpPullLightControl()); + UsermodManager::add(new HttpPullLightControl()); #endif #ifdef USERMOD_MPU6050_IMU - static MPU6050Driver mpu6050; usermods.add(&mpu6050); + static MPU6050Driver mpu6050; UsermodManager::add(&mpu6050); #endif #ifdef USERMOD_GYRO_SURGE - static GyroSurge gyro_surge; usermods.add(&gyro_surge); + static GyroSurge gyro_surge; UsermodManager::add(&gyro_surge); #endif #ifdef USERMOD_LDR_DUSK_DAWN - usermods.add(new LDR_Dusk_Dawn_v2()); + UsermodManager::add(new LDR_Dusk_Dawn_v2()); #endif #ifdef USERMOD_STAIRCASE_WIPE - usermods.add(new StairwayWipeUsermod()); + UsermodManager::add(new StairwayWipeUsermod()); #endif #ifdef USERMOD_MAX17048 - usermods.add(new Usermod_MAX17048()); + UsermodManager::add(new Usermod_MAX17048()); #endif #ifdef USERMOD_TETRISAI - usermods.add(new TetrisAIUsermod()); + UsermodManager::add(new TetrisAIUsermod()); #endif #ifdef USERMOD_AHT10 - usermods.add(new UsermodAHT10()); + UsermodManager::add(new UsermodAHT10()); #endif #ifdef USERMOD_INA226 - usermods.add(new UsermodINA226()); + UsermodManager::add(new UsermodINA226()); #endif #ifdef USERMOD_LD2410 - usermods.add(new LD2410Usermod()); + UsermodManager::add(new LD2410Usermod()); #endif #ifdef USERMOD_POV_DISPLAY - usermods.add(new PovDisplayUsermod()); + UsermodManager::add(new PovDisplayUsermod()); #endif } diff --git a/wled00/wled.cpp b/wled00/wled.cpp index bc1cc7b733..39e0d250be 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -72,7 +72,7 @@ void WLED::loop() unsigned long usermodMillis = millis(); #endif userLoop(); - usermods.loop(); + UsermodManager::loop(); #ifdef WLED_DEBUG usermodMillis = millis() - usermodMillis; avgUsermodMillis += usermodMillis; @@ -410,10 +410,10 @@ void WLED::setup() #endif #if defined(WLED_DEBUG) && !defined(WLED_DEBUG_HOST) - pinManager.allocatePin(hardwareTX, true, PinOwner::DebugOut); // TX (GPIO1 on ESP32) reserved for debug output + PinManager::allocatePin(hardwareTX, true, PinOwner::DebugOut); // TX (GPIO1 on ESP32) reserved for debug output #endif #ifdef WLED_ENABLE_DMX //reserve GPIO2 as hardcoded DMX pin - pinManager.allocatePin(2, true, PinOwner::DMX); + PinManager::allocatePin(2, true, PinOwner::DMX); #endif DEBUG_PRINTLN(F("Registering usermods ...")); @@ -452,7 +452,7 @@ void WLED::setup() DEBUG_PRINTF_P(PSTR("heap %u\n"), ESP.getFreeHeap()); #if defined(STATUSLED) && STATUSLED>=0 - if (!pinManager.isPinAllocated(STATUSLED)) { + if (!PinManager::isPinAllocated(STATUSLED)) { // NOTE: Special case: The status LED should *NOT* be allocated. // See comments in handleStatusLed(). pinMode(STATUSLED, OUTPUT); @@ -465,7 +465,7 @@ void WLED::setup() DEBUG_PRINTLN(F("Usermods setup")); userSetup(); - usermods.setup(); + UsermodManager::setup(); DEBUG_PRINTF_P(PSTR("heap %u\n"), ESP.getFreeHeap()); if (strcmp(multiWiFi[0].clientSSID, DEFAULT_CLIENT_SSID) == 0) @@ -479,8 +479,8 @@ void WLED::setup() findWiFi(true); // start scanning for available WiFi-s // all GPIOs are allocated at this point - serialCanRX = !pinManager.isPinAllocated(hardwareRX); // Serial RX pin (GPIO 3 on ESP32 and ESP8266) - serialCanTX = !pinManager.isPinAllocated(hardwareTX) || pinManager.getPinOwner(hardwareTX) == PinOwner::DebugOut; // Serial TX pin (GPIO 1 on ESP32 and ESP8266) + serialCanRX = !PinManager::isPinAllocated(hardwareRX); // Serial RX pin (GPIO 3 on ESP32 and ESP8266) + serialCanTX = !PinManager::isPinAllocated(hardwareTX) || PinManager::getPinOwner(hardwareTX) == PinOwner::DebugOut; // Serial TX pin (GPIO 1 on ESP32 and ESP8266) #ifdef WLED_ENABLE_ADALIGHT //Serial RX (Adalight, Improv, Serial JSON) only possible if GPIO3 unused @@ -685,7 +685,7 @@ bool WLED::initEthernet() return false; } - if (!pinManager.allocateMultiplePins(pinsToAllocate, 10, PinOwner::Ethernet)) { + if (!PinManager::allocateMultiplePins(pinsToAllocate, 10, PinOwner::Ethernet)) { DEBUG_PRINTLN(F("initE: Failed to allocate ethernet pins")); return false; } @@ -719,7 +719,7 @@ bool WLED::initEthernet() DEBUG_PRINTLN(F("initC: ETH.begin() failed")); // de-allocate the allocated pins for (managed_pin_type mpt : pinsToAllocate) { - pinManager.deallocatePin(mpt.pin, PinOwner::Ethernet); + PinManager::deallocatePin(mpt.pin, PinOwner::Ethernet); } return false; } @@ -1010,7 +1010,7 @@ void WLED::handleConnection() } initInterfaces(); userConnected(); - usermods.connected(); + UsermodManager::connected(); lastMqttReconnectAttempt = 0; // force immediate update // shut down AP @@ -1033,7 +1033,7 @@ void WLED::handleStatusLED() uint32_t c = 0; #if STATUSLED>=0 - if (pinManager.isPinAllocated(STATUSLED)) { + if (PinManager::isPinAllocated(STATUSLED)) { return; //lower priority if something else uses the same pin } #endif diff --git a/wled00/wled_server.cpp b/wled00/wled_server.cpp index 9d4e4c85b9..7d6fecd8b1 100644 --- a/wled00/wled_server.cpp +++ b/wled00/wled_server.cpp @@ -396,7 +396,7 @@ void initServer() #if WLED_WATCHDOG_TIMEOUT > 0 WLED::instance().disableWatchdog(); #endif - usermods.onUpdateBegin(true); // notify usermods that update is about to begin (some may require task de-init) + UsermodManager::onUpdateBegin(true); // notify usermods that update is about to begin (some may require task de-init) lastEditTime = millis(); // make sure PIN does not lock during update strip.suspend(); #ifdef ESP8266 @@ -412,7 +412,7 @@ void initServer() } else { DEBUG_PRINTLN(F("Update Failed")); strip.resume(); - usermods.onUpdateBegin(false); // notify usermods that update has failed (some may require task init) + UsermodManager::onUpdateBegin(false); // notify usermods that update has failed (some may require task init) #if WLED_WATCHDOG_TIMEOUT > 0 WLED::instance().enableWatchdog(); #endif diff --git a/wled00/xml.cpp b/wled00/xml.cpp index 71d66d0022..a9195a3090 100644 --- a/wled00/xml.cpp +++ b/wled00/xml.cpp @@ -135,7 +135,7 @@ void appendGPIOinfo() { if (requestJSONBufferLock(6)) { // if we can't allocate JSON buffer ignore usermod pins JsonObject mods = pDoc->createNestedObject(F("um")); - usermods.addToConfig(mods); + UsermodManager::addToConfig(mods); if (!mods.isNull()) fillUMPins(mods); releaseJSONBufferLock(); } @@ -144,7 +144,7 @@ void appendGPIOinfo() { // add reserved (unusable) pins oappend(SET_F("d.rsvd=[")); for (unsigned i = 0; i < WLED_NUM_PINS; i++) { - if (!pinManager.isPinOk(i, false)) { // include readonly pins + if (!PinManager::isPinOk(i, false)) { // include readonly pins oappendi(i); oappend(","); } } @@ -181,7 +181,7 @@ void appendGPIOinfo() { oappend(SET_F("d.ro_gpio=[")); bool firstPin = true; for (unsigned i = 0; i < WLED_NUM_PINS; i++) { - if (pinManager.isReadOnlyPin(i)) { + if (PinManager::isReadOnlyPin(i)) { // No comma before the first pin if (!firstPin) oappend(SET_F(",")); oappendi(i); @@ -370,7 +370,7 @@ void getSettingsJS(byte subPage, char* dest) int nPins = bus->getPins(pins); for (int i = 0; i < nPins; i++) { lp[1] = offset+i; - if (pinManager.isPinOk(pins[i]) || bus->isVirtual()) sappend('v',lp,pins[i]); + if (PinManager::isPinOk(pins[i]) || bus->isVirtual()) sappend('v',lp,pins[i]); } sappend('v',lc,bus->getLength()); sappend('v',lt,bus->getType()); @@ -694,7 +694,7 @@ void getSettingsJS(byte subPage, char* dest) { appendGPIOinfo(); oappend(SET_F("numM=")); - oappendi(usermods.getModCount()); + oappendi(UsermodManager::getModCount()); oappend(";"); sappend('v',SET_F("SDA"),i2c_sda); sappend('v',SET_F("SCL"),i2c_scl); @@ -706,7 +706,7 @@ void getSettingsJS(byte subPage, char* dest) oappend(SET_F("addInfo('MOSI','")); oappendi(HW_PIN_DATASPI); oappend(SET_F("');")); oappend(SET_F("addInfo('MISO','")); oappendi(HW_PIN_MISOSPI); oappend(SET_F("');")); oappend(SET_F("addInfo('SCLK','")); oappendi(HW_PIN_CLOCKSPI); oappend(SET_F("');")); - usermods.appendConfigData(); + UsermodManager::appendConfigData(); } if (subPage == SUBPAGE_UPDATE) // update From 9cb3531e2d3cd12dbdfc0669dfdbd1f5a58e75b6 Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Sat, 21 Sep 2024 22:24:36 +0200 Subject: [PATCH 087/145] Remove erroneous file Fix constant dependancy --- usermods/audioreactive/audio_reactive.old.h | 2071 ------------------- wled00/FX_fcn.cpp | 3 +- 2 files changed, 1 insertion(+), 2073 deletions(-) delete mode 100644 usermods/audioreactive/audio_reactive.old.h diff --git a/usermods/audioreactive/audio_reactive.old.h b/usermods/audioreactive/audio_reactive.old.h deleted file mode 100644 index 4f2e04c089..0000000000 --- a/usermods/audioreactive/audio_reactive.old.h +++ /dev/null @@ -1,2071 +0,0 @@ -#pragma once - -#include "wled.h" - -#ifdef ARDUINO_ARCH_ESP32 - -#include -#include - -#ifdef WLED_ENABLE_DMX - #error This audio reactive usermod is not compatible with DMX Out. -#endif - -#endif - -#if defined(ARDUINO_ARCH_ESP32) && (defined(WLED_DEBUG) || defined(SR_DEBUG)) -#include -#endif - -/* - * Usermods allow you to add own functionality to WLED more easily - * See: https://github.com/Aircoookie/WLED/wiki/Add-own-functionality - * - * This is an audioreactive v2 usermod. - * .... - */ - -#if !defined(FFTTASK_PRIORITY) -#define FFTTASK_PRIORITY 1 // standard: looptask prio -//#define FFTTASK_PRIORITY 2 // above looptask, below asyc_tcp -//#define FFTTASK_PRIORITY 4 // above asyc_tcp -#endif - -// Comment/Uncomment to toggle usb serial debugging -// #define MIC_LOGGER // MIC sampling & sound input debugging (serial plotter) -// #define FFT_SAMPLING_LOG // FFT result debugging -// #define SR_DEBUG // generic SR DEBUG messages - -#ifdef SR_DEBUG - #define DEBUGSR_PRINT(x) DEBUGOUT.print(x) - #define DEBUGSR_PRINTLN(x) DEBUGOUT.println(x) - #define DEBUGSR_PRINTF(x...) DEBUGOUT.printf(x) -#else - #define DEBUGSR_PRINT(x) - #define DEBUGSR_PRINTLN(x) - #define DEBUGSR_PRINTF(x...) -#endif - -#if defined(MIC_LOGGER) || defined(FFT_SAMPLING_LOG) - #define PLOT_PRINT(x) DEBUGOUT.print(x) - #define PLOT_PRINTLN(x) DEBUGOUT.println(x) - #define PLOT_PRINTF(x...) DEBUGOUT.printf(x) -#else - #define PLOT_PRINT(x) - #define PLOT_PRINTLN(x) - #define PLOT_PRINTF(x...) -#endif - -#define MAX_PALETTES 3 - -static volatile bool disableSoundProcessing = false; // if true, sound processing (FFT, filters, AGC) will be suspended. "volatile" as its shared between tasks. -static uint8_t audioSyncEnabled = 0; // bit field: bit 0 - send, bit 1 - receive (config value) -static bool udpSyncConnected = false; // UDP connection status -> true if connected to multicast group - -#define NUM_GEQ_CHANNELS 16 // number of frequency channels. Don't change !! - -// audioreactive variables -#ifdef ARDUINO_ARCH_ESP32 -static float micDataReal = 0.0f; // MicIn data with full 24bit resolution - lowest 8bit after decimal point -static float multAgc = 1.0f; // sample * multAgc = sampleAgc. Our AGC multiplier -static float sampleAvg = 0.0f; // Smoothed Average sample - sampleAvg < 1 means "quiet" (simple noise gate) -static float sampleAgc = 0.0f; // Smoothed AGC sample -static uint8_t soundAgc = 0; // Automagic gain control: 0 - none, 1 - normal, 2 - vivid, 3 - lazy (config value) -#endif -//static float volumeSmth = 0.0f; // either sampleAvg or sampleAgc depending on soundAgc; smoothed sample -static float FFT_MajorPeak = 1.0f; // FFT: strongest (peak) frequency -static float FFT_Magnitude = 0.0f; // FFT: volume (magnitude) of peak frequency -static bool samplePeak = false; // Boolean flag for peak - used in effects. Responding routine may reset this flag. Auto-reset after WS2812FX::getMinShowDelay() -static bool udpSamplePeak = false; // Boolean flag for peak. Set at the same time as samplePeak, but reset by transmitAudioData -static unsigned long timeOfPeak = 0; // time of last sample peak detection. -static uint8_t fftResult[NUM_GEQ_CHANNELS]= {0};// Our calculated freq. channel result table to be used by effects - -// TODO: probably best not used by receive nodes -//static float agcSensitivity = 128; // AGC sensitivity estimation, based on agc gain (multAgc). calculated by getSensitivity(). range 0..255 - -// user settable parameters for limitSoundDynamics() -#ifdef UM_AUDIOREACTIVE_DYNAMICS_LIMITER_OFF -static bool limiterOn = false; // bool: enable / disable dynamics limiter -#else -static bool limiterOn = true; -#endif -static uint16_t attackTime = 80; // int: attack time in milliseconds. Default 0.08sec -static uint16_t decayTime = 1400; // int: decay time in milliseconds. Default 1.40sec - -// peak detection -#ifdef ARDUINO_ARCH_ESP32 -static void detectSamplePeak(void); // peak detection function (needs scaled FFT results in vReal[]) - no used for 8266 receive-only mode -#endif -static void autoResetPeak(void); // peak auto-reset function -static uint8_t maxVol = 31; // (was 10) Reasonable value for constant volume for 'peak detector', as it won't always trigger (deprecated) -static uint8_t binNum = 8; // Used to select the bin for FFT based beat detection (deprecated) - -#ifdef ARDUINO_ARCH_ESP32 - -// use audio source class (ESP32 specific) -#include "audio_source.h" -constexpr i2s_port_t I2S_PORT = I2S_NUM_0; // I2S port to use (do not change !) -constexpr int BLOCK_SIZE = 128; // I2S buffer size (samples) - -// globals -static uint8_t inputLevel = 128; // UI slider value -#ifndef SR_SQUELCH - uint8_t soundSquelch = 10; // squelch value for volume reactive routines (config value) -#else - uint8_t soundSquelch = SR_SQUELCH; // squelch value for volume reactive routines (config value) -#endif -#ifndef SR_GAIN - uint8_t sampleGain = 60; // sample gain (config value) -#else - uint8_t sampleGain = SR_GAIN; // sample gain (config value) -#endif -// user settable options for FFTResult scaling -static uint8_t FFTScalingMode = 3; // 0 none; 1 optimized logarithmic; 2 optimized linear; 3 optimized square root - -// -// AGC presets -// Note: in C++, "const" implies "static" - no need to explicitly declare everything as "static const" -// -#define AGC_NUM_PRESETS 3 // AGC presets: normal, vivid, lazy -const double agcSampleDecay[AGC_NUM_PRESETS] = { 0.9994f, 0.9985f, 0.9997f}; // decay factor for sampleMax, in case the current sample is below sampleMax -const float agcZoneLow[AGC_NUM_PRESETS] = { 32, 28, 36}; // low volume emergency zone -const float agcZoneHigh[AGC_NUM_PRESETS] = { 240, 240, 248}; // high volume emergency zone -const float agcZoneStop[AGC_NUM_PRESETS] = { 336, 448, 304}; // disable AGC integrator if we get above this level -const float agcTarget0[AGC_NUM_PRESETS] = { 112, 144, 164}; // first AGC setPoint -> between 40% and 65% -const float agcTarget0Up[AGC_NUM_PRESETS] = { 88, 64, 116}; // setpoint switching value (a poor man's bang-bang) -const float agcTarget1[AGC_NUM_PRESETS] = { 220, 224, 216}; // second AGC setPoint -> around 85% -const double agcFollowFast[AGC_NUM_PRESETS] = { 1/192.f, 1/128.f, 1/256.f}; // quickly follow setpoint - ~0.15 sec -const double agcFollowSlow[AGC_NUM_PRESETS] = {1/6144.f,1/4096.f,1/8192.f}; // slowly follow setpoint - ~2-15 secs -const double agcControlKp[AGC_NUM_PRESETS] = { 0.6f, 1.5f, 0.65f}; // AGC - PI control, proportional gain parameter -const double agcControlKi[AGC_NUM_PRESETS] = { 1.7f, 1.85f, 1.2f}; // AGC - PI control, integral gain parameter -const float agcSampleSmooth[AGC_NUM_PRESETS] = { 1/12.f, 1/6.f, 1/16.f}; // smoothing factor for sampleAgc (use rawSampleAgc if you want the non-smoothed value) -// AGC presets end - -static AudioSource *audioSource = nullptr; -static bool useBandPassFilter = false; // if true, enables a bandpass filter 80Hz-16Khz to remove noise. Applies before FFT. - -//////////////////// -// Begin FFT Code // -//////////////////// - -// some prototypes, to ensure consistent interfaces -static float mapf(float x, float in_min, float in_max, float out_min, float out_max); // map function for float -static float fftAddAvg(int from, int to); // average of several FFT result bins -static void FFTcode(void * parameter); // audio processing task: read samples, run FFT, fill GEQ channels from FFT results -static void runMicFilter(uint16_t numSamples, float *sampleBuffer); // pre-filtering of raw samples (band-pass) -static void postProcessFFTResults(bool noiseGateOpen, int numberOfChannels); // post-processing and post-amp of GEQ channels - -static TaskHandle_t FFT_Task = nullptr; - -// Table of multiplication factors so that we can even out the frequency response. -static float fftResultPink[NUM_GEQ_CHANNELS] = { 1.70f, 1.71f, 1.73f, 1.78f, 1.68f, 1.56f, 1.55f, 1.63f, 1.79f, 1.62f, 1.80f, 2.06f, 2.47f, 3.35f, 6.83f, 9.55f }; - -// globals and FFT Output variables shared with animations -#if defined(WLED_DEBUG) || defined(SR_DEBUG) -static uint64_t fftTime = 0; -static uint64_t sampleTime = 0; -#endif - -// FFT Task variables (filtering and post-processing) -static float fftCalc[NUM_GEQ_CHANNELS] = {0.0f}; // Try and normalize fftBin values to a max of 4096, so that 4096/16 = 256. -static float fftAvg[NUM_GEQ_CHANNELS] = {0.0f}; // Calculated frequency channel results, with smoothing (used if dynamics limiter is ON) -#ifdef SR_DEBUG -static float fftResultMax[NUM_GEQ_CHANNELS] = {0.0f}; // A table used for testing to determine how our post-processing is working. -#endif - -// audio source parameters and constant -#ifdef ARDUINO_ARCH_ESP32C3 -constexpr SRate_t SAMPLE_RATE = 16000; // 16kHz - use if FFTtask takes more than 20ms. Physical sample time -> 32ms -#define FFT_MIN_CYCLE 30 // Use with 16Khz sampling -#else -constexpr SRate_t SAMPLE_RATE = 22050; // Base sample rate in Hz - 22Khz is a standard rate. Physical sample time -> 23ms -//constexpr SRate_t SAMPLE_RATE = 16000; // 16kHz - use if FFTtask takes more than 20ms. Physical sample time -> 32ms -//constexpr SRate_t SAMPLE_RATE = 20480; // Base sample rate in Hz - 20Khz is experimental. Physical sample time -> 25ms -//constexpr SRate_t SAMPLE_RATE = 10240; // Base sample rate in Hz - previous default. Physical sample time -> 50ms -#define FFT_MIN_CYCLE 21 // minimum time before FFT task is repeated. Use with 22Khz sampling -//#define FFT_MIN_CYCLE 30 // Use with 16Khz sampling -//#define FFT_MIN_CYCLE 23 // minimum time before FFT task is repeated. Use with 20Khz sampling -//#define FFT_MIN_CYCLE 46 // minimum time before FFT task is repeated. Use with 10Khz sampling -#endif - -// FFT Constants -constexpr uint16_t samplesFFT = 512; // Samples in an FFT batch - This value MUST ALWAYS be a power of 2 -constexpr uint16_t samplesFFT_2 = 256; // meaningfull part of FFT results - only the "lower half" contains useful information. -// the following are observed values, supported by a bit of "educated guessing" -//#define FFT_DOWNSCALE 0.65f // 20kHz - downscaling factor for FFT results - "Flat-Top" window @20Khz, old freq channels -#define FFT_DOWNSCALE 0.46f // downscaling factor for FFT results - for "Flat-Top" window @22Khz, new freq channels -#define LOG_256 5.54517744f // log(256) - -// These are the input and output vectors. Input vectors receive computed results from FFT. -static float vReal[samplesFFT] = {0.0f}; // FFT sample inputs / freq output - these are our raw result bins -static float vImag[samplesFFT] = {0.0f}; // imaginary parts - -// Create FFT object -// lib_deps += https://github.com/kosme/arduinoFFT#develop @ 2.0.1 -// these options actually cause slow-downs on all esp32 processors, don't use them. -// #define FFT_SPEED_OVER_PRECISION // enables use of reciprocals (1/x etc) - not faster on ESP32 -// #define FFT_SQRT_APPROXIMATION // enables "quake3" style inverse sqrt - slower on ESP32 -// Below options are forcing ArduinoFFT to use sqrtf() instead of sqrt() -#define sqrt(x) sqrtf(x) // little hack that reduces FFT time by 10-50% on ESP32 -#define sqrt_internal sqrtf // see https://github.com/kosme/arduinoFFT/pull/83 - -#include - -/* Create FFT object with weighing factor storage */ -static ArduinoFFT FFT = ArduinoFFT( vReal, vImag, samplesFFT, SAMPLE_RATE, true); - -// Helper functions - -// float version of map() -static float mapf(float x, float in_min, float in_max, float out_min, float out_max){ - return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; -} - -// compute average of several FFT result bins -static float fftAddAvg(int from, int to) { - float result = 0.0f; - for (int i = from; i <= to; i++) { - result += vReal[i]; - } - return result / float(to - from + 1); -} - -// -// FFT main task -// -void FFTcode(void * parameter) -{ - DEBUGSR_PRINT("FFT started on core: "); DEBUGSR_PRINTLN(xPortGetCoreID()); - - // see https://www.freertos.org/vtaskdelayuntil.html - const TickType_t xFrequency = FFT_MIN_CYCLE * portTICK_PERIOD_MS; - - TickType_t xLastWakeTime = xTaskGetTickCount(); - for(;;) { - delay(1); // DO NOT DELETE THIS LINE! It is needed to give the IDLE(0) task enough time and to keep the watchdog happy. - // taskYIELD(), yield(), vTaskDelay() and esp_task_wdt_feed() didn't seem to work. - - // Don't run FFT computing code if we're in Receive mode or in realtime mode - if (disableSoundProcessing || (audioSyncEnabled & 0x02)) { - vTaskDelayUntil( &xLastWakeTime, xFrequency); // release CPU, and let I2S fill its buffers - continue; - } - -#if defined(WLED_DEBUG) || defined(SR_DEBUG) - uint64_t start = esp_timer_get_time(); - bool haveDoneFFT = false; // indicates if second measurement (FFT time) is valid -#endif - - // get a fresh batch of samples from I2S - if (audioSource) audioSource->getSamples(vReal, samplesFFT); - -#if defined(WLED_DEBUG) || defined(SR_DEBUG) - if (start < esp_timer_get_time()) { // filter out overflows - uint64_t sampleTimeInMillis = (esp_timer_get_time() - start +5ULL) / 10ULL; // "+5" to ensure proper rounding - sampleTime = (sampleTimeInMillis*3 + sampleTime*7)/10; // smooth - } - start = esp_timer_get_time(); // start measuring FFT time -#endif - - xLastWakeTime = xTaskGetTickCount(); // update "last unblocked time" for vTaskDelay - - // band pass filter - can reduce noise floor by a factor of 50 - // downside: frequencies below 100Hz will be ignored - if (useBandPassFilter) runMicFilter(samplesFFT, vReal); - - // find highest sample in the batch - float maxSample = 0.0f; // max sample from FFT batch - for (int i=0; i < samplesFFT; i++) { - // set imaginary parts to 0 - vImag[i] = 0; - // pick our our current mic sample - we take the max value from all samples that go into FFT - if ((vReal[i] <= (INT16_MAX - 1024)) && (vReal[i] >= (INT16_MIN + 1024))) //skip extreme values - normally these are artefacts - if (fabsf((float)vReal[i]) > maxSample) maxSample = fabsf((float)vReal[i]); - } - // release highest sample to volume reactive effects early - not strictly necessary here - could also be done at the end of the function - // early release allows the filters (getSample() and agcAvg()) to work with fresh values - we will have matching gain and noise gate values when we want to process the FFT results. - micDataReal = maxSample; - -#ifdef SR_DEBUG - if (true) { // this allows measure FFT runtimes, as it disables the "only when needed" optimization -#else - if (sampleAvg > 0.25f) { // noise gate open means that FFT results will be used. Don't run FFT if results are not needed. -#endif - - // run FFT (takes 3-5ms on ESP32, ~12ms on ESP32-S2) - FFT.dcRemoval(); // remove DC offset - FFT.windowing( FFTWindow::Flat_top, FFTDirection::Forward); // Weigh data using "Flat Top" function - better amplitude accuracy - //FFT.windowing(FFTWindow::Blackman_Harris, FFTDirection::Forward); // Weigh data using "Blackman- Harris" window - sharp peaks due to excellent sideband rejection - FFT.compute( FFTDirection::Forward ); // Compute FFT - FFT.complexToMagnitude(); // Compute magnitudes - vReal[0] = 0.0f; // The remaining DC offset on the signal produces a strong spike on position 0 that should be eliminated to avoid issues. - - FFT.majorPeak(&FFT_MajorPeak, &FFT_Magnitude); // let the effects know which freq was most dominant - FFT_MajorPeak = constrain(FFT_MajorPeak, 1.0f, 11025.0f); // restrict value to range expected by effects - -#if defined(WLED_DEBUG) || defined(SR_DEBUG) - haveDoneFFT = true; -#endif - - } else { // noise gate closed - only clear results as FFT was skipped. MIC samples are still valid when we do this. - memset(vReal, 0, sizeof(vReal)); - FFT_MajorPeak = 1.0f; - FFT_Magnitude = 0.001f; - } - - for (int i = 0; i < samplesFFT; i++) { - float t = fabsf(vReal[i]); // just to be sure - values in fft bins should be positive any way - vReal[i] = t / 16.0f; // Reduce magnitude. Want end result to be scaled linear and ~4096 max. - } // for() - - // mapping of FFT result bins to frequency channels - if (fabsf(sampleAvg) > 0.5f) { // noise gate open -#if 0 - /* This FFT post processing is a DIY endeavour. What we really need is someone with sound engineering expertise to do a great job here AND most importantly, that the animations look GREAT as a result. - * - * Andrew's updated mapping of 256 bins down to the 16 result bins with Sample Freq = 10240, samplesFFT = 512 and some overlap. - * Based on testing, the lowest/Start frequency is 60 Hz (with bin 3) and a highest/End frequency of 5120 Hz in bin 255. - * Now, Take the 60Hz and multiply by 1.320367784 to get the next frequency and so on until the end. Then determine the bins. - * End frequency = Start frequency * multiplier ^ 16 - * Multiplier = (End frequency/ Start frequency) ^ 1/16 - * Multiplier = 1.320367784 - */ // Range - fftCalc[ 0] = fftAddAvg(2,4); // 60 - 100 - fftCalc[ 1] = fftAddAvg(4,5); // 80 - 120 - fftCalc[ 2] = fftAddAvg(5,7); // 100 - 160 - fftCalc[ 3] = fftAddAvg(7,9); // 140 - 200 - fftCalc[ 4] = fftAddAvg(9,12); // 180 - 260 - fftCalc[ 5] = fftAddAvg(12,16); // 240 - 340 - fftCalc[ 6] = fftAddAvg(16,21); // 320 - 440 - fftCalc[ 7] = fftAddAvg(21,29); // 420 - 600 - fftCalc[ 8] = fftAddAvg(29,37); // 580 - 760 - fftCalc[ 9] = fftAddAvg(37,48); // 740 - 980 - fftCalc[10] = fftAddAvg(48,64); // 960 - 1300 - fftCalc[11] = fftAddAvg(64,84); // 1280 - 1700 - fftCalc[12] = fftAddAvg(84,111); // 1680 - 2240 - fftCalc[13] = fftAddAvg(111,147); // 2220 - 2960 - fftCalc[14] = fftAddAvg(147,194); // 2940 - 3900 - fftCalc[15] = fftAddAvg(194,250); // 3880 - 5000 // avoid the last 5 bins, which are usually inaccurate -#else - /* new mapping, optimized for 22050 Hz by softhack007 */ - // bins frequency range - if (useBandPassFilter) { - // skip frequencies below 100hz - fftCalc[ 0] = 0.8f * fftAddAvg(3,4); - fftCalc[ 1] = 0.9f * fftAddAvg(4,5); - fftCalc[ 2] = fftAddAvg(5,6); - fftCalc[ 3] = fftAddAvg(6,7); - // don't use the last bins from 206 to 255. - fftCalc[15] = fftAddAvg(165,205) * 0.75f; // 40 7106 - 8828 high -- with some damping - } else { - fftCalc[ 0] = fftAddAvg(1,2); // 1 43 - 86 sub-bass - fftCalc[ 1] = fftAddAvg(2,3); // 1 86 - 129 bass - fftCalc[ 2] = fftAddAvg(3,5); // 2 129 - 216 bass - fftCalc[ 3] = fftAddAvg(5,7); // 2 216 - 301 bass + midrange - // don't use the last bins from 216 to 255. They are usually contaminated by aliasing (aka noise) - fftCalc[15] = fftAddAvg(165,215) * 0.70f; // 50 7106 - 9259 high -- with some damping - } - fftCalc[ 4] = fftAddAvg(7,10); // 3 301 - 430 midrange - fftCalc[ 5] = fftAddAvg(10,13); // 3 430 - 560 midrange - fftCalc[ 6] = fftAddAvg(13,19); // 5 560 - 818 midrange - fftCalc[ 7] = fftAddAvg(19,26); // 7 818 - 1120 midrange -- 1Khz should always be the center ! - fftCalc[ 8] = fftAddAvg(26,33); // 7 1120 - 1421 midrange - fftCalc[ 9] = fftAddAvg(33,44); // 9 1421 - 1895 midrange - fftCalc[10] = fftAddAvg(44,56); // 12 1895 - 2412 midrange + high mid - fftCalc[11] = fftAddAvg(56,70); // 14 2412 - 3015 high mid - fftCalc[12] = fftAddAvg(70,86); // 16 3015 - 3704 high mid - fftCalc[13] = fftAddAvg(86,104); // 18 3704 - 4479 high mid - fftCalc[14] = fftAddAvg(104,165) * 0.88f; // 61 4479 - 7106 high mid + high -- with slight damping -#endif - } else { // noise gate closed - just decay old values - for (int i=0; i < NUM_GEQ_CHANNELS; i++) { - fftCalc[i] *= 0.85f; // decay to zero - if (fftCalc[i] < 4.0f) fftCalc[i] = 0.0f; - } - } - - // post-processing of frequency channels (pink noise adjustment, AGC, smoothing, scaling) - postProcessFFTResults((fabsf(sampleAvg) > 0.25f)? true : false , NUM_GEQ_CHANNELS); - -#if defined(WLED_DEBUG) || defined(SR_DEBUG) - if (haveDoneFFT && (start < esp_timer_get_time())) { // filter out overflows - uint64_t fftTimeInMillis = ((esp_timer_get_time() - start) +5ULL) / 10ULL; // "+5" to ensure proper rounding - fftTime = (fftTimeInMillis*3 + fftTime*7)/10; // smooth - } -#endif - // run peak detection - autoResetPeak(); - detectSamplePeak(); - - #if !defined(I2S_GRAB_ADC1_COMPLETELY) - if ((audioSource == nullptr) || (audioSource->getType() != AudioSource::Type_I2SAdc)) // the "delay trick" does not help for analog ADC - #endif - vTaskDelayUntil( &xLastWakeTime, xFrequency); // release CPU, and let I2S fill its buffers - - } // for(;;)ever -} // FFTcode() task end - - -/////////////////////////// -// Pre / Postprocessing // -/////////////////////////// - -static void runMicFilter(uint16_t numSamples, float *sampleBuffer) // pre-filtering of raw samples (band-pass) -{ - // low frequency cutoff parameter - see https://dsp.stackexchange.com/questions/40462/exponential-moving-average-cut-off-frequency - //constexpr float alpha = 0.04f; // 150Hz - //constexpr float alpha = 0.03f; // 110Hz - constexpr float alpha = 0.0225f; // 80hz - //constexpr float alpha = 0.01693f;// 60hz - // high frequency cutoff parameter - //constexpr float beta1 = 0.75f; // 11Khz - //constexpr float beta1 = 0.82f; // 15Khz - //constexpr float beta1 = 0.8285f; // 18Khz - constexpr float beta1 = 0.85f; // 20Khz - - constexpr float beta2 = (1.0f - beta1) / 2.0f; - static float last_vals[2] = { 0.0f }; // FIR high freq cutoff filter - static float lowfilt = 0.0f; // IIR low frequency cutoff filter - - for (int i=0; i < numSamples; i++) { - // FIR lowpass, to remove high frequency noise - float highFilteredSample; - if (i < (numSamples-1)) highFilteredSample = beta1*sampleBuffer[i] + beta2*last_vals[0] + beta2*sampleBuffer[i+1]; // smooth out spikes - else highFilteredSample = beta1*sampleBuffer[i] + beta2*last_vals[0] + beta2*last_vals[1]; // special handling for last sample in array - last_vals[1] = last_vals[0]; - last_vals[0] = sampleBuffer[i]; - sampleBuffer[i] = highFilteredSample; - // IIR highpass, to remove low frequency noise - lowfilt += alpha * (sampleBuffer[i] - lowfilt); - sampleBuffer[i] = sampleBuffer[i] - lowfilt; - } -} - -static void postProcessFFTResults(bool noiseGateOpen, int numberOfChannels) // post-processing and post-amp of GEQ channels -{ - for (int i=0; i < numberOfChannels; i++) { - - if (noiseGateOpen) { // noise gate open - // Adjustment for frequency curves. - fftCalc[i] *= fftResultPink[i]; - if (FFTScalingMode > 0) fftCalc[i] *= FFT_DOWNSCALE; // adjustment related to FFT windowing function - // Manual linear adjustment of gain using sampleGain adjustment for different input types. - fftCalc[i] *= soundAgc ? multAgc : ((float)sampleGain/40.0f * (float)inputLevel/128.0f + 1.0f/16.0f); //apply gain, with inputLevel adjustment - if(fftCalc[i] < 0) fftCalc[i] = 0.0f; - } - - // smooth results - rise fast, fall slower - if (fftCalc[i] > fftAvg[i]) fftAvg[i] = fftCalc[i]*0.75f + 0.25f*fftAvg[i]; // rise fast; will need approx 2 cycles (50ms) for converging against fftCalc[i] - else { // fall slow - if (decayTime < 1000) fftAvg[i] = fftCalc[i]*0.22f + 0.78f*fftAvg[i]; // approx 5 cycles (225ms) for falling to zero - else if (decayTime < 2000) fftAvg[i] = fftCalc[i]*0.17f + 0.83f*fftAvg[i]; // default - approx 9 cycles (225ms) for falling to zero - else if (decayTime < 3000) fftAvg[i] = fftCalc[i]*0.14f + 0.86f*fftAvg[i]; // approx 14 cycles (350ms) for falling to zero - else fftAvg[i] = fftCalc[i]*0.1f + 0.9f*fftAvg[i]; // approx 20 cycles (500ms) for falling to zero - } - // constrain internal vars - just to be sure - fftCalc[i] = constrain(fftCalc[i], 0.0f, 1023.0f); - fftAvg[i] = constrain(fftAvg[i], 0.0f, 1023.0f); - - float currentResult; - if(limiterOn == true) - currentResult = fftAvg[i]; - else - currentResult = fftCalc[i]; - - switch (FFTScalingMode) { - case 1: - // Logarithmic scaling - currentResult *= 0.42f; // 42 is the answer ;-) - currentResult -= 8.0f; // this skips the lowest row, giving some room for peaks - if (currentResult > 1.0f) currentResult = logf(currentResult); // log to base "e", which is the fastest log() function - else currentResult = 0.0f; // special handling, because log(1) = 0; log(0) = undefined - currentResult *= 0.85f + (float(i)/18.0f); // extra up-scaling for high frequencies - currentResult = mapf(currentResult, 0.0f, LOG_256, 0.0f, 255.0f); // map [log(1) ... log(255)] to [0 ... 255] - break; - case 2: - // Linear scaling - currentResult *= 0.30f; // needs a bit more damping, get stay below 255 - currentResult -= 4.0f; // giving a bit more room for peaks (WLEDMM uses -2) - if (currentResult < 1.0f) currentResult = 0.0f; - currentResult *= 0.85f + (float(i)/1.8f); // extra up-scaling for high frequencies - break; - case 3: - // square root scaling - currentResult *= 0.38f; - currentResult -= 6.0f; - if (currentResult > 1.0f) currentResult = sqrtf(currentResult); - else currentResult = 0.0f; // special handling, because sqrt(0) = undefined - currentResult *= 0.85f + (float(i)/4.5f); // extra up-scaling for high frequencies - currentResult = mapf(currentResult, 0.0f, 16.0f, 0.0f, 255.0f); // map [sqrt(1) ... sqrt(256)] to [0 ... 255] - break; - - case 0: - default: - // no scaling - leave freq bins as-is - currentResult -= 4; // just a bit more room for peaks (WLEDMM uses -2) - break; - } - - // Now, let's dump it all into fftResult. Need to do this, otherwise other routines might grab fftResult values prematurely. - if (soundAgc > 0) { // apply extra "GEQ Gain" if set by user - float post_gain = (float)inputLevel/128.0f; - if (post_gain < 1.0f) post_gain = ((post_gain -1.0f) * 0.8f) +1.0f; - currentResult *= post_gain; - } - fftResult[i] = constrain((int)currentResult, 0, 255); - } -} -//////////////////// -// Peak detection // -//////////////////// - -// peak detection is called from FFT task when vReal[] contains valid FFT results -static void detectSamplePeak(void) { - bool havePeak = false; - // softhack007: this code continuously triggers while amplitude in the selected bin is above a certain threshold. So it does not detect peaks - it detects high activity in a frequency bin. - // Poor man's beat detection by seeing if sample > Average + some value. - // This goes through ALL of the 255 bins - but ignores stupid settings - // Then we got a peak, else we don't. The peak has to time out on its own in order to support UDP sound sync. - if ((sampleAvg > 1) && (maxVol > 0) && (binNum > 4) && (vReal[binNum] > maxVol) && ((millis() - timeOfPeak) > 100)) { - havePeak = true; - } - - if (havePeak) { - samplePeak = true; - timeOfPeak = millis(); - udpSamplePeak = true; - } -} - -#endif - -static void autoResetPeak(void) { - uint16_t MinShowDelay = MAX(50, WS2812FX::getMinShowDelay()); // Fixes private class variable compiler error. Unsure if this is the correct way of fixing the root problem. -THATDONFC - if (millis() - timeOfPeak > MinShowDelay) { // Auto-reset of samplePeak after a complete frame has passed. - samplePeak = false; - if (audioSyncEnabled == 0) udpSamplePeak = false; // this is normally reset by transmitAudioData - } -} - - -//////////////////// -// usermod class // -//////////////////// - -//class name. Use something descriptive and leave the ": public Usermod" part :) -class AudioReactive : public Usermod { - - private: -#ifdef ARDUINO_ARCH_ESP32 - - #ifndef AUDIOPIN - int8_t audioPin = -1; - #else - int8_t audioPin = AUDIOPIN; - #endif - #ifndef SR_DMTYPE // I2S mic type - uint8_t dmType = 1; // 0=none/disabled/analog; 1=generic I2S - #define SR_DMTYPE 1 // default type = I2S - #else - uint8_t dmType = SR_DMTYPE; - #endif - #ifndef I2S_SDPIN // aka DOUT - int8_t i2ssdPin = 32; - #else - int8_t i2ssdPin = I2S_SDPIN; - #endif - #ifndef I2S_WSPIN // aka LRCL - int8_t i2swsPin = 15; - #else - int8_t i2swsPin = I2S_WSPIN; - #endif - #ifndef I2S_CKPIN // aka BCLK - int8_t i2sckPin = 14; /*PDM: set to I2S_PIN_NO_CHANGE*/ - #else - int8_t i2sckPin = I2S_CKPIN; - #endif - #ifndef MCLK_PIN - int8_t mclkPin = I2S_PIN_NO_CHANGE; /* ESP32: only -1, 0, 1, 3 allowed*/ - #else - int8_t mclkPin = MCLK_PIN; - #endif -#endif - - // new "V2" audiosync struct - 44 Bytes - struct __attribute__ ((packed)) audioSyncPacket { // "packed" ensures that there are no additional gaps - char header[6]; // 06 Bytes offset 0 - uint8_t reserved1[2]; // 02 Bytes, offset 6 - gap required by the compiler - not used yet - float sampleRaw; // 04 Bytes offset 8 - either "sampleRaw" or "rawSampleAgc" depending on soundAgc setting - float sampleSmth; // 04 Bytes offset 12 - either "sampleAvg" or "sampleAgc" depending on soundAgc setting - uint8_t samplePeak; // 01 Bytes offset 16 - 0 no peak; >=1 peak detected. In future, this will also provide peak Magnitude - uint8_t reserved2; // 01 Bytes offset 17 - for future extensions - not used yet - uint8_t fftResult[16]; // 16 Bytes offset 18 - uint16_t reserved3; // 02 Bytes, offset 34 - gap required by the compiler - not used yet - float FFT_Magnitude; // 04 Bytes offset 36 - float FFT_MajorPeak; // 04 Bytes offset 40 - }; - - // old "V1" audiosync struct - 83 Bytes payload, 88 bytes total (with padding added by compiler) - for backwards compatibility - struct audioSyncPacket_v1 { - char header[6]; // 06 Bytes - uint8_t myVals[32]; // 32 Bytes - int sampleAgc; // 04 Bytes - int sampleRaw; // 04 Bytes - float sampleAvg; // 04 Bytes - bool samplePeak; // 01 Bytes - uint8_t fftResult[16]; // 16 Bytes - double FFT_Magnitude; // 08 Bytes - double FFT_MajorPeak; // 08 Bytes - }; - - constexpr static unsigned UDPSOUND_MAX_PACKET = MAX(sizeof(audioSyncPacket), sizeof(audioSyncPacket_v1)); - - // set your config variables to their boot default value (this can also be done in readFromConfig() or a constructor if you prefer) - #ifdef UM_AUDIOREACTIVE_ENABLE - bool enabled = true; - #else - bool enabled = false; - #endif - - bool initDone = false; - bool addPalettes = false; - int8_t palettes = 0; - - // variables for UDP sound sync - WiFiUDP fftUdp; // UDP object for sound sync (from WiFi UDP, not Async UDP!) - unsigned long lastTime = 0; // last time of running UDP Microphone Sync - const uint16_t delayMs = 10; // I don't want to sample too often and overload WLED - uint16_t audioSyncPort= 11988;// default port for UDP sound sync - - bool updateIsRunning = false; // true during OTA. - -#ifdef ARDUINO_ARCH_ESP32 - // used for AGC - int last_soundAgc = -1; // used to detect AGC mode change (for resetting AGC internal error buffers) - float control_integrated = 0.0f; // persistent across calls to agcAvg(); "integrator control" = accumulated error - // variables used by getSample() and agcAvg() - int16_t micIn = 0; // Current sample starts with negative values and large values, which is why it's 16 bit signed - float sampleMax = 0.0f; // Max sample over a few seconds. Needed for AGC controller. - float micLev = 0.0f; // Used to convert returned value to have '0' as minimum. A leveller - float expAdjF = 0.0f; // Used for exponential filter. - float sampleReal = 0.0f; // "sampleRaw" as float, to provide bits that are lost otherwise (before amplification by sampleGain or inputLevel). Needed for AGC. - int16_t sampleRaw = 0; // Current sample. Must only be updated ONCE!!! (amplified mic value by sampleGain and inputLevel) - int16_t rawSampleAgc = 0; // not smoothed AGC sample -#endif - - // variables used in effects - float volumeSmth = 0.0f; // either sampleAvg or sampleAgc depending on soundAgc; smoothed sample - int16_t volumeRaw = 0; // either sampleRaw or rawSampleAgc depending on soundAgc - float my_magnitude =0.0f; // FFT_Magnitude, scaled by multAgc - - // used to feed "Info" Page - unsigned long last_UDPTime = 0; // time of last valid UDP sound sync datapacket - int receivedFormat = 0; // last received UDP sound sync format - 0=none, 1=v1 (0.13.x), 2=v2 (0.14.x) - float maxSample5sec = 0.0f; // max sample (after AGC) in last 5 seconds - unsigned long sampleMaxTimer = 0; // last time maxSample5sec was reset - #define CYCLE_SAMPLEMAX 3500 // time window for merasuring - - // strings to reduce flash memory usage (used more than twice) - static const char _name[]; - static const char _enabled[]; - static const char _config[]; - static const char _dynamics[]; - static const char _frequency[]; - static const char _inputLvl[]; -#if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) - static const char _analogmic[]; -#endif - static const char _digitalmic[]; - static const char _addPalettes[]; - static const char UDP_SYNC_HEADER[]; - static const char UDP_SYNC_HEADER_v1[]; - - // private methods - void removeAudioPalettes(void); - void createAudioPalettes(void); - CRGB getCRGBForBand(int x, int pal); - void fillAudioPalettes(void); - - //////////////////// - // Debug support // - //////////////////// - void logAudio() - { - if (disableSoundProcessing && (!udpSyncConnected || ((audioSyncEnabled & 0x02) == 0))) return; // no audio availeable - #ifdef MIC_LOGGER - // Debugging functions for audio input and sound processing. Comment out the values you want to see - PLOT_PRINT("micReal:"); PLOT_PRINT(micDataReal); PLOT_PRINT("\t"); - PLOT_PRINT("volumeSmth:"); PLOT_PRINT(volumeSmth); PLOT_PRINT("\t"); - //PLOT_PRINT("volumeRaw:"); PLOT_PRINT(volumeRaw); PLOT_PRINT("\t"); - PLOT_PRINT("DC_Level:"); PLOT_PRINT(micLev); PLOT_PRINT("\t"); - //PLOT_PRINT("sampleAgc:"); PLOT_PRINT(sampleAgc); PLOT_PRINT("\t"); - //PLOT_PRINT("sampleAvg:"); PLOT_PRINT(sampleAvg); PLOT_PRINT("\t"); - //PLOT_PRINT("sampleReal:"); PLOT_PRINT(sampleReal); PLOT_PRINT("\t"); - #ifdef ARDUINO_ARCH_ESP32 - //PLOT_PRINT("micIn:"); PLOT_PRINT(micIn); PLOT_PRINT("\t"); - //PLOT_PRINT("sample:"); PLOT_PRINT(sample); PLOT_PRINT("\t"); - //PLOT_PRINT("sampleMax:"); PLOT_PRINT(sampleMax); PLOT_PRINT("\t"); - //PLOT_PRINT("samplePeak:"); PLOT_PRINT((samplePeak!=0) ? 128:0); PLOT_PRINT("\t"); - //PLOT_PRINT("multAgc:"); PLOT_PRINT(multAgc, 4); PLOT_PRINT("\t"); - #endif - PLOT_PRINTLN(); - #endif - - #ifdef FFT_SAMPLING_LOG - #if 0 - for(int i=0; i maxVal) maxVal = fftResult[i]; - if(fftResult[i] < minVal) minVal = fftResult[i]; - } - for(int i = 0; i < NUM_GEQ_CHANNELS; i++) { - PLOT_PRINT(i); PLOT_PRINT(":"); - PLOT_PRINTF("%04ld ", map(fftResult[i], 0, (scaleValuesFromCurrentMaxVal ? maxVal : defaultScalingFromHighValue), (mapValuesToPlotterSpace*i*scalingToHighValue)+0, (mapValuesToPlotterSpace*i*scalingToHighValue)+scalingToHighValue-1)); - } - if(printMaxVal) { - PLOT_PRINTF("maxVal:%04d ", maxVal + (mapValuesToPlotterSpace ? 16*256 : 0)); - } - if(printMinVal) { - PLOT_PRINTF("%04d:minVal ", minVal); // printed with value first, then label, so negative values can be seen in Serial Monitor but don't throw off y axis in Serial Plotter - } - if(mapValuesToPlotterSpace) - PLOT_PRINTF("max:%04d ", (printMaxVal ? 17 : 16)*256); // print line above the maximum value we expect to see on the plotter to avoid autoscaling y axis - else { - PLOT_PRINTF("max:%04d ", 256); - } - PLOT_PRINTLN(); - #endif // FFT_SAMPLING_LOG - } // logAudio() - - -#ifdef ARDUINO_ARCH_ESP32 - ////////////////////// - // Audio Processing // - ////////////////////// - - /* - * A "PI controller" multiplier to automatically adjust sound sensitivity. - * - * A few tricks are implemented so that sampleAgc does't only utilize 0% and 100%: - * 0. don't amplify anything below squelch (but keep previous gain) - * 1. gain input = maximum signal observed in the last 5-10 seconds - * 2. we use two setpoints, one at ~60%, and one at ~80% of the maximum signal - * 3. the amplification depends on signal level: - * a) normal zone - very slow adjustment - * b) emergency zone (<10% or >90%) - very fast adjustment - */ - void agcAvg(unsigned long the_time) - { - const int AGC_preset = (soundAgc > 0)? (soundAgc-1): 0; // make sure the _compiler_ knows this value will not change while we are inside the function - - float lastMultAgc = multAgc; // last multiplier used - float multAgcTemp = multAgc; // new multiplier - float tmpAgc = sampleReal * multAgc; // what-if amplified signal - - float control_error; // "control error" input for PI control - - if (last_soundAgc != soundAgc) control_integrated = 0.0f; // new preset - reset integrator - - // For PI controller, we need to have a constant "frequency" - // so let's make sure that the control loop is not running at insane speed - static unsigned long last_time = 0; - unsigned long time_now = millis(); - if ((the_time > 0) && (the_time < time_now)) time_now = the_time; // allow caller to override my clock - - if (time_now - last_time > 2) { - last_time = time_now; - - if ((fabsf(sampleReal) < 2.0f) || (sampleMax < 1.0f)) { - // MIC signal is "squelched" - deliver silence - tmpAgc = 0; - // we need to "spin down" the intgrated error buffer - if (fabs(control_integrated) < 0.01f) control_integrated = 0.0f; - else control_integrated *= 0.91f; - } else { - // compute new setpoint - if (tmpAgc <= agcTarget0Up[AGC_preset]) - multAgcTemp = agcTarget0[AGC_preset] / sampleMax; // Make the multiplier so that sampleMax * multiplier = first setpoint - else - multAgcTemp = agcTarget1[AGC_preset] / sampleMax; // Make the multiplier so that sampleMax * multiplier = second setpoint - } - // limit amplification - if (multAgcTemp > 32.0f) multAgcTemp = 32.0f; - if (multAgcTemp < 1.0f/64.0f) multAgcTemp = 1.0f/64.0f; - - // compute error terms - control_error = multAgcTemp - lastMultAgc; - - if (((multAgcTemp > 0.085f) && (multAgcTemp < 6.5f)) //integrator anti-windup by clamping - && (multAgc*sampleMax < agcZoneStop[AGC_preset])) //integrator ceiling (>140% of max) - control_integrated += control_error * 0.002f * 0.25f; // 2ms = integration time; 0.25 for damping - else - control_integrated *= 0.9f; // spin down that beasty integrator - - // apply PI Control - tmpAgc = sampleReal * lastMultAgc; // check "zone" of the signal using previous gain - if ((tmpAgc > agcZoneHigh[AGC_preset]) || (tmpAgc < soundSquelch + agcZoneLow[AGC_preset])) { // upper/lower energy zone - multAgcTemp = lastMultAgc + agcFollowFast[AGC_preset] * agcControlKp[AGC_preset] * control_error; - multAgcTemp += agcFollowFast[AGC_preset] * agcControlKi[AGC_preset] * control_integrated; - } else { // "normal zone" - multAgcTemp = lastMultAgc + agcFollowSlow[AGC_preset] * agcControlKp[AGC_preset] * control_error; - multAgcTemp += agcFollowSlow[AGC_preset] * agcControlKi[AGC_preset] * control_integrated; - } - - // limit amplification again - PI controller sometimes "overshoots" - //multAgcTemp = constrain(multAgcTemp, 0.015625f, 32.0f); // 1/64 < multAgcTemp < 32 - if (multAgcTemp > 32.0f) multAgcTemp = 32.0f; - if (multAgcTemp < 1.0f/64.0f) multAgcTemp = 1.0f/64.0f; - } - - // NOW finally amplify the signal - tmpAgc = sampleReal * multAgcTemp; // apply gain to signal - if (fabsf(sampleReal) < 2.0f) tmpAgc = 0.0f; // apply squelch threshold - //tmpAgc = constrain(tmpAgc, 0, 255); - if (tmpAgc > 255) tmpAgc = 255.0f; // limit to 8bit - if (tmpAgc < 1) tmpAgc = 0.0f; // just to be sure - - // update global vars ONCE - multAgc, sampleAGC, rawSampleAgc - multAgc = multAgcTemp; - rawSampleAgc = 0.8f * tmpAgc + 0.2f * (float)rawSampleAgc; - // update smoothed AGC sample - if (fabsf(tmpAgc) < 1.0f) - sampleAgc = 0.5f * tmpAgc + 0.5f * sampleAgc; // fast path to zero - else - sampleAgc += agcSampleSmooth[AGC_preset] * (tmpAgc - sampleAgc); // smooth path - - sampleAgc = fabsf(sampleAgc); // // make sure we have a positive value - last_soundAgc = soundAgc; - } // agcAvg() - - // post-processing and filtering of MIC sample (micDataReal) from FFTcode() - void getSample() - { - float sampleAdj; // Gain adjusted sample value - float tmpSample; // An interim sample variable used for calculations. - const float weighting = 0.2f; // Exponential filter weighting. Will be adjustable in a future release. - const int AGC_preset = (soundAgc > 0)? (soundAgc-1): 0; // make sure the _compiler_ knows this value will not change while we are inside the function - - #ifdef WLED_DISABLE_SOUND - micIn = inoise8(millis(), millis()); // Simulated analog read - micDataReal = micIn; - #else - #ifdef ARDUINO_ARCH_ESP32 - micIn = int(micDataReal); // micDataSm = ((micData * 3) + micData)/4; - #else - // this is the minimal code for reading analog mic input on 8266. - // warning!! Absolutely experimental code. Audio on 8266 is still not working. Expects a million follow-on problems. - static unsigned long lastAnalogTime = 0; - static float lastAnalogValue = 0.0f; - if (millis() - lastAnalogTime > 20) { - micDataReal = analogRead(A0); // read one sample with 10bit resolution. This is a dirty hack, supporting volumereactive effects only. - lastAnalogTime = millis(); - lastAnalogValue = micDataReal; - yield(); - } else micDataReal = lastAnalogValue; - micIn = int(micDataReal); - #endif - #endif - - micLev += (micDataReal-micLev) / 12288.0f; - if (micIn < micLev) micLev = ((micLev * 31.0f) + micDataReal) / 32.0f; // align micLev to lowest input signal - - micIn -= micLev; // Let's center it to 0 now - // Using an exponential filter to smooth out the signal. We'll add controls for this in a future release. - float micInNoDC = fabsf(micDataReal - micLev); - expAdjF = (weighting * micInNoDC + (1.0f-weighting) * expAdjF); - expAdjF = fabsf(expAdjF); // Now (!) take the absolute value - - expAdjF = (expAdjF <= soundSquelch) ? 0.0f : expAdjF; // simple noise gate - if ((soundSquelch == 0) && (expAdjF < 0.25f)) expAdjF = 0.0f; // do something meaningfull when "squelch = 0" - - tmpSample = expAdjF; - micIn = abs(micIn); // And get the absolute value of each sample - - sampleAdj = tmpSample * sampleGain * inputLevel / 5120.0f /* /40 /128 */ + tmpSample / 16.0f; // Adjust the gain. with inputLevel adjustment - sampleReal = tmpSample; - - sampleAdj = fmax(fmin(sampleAdj, 255.0f), 0.0f); // Question: why are we limiting the value to 8 bits ??? - sampleRaw = (int16_t)sampleAdj; // ONLY update sample ONCE!!!! - - // keep "peak" sample, but decay value if current sample is below peak - if ((sampleMax < sampleReal) && (sampleReal > 0.5f)) { - sampleMax += 0.5f * (sampleReal - sampleMax); // new peak - with some filtering - // another simple way to detect samplePeak - cannot detect beats, but reacts on peak volume - if (((binNum < 12) || ((maxVol < 1))) && (millis() - timeOfPeak > 80) && (sampleAvg > 1)) { - samplePeak = true; - timeOfPeak = millis(); - udpSamplePeak = true; - } - } else { - if ((multAgc*sampleMax > agcZoneStop[AGC_preset]) && (soundAgc > 0)) - sampleMax += 0.5f * (sampleReal - sampleMax); // over AGC Zone - get back quickly - else - sampleMax *= agcSampleDecay[AGC_preset]; // signal to zero --> 5-8sec - } - if (sampleMax < 0.5f) sampleMax = 0.0f; - - sampleAvg = ((sampleAvg * 15.0f) + sampleAdj) / 16.0f; // Smooth it out over the last 16 samples. - sampleAvg = fabsf(sampleAvg); // make sure we have a positive value - } // getSample() - -#endif - - /* Limits the dynamics of volumeSmth (= sampleAvg or sampleAgc). - * does not affect FFTResult[] or volumeRaw ( = sample or rawSampleAgc) - */ - // effects: Gravimeter, Gravcenter, Gravcentric, Noisefire, Plasmoid, Freqpixels, Freqwave, Gravfreq, (2D Swirl, 2D Waverly) - void limitSampleDynamics(void) { - const float bigChange = 196.0f; // just a representative number - a large, expected sample value - static unsigned long last_time = 0; - static float last_volumeSmth = 0.0f; - - if (limiterOn == false) return; - - long delta_time = millis() - last_time; - delta_time = constrain(delta_time , 1, 1000); // below 1ms -> 1ms; above 1sec -> sily lil hick-up - float deltaSample = volumeSmth - last_volumeSmth; - - if (attackTime > 0) { // user has defined attack time > 0 - float maxAttack = bigChange * float(delta_time) / float(attackTime); - if (deltaSample > maxAttack) deltaSample = maxAttack; - } - if (decayTime > 0) { // user has defined decay time > 0 - float maxDecay = - bigChange * float(delta_time) / float(decayTime); - if (deltaSample < maxDecay) deltaSample = maxDecay; - } - - volumeSmth = last_volumeSmth + deltaSample; - - last_volumeSmth = volumeSmth; - last_time = millis(); - } - - - ////////////////////// - // UDP Sound Sync // - ////////////////////// - - // try to establish UDP sound sync connection - void connectUDPSoundSync(void) { - // This function tries to establish a UDP sync connection if needed - // necessary as we also want to transmit in "AP Mode", but the standard "connected()" callback only reacts on STA connection - static unsigned long last_connection_attempt = 0; - - if ((audioSyncPort <= 0) || ((audioSyncEnabled & 0x03) == 0)) return; // Sound Sync not enabled - if (udpSyncConnected) return; // already connected - if (!(apActive || interfacesInited)) return; // neither AP nor other connections availeable - if (millis() - last_connection_attempt < 15000) return; // only try once in 15 seconds - if (updateIsRunning) return; - - // if we arrive here, we need a UDP connection but don't have one - last_connection_attempt = millis(); - connected(); // try to start UDP - } - -#ifdef ARDUINO_ARCH_ESP32 - void transmitAudioData() - { - //DEBUGSR_PRINTLN("Transmitting UDP Mic Packet"); - - audioSyncPacket transmitData; - memset(reinterpret_cast(&transmitData), 0, sizeof(transmitData)); // make sure that the packet - including "invisible" padding bytes added by the compiler - is fully initialized - - strncpy_P(transmitData.header, PSTR(UDP_SYNC_HEADER), 6); - // transmit samples that were not modified by limitSampleDynamics() - transmitData.sampleRaw = (soundAgc) ? rawSampleAgc: sampleRaw; - transmitData.sampleSmth = (soundAgc) ? sampleAgc : sampleAvg; - transmitData.samplePeak = udpSamplePeak ? 1:0; - udpSamplePeak = false; // Reset udpSamplePeak after we've transmitted it - - for (int i = 0; i < NUM_GEQ_CHANNELS; i++) { - transmitData.fftResult[i] = (uint8_t)constrain(fftResult[i], 0, 254); - } - - transmitData.FFT_Magnitude = my_magnitude; - transmitData.FFT_MajorPeak = FFT_MajorPeak; - -#ifndef WLED_DISABLE_ESPNOW - if (useESPNowSync && statusESPNow == ESP_NOW_STATE_ON) { - EspNowPartialPacket buffer = {{'W','L','E','D'}, 0, 1, {0}}; - //DEBUGSR_PRINTLN(F("ESP-NOW Sending audio packet.")); - size_t packetSize = sizeof(EspNowPartialPacket) - sizeof(EspNowPartialPacket::data) + sizeof(transmitData); - memcpy(buffer.data, &transmitData, sizeof(transmitData)); - quickEspNow.send(ESPNOW_BROADCAST_ADDRESS, reinterpret_cast(&buffer), packetSize); - } -#endif - - if (udpSyncConnected && fftUdp.beginMulticastPacket() != 0) { // beginMulticastPacket returns 0 in case of error - fftUdp.write(reinterpret_cast(&transmitData), sizeof(transmitData)); - fftUdp.endPacket(); - } - return; - } // transmitAudioData() - -#endif - - static inline bool isValidUdpSyncVersion(const char *header) { - return strncmp_P(header, UDP_SYNC_HEADER, 6) == 0; - } - static inline bool isValidUdpSyncVersion_v1(const char *header) { - return strncmp_P(header, UDP_SYNC_HEADER_v1, 6) == 0; - } - - void decodeAudioData(int packetSize, uint8_t *fftBuff) { - audioSyncPacket receivedPacket; - memset(&receivedPacket, 0, sizeof(receivedPacket)); // start clean - memcpy(&receivedPacket, fftBuff, min((unsigned)packetSize, (unsigned)sizeof(receivedPacket))); // don't violate alignment - thanks @willmmiles# - - // update samples for effects - volumeSmth = fmaxf(receivedPacket.sampleSmth, 0.0f); - volumeRaw = fmaxf(receivedPacket.sampleRaw, 0.0f); -#ifdef ARDUINO_ARCH_ESP32 - // update internal samples - sampleRaw = volumeRaw; - sampleAvg = volumeSmth; - rawSampleAgc = volumeRaw; - sampleAgc = volumeSmth; - multAgc = 1.0f; -#endif - // Only change samplePeak IF it's currently false. - // If it's true already, then the animation still needs to respond. - autoResetPeak(); - if (!samplePeak) { - samplePeak = receivedPacket.samplePeak > 0; - if (samplePeak) timeOfPeak = millis(); - } - //These values are only computed by ESP32 - for (int i = 0; i < NUM_GEQ_CHANNELS; i++) fftResult[i] = receivedPacket.fftResult[i]; - my_magnitude = fmaxf(receivedPacket.FFT_Magnitude, 0.0f); - FFT_Magnitude = my_magnitude; - FFT_MajorPeak = constrain(receivedPacket.FFT_MajorPeak, 1.0f, 11025.0f); // restrict value to range expected by effects - } - - void decodeAudioData_v1(int packetSize, uint8_t *fftBuff) { - audioSyncPacket_v1 *receivedPacket = reinterpret_cast(fftBuff); - // update samples for effects - volumeSmth = fmaxf(receivedPacket->sampleAgc, 0.0f); - volumeRaw = volumeSmth; // V1 format does not have "raw" AGC sample -#ifdef ARDUINO_ARCH_ESP32 - // update internal samples - sampleRaw = fmaxf(receivedPacket->sampleRaw, 0.0f); - sampleAvg = fmaxf(receivedPacket->sampleAvg, 0.0f);; - sampleAgc = volumeSmth; - rawSampleAgc = volumeRaw; - multAgc = 1.0f; -#endif - // Only change samplePeak IF it's currently false. - // If it's true already, then the animation still needs to respond. - autoResetPeak(); - if (!samplePeak) { - samplePeak = receivedPacket->samplePeak > 0; - if (samplePeak) timeOfPeak = millis(); - } - //These values are only available on the ESP32 - for (int i = 0; i < NUM_GEQ_CHANNELS; i++) fftResult[i] = receivedPacket->fftResult[i]; - my_magnitude = fmaxf(receivedPacket->FFT_Magnitude, 0.0f); - FFT_Magnitude = my_magnitude; - FFT_MajorPeak = constrain(receivedPacket->FFT_MajorPeak, 1.0f, 11025.0f); // restrict value to range expected by effects - } - - bool receiveAudioData() // check & process new data. return TRUE in case that new audio data was received. - { - if (!udpSyncConnected) return false; - bool haveFreshData = false; - - size_t packetSize = fftUdp.parsePacket(); -#ifdef ARDUINO_ARCH_ESP32 - if ((packetSize > 0) && ((packetSize < 5) || (packetSize > UDPSOUND_MAX_PACKET))) fftUdp.flush(); // discard invalid packets (too small or too big) - only works on esp32 -#endif - if ((packetSize > 5) && (packetSize <= UDPSOUND_MAX_PACKET)) { - //DEBUGSR_PRINTLN("Received UDP Sync Packet"); - uint8_t fftBuff[UDPSOUND_MAX_PACKET+1] = { 0 }; // fixed-size buffer for receiving (stack), to avoid heap fragmentation caused by variable sized arrays - fftUdp.read(fftBuff, packetSize); - - // VERIFY THAT THIS IS A COMPATIBLE PACKET - if (packetSize == sizeof(audioSyncPacket) && (isValidUdpSyncVersion((const char *)fftBuff))) { - decodeAudioData(packetSize, fftBuff); - //DEBUGSR_PRINTLN("Finished parsing UDP Sync Packet v2"); - haveFreshData = true; - receivedFormat = 2; - } else { - if (packetSize == sizeof(audioSyncPacket_v1) && (isValidUdpSyncVersion_v1((const char *)fftBuff))) { - decodeAudioData_v1(packetSize, fftBuff); - //DEBUGSR_PRINTLN("Finished parsing UDP Sync Packet v1"); - haveFreshData = true; - receivedFormat = 1; - } else receivedFormat = 0; // unknown format - } - } - return haveFreshData; - } - - - ////////////////////// - // usermod functions// - ////////////////////// - - public: - //Functions called by WLED or other usermods - - /* - * setup() is called once at boot. WiFi is not yet connected at this point. - * You can use it to initialize variables, sensors or similar. - * It is called *AFTER* readFromConfig() - */ - void setup() override - { - disableSoundProcessing = true; // just to be sure - if (!initDone) { - // usermod exchangeable data - // we will assign all usermod exportable data here as pointers to original variables or arrays and allocate memory for pointers - um_data = new um_data_t; - um_data->u_size = 8; - um_data->u_type = new um_types_t[um_data->u_size]; - um_data->u_data = new void*[um_data->u_size]; - um_data->u_data[0] = &volumeSmth; //*used (New) - um_data->u_type[0] = UMT_FLOAT; - um_data->u_data[1] = &volumeRaw; // used (New) - um_data->u_type[1] = UMT_UINT16; - um_data->u_data[2] = fftResult; //*used (Blurz, DJ Light, Noisemove, GEQ_base, 2D Funky Plank, Akemi) - um_data->u_type[2] = UMT_BYTE_ARR; - um_data->u_data[3] = &samplePeak; //*used (Puddlepeak, Ripplepeak, Waterfall) - um_data->u_type[3] = UMT_BYTE; - um_data->u_data[4] = &FFT_MajorPeak; //*used (Ripplepeak, Freqmap, Freqmatrix, Freqpixels, Freqwave, Gravfreq, Rocktaves, Waterfall) - um_data->u_type[4] = UMT_FLOAT; - um_data->u_data[5] = &my_magnitude; // used (New) - um_data->u_type[5] = UMT_FLOAT; - um_data->u_data[6] = &maxVol; // assigned in effect function from UI element!!! (Puddlepeak, Ripplepeak, Waterfall) - um_data->u_type[6] = UMT_BYTE; - um_data->u_data[7] = &binNum; // assigned in effect function from UI element!!! (Puddlepeak, Ripplepeak, Waterfall) - um_data->u_type[7] = UMT_BYTE; - } - - -#ifdef ARDUINO_ARCH_ESP32 - - // Reset I2S peripheral for good measure - i2s_driver_uninstall(I2S_NUM_0); // E (696) I2S: i2s_driver_uninstall(2006): I2S port 0 has not installed - #if !defined(CONFIG_IDF_TARGET_ESP32C3) - delay(100); - periph_module_reset(PERIPH_I2S0_MODULE); // not possible on -C3 - #endif - delay(100); // Give that poor microphone some time to setup. - - useBandPassFilter = false; - - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) - if ((i2sckPin == I2S_PIN_NO_CHANGE) && (i2ssdPin >= 0) && (i2swsPin >= 0) && ((dmType == 1) || (dmType == 4)) ) dmType = 5; // dummy user support: SCK == -1 --means--> PDM microphone - #endif - - switch (dmType) { - #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3) - // stub cases for not-yet-supported I2S modes on other ESP32 chips - case 0: //ADC analog - #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) - case 5: //PDM Microphone - #endif - #endif - case 1: - DEBUGSR_PRINT(F("AR: Generic I2S Microphone - ")); DEBUGSR_PRINTLN(F(I2S_MIC_CHANNEL_TEXT)); - audioSource = new I2SSource(SAMPLE_RATE, BLOCK_SIZE); - delay(100); - if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin); - break; - case 2: - DEBUGSR_PRINTLN(F("AR: ES7243 Microphone (right channel only).")); - audioSource = new ES7243(SAMPLE_RATE, BLOCK_SIZE); - delay(100); - if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); - break; - case 3: - DEBUGSR_PRINT(F("AR: SPH0645 Microphone - ")); DEBUGSR_PRINTLN(F(I2S_MIC_CHANNEL_TEXT)); - audioSource = new SPH0654(SAMPLE_RATE, BLOCK_SIZE); - delay(100); - audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin); - break; - case 4: - DEBUGSR_PRINT(F("AR: Generic I2S Microphone with Master Clock - ")); DEBUGSR_PRINTLN(F(I2S_MIC_CHANNEL_TEXT)); - audioSource = new I2SSource(SAMPLE_RATE, BLOCK_SIZE, 1.0f/24.0f); - delay(100); - if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); - break; - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) - case 5: - DEBUGSR_PRINT(F("AR: I2S PDM Microphone - ")); DEBUGSR_PRINTLN(F(I2S_PDM_MIC_CHANNEL_TEXT)); - audioSource = new I2SSource(SAMPLE_RATE, BLOCK_SIZE, 1.0f/4.0f); - useBandPassFilter = true; // this reduces the noise floor on SPM1423 from 5% Vpp (~380) down to 0.05% Vpp (~5) - delay(100); - if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin); - break; - #endif - case 6: - DEBUGSR_PRINTLN(F("AR: ES8388 Source")); - audioSource = new ES8388Source(SAMPLE_RATE, BLOCK_SIZE); - delay(100); - if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); - break; - - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) - // ADC over I2S is only possible on "classic" ESP32 - case 0: - default: - DEBUGSR_PRINTLN(F("AR: Analog Microphone (left channel only).")); - audioSource = new I2SAdcSource(SAMPLE_RATE, BLOCK_SIZE); - delay(100); - useBandPassFilter = true; // PDM bandpass filter seems to help for bad quality analog - if (audioSource) audioSource->initialize(audioPin); - break; - #endif - } - delay(250); // give microphone enough time to initialise - - if (!audioSource) enabled = false; // audio failed to initialise -#endif - if (enabled) onUpdateBegin(false); // create FFT task, and initialize network - if (enabled) disableSoundProcessing = false; // all good - enable audio processing -#ifdef ARDUINO_ARCH_ESP32 - if (FFT_Task == nullptr) enabled = false; // FFT task creation failed - if ((!audioSource) || (!audioSource->isInitialized())) { - // audio source failed to initialize. Still stay "enabled", as there might be input arriving via UDP Sound Sync - #ifdef WLED_DEBUG - DEBUG_PRINTLN(F("AR: Failed to initialize sound input driver. Please check input PIN settings.")); - #else - DEBUGSR_PRINTLN(F("AR: Failed to initialize sound input driver. Please check input PIN settings.")); - #endif - disableSoundProcessing = true; - } -#endif - if (enabled) connectUDPSoundSync(); - if (enabled && addPalettes) createAudioPalettes(); - initDone = true; - } - - - /* - * connected() is called every time the WiFi is (re)connected - * Use it to initialize network interfaces - */ - void connected() override - { - if (udpSyncConnected) { // clean-up: if open, close old UDP sync connection - udpSyncConnected = false; - fftUdp.stop(); - } - - if (audioSyncPort > 0 && (audioSyncEnabled & 0x03)) { - #ifdef ARDUINO_ARCH_ESP32 - udpSyncConnected = fftUdp.beginMulticast(IPAddress(239, 0, 0, 1), audioSyncPort); - #else - udpSyncConnected = fftUdp.beginMulticast(WiFi.localIP(), IPAddress(239, 0, 0, 1), audioSyncPort); - #endif - } - } - - - /* - * loop() is called continuously. Here you can check for events, read sensors, etc. - * - * Tips: - * 1. You can use "if (WLED_CONNECTED)" to check for a successful network connection. - * Additionally, "if (WLED_MQTT_CONNECTED)" is available to check for a connection to an MQTT broker. - * - * 2. Try to avoid using the delay() function. NEVER use delays longer than 10 milliseconds. - * Instead, use a timer check as shown here. - */ - void loop() override - { - static unsigned long lastUMRun = millis(); - - if (!enabled) { - disableSoundProcessing = true; // keep processing suspended (FFT task) - lastUMRun = millis(); // update time keeping - return; - } - // We cannot wait indefinitely before processing audio data - if (WS2812FX::isUpdating() && (millis() - lastUMRun < 2)) return; // be nice, but not too nice - - // suspend local sound processing when "real time mode" is active (E131, UDP, ADALIGHT, ARTNET) - if ( (realtimeOverride == REALTIME_OVERRIDE_NONE) // please add other overrides here if needed - &&( (realtimeMode == REALTIME_MODE_GENERIC) - ||(realtimeMode == REALTIME_MODE_E131) - ||(realtimeMode == REALTIME_MODE_UDP) - ||(realtimeMode == REALTIME_MODE_ADALIGHT) - ||(realtimeMode == REALTIME_MODE_ARTNET) ) ) // please add other modes here if needed - { - #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_DEBUG) - if ((disableSoundProcessing == false) && (audioSyncEnabled == 0)) { // we just switched to "disabled" - DEBUG_PRINTLN(F("[AR userLoop] realtime mode active - audio processing suspended.")); - DEBUG_PRINTF_P(PSTR(" RealtimeMode = %d; RealtimeOverride = %d\n"), int(realtimeMode), int(realtimeOverride)); - } - #endif - disableSoundProcessing = true; - } else { - #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_DEBUG) - if ((disableSoundProcessing == true) && (audioSyncEnabled == 0) && audioSource->isInitialized()) { // we just switched to "enabled" - DEBUG_PRINTLN(F("[AR userLoop] realtime mode ended - audio processing resumed.")); - DEBUG_PRINTF_P(PSTR(" RealtimeMode = %d; RealtimeOverride = %d\n"), int(realtimeMode), int(realtimeOverride)); - } - #endif - if ((disableSoundProcessing == true) && (audioSyncEnabled == 0)) lastUMRun = millis(); // just left "realtime mode" - update timekeeping - disableSoundProcessing = false; - } - - if (audioSyncEnabled & 0x02) disableSoundProcessing = true; // make sure everything is disabled IF in audio Receive mode - if (audioSyncEnabled & 0x01) disableSoundProcessing = false; // keep running audio IF we're in audio Transmit mode -#ifdef ARDUINO_ARCH_ESP32 - if (!audioSource->isInitialized()) disableSoundProcessing = true; // no audio source - - - // Only run the sampling code IF we're not in Receive mode or realtime mode - if (!(audioSyncEnabled & 0x02) && !disableSoundProcessing) { - if (soundAgc > AGC_NUM_PRESETS) soundAgc = 0; // make sure that AGC preset is valid (to avoid array bounds violation) - - unsigned long t_now = millis(); // remember current time - int userloopDelay = int(t_now - lastUMRun); - if (lastUMRun == 0) userloopDelay=0; // startup - don't have valid data from last run. - - #ifdef WLED_DEBUG - // complain when audio userloop has been delayed for long time. Currently we need userloop running between 500 and 1500 times per second. - // softhack007 disabled temporarily - avoid serial console spam with MANY leds and low FPS - //if ((userloopDelay > 65) && !disableSoundProcessing && (audioSyncEnabled == 0)) { - // DEBUG_PRINTF_P(PSTR("[AR userLoop] hiccup detected -> was inactive for last %d millis!\n"), userloopDelay); - //} - #endif - - // run filters, and repeat in case of loop delays (hick-up compensation) - if (userloopDelay <2) userloopDelay = 0; // minor glitch, no problem - if (userloopDelay >200) userloopDelay = 200; // limit number of filter re-runs - do { - getSample(); // run microphone sampling filters - agcAvg(t_now - userloopDelay); // Calculated the PI adjusted value as sampleAvg - userloopDelay -= 2; // advance "simulated time" by 2ms - } while (userloopDelay > 0); - lastUMRun = t_now; // update time keeping - - // update samples for effects (raw, smooth) - volumeSmth = (soundAgc) ? sampleAgc : sampleAvg; - volumeRaw = (soundAgc) ? rawSampleAgc: sampleRaw; - // update FFTMagnitude, taking into account AGC amplification - my_magnitude = FFT_Magnitude; // / 16.0f, 8.0f, 4.0f done in effects - if (soundAgc) my_magnitude *= multAgc; - if (volumeSmth < 1 ) my_magnitude = 0.001f; // noise gate closed - mute - - limitSampleDynamics(); - } // if (!disableSoundProcessing) -#endif - - autoResetPeak(); // auto-reset sample peak after strip minShowDelay - if (!udpSyncConnected) udpSamplePeak = false; // reset UDP samplePeak while UDP is unconnected - - connectUDPSoundSync(); // ensure we have a connection - if needed - - // UDP Microphone Sync - receive mode - if ((audioSyncEnabled & 0x02) && udpSyncConnected) { - // Only run the audio listener code if we're in Receive mode - static float syncVolumeSmth = 0; - bool have_new_sample = false; - if (millis() - lastTime > delayMs) { - have_new_sample = receiveAudioData(); - if (have_new_sample) last_UDPTime = millis(); -#ifdef ARDUINO_ARCH_ESP32 - else fftUdp.flush(); // Flush udp input buffers if we haven't read it - avoids hickups in receive mode. Does not work on 8266. -#endif - lastTime = millis(); - } - if (have_new_sample) syncVolumeSmth = volumeSmth; // remember received sample - else volumeSmth = syncVolumeSmth; // restore originally received sample for next run of dynamics limiter - limitSampleDynamics(); // run dynamics limiter on received volumeSmth, to hide jumps and hickups - } - - #if defined(MIC_LOGGER) || defined(MIC_SAMPLING_LOG) || defined(FFT_SAMPLING_LOG) - static unsigned long lastMicLoggerTime = 0; - if (millis()-lastMicLoggerTime > 20) { - lastMicLoggerTime = millis(); - logAudio(); - } - #endif - - // Info Page: keep max sample from last 5 seconds -#ifdef ARDUINO_ARCH_ESP32 - if ((millis() - sampleMaxTimer) > CYCLE_SAMPLEMAX) { - sampleMaxTimer = millis(); - maxSample5sec = (0.15f * maxSample5sec) + 0.85f *((soundAgc) ? sampleAgc : sampleAvg); // reset, and start with some smoothing - if (sampleAvg < 1) maxSample5sec = 0; // noise gate - } else { - if ((sampleAvg >= 1)) maxSample5sec = fmaxf(maxSample5sec, (soundAgc) ? rawSampleAgc : sampleRaw); // follow maximum volume - } -#else // similar functionality for 8266 receive only - use VolumeSmth instead of raw sample data - if ((millis() - sampleMaxTimer) > CYCLE_SAMPLEMAX) { - sampleMaxTimer = millis(); - maxSample5sec = (0.15 * maxSample5sec) + 0.85 * volumeSmth; // reset, and start with some smoothing - if (volumeSmth < 1.0f) maxSample5sec = 0; // noise gate - if (maxSample5sec < 0.0f) maxSample5sec = 0; // avoid negative values - } else { - if (volumeSmth >= 1.0f) maxSample5sec = fmaxf(maxSample5sec, volumeRaw); // follow maximum volume - } -#endif - -#ifdef ARDUINO_ARCH_ESP32 - //UDP Microphone Sync - transmit mode - if ((audioSyncEnabled & 0x01) && (millis() - lastTime > 20)) { - // Only run the transmit code IF we're in Transmit mode - transmitAudioData(); - lastTime = millis(); - } -#endif - - fillAudioPalettes(); - } - - - bool getUMData(um_data_t **data) override - { - if (!data || !enabled) return false; // no pointer provided by caller or not enabled -> exit - *data = um_data; - return true; - } - -#ifdef ARDUINO_ARCH_ESP32 - void onUpdateBegin(bool init) override - { -#ifdef WLED_DEBUG - fftTime = sampleTime = 0; -#endif - // gracefully suspend FFT task (if running) - disableSoundProcessing = true; - - // reset sound data - micDataReal = 0.0f; - volumeRaw = 0; volumeSmth = 0.0f; - sampleAgc = 0.0f; sampleAvg = 0.0f; - sampleRaw = 0; rawSampleAgc = 0.0f; - my_magnitude = 0.0f; FFT_Magnitude = 0.0f; FFT_MajorPeak = 1.0f; - multAgc = 1.0f; - // reset FFT data - memset(fftCalc, 0, sizeof(fftCalc)); - memset(fftAvg, 0, sizeof(fftAvg)); - memset(fftResult, 0, sizeof(fftResult)); - for(int i=(init?0:1); i don't process audio - updateIsRunning = init; - } -#endif - -#ifdef ARDUINO_ARCH_ESP32 - /** - * handleButton() can be used to override default button behaviour. Returning true - * will prevent button working in a default way. - */ - bool handleButton(uint8_t b) override { - yield(); - // crude way of determining if audio input is analog - // better would be for AudioSource to implement getType() - if (enabled - && dmType == 0 && audioPin>=0 - && (buttonType[b] == BTN_TYPE_ANALOG || buttonType[b] == BTN_TYPE_ANALOG_INVERTED) - ) { - return true; - } - return false; - } - -#endif - //////////////////////////// - // Settings and Info Page // - //////////////////////////// - - /* - * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. - * Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI. - * Below it is shown how this could be used for e.g. a light sensor - */ - void addToJsonInfo(JsonObject& root) override - { -#ifdef ARDUINO_ARCH_ESP32 - char myStringBuffer[16]; // buffer for snprintf() - not used yet on 8266 -#endif - JsonObject user = root["u"]; - if (user.isNull()) user = root.createNestedObject("u"); - - JsonArray infoArr = user.createNestedArray(FPSTR(_name)); - - String uiDomString = F(""); - infoArr.add(uiDomString); - - if (enabled) { -#ifdef ARDUINO_ARCH_ESP32 - // Input Level Slider - if (disableSoundProcessing == false) { // only show slider when audio processing is running - if (soundAgc > 0) { - infoArr = user.createNestedArray(F("GEQ Input Level")); // if AGC is on, this slider only affects fftResult[] frequencies - } else { - infoArr = user.createNestedArray(F("Audio Input Level")); - } - uiDomString = F("
"); // - infoArr.add(uiDomString); - } -#endif - // The following can be used for troubleshooting user errors and is so not enclosed in #ifdef WLED_DEBUG - - // current Audio input - infoArr = user.createNestedArray(F("Audio Source")); - if (audioSyncEnabled & 0x02) { - // UDP sound sync - receive mode - infoArr.add(F("UDP sound sync")); - if (udpSyncConnected) { - if (millis() - last_UDPTime < 2500) - infoArr.add(F(" - receiving")); - else - infoArr.add(F(" - idle")); - } else { - infoArr.add(F(" - no connection")); - } -#ifndef ARDUINO_ARCH_ESP32 // substitute for 8266 - } else { - infoArr.add(F("sound sync Off")); - } -#else // ESP32 only - } else { - // Analog or I2S digital input - if (audioSource && (audioSource->isInitialized())) { - // audio source successfully configured - if (audioSource->getType() == AudioSource::Type_I2SAdc) { - infoArr.add(F("ADC analog")); - } else { - infoArr.add(F("I2S digital")); - } - // input level or "silence" - if (maxSample5sec > 1.0f) { - float my_usage = 100.0f * (maxSample5sec / 255.0f); - snprintf_P(myStringBuffer, 15, PSTR(" - peak %3d%%"), int(my_usage)); - infoArr.add(myStringBuffer); - } else { - infoArr.add(F(" - quiet")); - } - } else { - // error during audio source setup - infoArr.add(F("not initialized")); - infoArr.add(F(" - check pin settings")); - } - } - - // Sound processing (FFT and input filters) - infoArr = user.createNestedArray(F("Sound Processing")); - if (audioSource && (disableSoundProcessing == false)) { - infoArr.add(F("running")); - } else { - infoArr.add(F("suspended")); - } - - // AGC or manual Gain - if ((soundAgc==0) && (disableSoundProcessing == false) && !(audioSyncEnabled & 0x02)) { - infoArr = user.createNestedArray(F("Manual Gain")); - float myGain = ((float)sampleGain/40.0f * (float)inputLevel/128.0f) + 1.0f/16.0f; // non-AGC gain from presets - infoArr.add(roundf(myGain*100.0f) / 100.0f); - infoArr.add("x"); - } - if (soundAgc && (disableSoundProcessing == false) && !(audioSyncEnabled & 0x02)) { - infoArr = user.createNestedArray(F("AGC Gain")); - infoArr.add(roundf(multAgc*100.0f) / 100.0f); - infoArr.add("x"); - } -#endif - // UDP Sound Sync status - infoArr = user.createNestedArray(F("UDP Sound Sync")); - if (audioSyncEnabled) { - if (audioSyncEnabled & 0x01) { - infoArr.add(F("send mode")); - if ((udpSyncConnected) && (millis() - lastTime < 2500)) infoArr.add(F(" v2")); - } else if (audioSyncEnabled & 0x02) { - infoArr.add(F("receive mode")); - } - } else - infoArr.add("off"); - if (audioSyncEnabled && !udpSyncConnected) infoArr.add(" (unconnected)"); - if (audioSyncEnabled && udpSyncConnected && (millis() - last_UDPTime < 2500)) { - if (receivedFormat == 1) infoArr.add(F(" v1")); - if (receivedFormat == 2) infoArr.add(F(" v2")); - } - - #if defined(WLED_DEBUG) || defined(SR_DEBUG) - #ifdef ARDUINO_ARCH_ESP32 - infoArr = user.createNestedArray(F("Sampling time")); - infoArr.add(float(sampleTime)/100.0f); - infoArr.add(" ms"); - - infoArr = user.createNestedArray(F("FFT time")); - infoArr.add(float(fftTime)/100.0f); - if ((fftTime/100) >= FFT_MIN_CYCLE) // FFT time over budget -> I2S buffer will overflow - infoArr.add("! ms"); - else if ((fftTime/80 + sampleTime/80) >= FFT_MIN_CYCLE) // FFT time >75% of budget -> risk of instability - infoArr.add(" ms!"); - else - infoArr.add(" ms"); - - DEBUGSR_PRINTF("AR Sampling time: %5.2f ms\n", float(sampleTime)/100.0f); - DEBUGSR_PRINTF("AR FFT time : %5.2f ms\n", float(fftTime)/100.0f); - #endif - #endif - } - } - - - /* - * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). - * Values in the state object may be modified by connected clients - */ - void addToJsonState(JsonObject& root) override - { - if (!initDone) return; // prevent crash on boot applyPreset() - JsonObject usermod = root[FPSTR(_name)]; - if (usermod.isNull()) { - usermod = root.createNestedObject(FPSTR(_name)); - } - usermod["on"] = enabled; - } - - - /* - * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). - * Values in the state object may be modified by connected clients - */ - void readFromJsonState(JsonObject& root) override - { - if (!initDone) return; // prevent crash on boot applyPreset() - bool prevEnabled = enabled; - JsonObject usermod = root[FPSTR(_name)]; - if (!usermod.isNull()) { - if (usermod[FPSTR(_enabled)].is()) { - enabled = usermod[FPSTR(_enabled)].as(); - if (prevEnabled != enabled) onUpdateBegin(!enabled); - if (addPalettes) { - // add/remove custom/audioreactive palettes - if (prevEnabled && !enabled) removeAudioPalettes(); - if (!prevEnabled && enabled) createAudioPalettes(); - } - } -#ifdef ARDUINO_ARCH_ESP32 - if (usermod[FPSTR(_inputLvl)].is()) { - inputLevel = min(255,max(0,usermod[FPSTR(_inputLvl)].as())); - } -#endif - } - if (root.containsKey(F("rmcpal")) && root[F("rmcpal")].as()) { - // handle removal of custom palettes from JSON call so we don't break things - removeAudioPalettes(); - } - } - - void onStateChange(uint8_t callMode) override { - if (initDone && enabled && addPalettes && palettes==0 && WS2812FX::customPalettes.size()<10) { - // if palettes were removed during JSON call re-add them - createAudioPalettes(); - } - } - - /* - * addToConfig() can be used to add custom persistent settings to the cfg.json file in the "um" (usermod) object. - * It will be called by WLED when settings are actually saved (for example, LED settings are saved) - * If you want to force saving the current state, use serializeConfig() in your loop(). - * - * CAUTION: serializeConfig() will initiate a filesystem write operation. - * It might cause the LEDs to stutter and will cause flash wear if called too often. - * Use it sparingly and always in the loop, never in network callbacks! - * - * addToConfig() will make your settings editable through the Usermod Settings page automatically. - * - * Usermod Settings Overview: - * - Numeric values are treated as floats in the browser. - * - If the numeric value entered into the browser contains a decimal point, it will be parsed as a C float - * before being returned to the Usermod. The float data type has only 6-7 decimal digits of precision, and - * doubles are not supported, numbers will be rounded to the nearest float value when being parsed. - * The range accepted by the input field is +/- 1.175494351e-38 to +/- 3.402823466e+38. - * - If the numeric value entered into the browser doesn't contain a decimal point, it will be parsed as a - * C int32_t (range: -2147483648 to 2147483647) before being returned to the usermod. - * Overflows or underflows are truncated to the max/min value for an int32_t, and again truncated to the type - * used in the Usermod when reading the value from ArduinoJson. - * - Pin values can be treated differently from an integer value by using the key name "pin" - * - "pin" can contain a single or array of integer values - * - On the Usermod Settings page there is simple checking for pin conflicts and warnings for special pins - * - Red color indicates a conflict. Yellow color indicates a pin with a warning (e.g. an input-only pin) - * - Tip: use int8_t to store the pin value in the Usermod, so a -1 value (pin not set) can be used - * - * See usermod_v2_auto_save.h for an example that saves Flash space by reusing ArduinoJson key name strings - * - * If you need a dedicated settings page with custom layout for your Usermod, that takes a lot more work. - * You will have to add the setting to the HTML, xml.cpp and set.cpp manually. - * See the WLED Soundreactive fork (code and wiki) for reference. https://github.com/atuline/WLED - * - * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings! - */ - void addToConfig(JsonObject& root) override - { - JsonObject top = root.createNestedObject(FPSTR(_name)); - top[FPSTR(_enabled)] = enabled; - top[FPSTR(_addPalettes)] = addPalettes; - -#ifdef ARDUINO_ARCH_ESP32 - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) - JsonObject amic = top.createNestedObject(FPSTR(_analogmic)); - amic["pin"] = audioPin; - #endif - - JsonObject dmic = top.createNestedObject(FPSTR(_digitalmic)); - dmic["type"] = dmType; - JsonArray pinArray = dmic.createNestedArray("pin"); - pinArray.add(i2ssdPin); - pinArray.add(i2swsPin); - pinArray.add(i2sckPin); - pinArray.add(mclkPin); - - JsonObject cfg = top.createNestedObject(FPSTR(_config)); - cfg[F("squelch")] = soundSquelch; - cfg[F("gain")] = sampleGain; - cfg[F("AGC")] = soundAgc; - - JsonObject freqScale = top.createNestedObject(FPSTR(_frequency)); - freqScale[F("scale")] = FFTScalingMode; -#endif - - JsonObject dynLim = top.createNestedObject(FPSTR(_dynamics)); - dynLim[F("limiter")] = limiterOn; - dynLim[F("rise")] = attackTime; - dynLim[F("fall")] = decayTime; - - JsonObject sync = top.createNestedObject("sync"); - sync["port"] = audioSyncPort; - sync["mode"] = audioSyncEnabled; - } - - - /* - * readFromConfig() can be used to read back the custom settings you added with addToConfig(). - * This is called by WLED when settings are loaded (currently this only happens immediately after boot, or after saving on the Usermod Settings page) - * - * readFromConfig() is called BEFORE setup(). This means you can use your persistent values in setup() (e.g. pin assignments, buffer sizes), - * but also that if you want to write persistent values to a dynamic buffer, you'd need to allocate it here instead of in setup. - * If you don't know what that is, don't fret. It most likely doesn't affect your use case :) - * - * Return true in case the config values returned from Usermod Settings were complete, or false if you'd like WLED to save your defaults to disk (so any missing values are editable in Usermod Settings) - * - * getJsonValue() returns false if the value is missing, or copies the value into the variable provided and returns true if the value is present - * The configComplete variable is true only if the "exampleUsermod" object and all values are present. If any values are missing, WLED will know to call addToConfig() to save them - * - * This function is guaranteed to be called on boot, but could also be called every time settings are updated - */ - bool readFromConfig(JsonObject& root) override - { - JsonObject top = root[FPSTR(_name)]; - bool configComplete = !top.isNull(); - bool oldEnabled = enabled; - bool oldAddPalettes = addPalettes; - - configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled); - configComplete &= getJsonValue(top[FPSTR(_addPalettes)], addPalettes); - -#ifdef ARDUINO_ARCH_ESP32 - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) - configComplete &= getJsonValue(top[FPSTR(_analogmic)]["pin"], audioPin); - #else - audioPin = -1; // MCU does not support analog mic - #endif - - configComplete &= getJsonValue(top[FPSTR(_digitalmic)]["type"], dmType); - #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3) - if (dmType == 0) dmType = SR_DMTYPE; // MCU does not support analog - #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) - if (dmType == 5) dmType = SR_DMTYPE; // MCU does not support PDM - #endif - #endif - - configComplete &= getJsonValue(top[FPSTR(_digitalmic)]["pin"][0], i2ssdPin); - configComplete &= getJsonValue(top[FPSTR(_digitalmic)]["pin"][1], i2swsPin); - configComplete &= getJsonValue(top[FPSTR(_digitalmic)]["pin"][2], i2sckPin); - configComplete &= getJsonValue(top[FPSTR(_digitalmic)]["pin"][3], mclkPin); - - configComplete &= getJsonValue(top[FPSTR(_config)][F("squelch")], soundSquelch); - configComplete &= getJsonValue(top[FPSTR(_config)][F("gain")], sampleGain); - configComplete &= getJsonValue(top[FPSTR(_config)][F("AGC")], soundAgc); - - configComplete &= getJsonValue(top[FPSTR(_frequency)][F("scale")], FFTScalingMode); - - configComplete &= getJsonValue(top[FPSTR(_dynamics)][F("limiter")], limiterOn); - configComplete &= getJsonValue(top[FPSTR(_dynamics)][F("rise")], attackTime); - configComplete &= getJsonValue(top[FPSTR(_dynamics)][F("fall")], decayTime); -#endif - configComplete &= getJsonValue(top["sync"]["port"], audioSyncPort); - configComplete &= getJsonValue(top["sync"]["mode"], audioSyncEnabled); - - if (initDone) { - // add/remove custom/audioreactive palettes - if ((oldAddPalettes && !addPalettes) || (oldAddPalettes && !enabled)) removeAudioPalettes(); - if ((addPalettes && !oldAddPalettes && enabled) || (addPalettes && !oldEnabled && enabled)) createAudioPalettes(); - } // else setup() will create palettes - return configComplete; - } - - - void appendConfigData() override - { -#ifdef ARDUINO_ARCH_ESP32 - oappend(SET_F("dd=addDropdown('AudioReactive','digitalmic:type');")); - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) - oappend(SET_F("addOption(dd,'Generic Analog',0);")); - #endif - oappend(SET_F("addOption(dd,'Generic I2S',1);")); - oappend(SET_F("addOption(dd,'ES7243',2);")); - oappend(SET_F("addOption(dd,'SPH0654',3);")); - oappend(SET_F("addOption(dd,'Generic I2S with Mclk',4);")); - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) - oappend(SET_F("addOption(dd,'Generic I2S PDM',5);")); - #endif - oappend(SET_F("addOption(dd,'ES8388',6);")); - - oappend(SET_F("dd=addDropdown('AudioReactive','config:AGC');")); - oappend(SET_F("addOption(dd,'Off',0);")); - oappend(SET_F("addOption(dd,'Normal',1);")); - oappend(SET_F("addOption(dd,'Vivid',2);")); - oappend(SET_F("addOption(dd,'Lazy',3);")); - - oappend(SET_F("dd=addDropdown('AudioReactive','dynamics:limiter');")); - oappend(SET_F("addOption(dd,'Off',0);")); - oappend(SET_F("addOption(dd,'On',1);")); - oappend(SET_F("addInfo('AudioReactive:dynamics:limiter',0,' On ');")); // 0 is field type, 1 is actual field - oappend(SET_F("addInfo('AudioReactive:dynamics:rise',1,'ms (♪ effects only)');")); - oappend(SET_F("addInfo('AudioReactive:dynamics:fall',1,'ms (♪ effects only)');")); - - oappend(SET_F("dd=addDropdown('AudioReactive','frequency:scale');")); - oappend(SET_F("addOption(dd,'None',0);")); - oappend(SET_F("addOption(dd,'Linear (Amplitude)',2);")); - oappend(SET_F("addOption(dd,'Square Root (Energy)',3);")); - oappend(SET_F("addOption(dd,'Logarithmic (Loudness)',1);")); -#endif - - oappend(SET_F("dd=addDropdown('AudioReactive','sync:mode');")); - oappend(SET_F("addOption(dd,'Off',0);")); -#ifdef ARDUINO_ARCH_ESP32 - oappend(SET_F("addOption(dd,'Send',1);")); -#endif - oappend(SET_F("addOption(dd,'Receive',2);")); -#ifdef ARDUINO_ARCH_ESP32 - oappend(SET_F("addInfo('AudioReactive:digitalmic:type',1,'requires reboot!');")); // 0 is field type, 1 is actual field - oappend(SET_F("addInfo('AudioReactive:digitalmic:pin[]',0,'sd/data/dout','I2S SD');")); - oappend(SET_F("addInfo('AudioReactive:digitalmic:pin[]',1,'ws/clk/lrck','I2S WS');")); - oappend(SET_F("addInfo('AudioReactive:digitalmic:pin[]',2,'sck/bclk','I2S SCK');")); - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) - oappend(SET_F("addInfo('AudioReactive:digitalmic:pin[]',3,'only use -1, 0, 1 or 3','I2S MCLK');")); - #else - oappend(SET_F("addInfo('AudioReactive:digitalmic:pin[]',3,'master clock','I2S MCLK');")); - #endif -#endif - } - - - /* - * handleOverlayDraw() is called just before every show() (LED strip update frame) after effects have set the colors. - * Use this to blank out some LEDs or set them to a different color regardless of the set effect mode. - * Commonly used for custom clocks (Cronixie, 7 segment) - */ - //void handleOverlayDraw() override - //{ - //WS2812FX::setPixelColor(0, RGBW32(0,0,0,0)) // set the first pixel to black - //} - - - /* - * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). - * This could be used in the future for the system to determine whether your usermod is installed. - */ - uint16_t getId() override - { - return USERMOD_ID_AUDIOREACTIVE; - } -}; - -void AudioReactive::removeAudioPalettes(void) { - DEBUG_PRINTLN(F("Removing audio palettes.")); - while (palettes>0) { - WS2812FX::customPalettes.pop_back(); - DEBUG_PRINTLN(palettes); - palettes--; - } - DEBUG_PRINT(F("Total # of palettes: ")); DEBUG_PRINTLN(WS2812FX::customPalettes.size()); -} - -void AudioReactive::createAudioPalettes(void) { - DEBUG_PRINT(F("Total # of palettes: ")); DEBUG_PRINTLN(WS2812FX::customPalettes.size()); - if (palettes) return; - DEBUG_PRINTLN(F("Adding audio palettes.")); - for (int i=0; i= palettes) lastCustPalette -= palettes; - for (int pal=0; palgetStart() >= segStopIdx) continue; if (bus->getStart() + bus->getLength() <= segStartIdx) continue; - //uint8_t type = bus->getType(); if (bus->hasRGB() || (strip.cctFromRgb && bus->hasCCT())) capabilities |= SEG_CAPABILITY_RGB; if (!strip.cctFromRgb && bus->hasCCT()) capabilities |= SEG_CAPABILITY_CCT; if (strip.correctWB && (bus->hasRGB() || bus->hasCCT())) capabilities |= SEG_CAPABILITY_CCT; //white balance correction (CCT slider) @@ -1563,7 +1562,7 @@ uint16_t WS2812FX::getLengthPhysical() const { unsigned len = 0; for (size_t b = 0; b < BusManager::getNumBusses(); b++) { Bus *bus = BusManager::getBus(b); - if (bus->getType() >= TYPE_NET_DDP_RGB) continue; //exclude non-physical network busses + if (bus->isVirtual()) continue; //exclude non-physical network busses len += bus->getLength(); } return len; From bd7cd32f911d85963ec5aba22bdf3420a8770866 Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Sun, 22 Sep 2024 13:56:14 +0200 Subject: [PATCH 088/145] Add mandatory refresh capability to remove type dependency. --- wled00/bus_manager.cpp | 2 +- wled00/bus_manager.h | 2 ++ wled00/data/settings_leds.htm | 9 +++++---- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index 3766975f12..5b948b9c41 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -830,7 +830,7 @@ static String LEDTypesToJson(const std::vector& types) { String json; for (const auto &type : types) { // capabilities follows similar pattern as JSON API - int capabilities = Bus::hasRGB(type.id) | Bus::hasWhite(type.id)<<1 | Bus::hasCCT(type.id)<<2 | Bus::is16bit(type.id)<<4; + int capabilities = Bus::hasRGB(type.id) | Bus::hasWhite(type.id)<<1 | Bus::hasCCT(type.id)<<2 | Bus::is16bit(type.id)<<4 | Bus::mustRefresh(type.id)<<5; char str[256]; sprintf_P(str, PSTR("{i:%d,c:%d,t:\"%s\",n:\"%s\"},"), type.id, capabilities, type.type, type.name); json += str; diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index 40fe61f40a..e96b9de714 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -104,6 +104,7 @@ class Bus { inline bool isPWM() const { return isPWM(_type); } inline bool isVirtual() const { return isVirtual(_type); } inline bool is16bit() const { return is16bit(_type); } + inline bool mustRefresh() const { return mustRefresh(_type); } inline void setReversed(bool reversed) { _reversed = reversed; } inline void setStart(uint16_t start) { _start = start; } inline void setAutoWhiteMode(uint8_t m) { if (m < 5) _autoWhiteMode = m; } @@ -142,6 +143,7 @@ class Bus { static constexpr bool isPWM(uint8_t type) { return (type >= TYPE_ANALOG_MIN && type <= TYPE_ANALOG_MAX); } static constexpr bool isVirtual(uint8_t type) { return (type >= TYPE_VIRTUAL_MIN && type <= TYPE_VIRTUAL_MAX); } static constexpr bool is16bit(uint8_t type) { return type == TYPE_UCS8903 || type == TYPE_UCS8904 || type == TYPE_SM16825; } + static constexpr bool mustRefresh(uint8_t type) { return type == TYPE_TM1814; } static constexpr int numPWMPins(uint8_t type) { return (type - 40); } static inline int16_t getCCT() { return _cct; } diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index 54ba9d8ba5..dd0e8ee8be 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -22,6 +22,7 @@ function hasW(t) { return !!(gT(t).c & 0x02); } // has white channel function hasCCT(t) { return !!(gT(t).c & 0x04); } // is white CCT enabled function is16b(t) { return !!(gT(t).c & 0x10); } // is digital 16 bit type + function mustR(t) { return !!(gT(t).c & 0x20); } // Off refresh is mandatory function numPins(t){ return Math.max(gT(t).t.length, 1); } // type length determines number of GPIO pins function S() { getLoc(); @@ -255,7 +256,7 @@ d.Sf["LA"+n].min = (isVir(t) || isAna(t)) ? 0 : 1; d.Sf["MA"+n].min = (isVir(t) || isAna(t)) ? 0 : 250; } - gId("rf"+n).onclick = (t == 31) ? (()=>{return false}) : (()=>{}); // prevent change for TM1814 + gId("rf"+n).onclick = mustR(t) ? (()=>{return false}) : (()=>{}); // prevent change change of "Refresh" checkmark when mandatory gRGBW |= hasW(t); // RGBW checkbox gId("co"+n).style.display = (isVir(t) || isAna(t)) ? "none":"inline"; // hide color order for PWM gId("dig"+n+"w").style.display = (isDig(t) && hasW(t)) ? "inline":"none"; // show swap channels dropdown @@ -457,9 +458,9 @@ }); // disable inappropriate LED types let sel = d.getElementsByName("LT"+s)[0] - if (i >= maxB || digitalB >= maxD) disable(sel,'option[data-type="D"]'); - if (i >= maxB || twopinB >= 1) disable(sel,'option[data-type="2P"]'); - disable(sel,`option[data-type^="${'A'.repeat(maxA-analogB+1)}"]`); + if (i >= maxB || digitalB >= maxD) disable(sel,'option[data-type="D"]'); // NOTE: see isDig() + if (i >= maxB || twopinB >= 1) disable(sel,'option[data-type="2P"]'); // NOTE: see isD2P() + disable(sel,`option[data-type^="${'A'.repeat(maxA-analogB+1)}"]`); // NOTE: see isPWM() sel.selectedIndex = sel.querySelector('option:not(:disabled)').index; } if (n==-1) { From 3ccc5babc13cc16ed1f141115f83c236b1d3c2ba Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Mon, 23 Sep 2024 20:39:16 +0200 Subject: [PATCH 089/145] Remov superfluous #if --- wled00/udp.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/wled00/udp.cpp b/wled00/udp.cpp index 09e1440efa..60774d7010 100644 --- a/wled00/udp.cpp +++ b/wled00/udp.cpp @@ -974,10 +974,8 @@ void espNowReceiveCB(uint8_t* address, uint8_t* data, uint8_t len, signed int rs DEBUG_PRINTLN(); #endif -#ifndef WLED_DISABLE_ESPNOW // usermods hook can override processing if (UsermodManager::onEspNowMessage(address, data, len)) return; -#endif // handle WiZ Mote data if (data[0] == 0x91 || data[0] == 0x81 || data[0] == 0x80) { From c600c6da63f2a818814595ea9cf6d8dd5d8e0b47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Kristan?= Date: Wed, 25 Sep 2024 09:33:16 +0200 Subject: [PATCH 090/145] Bus length fix --- wled00/FX_fcn.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 737d5f7a47..1bbfa365bd 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -1290,7 +1290,7 @@ void WS2812FX::finalizeInit() { // if we have less counts than pins and they do not align, use last known count to set current count unsigned count = defCounts[(i < defNumCounts) ? i : defNumCounts -1]; // analog always has length 1 - if (Bus::isPWM(dataType)) count = 1; + if (Bus::isPWM(dataType) || Bus::isOnOff(dataType)) count = 1; prevLen += count; BusConfig defCfg = BusConfig(dataType, defPin, start, count, DEFAULT_LED_COLOR_ORDER, false, 0, RGBW_MODE_MANUAL_ONLY, 0, useGlobalLedBuffer); if (BusManager::add(defCfg) == -1) break; From 8180f2c7422557e4753d97a2d06cfda57a979f1e Mon Sep 17 00:00:00 2001 From: Christian Schwinne Date: Fri, 27 Sep 2024 13:46:01 +0200 Subject: [PATCH 091/145] Bump build tool dependencies --- package-lock.json | 119 ++++++++++++++++------------------------------ package.json | 2 +- requirements.txt | 34 ++++++------- 3 files changed, 56 insertions(+), 99 deletions(-) diff --git a/package-lock.json b/package-lock.json index ce2e7a4648..415d881519 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,9 +54,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", @@ -67,15 +67,10 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" - }, "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "bin": { "acorn": "bin/acorn" }, @@ -190,9 +185,9 @@ } }, "node_modules/aws4": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", - "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==" + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==" }, "node_modules/balanced-match": { "version": "1.0.2", @@ -233,11 +228,11 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -701,9 +696,9 @@ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -1281,17 +1276,6 @@ "node": ">=0.10.0" } }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -1375,9 +1359,9 @@ } }, "node_modules/nodemon": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.0.tgz", - "integrity": "sha512-xqlktYlDMCepBJd43ZQhjWwMw2obW/JRvkrLxq5RCNcuDDX1DbcPT+qT1IlIIdf+DhnWs90JpTMe+Y5KxOchvA==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.7.tgz", + "integrity": "sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ==", "dependencies": { "chokidar": "^3.5.2", "debug": "^4", @@ -1402,11 +1386,11 @@ } }, "node_modules/nodemon/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -1418,9 +1402,9 @@ } }, "node_modules/nodemon/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/nodemon/node_modules/supports-color": { "version": "5.5.0", @@ -1433,20 +1417,6 @@ "node": ">=4" } }, - "node_modules/nopt": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", - "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "*" - } - }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -1507,6 +1477,7 @@ "version": "0.1.5", "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "deprecated": "This package is no longer supported.", "dependencies": { "os-homedir": "^1.0.0", "os-tmpdir": "^1.0.0" @@ -1620,6 +1591,7 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", + "deprecated": "You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\n\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)", "engines": { "node": ">=0.6.0", "teleport": ">=0.2.0" @@ -1826,12 +1798,9 @@ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, "node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dependencies": { - "lru-cache": "^6.0.0" - }, + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "bin": { "semver": "bin/semver.js" }, @@ -1993,9 +1962,9 @@ } }, "node_modules/terser": { - "version": "5.29.2", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.29.2.tgz", - "integrity": "sha512-ZiGkhUBIM+7LwkNjXYJq8svgkd+QK3UUr0wJqY4MieaezBSAIPgbSPZyIx0idM6XWK5CMzSWa8MJIzmRcB8Caw==", + "version": "5.34.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.34.0.tgz", + "integrity": "sha512-y5NUX+U9HhVsK/zihZwoq4r9dICLyV2jXGOriDAVOeKhq3LKVjgJbGO90FisozXLlJfvjHqgckGmJFBb9KYoWQ==", "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -2042,12 +2011,9 @@ } }, "node_modules/touch": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", - "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", - "dependencies": { - "nopt": "~1.0.10" - }, + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", "bin": { "nodetouch": "bin/nodetouch.js" } @@ -2065,9 +2031,9 @@ } }, "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" }, "node_modules/tunnel-agent": { "version": "0.6.0", @@ -2229,11 +2195,6 @@ "node": ">=0.10.0" } }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, "node_modules/yargs": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", diff --git a/package.json b/package.json index e47a46b261..721455bff3 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,6 @@ "clean-css": "^5.3.3", "html-minifier-terser": "^7.2.0", "inliner": "^1.13.1", - "nodemon": "^3.0.2" + "nodemon": "^3.1.7" } } diff --git a/requirements.txt b/requirements.txt index c4ce9445fd..666122aa28 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,19 +4,17 @@ # # pip-compile # -aiofiles==22.1.0 - # via platformio ajsonrpc==1.2.0 # via platformio -anyio==3.6.2 +anyio==4.6.0 # via starlette -bottle==0.12.25 +bottle==0.13.1 # via platformio -certifi==2023.7.22 +certifi==2024.8.30 # via requests -charset-normalizer==3.1.0 +charset-normalizer==3.3.2 # via requests -click==8.1.3 +click==8.1.7 # via # platformio # uvicorn @@ -28,35 +26,33 @@ h11==0.14.0 # via # uvicorn # wsproto -idna==3.7 +idna==3.10 # via # anyio # requests -marshmallow==3.19.0 +marshmallow==3.22.0 # via platformio -packaging==23.1 +packaging==24.1 # via marshmallow -platformio==6.1.14 +platformio==6.1.16 # via -r requirements.in -pyelftools==0.29 +pyelftools==0.31 # via platformio pyserial==3.5 # via platformio -requests==2.32.0 +requests==2.32.3 # via platformio semantic-version==2.10.0 # via platformio -sniffio==1.3.0 +sniffio==1.3.1 # via anyio -starlette==0.23.1 +starlette==0.39.1 # via platformio tabulate==0.9.0 # via platformio -typing-extensions==4.11.0 - # via starlette -urllib3==1.26.19 +urllib3==2.2.3 # via requests -uvicorn==0.20.0 +uvicorn==0.30.6 # via platformio wsproto==1.2.0 # via platformio From 9a4b56db6e6a8fa06bbcafdb3334ab4ce4414c18 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Fri, 27 Sep 2024 21:05:28 -0400 Subject: [PATCH 092/145] Fix incorrect F-strings A merge issue with end-oappend: some strings did not get correctly converted from SET_F() to F(), which can cause crashes. --- wled00/xml.cpp | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/wled00/xml.cpp b/wled00/xml.cpp index 68a26036b5..424842a1d0 100644 --- a/wled00/xml.cpp +++ b/wled00/xml.cpp @@ -91,14 +91,14 @@ void appendGPIOinfo(Print& settingsScript) { settingsScript.print(F("];")); // add reserved (unusable) pins - settingsScript.print(SET_F("d.rsvd=[")); + settingsScript.print(F("d.rsvd=[")); for (unsigned i = 0; i < WLED_NUM_PINS; i++) { if (!PinManager::isPinOk(i, false)) { // include readonly pins settingsScript.print(i); settingsScript.print(","); } } #ifdef WLED_ENABLE_DMX - settingsScript.print(SET_F("2,")); // DMX hardcoded pin + settingsScript.print(F("2,")); // DMX hardcoded pin #endif #if defined(WLED_DEBUG) && !defined(WLED_DEBUG_HOST) settingsScript.printf_P(PSTR(",%d"),hardwareTX); // debug output (TX) pin @@ -113,36 +113,36 @@ void appendGPIOinfo(Print& settingsScript) { switch (ethernetBoards[ethernetType].eth_clk_mode) { case ETH_CLOCK_GPIO0_IN: case ETH_CLOCK_GPIO0_OUT: - settingsScript.print(SET_F("0")); + settingsScript.print(F("0")); break; case ETH_CLOCK_GPIO16_OUT: - settingsScript.print(SET_F("16")); + settingsScript.print(F("16")); break; case ETH_CLOCK_GPIO17_OUT: - settingsScript.print(SET_F("17")); + settingsScript.print(F("17")); break; } } #endif - settingsScript.print(SET_F("];")); // rsvd + settingsScript.print(F("];")); // rsvd // add info for read-only GPIO - settingsScript.print(SET_F("d.ro_gpio=[")); + settingsScript.print(F("d.ro_gpio=[")); bool firstPin = true; for (unsigned i = 0; i < WLED_NUM_PINS; i++) { if (PinManager::isReadOnlyPin(i)) { // No comma before the first pin - if (!firstPin) settingsScript.print(SET_F(",")); + if (!firstPin) settingsScript.print(F(",")); settingsScript.print(i); firstPin = false; } } - settingsScript.print(SET_F("];")); + settingsScript.print(F("];")); // add info about max. # of pins - settingsScript.print(SET_F("d.max_gpio=")); + settingsScript.print(F("d.max_gpio=")); settingsScript.print(WLED_NUM_PINS); - settingsScript.print(SET_F(";")); + settingsScript.print(F(";")); } //get values for settings form in javascript @@ -263,7 +263,7 @@ void getSettingsJS(byte subPage, Print& settingsScript) appendGPIOinfo(settingsScript); - settingsScript.print(SET_F("d.ledTypes=")); settingsScript.print(BusManager::getLEDTypesJSONString().c_str()); settingsScript.print(";"); + settingsScript.print(F("d.ledTypes=")); settingsScript.print(BusManager::getLEDTypesJSONString().c_str()); settingsScript.print(";"); // set limits settingsScript.printf_P(PSTR("bLimits(%d,%d,%d,%d,%d,%d,%d,%d);"), @@ -501,7 +501,7 @@ void getSettingsJS(byte subPage, Print& settingsScript) #endif printSetFormValue(settingsScript,PSTR("BD"),serialBaud); #ifndef WLED_ENABLE_ADALIGHT - settingsScript.print(SET_F("toggle('Serial);")); + settingsScript.print(F("toggle('Serial);")); #endif } From 6f221852a262d9f2ba6f6817b1284990d8f4f54d Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Sat, 28 Sep 2024 18:25:16 +0200 Subject: [PATCH 093/145] partition file for 512Kb Filesystem, 1.7MB Program the missing link between 256KB (very small FS) and 700KB (only 100KB extra program) --- tools/WLED_ESP32_4MB_512KB_FS.csv | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 tools/WLED_ESP32_4MB_512KB_FS.csv diff --git a/tools/WLED_ESP32_4MB_512KB_FS.csv b/tools/WLED_ESP32_4MB_512KB_FS.csv new file mode 100644 index 0000000000..5281a61244 --- /dev/null +++ b/tools/WLED_ESP32_4MB_512KB_FS.csv @@ -0,0 +1,7 @@ +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x5000, +otadata, data, ota, 0xe000, 0x2000, +app0, app, ota_0, 0x10000, 0x1B0000, +app1, app, ota_1, 0x1C0000,0x1B0000, +spiffs, data, spiffs, 0x370000,0x80000, +coredump, data, coredump,,64K \ No newline at end of file From 10d8cfde8555bb3b817ede0e7dbeb45d02a38517 Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Sun, 29 Sep 2024 13:00:07 +0200 Subject: [PATCH 094/145] Fix FX filter bug --- wled00/data/index.css | 15 ++++++++++----- wled00/data/index.js | 32 ++++++++++++++++++++++---------- 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/wled00/data/index.css b/wled00/data/index.css index 0952cca210..c4e85f73f2 100644 --- a/wled00/data/index.css +++ b/wled00/data/index.css @@ -35,6 +35,7 @@ --sgp: "block"; --bmt: 0; --sti: 42px; + --stp: 42px; } html { @@ -468,7 +469,7 @@ button { padding: 4px 2px; position: relative; opacity: 1; - transition: opacity .5s linear, height .25s, transform .25s; + transition: opacity .25s linear, height .2s, transform .2s; } .filter { @@ -1335,10 +1336,12 @@ TD .checkmark, TD .radiomark { top: 42px; } -#fxlist .lstI.selected, -#pallist .lstI.selected { +#fxlist .lstI.selected { top: calc(var(--sti) + 42px); } +#pallist .lstI.selected { + top: calc(var(--stp) + 42px); +} dialog::backdrop { backdrop-filter: blur(10px); @@ -1353,10 +1356,12 @@ dialog { color: var(--c-f); } -#fxlist .lstI.sticky, -#pallist .lstI.sticky { +#fxlist .lstI.sticky { top: var(--sti); } +#pallist .lstI.sticky { + top: var(--stp); +} /* list item content */ .lstIcontent { diff --git a/wled00/data/index.js b/wled00/data/index.js index 25ade11639..d9c64bdfbf 100644 --- a/wled00/data/index.js +++ b/wled00/data/index.js @@ -2828,7 +2828,12 @@ function search(field, listId = null) { // restore default preset sorting if no search term is entered if (!search) { if (listId === 'pcont') { populatePresets(); return; } - if (listId === 'pallist') { populatePalettes(); return; } + if (listId === 'pallist') { + let id = parseInt(d.querySelector('#pallist input[name="palette"]:checked').value); // preserve selected palette + populatePalettes(); + updateSelectedPalette(id); + return; + } } // clear filter if searching in fxlist @@ -2887,18 +2892,25 @@ function initFilters() { function filterFocus(e) { const f = gId("filters"); - if (e.type === "focus") f.classList.remove('fade'); // immediately show (still has transition) - // compute sticky top (with delay for transition) - setTimeout(() => { - const sti = parseInt(getComputedStyle(d.documentElement).getPropertyValue('--sti')) + (e.type === "focus" ? 1 : -1) * f.offsetHeight; - sCol('--sti', sti + "px"); - }, 252); + const c = !!f.querySelectorAll("input[type=checkbox]:checked").length; + const h = f.offsetHeight; + const sti = parseInt(getComputedStyle(d.documentElement).getPropertyValue('--sti')); + if (e.type === "focus") { + // compute sticky top (with delay for transition) + if (!h) setTimeout(() => { + sCol('--sti', (sti+f.offsetHeight) + "px"); // has an unpleasant consequence on palette offset + }, 255); + f.classList.remove('fade'); // immediately show (still has transition) + } if (e.type === "blur") { setTimeout(() => { if (e.target === document.activeElement && document.hasFocus()) return; // do not hide if filter is active - if (gId("filters").querySelectorAll("input[type=checkbox]:checked").length) return; - f.classList.add('fade'); + if (!c) { + // compute sticky top + sCol('--sti', (sti-h) + "px"); // has an unpleasant consequence on palette offset + f.classList.add('fade'); + } }, 255); // wait with hiding } } @@ -2911,7 +2923,7 @@ function filterFx() { gId("fxlist").querySelectorAll('.lstI').forEach((listItem,i) => { const listItemName = listItem.querySelector('.lstIname').innerText; let hide = false; - gId("filters").querySelectorAll("input[type=checkbox]").forEach((e) => { if (e.checked && !listItemName.includes(e.dataset.flt)) hide = true; }); + gId("filters").querySelectorAll("input[type=checkbox]").forEach((e) => { if (e.checked && !listItemName.includes(e.dataset.flt)) hide = i>0 /*true*/; }); listItem.style.display = hide ? 'none' : ''; }); } From d3c401ed4e59ab19cc79be7d3bb1ed53de5e56ae Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Sun, 29 Sep 2024 18:37:18 +0200 Subject: [PATCH 095/145] wu_pixel small optimization 5% faster --- wled00/FX_2Dfcn.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/wled00/FX_2Dfcn.cpp b/wled00/FX_2Dfcn.cpp index 26ec1d608a..0f66905496 100644 --- a/wled00/FX_2Dfcn.cpp +++ b/wled00/FX_2Dfcn.cpp @@ -704,11 +704,14 @@ void Segment::wu_pixel(uint32_t x, uint32_t y, CRGB c) { //awesome wu_pixel WU_WEIGHT(ix, yy), WU_WEIGHT(xx, yy)}; // multiply the intensities by the colour, and saturating-add them to the pixels for (int i = 0; i < 4; i++) { - CRGB led = getPixelColorXY((x >> 8) + (i & 1), (y >> 8) + ((i >> 1) & 1)); + int wu_x = (x >> 8) + (i & 1); // precalculate x + int wu_y = (y >> 8) + ((i >> 1) & 1); // precalculate y + CRGB led = getPixelColorXY(wu_x, wu_y); + CRGB oldLed = led; led.r = qadd8(led.r, c.r * wu[i] >> 8); led.g = qadd8(led.g, c.g * wu[i] >> 8); led.b = qadd8(led.b, c.b * wu[i] >> 8); - setPixelColorXY(int((x >> 8) + (i & 1)), int((y >> 8) + ((i >> 1) & 1)), led); + if (led != oldLed) setPixelColorXY(wu_x, wu_y, led); // don't repaint if same color } } #undef WU_WEIGHT From 7fa25ca7aec7ad3969011ecfeba63197cb43190e Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Mon, 30 Sep 2024 11:25:58 +0200 Subject: [PATCH 096/145] pio update - flash size of non-standard boards * adding missing flash size flags that were lost between 0.14 and 0.15 (necessary if you don't flash using esptool) * adding env:esp32dev_16M for 16MB flash (serg74 esp32-16M, twilightlord esp32 16M) --- platformio.ini | 21 +++++++++++++++++++++ platformio_override.sample.ini | 2 ++ 2 files changed, 23 insertions(+) diff --git a/platformio.ini b/platformio.ini index 4d30be322a..3005ba2208 100644 --- a/platformio.ini +++ b/platformio.ini @@ -430,7 +430,26 @@ lib_deps = ${esp32_idf_V4.lib_deps} ${esp32.AR_lib_deps} monitor_filters = esp32_exception_decoder board_build.partitions = ${esp32.large_partitions} +board_upload.flash_size = 8MB +board_upload.maximum_size = 8388608 ; board_build.f_flash = 80000000L +; board_build.flash_mode = qio + +[env:esp32dev_16M] +board = esp32dev +platform = ${esp32_idf_V4.platform} +platform_packages = ${esp32_idf_V4.platform_packages} +build_unflags = ${common.build_unflags} +build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=ESP32_16M #-D WLED_DISABLE_BROWNOUT_DET + ${esp32.AR_build_flags} +lib_deps = ${esp32_idf_V4.lib_deps} + ${esp32.AR_lib_deps} +monitor_filters = esp32_exception_decoder +board_build.partitions = ${esp32.extreme_partitions} +board_upload.flash_size = 16MB +board_upload.maximum_size = 16777216 +board_build.f_flash = 80000000L +board_build.flash_mode = dio ;[env:esp32dev_audioreactive] ;board = esp32dev @@ -508,6 +527,8 @@ build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME= lib_deps = ${esp32s3.lib_deps} ${esp32.AR_lib_deps} board_build.partitions = ${esp32.extreme_partitions} +board_upload.flash_size = 16MB +board_upload.maximum_size = 16777216 board_build.f_flash = 80000000L board_build.flash_mode = qio monitor_filters = esp32_exception_decoder diff --git a/platformio_override.sample.ini b/platformio_override.sample.ini index dedc8edf53..8e5fdf0030 100644 --- a/platformio_override.sample.ini +++ b/platformio_override.sample.ini @@ -359,6 +359,8 @@ upload_speed = 115200 lib_deps = ${esp32c3.lib_deps} board_build.partitions = tools/WLED_ESP32_2MB_noOTA.csv board_build.flash_mode = dio +board_upload.flash_size = 2MB +board_upload.maximum_size = 2097152 [env:wemos_shield_esp32] board = esp32dev From 4ed8ded502fa77396303a5241b643cb7fc406dce Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Mon, 30 Sep 2024 17:44:38 +0200 Subject: [PATCH 097/145] Akemi bugfix for panel width > 32 due to a math accident, Akemi did not show proper GEQ bands in its hands when width>32 --- wled00/FX.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index ad843f0f95..40db1a3df2 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7528,8 +7528,9 @@ uint16_t mode_2DAkemi(void) { //add geq left and right if (um_data && fftResult) { - for (int x=0; x < cols/8; x++) { - unsigned band = x * cols/8; + int xMax = cols/8; + for (int x=0; x < xMax; x++) { + unsigned band = map2(x, 0, max(xMax,4), 0, 15); // map 0..cols/8 to 16 GEQ bands band = constrain(band, 0, 15); int barHeight = map(fftResult[band], 0, 255, 0, 17*rows/32); CRGB color = CRGB(SEGMENT.color_from_palette((band * 35), false, PALETTE_SOLID_WRAP, 0)); From 3765d558b6ebfb4e9d90074bf545580d4475dcbd Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Mon, 30 Sep 2024 18:26:00 +0200 Subject: [PATCH 098/145] akemi bugfix fix map2 --> map --- wled00/FX.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 40db1a3df2..2ac773099a 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7530,7 +7530,7 @@ uint16_t mode_2DAkemi(void) { if (um_data && fftResult) { int xMax = cols/8; for (int x=0; x < xMax; x++) { - unsigned band = map2(x, 0, max(xMax,4), 0, 15); // map 0..cols/8 to 16 GEQ bands + unsigned band = map(x, 0, max(xMax,4), 0, 15); // map 0..cols/8 to 16 GEQ bands band = constrain(band, 0, 15); int barHeight = map(fftResult[band], 0, 255, 0, 17*rows/32); CRGB color = CRGB(SEGMENT.color_from_palette((band * 35), false, PALETTE_SOLID_WRAP, 0)); From 262af0678f3c2ce367b244dc99acc4764c76c723 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Mon, 30 Sep 2024 18:35:14 +0200 Subject: [PATCH 099/145] colored burst effect bugfix (swapped XY dimensions) fixing a bug where width and height got swapped (visible on non-square panels) --- wled00/FX.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 2ac773099a..a3cd32e8e3 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -4931,8 +4931,8 @@ uint16_t mode_2DColoredBursts() { // By: ldirko https://editor.so SEGMENT.fadeToBlackBy(40); for (size_t i = 0; i < numLines; i++) { byte x1 = beatsin8(2 + SEGMENT.speed/16, 0, (cols - 1)); - byte x2 = beatsin8(1 + SEGMENT.speed/16, 0, (cols - 1)); - byte y1 = beatsin8(5 + SEGMENT.speed/16, 0, (rows - 1), 0, i * 24); + byte x2 = beatsin8(1 + SEGMENT.speed/16, 0, (rows - 1)); + byte y1 = beatsin8(5 + SEGMENT.speed/16, 0, (cols - 1), 0, i * 24); byte y2 = beatsin8(3 + SEGMENT.speed/16, 0, (rows - 1), 0, i * 48 + 64); CRGB color = ColorFromPalette(SEGPALETTE, i * 255 / numLines + (SEGENV.aux0&0xFF), 255, LINEARBLEND); From 402fba734ad4dbade8aef23382d77a856d497bb7 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Wed, 2 Oct 2024 16:34:36 +0200 Subject: [PATCH 100/145] bugfix for holes in 2D DNA Spiral Holes were visible at height > 32. Root cause: "lerp8x8" seems to be inaccurate --> replaced by a simple linear calculation. --- wled00/FX.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index a3cd32e8e3..f3e82275c6 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -5010,9 +5010,11 @@ uint16_t mode_2DDNASpiral() { // By: ldirko https://editor.soulma // draw a gradient line between x and x1 x = x / 2; x1 = x1 / 2; unsigned steps = abs8(x - x1) + 1; + bool positive = (x1 >= x); // direction of drawing for (size_t k = 1; k <= steps; k++) { unsigned rate = k * 255 / steps; - unsigned dx = lerp8by8(x, x1, rate); + //unsigned dx = lerp8by8(x, x1, rate); + unsigned dx = positive? (x + k-1) : (x - k+1); // behaves the same as "lerp8by8" but does not create holes //SEGMENT.setPixelColorXY(dx, i, ColorFromPalette(SEGPALETTE, hue, 255, LINEARBLEND).nscale8_video(rate)); SEGMENT.addPixelColorXY(dx, i, ColorFromPalette(SEGPALETTE, hue, 255, LINEARBLEND)); // use setPixelColorXY for different look SEGMENT.fadePixelColorXY(dx, i, rate); From a4c49aa35e93d158033844f6d433fae56def8514 Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Wed, 2 Oct 2024 20:15:58 +0200 Subject: [PATCH 101/145] Fix for #4005 --- wled00/wled.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/wled00/wled.cpp b/wled00/wled.cpp index 39e0d250be..13d43218af 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -219,6 +219,7 @@ void WLED::loop() busConfigs[i] = nullptr; } strip.finalizeInit(); // also loads default ledmap if present + BusManager::setBrightness(bri); // fix re-initialised bus' brightness #4005 if (aligned) strip.makeAutoSegments(); else strip.fixInvalidSegments(); doSerializeConfig = true; From dd27504d30e021e038a70ba5900ce47079a09f96 Mon Sep 17 00:00:00 2001 From: Nicolas Saugnier Date: Thu, 3 Oct 2024 11:04:47 +0200 Subject: [PATCH 102/145] Fixed Improv rejecting all properly formatted packets. --- wled00/improv.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/wled00/improv.cpp b/wled00/improv.cpp index abfd463c6d..66f14aec60 100644 --- a/wled00/improv.cpp +++ b/wled00/improv.cpp @@ -123,6 +123,7 @@ void handleImprovPacket() { } checksum += next; + checksum &= 0xFF; packetByte++; } } From ae1b6af0d43b79557ff10a4feb4d709722bd69f8 Mon Sep 17 00:00:00 2001 From: Nicolas Saugnier Date: Thu, 3 Oct 2024 11:07:58 +0200 Subject: [PATCH 103/145] Indent formatting... --- wled00/improv.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/improv.cpp b/wled00/improv.cpp index 66f14aec60..31547f86c8 100644 --- a/wled00/improv.cpp +++ b/wled00/improv.cpp @@ -123,7 +123,7 @@ void handleImprovPacket() { } checksum += next; - checksum &= 0xFF; + checksum &= 0xFF; packetByte++; } } From 949b9fb10ea7a841f6b9f2ccc49da4347cc84e36 Mon Sep 17 00:00:00 2001 From: Nicolas Saugnier Date: Thu, 3 Oct 2024 15:21:39 +0200 Subject: [PATCH 104/145] Fixed Polybus.canShow always returning true on ESP32 --- wled00/bus_wrapper.h | 72 ++++++++++++++++++++++---------------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/wled00/bus_wrapper.h b/wled00/bus_wrapper.h index bf2d30c0e6..84c32f46bc 100644 --- a/wled00/bus_wrapper.h +++ b/wled00/bus_wrapper.h @@ -766,47 +766,47 @@ class PolyBus { #endif #ifdef ARDUINO_ARCH_ESP32 // RMT buses - case I_32_RN_NEO_3: (static_cast(busPtr))->CanShow(); break; - case I_32_RN_NEO_4: (static_cast(busPtr))->CanShow(); break; - case I_32_RN_400_3: (static_cast(busPtr))->CanShow(); break; - case I_32_RN_TM1_4: (static_cast(busPtr))->CanShow(); break; - case I_32_RN_TM2_3: (static_cast(busPtr))->CanShow(); break; - case I_32_RN_UCS_3: (static_cast(busPtr))->CanShow(); break; - case I_32_RN_UCS_4: (static_cast(busPtr))->CanShow(); break; - case I_32_RN_APA106_3: (static_cast(busPtr))->CanShow(); break; - case I_32_RN_FW6_5: (static_cast(busPtr))->CanShow(); break; - case I_32_RN_2805_5: (static_cast(busPtr))->CanShow(); break; - case I_32_RN_TM1914_3: (static_cast(busPtr))->CanShow(); break; - case I_32_RN_SM16825_5: (static_cast(busPtr))->CanShow(); break; + case I_32_RN_NEO_3: return (static_cast(busPtr))->CanShow(); break; + case I_32_RN_NEO_4: return (static_cast(busPtr))->CanShow(); break; + case I_32_RN_400_3: return (static_cast(busPtr))->CanShow(); break; + case I_32_RN_TM1_4: return (static_cast(busPtr))->CanShow(); break; + case I_32_RN_TM2_3: return (static_cast(busPtr))->CanShow(); break; + case I_32_RN_UCS_3: return (static_cast(busPtr))->CanShow(); break; + case I_32_RN_UCS_4: return (static_cast(busPtr))->CanShow(); break; + case I_32_RN_APA106_3: return (static_cast(busPtr))->CanShow(); break; + case I_32_RN_FW6_5: return (static_cast(busPtr))->CanShow(); break; + case I_32_RN_2805_5: return (static_cast(busPtr))->CanShow(); break; + case I_32_RN_TM1914_3: return (static_cast(busPtr))->CanShow(); break; + case I_32_RN_SM16825_5: return (static_cast(busPtr))->CanShow(); break; // I2S1 bus or paralell buses #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_3: if (useParallelI2S) (static_cast(busPtr))->CanShow(); else (static_cast(busPtr))->CanShow(); break; - case I_32_I1_NEO_4: if (useParallelI2S) (static_cast(busPtr))->CanShow(); else (static_cast(busPtr))->CanShow(); break; - case I_32_I1_400_3: if (useParallelI2S) (static_cast(busPtr))->CanShow(); else (static_cast(busPtr))->CanShow(); break; - case I_32_I1_TM1_4: if (useParallelI2S) (static_cast(busPtr))->CanShow(); else (static_cast(busPtr))->CanShow(); break; - case I_32_I1_TM2_3: if (useParallelI2S) (static_cast(busPtr))->CanShow(); else (static_cast(busPtr))->CanShow(); break; - case I_32_I1_UCS_3: if (useParallelI2S) (static_cast(busPtr))->CanShow(); else (static_cast(busPtr))->CanShow(); break; - case I_32_I1_UCS_4: if (useParallelI2S) (static_cast(busPtr))->CanShow(); else (static_cast(busPtr))->CanShow(); break; - case I_32_I1_APA106_3: if (useParallelI2S) (static_cast(busPtr))->CanShow(); else (static_cast(busPtr))->CanShow(); break; - case I_32_I1_FW6_5: if (useParallelI2S) (static_cast(busPtr))->CanShow(); else (static_cast(busPtr))->CanShow(); break; - case I_32_I1_2805_5: if (useParallelI2S) (static_cast(busPtr))->CanShow(); else (static_cast(busPtr))->CanShow(); break; - case I_32_I1_TM1914_3: if (useParallelI2S) (static_cast(busPtr))->CanShow(); else (static_cast(busPtr))->CanShow(); break; - case I_32_I1_SM16825_5: if (useParallelI2S) (static_cast(busPtr))->CanShow(); else (static_cast(busPtr))->CanShow(); break; + case I_32_I1_NEO_3: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; + case I_32_I1_NEO_4: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; + case I_32_I1_400_3: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; + case I_32_I1_TM1_4: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; + case I_32_I1_TM2_3: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; + case I_32_I1_UCS_3: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; + case I_32_I1_UCS_4: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; + case I_32_I1_APA106_3: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; + case I_32_I1_FW6_5: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; + case I_32_I1_2805_5: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; + case I_32_I1_TM1914_3: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; + case I_32_I1_SM16825_5: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; #endif // I2S0 bus #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_3: (static_cast(busPtr))->CanShow(); break; - case I_32_I0_NEO_4: (static_cast(busPtr))->CanShow(); break; - case I_32_I0_400_3: (static_cast(busPtr))->CanShow(); break; - case I_32_I0_TM1_4: (static_cast(busPtr))->CanShow(); break; - case I_32_I0_TM2_3: (static_cast(busPtr))->CanShow(); break; - case I_32_I0_UCS_3: (static_cast(busPtr))->CanShow(); break; - case I_32_I0_UCS_4: (static_cast(busPtr))->CanShow(); break; - case I_32_I0_APA106_3: (static_cast(busPtr))->CanShow(); break; - case I_32_I0_FW6_5: (static_cast(busPtr))->CanShow(); break; - case I_32_I0_2805_5: (static_cast(busPtr))->CanShow(); break; - case I_32_I0_TM1914_3: (static_cast(busPtr))->CanShow(); break; - case I_32_I0_SM16825_5: (static_cast(busPtr))->CanShow(); break; + case I_32_I0_NEO_3: return (static_cast(busPtr))->CanShow(); break; + case I_32_I0_NEO_4: return (static_cast(busPtr))->CanShow(); break; + case I_32_I0_400_3: return (static_cast(busPtr))->CanShow(); break; + case I_32_I0_TM1_4: return (static_cast(busPtr))->CanShow(); break; + case I_32_I0_TM2_3: return (static_cast(busPtr))->CanShow(); break; + case I_32_I0_UCS_3: return (static_cast(busPtr))->CanShow(); break; + case I_32_I0_UCS_4: return (static_cast(busPtr))->CanShow(); break; + case I_32_I0_APA106_3: return (static_cast(busPtr))->CanShow(); break; + case I_32_I0_FW6_5: return (static_cast(busPtr))->CanShow(); break; + case I_32_I0_2805_5: return (static_cast(busPtr))->CanShow(); break; + case I_32_I0_TM1914_3: return (static_cast(busPtr))->CanShow(); break; + case I_32_I0_SM16825_5: return (static_cast(busPtr))->CanShow(); break; #endif #endif case I_HS_DOT_3: return (static_cast(busPtr))->CanShow(); break; From 1b0ce9a123617abbe6580cca0f0548f1f9760fb5 Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Sat, 5 Oct 2024 15:00:58 +0200 Subject: [PATCH 105/145] Fix for #4179 --- wled00/FX.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index f3e82275c6..e7429d19f1 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -4031,7 +4031,7 @@ uint16_t mode_pacifica() // Increment the four "color index start" counters, one for each wave layer. // Each is incremented at a different speed, and the speeds vary over time. - unsigned sCIStart1 = SEGENV.aux0, sCIStart2 = SEGENV.aux1, sCIStart3 = SEGENV.step, sCIStart4 = SEGENV.step >> 16; + unsigned sCIStart1 = SEGENV.aux0, sCIStart2 = SEGENV.aux1, sCIStart3 = SEGENV.step & 0xFFFF, sCIStart4 = (SEGENV.step >> 16); uint32_t deltams = (FRAMETIME >> 2) + ((FRAMETIME * SEGMENT.speed) >> 7); uint64_t deltat = (strip.now >> 2) + ((strip.now * SEGMENT.speed) >> 7); strip.now = deltat; @@ -4046,7 +4046,7 @@ uint16_t mode_pacifica() sCIStart3 -= (deltams1 * beatsin88(501,5,7)); sCIStart4 -= (deltams2 * beatsin88(257,4,6)); SEGENV.aux0 = sCIStart1; SEGENV.aux1 = sCIStart2; - SEGENV.step = sCIStart4; SEGENV.step = (SEGENV.step << 16) + sCIStart3; + SEGENV.step = (sCIStart4 << 16) | (sCIStart3 & 0xFFFF); // Clear out the LED array to a dim background blue-green //SEGMENT.fill(132618); @@ -4077,7 +4077,7 @@ uint16_t mode_pacifica() c.green = scale8(c.green, 200); c |= CRGB( 2, 5, 7); - SEGMENT.setPixelColor(i, c.red, c.green, c.blue); + SEGMENT.setPixelColor(i, c); } strip.now = nowOld; From 407477dc6816b98ef2b8091dcb59628013ef2ba5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Kristan?= Date: Sun, 6 Oct 2024 15:42:58 +0200 Subject: [PATCH 106/145] Fix for #4168 - set min value to 0 for disabled ABL --- wled00/data/settings_leds.htm | 58 ++++++++++++++++++++--------------- 1 file changed, 34 insertions(+), 24 deletions(-) diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index dd0e8ee8be..6be5becd10 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -119,7 +119,12 @@ var en = d.Sf.ABL.checked; gId('abl').style.display = (en) ? 'inline':'none'; gId('psu2').style.display = (en) ? 'inline':'none'; - if (!en) d.Sf.PPL.checked = false; + if (!en) { + // limiter disabled + d.Sf.PPL.checked = false; +// d.Sf.querySelectorAll("#mLC select[name^=LAsel]").forEach((e)=>{e.selectedIndex = 0;}); // select default LED mA +// d.Sf.querySelectorAll("#mLC input[name^=LA]").forEach((e)=>{e.min = 0; e.value = 0;}); // set min & value to 0 + } UI(); } // enable per port limiter and calculate current @@ -132,46 +137,51 @@ d.Sf.MA.min = abl && !ppl ? 250 : 0; gId("psuMA").style.display = ppl ? 'none' : 'inline'; gId("ppldis").style.display = ppl ? 'inline' : 'none'; - // set PPL minimum value and clear actual PPL limit if ABL disabled + // set PPL minimum value and clear actual PPL limit if ABL is disabled d.Sf.querySelectorAll("#mLC input[name^=MA]").forEach((i,x)=>{ var n = String.fromCharCode((x<10?48:55)+x); gId("PSU"+n).style.display = ppl ? "inline" : "none"; const t = parseInt(d.Sf["LT"+n].value); // LED type SELECT const c = parseInt(d.Sf["LC"+n].value); //get LED count - i.min = ppl && !(isVir(t) || isAna(t)) ? 250 : 0; - if (!abl || isVir(t) || isAna(t)) i.value = 0; + i.min = ppl && isDig(t) ? 250 : 0; + if (!abl || !isDig(t)) i.value = 0; else if (ppl) sumMA += parseInt(i.value,10); else if (sDI) i.value = Math.round(parseInt(d.Sf.MA.value,10)*c/sDI); }); if (ppl) d.Sf.MA.value = sumMA; // populate UI ABL value if PPL used } + // enable and update LED Amps function enLA(s,n) { + const abl = d.Sf.ABL.checked; const t = parseInt(d.Sf["LT"+n].value); // LED type SELECT - gId('LAdis'+n).style.display = s.selectedIndex==5 ? "inline" : "none"; - if (s.value!=="0") d.Sf["LA"+n].value = s.value; - d.Sf["LA"+n].min = (isVir(t) || isAna(t)) ? 0 : 1; + gId('LAdis'+n).style.display = s.selectedIndex==5 ? "inline" : "none"; // show/hide custom mA field + if (s.value!=="0") d.Sf["LA"+n].value = s.value; // set value from select object + d.Sf["LA"+n].min = (!isDig(t) || !abl) ? 0 : 1; // set minimum value for validation } function setABL() { - d.Sf.ABL.checked = parseInt(d.Sf.MA.value) > 0; + let en = parseInt(d.Sf.MA.value) > 0; // check if ABL is enabled (max mA entered per output) d.Sf.querySelectorAll("#mLC input[name^=MA]").forEach((i,n)=>{ - if (parseInt(i.value) > 0) d.Sf.ABL.checked = true; + if (parseInt(i.value) > 0) en = true; }); + d.Sf.ABL.checked = en; // select appropriate LED current d.Sf.querySelectorAll("#mLC select[name^=LAsel]").forEach((sel,x)=>{ sel.value = 0; // set custom var n = String.fromCharCode((x<10?48:55)+x); - switch (parseInt(d.Sf["LA"+n].value)) { - case 0: break; // disable ABL - case 15: sel.value = 15; break; - case 30: sel.value = 30; break; - case 35: sel.value = 35; break; - case 55: sel.value = 55; break; - case 255: sel.value = 255; break; - } - enLA(sel,n); + if (en) + switch (parseInt(d.Sf["LA"+n].value)) { + case 0: break; // disable ABL + case 15: sel.value = 15; break; + case 30: sel.value = 30; break; + case 35: sel.value = 35; break; + case 55: sel.value = 55; break; + case 255: sel.value = 255; break; + } + else sel.value = 0; + enLA(sel,n); // configure individual limiter }); enABL(); gId('m1').innerHTML = maxM; @@ -202,7 +212,7 @@ let gRGBW = false, memu = 0; let busMA = 0; let sLC = 0, sPC = 0, sDI = 0, maxLC = 0; - const ablEN = d.Sf.ABL.checked; + const abl = d.Sf.ABL.checked; maxB = oMaxB; // TODO make sure we start with all possible buses let setPinConfig = (n,t) => { let p0d = "GPIO:"; @@ -249,12 +259,12 @@ var t = parseInt(s.value); memu += getMem(t, n); // calc memory setPinConfig(n,t); - gId("abl"+n).style.display = (!ablEN || isVir(t) || isAna(t)) ? "none" : "inline"; - if (change) { + gId("abl"+n).style.display = (!abl || !isDig(t)) ? "none" : "inline"; // show/hide individual ABL settings + if (change) { // did we change LED type? gId("rf"+n).checked = (gId("rf"+n).checked || t == 31); // LEDs require data in off state (mandatory for TM1814) if (isAna(t)) d.Sf["LC"+n].value = 1; // for sanity change analog count just to 1 LED - d.Sf["LA"+n].min = (isVir(t) || isAna(t)) ? 0 : 1; - d.Sf["MA"+n].min = (isVir(t) || isAna(t)) ? 0 : 250; + d.Sf["LA"+n].min = (!isDig(t) || !abl) ? 0 : 1; // set minimum value for LED mA + d.Sf["MA"+n].min = (!isDig(t)) ? 0 : 250; // set minimum value for PSU mA } gId("rf"+n).onclick = mustR(t) ? (()=>{return false}) : (()=>{}); // prevent change change of "Refresh" checkmark when mandatory gRGBW |= hasW(t); // RGBW checkbox @@ -294,7 +304,7 @@ if (s+c > sLC) sLC = s+c; //update total count if (c > maxLC) maxLC = c; //max per output if (!isVir(t)) sPC += c; //virtual out busses do not count towards physical LEDs - if (!(isVir(t) || isAna(t))) { + if (isDig(t)) { sDI += c; // summarize digital LED count let maPL = parseInt(d.Sf["LA"+n].value); if (maPL == 255) maPL = 12; From 5975b9125f9e5b993a1e13b97bf2972bd0137079 Mon Sep 17 00:00:00 2001 From: PaoloTK Date: Sun, 6 Oct 2024 22:56:30 +0200 Subject: [PATCH 107/145] add autosegment outputs compile flag --- wled00/FX.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/wled00/FX.h b/wled00/FX.h index 3c28274d60..1d3ce1f933 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -720,7 +720,11 @@ class WS2812FX { // 96 bytes #ifndef WLED_DISABLE_2D panels(1), #endif +#ifdef WLED_AUTOSEGMENT_OUTPUTS + autoSegments(true), +#else autoSegments(false), +#endif correctWB(false), cctFromRgb(false), // semi-private (just obscured) used in effect functions through macros From 488974dd3e969fd8a723aa709b5944fc2308c0ea Mon Sep 17 00:00:00 2001 From: PaoloTK Date: Mon, 7 Oct 2024 10:39:45 +0200 Subject: [PATCH 108/145] change flag --- wled00/FX.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/FX.h b/wled00/FX.h index 1d3ce1f933..f16f07924f 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -720,7 +720,7 @@ class WS2812FX { // 96 bytes #ifndef WLED_DISABLE_2D panels(1), #endif -#ifdef WLED_AUTOSEGMENT_OUTPUTS +#ifdef WLED_AUTOSEGMENTS autoSegments(true), #else autoSegments(false), From 5e9a46d54d86c2f08b65b2152cab194794030553 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Kristan?= Date: Mon, 7 Oct 2024 17:15:35 +0200 Subject: [PATCH 109/145] Fix for #4154 --- wled00/json.cpp | 1 - wled00/presets.cpp | 4 ++-- wled00/wled.h | 2 -- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/wled00/json.cpp b/wled00/json.cpp index 0df7294c85..c877d1f3b7 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -465,7 +465,6 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId) ps = presetCycCurr; if (root["win"].isNull() && getVal(root["ps"], &ps, 0, 0) && ps > 0 && ps < 251 && ps != currentPreset) { // b) preset ID only or preset that does not change state (use embedded cycling limits if they exist in getVal()) - presetCycCurr = ps; applyPreset(ps, callMode); // async load from file system (only preset ID was specified) return stateResponse; } diff --git a/wled00/presets.cpp b/wled00/presets.cpp index 20edfd91e8..2749d46772 100644 --- a/wled00/presets.cpp +++ b/wled00/presets.cpp @@ -118,7 +118,7 @@ void initPresetsFile() bool applyPresetFromPlaylist(byte index) { DEBUG_PRINTF_P(PSTR("Request to apply preset: %d\n"), index); - presetToApply = index; + presetToApply = presetCycCurr = index; callModeToApply = CALL_MODE_DIRECT_CHANGE; return true; } @@ -127,7 +127,7 @@ bool applyPreset(byte index, byte callMode) { unloadPlaylist(); // applying a preset unloads the playlist (#3827) DEBUG_PRINTF_P(PSTR("Request to apply preset: %u\n"), index); - presetToApply = index; + presetToApply = presetCycCurr = index; callModeToApply = callMode; return true; } diff --git a/wled00/wled.h b/wled00/wled.h index 052f29b29f..173bd65548 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -316,8 +316,6 @@ WLED_GLOBAL bool rlyOpenDrain _INIT(RLYODRAIN); constexpr uint8_t hardwareTX = 1; #endif -//WLED_GLOBAL byte presetToApply _INIT(0); - WLED_GLOBAL char ntpServerName[33] _INIT("0.wled.pool.ntp.org"); // NTP server to use // WiFi CONFIG (all these can be changed via web UI, no need to set them here) From 7deea9eb75e651b19c04217c5fabde3f544c542b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Kristan?= Date: Mon, 7 Oct 2024 17:52:36 +0200 Subject: [PATCH 110/145] Minor button & rover CSS tweak. --- wled00/data/index.css | 12 ++++++++---- wled00/data/index.htm | 14 +++++++------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/wled00/data/index.css b/wled00/data/index.css index c4e85f73f2..6f465e4072 100644 --- a/wled00/data/index.css +++ b/wled00/data/index.css @@ -144,7 +144,7 @@ button { } .huge { - font-size: 42px; + font-size: 60px !important; } .segt, .plentry TABLE { @@ -584,6 +584,10 @@ button { z-index: 3; } +#rover .ibtn { + margin: 5px; +} + #ndlt { margin: 12px 0; } @@ -624,7 +628,7 @@ button { padding-bottom: 8px; } -.infobtn { +#info .ibtn { margin: 5px; } @@ -848,7 +852,7 @@ input[type=range]::-moz-range-thumb { width: 135px; } -#nodes .infobtn { +#nodes .ibtn { margin: 0; } @@ -1524,7 +1528,7 @@ dialog { #info table .btn, #nodes table .btn { width: 200px; } - #info .infobtn, #nodes .infobtn { + #info .ibtn, #nodes .ibtn { width: 145px; } #info div, #nodes div, #nodes a.btn { diff --git a/wled00/data/index.htm b/wled00/data/index.htm index 86b3b18796..e74ea00764 100644 --- a/wled00/data/index.htm +++ b/wled00/data/index.htm @@ -304,10 +304,10 @@
Loading...

- - - - + + + +

Made with ❤︎ by Aircoookie and the WLED community @@ -318,7 +318,7 @@
WLED instances
Loading...
- +
@@ -331,8 +331,8 @@
?


To use built-in effects, use an override button below.
You can return to realtime mode by pressing the star in the top left corner.
- -
+ +
For best performance, it is recommended to turn off the streaming source when not in use. From a60231ba59f285617086e501c27546beb5e998eb Mon Sep 17 00:00:00 2001 From: maxi4329 <84231420+maxi4329@users.noreply.github.com> Date: Wed, 9 Oct 2024 22:10:59 +0200 Subject: [PATCH 111/145] Fixed the positioning of the "Download the latest binary" button (#4184) * fixed the positioning of the download button * fixed space after "Download the latest binary:" disapering after building * fixed typo --------- Co-authored-by: maxi4329 --- wled00/data/update.htm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/wled00/data/update.htm b/wled00/data/update.htm index f157f98d88..b68645a527 100644 --- a/wled00/data/update.htm +++ b/wled00/data/update.htm @@ -17,7 +17,8 @@

WLED Software Update

Installed version: ##VERSION##
- Download the latest binary: + Download the latest binary: 


From 37f32ab197dac8399e2f2b302992344071094e7c Mon Sep 17 00:00:00 2001 From: Luis <84397555+LuisFadini@users.noreply.github.com> Date: Sat, 12 Oct 2024 10:56:40 -0300 Subject: [PATCH 112/145] Added BRT timezone --- wled00/data/settings_time.htm | 1 + wled00/ntp.cpp | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/wled00/data/settings_time.htm b/wled00/data/settings_time.htm index 52f79eb7df..df054f4174 100644 --- a/wled00/data/settings_time.htm +++ b/wled00/data/settings_time.htm @@ -156,6 +156,7 @@

Time setup

+
UTC offset: seconds (max. 18 hours)
Current local time is unknown.
diff --git a/wled00/ntp.cpp b/wled00/ntp.cpp index 7b7dac96e2..8d44e634ec 100644 --- a/wled00/ntp.cpp +++ b/wled00/ntp.cpp @@ -36,8 +36,9 @@ Timezone* tz; #define TZ_ANCHORAGE 20 #define TZ_MX_CENTRAL 21 #define TZ_PAKISTAN 22 +#define TZ_BRASILIA 23 -#define TZ_COUNT 23 +#define TZ_COUNT 24 #define TZ_INIT 255 byte tzCurrent = TZ_INIT; //uninitialized @@ -135,6 +136,10 @@ static const std::pair TZ_TABLE[] PROGMEM = { /* TZ_PAKISTAN */ { {Last, Sun, Mar, 1, 300}, //Pakistan Standard Time = UTC + 5 hours {Last, Sun, Mar, 1, 300} + }, + /* TZ_BRASILIA */ { + {Last, Sun, Mar, 1, -180}, //Brasília Standard Time = UTC - 3 hours + {Last, Sun, Mar, 1, -180} } }; From 49f044ecde8119d03d78fe167f375e71eabf6150 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Kristan?= Date: Sun, 13 Oct 2024 10:43:56 +0200 Subject: [PATCH 113/145] Better fix for #4154 --- wled00/json.cpp | 19 ++++++++++++------- wled00/presets.cpp | 4 ++-- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/wled00/json.cpp b/wled00/json.cpp index c877d1f3b7..06eb3015e5 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -454,20 +454,25 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId) handleSet(nullptr, apireq, false); // may set stateChanged } - // applying preset (2 cases: a) API call includes all preset values ("pd"), b) API only specifies preset ID ("ps")) + // Applying preset from JSON API has 2 cases: a) "pd" AKA "preset direct" and b) "ps" AKA "preset select" + // a) "preset direct" can only be an integer value representing preset ID. "preset direct" assumes JSON API contains the rest of preset content (i.e. from UI call) + // "preset direct" JSON can contain "ps" API (i.e. call from UI to cycle presets) in such case stateChanged has to be false (i.e. no "win" or "seg" API) + // b) "preset select" can be cycling ("1~5~""), random ("r" or "1~5r"), ID, etc. value allowed from JSON API. This type of call assumes no state changing content in API call byte presetToRestore = 0; - // a) already applied preset content (requires "seg" or "win" but will ignore the rest) if (!root[F("pd")].isNull() && stateChanged) { + // a) already applied preset content (requires "seg" or "win" but will ignore the rest) currentPreset = root[F("pd")] | currentPreset; - if (root["win"].isNull()) presetCycCurr = currentPreset; // otherwise it was set in handleSet() [set.cpp] + if (root["win"].isNull()) presetCycCurr = currentPreset; // otherwise presetCycCurr was set in handleSet() [set.cpp] presetToRestore = currentPreset; // stateUpdated() will clear the preset, so we need to restore it after + DEBUG_PRINTF_P(PSTR("Preset direct: %d\n"), currentPreset); } else if (!root["ps"].isNull()) { - ps = presetCycCurr; - if (root["win"].isNull() && getVal(root["ps"], &ps, 0, 0) && ps > 0 && ps < 251 && ps != currentPreset) { + // we have "ps" call (i.e. from button or external API call) or "pd" that includes "ps" (i.e. from UI call) + if (root["win"].isNull() && getVal(root["ps"], &presetCycCurr, 0, 0) && presetCycCurr > 0 && presetCycCurr < 251 && presetCycCurr != currentPreset) { + DEBUG_PRINTF_P(PSTR("Preset select: %d\n"), presetCycCurr); // b) preset ID only or preset that does not change state (use embedded cycling limits if they exist in getVal()) - applyPreset(ps, callMode); // async load from file system (only preset ID was specified) + applyPreset(presetCycCurr, callMode); // async load from file system (only preset ID was specified) return stateResponse; - } + } else presetCycCurr = currentPreset; // restore presetCycCurr } JsonObject playlist = root[F("playlist")]; diff --git a/wled00/presets.cpp b/wled00/presets.cpp index 2749d46772..20edfd91e8 100644 --- a/wled00/presets.cpp +++ b/wled00/presets.cpp @@ -118,7 +118,7 @@ void initPresetsFile() bool applyPresetFromPlaylist(byte index) { DEBUG_PRINTF_P(PSTR("Request to apply preset: %d\n"), index); - presetToApply = presetCycCurr = index; + presetToApply = index; callModeToApply = CALL_MODE_DIRECT_CHANGE; return true; } @@ -127,7 +127,7 @@ bool applyPreset(byte index, byte callMode) { unloadPlaylist(); // applying a preset unloads the playlist (#3827) DEBUG_PRINTF_P(PSTR("Request to apply preset: %u\n"), index); - presetToApply = presetCycCurr = index; + presetToApply = index; callModeToApply = callMode; return true; } From 01e07ca0bc0157bcb279f7715c33916e39d7e0be Mon Sep 17 00:00:00 2001 From: AlDIY <87589371+dosipod@users.noreply.github.com> Date: Sun, 13 Oct 2024 20:34:18 +0300 Subject: [PATCH 114/145] Update xml.cpp --- wled00/xml.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/xml.cpp b/wled00/xml.cpp index 424842a1d0..1ac22c9ce8 100644 --- a/wled00/xml.cpp +++ b/wled00/xml.cpp @@ -172,7 +172,7 @@ void getSettingsJS(byte subPage, Print& settingsScript) char fpass[l+1]; //fill password field with *** fpass[l] = 0; memset(fpass,'*',l); - settingsScript.printf_P(PSTR("addWiFi(\"%s\",\",%s\",0x%X,0x%X,0x%X);"), + settingsScript.printf_P(PSTR("addWiFi(\"%s\",\"%s\",0x%X,0x%X,0x%X);"), multiWiFi[n].clientSSID, fpass, (uint32_t) multiWiFi[n].staticIP, // explicit cast required as this is a struct From a0e81da8c5327e2083d616c5b7d45031b09a27d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Kristan?= Date: Mon, 14 Oct 2024 20:13:59 +0200 Subject: [PATCH 115/145] WLED 0.15.0-b6 release (#4180) * modified Improv chip & version handling * Update build and changelog --- CHANGELOG.md | 17 +++++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- wled00/improv.cpp | 30 ++++++++++++++---------------- wled00/wled.h | 4 ++-- 5 files changed, 36 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e377418e30..4dad83d9b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,26 @@ ## WLED changelog +#### Build 2410140 +- WLED 0.15.0-b6 release +- Added BRT timezone (#4188 by @LuisFadini) +- Fixed the positioning of the "Download the latest binary" button (#4184 by @maxi4329) +- Add WLED_AUTOSEGMENTS compile flag (#4183 by @PaoloTK) +- New 512kB FS parition map for 4MB devices +- Internal API change: Static PinManager & UsermodManager +- Change in Improv chip ID and version generation +- Various optimisations, bugfixes and enhancements (#4005, #4174 & #4175 by @Xevel, #4180, #4168, #4154, #4189 by @dosipod) + +#### Build 2409170 +- UI: Introduce common.js in settings pages (size optimisation) +- Add the ability to toggle the reception of palette synchronizations (#4137 by @felddy) +- Usermod/FX: Temperature usermod added Temperature effect (example usermod effect by @blazoncek) +- Fix AsyncWebServer version pin + #### Build 2409140 - Configure different kinds of busses at compile (#4107 by @PaoloTK) - BREAKING: removes LEDPIN and DEFAULT_LED_TYPE compile overrides - Fetch LED types from Bus classes (dynamic UI) (#4129 by @netmindz, @blazoncek, @dedehai) +- Temperature usermod: update OneWire to 2.3.8 (#4131 by @iammattcoleman) #### Build 2409100 - WLED 0.15.0-b5 release diff --git a/package-lock.json b/package-lock.json index 415d881519..85ee1df0fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "wled", - "version": "0.15.0-b5", + "version": "0.15.0-b6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "wled", - "version": "0.15.0-b5", + "version": "0.15.0-b6", "license": "ISC", "dependencies": { "clean-css": "^5.3.3", diff --git a/package.json b/package.json index 721455bff3..d76d87687d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wled", - "version": "0.15.0-b5", + "version": "0.15.0-b6", "description": "Tools for WLED project", "main": "tools/cdata.js", "directories": { diff --git a/wled00/improv.cpp b/wled00/improv.cpp index 31547f86c8..197148b2bf 100644 --- a/wled00/improv.cpp +++ b/wled00/improv.cpp @@ -194,24 +194,22 @@ void sendImprovIPRPCResult(ImprovRPCType type) { } void sendImprovInfoResponse() { - const char* bString = - #ifdef ESP8266 - "esp8266" - #elif CONFIG_IDF_TARGET_ESP32C3 - "esp32-c3" - #elif CONFIG_IDF_TARGET_ESP32S2 - "esp32-s2" - #elif CONFIG_IDF_TARGET_ESP32S3 - "esp32-s3"; - #else // ESP32 - "esp32"; - #endif - ; - + char bString[32]; + #ifdef ESP8266 + strcpy(bString, "esp8266"); + #else // ESP32 + strncpy(bString, ESP.getChipModel(), 31); + #if CONFIG_IDF_TARGET_ESP32 + bString[5] = '\0'; // disregard chip revision for classic ESP32 + #else + bString[31] = '\0'; // just in case + #endif + strlwr(bString); + #endif //Use serverDescription if it has been changed from the default "WLED", else mDNS name bool useMdnsName = (strcmp(serverDescription, "WLED") == 0 && strlen(cmDNS) > 0); - char vString[20]; - sprintf_P(vString, PSTR("0.15.0-b5/%i"), VERSION); + char vString[32]; + sprintf_P(vString, PSTR("%s/%i"), versionString, VERSION); const char *str[4] = {"WLED", vString, bString, useMdnsName ? cmDNS : serverDescription}; sendImprovRPCResult(ImprovRPCType::Request_Info, 4, str); diff --git a/wled00/wled.h b/wled00/wled.h index 173bd65548..bc525cd6fa 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -3,12 +3,12 @@ /* Main sketch, global variable declarations @title WLED project sketch - @version 0.15.0-b5 + @version 0.15.0-b6 @author Christian Schwinne */ // version code in format yymmddb (b = daily build) -#define VERSION 2409170 +#define VERSION 2410140 //uncomment this if you have a "my_config.h" file you'd like to use //#define WLED_USE_MY_CONFIG From 44e28f96e0af0c78cb1b902a45b6332dcacd10e0 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 15 Oct 2024 13:16:18 +0200 Subject: [PATCH 116/145] Fix for Octopus on ESP32 C3 Apparently the C3 can not convert negative floats to uint8_t directly, casting it into an int first fixes it. --- wled00/FX.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index e7429d19f1..c6c8222be6 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7749,7 +7749,7 @@ uint16_t mode_2Doctopus() { const int C_Y = (rows / 2) + ((SEGMENT.custom2 - 128)*rows)/255; for (int x = 0; x < cols; x++) { for (int y = 0; y < rows; y++) { - rMap[XY(x, y)].angle = 40.7436f * atan2f((y - C_Y), (x - C_X)); // avoid 128*atan2()/PI + rMap[XY(x, y)].angle = int(40.7436f * atan2f((y - C_Y), (x - C_X))); // avoid 128*atan2()/PI rMap[XY(x, y)].radius = hypotf((x - C_X), (y - C_Y)) * mapp; //thanks Sutaburosu } } From e9d2182390d43d7dd25492f6555d082280e79b3b Mon Sep 17 00:00:00 2001 From: Christian Schwinne Date: Wed, 16 Oct 2024 00:07:19 +0200 Subject: [PATCH 117/145] Re-license the WLED project from MIT to EUPL (#4194) --- LICENSE | 315 ++++++++++++++++++++++++++++++++--- readme.md | 4 +- wled00/FX.cpp | 20 +-- wled00/FX.h | 20 +-- wled00/FX_2Dfcn.cpp | 19 +-- wled00/FX_fcn.cpp | 20 +-- wled00/data/settings_sec.htm | 2 +- 7 files changed, 308 insertions(+), 92 deletions(-) diff --git a/LICENSE b/LICENSE index 69325d21c8..cca21c008b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,294 @@ -MIT License - -Copyright (c) 2016 Christian Schwinne - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +Copyright (c) 2016-present Christian Schwinne and individual WLED contributors +Licensed under the EUPL v. 1.2 or later + + EUROPEAN UNION PUBLIC LICENCE v. 1.2 + EUPL © the European Union 2007, 2016 + +This European Union Public Licence (the ‘EUPL’) applies to the Work (as +defined below) which is provided under the terms of this Licence. Any use of +the Work, other than as authorised under this Licence is prohibited (to the +extent such use is covered by a right of the copyright holder of the Work). + +The Work is provided under the terms of this Licence when the Licensor (as +defined below) has placed the following notice immediately following the +copyright notice for the Work: + + Licensed under the EUPL + +or has expressed by any other means his willingness to license under the EUPL. + +1. Definitions + +In this Licence, the following terms have the following meaning: + +- ‘The Licence’: this Licence. + +- ‘The Original Work’: the work or software distributed or communicated by the + Licensor under this Licence, available as Source Code and also as Executable + Code as the case may be. + +- ‘Derivative Works’: the works or software that could be created by the + Licensee, based upon the Original Work or modifications thereof. This + Licence does not define the extent of modification or dependence on the + Original Work required in order to classify a work as a Derivative Work; + this extent is determined by copyright law applicable in the country + mentioned in Article 15. + +- ‘The Work’: the Original Work or its Derivative Works. + +- ‘The Source Code’: the human-readable form of the Work which is the most + convenient for people to study and modify. + +- ‘The Executable Code’: any code which has generally been compiled and which + is meant to be interpreted by a computer as a program. + +- ‘The Licensor’: the natural or legal person that distributes or communicates + the Work under the Licence. + +- ‘Contributor(s)’: any natural or legal person who modifies the Work under + the Licence, or otherwise contributes to the creation of a Derivative Work. + +- ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of + the Work under the terms of the Licence. + +- ‘Distribution’ or ‘Communication’: any act of selling, giving, lending, + renting, distributing, communicating, transmitting, or otherwise making + available, online or offline, copies of the Work or providing access to its + essential functionalities at the disposal of any other natural or legal + person. + +2. Scope of the rights granted by the Licence + +The Licensor hereby grants You a worldwide, royalty-free, non-exclusive, +sublicensable licence to do the following, for the duration of copyright +vested in the Original Work: + +- use the Work in any circumstance and for all usage, +- reproduce the Work, +- modify the Work, and make Derivative Works based upon the Work, +- communicate to the public, including the right to make available or display + the Work or copies thereof to the public and perform publicly, as the case + may be, the Work, +- distribute the Work or copies thereof, +- lend and rent the Work or copies thereof, +- sublicense rights in the Work or copies thereof. + +Those rights can be exercised on any media, supports and formats, whether now +known or later invented, as far as the applicable law permits so. + +In the countries where moral rights apply, the Licensor waives his right to +exercise his moral right to the extent allowed by law in order to make +effective the licence of the economic rights here above listed. + +The Licensor grants to the Licensee royalty-free, non-exclusive usage rights +to any patents held by the Licensor, to the extent necessary to make use of +the rights granted on the Work under this Licence. + +3. Communication of the Source Code + +The Licensor may provide the Work either in its Source Code form, or as +Executable Code. If the Work is provided as Executable Code, the Licensor +provides in addition a machine-readable copy of the Source Code of the Work +along with each copy of the Work that the Licensor distributes or indicates, +in a notice following the copyright notice attached to the Work, a repository +where the Source Code is easily and freely accessible for as long as the +Licensor continues to distribute or communicate the Work. + +4. Limitations on copyright + +Nothing in this Licence is intended to deprive the Licensee of the benefits +from any exception or limitation to the exclusive rights of the rights owners +in the Work, of the exhaustion of those rights or of other applicable +limitations thereto. + +5. Obligations of the Licensee + +The grant of the rights mentioned above is subject to some restrictions and +obligations imposed on the Licensee. Those obligations are the following: + +Attribution right: The Licensee shall keep intact all copyright, patent or +trademarks notices and all notices that refer to the Licence and to the +disclaimer of warranties. The Licensee must include a copy of such notices and +a copy of the Licence with every copy of the Work he/she distributes or +communicates. The Licensee must cause any Derivative Work to carry prominent +notices stating that the Work has been modified and the date of modification. + +Copyleft clause: If the Licensee distributes or communicates copies of the +Original Works or Derivative Works, this Distribution or Communication will be +done under the terms of this Licence or of a later version of this Licence +unless the Original Work is expressly distributed only under this version of +the Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee +(becoming Licensor) cannot offer or impose any additional terms or conditions +on the Work or Derivative Work that alter or restrict the terms of the +Licence. + +Compatibility clause: If the Licensee Distributes or Communicates Derivative +Works or copies thereof based upon both the Work and another work licensed +under a Compatible Licence, this Distribution or Communication can be done +under the terms of this Compatible Licence. For the sake of this clause, +‘Compatible Licence’ refers to the licences listed in the appendix attached to +this Licence. Should the Licensee's obligations under the Compatible Licence +conflict with his/her obligations under this Licence, the obligations of the +Compatible Licence shall prevail. + +Provision of Source Code: When distributing or communicating copies of the +Work, the Licensee will provide a machine-readable copy of the Source Code or +indicate a repository where this Source will be easily and freely available +for as long as the Licensee continues to distribute or communicate the Work. + +Legal Protection: This Licence does not grant permission to use the trade +names, trademarks, service marks, or names of the Licensor, except as required +for reasonable and customary use in describing the origin of the Work and +reproducing the content of the copyright notice. + +6. Chain of Authorship + +The original Licensor warrants that the copyright in the Original Work granted +hereunder is owned by him/her or licensed to him/her and that he/she has the +power and authority to grant the Licence. + +Each Contributor warrants that the copyright in the modifications he/she +brings to the Work are owned by him/her or licensed to him/her and that he/she +has the power and authority to grant the Licence. + +Each time You accept the Licence, the original Licensor and subsequent +Contributors grant You a licence to their contributions to the Work, under the +terms of this Licence. + +7. Disclaimer of Warranty + +The Work is a work in progress, which is continuously improved by numerous +Contributors. It is not a finished work and may therefore contain defects or +‘bugs’ inherent to this type of development. + +For the above reason, the Work is provided under the Licence on an ‘as is’ +basis and without warranties of any kind concerning the Work, including +without limitation merchantability, fitness for a particular purpose, absence +of defects or errors, accuracy, non-infringement of intellectual property +rights other than copyright as stated in Article 6 of this Licence. + +This disclaimer of warranty is an essential part of the Licence and a +condition for the grant of any rights to the Work. + +8. Disclaimer of Liability + +Except in the cases of wilful misconduct or damages directly caused to natural +persons, the Licensor will in no event be liable for any direct or indirect, +material or moral, damages of any kind, arising out of the Licence or of the +use of the Work, including without limitation, damages for loss of goodwill, +work stoppage, computer failure or malfunction, loss of data or any commercial +damage, even if the Licensor has been advised of the possibility of such +damage. However, the Licensor will be liable under statutory product liability +laws as far such laws apply to the Work. + +9. Additional agreements + +While distributing the Work, You may choose to conclude an additional +agreement, defining obligations or services consistent with this Licence. +However, if accepting obligations, You may act only on your own behalf and on +your sole responsibility, not on behalf of the original Licensor or any other +Contributor, and only if You agree to indemnify, defend, and hold each +Contributor harmless for any liability incurred by, or claims asserted against +such Contributor by the fact You have accepted any warranty or additional +liability. + +10. Acceptance of the Licence + +The provisions of this Licence can be accepted by clicking on an icon ‘I +agree’ placed under the bottom of a window displaying the text of this Licence +or by affirming consent in any other similar way, in accordance with the rules +of applicable law. Clicking on that icon indicates your clear and irrevocable +acceptance of this Licence and all of its terms and conditions. + +Similarly, you irrevocably accept this Licence and all of its terms and +conditions by exercising any rights granted to You by Article 2 of this +Licence, such as the use of the Work, the creation by You of a Derivative Work +or the Distribution or Communication by You of the Work or copies thereof. + +11. Information to the public + +In case of any Distribution or Communication of the Work by means of +electronic communication by You (for example, by offering to download the Work +from a remote location) the distribution channel or media (for example, a +website) must at least provide to the public the information requested by the +applicable law regarding the Licensor, the Licence and the way it may be +accessible, concluded, stored and reproduced by the Licensee. + +12. Termination of the Licence + +The Licence and the rights granted hereunder will terminate automatically upon +any breach by the Licensee of the terms of the Licence. + +Such a termination will not terminate the licences of any person who has +received the Work from the Licensee under the Licence, provided such persons +remain in full compliance with the Licence. + +13. Miscellaneous + +Without prejudice of Article 9 above, the Licence represents the complete +agreement between the Parties as to the Work. + +If any provision of the Licence is invalid or unenforceable under applicable +law, this will not affect the validity or enforceability of the Licence as a +whole. Such provision will be construed or reformed so as necessary to make it +valid and enforceable. + +The European Commission may publish other linguistic versions or new versions +of this Licence or updated versions of the Appendix, so far this is required +and reasonable, without reducing the scope of the rights granted by the +Licence. New versions of the Licence will be published with a unique version +number. + +All linguistic versions of this Licence, approved by the European Commission, +have identical value. Parties can take advantage of the linguistic version of +their choice. + +14. Jurisdiction + +Without prejudice to specific agreement between parties, + +- any litigation resulting from the interpretation of this License, arising + between the European Union institutions, bodies, offices or agencies, as a + Licensor, and any Licensee, will be subject to the jurisdiction of the Court + of Justice of the European Union, as laid down in article 272 of the Treaty + on the Functioning of the European Union, + +- any litigation arising between other parties and resulting from the + interpretation of this License, will be subject to the exclusive + jurisdiction of the competent court where the Licensor resides or conducts + its primary business. + +15. Applicable Law + +Without prejudice to specific agreement between parties, + +- this Licence shall be governed by the law of the European Union Member State + where the Licensor has his seat, resides or has his registered office, + +- this licence shall be governed by Belgian law if the Licensor has no seat, + residence or registered office inside a European Union Member State. + +Appendix + +‘Compatible Licences’ according to Article 5 EUPL are: + +- GNU General Public License (GPL) v. 2, v. 3 +- GNU Affero General Public License (AGPL) v. 3 +- Open Software License (OSL) v. 2.1, v. 3.0 +- Eclipse Public License (EPL) v. 1.0 +- CeCILL v. 2.0, v. 2.1 +- Mozilla Public Licence (MPL) v. 2 +- GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3 +- Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for + works other than software +- European Union Public Licence (EUPL) v. 1.1, v. 1.2 +- Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong + Reciprocity (LiLiQ-R+). + +The European Commission may update this Appendix to later versions of the +above licences without producing a new version of the EUPL, as long as they +provide the rights granted in Article 2 of this Licence and protect the +covered Source Code from exclusive appropriation. + +All other changes or additions to this Appendix require the production of a +new EUPL version. \ No newline at end of file diff --git a/readme.md b/readme.md index 11c1733f87..80256560af 100644 --- a/readme.md +++ b/readme.md @@ -61,7 +61,7 @@ See [here](https://kno.wled.ge/basics/compatible-hardware)! ## ✌️ Other -Licensed under the MIT license +Licensed under the EUPL v1.2 license Credits [here](https://kno.wled.ge/about/contributors/)! Join the Discord server to discuss everything about WLED! @@ -80,5 +80,5 @@ If WLED really brightens up your day, you can [![](https://img.shields.io/badge/ If you are prone to photosensitive epilepsy, we recommended you do **not** use this software. If you still want to try, don't use strobe, lighting or noise modes or high effect speed settings. -As per the MIT license, I assume no liability for any damage to you or any other person or equipment. +As per the EUPL license, I assume no liability for any damage to you or any other person or equipment. diff --git a/wled00/FX.cpp b/wled00/FX.cpp index c6c8222be6..d4b83de6cf 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -2,24 +2,10 @@ WS2812FX.cpp contains all effect methods Harm Aldick - 2016 www.aldick.org - LICENSE - The MIT License (MIT) + Copyright (c) 2016 Harm Aldick - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. + Licensed under the EUPL v. 1.2 or later + Adapted from code originally licensed under the MIT license Modified heavily for WLED */ diff --git a/wled00/FX.h b/wled00/FX.h index f16f07924f..385c524769 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -2,24 +2,10 @@ WS2812FX.h - Library for WS2812 LED effects. Harm Aldick - 2016 www.aldick.org - LICENSE - The MIT License (MIT) + Copyright (c) 2016 Harm Aldick - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. + Licensed under the EUPL v. 1.2 or later + Adapted from code originally licensed under the MIT license Modified for WLED */ diff --git a/wled00/FX_2Dfcn.cpp b/wled00/FX_2Dfcn.cpp index 0f66905496..e38602ebc0 100644 --- a/wled00/FX_2Dfcn.cpp +++ b/wled00/FX_2Dfcn.cpp @@ -1,24 +1,9 @@ /* FX_2Dfcn.cpp contains all 2D utility functions - LICENSE - The MIT License (MIT) Copyright (c) 2022 Blaz Kristan (https://blaz.at/home) - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. + Licensed under the EUPL v. 1.2 or later + Adapted from code originally licensed under the MIT license Parts of the code adapted from WLED Sound Reactive */ diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 1bbfa365bd..79189ef57a 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -2,24 +2,10 @@ WS2812FX_fcn.cpp contains all utility functions Harm Aldick - 2016 www.aldick.org - LICENSE - The MIT License (MIT) + Copyright (c) 2016 Harm Aldick - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. + Licensed under the EUPL v. 1.2 or later + Adapted from code originally licensed under the MIT license Modified heavily for WLED */ diff --git a/wled00/data/settings_sec.htm b/wled00/data/settings_sec.htm index ce9bd8aa32..fa75882c0e 100644 --- a/wled00/data/settings_sec.htm +++ b/wled00/data/settings_sec.htm @@ -72,7 +72,7 @@

About

Contributors, dependencies and special thanks
A huge thank you to everyone who helped me create WLED!

(c) 2016-2024 Christian Schwinne
- Licensed under the MIT license

+ Licensed under the EUPL v1.2 license

Server message: Response error!
From 0a97e28aab766ada3a7cf0fc2c9eb63a5f6afdf3 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Sat, 19 Oct 2024 18:43:10 +0200 Subject: [PATCH 118/145] bugfix: prevent preset loading from resetting other errors without this fix, any not-yet reported error - like filesystem problems at startup, or out-of-memory - was rest by successfully loading a preset. --- wled00/presets.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/wled00/presets.cpp b/wled00/presets.cpp index 20edfd91e8..04474113d1 100644 --- a/wled00/presets.cpp +++ b/wled00/presets.cpp @@ -143,6 +143,7 @@ void applyPresetWithFallback(uint8_t index, uint8_t callMode, uint8_t effectID, void handlePresets() { + byte presetErrFlag = ERR_NONE; if (presetToSave) { strip.suspend(); doSaveState(); @@ -166,14 +167,16 @@ void handlePresets() #ifdef ARDUINO_ARCH_ESP32 if (tmpPreset==255 && tmpRAMbuffer!=nullptr) { deserializeJson(*pDoc,tmpRAMbuffer); - errorFlag = ERR_NONE; } else #endif { - errorFlag = readObjectFromFileUsingId(getPresetsFileName(tmpPreset < 255), tmpPreset, pDoc) ? ERR_NONE : ERR_FS_PLOAD; + presetErrFlag = readObjectFromFileUsingId(getPresetsFileName(tmpPreset < 255), tmpPreset, pDoc) ? ERR_NONE : ERR_FS_PLOAD; } fdo = pDoc->as(); + // only reset errorflag if previous error was preset-related + if ((errorFlag == ERR_NONE) || (errorFlag == ERR_FS_PLOAD)) errorFlag = presetErrFlag; + //HTTP API commands const char* httpwin = fdo["win"]; if (httpwin) { From 7db198909398feeab431031c0ae3f21e47266bd3 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Tue, 22 Oct 2024 14:42:48 +0200 Subject: [PATCH 119/145] fix major performance regression in ArduinoFFT since v2.0.0, we cannot override the internal sqrt function by #define --> moved to build_flags. Average FFT time on esp32 : 4.5ms --> 1.8ms --- platformio.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index 3005ba2208..b130b687c5 100644 --- a/platformio.ini +++ b/platformio.ini @@ -259,7 +259,8 @@ lib_deps = https://github.com/pbolduc/AsyncTCP.git @ 1.2.0 ${env.lib_deps} # additional build flags for audioreactive -AR_build_flags = -D USERMOD_AUDIOREACTIVE +AR_build_flags = -D USERMOD_AUDIOREACTIVE + -D sqrt_internal=sqrtf ;; -fsingle-precision-constant ;; forces ArduinoFFT to use float math (2x faster) AR_lib_deps = kosme/arduinoFFT @ 2.0.1 [esp32_idf_V4] From 01d43c69fb5d9fa616bc276039fc00c33742bad0 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Tue, 22 Oct 2024 14:45:32 +0200 Subject: [PATCH 120/145] AR memory optimization - part 1 allocating FFT buffers late makes up to 16Kb heap available when audioreactive is not enabled. Already tested in MM fork. --- usermods/audioreactive/audio_reactive.h | 30 +++++++++++++++---------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/usermods/audioreactive/audio_reactive.h b/usermods/audioreactive/audio_reactive.h index fde7afded0..1a91c333f5 100644 --- a/usermods/audioreactive/audio_reactive.h +++ b/usermods/audioreactive/audio_reactive.h @@ -191,8 +191,8 @@ constexpr uint16_t samplesFFT_2 = 256; // meaningfull part of FFT resul #define LOG_256 5.54517744f // log(256) // These are the input and output vectors. Input vectors receive computed results from FFT. -static float vReal[samplesFFT] = {0.0f}; // FFT sample inputs / freq output - these are our raw result bins -static float vImag[samplesFFT] = {0.0f}; // imaginary parts +static float* vReal = nullptr; // FFT sample inputs / freq output - these are our raw result bins +static float* vImag = nullptr; // imaginary parts // Create FFT object // lib_deps += https://github.com/kosme/arduinoFFT#develop @ 1.9.2 @@ -200,14 +200,9 @@ static float vImag[samplesFFT] = {0.0f}; // imaginary parts // #define FFT_SPEED_OVER_PRECISION // enables use of reciprocals (1/x etc) - not faster on ESP32 // #define FFT_SQRT_APPROXIMATION // enables "quake3" style inverse sqrt - slower on ESP32 // Below options are forcing ArduinoFFT to use sqrtf() instead of sqrt() -#define sqrt(x) sqrtf(x) // little hack that reduces FFT time by 10-50% on ESP32 -#define sqrt_internal sqrtf // see https://github.com/kosme/arduinoFFT/pull/83 - -#include - -/* Create FFT object with weighing factor storage */ -static ArduinoFFT FFT = ArduinoFFT( vReal, vImag, samplesFFT, SAMPLE_RATE, true); +// #define sqrt_internal sqrtf // see https://github.com/kosme/arduinoFFT/pull/83 - since v2.0.0 this must be done in build_flags +#include // FFT object is created in FFTcode // Helper functions // compute average of several FFT result bins @@ -226,6 +221,18 @@ void FFTcode(void * parameter) { DEBUGSR_PRINT("FFT started on core: "); DEBUGSR_PRINTLN(xPortGetCoreID()); + // allocate FFT buffers on first call + if (vReal == nullptr) vReal = (float*) calloc(sizeof(float), samplesFFT); + if (vImag == nullptr) vImag = (float*) calloc(sizeof(float), samplesFFT); + if ((vReal == nullptr) || (vImag == nullptr)) { + // something went wrong + if (vReal) free(vReal); vReal = nullptr; + if (vImag) free(vImag); vImag = nullptr; + return; + } + // Create FFT object with weighing factor storage + ArduinoFFT FFT = ArduinoFFT( vReal, vImag, samplesFFT, SAMPLE_RATE, true); + // see https://www.freertos.org/vtaskdelayuntil.html const TickType_t xFrequency = FFT_MIN_CYCLE * portTICK_PERIOD_MS; @@ -247,6 +254,7 @@ void FFTcode(void * parameter) // get a fresh batch of samples from I2S if (audioSource) audioSource->getSamples(vReal, samplesFFT); + memset(vImag, 0, samplesFFT * sizeof(float)); // set imaginary parts to 0 #if defined(WLED_DEBUG) || defined(SR_DEBUG) if (start < esp_timer_get_time()) { // filter out overflows @@ -265,8 +273,6 @@ void FFTcode(void * parameter) // find highest sample in the batch float maxSample = 0.0f; // max sample from FFT batch for (int i=0; i < samplesFFT; i++) { - // set imaginary parts to 0 - vImag[i] = 0; // pick our our current mic sample - we take the max value from all samples that go into FFT if ((vReal[i] <= (INT16_MAX - 1024)) && (vReal[i] >= (INT16_MIN + 1024))) //skip extreme values - normally these are artefacts if (fabsf((float)vReal[i]) > maxSample) maxSample = fabsf((float)vReal[i]); @@ -297,7 +303,7 @@ void FFTcode(void * parameter) #endif } else { // noise gate closed - only clear results as FFT was skipped. MIC samples are still valid when we do this. - memset(vReal, 0, sizeof(vReal)); + memset(vReal, 0, samplesFFT * sizeof(float)); FFT_MajorPeak = 1; FFT_Magnitude = 0.001; } From 26a47537f98f06dc9645e7eaf16016d349c2b297 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Tue, 22 Oct 2024 15:15:14 +0200 Subject: [PATCH 121/145] AR memory optimization - part 2 shorten strings in UI script - saves a few hundred bytes on RAM --- usermods/audioreactive/audio_reactive.h | 78 +++++++++++++------------ 1 file changed, 40 insertions(+), 38 deletions(-) diff --git a/usermods/audioreactive/audio_reactive.h b/usermods/audioreactive/audio_reactive.h index 1a91c333f5..ad449fc83a 100644 --- a/usermods/audioreactive/audio_reactive.h +++ b/usermods/audioreactive/audio_reactive.h @@ -1885,57 +1885,59 @@ class AudioReactive : public Usermod { } - void appendConfigData() override + void appendConfigData(Print& uiScript) override { -#ifdef ARDUINO_ARCH_ESP32 - oappend(SET_F("dd=addDropdown('AudioReactive','digitalmic:type');")); + uiScript.print(F("ux='AudioReactive';")); // ux = shortcut for Audioreactive - fingers crossed that "ux" isn't already used as JS var, html post parameter or css style +#ifdef ARDUINO_ARCH_ESP32 + uiScript.print(F("uxp=ux+':digitalmic:pin[]';")); // uxp = shortcut for AudioReactive:digitalmic:pin[] + uiScript.print(F("dd=addDropdown(ux,'digitalmic:type');")); #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) - oappend(SET_F("addOption(dd,'Generic Analog',0);")); + uiScript.print(F("addOption(dd,'Generic Analog',0);")); #endif - oappend(SET_F("addOption(dd,'Generic I2S',1);")); - oappend(SET_F("addOption(dd,'ES7243',2);")); - oappend(SET_F("addOption(dd,'SPH0654',3);")); - oappend(SET_F("addOption(dd,'Generic I2S with Mclk',4);")); + uiScript.print(F("addOption(dd,'Generic I2S',1);")); + uiScript.print(F("addOption(dd,'ES7243',2);")); + uiScript.print(F("addOption(dd,'SPH0654',3);")); + uiScript.print(F("addOption(dd,'Generic I2S with Mclk',4);")); #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) - oappend(SET_F("addOption(dd,'Generic I2S PDM',5);")); + uiScript.print(F("addOption(dd,'Generic I2S PDM',5);")); #endif - oappend(SET_F("addOption(dd,'ES8388',6);")); + uiScript.print(F("addOption(dd,'ES8388',6);")); - oappend(SET_F("dd=addDropdown('AudioReactive','config:AGC');")); - oappend(SET_F("addOption(dd,'Off',0);")); - oappend(SET_F("addOption(dd,'Normal',1);")); - oappend(SET_F("addOption(dd,'Vivid',2);")); - oappend(SET_F("addOption(dd,'Lazy',3);")); - - oappend(SET_F("dd=addDropdown('AudioReactive','dynamics:limiter');")); - oappend(SET_F("addOption(dd,'Off',0);")); - oappend(SET_F("addOption(dd,'On',1);")); - oappend(SET_F("addInfo('AudioReactive:dynamics:limiter',0,' On ');")); // 0 is field type, 1 is actual field - oappend(SET_F("addInfo('AudioReactive:dynamics:rise',1,'ms (♪ effects only)');")); - oappend(SET_F("addInfo('AudioReactive:dynamics:fall',1,'ms (♪ effects only)');")); - - oappend(SET_F("dd=addDropdown('AudioReactive','frequency:scale');")); - oappend(SET_F("addOption(dd,'None',0);")); - oappend(SET_F("addOption(dd,'Linear (Amplitude)',2);")); - oappend(SET_F("addOption(dd,'Square Root (Energy)',3);")); - oappend(SET_F("addOption(dd,'Logarithmic (Loudness)',1);")); + uiScript.print(F("dd=addDropdown(ux,'config:AGC');")); + uiScript.print(F("addOption(dd,'Off',0);")); + uiScript.print(F("addOption(dd,'Normal',1);")); + uiScript.print(F("addOption(dd,'Vivid',2);")); + uiScript.print(F("addOption(dd,'Lazy',3);")); + + uiScript.print(F("dd=addDropdown(ux,'dynamics:limiter');")); + uiScript.print(F("addOption(dd,'Off',0);")); + uiScript.print(F("addOption(dd,'On',1);")); + uiScript.print(F("addInfo(ux+':dynamics:limiter',0,' On ');")); // 0 is field type, 1 is actual field + uiScript.print(F("addInfo(ux+':dynamics:rise',1,'ms (♪ effects only)');")); + uiScript.print(F("addInfo(ux+':dynamics:fall',1,'ms (♪ effects only)');")); + + uiScript.print(F("dd=addDropdown(ux,'frequency:scale');")); + uiScript.print(F("addOption(dd,'None',0);")); + uiScript.print(F("addOption(dd,'Linear (Amplitude)',2);")); + uiScript.print(F("addOption(dd,'Square Root (Energy)',3);")); + uiScript.print(F("addOption(dd,'Logarithmic (Loudness)',1);")); #endif - oappend(SET_F("dd=addDropdown('AudioReactive','sync:mode');")); - oappend(SET_F("addOption(dd,'Off',0);")); + uiScript.print(F("dd=addDropdown(ux,'sync:mode');")); + uiScript.print(F("addOption(dd,'Off',0);")); #ifdef ARDUINO_ARCH_ESP32 - oappend(SET_F("addOption(dd,'Send',1);")); + uiScript.print(F("addOption(dd,'Send',1);")); #endif - oappend(SET_F("addOption(dd,'Receive',2);")); + uiScript.print(F("addOption(dd,'Receive',2);")); #ifdef ARDUINO_ARCH_ESP32 - oappend(SET_F("addInfo('AudioReactive:digitalmic:type',1,'requires reboot!');")); // 0 is field type, 1 is actual field - oappend(SET_F("addInfo('AudioReactive:digitalmic:pin[]',0,'sd/data/dout','I2S SD');")); - oappend(SET_F("addInfo('AudioReactive:digitalmic:pin[]',1,'ws/clk/lrck','I2S WS');")); - oappend(SET_F("addInfo('AudioReactive:digitalmic:pin[]',2,'sck/bclk','I2S SCK');")); + uiScript.print(F("addInfo(ux+':digitalmic:type',1,'requires reboot!');")); // 0 is field type, 1 is actual field + uiScript.print(F("addInfo(uxp,0,'sd/data/dout','I2S SD');")); + uiScript.print(F("addInfo(uxp,1,'ws/clk/lrck','I2S WS');")); + uiScript.print(F("addInfo(uxp,2,'sck/bclk','I2S SCK');")); #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) - oappend(SET_F("addInfo('AudioReactive:digitalmic:pin[]',3,'only use -1, 0, 1 or 3','I2S MCLK');")); + uiScript.print(F("addInfo(uxp,3,'only use -1, 0, 1 or 3','I2S MCLK');")); #else - oappend(SET_F("addInfo('AudioReactive:digitalmic:pin[]',3,'master clock','I2S MCLK');")); + uiScript.print(F("addInfo(uxp,3,'master clock','I2S MCLK');")); #endif #endif } From 6d1126b8aa123ebf351bb73b2618ccfcb9199682 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Tue, 22 Oct 2024 16:19:46 +0200 Subject: [PATCH 122/145] Update audioreactive readme.md added `-D sqrt_internal=sqrtf` -> needed for good performance --- usermods/audioreactive/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/usermods/audioreactive/readme.md b/usermods/audioreactive/readme.md index 4668ca8814..aad269c675 100644 --- a/usermods/audioreactive/readme.md +++ b/usermods/audioreactive/readme.md @@ -30,7 +30,7 @@ There are however plans to create a lightweight audioreactive for the 8266, with ### using latest _arduinoFFT_ library version 2.x The latest arduinoFFT release version should be used for audioreactive. -* `build_flags` = `-D USERMOD_AUDIOREACTIVE` +* `build_flags` = `-D USERMOD_AUDIOREACTIVE -D sqrt_internal=sqrtf` * `lib_deps`= `kosme/arduinoFFT @ 2.0.1` ## Configuration From 2a094883ad7b8d59630d88733c0df1ee43969811 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Fri, 27 Sep 2024 22:33:20 -0400 Subject: [PATCH 123/145] Better oappend shim on ESP8266 Detect IRAM pointers if we can't be sure. --- wled00/fcn_declare.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 71b00599cd..d44ed43a0d 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -324,6 +324,10 @@ class Usermod { protected: // Shim for oappend(), which used to exist in utils.cpp template static inline void oappend(const T& t) { oappend_shim->print(t); }; +#ifdef ESP8266 + // Handle print(PSTR()) without crashing by detecting PROGMEM strings + static void oappend(const char* c) { if ((intptr_t) c >= 0x40000000) oappend_shim->print(FPSTR(c)); else oappend_shim->print(c); }; +#endif }; class UsermodManager { From 2bb2caf2d2ae2d06d7dd45be6e898e3b731fbcf7 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Wed, 23 Oct 2024 19:47:44 -0400 Subject: [PATCH 124/145] Enable NON32XFER_HANDLER on ESP8266 This is a platform feature that asks forgiveness for PROGMEM misuse: it adds a handler such that incorrectly used PROGMEM will work without crashing, just really, *really* inefficiently. Given that most of our real-world use cases for PROGMEM strings are relatively infrequent text calls, we can err on the side of developer convenience and address performance problems if and when they arise. --- platformio.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/platformio.ini b/platformio.ini index b130b687c5..9628722aa4 100644 --- a/platformio.ini +++ b/platformio.ini @@ -197,6 +197,7 @@ build_flags = ; decrease code cache size and increase IRAM to fit all pixel functions -D PIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48 ;; in case of linker errors like "section `.text1' will not fit in region `iram1_0_seg'" ; -D PIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48_SECHEAP_SHARED ;; (experimental) adds some extra heap, but may cause slowdown + -D NON32XFER_HANDLER ;; ask forgiveness for PROGMEM misuse lib_deps = #https://github.com/lorol/LITTLEFS.git From b3b326738c14808ed2cac069a70bf9ea18bccd20 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Wed, 23 Oct 2024 19:58:52 -0400 Subject: [PATCH 125/145] Fix incorrect SET_F calls Replace with F() or PSTR() as appropriate. --- wled00/wled_server.cpp | 8 ++++---- wled00/xml.cpp | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/wled00/wled_server.cpp b/wled00/wled_server.cpp index 958b513303..e8cbb41ae5 100644 --- a/wled00/wled_server.cpp +++ b/wled00/wled_server.cpp @@ -193,12 +193,12 @@ void createEditHandler(bool enable) { editHandler = &server.addHandler(new SPIFFSEditor("","",WLED_FS));//http_username,http_password)); #endif #else - editHandler = &server.on(SET_F("/edit"), HTTP_GET, [](AsyncWebServerRequest *request){ + editHandler = &server.on(F("/edit"), HTTP_GET, [](AsyncWebServerRequest *request){ serveMessage(request, 501, FPSTR(s_notimplemented), F("The FS editor is disabled in this build."), 254); }); #endif } else { - editHandler = &server.on(SET_F("/edit"), HTTP_ANY, [](AsyncWebServerRequest *request){ + editHandler = &server.on(F("/edit"), HTTP_ANY, [](AsyncWebServerRequest *request){ serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_unlock_cfg), 254); }); } @@ -427,11 +427,11 @@ void initServer() #ifdef WLED_ENABLE_DMX - server.on(SET_F("/dmxmap"), HTTP_GET, [](AsyncWebServerRequest *request){ + server.on(F("/dmxmap"), HTTP_GET, [](AsyncWebServerRequest *request){ request->send_P(200, FPSTR(CONTENT_TYPE_HTML), PAGE_dmxmap , dmxProcessor); }); #else - server.on(SET_F("/dmxmap"), HTTP_GET, [](AsyncWebServerRequest *request){ + server.on(F("/dmxmap"), HTTP_GET, [](AsyncWebServerRequest *request){ serveMessage(request, 501, FPSTR(s_notimplemented), F("DMX support is not enabled in this build."), 254); }); #endif diff --git a/wled00/xml.cpp b/wled00/xml.cpp index 1ac22c9ce8..6d1ff2f863 100644 --- a/wled00/xml.cpp +++ b/wled00/xml.cpp @@ -227,7 +227,7 @@ void getSettingsJS(byte subPage, Print& settingsScript) sprintf(s, "%d.%d.%d.%d", localIP[0], localIP[1], localIP[2], localIP[3]); #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) - if (Network.isEthernet()) strcat_P(s ,SET_F(" (Ethernet)")); + if (Network.isEthernet()) strcat_P(s ,PSTR(" (Ethernet)")); #endif printSetClassElementHTML(settingsScript,PSTR("sip"),0,s); } else From 7d067d8c305c3c6f397b6863f6849659a0aadd24 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Wed, 23 Oct 2024 20:00:22 -0400 Subject: [PATCH 126/145] Replace SET_F with F in usermods Since oappend() is now strongly typed, pass the correct type. This is a step towards removing the extra shim logic on ESP8266. --- .../Animated_Staircase/Animated_Staircase.h | 8 ++--- usermods/BME68X_v2/usermod_bme68x.h | 20 +++++------ usermods/Battery/usermod_v2_Battery.h | 34 +++++++++---------- usermods/EXAMPLE_v2/usermod_v2_example.h | 10 +++--- .../usermod_internal_temperature.h | 6 ++-- .../usermod_PIR_sensor_switch.h | 4 +-- usermods/ST7789_display/ST7789_display.h | 8 ++--- usermods/Temperature/usermod_temperature.h | 8 ++--- usermods/boblight/boblight.h | 16 ++++----- usermods/multi_relay/usermod_multi_relay.h | 10 +++--- usermods/pixels_dice_tray/pixels_dice_tray.h | 22 ++++++------ usermods/sht/usermod_sht.h | 24 ++++++------- .../usermod_v2_four_line_display_ALT.h | 30 ++++++++-------- .../usermod_v2_rotary_encoder_ui_ALT.h | 4 +-- .../usermod_v2_word_clock.h | 4 +-- usermods/wireguard/wireguard.h | 14 ++++---- 16 files changed, 111 insertions(+), 111 deletions(-) diff --git a/usermods/Animated_Staircase/Animated_Staircase.h b/usermods/Animated_Staircase/Animated_Staircase.h index d1ec9bb7f6..54a9b3331e 100644 --- a/usermods/Animated_Staircase/Animated_Staircase.h +++ b/usermods/Animated_Staircase/Animated_Staircase.h @@ -425,10 +425,10 @@ class Animated_Staircase : public Usermod { } void appendConfigData() { - //oappend(SET_F("dd=addDropdown('staircase','selectfield');")); - //oappend(SET_F("addOption(dd,'1st value',0);")); - //oappend(SET_F("addOption(dd,'2nd value',1);")); - //oappend(SET_F("addInfo('staircase:selectfield',1,'additional info');")); // 0 is field type, 1 is actual field + //oappend(F("dd=addDropdown('staircase','selectfield');")); + //oappend(F("addOption(dd,'1st value',0);")); + //oappend(F("addOption(dd,'2nd value',1);")); + //oappend(F("addInfo('staircase:selectfield',1,'additional info');")); // 0 is field type, 1 is actual field } diff --git a/usermods/BME68X_v2/usermod_bme68x.h b/usermods/BME68X_v2/usermod_bme68x.h index 8e360515a2..aca24d0a29 100644 --- a/usermods/BME68X_v2/usermod_bme68x.h +++ b/usermods/BME68X_v2/usermod_bme68x.h @@ -767,22 +767,22 @@ void UsermodBME68X::appendConfigData() { // snprintf_P(charbuffer, 127, PSTR("addInfo('%s:%s',1,'*) Set to minus to deactivate (all sensors)');"), UMOD_NAME, _nameTemp); oappend(charbuffer); /* Dropdown for Celsius/Fahrenheit*/ - oappend(SET_F("dd=addDropdown('")); + oappend(F("dd=addDropdown('")); oappend(UMOD_NAME); - oappend(SET_F("','")); + oappend(F("','")); oappend(_nameTempScale); - oappend(SET_F("');")); - oappend(SET_F("addOption(dd,'Celsius',0);")); - oappend(SET_F("addOption(dd,'Fahrenheit',1);")); + oappend(F("');")); + oappend(F("addOption(dd,'Celsius',0);")); + oappend(F("addOption(dd,'Fahrenheit',1);")); /* i²C Address*/ - oappend(SET_F("dd=addDropdown('")); + oappend(F("dd=addDropdown('")); oappend(UMOD_NAME); - oappend(SET_F("','")); + oappend(F("','")); oappend(_nameI2CAdr); - oappend(SET_F("');")); - oappend(SET_F("addOption(dd,'0x76',0x76);")); - oappend(SET_F("addOption(dd,'0x77',0x77);")); + oappend(F("');")); + oappend(F("addOption(dd,'0x76',0x76);")); + oappend(F("addOption(dd,'0x77',0x77);")); } /** diff --git a/usermods/Battery/usermod_v2_Battery.h b/usermods/Battery/usermod_v2_Battery.h index e91de850c2..b36c5f4d60 100644 --- a/usermods/Battery/usermod_v2_Battery.h +++ b/usermods/Battery/usermod_v2_Battery.h @@ -478,29 +478,29 @@ class UsermodBattery : public Usermod void appendConfigData() { // Total: 462 Bytes - oappend(SET_F("td=addDropdown('Battery','type');")); // 34 Bytes - oappend(SET_F("addOption(td,'Unkown','0');")); // 28 Bytes - oappend(SET_F("addOption(td,'LiPo','1');")); // 26 Bytes - oappend(SET_F("addOption(td,'LiOn','2');")); // 26 Bytes - oappend(SET_F("addInfo('Battery:type',1,'requires reboot');")); // 81 Bytes - oappend(SET_F("addInfo('Battery:min-voltage',1,'v');")); // 38 Bytes - oappend(SET_F("addInfo('Battery:max-voltage',1,'v');")); // 38 Bytes - oappend(SET_F("addInfo('Battery:interval',1,'ms');")); // 36 Bytes - oappend(SET_F("addInfo('Battery:HA-discovery',1,'');")); // 38 Bytes - oappend(SET_F("addInfo('Battery:auto-off:threshold',1,'%');")); // 45 Bytes - oappend(SET_F("addInfo('Battery:indicator:threshold',1,'%');")); // 46 Bytes - oappend(SET_F("addInfo('Battery:indicator:duration',1,'s');")); // 45 Bytes + oappend(F("td=addDropdown('Battery','type');")); // 34 Bytes + oappend(F("addOption(td,'Unkown','0');")); // 28 Bytes + oappend(F("addOption(td,'LiPo','1');")); // 26 Bytes + oappend(F("addOption(td,'LiOn','2');")); // 26 Bytes + oappend(F("addInfo('Battery:type',1,'requires reboot');")); // 81 Bytes + oappend(F("addInfo('Battery:min-voltage',1,'v');")); // 38 Bytes + oappend(F("addInfo('Battery:max-voltage',1,'v');")); // 38 Bytes + oappend(F("addInfo('Battery:interval',1,'ms');")); // 36 Bytes + oappend(F("addInfo('Battery:HA-discovery',1,'');")); // 38 Bytes + oappend(F("addInfo('Battery:auto-off:threshold',1,'%');")); // 45 Bytes + oappend(F("addInfo('Battery:indicator:threshold',1,'%');")); // 46 Bytes + oappend(F("addInfo('Battery:indicator:duration',1,'s');")); // 45 Bytes // this option list would exeed the oappend() buffer // a list of all presets to select one from - // oappend(SET_F("bd=addDropdown('Battery:low-power-indicator', 'preset');")); - // the loop generates: oappend(SET_F("addOption(bd, 'preset name', preset id);")); + // oappend(F("bd=addDropdown('Battery:low-power-indicator', 'preset');")); + // the loop generates: oappend(F("addOption(bd, 'preset name', preset id);")); // for(int8_t i=1; i < 42; i++) { - // oappend(SET_F("addOption(bd, 'Preset#")); + // oappend(F("addOption(bd, 'Preset#")); // oappendi(i); - // oappend(SET_F("',")); + // oappend(F("',")); // oappendi(i); - // oappend(SET_F(");")); + // oappend(F(");")); // } } diff --git a/usermods/EXAMPLE_v2/usermod_v2_example.h b/usermods/EXAMPLE_v2/usermod_v2_example.h index 3d562b5857..df05f3e3dc 100644 --- a/usermods/EXAMPLE_v2/usermod_v2_example.h +++ b/usermods/EXAMPLE_v2/usermod_v2_example.h @@ -287,11 +287,11 @@ class MyExampleUsermod : public Usermod { */ void appendConfigData() override { - oappend(SET_F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(SET_F(":great")); oappend(SET_F("',1,'(this is a great config value)');")); - oappend(SET_F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(SET_F(":testString")); oappend(SET_F("',1,'enter any string you want');")); - oappend(SET_F("dd=addDropdown('")); oappend(String(FPSTR(_name)).c_str()); oappend(SET_F("','testInt');")); - oappend(SET_F("addOption(dd,'Nothing',0);")); - oappend(SET_F("addOption(dd,'Everything',42);")); + oappend(F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(F(":great")); oappend(F("',1,'(this is a great config value)');")); + oappend(F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(F(":testString")); oappend(F("',1,'enter any string you want');")); + oappend(F("dd=addDropdown('")); oappend(String(FPSTR(_name)).c_str()); oappend(F("','testInt');")); + oappend(F("addOption(dd,'Nothing',0);")); + oappend(F("addOption(dd,'Everything',42);")); } diff --git a/usermods/Internal_Temperature_v2/usermod_internal_temperature.h b/usermods/Internal_Temperature_v2/usermod_internal_temperature.h index 2236bfeaba..c24b4c6288 100644 --- a/usermods/Internal_Temperature_v2/usermod_internal_temperature.h +++ b/usermods/Internal_Temperature_v2/usermod_internal_temperature.h @@ -149,11 +149,11 @@ class InternalTemperatureUsermod : public Usermod void appendConfigData() { // Display 'ms' next to the 'Loop Interval' setting - oappend(SET_F("addInfo('Internal Temperature:Loop Interval', 1, 'ms');")); + oappend(F("addInfo('Internal Temperature:Loop Interval', 1, 'ms');")); // Display '°C' next to the 'Activation Threshold' setting - oappend(SET_F("addInfo('Internal Temperature:Activation Threshold', 1, '°C');")); + oappend(F("addInfo('Internal Temperature:Activation Threshold', 1, '°C');")); // Display '0 = Disabled' next to the 'Preset To Activate' setting - oappend(SET_F("addInfo('Internal Temperature:Preset To Activate', 1, '0 = unused');")); + oappend(F("addInfo('Internal Temperature:Preset To Activate', 1, '0 = unused');")); } bool readFromConfig(JsonObject &root) diff --git a/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h b/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h index 29070cf84e..0deda181c2 100644 --- a/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h +++ b/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h @@ -511,8 +511,8 @@ void PIRsensorSwitch::addToConfig(JsonObject &root) void PIRsensorSwitch::appendConfigData() { - oappend(SET_F("addInfo('PIRsensorSwitch:HA-discovery',1,'HA=Home Assistant');")); // 0 is field type, 1 is actual field - oappend(SET_F("addInfo('PIRsensorSwitch:override',1,'Cancel timer on change');")); // 0 is field type, 1 is actual field + oappend(F("addInfo('PIRsensorSwitch:HA-discovery',1,'HA=Home Assistant');")); // 0 is field type, 1 is actual field + oappend(F("addInfo('PIRsensorSwitch:override',1,'Cancel timer on change');")); // 0 is field type, 1 is actual field for (int i = 0; i < PIR_SENSOR_MAX_SENSORS; i++) { char str[128]; sprintf_P(str, PSTR("addInfo('PIRsensorSwitch:pin[]',%d,'','#%d');"), i, i); diff --git a/usermods/ST7789_display/ST7789_display.h b/usermods/ST7789_display/ST7789_display.h index 0dbada382f..65f4cae5d3 100644 --- a/usermods/ST7789_display/ST7789_display.h +++ b/usermods/ST7789_display/ST7789_display.h @@ -377,10 +377,10 @@ class St7789DisplayUsermod : public Usermod { void appendConfigData() override { - oappend(SET_F("addInfo('ST7789:pin[]',0,'','SPI CS');")); - oappend(SET_F("addInfo('ST7789:pin[]',1,'','SPI DC');")); - oappend(SET_F("addInfo('ST7789:pin[]',2,'','SPI RST');")); - oappend(SET_F("addInfo('ST7789:pin[]',3,'','SPI BL');")); + oappend(F("addInfo('ST7789:pin[]',0,'','SPI CS');")); + oappend(F("addInfo('ST7789:pin[]',1,'','SPI DC');")); + oappend(F("addInfo('ST7789:pin[]',2,'','SPI RST');")); + oappend(F("addInfo('ST7789:pin[]',3,'','SPI BL');")); } /* diff --git a/usermods/Temperature/usermod_temperature.h b/usermods/Temperature/usermod_temperature.h index ad755eaeec..178bc05a0d 100644 --- a/usermods/Temperature/usermod_temperature.h +++ b/usermods/Temperature/usermod_temperature.h @@ -435,10 +435,10 @@ bool UsermodTemperature::readFromConfig(JsonObject &root) { } void UsermodTemperature::appendConfigData() { - oappend(SET_F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(SET_F(":")); oappend(String(FPSTR(_parasite)).c_str()); - oappend(SET_F("',1,'(if no Vcc connected)');")); // 0 is field type, 1 is actual field - oappend(SET_F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(SET_F(":")); oappend(String(FPSTR(_parasitePin)).c_str()); - oappend(SET_F("',1,'(for external MOSFET)');")); // 0 is field type, 1 is actual field + oappend(F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(F(":")); oappend(String(FPSTR(_parasite)).c_str()); + oappend(F("',1,'(if no Vcc connected)');")); // 0 is field type, 1 is actual field + oappend(F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(F(":")); oappend(String(FPSTR(_parasitePin)).c_str()); + oappend(F("',1,'(for external MOSFET)');")); // 0 is field type, 1 is actual field } float UsermodTemperature::getTemperature() { diff --git a/usermods/boblight/boblight.h b/usermods/boblight/boblight.h index 916f7da988..b04b78fac7 100644 --- a/usermods/boblight/boblight.h +++ b/usermods/boblight/boblight.h @@ -305,14 +305,14 @@ class BobLightUsermod : public Usermod { } void appendConfigData() override { - //oappend(SET_F("dd=addDropdown('usermod','selectfield');")); - //oappend(SET_F("addOption(dd,'1st value',0);")); - //oappend(SET_F("addOption(dd,'2nd value',1);")); - oappend(SET_F("addInfo('BobLight:top',1,'LEDs');")); // 0 is field type, 1 is actual field - oappend(SET_F("addInfo('BobLight:bottom',1,'LEDs');")); // 0 is field type, 1 is actual field - oappend(SET_F("addInfo('BobLight:left',1,'LEDs');")); // 0 is field type, 1 is actual field - oappend(SET_F("addInfo('BobLight:right',1,'LEDs');")); // 0 is field type, 1 is actual field - oappend(SET_F("addInfo('BobLight:pct',1,'Depth of scan [%]');")); // 0 is field type, 1 is actual field + //oappend(F("dd=addDropdown('usermod','selectfield');")); + //oappend(F("addOption(dd,'1st value',0);")); + //oappend(F("addOption(dd,'2nd value',1);")); + oappend(F("addInfo('BobLight:top',1,'LEDs');")); // 0 is field type, 1 is actual field + oappend(F("addInfo('BobLight:bottom',1,'LEDs');")); // 0 is field type, 1 is actual field + oappend(F("addInfo('BobLight:left',1,'LEDs');")); // 0 is field type, 1 is actual field + oappend(F("addInfo('BobLight:right',1,'LEDs');")); // 0 is field type, 1 is actual field + oappend(F("addInfo('BobLight:pct',1,'Depth of scan [%]');")); // 0 is field type, 1 is actual field } void addToConfig(JsonObject& root) override { diff --git a/usermods/multi_relay/usermod_multi_relay.h b/usermods/multi_relay/usermod_multi_relay.h index 33a6cf85e2..c4446c7a20 100644 --- a/usermods/multi_relay/usermod_multi_relay.h +++ b/usermods/multi_relay/usermod_multi_relay.h @@ -264,7 +264,7 @@ void MultiRelay::handleOffTimer() { void MultiRelay::InitHtmlAPIHandle() { // https://github.com/me-no-dev/ESPAsyncWebServer DEBUG_PRINTLN(F("Relays: Initialize HTML API")); - server.on(SET_F("/relays"), HTTP_GET, [this](AsyncWebServerRequest *request) { + server.on(F("/relays"), HTTP_GET, [this](AsyncWebServerRequest *request) { DEBUG_PRINTLN(F("Relays: HTML API")); String janswer; String error = ""; @@ -765,10 +765,10 @@ void MultiRelay::addToConfig(JsonObject &root) { } void MultiRelay::appendConfigData() { - oappend(SET_F("addInfo('MultiRelay:PCF8574-address',1,'(not hex!)');")); - oappend(SET_F("addInfo('MultiRelay:broadcast-sec',1,'(MQTT message)');")); - //oappend(SET_F("addInfo('MultiRelay:relay-0:pin',1,'(use -1 for PCF8574)');")); - oappend(SET_F("d.extra.push({'MultiRelay':{pin:[['P0',100],['P1',101],['P2',102],['P3',103],['P4',104],['P5',105],['P6',106],['P7',107]]}});")); + oappend(F("addInfo('MultiRelay:PCF8574-address',1,'(not hex!)');")); + oappend(F("addInfo('MultiRelay:broadcast-sec',1,'(MQTT message)');")); + //oappend(F("addInfo('MultiRelay:relay-0:pin',1,'(use -1 for PCF8574)');")); + oappend(F("d.extra.push({'MultiRelay':{pin:[['P0',100],['P1',101],['P2',102],['P3',103],['P4',104],['P5',105],['P6',106],['P7',107]]}});")); } /** diff --git a/usermods/pixels_dice_tray/pixels_dice_tray.h b/usermods/pixels_dice_tray/pixels_dice_tray.h index a1e45ba33b..61348ebb8e 100644 --- a/usermods/pixels_dice_tray/pixels_dice_tray.h +++ b/usermods/pixels_dice_tray/pixels_dice_tray.h @@ -387,23 +387,23 @@ class PixelsDiceTrayUsermod : public Usermod { // To work around this, add info text to the end of the preceding item. // // See addInfo in wled00/data/settings_um.htm for details on what this function does. - oappend(SET_F( + oappend(F( "addInfo('DiceTray:ble_scan_duration',1,'

Set to \"*\" to " "connect to any die.
Leave Blank to disable.

Saving will replace \"*\" with die names.','');")); #if USING_TFT_DISPLAY - oappend(SET_F("ddr=addDropdown('DiceTray','rotation');")); - oappend(SET_F("addOption(ddr,'0 deg',0);")); - oappend(SET_F("addOption(ddr,'90 deg',1);")); - oappend(SET_F("addOption(ddr,'180 deg',2);")); - oappend(SET_F("addOption(ddr,'270 deg',3);")); - oappend(SET_F( + oappend(F("ddr=addDropdown('DiceTray','rotation');")); + oappend(F("addOption(ddr,'0 deg',0);")); + oappend(F("addOption(ddr,'90 deg',1);")); + oappend(F("addOption(ddr,'180 deg',2);")); + oappend(F("addOption(ddr,'270 deg',3);")); + oappend(F( "addInfo('DiceTray:rotation',1,'
DO NOT CHANGE " "SPI PINS.
CHANGES ARE IGNORED.','');")); - oappend(SET_F("addInfo('TFT:pin[]',0,'','SPI CS');")); - oappend(SET_F("addInfo('TFT:pin[]',1,'','SPI DC');")); - oappend(SET_F("addInfo('TFT:pin[]',2,'','SPI RST');")); - oappend(SET_F("addInfo('TFT:pin[]',3,'','SPI BL');")); + oappend(F("addInfo('TFT:pin[]',0,'','SPI CS');")); + oappend(F("addInfo('TFT:pin[]',1,'','SPI DC');")); + oappend(F("addInfo('TFT:pin[]',2,'','SPI RST');")); + oappend(F("addInfo('TFT:pin[]',3,'','SPI BL');")); #endif } diff --git a/usermods/sht/usermod_sht.h b/usermods/sht/usermod_sht.h index c6e17221be..f10c78a251 100644 --- a/usermods/sht/usermod_sht.h +++ b/usermods/sht/usermod_sht.h @@ -310,22 +310,22 @@ void ShtUsermod::onMqttConnect(bool sessionPresent) { * @return void */ void ShtUsermod::appendConfigData() { - oappend(SET_F("dd=addDropdown('")); + oappend(F("dd=addDropdown('")); oappend(_name); - oappend(SET_F("','")); + oappend(F("','")); oappend(_shtType); - oappend(SET_F("');")); - oappend(SET_F("addOption(dd,'SHT30',0);")); - oappend(SET_F("addOption(dd,'SHT31',1);")); - oappend(SET_F("addOption(dd,'SHT35',2);")); - oappend(SET_F("addOption(dd,'SHT85',3);")); - oappend(SET_F("dd=addDropdown('")); + oappend(F("');")); + oappend(F("addOption(dd,'SHT30',0);")); + oappend(F("addOption(dd,'SHT31',1);")); + oappend(F("addOption(dd,'SHT35',2);")); + oappend(F("addOption(dd,'SHT85',3);")); + oappend(F("dd=addDropdown('")); oappend(_name); - oappend(SET_F("','")); + oappend(F("','")); oappend(_unitOfTemp); - oappend(SET_F("');")); - oappend(SET_F("addOption(dd,'Celsius',0);")); - oappend(SET_F("addOption(dd,'Fahrenheit',1);")); + oappend(F("');")); + oappend(F("addOption(dd,'Celsius',0);")); + oappend(F("addOption(dd,'Fahrenheit',1);")); } /** diff --git a/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h b/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h index dfab7e6ffb..684dd86e46 100644 --- a/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h +++ b/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h @@ -1202,21 +1202,21 @@ void FourLineDisplayUsermod::onUpdateBegin(bool init) { //} void FourLineDisplayUsermod::appendConfigData() { - oappend(SET_F("dd=addDropdown('4LineDisplay','type');")); - oappend(SET_F("addOption(dd,'None',0);")); - oappend(SET_F("addOption(dd,'SSD1306',1);")); - oappend(SET_F("addOption(dd,'SH1106',2);")); - oappend(SET_F("addOption(dd,'SSD1306 128x64',3);")); - oappend(SET_F("addOption(dd,'SSD1305',4);")); - oappend(SET_F("addOption(dd,'SSD1305 128x64',5);")); - oappend(SET_F("addOption(dd,'SSD1309 128x64',9);")); - oappend(SET_F("addOption(dd,'SSD1306 SPI',6);")); - oappend(SET_F("addOption(dd,'SSD1306 SPI 128x64',7);")); - oappend(SET_F("addOption(dd,'SSD1309 SPI 128x64',8);")); - oappend(SET_F("addInfo('4LineDisplay:type',1,'
Change may require reboot','');")); - oappend(SET_F("addInfo('4LineDisplay:pin[]',0,'','SPI CS');")); - oappend(SET_F("addInfo('4LineDisplay:pin[]',1,'','SPI DC');")); - oappend(SET_F("addInfo('4LineDisplay:pin[]',2,'','SPI RST');")); + oappend(F("dd=addDropdown('4LineDisplay','type');")); + oappend(F("addOption(dd,'None',0);")); + oappend(F("addOption(dd,'SSD1306',1);")); + oappend(F("addOption(dd,'SH1106',2);")); + oappend(F("addOption(dd,'SSD1306 128x64',3);")); + oappend(F("addOption(dd,'SSD1305',4);")); + oappend(F("addOption(dd,'SSD1305 128x64',5);")); + oappend(F("addOption(dd,'SSD1309 128x64',9);")); + oappend(F("addOption(dd,'SSD1306 SPI',6);")); + oappend(F("addOption(dd,'SSD1306 SPI 128x64',7);")); + oappend(F("addOption(dd,'SSD1309 SPI 128x64',8);")); + oappend(F("addInfo('4LineDisplay:type',1,'
Change may require reboot','');")); + oappend(F("addInfo('4LineDisplay:pin[]',0,'','SPI CS');")); + oappend(F("addInfo('4LineDisplay:pin[]',1,'','SPI DC');")); + oappend(F("addInfo('4LineDisplay:pin[]',2,'','SPI RST');")); } /* diff --git a/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h b/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h index 55715b7c76..383c1193eb 100644 --- a/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h +++ b/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h @@ -1090,8 +1090,8 @@ void RotaryEncoderUIUsermod::addToConfig(JsonObject &root) { } void RotaryEncoderUIUsermod::appendConfigData() { - oappend(SET_F("addInfo('Rotary-Encoder:PCF8574-address',1,'(not hex!)');")); - oappend(SET_F("d.extra.push({'Rotary-Encoder':{pin:[['P0',100],['P1',101],['P2',102],['P3',103],['P4',104],['P5',105],['P6',106],['P7',107]]}});")); + oappend(F("addInfo('Rotary-Encoder:PCF8574-address',1,'(not hex!)');")); + oappend(F("d.extra.push({'Rotary-Encoder':{pin:[['P0',100],['P1',101],['P2',102],['P3',103],['P4',104],['P5',105],['P6',106],['P7',107]]}});")); } /** diff --git a/usermods/usermod_v2_word_clock/usermod_v2_word_clock.h b/usermods/usermod_v2_word_clock/usermod_v2_word_clock.h index b66be290a5..7ecec08e59 100644 --- a/usermods/usermod_v2_word_clock/usermod_v2_word_clock.h +++ b/usermods/usermod_v2_word_clock/usermod_v2_word_clock.h @@ -433,8 +433,8 @@ class WordClockUsermod : public Usermod void appendConfigData() { - oappend(SET_F("addInfo('WordClockUsermod:ledOffset', 1, 'Number of LEDs before the letters');")); - oappend(SET_F("addInfo('WordClockUsermod:Norddeutsch', 1, 'Viertel vor instead of Dreiviertel');")); + oappend(F("addInfo('WordClockUsermod:ledOffset', 1, 'Number of LEDs before the letters');")); + oappend(F("addInfo('WordClockUsermod:Norddeutsch', 1, 'Viertel vor instead of Dreiviertel');")); } /* diff --git a/usermods/wireguard/wireguard.h b/usermods/wireguard/wireguard.h index 8c88d00018..8656a704af 100644 --- a/usermods/wireguard/wireguard.h +++ b/usermods/wireguard/wireguard.h @@ -54,13 +54,13 @@ class WireguardUsermod : public Usermod { } void appendConfigData() { - oappend(SET_F("addInfo('WireGuard:host',1,'Server Hostname');")); // 0 is field type, 1 is actual field - oappend(SET_F("addInfo('WireGuard:port',1,'Server Port');")); // 0 is field type, 1 is actual field - oappend(SET_F("addInfo('WireGuard:ip',1,'Device IP');")); // 0 is field type, 1 is actual field - oappend(SET_F("addInfo('WireGuard:psk',1,'Pre Shared Key (optional)');")); // 0 is field type, 1 is actual field - oappend(SET_F("addInfo('WireGuard:pem',1,'Private Key');")); // 0 is field type, 1 is actual field - oappend(SET_F("addInfo('WireGuard:pub',1,'Public Key');")); // 0 is field type, 1 is actual field - oappend(SET_F("addInfo('WireGuard:tz',1,'POSIX timezone string');")); // 0 is field type, 1 is actual field + oappend(F("addInfo('WireGuard:host',1,'Server Hostname');")); // 0 is field type, 1 is actual field + oappend(F("addInfo('WireGuard:port',1,'Server Port');")); // 0 is field type, 1 is actual field + oappend(F("addInfo('WireGuard:ip',1,'Device IP');")); // 0 is field type, 1 is actual field + oappend(F("addInfo('WireGuard:psk',1,'Pre Shared Key (optional)');")); // 0 is field type, 1 is actual field + oappend(F("addInfo('WireGuard:pem',1,'Private Key');")); // 0 is field type, 1 is actual field + oappend(F("addInfo('WireGuard:pub',1,'Public Key');")); // 0 is field type, 1 is actual field + oappend(F("addInfo('WireGuard:tz',1,'POSIX timezone string');")); // 0 is field type, 1 is actual field } void addToConfig(JsonObject& root) { From 2e01fe0b5bf853f068fc255896994b324a56c2d8 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Wed, 23 Oct 2024 21:34:35 -0400 Subject: [PATCH 127/145] Revert "Replace SET_F with F in usermods" This reverts commit 7d067d8c305c3c6f397b6863f6849659a0aadd24. --- .../Animated_Staircase/Animated_Staircase.h | 8 ++--- usermods/BME68X_v2/usermod_bme68x.h | 20 +++++------ usermods/Battery/usermod_v2_Battery.h | 34 +++++++++---------- usermods/EXAMPLE_v2/usermod_v2_example.h | 10 +++--- .../usermod_internal_temperature.h | 6 ++-- .../usermod_PIR_sensor_switch.h | 4 +-- usermods/ST7789_display/ST7789_display.h | 8 ++--- usermods/Temperature/usermod_temperature.h | 8 ++--- usermods/boblight/boblight.h | 16 ++++----- usermods/multi_relay/usermod_multi_relay.h | 10 +++--- usermods/pixels_dice_tray/pixels_dice_tray.h | 22 ++++++------ usermods/sht/usermod_sht.h | 24 ++++++------- .../usermod_v2_four_line_display_ALT.h | 30 ++++++++-------- .../usermod_v2_rotary_encoder_ui_ALT.h | 4 +-- .../usermod_v2_word_clock.h | 4 +-- usermods/wireguard/wireguard.h | 14 ++++---- 16 files changed, 111 insertions(+), 111 deletions(-) diff --git a/usermods/Animated_Staircase/Animated_Staircase.h b/usermods/Animated_Staircase/Animated_Staircase.h index 54a9b3331e..d1ec9bb7f6 100644 --- a/usermods/Animated_Staircase/Animated_Staircase.h +++ b/usermods/Animated_Staircase/Animated_Staircase.h @@ -425,10 +425,10 @@ class Animated_Staircase : public Usermod { } void appendConfigData() { - //oappend(F("dd=addDropdown('staircase','selectfield');")); - //oappend(F("addOption(dd,'1st value',0);")); - //oappend(F("addOption(dd,'2nd value',1);")); - //oappend(F("addInfo('staircase:selectfield',1,'additional info');")); // 0 is field type, 1 is actual field + //oappend(SET_F("dd=addDropdown('staircase','selectfield');")); + //oappend(SET_F("addOption(dd,'1st value',0);")); + //oappend(SET_F("addOption(dd,'2nd value',1);")); + //oappend(SET_F("addInfo('staircase:selectfield',1,'additional info');")); // 0 is field type, 1 is actual field } diff --git a/usermods/BME68X_v2/usermod_bme68x.h b/usermods/BME68X_v2/usermod_bme68x.h index aca24d0a29..8e360515a2 100644 --- a/usermods/BME68X_v2/usermod_bme68x.h +++ b/usermods/BME68X_v2/usermod_bme68x.h @@ -767,22 +767,22 @@ void UsermodBME68X::appendConfigData() { // snprintf_P(charbuffer, 127, PSTR("addInfo('%s:%s',1,'*) Set to minus to deactivate (all sensors)');"), UMOD_NAME, _nameTemp); oappend(charbuffer); /* Dropdown for Celsius/Fahrenheit*/ - oappend(F("dd=addDropdown('")); + oappend(SET_F("dd=addDropdown('")); oappend(UMOD_NAME); - oappend(F("','")); + oappend(SET_F("','")); oappend(_nameTempScale); - oappend(F("');")); - oappend(F("addOption(dd,'Celsius',0);")); - oappend(F("addOption(dd,'Fahrenheit',1);")); + oappend(SET_F("');")); + oappend(SET_F("addOption(dd,'Celsius',0);")); + oappend(SET_F("addOption(dd,'Fahrenheit',1);")); /* i²C Address*/ - oappend(F("dd=addDropdown('")); + oappend(SET_F("dd=addDropdown('")); oappend(UMOD_NAME); - oappend(F("','")); + oappend(SET_F("','")); oappend(_nameI2CAdr); - oappend(F("');")); - oappend(F("addOption(dd,'0x76',0x76);")); - oappend(F("addOption(dd,'0x77',0x77);")); + oappend(SET_F("');")); + oappend(SET_F("addOption(dd,'0x76',0x76);")); + oappend(SET_F("addOption(dd,'0x77',0x77);")); } /** diff --git a/usermods/Battery/usermod_v2_Battery.h b/usermods/Battery/usermod_v2_Battery.h index b36c5f4d60..e91de850c2 100644 --- a/usermods/Battery/usermod_v2_Battery.h +++ b/usermods/Battery/usermod_v2_Battery.h @@ -478,29 +478,29 @@ class UsermodBattery : public Usermod void appendConfigData() { // Total: 462 Bytes - oappend(F("td=addDropdown('Battery','type');")); // 34 Bytes - oappend(F("addOption(td,'Unkown','0');")); // 28 Bytes - oappend(F("addOption(td,'LiPo','1');")); // 26 Bytes - oappend(F("addOption(td,'LiOn','2');")); // 26 Bytes - oappend(F("addInfo('Battery:type',1,'requires reboot');")); // 81 Bytes - oappend(F("addInfo('Battery:min-voltage',1,'v');")); // 38 Bytes - oappend(F("addInfo('Battery:max-voltage',1,'v');")); // 38 Bytes - oappend(F("addInfo('Battery:interval',1,'ms');")); // 36 Bytes - oappend(F("addInfo('Battery:HA-discovery',1,'');")); // 38 Bytes - oappend(F("addInfo('Battery:auto-off:threshold',1,'%');")); // 45 Bytes - oappend(F("addInfo('Battery:indicator:threshold',1,'%');")); // 46 Bytes - oappend(F("addInfo('Battery:indicator:duration',1,'s');")); // 45 Bytes + oappend(SET_F("td=addDropdown('Battery','type');")); // 34 Bytes + oappend(SET_F("addOption(td,'Unkown','0');")); // 28 Bytes + oappend(SET_F("addOption(td,'LiPo','1');")); // 26 Bytes + oappend(SET_F("addOption(td,'LiOn','2');")); // 26 Bytes + oappend(SET_F("addInfo('Battery:type',1,'requires reboot');")); // 81 Bytes + oappend(SET_F("addInfo('Battery:min-voltage',1,'v');")); // 38 Bytes + oappend(SET_F("addInfo('Battery:max-voltage',1,'v');")); // 38 Bytes + oappend(SET_F("addInfo('Battery:interval',1,'ms');")); // 36 Bytes + oappend(SET_F("addInfo('Battery:HA-discovery',1,'');")); // 38 Bytes + oappend(SET_F("addInfo('Battery:auto-off:threshold',1,'%');")); // 45 Bytes + oappend(SET_F("addInfo('Battery:indicator:threshold',1,'%');")); // 46 Bytes + oappend(SET_F("addInfo('Battery:indicator:duration',1,'s');")); // 45 Bytes // this option list would exeed the oappend() buffer // a list of all presets to select one from - // oappend(F("bd=addDropdown('Battery:low-power-indicator', 'preset');")); - // the loop generates: oappend(F("addOption(bd, 'preset name', preset id);")); + // oappend(SET_F("bd=addDropdown('Battery:low-power-indicator', 'preset');")); + // the loop generates: oappend(SET_F("addOption(bd, 'preset name', preset id);")); // for(int8_t i=1; i < 42; i++) { - // oappend(F("addOption(bd, 'Preset#")); + // oappend(SET_F("addOption(bd, 'Preset#")); // oappendi(i); - // oappend(F("',")); + // oappend(SET_F("',")); // oappendi(i); - // oappend(F(");")); + // oappend(SET_F(");")); // } } diff --git a/usermods/EXAMPLE_v2/usermod_v2_example.h b/usermods/EXAMPLE_v2/usermod_v2_example.h index df05f3e3dc..3d562b5857 100644 --- a/usermods/EXAMPLE_v2/usermod_v2_example.h +++ b/usermods/EXAMPLE_v2/usermod_v2_example.h @@ -287,11 +287,11 @@ class MyExampleUsermod : public Usermod { */ void appendConfigData() override { - oappend(F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(F(":great")); oappend(F("',1,'(this is a great config value)');")); - oappend(F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(F(":testString")); oappend(F("',1,'enter any string you want');")); - oappend(F("dd=addDropdown('")); oappend(String(FPSTR(_name)).c_str()); oappend(F("','testInt');")); - oappend(F("addOption(dd,'Nothing',0);")); - oappend(F("addOption(dd,'Everything',42);")); + oappend(SET_F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(SET_F(":great")); oappend(SET_F("',1,'(this is a great config value)');")); + oappend(SET_F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(SET_F(":testString")); oappend(SET_F("',1,'enter any string you want');")); + oappend(SET_F("dd=addDropdown('")); oappend(String(FPSTR(_name)).c_str()); oappend(SET_F("','testInt');")); + oappend(SET_F("addOption(dd,'Nothing',0);")); + oappend(SET_F("addOption(dd,'Everything',42);")); } diff --git a/usermods/Internal_Temperature_v2/usermod_internal_temperature.h b/usermods/Internal_Temperature_v2/usermod_internal_temperature.h index c24b4c6288..2236bfeaba 100644 --- a/usermods/Internal_Temperature_v2/usermod_internal_temperature.h +++ b/usermods/Internal_Temperature_v2/usermod_internal_temperature.h @@ -149,11 +149,11 @@ class InternalTemperatureUsermod : public Usermod void appendConfigData() { // Display 'ms' next to the 'Loop Interval' setting - oappend(F("addInfo('Internal Temperature:Loop Interval', 1, 'ms');")); + oappend(SET_F("addInfo('Internal Temperature:Loop Interval', 1, 'ms');")); // Display '°C' next to the 'Activation Threshold' setting - oappend(F("addInfo('Internal Temperature:Activation Threshold', 1, '°C');")); + oappend(SET_F("addInfo('Internal Temperature:Activation Threshold', 1, '°C');")); // Display '0 = Disabled' next to the 'Preset To Activate' setting - oappend(F("addInfo('Internal Temperature:Preset To Activate', 1, '0 = unused');")); + oappend(SET_F("addInfo('Internal Temperature:Preset To Activate', 1, '0 = unused');")); } bool readFromConfig(JsonObject &root) diff --git a/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h b/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h index 0deda181c2..29070cf84e 100644 --- a/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h +++ b/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h @@ -511,8 +511,8 @@ void PIRsensorSwitch::addToConfig(JsonObject &root) void PIRsensorSwitch::appendConfigData() { - oappend(F("addInfo('PIRsensorSwitch:HA-discovery',1,'HA=Home Assistant');")); // 0 is field type, 1 is actual field - oappend(F("addInfo('PIRsensorSwitch:override',1,'Cancel timer on change');")); // 0 is field type, 1 is actual field + oappend(SET_F("addInfo('PIRsensorSwitch:HA-discovery',1,'HA=Home Assistant');")); // 0 is field type, 1 is actual field + oappend(SET_F("addInfo('PIRsensorSwitch:override',1,'Cancel timer on change');")); // 0 is field type, 1 is actual field for (int i = 0; i < PIR_SENSOR_MAX_SENSORS; i++) { char str[128]; sprintf_P(str, PSTR("addInfo('PIRsensorSwitch:pin[]',%d,'','#%d');"), i, i); diff --git a/usermods/ST7789_display/ST7789_display.h b/usermods/ST7789_display/ST7789_display.h index 65f4cae5d3..0dbada382f 100644 --- a/usermods/ST7789_display/ST7789_display.h +++ b/usermods/ST7789_display/ST7789_display.h @@ -377,10 +377,10 @@ class St7789DisplayUsermod : public Usermod { void appendConfigData() override { - oappend(F("addInfo('ST7789:pin[]',0,'','SPI CS');")); - oappend(F("addInfo('ST7789:pin[]',1,'','SPI DC');")); - oappend(F("addInfo('ST7789:pin[]',2,'','SPI RST');")); - oappend(F("addInfo('ST7789:pin[]',3,'','SPI BL');")); + oappend(SET_F("addInfo('ST7789:pin[]',0,'','SPI CS');")); + oappend(SET_F("addInfo('ST7789:pin[]',1,'','SPI DC');")); + oappend(SET_F("addInfo('ST7789:pin[]',2,'','SPI RST');")); + oappend(SET_F("addInfo('ST7789:pin[]',3,'','SPI BL');")); } /* diff --git a/usermods/Temperature/usermod_temperature.h b/usermods/Temperature/usermod_temperature.h index 178bc05a0d..ad755eaeec 100644 --- a/usermods/Temperature/usermod_temperature.h +++ b/usermods/Temperature/usermod_temperature.h @@ -435,10 +435,10 @@ bool UsermodTemperature::readFromConfig(JsonObject &root) { } void UsermodTemperature::appendConfigData() { - oappend(F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(F(":")); oappend(String(FPSTR(_parasite)).c_str()); - oappend(F("',1,'(if no Vcc connected)');")); // 0 is field type, 1 is actual field - oappend(F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(F(":")); oappend(String(FPSTR(_parasitePin)).c_str()); - oappend(F("',1,'(for external MOSFET)');")); // 0 is field type, 1 is actual field + oappend(SET_F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(SET_F(":")); oappend(String(FPSTR(_parasite)).c_str()); + oappend(SET_F("',1,'(if no Vcc connected)');")); // 0 is field type, 1 is actual field + oappend(SET_F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(SET_F(":")); oappend(String(FPSTR(_parasitePin)).c_str()); + oappend(SET_F("',1,'(for external MOSFET)');")); // 0 is field type, 1 is actual field } float UsermodTemperature::getTemperature() { diff --git a/usermods/boblight/boblight.h b/usermods/boblight/boblight.h index b04b78fac7..916f7da988 100644 --- a/usermods/boblight/boblight.h +++ b/usermods/boblight/boblight.h @@ -305,14 +305,14 @@ class BobLightUsermod : public Usermod { } void appendConfigData() override { - //oappend(F("dd=addDropdown('usermod','selectfield');")); - //oappend(F("addOption(dd,'1st value',0);")); - //oappend(F("addOption(dd,'2nd value',1);")); - oappend(F("addInfo('BobLight:top',1,'LEDs');")); // 0 is field type, 1 is actual field - oappend(F("addInfo('BobLight:bottom',1,'LEDs');")); // 0 is field type, 1 is actual field - oappend(F("addInfo('BobLight:left',1,'LEDs');")); // 0 is field type, 1 is actual field - oappend(F("addInfo('BobLight:right',1,'LEDs');")); // 0 is field type, 1 is actual field - oappend(F("addInfo('BobLight:pct',1,'Depth of scan [%]');")); // 0 is field type, 1 is actual field + //oappend(SET_F("dd=addDropdown('usermod','selectfield');")); + //oappend(SET_F("addOption(dd,'1st value',0);")); + //oappend(SET_F("addOption(dd,'2nd value',1);")); + oappend(SET_F("addInfo('BobLight:top',1,'LEDs');")); // 0 is field type, 1 is actual field + oappend(SET_F("addInfo('BobLight:bottom',1,'LEDs');")); // 0 is field type, 1 is actual field + oappend(SET_F("addInfo('BobLight:left',1,'LEDs');")); // 0 is field type, 1 is actual field + oappend(SET_F("addInfo('BobLight:right',1,'LEDs');")); // 0 is field type, 1 is actual field + oappend(SET_F("addInfo('BobLight:pct',1,'Depth of scan [%]');")); // 0 is field type, 1 is actual field } void addToConfig(JsonObject& root) override { diff --git a/usermods/multi_relay/usermod_multi_relay.h b/usermods/multi_relay/usermod_multi_relay.h index c4446c7a20..33a6cf85e2 100644 --- a/usermods/multi_relay/usermod_multi_relay.h +++ b/usermods/multi_relay/usermod_multi_relay.h @@ -264,7 +264,7 @@ void MultiRelay::handleOffTimer() { void MultiRelay::InitHtmlAPIHandle() { // https://github.com/me-no-dev/ESPAsyncWebServer DEBUG_PRINTLN(F("Relays: Initialize HTML API")); - server.on(F("/relays"), HTTP_GET, [this](AsyncWebServerRequest *request) { + server.on(SET_F("/relays"), HTTP_GET, [this](AsyncWebServerRequest *request) { DEBUG_PRINTLN(F("Relays: HTML API")); String janswer; String error = ""; @@ -765,10 +765,10 @@ void MultiRelay::addToConfig(JsonObject &root) { } void MultiRelay::appendConfigData() { - oappend(F("addInfo('MultiRelay:PCF8574-address',1,'(not hex!)');")); - oappend(F("addInfo('MultiRelay:broadcast-sec',1,'(MQTT message)');")); - //oappend(F("addInfo('MultiRelay:relay-0:pin',1,'(use -1 for PCF8574)');")); - oappend(F("d.extra.push({'MultiRelay':{pin:[['P0',100],['P1',101],['P2',102],['P3',103],['P4',104],['P5',105],['P6',106],['P7',107]]}});")); + oappend(SET_F("addInfo('MultiRelay:PCF8574-address',1,'(not hex!)');")); + oappend(SET_F("addInfo('MultiRelay:broadcast-sec',1,'(MQTT message)');")); + //oappend(SET_F("addInfo('MultiRelay:relay-0:pin',1,'(use -1 for PCF8574)');")); + oappend(SET_F("d.extra.push({'MultiRelay':{pin:[['P0',100],['P1',101],['P2',102],['P3',103],['P4',104],['P5',105],['P6',106],['P7',107]]}});")); } /** diff --git a/usermods/pixels_dice_tray/pixels_dice_tray.h b/usermods/pixels_dice_tray/pixels_dice_tray.h index 61348ebb8e..a1e45ba33b 100644 --- a/usermods/pixels_dice_tray/pixels_dice_tray.h +++ b/usermods/pixels_dice_tray/pixels_dice_tray.h @@ -387,23 +387,23 @@ class PixelsDiceTrayUsermod : public Usermod { // To work around this, add info text to the end of the preceding item. // // See addInfo in wled00/data/settings_um.htm for details on what this function does. - oappend(F( + oappend(SET_F( "addInfo('DiceTray:ble_scan_duration',1,'

Set to \"*\" to " "connect to any die.
Leave Blank to disable.

Saving will replace \"*\" with die names.','');")); #if USING_TFT_DISPLAY - oappend(F("ddr=addDropdown('DiceTray','rotation');")); - oappend(F("addOption(ddr,'0 deg',0);")); - oappend(F("addOption(ddr,'90 deg',1);")); - oappend(F("addOption(ddr,'180 deg',2);")); - oappend(F("addOption(ddr,'270 deg',3);")); - oappend(F( + oappend(SET_F("ddr=addDropdown('DiceTray','rotation');")); + oappend(SET_F("addOption(ddr,'0 deg',0);")); + oappend(SET_F("addOption(ddr,'90 deg',1);")); + oappend(SET_F("addOption(ddr,'180 deg',2);")); + oappend(SET_F("addOption(ddr,'270 deg',3);")); + oappend(SET_F( "addInfo('DiceTray:rotation',1,'
DO NOT CHANGE " "SPI PINS.
CHANGES ARE IGNORED.','');")); - oappend(F("addInfo('TFT:pin[]',0,'','SPI CS');")); - oappend(F("addInfo('TFT:pin[]',1,'','SPI DC');")); - oappend(F("addInfo('TFT:pin[]',2,'','SPI RST');")); - oappend(F("addInfo('TFT:pin[]',3,'','SPI BL');")); + oappend(SET_F("addInfo('TFT:pin[]',0,'','SPI CS');")); + oappend(SET_F("addInfo('TFT:pin[]',1,'','SPI DC');")); + oappend(SET_F("addInfo('TFT:pin[]',2,'','SPI RST');")); + oappend(SET_F("addInfo('TFT:pin[]',3,'','SPI BL');")); #endif } diff --git a/usermods/sht/usermod_sht.h b/usermods/sht/usermod_sht.h index f10c78a251..c6e17221be 100644 --- a/usermods/sht/usermod_sht.h +++ b/usermods/sht/usermod_sht.h @@ -310,22 +310,22 @@ void ShtUsermod::onMqttConnect(bool sessionPresent) { * @return void */ void ShtUsermod::appendConfigData() { - oappend(F("dd=addDropdown('")); + oappend(SET_F("dd=addDropdown('")); oappend(_name); - oappend(F("','")); + oappend(SET_F("','")); oappend(_shtType); - oappend(F("');")); - oappend(F("addOption(dd,'SHT30',0);")); - oappend(F("addOption(dd,'SHT31',1);")); - oappend(F("addOption(dd,'SHT35',2);")); - oappend(F("addOption(dd,'SHT85',3);")); - oappend(F("dd=addDropdown('")); + oappend(SET_F("');")); + oappend(SET_F("addOption(dd,'SHT30',0);")); + oappend(SET_F("addOption(dd,'SHT31',1);")); + oappend(SET_F("addOption(dd,'SHT35',2);")); + oappend(SET_F("addOption(dd,'SHT85',3);")); + oappend(SET_F("dd=addDropdown('")); oappend(_name); - oappend(F("','")); + oappend(SET_F("','")); oappend(_unitOfTemp); - oappend(F("');")); - oappend(F("addOption(dd,'Celsius',0);")); - oappend(F("addOption(dd,'Fahrenheit',1);")); + oappend(SET_F("');")); + oappend(SET_F("addOption(dd,'Celsius',0);")); + oappend(SET_F("addOption(dd,'Fahrenheit',1);")); } /** diff --git a/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h b/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h index 684dd86e46..dfab7e6ffb 100644 --- a/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h +++ b/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h @@ -1202,21 +1202,21 @@ void FourLineDisplayUsermod::onUpdateBegin(bool init) { //} void FourLineDisplayUsermod::appendConfigData() { - oappend(F("dd=addDropdown('4LineDisplay','type');")); - oappend(F("addOption(dd,'None',0);")); - oappend(F("addOption(dd,'SSD1306',1);")); - oappend(F("addOption(dd,'SH1106',2);")); - oappend(F("addOption(dd,'SSD1306 128x64',3);")); - oappend(F("addOption(dd,'SSD1305',4);")); - oappend(F("addOption(dd,'SSD1305 128x64',5);")); - oappend(F("addOption(dd,'SSD1309 128x64',9);")); - oappend(F("addOption(dd,'SSD1306 SPI',6);")); - oappend(F("addOption(dd,'SSD1306 SPI 128x64',7);")); - oappend(F("addOption(dd,'SSD1309 SPI 128x64',8);")); - oappend(F("addInfo('4LineDisplay:type',1,'
Change may require reboot','');")); - oappend(F("addInfo('4LineDisplay:pin[]',0,'','SPI CS');")); - oappend(F("addInfo('4LineDisplay:pin[]',1,'','SPI DC');")); - oappend(F("addInfo('4LineDisplay:pin[]',2,'','SPI RST');")); + oappend(SET_F("dd=addDropdown('4LineDisplay','type');")); + oappend(SET_F("addOption(dd,'None',0);")); + oappend(SET_F("addOption(dd,'SSD1306',1);")); + oappend(SET_F("addOption(dd,'SH1106',2);")); + oappend(SET_F("addOption(dd,'SSD1306 128x64',3);")); + oappend(SET_F("addOption(dd,'SSD1305',4);")); + oappend(SET_F("addOption(dd,'SSD1305 128x64',5);")); + oappend(SET_F("addOption(dd,'SSD1309 128x64',9);")); + oappend(SET_F("addOption(dd,'SSD1306 SPI',6);")); + oappend(SET_F("addOption(dd,'SSD1306 SPI 128x64',7);")); + oappend(SET_F("addOption(dd,'SSD1309 SPI 128x64',8);")); + oappend(SET_F("addInfo('4LineDisplay:type',1,'
Change may require reboot','');")); + oappend(SET_F("addInfo('4LineDisplay:pin[]',0,'','SPI CS');")); + oappend(SET_F("addInfo('4LineDisplay:pin[]',1,'','SPI DC');")); + oappend(SET_F("addInfo('4LineDisplay:pin[]',2,'','SPI RST');")); } /* diff --git a/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h b/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h index 383c1193eb..55715b7c76 100644 --- a/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h +++ b/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h @@ -1090,8 +1090,8 @@ void RotaryEncoderUIUsermod::addToConfig(JsonObject &root) { } void RotaryEncoderUIUsermod::appendConfigData() { - oappend(F("addInfo('Rotary-Encoder:PCF8574-address',1,'(not hex!)');")); - oappend(F("d.extra.push({'Rotary-Encoder':{pin:[['P0',100],['P1',101],['P2',102],['P3',103],['P4',104],['P5',105],['P6',106],['P7',107]]}});")); + oappend(SET_F("addInfo('Rotary-Encoder:PCF8574-address',1,'(not hex!)');")); + oappend(SET_F("d.extra.push({'Rotary-Encoder':{pin:[['P0',100],['P1',101],['P2',102],['P3',103],['P4',104],['P5',105],['P6',106],['P7',107]]}});")); } /** diff --git a/usermods/usermod_v2_word_clock/usermod_v2_word_clock.h b/usermods/usermod_v2_word_clock/usermod_v2_word_clock.h index 7ecec08e59..b66be290a5 100644 --- a/usermods/usermod_v2_word_clock/usermod_v2_word_clock.h +++ b/usermods/usermod_v2_word_clock/usermod_v2_word_clock.h @@ -433,8 +433,8 @@ class WordClockUsermod : public Usermod void appendConfigData() { - oappend(F("addInfo('WordClockUsermod:ledOffset', 1, 'Number of LEDs before the letters');")); - oappend(F("addInfo('WordClockUsermod:Norddeutsch', 1, 'Viertel vor instead of Dreiviertel');")); + oappend(SET_F("addInfo('WordClockUsermod:ledOffset', 1, 'Number of LEDs before the letters');")); + oappend(SET_F("addInfo('WordClockUsermod:Norddeutsch', 1, 'Viertel vor instead of Dreiviertel');")); } /* diff --git a/usermods/wireguard/wireguard.h b/usermods/wireguard/wireguard.h index 8656a704af..8c88d00018 100644 --- a/usermods/wireguard/wireguard.h +++ b/usermods/wireguard/wireguard.h @@ -54,13 +54,13 @@ class WireguardUsermod : public Usermod { } void appendConfigData() { - oappend(F("addInfo('WireGuard:host',1,'Server Hostname');")); // 0 is field type, 1 is actual field - oappend(F("addInfo('WireGuard:port',1,'Server Port');")); // 0 is field type, 1 is actual field - oappend(F("addInfo('WireGuard:ip',1,'Device IP');")); // 0 is field type, 1 is actual field - oappend(F("addInfo('WireGuard:psk',1,'Pre Shared Key (optional)');")); // 0 is field type, 1 is actual field - oappend(F("addInfo('WireGuard:pem',1,'Private Key');")); // 0 is field type, 1 is actual field - oappend(F("addInfo('WireGuard:pub',1,'Public Key');")); // 0 is field type, 1 is actual field - oappend(F("addInfo('WireGuard:tz',1,'POSIX timezone string');")); // 0 is field type, 1 is actual field + oappend(SET_F("addInfo('WireGuard:host',1,'Server Hostname');")); // 0 is field type, 1 is actual field + oappend(SET_F("addInfo('WireGuard:port',1,'Server Port');")); // 0 is field type, 1 is actual field + oappend(SET_F("addInfo('WireGuard:ip',1,'Device IP');")); // 0 is field type, 1 is actual field + oappend(SET_F("addInfo('WireGuard:psk',1,'Pre Shared Key (optional)');")); // 0 is field type, 1 is actual field + oappend(SET_F("addInfo('WireGuard:pem',1,'Private Key');")); // 0 is field type, 1 is actual field + oappend(SET_F("addInfo('WireGuard:pub',1,'Public Key');")); // 0 is field type, 1 is actual field + oappend(SET_F("addInfo('WireGuard:tz',1,'POSIX timezone string');")); // 0 is field type, 1 is actual field } void addToConfig(JsonObject& root) { From 4f48ddfaec8a4d3d5dfe548204e3fde90baf8e9f Mon Sep 17 00:00:00 2001 From: Will Miles Date: Wed, 23 Oct 2024 20:00:22 -0400 Subject: [PATCH 128/145] Replace SET_F with F in usermods Since oappend() is now strongly typed, pass the correct type. This is a step towards removing the extra shim logic on ESP8266. --- .../Animated_Staircase/Animated_Staircase.h | 8 ++--- usermods/BME68X_v2/usermod_bme68x.h | 20 +++++------ usermods/Battery/usermod_v2_Battery.h | 34 +++++++++---------- usermods/EXAMPLE_v2/usermod_v2_example.h | 10 +++--- .../usermod_internal_temperature.h | 6 ++-- .../usermod_PIR_sensor_switch.h | 4 +-- usermods/ST7789_display/ST7789_display.h | 8 ++--- usermods/Temperature/usermod_temperature.h | 8 ++--- usermods/boblight/boblight.h | 16 ++++----- usermods/multi_relay/usermod_multi_relay.h | 10 +++--- usermods/pixels_dice_tray/pixels_dice_tray.h | 22 ++++++------ usermods/sht/usermod_sht.h | 24 ++++++------- .../usermod_v2_four_line_display_ALT.h | 30 ++++++++-------- .../usermod_v2_rotary_encoder_ui_ALT.h | 4 +-- .../usermod_v2_word_clock.h | 4 +-- usermods/wireguard/wireguard.h | 14 ++++---- 16 files changed, 111 insertions(+), 111 deletions(-) diff --git a/usermods/Animated_Staircase/Animated_Staircase.h b/usermods/Animated_Staircase/Animated_Staircase.h index d1ec9bb7f6..54a9b3331e 100644 --- a/usermods/Animated_Staircase/Animated_Staircase.h +++ b/usermods/Animated_Staircase/Animated_Staircase.h @@ -425,10 +425,10 @@ class Animated_Staircase : public Usermod { } void appendConfigData() { - //oappend(SET_F("dd=addDropdown('staircase','selectfield');")); - //oappend(SET_F("addOption(dd,'1st value',0);")); - //oappend(SET_F("addOption(dd,'2nd value',1);")); - //oappend(SET_F("addInfo('staircase:selectfield',1,'additional info');")); // 0 is field type, 1 is actual field + //oappend(F("dd=addDropdown('staircase','selectfield');")); + //oappend(F("addOption(dd,'1st value',0);")); + //oappend(F("addOption(dd,'2nd value',1);")); + //oappend(F("addInfo('staircase:selectfield',1,'additional info');")); // 0 is field type, 1 is actual field } diff --git a/usermods/BME68X_v2/usermod_bme68x.h b/usermods/BME68X_v2/usermod_bme68x.h index 8e360515a2..aca24d0a29 100644 --- a/usermods/BME68X_v2/usermod_bme68x.h +++ b/usermods/BME68X_v2/usermod_bme68x.h @@ -767,22 +767,22 @@ void UsermodBME68X::appendConfigData() { // snprintf_P(charbuffer, 127, PSTR("addInfo('%s:%s',1,'*) Set to minus to deactivate (all sensors)');"), UMOD_NAME, _nameTemp); oappend(charbuffer); /* Dropdown for Celsius/Fahrenheit*/ - oappend(SET_F("dd=addDropdown('")); + oappend(F("dd=addDropdown('")); oappend(UMOD_NAME); - oappend(SET_F("','")); + oappend(F("','")); oappend(_nameTempScale); - oappend(SET_F("');")); - oappend(SET_F("addOption(dd,'Celsius',0);")); - oappend(SET_F("addOption(dd,'Fahrenheit',1);")); + oappend(F("');")); + oappend(F("addOption(dd,'Celsius',0);")); + oappend(F("addOption(dd,'Fahrenheit',1);")); /* i²C Address*/ - oappend(SET_F("dd=addDropdown('")); + oappend(F("dd=addDropdown('")); oappend(UMOD_NAME); - oappend(SET_F("','")); + oappend(F("','")); oappend(_nameI2CAdr); - oappend(SET_F("');")); - oappend(SET_F("addOption(dd,'0x76',0x76);")); - oappend(SET_F("addOption(dd,'0x77',0x77);")); + oappend(F("');")); + oappend(F("addOption(dd,'0x76',0x76);")); + oappend(F("addOption(dd,'0x77',0x77);")); } /** diff --git a/usermods/Battery/usermod_v2_Battery.h b/usermods/Battery/usermod_v2_Battery.h index e91de850c2..b36c5f4d60 100644 --- a/usermods/Battery/usermod_v2_Battery.h +++ b/usermods/Battery/usermod_v2_Battery.h @@ -478,29 +478,29 @@ class UsermodBattery : public Usermod void appendConfigData() { // Total: 462 Bytes - oappend(SET_F("td=addDropdown('Battery','type');")); // 34 Bytes - oappend(SET_F("addOption(td,'Unkown','0');")); // 28 Bytes - oappend(SET_F("addOption(td,'LiPo','1');")); // 26 Bytes - oappend(SET_F("addOption(td,'LiOn','2');")); // 26 Bytes - oappend(SET_F("addInfo('Battery:type',1,'requires reboot');")); // 81 Bytes - oappend(SET_F("addInfo('Battery:min-voltage',1,'v');")); // 38 Bytes - oappend(SET_F("addInfo('Battery:max-voltage',1,'v');")); // 38 Bytes - oappend(SET_F("addInfo('Battery:interval',1,'ms');")); // 36 Bytes - oappend(SET_F("addInfo('Battery:HA-discovery',1,'');")); // 38 Bytes - oappend(SET_F("addInfo('Battery:auto-off:threshold',1,'%');")); // 45 Bytes - oappend(SET_F("addInfo('Battery:indicator:threshold',1,'%');")); // 46 Bytes - oappend(SET_F("addInfo('Battery:indicator:duration',1,'s');")); // 45 Bytes + oappend(F("td=addDropdown('Battery','type');")); // 34 Bytes + oappend(F("addOption(td,'Unkown','0');")); // 28 Bytes + oappend(F("addOption(td,'LiPo','1');")); // 26 Bytes + oappend(F("addOption(td,'LiOn','2');")); // 26 Bytes + oappend(F("addInfo('Battery:type',1,'requires reboot');")); // 81 Bytes + oappend(F("addInfo('Battery:min-voltage',1,'v');")); // 38 Bytes + oappend(F("addInfo('Battery:max-voltage',1,'v');")); // 38 Bytes + oappend(F("addInfo('Battery:interval',1,'ms');")); // 36 Bytes + oappend(F("addInfo('Battery:HA-discovery',1,'');")); // 38 Bytes + oappend(F("addInfo('Battery:auto-off:threshold',1,'%');")); // 45 Bytes + oappend(F("addInfo('Battery:indicator:threshold',1,'%');")); // 46 Bytes + oappend(F("addInfo('Battery:indicator:duration',1,'s');")); // 45 Bytes // this option list would exeed the oappend() buffer // a list of all presets to select one from - // oappend(SET_F("bd=addDropdown('Battery:low-power-indicator', 'preset');")); - // the loop generates: oappend(SET_F("addOption(bd, 'preset name', preset id);")); + // oappend(F("bd=addDropdown('Battery:low-power-indicator', 'preset');")); + // the loop generates: oappend(F("addOption(bd, 'preset name', preset id);")); // for(int8_t i=1; i < 42; i++) { - // oappend(SET_F("addOption(bd, 'Preset#")); + // oappend(F("addOption(bd, 'Preset#")); // oappendi(i); - // oappend(SET_F("',")); + // oappend(F("',")); // oappendi(i); - // oappend(SET_F(");")); + // oappend(F(");")); // } } diff --git a/usermods/EXAMPLE_v2/usermod_v2_example.h b/usermods/EXAMPLE_v2/usermod_v2_example.h index 3d562b5857..df05f3e3dc 100644 --- a/usermods/EXAMPLE_v2/usermod_v2_example.h +++ b/usermods/EXAMPLE_v2/usermod_v2_example.h @@ -287,11 +287,11 @@ class MyExampleUsermod : public Usermod { */ void appendConfigData() override { - oappend(SET_F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(SET_F(":great")); oappend(SET_F("',1,'(this is a great config value)');")); - oappend(SET_F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(SET_F(":testString")); oappend(SET_F("',1,'enter any string you want');")); - oappend(SET_F("dd=addDropdown('")); oappend(String(FPSTR(_name)).c_str()); oappend(SET_F("','testInt');")); - oappend(SET_F("addOption(dd,'Nothing',0);")); - oappend(SET_F("addOption(dd,'Everything',42);")); + oappend(F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(F(":great")); oappend(F("',1,'(this is a great config value)');")); + oappend(F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(F(":testString")); oappend(F("',1,'enter any string you want');")); + oappend(F("dd=addDropdown('")); oappend(String(FPSTR(_name)).c_str()); oappend(F("','testInt');")); + oappend(F("addOption(dd,'Nothing',0);")); + oappend(F("addOption(dd,'Everything',42);")); } diff --git a/usermods/Internal_Temperature_v2/usermod_internal_temperature.h b/usermods/Internal_Temperature_v2/usermod_internal_temperature.h index 2236bfeaba..c24b4c6288 100644 --- a/usermods/Internal_Temperature_v2/usermod_internal_temperature.h +++ b/usermods/Internal_Temperature_v2/usermod_internal_temperature.h @@ -149,11 +149,11 @@ class InternalTemperatureUsermod : public Usermod void appendConfigData() { // Display 'ms' next to the 'Loop Interval' setting - oappend(SET_F("addInfo('Internal Temperature:Loop Interval', 1, 'ms');")); + oappend(F("addInfo('Internal Temperature:Loop Interval', 1, 'ms');")); // Display '°C' next to the 'Activation Threshold' setting - oappend(SET_F("addInfo('Internal Temperature:Activation Threshold', 1, '°C');")); + oappend(F("addInfo('Internal Temperature:Activation Threshold', 1, '°C');")); // Display '0 = Disabled' next to the 'Preset To Activate' setting - oappend(SET_F("addInfo('Internal Temperature:Preset To Activate', 1, '0 = unused');")); + oappend(F("addInfo('Internal Temperature:Preset To Activate', 1, '0 = unused');")); } bool readFromConfig(JsonObject &root) diff --git a/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h b/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h index 29070cf84e..0deda181c2 100644 --- a/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h +++ b/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h @@ -511,8 +511,8 @@ void PIRsensorSwitch::addToConfig(JsonObject &root) void PIRsensorSwitch::appendConfigData() { - oappend(SET_F("addInfo('PIRsensorSwitch:HA-discovery',1,'HA=Home Assistant');")); // 0 is field type, 1 is actual field - oappend(SET_F("addInfo('PIRsensorSwitch:override',1,'Cancel timer on change');")); // 0 is field type, 1 is actual field + oappend(F("addInfo('PIRsensorSwitch:HA-discovery',1,'HA=Home Assistant');")); // 0 is field type, 1 is actual field + oappend(F("addInfo('PIRsensorSwitch:override',1,'Cancel timer on change');")); // 0 is field type, 1 is actual field for (int i = 0; i < PIR_SENSOR_MAX_SENSORS; i++) { char str[128]; sprintf_P(str, PSTR("addInfo('PIRsensorSwitch:pin[]',%d,'','#%d');"), i, i); diff --git a/usermods/ST7789_display/ST7789_display.h b/usermods/ST7789_display/ST7789_display.h index 0dbada382f..65f4cae5d3 100644 --- a/usermods/ST7789_display/ST7789_display.h +++ b/usermods/ST7789_display/ST7789_display.h @@ -377,10 +377,10 @@ class St7789DisplayUsermod : public Usermod { void appendConfigData() override { - oappend(SET_F("addInfo('ST7789:pin[]',0,'','SPI CS');")); - oappend(SET_F("addInfo('ST7789:pin[]',1,'','SPI DC');")); - oappend(SET_F("addInfo('ST7789:pin[]',2,'','SPI RST');")); - oappend(SET_F("addInfo('ST7789:pin[]',3,'','SPI BL');")); + oappend(F("addInfo('ST7789:pin[]',0,'','SPI CS');")); + oappend(F("addInfo('ST7789:pin[]',1,'','SPI DC');")); + oappend(F("addInfo('ST7789:pin[]',2,'','SPI RST');")); + oappend(F("addInfo('ST7789:pin[]',3,'','SPI BL');")); } /* diff --git a/usermods/Temperature/usermod_temperature.h b/usermods/Temperature/usermod_temperature.h index ad755eaeec..178bc05a0d 100644 --- a/usermods/Temperature/usermod_temperature.h +++ b/usermods/Temperature/usermod_temperature.h @@ -435,10 +435,10 @@ bool UsermodTemperature::readFromConfig(JsonObject &root) { } void UsermodTemperature::appendConfigData() { - oappend(SET_F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(SET_F(":")); oappend(String(FPSTR(_parasite)).c_str()); - oappend(SET_F("',1,'(if no Vcc connected)');")); // 0 is field type, 1 is actual field - oappend(SET_F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(SET_F(":")); oappend(String(FPSTR(_parasitePin)).c_str()); - oappend(SET_F("',1,'(for external MOSFET)');")); // 0 is field type, 1 is actual field + oappend(F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(F(":")); oappend(String(FPSTR(_parasite)).c_str()); + oappend(F("',1,'(if no Vcc connected)');")); // 0 is field type, 1 is actual field + oappend(F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(F(":")); oappend(String(FPSTR(_parasitePin)).c_str()); + oappend(F("',1,'(for external MOSFET)');")); // 0 is field type, 1 is actual field } float UsermodTemperature::getTemperature() { diff --git a/usermods/boblight/boblight.h b/usermods/boblight/boblight.h index 916f7da988..b04b78fac7 100644 --- a/usermods/boblight/boblight.h +++ b/usermods/boblight/boblight.h @@ -305,14 +305,14 @@ class BobLightUsermod : public Usermod { } void appendConfigData() override { - //oappend(SET_F("dd=addDropdown('usermod','selectfield');")); - //oappend(SET_F("addOption(dd,'1st value',0);")); - //oappend(SET_F("addOption(dd,'2nd value',1);")); - oappend(SET_F("addInfo('BobLight:top',1,'LEDs');")); // 0 is field type, 1 is actual field - oappend(SET_F("addInfo('BobLight:bottom',1,'LEDs');")); // 0 is field type, 1 is actual field - oappend(SET_F("addInfo('BobLight:left',1,'LEDs');")); // 0 is field type, 1 is actual field - oappend(SET_F("addInfo('BobLight:right',1,'LEDs');")); // 0 is field type, 1 is actual field - oappend(SET_F("addInfo('BobLight:pct',1,'Depth of scan [%]');")); // 0 is field type, 1 is actual field + //oappend(F("dd=addDropdown('usermod','selectfield');")); + //oappend(F("addOption(dd,'1st value',0);")); + //oappend(F("addOption(dd,'2nd value',1);")); + oappend(F("addInfo('BobLight:top',1,'LEDs');")); // 0 is field type, 1 is actual field + oappend(F("addInfo('BobLight:bottom',1,'LEDs');")); // 0 is field type, 1 is actual field + oappend(F("addInfo('BobLight:left',1,'LEDs');")); // 0 is field type, 1 is actual field + oappend(F("addInfo('BobLight:right',1,'LEDs');")); // 0 is field type, 1 is actual field + oappend(F("addInfo('BobLight:pct',1,'Depth of scan [%]');")); // 0 is field type, 1 is actual field } void addToConfig(JsonObject& root) override { diff --git a/usermods/multi_relay/usermod_multi_relay.h b/usermods/multi_relay/usermod_multi_relay.h index 33a6cf85e2..c4446c7a20 100644 --- a/usermods/multi_relay/usermod_multi_relay.h +++ b/usermods/multi_relay/usermod_multi_relay.h @@ -264,7 +264,7 @@ void MultiRelay::handleOffTimer() { void MultiRelay::InitHtmlAPIHandle() { // https://github.com/me-no-dev/ESPAsyncWebServer DEBUG_PRINTLN(F("Relays: Initialize HTML API")); - server.on(SET_F("/relays"), HTTP_GET, [this](AsyncWebServerRequest *request) { + server.on(F("/relays"), HTTP_GET, [this](AsyncWebServerRequest *request) { DEBUG_PRINTLN(F("Relays: HTML API")); String janswer; String error = ""; @@ -765,10 +765,10 @@ void MultiRelay::addToConfig(JsonObject &root) { } void MultiRelay::appendConfigData() { - oappend(SET_F("addInfo('MultiRelay:PCF8574-address',1,'(not hex!)');")); - oappend(SET_F("addInfo('MultiRelay:broadcast-sec',1,'(MQTT message)');")); - //oappend(SET_F("addInfo('MultiRelay:relay-0:pin',1,'(use -1 for PCF8574)');")); - oappend(SET_F("d.extra.push({'MultiRelay':{pin:[['P0',100],['P1',101],['P2',102],['P3',103],['P4',104],['P5',105],['P6',106],['P7',107]]}});")); + oappend(F("addInfo('MultiRelay:PCF8574-address',1,'(not hex!)');")); + oappend(F("addInfo('MultiRelay:broadcast-sec',1,'(MQTT message)');")); + //oappend(F("addInfo('MultiRelay:relay-0:pin',1,'(use -1 for PCF8574)');")); + oappend(F("d.extra.push({'MultiRelay':{pin:[['P0',100],['P1',101],['P2',102],['P3',103],['P4',104],['P5',105],['P6',106],['P7',107]]}});")); } /** diff --git a/usermods/pixels_dice_tray/pixels_dice_tray.h b/usermods/pixels_dice_tray/pixels_dice_tray.h index a1e45ba33b..61348ebb8e 100644 --- a/usermods/pixels_dice_tray/pixels_dice_tray.h +++ b/usermods/pixels_dice_tray/pixels_dice_tray.h @@ -387,23 +387,23 @@ class PixelsDiceTrayUsermod : public Usermod { // To work around this, add info text to the end of the preceding item. // // See addInfo in wled00/data/settings_um.htm for details on what this function does. - oappend(SET_F( + oappend(F( "addInfo('DiceTray:ble_scan_duration',1,'

Set to \"*\" to " "connect to any die.
Leave Blank to disable.

Saving will replace \"*\" with die names.','');")); #if USING_TFT_DISPLAY - oappend(SET_F("ddr=addDropdown('DiceTray','rotation');")); - oappend(SET_F("addOption(ddr,'0 deg',0);")); - oappend(SET_F("addOption(ddr,'90 deg',1);")); - oappend(SET_F("addOption(ddr,'180 deg',2);")); - oappend(SET_F("addOption(ddr,'270 deg',3);")); - oappend(SET_F( + oappend(F("ddr=addDropdown('DiceTray','rotation');")); + oappend(F("addOption(ddr,'0 deg',0);")); + oappend(F("addOption(ddr,'90 deg',1);")); + oappend(F("addOption(ddr,'180 deg',2);")); + oappend(F("addOption(ddr,'270 deg',3);")); + oappend(F( "addInfo('DiceTray:rotation',1,'
DO NOT CHANGE " "SPI PINS.
CHANGES ARE IGNORED.','');")); - oappend(SET_F("addInfo('TFT:pin[]',0,'','SPI CS');")); - oappend(SET_F("addInfo('TFT:pin[]',1,'','SPI DC');")); - oappend(SET_F("addInfo('TFT:pin[]',2,'','SPI RST');")); - oappend(SET_F("addInfo('TFT:pin[]',3,'','SPI BL');")); + oappend(F("addInfo('TFT:pin[]',0,'','SPI CS');")); + oappend(F("addInfo('TFT:pin[]',1,'','SPI DC');")); + oappend(F("addInfo('TFT:pin[]',2,'','SPI RST');")); + oappend(F("addInfo('TFT:pin[]',3,'','SPI BL');")); #endif } diff --git a/usermods/sht/usermod_sht.h b/usermods/sht/usermod_sht.h index c6e17221be..f10c78a251 100644 --- a/usermods/sht/usermod_sht.h +++ b/usermods/sht/usermod_sht.h @@ -310,22 +310,22 @@ void ShtUsermod::onMqttConnect(bool sessionPresent) { * @return void */ void ShtUsermod::appendConfigData() { - oappend(SET_F("dd=addDropdown('")); + oappend(F("dd=addDropdown('")); oappend(_name); - oappend(SET_F("','")); + oappend(F("','")); oappend(_shtType); - oappend(SET_F("');")); - oappend(SET_F("addOption(dd,'SHT30',0);")); - oappend(SET_F("addOption(dd,'SHT31',1);")); - oappend(SET_F("addOption(dd,'SHT35',2);")); - oappend(SET_F("addOption(dd,'SHT85',3);")); - oappend(SET_F("dd=addDropdown('")); + oappend(F("');")); + oappend(F("addOption(dd,'SHT30',0);")); + oappend(F("addOption(dd,'SHT31',1);")); + oappend(F("addOption(dd,'SHT35',2);")); + oappend(F("addOption(dd,'SHT85',3);")); + oappend(F("dd=addDropdown('")); oappend(_name); - oappend(SET_F("','")); + oappend(F("','")); oappend(_unitOfTemp); - oappend(SET_F("');")); - oappend(SET_F("addOption(dd,'Celsius',0);")); - oappend(SET_F("addOption(dd,'Fahrenheit',1);")); + oappend(F("');")); + oappend(F("addOption(dd,'Celsius',0);")); + oappend(F("addOption(dd,'Fahrenheit',1);")); } /** diff --git a/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h b/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h index dfab7e6ffb..684dd86e46 100644 --- a/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h +++ b/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h @@ -1202,21 +1202,21 @@ void FourLineDisplayUsermod::onUpdateBegin(bool init) { //} void FourLineDisplayUsermod::appendConfigData() { - oappend(SET_F("dd=addDropdown('4LineDisplay','type');")); - oappend(SET_F("addOption(dd,'None',0);")); - oappend(SET_F("addOption(dd,'SSD1306',1);")); - oappend(SET_F("addOption(dd,'SH1106',2);")); - oappend(SET_F("addOption(dd,'SSD1306 128x64',3);")); - oappend(SET_F("addOption(dd,'SSD1305',4);")); - oappend(SET_F("addOption(dd,'SSD1305 128x64',5);")); - oappend(SET_F("addOption(dd,'SSD1309 128x64',9);")); - oappend(SET_F("addOption(dd,'SSD1306 SPI',6);")); - oappend(SET_F("addOption(dd,'SSD1306 SPI 128x64',7);")); - oappend(SET_F("addOption(dd,'SSD1309 SPI 128x64',8);")); - oappend(SET_F("addInfo('4LineDisplay:type',1,'
Change may require reboot','');")); - oappend(SET_F("addInfo('4LineDisplay:pin[]',0,'','SPI CS');")); - oappend(SET_F("addInfo('4LineDisplay:pin[]',1,'','SPI DC');")); - oappend(SET_F("addInfo('4LineDisplay:pin[]',2,'','SPI RST');")); + oappend(F("dd=addDropdown('4LineDisplay','type');")); + oappend(F("addOption(dd,'None',0);")); + oappend(F("addOption(dd,'SSD1306',1);")); + oappend(F("addOption(dd,'SH1106',2);")); + oappend(F("addOption(dd,'SSD1306 128x64',3);")); + oappend(F("addOption(dd,'SSD1305',4);")); + oappend(F("addOption(dd,'SSD1305 128x64',5);")); + oappend(F("addOption(dd,'SSD1309 128x64',9);")); + oappend(F("addOption(dd,'SSD1306 SPI',6);")); + oappend(F("addOption(dd,'SSD1306 SPI 128x64',7);")); + oappend(F("addOption(dd,'SSD1309 SPI 128x64',8);")); + oappend(F("addInfo('4LineDisplay:type',1,'
Change may require reboot','');")); + oappend(F("addInfo('4LineDisplay:pin[]',0,'','SPI CS');")); + oappend(F("addInfo('4LineDisplay:pin[]',1,'','SPI DC');")); + oappend(F("addInfo('4LineDisplay:pin[]',2,'','SPI RST');")); } /* diff --git a/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h b/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h index 55715b7c76..383c1193eb 100644 --- a/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h +++ b/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h @@ -1090,8 +1090,8 @@ void RotaryEncoderUIUsermod::addToConfig(JsonObject &root) { } void RotaryEncoderUIUsermod::appendConfigData() { - oappend(SET_F("addInfo('Rotary-Encoder:PCF8574-address',1,'(not hex!)');")); - oappend(SET_F("d.extra.push({'Rotary-Encoder':{pin:[['P0',100],['P1',101],['P2',102],['P3',103],['P4',104],['P5',105],['P6',106],['P7',107]]}});")); + oappend(F("addInfo('Rotary-Encoder:PCF8574-address',1,'(not hex!)');")); + oappend(F("d.extra.push({'Rotary-Encoder':{pin:[['P0',100],['P1',101],['P2',102],['P3',103],['P4',104],['P5',105],['P6',106],['P7',107]]}});")); } /** diff --git a/usermods/usermod_v2_word_clock/usermod_v2_word_clock.h b/usermods/usermod_v2_word_clock/usermod_v2_word_clock.h index b66be290a5..7ecec08e59 100644 --- a/usermods/usermod_v2_word_clock/usermod_v2_word_clock.h +++ b/usermods/usermod_v2_word_clock/usermod_v2_word_clock.h @@ -433,8 +433,8 @@ class WordClockUsermod : public Usermod void appendConfigData() { - oappend(SET_F("addInfo('WordClockUsermod:ledOffset', 1, 'Number of LEDs before the letters');")); - oappend(SET_F("addInfo('WordClockUsermod:Norddeutsch', 1, 'Viertel vor instead of Dreiviertel');")); + oappend(F("addInfo('WordClockUsermod:ledOffset', 1, 'Number of LEDs before the letters');")); + oappend(F("addInfo('WordClockUsermod:Norddeutsch', 1, 'Viertel vor instead of Dreiviertel');")); } /* diff --git a/usermods/wireguard/wireguard.h b/usermods/wireguard/wireguard.h index 8c88d00018..8656a704af 100644 --- a/usermods/wireguard/wireguard.h +++ b/usermods/wireguard/wireguard.h @@ -54,13 +54,13 @@ class WireguardUsermod : public Usermod { } void appendConfigData() { - oappend(SET_F("addInfo('WireGuard:host',1,'Server Hostname');")); // 0 is field type, 1 is actual field - oappend(SET_F("addInfo('WireGuard:port',1,'Server Port');")); // 0 is field type, 1 is actual field - oappend(SET_F("addInfo('WireGuard:ip',1,'Device IP');")); // 0 is field type, 1 is actual field - oappend(SET_F("addInfo('WireGuard:psk',1,'Pre Shared Key (optional)');")); // 0 is field type, 1 is actual field - oappend(SET_F("addInfo('WireGuard:pem',1,'Private Key');")); // 0 is field type, 1 is actual field - oappend(SET_F("addInfo('WireGuard:pub',1,'Public Key');")); // 0 is field type, 1 is actual field - oappend(SET_F("addInfo('WireGuard:tz',1,'POSIX timezone string');")); // 0 is field type, 1 is actual field + oappend(F("addInfo('WireGuard:host',1,'Server Hostname');")); // 0 is field type, 1 is actual field + oappend(F("addInfo('WireGuard:port',1,'Server Port');")); // 0 is field type, 1 is actual field + oappend(F("addInfo('WireGuard:ip',1,'Device IP');")); // 0 is field type, 1 is actual field + oappend(F("addInfo('WireGuard:psk',1,'Pre Shared Key (optional)');")); // 0 is field type, 1 is actual field + oappend(F("addInfo('WireGuard:pem',1,'Private Key');")); // 0 is field type, 1 is actual field + oappend(F("addInfo('WireGuard:pub',1,'Public Key');")); // 0 is field type, 1 is actual field + oappend(F("addInfo('WireGuard:tz',1,'POSIX timezone string');")); // 0 is field type, 1 is actual field } void addToConfig(JsonObject& root) { From 832599b8c51ba7df3a565901ddf338b18d82f8a4 Mon Sep 17 00:00:00 2001 From: Svennte <105973347+Svennte@users.noreply.github.com> Date: Fri, 25 Oct 2024 09:33:05 +0200 Subject: [PATCH 129/145] Fix alexa devices invisible/uncontrollable (#4214) Fix for LED and Scenes uncontrollable using Alexa. Weird behavior regarding to the device names and shared scenes fixed with this. Seen in issue Aircoookie/Espalexa#228 and fixed from @ams-hh Tested by myself and works just fine. Created second pull request here because the library seems to be a bit different from the official Espalexa repo. --------- Co-authored-by: Frank <91616163+softhack007@users.noreply.github.com> Co-authored-by: Blaz Kristan --- wled00/src/dependencies/espalexa/Espalexa.h | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/wled00/src/dependencies/espalexa/Espalexa.h b/wled00/src/dependencies/espalexa/Espalexa.h index 5c780e248e..ae761e9faa 100644 --- a/wled00/src/dependencies/espalexa/Espalexa.h +++ b/wled00/src/dependencies/espalexa/Espalexa.h @@ -120,10 +120,8 @@ class Espalexa { void encodeLightId(uint8_t idx, char* out) { - uint8_t mac[6]; - WiFi.macAddress(mac); - - sprintf_P(out, PSTR("%02X:%02X:%02X:%02X:%02X:%02X:00:11-%02X"), mac[0],mac[1],mac[2],mac[3],mac[4],mac[5], idx); + String mymac = WiFi.macAddress(); + sprintf_P(out, PSTR("%02X:%s:AB-%02X"), idx, mymac.c_str(), idx); } // construct 'globally unique' Json dict key fitting into signed int From 4cc2cc4ad40b4611124587f7496f1554abd9ef07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Kristan?= Date: Sat, 26 Oct 2024 15:16:11 +0200 Subject: [PATCH 130/145] Multiple fixes - increase WLED_MAX_BUSSES for C3 (fixes #4215) - fix for #4228 - fix for very long running effect (strip.now, strip.timebase) - C++ API change to allow `seg.setColor().setOpacity()` --- .../stairway-wipe-usermod-v2.h | 2 +- .../stairway_wipe_basic/wled06_usermod.ino | 2 +- wled00/FX.h | 35 ++++++---- wled00/FX_2Dfcn.cpp | 4 +- wled00/FX_fcn.cpp | 70 ++++++++----------- wled00/button.cpp | 1 + wled00/const.h | 2 +- wled00/fcn_declare.h | 1 - wled00/json.cpp | 4 +- wled00/led.cpp | 9 +-- wled00/wled.h | 2 +- 11 files changed, 62 insertions(+), 70 deletions(-) diff --git a/usermods/stairway_wipe_basic/stairway-wipe-usermod-v2.h b/usermods/stairway_wipe_basic/stairway-wipe-usermod-v2.h index f712316b86..707479df17 100644 --- a/usermods/stairway_wipe_basic/stairway-wipe-usermod-v2.h +++ b/usermods/stairway_wipe_basic/stairway-wipe-usermod-v2.h @@ -96,7 +96,7 @@ void setup() { jsonTransitionOnce = true; strip.setTransition(0); //no transition effectCurrent = FX_MODE_COLOR_WIPE; - resetTimebase(); //make sure wipe starts from beginning + strip.resetTimebase(); //make sure wipe starts from beginning //set wipe direction Segment& seg = strip.getSegment(0); diff --git a/usermods/stairway_wipe_basic/wled06_usermod.ino b/usermods/stairway_wipe_basic/wled06_usermod.ino index c1264ebfb2..dc2159ee9d 100644 --- a/usermods/stairway_wipe_basic/wled06_usermod.ino +++ b/usermods/stairway_wipe_basic/wled06_usermod.ino @@ -86,7 +86,7 @@ void startWipe() bri = briLast; //turn on transitionDelayTemp = 0; //no transition effectCurrent = FX_MODE_COLOR_WIPE; - resetTimebase(); //make sure wipe starts from beginning + strip.resetTimebase(); //make sure wipe starts from beginning //set wipe direction Segment& seg = strip.getSegment(0); diff --git a/wled00/FX.h b/wled00/FX.h index 385c524769..ad39a7c06d 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -525,12 +525,12 @@ typedef struct Segment { inline static const CRGBPalette16 &getCurrentPalette() { return Segment::_currentPalette; } void setUp(uint16_t i1, uint16_t i2, uint8_t grp=1, uint8_t spc=0, uint16_t ofs=UINT16_MAX, uint16_t i1Y=0, uint16_t i2Y=1); - bool setColor(uint8_t slot, uint32_t c); //returns true if changed - void setCCT(uint16_t k); - void setOpacity(uint8_t o); - void setOption(uint8_t n, bool val); - void setMode(uint8_t fx, bool loadDefaults = false); - void setPalette(uint8_t pal); + Segment &setColor(uint8_t slot, uint32_t c); + Segment &setCCT(uint16_t k); + Segment &setOpacity(uint8_t o); + Segment &setOption(uint8_t n, bool val); + Segment &setMode(uint8_t fx, bool loadDefaults = false); + Segment &setPalette(uint8_t pal); uint8_t differs(Segment& b) const; void refreshLightCapabilities(); @@ -545,7 +545,7 @@ typedef struct Segment { * Call resetIfRequired before calling the next effect function. * Safe to call from interrupts and network requests. */ - inline void markForReset() { reset = true; } // setOption(SEG_OPTION_RESET, true) + inline Segment &markForReset() { reset = true; return *this; } // setOption(SEG_OPTION_RESET, true) // transition functions void startTransition(uint16_t dur); // transition has to start before actual segment values change @@ -599,9 +599,15 @@ typedef struct Segment { } // 2D matrix - [[gnu::hot]] uint16_t virtualWidth() const; // segment width in virtual pixels (accounts for groupping and spacing) - [[gnu::hot]] uint16_t virtualHeight() const; // segment height in virtual pixels (accounts for groupping and spacing) - uint16_t nrOfVStrips() const; // returns number of virtual vertical strips in 2D matrix (used to expand 1D effects into 2D) + [[gnu::hot]] unsigned virtualWidth() const; // segment width in virtual pixels (accounts for groupping and spacing) + [[gnu::hot]] unsigned virtualHeight() const; // segment height in virtual pixels (accounts for groupping and spacing) + inline unsigned nrOfVStrips() const { // returns number of virtual vertical strips in 2D matrix (used to expand 1D effects into 2D) + #ifndef WLED_DISABLE_2D + return (is2D() && map1D2D == M12_pBar) ? virtualWidth() : 1; + #else + return 1; + #endif + } #ifndef WLED_DISABLE_2D [[gnu::hot]] uint16_t XY(int x, int y); // support function to get relative index within segment [[gnu::hot]] void setPixelColorXY(int x, int y, uint32_t c); // set relative pixel within segment with color @@ -778,7 +784,8 @@ class WS2812FX { // 96 bytes setTargetFps(uint8_t fps), setupEffectData(); // add default effects to the list; defined in FX.cpp - inline void restartRuntime() { for (Segment &seg : _segments) seg.markForReset(); } + inline void resetTimebase() { timebase = 0UL - millis(); } + inline void restartRuntime() { for (Segment &seg : _segments) { seg.markForReset().resetIfRequired(); } } inline void setTransitionMode(bool t) { for (Segment &seg : _segments) seg.startTransition(t ? _transitionDur : 0); } inline void setColor(uint8_t slot, uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0) { setColor(slot, RGBW32(r,g,b,w)); } inline void setPixelColor(unsigned n, uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0) { setPixelColor(n, RGBW32(r,g,b,w)); } @@ -834,10 +841,8 @@ class WS2812FX { // 96 bytes inline uint16_t getLength() const { return _length; } // returns actual amount of LEDs on a strip (2D matrix may have less LEDs than W*H) inline uint16_t getTransition() const { return _transitionDur; } // returns currently set transition time (in ms) - uint32_t - now, - timebase, - getPixelColor(uint16_t) const; + unsigned long now, timebase; + uint32_t getPixelColor(unsigned) const; inline uint32_t getLastShow() const { return _lastShow; } // returns millis() timestamp of last strip.show() call inline uint32_t segColor(uint8_t i) const { return _colors_t[i]; } // returns currently valid color (for slot i) AKA SEGCOLOR(); may be blended between two colors while in transition diff --git a/wled00/FX_2Dfcn.cpp b/wled00/FX_2Dfcn.cpp index e38602ebc0..7c1ae366b7 100644 --- a/wled00/FX_2Dfcn.cpp +++ b/wled00/FX_2Dfcn.cpp @@ -156,7 +156,7 @@ uint16_t IRAM_ATTR_YN Segment::XY(int x, int y) void IRAM_ATTR_YN Segment::setPixelColorXY(int x, int y, uint32_t col) { if (!isActive()) return; // not active - if (x >= virtualWidth() || y >= virtualHeight() || x<0 || y<0) return; // if pixel would fall out of virtual segment just exit + if ((unsigned)x >= virtualWidth() || (unsigned)y >= virtualHeight() || x<0 || y<0) return; // if pixel would fall out of virtual segment just exit uint8_t _bri_t = currentBri(); if (_bri_t < 255) { @@ -251,7 +251,7 @@ void Segment::setPixelColorXY(float x, float y, uint32_t col, bool aa) // returns RGBW values of pixel uint32_t IRAM_ATTR_YN Segment::getPixelColorXY(int x, int y) const { if (!isActive()) return 0; // not active - if (x >= virtualWidth() || y >= virtualHeight() || x<0 || y<0) return 0; // if pixel would fall out of virtual segment just exit + if ((unsigned)x >= virtualWidth() || (unsigned)y >= virtualHeight() || x<0 || y<0) return 0; // if pixel would fall out of virtual segment just exit if (reverse ) x = virtualWidth() - x - 1; if (reverse_y) y = virtualHeight() - y - 1; if (transpose) { std::swap(x,y); } // swap X & Y if segment transposed diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 79189ef57a..949b6a932b 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -509,46 +509,53 @@ void Segment::setUp(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, uint16_t } -bool Segment::setColor(uint8_t slot, uint32_t c) { //returns true if changed - if (slot >= NUM_COLORS || c == colors[slot]) return false; +Segment &Segment::setColor(uint8_t slot, uint32_t c) { + if (slot >= NUM_COLORS || c == colors[slot]) return *this; if (!_isRGB && !_hasW) { - if (slot == 0 && c == BLACK) return false; // on/off segment cannot have primary color black - if (slot == 1 && c != BLACK) return false; // on/off segment cannot have secondary color non black + if (slot == 0 && c == BLACK) return *this; // on/off segment cannot have primary color black + if (slot == 1 && c != BLACK) return *this; // on/off segment cannot have secondary color non black } if (fadeTransition) startTransition(strip.getTransition()); // start transition prior to change colors[slot] = c; stateChanged = true; // send UDP/WS broadcast - return true; + return *this; } -void Segment::setCCT(uint16_t k) { +Segment &Segment::setCCT(uint16_t k) { if (k > 255) { //kelvin value, convert to 0-255 if (k < 1900) k = 1900; if (k > 10091) k = 10091; k = (k - 1900) >> 5; } - if (cct == k) return; - if (fadeTransition) startTransition(strip.getTransition()); // start transition prior to change - cct = k; - stateChanged = true; // send UDP/WS broadcast + if (cct != k) { + //DEBUGFX_PRINTF_P(PSTR("- Starting CCT transition: %d\n"), k); + startTransition(strip.getTransition()); // start transition prior to change + cct = k; + stateChanged = true; // send UDP/WS broadcast + } + return *this; } -void Segment::setOpacity(uint8_t o) { - if (opacity == o) return; - if (fadeTransition) startTransition(strip.getTransition()); // start transition prior to change - opacity = o; - stateChanged = true; // send UDP/WS broadcast +Segment &Segment::setOpacity(uint8_t o) { + if (opacity != o) { + //DEBUGFX_PRINTF_P(PSTR("- Starting opacity transition: %d\n"), o); + startTransition(strip.getTransition()); // start transition prior to change + opacity = o; + stateChanged = true; // send UDP/WS broadcast + } + return *this; } -void Segment::setOption(uint8_t n, bool val) { +Segment &Segment::setOption(uint8_t n, bool val) { bool prevOn = on; if (fadeTransition && n == SEG_OPTION_ON && val != prevOn) startTransition(strip.getTransition()); // start transition prior to change if (val) options |= 0x01 << n; else options &= ~(0x01 << n); if (!(n == SEG_OPTION_SELECTED || n == SEG_OPTION_RESET)) stateChanged = true; // send UDP/WS broadcast + return *this; } -void Segment::setMode(uint8_t fx, bool loadDefaults) { +Segment &Segment::setMode(uint8_t fx, bool loadDefaults) { // skip reserved while (fx < strip.getModeCount() && strncmp_P("RSVD", strip.getModeData(fx), 4) == 0) fx++; if (fx >= strip.getModeCount()) fx = 0; // set solid mode @@ -580,9 +587,10 @@ void Segment::setMode(uint8_t fx, bool loadDefaults) { markForReset(); stateChanged = true; // send UDP/WS broadcast } + return *this; } -void Segment::setPalette(uint8_t pal) { +Segment &Segment::setPalette(uint8_t pal) { if (pal < 245 && pal > GRADIENT_PALETTE_COUNT+13) pal = 0; // built in palettes if (pal > 245 && (strip.customPalettes.size() == 0 || 255U-pal > strip.customPalettes.size()-1)) pal = 0; // custom palettes if (pal != palette) { @@ -590,37 +598,24 @@ void Segment::setPalette(uint8_t pal) { palette = pal; stateChanged = true; // send UDP/WS broadcast } + return *this; } // 2D matrix -uint16_t IRAM_ATTR Segment::virtualWidth() const { +unsigned IRAM_ATTR Segment::virtualWidth() const { unsigned groupLen = groupLength(); unsigned vWidth = ((transpose ? height() : width()) + groupLen - 1) / groupLen; if (mirror) vWidth = (vWidth + 1) /2; // divide by 2 if mirror, leave at least a single LED return vWidth; } -uint16_t IRAM_ATTR Segment::virtualHeight() const { +unsigned IRAM_ATTR Segment::virtualHeight() const { unsigned groupLen = groupLength(); unsigned vHeight = ((transpose ? width() : height()) + groupLen - 1) / groupLen; if (mirror_y) vHeight = (vHeight + 1) /2; // divide by 2 if mirror, leave at least a single LED return vHeight; } -uint16_t IRAM_ATTR_YN Segment::nrOfVStrips() const { - unsigned vLen = 1; -#ifndef WLED_DISABLE_2D - if (is2D()) { - switch (map1D2D) { - case M12_pBar: - vLen = virtualWidth(); - break; - } - } -#endif - return vLen; -} - // Constants for mapping mode "Pinwheel" #ifndef WLED_DISABLE_2D constexpr int Pinwheel_Steps_Small = 72; // no holes up to 16x16 @@ -1187,10 +1182,7 @@ uint32_t Segment::color_from_palette(uint16_t i, bool mapping, bool wrap, uint8_ //do not call this method from system context (network callback) void WS2812FX::finalizeInit() { //reset segment runtimes - for (segment &seg : _segments) { - seg.markForReset(); - seg.resetIfRequired(); - } + restartRuntime(); // for the lack of better place enumerate ledmaps here // if we do it in json.cpp (serializeInfo()) we are getting flashes on LEDs @@ -1402,7 +1394,7 @@ void IRAM_ATTR WS2812FX::setPixelColor(unsigned i, uint32_t col) { BusManager::setPixelColor(i, col); } -uint32_t IRAM_ATTR WS2812FX::getPixelColor(uint16_t i) const { +uint32_t IRAM_ATTR WS2812FX::getPixelColor(unsigned i) const { i = getMappedPixelIndex(i); if (i >= _length) return 0; return BusManager::getPixelColor(i); diff --git a/wled00/button.cpp b/wled00/button.cpp index f02ed3d6d8..4d6f954f60 100644 --- a/wled00/button.cpp +++ b/wled00/button.cpp @@ -215,6 +215,7 @@ void handleAnalog(uint8_t b) briLast = bri; bri = 0; } else { + if (bri == 0) strip.restartRuntime(); bri = aRead; } } else if (macroDoublePress[b] == 249) { diff --git a/wled00/const.h b/wled00/const.h index 14ec23b58a..07873deca1 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -53,7 +53,7 @@ #else #define WLED_MAX_ANALOG_CHANNELS (LEDC_CHANNEL_MAX*LEDC_SPEED_MODE_MAX) #if defined(CONFIG_IDF_TARGET_ESP32C3) // 2 RMT, 6 LEDC, only has 1 I2S but NPB does not support it ATM - #define WLED_MAX_BUSSES 4 // will allow 2 digital & 2 analog RGB + #define WLED_MAX_BUSSES 6 // will allow 2 digital & 2 analog RGB or 6 PWM white #define WLED_MAX_DIGITAL_CHANNELS 2 //#define WLED_MAX_ANALOG_CHANNELS 6 #define WLED_MIN_VIRTUAL_BUSSES 3 diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index d44ed43a0d..1855a8b63b 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -169,7 +169,6 @@ bool serveLiveLeds(AsyncWebServerRequest* request, uint32_t wsClient = 0); void setValuesFromSegment(uint8_t s); void setValuesFromMainSeg(); void setValuesFromFirstSelectedSeg(); -void resetTimebase(); void toggleOnOff(); void applyBri(); void applyFinalBri(); diff --git a/wled00/json.cpp b/wled00/json.cpp index 06eb3015e5..288059653f 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -346,7 +346,7 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId) } } - int tr = -1; + long tr = -1; if (!presetId || currentPlaylist < 0) { //do not apply transition time from preset if playlist active, as it would override playlist transition times tr = root[F("transition")] | -1; if (tr >= 0) { @@ -363,7 +363,7 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId) } tr = root[F("tb")] | -1; - if (tr >= 0) strip.timebase = ((uint32_t)tr) - millis(); + if (tr >= 0) strip.timebase = (unsigned long)tr - millis(); JsonObject nl = root["nl"]; nightlightActive = getBoolVal(nl["on"], nightlightActive); diff --git a/wled00/led.cpp b/wled00/led.cpp index 9de0495b45..9b97091e64 100644 --- a/wled00/led.cpp +++ b/wled00/led.cpp @@ -47,17 +47,12 @@ void applyValuesToSelectedSegs() } -void resetTimebase() -{ - strip.timebase = 0 - millis(); -} - - void toggleOnOff() { if (bri == 0) { bri = briLast; + strip.restartRuntime(); } else { briLast = bri; @@ -122,7 +117,7 @@ void stateUpdated(byte callMode) { nightlightStartTime = millis(); } if (briT == 0) { - if (callMode != CALL_MODE_NOTIFICATION) resetTimebase(); //effect start from beginning + if (callMode != CALL_MODE_NOTIFICATION) strip.resetTimebase(); //effect start from beginning } if (bri > 0) briLast = bri; diff --git a/wled00/wled.h b/wled00/wled.h index bc525cd6fa..912e0b3c03 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -8,7 +8,7 @@ */ // version code in format yymmddb (b = daily build) -#define VERSION 2410140 +#define VERSION 2410260 //uncomment this if you have a "my_config.h" file you'd like to use //#define WLED_USE_MY_CONFIG From 2703c9899afd6a9c6f3f41eccce03b6b11208a0a Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 27 Oct 2024 15:08:25 +0100 Subject: [PATCH 131/145] Bugfix in FX `ripple_base()` --- wled00/FX.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index d4b83de6cf..2f24f745a7 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -2506,9 +2506,9 @@ static uint16_t ripple_base() { #endif { int left = rippleorigin - propI -1; - int right = rippleorigin + propI +3; + int right = rippleorigin + propI +2; for (int v = 0; v < 4; v++) { - unsigned mag = scale8(cubicwave8((propF>>2)+(v-left)*64), amp); + unsigned mag = scale8(cubicwave8((propF>>2) + v * 64), amp); SEGMENT.setPixelColor(left + v, color_blend(SEGMENT.getPixelColor(left + v), col, mag)); // TODO SEGMENT.setPixelColor(right - v, color_blend(SEGMENT.getPixelColor(right - v), col, mag)); // TODO } From 6e89346f00e8b240915313f9d99a9121cbc60190 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Kristan?= Date: Sun, 27 Oct 2024 18:47:10 +0100 Subject: [PATCH 132/145] WLED 0.15.0-b7 release - fix for #4172 - fix for #4230 - /json/live enabled when WS disabled --- CHANGELOG.md | 6 ++++++ package-lock.json | 4 ++-- package.json | 2 +- wled00/data/index.css | 7 ++++++- wled00/wled.h | 7 ++++--- wled00/xml.cpp | 2 +- 6 files changed, 20 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4dad83d9b8..452e02b254 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ ## WLED changelog +#### Build 2410270 +- WLED 0.15.0-b7 release +- Add visual expand button on hover (#4172) +- `/json/live` (JSON live data/peek) only enabled when WebSockets are disabled +- Bugfixes: #4179, #4215, #4219, #4224, #4228, #4230 + #### Build 2410140 - WLED 0.15.0-b6 release - Added BRT timezone (#4188 by @LuisFadini) diff --git a/package-lock.json b/package-lock.json index 85ee1df0fc..0f73bff0c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "wled", - "version": "0.15.0-b6", + "version": "0.15.0-b7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "wled", - "version": "0.15.0-b6", + "version": "0.15.0-b7", "license": "ISC", "dependencies": { "clean-css": "^5.3.3", diff --git a/package.json b/package.json index d76d87687d..9d095c82cc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wled", - "version": "0.15.0-b6", + "version": "0.15.0-b7", "description": "Tools for WLED project", "main": "tools/cdata.js", "directories": { diff --git a/wled00/data/index.css b/wled00/data/index.css index 6f465e4072..0e36ff08c9 100644 --- a/wled00/data/index.css +++ b/wled00/data/index.css @@ -1040,7 +1040,7 @@ textarea { .segname .flr, .pname .flr { transform: rotate(0deg); - right: -6px; + /*right: -6px;*/ } /* segment power wrapper */ @@ -1335,6 +1335,11 @@ TD .checkmark, TD .radiomark { box-shadow: 0 0 10px 4px var(--c-1); } +.lstI .flr:hover { + background: var(--c-6); + border-radius: 100%; +} + #pcont .selected:not([class*="expanded"]) { bottom: 52px; top: 42px; diff --git a/wled00/wled.h b/wled00/wled.h index 912e0b3c03..2b3a77d24b 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -3,12 +3,12 @@ /* Main sketch, global variable declarations @title WLED project sketch - @version 0.15.0-b6 + @version 0.15.0-b7 @author Christian Schwinne */ // version code in format yymmddb (b = daily build) -#define VERSION 2410260 +#define VERSION 2410270 //uncomment this if you have a "my_config.h" file you'd like to use //#define WLED_USE_MY_CONFIG @@ -36,12 +36,13 @@ #undef WLED_ENABLE_ADALIGHT // disable has priority over enable #endif //#define WLED_ENABLE_DMX // uses 3.5kb -//#define WLED_ENABLE_JSONLIVE // peek LED output via /json/live (WS binary peek is always enabled) #ifndef WLED_DISABLE_LOXONE #define WLED_ENABLE_LOXONE // uses 1.2kb #endif #ifndef WLED_DISABLE_WEBSOCKETS #define WLED_ENABLE_WEBSOCKETS +#else + #define WLED_ENABLE_JSONLIVE // peek LED output via /json/live (WS binary peek is always enabled) #endif //#define WLED_DISABLE_ESPNOW // Removes dependence on esp now diff --git a/wled00/xml.cpp b/wled00/xml.cpp index 6d1ff2f863..dc26732712 100644 --- a/wled00/xml.cpp +++ b/wled00/xml.cpp @@ -501,7 +501,7 @@ void getSettingsJS(byte subPage, Print& settingsScript) #endif printSetFormValue(settingsScript,PSTR("BD"),serialBaud); #ifndef WLED_ENABLE_ADALIGHT - settingsScript.print(F("toggle('Serial);")); + settingsScript.print(F("toggle('Serial');")); #endif } From 4588219e3134481a9f3dc66077811bf5d16f7f88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Kristan?= Date: Mon, 28 Oct 2024 12:42:53 +0100 Subject: [PATCH 133/145] Update changelog --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 452e02b254..c570ac1f7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,12 @@ #### Build 2410270 - WLED 0.15.0-b7 release +- Re-license the WLED project from MIT to EUPL (#4194 by @Aircoookie) +- Fix alexa devices invisible/uncontrollable (#4214 by @Svennte) - Add visual expand button on hover (#4172) +- Usermod: Audioreactive tuning and performance enhancements (by @softhack007) - `/json/live` (JSON live data/peek) only enabled when WebSockets are disabled -- Bugfixes: #4179, #4215, #4219, #4224, #4228, #4230 +- Various bugfixes and optimisations: #4179, #4215, #4219, #4222, #4223, #4224, #4228, #4230 #### Build 2410140 - WLED 0.15.0-b6 release From 1898be2fe1c9c3e9c05b5b41d2a2d9b597930156 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Fri, 1 Nov 2024 21:13:43 +0100 Subject: [PATCH 134/145] S3 WROOM-2 buildenv this chip has 16MB or 32MB flash, and requires .memory_type = opi_opi --- platformio.ini | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/platformio.ini b/platformio.ini index 9628722aa4..48731f39bf 100644 --- a/platformio.ini +++ b/platformio.ini @@ -556,6 +556,32 @@ board_build.f_flash = 80000000L board_build.flash_mode = qio monitor_filters = esp32_exception_decoder +[env:esp32S3_wroom2] +;; For ESP32-S3 WROOM-2, a.k.a. ESP32-S3 DevKitC-1 v1.1 +;; with >= 16MB FLASH and >= 8MB PSRAM (memory_type: opi_opi) +platform = ${esp32s3.platform} +platform_packages = ${esp32s3.platform_packages} +board = esp32s3camlcd ;; this is the only standard board with "opi_opi" +board_build.arduino.memory_type = opi_opi +upload_speed = 921600 +build_unflags = ${common.build_unflags} +build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=ESP32-S3_WROOM-2 + -D CONFIG_LITTLEFS_FOR_IDF_3_2 -D WLED_WATCHDOG_TIMEOUT=0 + -D ARDUINO_USB_CDC_ON_BOOT=0 ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip + ;; -D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") + -DBOARD_HAS_PSRAM + -D LEDPIN=38 ;; buildin LED + -D BTNPIN=0 -D RLYPIN=16 -D IRPIN=17 -D AUDIOPIN=-1 + ${esp32.AR_build_flags} + -D SR_DMTYPE=1 -D I2S_SDPIN=13 -D I2S_CKPIN=14 -D I2S_WSPIN=15 -D MCLK_PIN=4 ;; I2S mic +lib_deps = ${esp32s3.lib_deps} + ${esp32.AR_lib_deps} + +board_build.partitions = ${esp32.extreme_partitions} +board_upload.flash_size = 16MB +board_upload.maximum_size = 16777216 +monitor_filters = esp32_exception_decoder + [env:esp32s3_4M_qspi] ;; ESP32-S3, with 4MB FLASH and <= 4MB PSRAM (memory_type: qio_qspi) board = lolin_s3_mini ;; -S3 mini, 4MB flash 2MB PSRAM From 749d34cd30a76e7c17be04f71016960d2331a4de Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Fri, 1 Nov 2024 21:31:57 +0100 Subject: [PATCH 135/145] pinmanager support for S3 WROOM-2 (pin 33-37 reserved for flash) --- wled00/pin_manager.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/wled00/pin_manager.cpp b/wled00/pin_manager.cpp index 793b5440c8..0d4c2ad5cb 100644 --- a/wled00/pin_manager.cpp +++ b/wled00/pin_manager.cpp @@ -201,7 +201,12 @@ bool PinManager::isPinOk(byte gpio, bool output) if (gpio > 18 && gpio < 21) return false; // 19 + 20 = USB-JTAG. Not recommended for other uses. #endif if (gpio > 21 && gpio < 33) return false; // 22 to 32: not connected + SPI FLASH - if (gpio > 32 && gpio < 38) return !psramFound(); // 33 to 37: not available if using _octal_ SPI Flash or _octal_ PSRAM + #if CONFIG_ESPTOOLPY_FLASHMODE_OPI // 33-37: never available if using _octal_ Flash (opi_opi) + if (gpio > 32 && gpio < 38) return false; + #endif + #if CONFIG_SPIRAM_MODE_OCT // 33-37: not available if using _octal_ PSRAM (qio_opi), but free to use on _quad_ PSRAM (qio_qspi) + if (gpio > 32 && gpio < 38) return !psramFound(); + #endif // 38 to 48 are for general use. Be careful about straping pins GPIO45 and GPIO46 - these may be pull-up or pulled-down on your board. #elif defined(CONFIG_IDF_TARGET_ESP32S2) // strapping pins: 0, 45 & 46 From 3c2c5bedc50845c168f7e0881e3a1d6aa973e7d5 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Fri, 1 Nov 2024 21:42:54 +0100 Subject: [PATCH 136/145] LEDPIN --> DATA_PINS --- platformio.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index 48731f39bf..6d4aa1dc13 100644 --- a/platformio.ini +++ b/platformio.ini @@ -570,8 +570,9 @@ build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME= -D ARDUINO_USB_CDC_ON_BOOT=0 ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip ;; -D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") -DBOARD_HAS_PSRAM - -D LEDPIN=38 ;; buildin LED + -D LEDPIN=38 -D DATA_PINS=38 ;; buildin WS2812b LED -D BTNPIN=0 -D RLYPIN=16 -D IRPIN=17 -D AUDIOPIN=-1 + -D WLED_DEBUG ${esp32.AR_build_flags} -D SR_DMTYPE=1 -D I2S_SDPIN=13 -D I2S_CKPIN=14 -D I2S_WSPIN=15 -D MCLK_PIN=4 ;; I2S mic lib_deps = ${esp32s3.lib_deps} From d98ca9a202795f9851bbeca7e895cb790abd2727 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Fri, 1 Nov 2024 21:51:46 +0100 Subject: [PATCH 137/145] show correct flash mode in WLED_DEBUG --- wled00/wled.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/wled00/wled.cpp b/wled00/wled.cpp index 13d43218af..d6a39a399f 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -380,6 +380,12 @@ void WLED::setup() case FM_QOUT: DEBUG_PRINT(F("(QOUT)"));break; case FM_DIO: DEBUG_PRINT(F("(DIO)")); break; case FM_DOUT: DEBUG_PRINT(F("(DOUT)"));break; + #if defined(CONFIG_IDF_TARGET_ESP32S3) && CONFIG_ESPTOOLPY_FLASHMODE_OPI + case FM_FAST_READ: DEBUG_PRINT(F("(OPI)")); break; + #else + case FM_FAST_READ: DEBUG_PRINT(F("(fast_read)")); break; + #endif + case FM_SLOW_READ: DEBUG_PRINT(F("(slow_read)")); break; default: break; } #endif From 70323b947745e81644b955d3d16cc2bd24059636 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Sat, 2 Nov 2024 17:50:30 +0100 Subject: [PATCH 138/145] rename delay -> frameDelay Avoiding name collisions with the 'delay' function. --- wled00/FX_fcn.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 949b6a932b..f45256f0fd 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -1329,7 +1329,7 @@ void WS2812FX::service() { if (nowUp > seg.next_time || _triggered || (doShow && seg.mode == FX_MODE_STATIC)) { doShow = true; - unsigned delay = FRAMETIME; + unsigned frameDelay = FRAMETIME; if (!seg.freeze) { //only run effect function if not frozen int oldCCT = BusManager::getSegmentCCT(); // store original CCT value (actually it is not Segment based) @@ -1349,7 +1349,7 @@ void WS2812FX::service() { // overwritten by later effect. To enable seamless blending for every effect, additional LED buffer // would need to be allocated for each effect and then blended together for each pixel. [[maybe_unused]] uint8_t tmpMode = seg.currentMode(); // this will return old mode while in transition - delay = (*_mode[seg.mode])(); // run new/current mode + frameDelay = (*_mode[seg.mode])(); // run new/current mode #ifndef WLED_DISABLE_MODE_BLEND if (modeBlending && seg.mode != tmpMode) { Segment::tmpsegd_t _tmpSegData; @@ -1358,16 +1358,16 @@ void WS2812FX::service() { _virtualSegmentLength = seg.virtualLength(); // update SEGLEN (mapping may have changed) unsigned d2 = (*_mode[tmpMode])(); // run old mode seg.restoreSegenv(_tmpSegData); // restore mode state (will also update transitional state) - delay = MIN(delay,d2); // use shortest delay + frameDelay = min(frameDelay,d2); // use shortest delay Segment::modeBlend(false); // unset semaphore } #endif seg.call++; - if (seg.isInTransition() && delay > FRAMETIME) delay = FRAMETIME; // force faster updates during transition + if (seg.isInTransition() && frameDelay > FRAMETIME) frameDelay = FRAMETIME; // force faster updates during transition BusManager::setSegmentCCT(oldCCT); // restore old CCT for ABL adjustments } - seg.next_time = nowUp + delay; + seg.next_time = nowUp + frameDelay; } _segment_index++; } From bf37ac53a3dd5c666027932027502c72e69b6996 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Mon, 4 Nov 2024 08:10:05 +0100 Subject: [PATCH 139/145] improved FPS calc resolution, added averaging & multiplier compileflags Fixed point calculation for improved accuracy, dithering in debug builds only. Averaging and optional multiplier can be set as compile flags, example for speed testing with long averaging and a 10x multiplier: -D FPS_CALC_AVG=200 -D FPS_MULTIPLIER=10 The calculation resolution is limited (9.7bit fixed point) so values larger than 200 can hit resolution limit and get stuck before reaching the final value. If WLED_DEBUG is defined, dithering is added to the returned value so sub-frame accuracy is possible in post-processingwithout enabling the multiplier. --- wled00/FX.h | 10 +++++++++- wled00/FX_fcn.cpp | 16 +++++++++++----- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/wled00/FX.h b/wled00/FX.h index ad39a7c06d..1579a5bcbd 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -47,6 +47,14 @@ #define FRAMETIME_FIXED (1000/WLED_FPS) #define FRAMETIME strip.getFrameTime() +// FPS calculation (can be defined as compile flag for debugging) +#ifndef FPS_CALC_AVG +#define FPS_CALC_AVG 7 // average FPS calculation over this many frames (moving average) +#endif +#ifndef FPS_MULTIPLIER +#define FPS_MULTIPLIER 1 // dev option: multiplier to get sub-frame FPS without floats +#endif + /* each segment uses 82 bytes of SRAM memory, so if you're application fails because of insufficient memory, decreasing MAX_NUM_SEGMENTS may help */ #ifdef ESP8266 @@ -729,7 +737,7 @@ class WS2812FX { // 96 bytes _transitionDur(750), _targetFps(WLED_FPS), _frametime(FRAMETIME_FIXED), - _cumulativeFps(2), + _cumulativeFps(50<<6), _isServicing(false), _isOffRefreshRequired(false), _hasWhiteChannel(false), diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index f45256f0fd..395451466d 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -1412,10 +1412,12 @@ void WS2812FX::show() { unsigned long showNow = millis(); size_t diff = showNow - _lastShow; - size_t fpsCurr = 200; - if (diff > 0) fpsCurr = 1000 / diff; - _cumulativeFps = (3 * _cumulativeFps + fpsCurr +2) >> 2; // "+2" for proper rounding (2/4 = 0.5) - _lastShow = showNow; + + if (diff > 0) { // skip calculation if no time has passed + size_t fpsCurr = (1000<<7) / diff; // fixed point 9.7 bit + _cumulativeFps = (FPS_CALC_AVG * _cumulativeFps + fpsCurr + FPS_CALC_AVG / 2) / (FPS_CALC_AVG + 1); // "+FPS_CALC_AVG/2" for proper rounding + _lastShow = showNow; + } } /** @@ -1432,7 +1434,11 @@ bool WS2812FX::isUpdating() const { */ uint16_t WS2812FX::getFps() const { if (millis() - _lastShow > 2000) return 0; - return _cumulativeFps +1; + #ifdef WLED_DEBUG + return (FPS_MULTIPLIER * (_cumulativeFps + (random16() & 63))) >> 7; // + random("0.5") for dithering + #else + return (FPS_MULTIPLIER * _cumulativeFps) >> 7; // _cumulativeFps is stored in fixed point 9.7 bit + #endif } void WS2812FX::setTargetFps(uint8_t fps) { From 3733715184df2a684820a54f0d7caf61ab1dc0ac Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Mon, 4 Nov 2024 17:38:45 +0100 Subject: [PATCH 140/145] bugfix bitshift was still set from testing, forgot to update --- wled00/FX.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/FX.h b/wled00/FX.h index 1579a5bcbd..e5b7a0e950 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -737,7 +737,7 @@ class WS2812FX { // 96 bytes _transitionDur(750), _targetFps(WLED_FPS), _frametime(FRAMETIME_FIXED), - _cumulativeFps(50<<6), + _cumulativeFps(50<<7), _isServicing(false), _isOffRefreshRequired(false), _hasWhiteChannel(false), From 4634ace74e9e7295ff1518b449112146475df349 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Mon, 4 Nov 2024 19:33:42 +0100 Subject: [PATCH 141/145] Added define for bitshift, removed dithering dithering is not really needed, the FPS_MULTIPLIER is a much better option. --- wled00/FX.h | 3 ++- wled00/FX_fcn.cpp | 8 ++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/wled00/FX.h b/wled00/FX.h index e5b7a0e950..5451615464 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -54,6 +54,7 @@ #ifndef FPS_MULTIPLIER #define FPS_MULTIPLIER 1 // dev option: multiplier to get sub-frame FPS without floats #endif +#define FPS_CALC_SHIFT 7 // bit shift for fixed point math /* each segment uses 82 bytes of SRAM memory, so if you're application fails because of insufficient memory, decreasing MAX_NUM_SEGMENTS may help */ @@ -737,7 +738,7 @@ class WS2812FX { // 96 bytes _transitionDur(750), _targetFps(WLED_FPS), _frametime(FRAMETIME_FIXED), - _cumulativeFps(50<<7), + _cumulativeFps(50 << FPS_CALC_SHIFT), _isServicing(false), _isOffRefreshRequired(false), _hasWhiteChannel(false), diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 395451466d..e706f2b431 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -1414,7 +1414,7 @@ void WS2812FX::show() { size_t diff = showNow - _lastShow; if (diff > 0) { // skip calculation if no time has passed - size_t fpsCurr = (1000<<7) / diff; // fixed point 9.7 bit + size_t fpsCurr = (1000 << FPS_CALC_SHIFT) / diff; // fixed point math _cumulativeFps = (FPS_CALC_AVG * _cumulativeFps + fpsCurr + FPS_CALC_AVG / 2) / (FPS_CALC_AVG + 1); // "+FPS_CALC_AVG/2" for proper rounding _lastShow = showNow; } @@ -1434,11 +1434,7 @@ bool WS2812FX::isUpdating() const { */ uint16_t WS2812FX::getFps() const { if (millis() - _lastShow > 2000) return 0; - #ifdef WLED_DEBUG - return (FPS_MULTIPLIER * (_cumulativeFps + (random16() & 63))) >> 7; // + random("0.5") for dithering - #else - return (FPS_MULTIPLIER * _cumulativeFps) >> 7; // _cumulativeFps is stored in fixed point 9.7 bit - #endif + return (FPS_MULTIPLIER * _cumulativeFps) >> FPS_CALC_SHIFT; // _cumulativeFps is stored in fixed point } void WS2812FX::setTargetFps(uint8_t fps) { From 03eb5211ead0d98a6f3f96a1ff4cca9b55c3406f Mon Sep 17 00:00:00 2001 From: Tomas Kuchta Date: Mon, 11 Nov 2024 15:08:09 +0100 Subject: [PATCH 142/145] Refactor Power Measurement usermod: rename kilowatthours to watthours and update calculations --- .../Power_Measurement/Power_Measurement.h | 35 +++++++++++-------- usermods/Power_Measurement/readme.md | 2 +- wled00/usermods_list.cpp | 4 --- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/usermods/Power_Measurement/Power_Measurement.h b/usermods/Power_Measurement/Power_Measurement.h index d54ac790c5..297d007c75 100644 --- a/usermods/Power_Measurement/Power_Measurement.h +++ b/usermods/Power_Measurement/Power_Measurement.h @@ -100,7 +100,7 @@ class UsermodPower_Measurement : public Usermod { float Voltage = 0; float Current = 0; float Power = 0; - unsigned long kilowatthours = 0; + unsigned long watthours = 0; void setup() { analogReadResolution(ADCResolution); @@ -177,6 +177,8 @@ class UsermodPower_Measurement : public Usermod { } + + void pinAlocation() { DEBUG_PRINTLN(F("Allocating power pins...")); if (VoltagePin >= 0 && pinManager.allocatePin(VoltagePin, true, PinOwner::UM_Power_Measurement)) { @@ -222,7 +224,8 @@ class UsermodPower_Measurement : public Usermod { DEBUG_PRINTLN(Power); DEBUG_PRINT("Energy: "); - DEBUG_PRINTLN(kilowatthours); + DEBUG_PRINT(watthours); + DEBUG_PRINTLN(" Wh"); DEBUG_PRINT("Energy Wms: "); DEBUG_PRINTLN(wattmiliseconds); } @@ -287,14 +290,18 @@ class UsermodPower_Measurement : public Usermod { // Calculate energy - dont do it when led is off if (Power > 0) { - unsigned long elapsedTime = millis() - lastTime_energy; - wattmiliseconds += Power * elapsedTime; - } - lastTime_energy = millis(); + unsigned long currentTime = millis(); + unsigned long elapsedTime = currentTime - lastTime_energy; + lastTime_energy = currentTime; - if (wattmiliseconds >= 3600000000) { // 3,600,000 milliseconds = 1 hour - kilowatthours += wattmiliseconds / 3600000000; // Convert watt-milliseconds to kilowatt-hours (1 watt-millisecond = 1/3,600,000,000 kilowatt-hours) - wattmiliseconds = 0; + unsigned long long wattMilliseconds = static_cast(Power) * elapsedTime; + wattmiliseconds += wattMilliseconds; + + // Convert watt-milliseconds to kilowatt-hours + if (wattmiliseconds >= 3600000ULL) { // 3,600,000 milliseconds = 1 hour + watthours += static_cast(wattmiliseconds) / 3600000.0; + wattmiliseconds = 0; + } } } @@ -415,8 +422,8 @@ class UsermodPower_Measurement : public Usermod { Power_json.add(F(" W")); JsonArray Energy_json = user.createNestedArray(FPSTR("Energy")); - Energy_json.add(kilowatthours); - Energy_json.add(F(" kWh")); + Energy_json.add(watthours); + Energy_json.add(F(" Wh")); } void addToConfig(JsonObject& root) { @@ -546,10 +553,10 @@ class UsermodPower_Measurement : public Usermod { dtostrf(Power, 6, 2, payload); // Convert float to string mqtt->publish(subuf, 0, true, payload); - // Publish kilowatthours + // Publish watthours strcpy(subuf, mqttDeviceTopic); - strcat_P(subuf, PSTR("/power_measurement/kilowatthours")); - ultoa(kilowatthours, payload, 10); // Convert unsigned long to string + strcat_P(subuf, PSTR("/power_measurement/watthours")); + ultoa(watthours, payload, 10); // Convert unsigned long to string mqtt->publish(subuf, 0, true, payload); } } diff --git a/usermods/Power_Measurement/readme.md b/usermods/Power_Measurement/readme.md index 4df846179c..edbceb193d 100644 --- a/usermods/Power_Measurement/readme.md +++ b/usermods/Power_Measurement/readme.md @@ -91,4 +91,4 @@ This code was created by Tomáš Kuchta. ## Contributions -- Tomáš Kuchta (Initial idea) \ No newline at end of file +- Tomáš Kuchta (Initial idea and code) \ No newline at end of file diff --git a/wled00/usermods_list.cpp b/wled00/usermods_list.cpp index 115cfe9078..5bd6983d7b 100644 --- a/wled00/usermods_list.cpp +++ b/wled00/usermods_list.cpp @@ -186,10 +186,6 @@ #include "../usermods/Power_Measurement/Power_Measurement.h" #endif -#ifdef USERMOD_POWER_MEASUREMENT - #include "../usermods/Power_Measurement/Power_Measurement.h" -#endif - #if defined(WLED_USE_SD_MMC) || defined(WLED_USE_SD_SPI) // This include of SD.h and SD_MMC.h must happen here, else they won't be // resolved correctly (when included in mod's header only) From 33f3194b7f882843d134d71db5c7d3c43f01bd36 Mon Sep 17 00:00:00 2001 From: Tomas Kuchta Date: Sun, 18 Aug 2024 22:32:05 +0200 Subject: [PATCH 143/145] Add Power Measurement usermod - Implement functions to measure power consumption --- .../Power_Measurement/Power_Measurement.h | 576 +++ .../assets/example_schematic.kicad_sch | 3266 +++++++++++++++++ .../assets/img/example schematic.png | Bin 0 -> 53358 bytes .../assets/img/screenshot 1 - info.jpg | Bin 0 -> 48340 bytes .../assets/img/screenshot 2 - settings.png | Bin 0 -> 24762 bytes .../assets/img/screenshot 3 - settings.png | Bin 0 -> 35492 bytes usermods/Power_Measurement/readme.md | 94 + wled00/const.h | 1 + wled00/pin_manager.h | 1 + wled00/usermods_list.cpp | 8 + 10 files changed, 3946 insertions(+) create mode 100644 usermods/Power_Measurement/Power_Measurement.h create mode 100644 usermods/Power_Measurement/assets/example_schematic.kicad_sch create mode 100644 usermods/Power_Measurement/assets/img/example schematic.png create mode 100644 usermods/Power_Measurement/assets/img/screenshot 1 - info.jpg create mode 100644 usermods/Power_Measurement/assets/img/screenshot 2 - settings.png create mode 100644 usermods/Power_Measurement/assets/img/screenshot 3 - settings.png create mode 100644 usermods/Power_Measurement/readme.md diff --git a/usermods/Power_Measurement/Power_Measurement.h b/usermods/Power_Measurement/Power_Measurement.h new file mode 100644 index 0000000000..d54ac790c5 --- /dev/null +++ b/usermods/Power_Measurement/Power_Measurement.h @@ -0,0 +1,576 @@ +// Filename: Power_Measurement.h +// This code was cocreated by github copilot and created by Tomáš Kuchta +#pragma once + +#include "wled.h" +#include "esp_adc_cal.h" + +#ifndef CURRENT_PIN + #define CURRENT_PIN 1 +#endif + +#ifndef VOLTAGE_PIN + #define VOLTAGE_PIN 0 +#endif + +#define NUM_READINGS 10 +#define NUM_READINGS_CAL 100 +#define ADC_MAX_VALUE (pow(2, ADCResolution) - 1) // For 12-bit ADC, the max value is 4095 +#define UPDATE_INTERVAL_MAIN 100 +#define UPDATE_INTERVAL_MQTT 60000 + +class UsermodPower_Measurement : public Usermod { + private: + bool initDone = false; + unsigned long lastTime_slow = 0; + unsigned long lastTime_main = 0; + unsigned long lastTime_energy = 0; + unsigned long lastTime_mqtt = 0; + boolean enabled = true; + boolean calibration_enable = false; + boolean cal_adavnced = false; + + int Voltage_raw = 0; + float AverageVoltage_raw = 0; + int Voltage_raw_adj = 0; + int Voltage_calc = 0; + + int Current_raw = 0; + float AverageCurrent_raw = 0; + int Current_calc = 0; + + float voltageReadings_raw[NUM_READINGS]; + float currentReadings_raw[NUM_READINGS]; + int readIndex = 0; + float totalVoltage_raw = 0; + float totalCurrent_raw = 0; + + // Low-pass filter variables + float alpha = 0.1; + float filtered_Voltage_raw = 0; + float filtered_Current_raw = 0; + + unsigned long long wattmiliseconds = 0; //energy counter in watt milliseconds + + + // calibration variables + int Num_Readings_Cal = NUM_READINGS_CAL; + bool Cal_In_Progress = false; + bool Cal_Zero_Points = false; + bool Cal_calibrate_Measured_Voltage = false; + bool Cal_calibrate_Measured_Current = false; + float Cal_Measured_Voltage = 0; + float Cal_Measured_Current = 0; + + float Cal_min_Voltage_raw = 17; + float Cal_min_Current_calc = 718; + + float Cal_Voltage_raw_averaged = 0; + float Cal_Voltage_calc_averaged = 0; + float Cal_Current_calc_averaged = 0; + + int Cal_Current_at_x = 1000; + int Cal_Current_calc_at_x = 775; + float Cal_Voltage_Coefficient = 22.97; + + // averiging variables + float Cal_Voltage_raw_Readings_Avg[NUM_READINGS_CAL]; + float Cal_Voltage_calc_Readings_Avg[NUM_READINGS_CAL]; + float Cal_Current_calc_Readings_Avg[NUM_READINGS_CAL]; + int Cal_Read_Index = 0; + float Cal_Total_Voltage_raw = 0; + float Cal_Total_Voltage_calc = 0; + float Cal_Total_Current_calc = 0; + + int8_t VoltagePin = VOLTAGE_PIN; + int8_t CurrentPin = CURRENT_PIN; + + int Update_Interval_Mqtt = UPDATE_INTERVAL_MQTT; + int Update_Interval_Main = UPDATE_INTERVAL_MAIN; + + // String used more than once + static const char _name[] PROGMEM; + static const char _no_data[] PROGMEM; + + public: + int ADCResolution = 12; + int ADCAttenuation = ADC_6db; + + //For usage in other parts of the main code + float Voltage = 0; + float Current = 0; + float Power = 0; + unsigned long kilowatthours = 0; + + void setup() { + analogReadResolution(ADCResolution); + analogSetAttenuation(static_cast(ADCAttenuation)); // Set the ADC attenuation (ADC_ATTEN_DB_6 = 0 mV ~ 1300 mV) + + // Initialize all readings to 0: + for (int i = 0; i < NUM_READINGS; i++) { + voltageReadings_raw[i] = 0; + currentReadings_raw[i] = 0; + } + + Current_raw = 1800; + filtered_Current_raw = 1800; + + Cal_Zero_Points = false; + Cal_calibrate_Measured_Voltage = false; + Cal_calibrate_Measured_Current = false; + Cal_In_Progress = false; + Num_Readings_Cal = NUM_READINGS_CAL; + + + #ifdef WLED_DEBUG + esp_adc_cal_characteristics_t adc_chars; + esp_adc_cal_value_t val_type = esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_6, ADC_WIDTH_BIT_12, ESP_ADC_CAL_VAL_DEFAULT_VREF, &adc_chars); + + if (val_type == ESP_ADC_CAL_VAL_EFUSE_VREF) { + // eFuse Vref is available + DEBUG_PRINTLN(F("PM: Using eFuse Vref for ADC calibration_enable")); + } else if (val_type == ESP_ADC_CAL_VAL_EFUSE_TP) { + // Two Point calibration_enable is available + DEBUG_PRINTLN(F("PM: Using Two Point calibration_enable for ADC calibration_enable")); + } else { + // Default Vref is used + DEBUG_PRINTLN(F("PM: Using default Vref for ADC calibration_enable")); + } + #endif + + + if (enabled) { + pinAlocation(); + } + + initDone = true; + + } + + void loop() { + if (!enabled || strip.isUpdating()) return; + + unsigned long currentTime = millis(); + + #ifdef WLED_DEBUG + if (currentTime - lastTime_slow >= 1000) { + printDebugInfo(); + lastTime_slow = currentTime; + } + #endif + + #ifndef WLED_DISABLE_MQTT + if (currentTime - lastTime_mqtt >= Update_Interval_Mqtt) { + publishPowerMeasurements(); + lastTime_mqtt = currentTime; + } + #endif + + if (currentTime - lastTime_main >= Update_Interval_Main) { + updateReadings(); + + if (Cal_Zero_Points || Cal_calibrate_Measured_Voltage || Cal_calibrate_Measured_Current) calibration(); + + lastTime_main = currentTime; + } + + + } + + void pinAlocation() { + DEBUG_PRINTLN(F("Allocating power pins...")); + if (VoltagePin >= 0 && pinManager.allocatePin(VoltagePin, true, PinOwner::UM_Power_Measurement)) { + DEBUG_PRINT(F("Voltage pin allocated: ")); + DEBUG_PRINTLN(VoltagePin); + } else { + if (VoltagePin >= 0) { + DEBUG_PRINTLN(F("Voltage pin allocation failed.")); + } + VoltagePin = -1; // allocation failed, disable + } + + if (CurrentPin >= 0 && pinManager.allocatePin(CurrentPin, true, PinOwner::UM_Power_Measurement)) { + DEBUG_PRINT(F("Current pin allocated: ")); + DEBUG_PRINTLN(CurrentPin); + } else { + if (CurrentPin >= 0) { + DEBUG_PRINTLN(F("Current pin allocation failed.")); + } + CurrentPin = -1; // allocation failed, disable + } + } + + + void printDebugInfo() { + DEBUG_PRINT(F("Voltage raw: ")); + DEBUG_PRINTLN(Voltage_raw); + DEBUG_PRINTLN(AverageVoltage_raw); + DEBUG_PRINT(F("Calc: ")); + DEBUG_PRINTLN(Voltage_calc); + DEBUG_PRINT(F("Voltage: ")); + DEBUG_PRINTLN(Voltage); + + DEBUG_PRINT(F("Current raw: ")); + DEBUG_PRINTLN(Current_raw); + DEBUG_PRINTLN(AverageCurrent_raw); + DEBUG_PRINT(F("Calc: ")); + DEBUG_PRINTLN(Current_calc); + DEBUG_PRINT("Current: "); + DEBUG_PRINTLN(Current); + + DEBUG_PRINT("Power: "); + DEBUG_PRINTLN(Power); + + DEBUG_PRINT("Energy: "); + DEBUG_PRINTLN(kilowatthours); + DEBUG_PRINT("Energy Wms: "); + DEBUG_PRINTLN(wattmiliseconds); + } + + void updateReadings() { + // Measure the voltage and current and store them in the arrays for the moving average and convert via map function: + totalVoltage_raw -= voltageReadings_raw[readIndex]; + totalCurrent_raw -= currentReadings_raw[readIndex]; + + if (VoltagePin == -1) { + Voltage_raw = 0; + DEBUG_PRINTLN("Voltage pin not allocated"); + } else { + Voltage_raw = analogRead(VoltagePin); + } + + if (CurrentPin == -1) { + Current_raw = 0; + DEBUG_PRINTLN("Current pin not allocated"); + } else { + Current_raw = analogRead(CurrentPin); + } + + if (millis() > 1000) { // To avoid the initial spike in readings + filtered_Voltage_raw = (alpha * Voltage_raw) + ((1 - alpha) * filtered_Voltage_raw); + filtered_Current_raw = (alpha * Current_raw) + ((1 - alpha) * filtered_Current_raw); + } else { + filtered_Voltage_raw = Voltage_raw; + filtered_Current_raw = Current_raw; + } + + voltageReadings_raw[readIndex] = filtered_Voltage_raw; + currentReadings_raw[readIndex] = filtered_Current_raw; + + totalVoltage_raw += filtered_Voltage_raw; + totalCurrent_raw += filtered_Current_raw; + + AverageVoltage_raw = totalVoltage_raw / NUM_READINGS; + AverageCurrent_raw = totalCurrent_raw / NUM_READINGS; + + readIndex = (readIndex + 1) % NUM_READINGS; + + Voltage_raw_adj = map(AverageVoltage_raw, Cal_min_Voltage_raw, ADC_MAX_VALUE, 0, ADC_MAX_VALUE); + if (Voltage_raw_adj < 0) Voltage_raw_adj = 0; + Voltage_calc = readADC_Cal(Voltage_raw_adj); + Voltage = (Voltage_calc / 1000.0) * Cal_Voltage_Coefficient; + if (Voltage < 0.05) Voltage = 0; + Voltage = round(Voltage * 100.0) / 100.0; // Round to 2 decimal places + if (VoltagePin == -1) Voltage = 0; + + Current_calc = readADC_Cal(AverageCurrent_raw); + Current = (map(Current_calc, Cal_min_Current_calc, Cal_Current_calc_at_x, 0, Cal_Current_at_x)) / 1000.0; + if (Current > -0.1 && Current < 0.05) { + Current = 0; + } + Current = round(Current * 100.0) / 100.0; + if (CurrentPin == -1) Current = 0; + + // Calculate power + Power = Voltage * Current; + Power = round(Power * 100.0) / 100.0; + + // Calculate energy - dont do it when led is off + if (Power > 0) { + unsigned long elapsedTime = millis() - lastTime_energy; + wattmiliseconds += Power * elapsedTime; + } + lastTime_energy = millis(); + + if (wattmiliseconds >= 3600000000) { // 3,600,000 milliseconds = 1 hour + kilowatthours += wattmiliseconds / 3600000000; // Convert watt-milliseconds to kilowatt-hours (1 watt-millisecond = 1/3,600,000,000 kilowatt-hours) + wattmiliseconds = 0; + } + } + + void calibration() { + if (Num_Readings_Cal == NUM_READINGS_CAL) { + DEBUG_PRINTLN("calibration_enable started"); + Cal_In_Progress = true; + serializeConfig(); // To update the checkboxes in the config + } + if (Num_Readings_Cal > 0) { + Num_Readings_Cal--; + // Average the readings + Cal_Total_Voltage_raw -= Cal_Voltage_raw_Readings_Avg[Cal_Read_Index]; + Cal_Total_Voltage_calc -= Cal_Voltage_calc_Readings_Avg[Cal_Read_Index]; + Cal_Total_Current_calc -= Cal_Current_calc_Readings_Avg[Cal_Read_Index]; + + Cal_Voltage_raw_Readings_Avg[Cal_Read_Index] = Voltage_raw; + Cal_Voltage_calc_Readings_Avg[Cal_Read_Index] = Voltage_calc; + Cal_Current_calc_Readings_Avg[Cal_Read_Index] = Current_calc; + + Cal_Total_Voltage_raw += Voltage_raw; + Cal_Total_Voltage_calc += Voltage_calc; + Cal_Total_Current_calc += Current_calc; + + Cal_Read_Index = (Cal_Read_Index + 1) % NUM_READINGS_CAL; + + Cal_Voltage_raw_averaged = Cal_Total_Voltage_raw / NUM_READINGS_CAL; + Cal_Voltage_calc_averaged = Cal_Total_Voltage_calc / NUM_READINGS_CAL; + Cal_Current_calc_averaged = Cal_Total_Current_calc / NUM_READINGS_CAL; + } else { + + DEBUG_PRINTLN("calibration_enable Flags:"); + DEBUG_PRINTLN(Cal_In_Progress); + DEBUG_PRINTLN(Num_Readings_Cal); + DEBUG_PRINTLN(Cal_Zero_Points); + DEBUG_PRINTLN(Cal_calibrate_Measured_Voltage); + DEBUG_PRINTLN(Cal_calibrate_Measured_Current); + DEBUG_PRINTLN("the averaged values are:"); + DEBUG_PRINTLN(Cal_Voltage_raw_averaged); + DEBUG_PRINTLN(Cal_Voltage_calc_averaged); + DEBUG_PRINTLN(Cal_Current_calc_averaged); + DEBUG_PRINTLN("Inputed values are:"); + DEBUG_PRINTLN(Cal_Measured_Voltage); + DEBUG_PRINTLN(Cal_Measured_Current); + + Calibration_calculation(); + + Cal_In_Progress = false; + Cal_Zero_Points = false; + Cal_calibrate_Measured_Voltage = false; + Cal_calibrate_Measured_Current = false; + Num_Readings_Cal = NUM_READINGS_CAL; + serializeConfig(); // To update the checkboxes in the config + + DEBUG_PRINTLN("calibration_enable finished"); + } + } + + void Calibration_calculation() { + DEBUG_PRINTLN("Calculating calibration_enable values"); + + if (Cal_calibrate_Measured_Current) { + Cal_Current_at_x = Cal_Measured_Current * 1000; + Cal_Current_calc_at_x = Cal_Current_calc_averaged; + + } else if (Cal_calibrate_Measured_Voltage) { + Cal_Voltage_Coefficient = (Cal_Measured_Voltage * 1000) / Cal_Voltage_calc_averaged; + + } else if (Cal_Zero_Points) { + Cal_min_Voltage_raw = Cal_Voltage_raw_averaged; + Cal_min_Current_calc = Cal_Current_calc_averaged; + } else { + DEBUG_PRINTLN("No calibration_enable values selected - but that should not happen"); + } + + } + + void addToJsonInfo(JsonObject& root) { + if (!enabled)return; + + JsonObject user = root["u"]; + if (user.isNull())user = root.createNestedObject("u"); + + JsonArray Current_json = user.createNestedArray(FPSTR("Current")); + if (Current_raw == 0 || CurrentPin == -1) { + Current_json.add(F(_no_data)); + } else if (Current_raw >= (ADC_MAX_VALUE - 3)) { + Current_json.add(F("Overrange")); + } else { + Current_json.add(Current); + Current_json.add(F(" A")); + } + + JsonArray Voltage_json = user.createNestedArray(FPSTR("Voltage")); + if (Voltage_raw == 0 || VoltagePin == -1) { + Voltage_json.add(F(_no_data)); + } else if (Voltage_raw >= (ADC_MAX_VALUE - 3)) { + Voltage_json.add(F(_no_data)); + } else if (Voltage_raw >= (ADC_MAX_VALUE - 3)) { + Voltage_json.add(F("Overrange")); + } else { + Voltage_json.add(Voltage); + Voltage_json.add(F(" V")); + } + + if (calibration_enable) { + JsonArray Current_raw_json = user.createNestedArray(FPSTR("Current raw")); + Current_raw_json.add(Current_raw); + Current_raw_json.add(" -> " + String(Current_calc)); + + JsonArray Voltage_raw_json = user.createNestedArray(FPSTR("Voltage raw")); + Voltage_raw_json.add(Voltage_raw); + Voltage_raw_json.add(" -> " + String(Voltage_calc)); + } + + JsonArray Power_json = user.createNestedArray(FPSTR("Power")); + Power_json.add(Power); + Power_json.add(F(" W")); + + JsonArray Energy_json = user.createNestedArray(FPSTR("Energy")); + Energy_json.add(kilowatthours); + Energy_json.add(F(" kWh")); + } + + void addToConfig(JsonObject& root) { + JsonObject top = root.createNestedObject(FPSTR(_name)); + top[FPSTR("enabled")] = enabled; + + JsonObject power_pins = top.createNestedObject(FPSTR("power_pins")); + power_pins[FPSTR("Voltage Pin")] = VoltagePin; + power_pins[FPSTR("Current Pin")] = CurrentPin; + + JsonObject update = top.createNestedObject(FPSTR("update rate in ms")); + update[FPSTR("update rate of mqtt")] = Update_Interval_Mqtt; + update[FPSTR("update rate of main")] = Update_Interval_Main; + + JsonObject cal = top.createNestedObject(FPSTR("calibration")); + cal[FPSTR("calibration Mode")] = calibration_enable; + if (calibration_enable && !Cal_In_Progress) { + cal[FPSTR("Advanced")] = cal_adavnced; + + cal["Zero Points"] = Cal_Zero_Points; + cal["Measured Voltage"] = Cal_Measured_Voltage; + cal["Calibrate Voltage?"] = Cal_calibrate_Measured_Voltage; + cal["Measured Current"] = Cal_Measured_Current; + cal["Calibrate Current?"] = Cal_calibrate_Measured_Current; + } else if (Cal_In_Progress) { + cal[FPSTR("calibration_enable is in progress please wait")] = "Non-Essential Data Entry Zone: Just for Kicks and Giggles"; + } + + if (calibration_enable && cal_adavnced && !Cal_In_Progress) { + cal[FPSTR("Number of samples")] = Num_Readings_Cal; + cal[FPSTR("Zero Point of Voltage")] = Cal_min_Voltage_raw; + cal[FPSTR("Zero Point of Current")] = Cal_min_Current_calc; + cal[FPSTR("Voltage Coefficient")] = Cal_Voltage_Coefficient; + cal[FPSTR("Current at X (mV at ADC)")] = Cal_Current_calc_at_x; + cal[FPSTR("Current at X (mA)")] = Cal_Current_at_x; + } + } + + bool readFromConfig(JsonObject& root) { + int8_t tmpVoltagePin = VoltagePin; + int8_t tmpCurrentPin = CurrentPin; + + JsonObject top = root[FPSTR(_name)]; + if (top.isNull()) { + DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); + return false; + } + + enabled = top[FPSTR("Enabled")] | enabled; + + tmpVoltagePin = top[FPSTR("power_pins")][FPSTR("Voltage Pin")] | tmpVoltagePin; + tmpCurrentPin = top[FPSTR("power_pins")][FPSTR("Current Pin")] | tmpCurrentPin; + + Update_Interval_Mqtt = top[FPSTR("update rate in ms")][FPSTR("update rate of mqtt")] | Update_Interval_Mqtt; + Update_Interval_Main = top[FPSTR("update rate in ms")][FPSTR("update rate of main")] | Update_Interval_Main; + + JsonObject cal = top[FPSTR("calibration")]; + calibration_enable = cal[FPSTR("calibration Mode")] | calibration_enable; + + if (calibration_enable && !Cal_In_Progress) { + cal_adavnced = cal[FPSTR("Advanced")] | cal_adavnced; + + Cal_Zero_Points = cal["Zero Points"] | Cal_Zero_Points; + Cal_Measured_Voltage = cal["Measured Voltage"] | Cal_Measured_Voltage; + Cal_calibrate_Measured_Voltage = cal["Calibrate Voltage?"] | Cal_calibrate_Measured_Voltage; + Cal_Measured_Current = cal["Measured Current"] | Cal_Measured_Current; + Cal_calibrate_Measured_Current = cal["Calibrate Current?"] | Cal_calibrate_Measured_Current; + } + + if (calibration_enable && cal_adavnced && !Cal_In_Progress) { + Num_Readings_Cal = cal[FPSTR("Number of samples")] | Num_Readings_Cal; + Cal_min_Voltage_raw = cal[FPSTR("Zero Point of Voltage")] | Cal_min_Voltage_raw; + Cal_min_Current_calc = cal[FPSTR("Zero Point of Current")] | Cal_min_Current_calc; + Cal_Voltage_Coefficient = cal[FPSTR("Voltage Coefficient")] | Cal_Voltage_Coefficient; + Cal_Current_calc_at_x = cal[FPSTR("Current at X (mV at ADC)")] | Cal_Current_calc_at_x; + Cal_Current_at_x = cal[FPSTR("Current at X (mA)")] | Cal_Current_at_x; + } + + if (!initDone) { + // first run: reading from cfg.json + VoltagePin = tmpVoltagePin; + CurrentPin = tmpCurrentPin; + DEBUG_PRINTLN(F(" config loaded.")); + } else { + DEBUG_PRINTLN(F(" config (re)loaded.")); + // changing paramters from settings page + if (tmpVoltagePin != VoltagePin || tmpCurrentPin != CurrentPin) { + DEBUG_PRINTLN(F("Re-init Power pins.")); + // deallocate pin and release memory + pinManager.deallocatePin(VoltagePin, PinOwner::UM_Power_Measurement); + VoltagePin = tmpVoltagePin; + pinManager.deallocatePin(CurrentPin, PinOwner::UM_Power_Measurement); + CurrentPin = tmpCurrentPin; + // initialise + pinAlocation(); + } + } + + return true; + } + + #ifndef WLED_DISABLE_MQTT + void onMqttConnect(bool sessionPresent) { + publishPowerMeasurements(); + } + + void publishPowerMeasurements() { + if (WLED_MQTT_CONNECTED) { + char subuf[64]; + char payload[32]; + + // Publish Voltage + strcpy(subuf, mqttDeviceTopic); + strcat_P(subuf, PSTR("/power_measurement/voltage")); + dtostrf(Voltage, 6, 2, payload); // Convert float to string + mqtt->publish(subuf, 0, true, payload); + + // Publish Current + strcpy(subuf, mqttDeviceTopic); + strcat_P(subuf, PSTR("/power_measurement/current")); + dtostrf(Current, 6, 2, payload); // Convert float to string + mqtt->publish(subuf, 0, true, payload); + + // Publish Power + strcpy(subuf, mqttDeviceTopic); + strcat_P(subuf, PSTR("/power_measurement/power")); + dtostrf(Power, 6, 2, payload); // Convert float to string + mqtt->publish(subuf, 0, true, payload); + + // Publish kilowatthours + strcpy(subuf, mqttDeviceTopic); + strcat_P(subuf, PSTR("/power_measurement/kilowatthours")); + ultoa(kilowatthours, payload, 10); // Convert unsigned long to string + mqtt->publish(subuf, 0, true, payload); + } + } + #endif + + uint16_t getId() override { + return USERMOD_ID_POWER_MEASUREMENT; + } + + uint32_t readADC_Cal(int ADC_Raw) { + esp_adc_cal_characteristics_t adc_chars; + esp_adc_cal_value_t val_type = esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_6, ADC_WIDTH_BIT_12, ESP_ADC_CAL_VAL_DEFAULT_VREF, &adc_chars); + if (val_type == ESP_ADC_CAL_VAL_EFUSE_VREF) { + // Handle error if calibration_enable value is not available + DEBUG_PRINTF("Error: eFuse Vref not available"); + return 0; + } + return (esp_adc_cal_raw_to_voltage(ADC_Raw, &adc_chars)); + } +}; + +// String used more than once +const char UsermodPower_Measurement::_name[] PROGMEM = "Power Measurement"; +const char UsermodPower_Measurement::_no_data[] PROGMEM = "No data"; \ No newline at end of file diff --git a/usermods/Power_Measurement/assets/example_schematic.kicad_sch b/usermods/Power_Measurement/assets/example_schematic.kicad_sch new file mode 100644 index 0000000000..7b0c9bb933 --- /dev/null +++ b/usermods/Power_Measurement/assets/example_schematic.kicad_sch @@ -0,0 +1,3266 @@ +(kicad_sch + (version 20231120) + (generator "eeschema") + (generator_version "8.0") + (uuid "2360a543-140e-4488-b7ed-7d45263c2314") + (paper "A4") + (lib_symbols + (symbol "Device:C" + (pin_numbers hide) + (pin_names + (offset 0.254) + ) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (property "Reference" "C" + (at 0.635 2.54 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Value" "C" + (at 0.635 -2.54 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Footprint" "" + (at 0.9652 -3.81 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "~" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Unpolarized capacitor" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "ki_keywords" "cap capacitor" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "ki_fp_filters" "C_*" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (symbol "C_0_1" + (polyline + (pts + (xy -2.032 -0.762) (xy 2.032 -0.762) + ) + (stroke + (width 0.508) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy -2.032 0.762) (xy 2.032 0.762) + ) + (stroke + (width 0.508) + (type default) + ) + (fill + (type none) + ) + ) + ) + (symbol "C_1_1" + (pin passive line + (at 0 3.81 270) + (length 2.794) + (name "~" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin passive line + (at 0 -3.81 90) + (length 2.794) + (name "~" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "2" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + ) + ) + (symbol "Device:C_Polarized" + (pin_numbers hide) + (pin_names + (offset 0.254) + ) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (property "Reference" "C" + (at 0.635 2.54 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Value" "C_Polarized" + (at 0.635 -2.54 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Footprint" "" + (at 0.9652 -3.81 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "~" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Polarized capacitor" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "ki_keywords" "cap capacitor" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "ki_fp_filters" "CP_*" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (symbol "C_Polarized_0_1" + (rectangle + (start -2.286 0.508) + (end 2.286 1.016) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy -1.778 2.286) (xy -0.762 2.286) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy -1.27 2.794) (xy -1.27 1.778) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (rectangle + (start 2.286 -0.508) + (end -2.286 -1.016) + (stroke + (width 0) + (type default) + ) + (fill + (type outline) + ) + ) + ) + (symbol "C_Polarized_1_1" + (pin passive line + (at 0 3.81 270) + (length 2.794) + (name "~" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin passive line + (at 0 -3.81 90) + (length 2.794) + (name "~" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "2" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + ) + ) + (symbol "Device:Fuse" + (pin_numbers hide) + (pin_names + (offset 0) + ) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (property "Reference" "F" + (at 2.032 0 90) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "Fuse" + (at -1.905 0 90) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at -1.778 0 90) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "~" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Fuse" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "ki_keywords" "fuse" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "ki_fp_filters" "*Fuse*" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (symbol "Fuse_0_1" + (rectangle + (start -0.762 -2.54) + (end 0.762 2.54) + (stroke + (width 0.254) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy 0 2.54) (xy 0 -2.54) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + ) + (symbol "Fuse_1_1" + (pin passive line + (at 0 3.81 270) + (length 1.27) + (name "~" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin passive line + (at 0 -3.81 90) + (length 1.27) + (name "~" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "2" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + ) + ) + (symbol "Device:R_Small" + (pin_numbers hide) + (pin_names + (offset 0.254) hide) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (property "Reference" "R" + (at 0.762 0.508 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Value" "R_Small" + (at 0.762 -1.016 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Footprint" "" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "~" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Resistor, small symbol" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "ki_keywords" "R resistor" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "ki_fp_filters" "R_*" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (symbol "R_Small_0_1" + (rectangle + (start -0.762 1.778) + (end 0.762 -1.778) + (stroke + (width 0.2032) + (type default) + ) + (fill + (type none) + ) + ) + ) + (symbol "R_Small_1_1" + (pin passive line + (at 0 2.54 270) + (length 0.762) + (name "~" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin passive line + (at 0 -2.54 90) + (length 0.762) + (name "~" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "2" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + ) + ) + (symbol "Sensor_Current:ACS722xLCTR-10AB" + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (property "Reference" "U" + (at 2.54 11.43 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Value" "ACS722xLCTR-10AB" + (at 2.54 8.89 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Footprint" "Package_SO:SOIC-8_3.9x4.9mm_P1.27mm" + (at 2.54 -8.89 0) + (effects + (font + (size 1.27 1.27) + (italic yes) + ) + (justify left) + (hide yes) + ) + ) + (property "Datasheet" "http://www.allegromicro.com/~/media/Files/Datasheets/ACS722-Datasheet.ashx?la=en" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "±10A Bidirectional Hall-Effect Current Sensor, +3.3V supply, 132mV/A, SOIC-8" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "ki_keywords" "hall effect current monitor sensor isolated" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "ki_fp_filters" "SOIC*3.9x4.9mm*P1.27mm*" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (symbol "ACS722xLCTR-10AB_0_1" + (rectangle + (start -7.62 7.62) + (end 7.62 -7.62) + (stroke + (width 0.254) + (type default) + ) + (fill + (type background) + ) + ) + ) + (symbol "ACS722xLCTR-10AB_1_1" + (pin passive line + (at -10.16 5.08 0) + (length 2.54) + (name "IP+" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin passive line + (at -10.16 5.08 0) + (length 2.54) hide + (name "IP+" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "2" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin passive line + (at -10.16 -2.54 0) + (length 2.54) + (name "IP-" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "3" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin passive line + (at -10.16 -2.54 0) + (length 2.54) hide + (name "IP-" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "4" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin power_in line + (at 0 -10.16 90) + (length 2.54) + (name "GND" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "5" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin input line + (at 10.16 -2.54 180) + (length 2.54) + (name "BW_SEL" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "6" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin output line + (at 10.16 5.08 180) + (length 2.54) + (name "VIOUT" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "7" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin power_in line + (at 0 10.16 270) + (length 2.54) + (name "VCC" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "8" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + ) + ) + (symbol "power:+12V" + (power) + (pin_numbers hide) + (pin_names + (offset 0) hide) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (property "Reference" "#PWR" + (at 0 -3.81 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Value" "+12V" + (at 0 3.556 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Power symbol creates a global label with name \"+12V\"" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "ki_keywords" "global power" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (symbol "+12V_0_1" + (polyline + (pts + (xy -0.762 1.27) (xy 0 2.54) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy 0 0) (xy 0 2.54) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy 0 2.54) (xy 0.762 1.27) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + ) + (symbol "+12V_1_1" + (pin power_in line + (at 0 0 90) + (length 0) + (name "~" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + ) + ) + (symbol "power:+3.3V" + (power) + (pin_numbers hide) + (pin_names + (offset 0) hide) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (property "Reference" "#PWR" + (at 0 -3.81 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Value" "+3.3V" + (at 0 3.556 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Power symbol creates a global label with name \"+3.3V\"" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "ki_keywords" "global power" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (symbol "+3.3V_0_1" + (polyline + (pts + (xy -0.762 1.27) (xy 0 2.54) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy 0 0) (xy 0 2.54) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy 0 2.54) (xy 0.762 1.27) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + ) + (symbol "+3.3V_1_1" + (pin power_in line + (at 0 0 90) + (length 0) + (name "~" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + ) + ) + (symbol "power:GND" + (power) + (pin_numbers hide) + (pin_names + (offset 0) hide) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (property "Reference" "#PWR" + (at 0 -6.35 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Value" "GND" + (at 0 -3.81 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Power symbol creates a global label with name \"GND\" , ground" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "ki_keywords" "global power" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (symbol "GND_0_1" + (polyline + (pts + (xy 0 0) (xy 0 -1.27) (xy 1.27 -1.27) (xy 0 -2.54) (xy -1.27 -1.27) (xy 0 -1.27) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + ) + (symbol "GND_1_1" + (pin power_in line + (at 0 0 270) + (length 0) + (name "~" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + ) + ) + ) + (junction + (at 153.67 107.95) + (diameter 0) + (color 0 0 0 0) + (uuid "1b07dc7b-9356-4ae8-9e9e-b57b58329148") + ) + (junction + (at 113.03 76.2) + (diameter 0) + (color 0 0 0 0) + (uuid "2d154cf4-f5b7-4323-b3e9-9eac8537ff79") + ) + (junction + (at 193.04 76.2) + (diameter 0) + (color 0 0 0 0) + (uuid "2db59dc4-ddf7-4b94-a277-fb8c5b964aea") + ) + (junction + (at 125.73 76.2) + (diameter 0) + (color 0 0 0 0) + (uuid "3c475897-b6ee-4b26-aabb-a108d9c0d018") + ) + (junction + (at 113.03 87.63) + (diameter 0) + (color 0 0 0 0) + (uuid "c840daac-a559-40e2-bec1-819a61bd16f0") + ) + (junction + (at 139.7 107.95) + (diameter 0) + (color 0 0 0 0) + (uuid "cab8a5f1-5511-4c6e-a5cc-7be5a16b3808") + ) + (junction + (at 99.06 87.63) + (diameter 0) + (color 0 0 0 0) + (uuid "ec34c1bf-1f80-4e09-b75a-207e2d51eee0") + ) + (wire + (pts + (xy 91.44 76.2) (xy 95.25 76.2) + ) + (stroke + (width 0) + (type default) + ) + (uuid "09e68bce-a76f-468d-bd1f-3498704202a5") + ) + (wire + (pts + (xy 193.04 76.2) (xy 193.04 78.74) + ) + (stroke + (width 0) + (type default) + ) + (uuid "0e106dab-edbd-4402-baed-7cfb8c61d0fb") + ) + (wire + (pts + (xy 125.73 76.2) (xy 153.67 76.2) + ) + (stroke + (width 0) + (type default) + ) + (uuid "382618b9-c8aa-4d10-94fb-aa43deac4ea5") + ) + (wire + (pts + (xy 113.03 93.98) (xy 113.03 95.25) + ) + (stroke + (width 0) + (type default) + ) + (uuid "4bcdf674-c19f-4d9b-a412-c93407b26cea") + ) + (wire + (pts + (xy 139.7 111.76) (xy 139.7 107.95) + ) + (stroke + (width 0) + (type default) + ) + (uuid "4c39f15a-8351-403a-9d89-fe42188e85a2") + ) + (wire + (pts + (xy 125.73 87.63) (xy 125.73 86.36) + ) + (stroke + (width 0) + (type default) + ) + (uuid "4e53bfda-b832-4d7f-9baf-b0b01d5fdcb0") + ) + (wire + (pts + (xy 113.03 87.63) (xy 113.03 88.9) + ) + (stroke + (width 0) + (type default) + ) + (uuid "5241d529-f06d-413f-b170-da5727d7a0cf") + ) + (wire + (pts + (xy 193.04 76.2) (xy 200.66 76.2) + ) + (stroke + (width 0) + (type default) + ) + (uuid "548cd7f1-1ecc-42d5-9753-2b0cd4503ddb") + ) + (wire + (pts + (xy 193.04 87.63) (xy 193.04 86.36) + ) + (stroke + (width 0) + (type default) + ) + (uuid "633d110a-0f0e-41e8-b4ff-02cc1efb32dc") + ) + (wire + (pts + (xy 99.06 88.9) (xy 99.06 87.63) + ) + (stroke + (width 0) + (type default) + ) + (uuid "65593ed6-3469-4ce3-bb4d-c23dc0bac992") + ) + (wire + (pts + (xy 168.91 86.36) (xy 172.72 86.36) + ) + (stroke + (width 0) + (type default) + ) + (uuid "6e8528fd-746e-42ef-944b-bba142fa3f7f") + ) + (wire + (pts + (xy 144.78 86.36) (xy 144.78 83.82) + ) + (stroke + (width 0) + (type default) + ) + (uuid "6f12ed2d-ab77-4e6f-aa71-48b62d210b29") + ) + (wire + (pts + (xy 91.44 69.85) (xy 91.44 76.2) + ) + (stroke + (width 0) + (type default) + ) + (uuid "73e3453d-01b8-4011-ba4e-cbee25c77cd8") + ) + (wire + (pts + (xy 161.29 76.2) (xy 193.04 76.2) + ) + (stroke + (width 0) + (type default) + ) + (uuid "8fd82abc-2ff7-4ef7-b5f5-a611d22054ec") + ) + (wire + (pts + (xy 139.7 119.38) (xy 139.7 120.65) + ) + (stroke + (width 0) + (type default) + ) + (uuid "90109a20-1e33-4e88-b4cd-9b90e3d7275a") + ) + (wire + (pts + (xy 144.78 86.36) (xy 148.59 86.36) + ) + (stroke + (width 0) + (type default) + ) + (uuid "9068dd4f-f645-4baf-9b81-194e52f99ac6") + ) + (wire + (pts + (xy 125.73 76.2) (xy 125.73 78.74) + ) + (stroke + (width 0) + (type default) + ) + (uuid "970ce8ae-e94c-4e2f-bccc-11ad86d40bd2") + ) + (wire + (pts + (xy 138.43 107.95) (xy 139.7 107.95) + ) + (stroke + (width 0) + (type default) + ) + (uuid "973de61e-2303-4b5d-a962-023dd5f0e210") + ) + (wire + (pts + (xy 139.7 107.95) (xy 153.67 107.95) + ) + (stroke + (width 0) + (type default) + ) + (uuid "a43648d4-9f86-4160-9f51-0bdc2feee276") + ) + (wire + (pts + (xy 153.67 120.65) (xy 153.67 119.38) + ) + (stroke + (width 0) + (type default) + ) + (uuid "a46f7b49-f088-4aaf-8f04-e7699744187b") + ) + (wire + (pts + (xy 113.03 76.2) (xy 125.73 76.2) + ) + (stroke + (width 0) + (type default) + ) + (uuid "acb41acf-8594-442e-9d0a-dac74b40272f") + ) + (wire + (pts + (xy 99.06 87.63) (xy 113.03 87.63) + ) + (stroke + (width 0) + (type default) + ) + (uuid "c31c1aea-00f9-48f8-813e-f98f2c48866f") + ) + (wire + (pts + (xy 153.67 114.3) (xy 153.67 107.95) + ) + (stroke + (width 0) + (type default) + ) + (uuid "d703656b-3d51-44da-8626-4f4187e70bef") + ) + (wire + (pts + (xy 113.03 76.2) (xy 113.03 81.28) + ) + (stroke + (width 0) + (type default) + ) + (uuid "de954c79-1743-46bc-8ed2-bf76ba626004") + ) + (wire + (pts + (xy 99.06 96.52) (xy 99.06 97.79) + ) + (stroke + (width 0) + (type default) + ) + (uuid "e2283c3f-7214-4471-9452-f46ed59fad2d") + ) + (wire + (pts + (xy 95.25 87.63) (xy 99.06 87.63) + ) + (stroke + (width 0) + (type default) + ) + (uuid "e2dab855-68c4-4b49-a858-2d5e5b0d7830") + ) + (wire + (pts + (xy 113.03 86.36) (xy 113.03 87.63) + ) + (stroke + (width 0) + (type default) + ) + (uuid "e527980f-78d5-4f87-9f2f-cefb29677c5f") + ) + (wire + (pts + (xy 153.67 96.52) (xy 153.67 99.06) + ) + (stroke + (width 0) + (type default) + ) + (uuid "e68c62bd-c95a-4c05-bb15-b7259fbb2f1b") + ) + (wire + (pts + (xy 153.67 104.14) (xy 153.67 107.95) + ) + (stroke + (width 0) + (type default) + ) + (uuid "f2d91e7a-679e-42c3-9e11-69d10d26f58f") + ) + (wire + (pts + (xy 102.87 76.2) (xy 113.03 76.2) + ) + (stroke + (width 0) + (type default) + ) + (uuid "f370ca2a-777b-4e89-b9fc-7a1dbf365c8b") + ) + (wire + (pts + (xy 172.72 86.36) (xy 172.72 90.17) + ) + (stroke + (width 0) + (type default) + ) + (uuid "f540e002-e775-4d10-b433-96e7a62da590") + ) + (wire + (pts + (xy 161.29 96.52) (xy 161.29 99.06) + ) + (stroke + (width 0) + (type default) + ) + (uuid "f67391a0-35e9-4361-b34d-fe2d8426bffd") + ) + (text "0.33V - 2.97V\nZero Current Output Voltage = 1.65\n1.32V 10A swing" + (exclude_from_sim no) + (at 157.48 72.136 0) + (effects + (font + (size 1.27 1.27) + ) + ) + (uuid "67a60731-3184-4129-a037-edab08d612f7") + ) + (global_label "IO0X-Voltage" + (shape input) + (at 95.25 87.63 180) + (fields_autoplaced yes) + (effects + (font + (size 1.27 1.27) + ) + (justify right) + ) + (uuid "4cd9524b-ddbd-409c-b13a-b8f411a39577") + (property "Intersheetrefs" "${INTERSHEET_REFS}" + (at 79.323 87.63 0) + (effects + (font + (size 1.27 1.27) + ) + (justify right) + (hide yes) + ) + ) + ) + (global_label "VIN_Measured" + (shape output) + (at 200.66 76.2 0) + (fields_autoplaced yes) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + (uuid "534fa94a-2362-42d7-a892-5ec26944b9f7") + (property "Intersheetrefs" "${INTERSHEET_REFS}" + (at 216.5266 76.2 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + (hide yes) + ) + ) + ) + (global_label "IO0Y-Current" + (shape input) + (at 138.43 107.95 180) + (fields_autoplaced yes) + (effects + (font + (size 1.27 1.27) + ) + (justify right) + ) + (uuid "551f3e47-8f86-4a64-81ac-10348a5ca32d") + (property "Intersheetrefs" "${INTERSHEET_REFS}" + (at 122.6843 107.95 0) + (effects + (font + (size 1.27 1.27) + ) + (justify right) + (hide yes) + ) + ) + ) + (symbol + (lib_id "power:GND") + (at 139.7 120.65 0) + (mirror y) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "025dd409-0965-4b1b-b4b7-a70978a388b5") + (property "Reference" "#PWR05" + (at 139.7 127 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Value" "GND" + (at 139.7 124.714 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 139.7 120.65 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 139.7 120.65 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Power symbol creates a global label with name \"GND\" , ground" + (at 139.7 120.65 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "b7b23158-8ed1-4860-b766-6b49aea9a474") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "#PWR05") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "power:GND") + (at 125.73 87.63 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "0dd6af27-53f3-4b81-b860-78e0e2a7f4c6") + (property "Reference" "#PWR04" + (at 125.73 93.98 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Value" "GND" + (at 125.73 92.71 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 125.73 87.63 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 125.73 87.63 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Power symbol creates a global label with name \"GND\" , ground" + (at 125.73 87.63 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "2cb6c609-62a5-4196-9689-ca66eed822b5") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "#PWR04") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "power:GND") + (at 193.04 87.63 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "15b563e7-f8f8-4f8b-a72e-7a81c4e86445") + (property "Reference" "#PWR010" + (at 193.04 93.98 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Value" "GND" + (at 193.04 92.71 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 193.04 87.63 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 193.04 87.63 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Power symbol creates a global label with name \"GND\" , ground" + (at 193.04 87.63 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "e5bd921a-8aac-4bcc-b564-5c8680cfd9f7") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "#PWR010") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "power:+12V") + (at 91.44 69.85 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "407df456-dee1-42cc-a267-d98fa7533233") + (property "Reference" "#PWR01" + (at 91.44 73.66 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Value" "+VIN" + (at 91.186 64.77 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 91.44 69.85 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 91.44 69.85 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Power symbol creates a global label with name \"+12V\"" + (at 91.44 69.85 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "aaaa8249-1eb5-42b2-8084-a2c6a598cc0c") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "#PWR01") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "Sensor_Current:ACS722xLCTR-10AB") + (at 158.75 86.36 90) + (mirror x) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (fields_autoplaced yes) + (uuid "56afd411-3977-437b-bb3a-8f28c710835e") + (property "Reference" "U1" + (at 177.8 80.0414 90) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "ACS722xLCTR-10AB" + (at 177.8 82.5814 90) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "Package_SO:SOIC-8_3.9x4.9mm_P1.27mm" + (at 167.64 88.9 0) + (effects + (font + (size 1.27 1.27) + (italic yes) + ) + (justify left) + (hide yes) + ) + ) + (property "Datasheet" "http://www.allegromicro.com/~/media/Files/Datasheets/ACS722-Datasheet.ashx?la=en" + (at 158.75 86.36 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "±10A Bidirectional Hall-Effect Current Sensor, +3.3V supply, 132mV/A, SOIC-8" + (at 158.75 86.36 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "2" + (uuid "72f645c4-4abf-4ed6-8444-41711e1da047") + ) + (pin "5" + (uuid "b2582dca-89d3-426c-abbb-c3fc19df7eab") + ) + (pin "8" + (uuid "2a6be634-5f04-46c7-8438-214e2044f43d") + ) + (pin "6" + (uuid "e8f38fe2-ecef-4bd0-8f8c-99f4b004dbb6") + ) + (pin "4" + (uuid "97ba3573-4081-4ca9-96e6-5cdb0ea88803") + ) + (pin "7" + (uuid "142838f0-c367-4e18-bf7b-ee3e7e9d7db5") + ) + (pin "3" + (uuid "b75ee556-6002-41b0-96db-16a7d7beea40") + ) + (pin "1" + (uuid "cde11bc5-dfa3-4dd4-9658-9eb7c8fed8a7") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "U1") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "Device:C") + (at 125.73 82.55 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "5fad63ef-f4d6-456d-b079-2b3217b553f2") + (property "Reference" "C2" + (at 127 80.01 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Value" "100nF" + (at 127.254 85.09 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Footprint" "Capacitor_SMD:C_0805_2012Metric_Pad1.18x1.45mm_HandSolder" + (at 126.6952 86.36 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "~" + (at 125.73 82.55 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Unpolarized capacitor" + (at 125.73 82.55 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MANUFACTURER" "" + (at 125.73 82.55 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MAXIMUM_PACKAGE_HEIGHT" "" + (at 125.73 82.55 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "PARTREV" "" + (at 125.73 82.55 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "STANDARD" "" + (at 125.73 82.55 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "2" + (uuid "2cbac835-c714-454e-a097-f07e36ea31d4") + ) + (pin "1" + (uuid "d6ff9025-85f6-4ead-83e6-35d7daee59a5") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "C2") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "power:+3.3V") + (at 144.78 83.82 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "6017f11f-a4ef-46fa-bf62-b72b94c8bdfc") + (property "Reference" "#PWR06" + (at 144.78 87.63 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Value" "+3.3V" + (at 144.78 80.01 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 144.78 83.82 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 144.78 83.82 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Power symbol creates a global label with name \"+3.3V\"" + (at 144.78 83.82 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "0147def6-8cae-4008-9c8a-fb5fa5d131c6") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "#PWR06") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "Device:C_Polarized") + (at 193.04 82.55 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (fields_autoplaced yes) + (uuid "82226e50-0f54-4abb-ad36-90f7a425fbd1") + (property "Reference" "C4" + (at 196.85 80.3909 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Value" "1000uF" + (at 196.85 82.9309 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Footprint" "Capacitor_THT:CP_Radial_D10.0mm_P5.00mm" + (at 194.0052 86.36 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "~" + (at 193.04 82.55 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Polarized capacitor" + (at 193.04 82.55 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MANUFACTURER" "" + (at 193.04 82.55 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MAXIMUM_PACKAGE_HEIGHT" "" + (at 193.04 82.55 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "PARTREV" "" + (at 193.04 82.55 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "STANDARD" "" + (at 193.04 82.55 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "2" + (uuid "7ba46b09-0b39-4f1d-8c36-abf2cdc780df") + ) + (pin "1" + (uuid "683c3db6-7c90-44ef-ab5f-0091b7cee3da") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "C4") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "Device:Fuse") + (at 99.06 76.2 90) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "89446d9b-f672-45df-9d3a-68373b75dc56") + (property "Reference" "F1" + (at 99.06 74.168 90) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "10A" + (at 99.06 78.486 90) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "Fuse:Fuseholder_Littelfuse_Nano2_154x" + (at 99.06 77.978 90) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "~" + (at 99.06 76.2 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Fuse" + (at 99.06 76.2 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Can be found at" "https://www.digikey.cz/cs/products/detail/littelfuse-inc/0154010-DR/552684" + (at 99.06 76.2 90) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MANUFACTURER" "" + (at 99.06 76.2 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MAXIMUM_PACKAGE_HEIGHT" "" + (at 99.06 76.2 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "PARTREV" "" + (at 99.06 76.2 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "STANDARD" "" + (at 99.06 76.2 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "81156747-e392-4f5d-891c-de789d1cc7df") + ) + (pin "2" + (uuid "94ced624-93d2-41f8-ba92-b05615e69e68") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "F1") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "power:GND") + (at 99.06 97.79 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "90a71c55-8ccd-4b79-ad25-d3ab6fc7ddc7") + (property "Reference" "#PWR02" + (at 99.06 104.14 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Value" "GND" + (at 99.06 101.854 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 99.06 97.79 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 99.06 97.79 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Power symbol creates a global label with name \"GND\" , ground" + (at 99.06 97.79 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "d04aa806-3c19-4632-afa5-705e3b63bf07") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "#PWR02") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "Device:R_Small") + (at 113.03 91.44 0) + (mirror y) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (fields_autoplaced yes) + (uuid "916b1f8d-6642-488e-af1d-84205ae4c834") + (property "Reference" "R2" + (at 115.57 90.1699 0) + (effects + (font + (size 1.27 1.27) + ) + (justify right) + ) + ) + (property "Value" "10k" + (at 115.57 92.7099 0) + (effects + (font + (size 1.27 1.27) + ) + (justify right) + ) + ) + (property "Footprint" "Resistor_SMD:R_0805_2012Metric_Pad1.20x1.40mm_HandSolder" + (at 113.03 91.44 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "~" + (at 113.03 91.44 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Resistor, small symbol" + (at 113.03 91.44 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MANUFACTURER" "" + (at 113.03 91.44 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MAXIMUM_PACKAGE_HEIGHT" "" + (at 113.03 91.44 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "PARTREV" "" + (at 113.03 91.44 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "STANDARD" "" + (at 113.03 91.44 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "80da57cb-1acf-41ff-accf-438dbaa07ce8") + ) + (pin "2" + (uuid "d76cf884-9331-435b-a0fb-7c592b71d183") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "R2") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "Device:R_Small") + (at 153.67 116.84 0) + (mirror y) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "a2f78b9f-6eea-4b57-9350-7a56c8bf81fc") + (property "Reference" "R4" + (at 151.638 115.316 90) + (effects + (font + (size 1.27 1.27) + ) + (justify right) + ) + ) + (property "Value" "39k" + (at 155.702 114.808 90) + (effects + (font + (size 1.27 1.27) + ) + (justify right) + ) + ) + (property "Footprint" "Resistor_SMD:R_0805_2012Metric_Pad1.20x1.40mm_HandSolder" + (at 153.67 116.84 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "~" + (at 153.67 116.84 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Resistor, small symbol" + (at 153.67 116.84 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MANUFACTURER" "" + (at 153.67 116.84 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MAXIMUM_PACKAGE_HEIGHT" "" + (at 153.67 116.84 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "PARTREV" "" + (at 153.67 116.84 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "STANDARD" "" + (at 153.67 116.84 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "4663be56-5d7b-4dde-981e-26590e75cb77") + ) + (pin "2" + (uuid "5ebf0633-3a93-4867-9db1-76976ac19143") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "R4") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "Device:R_Small") + (at 113.03 83.82 0) + (mirror y) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (fields_autoplaced yes) + (uuid "b3f1d506-940d-4f50-933d-aeebc3a5b136") + (property "Reference" "R1" + (at 115.57 82.5499 0) + (effects + (font + (size 1.27 1.27) + ) + (justify right) + ) + ) + (property "Value" "220k" + (at 115.57 85.0899 0) + (effects + (font + (size 1.27 1.27) + ) + (justify right) + ) + ) + (property "Footprint" "Resistor_SMD:R_0805_2012Metric_Pad1.20x1.40mm_HandSolder" + (at 113.03 83.82 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "~" + (at 113.03 83.82 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Resistor, small symbol" + (at 113.03 83.82 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MANUFACTURER" "" + (at 113.03 83.82 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MAXIMUM_PACKAGE_HEIGHT" "" + (at 113.03 83.82 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "PARTREV" "" + (at 113.03 83.82 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "STANDARD" "" + (at 113.03 83.82 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "70c05b2c-4e80-403a-9e92-8542d45d1206") + ) + (pin "2" + (uuid "c3cfea87-c544-4c8e-9eab-e26c8713801d") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "R1") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "power:GND") + (at 172.72 90.17 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "b404e6ac-da9a-4b38-9fcc-469610d9c102") + (property "Reference" "#PWR09" + (at 172.72 96.52 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Value" "GND" + (at 172.974 93.98 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 172.72 90.17 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 172.72 90.17 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Power symbol creates a global label with name \"GND\" , ground" + (at 172.72 90.17 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "3c7ac4e1-3263-4e11-b848-2ee2b98850b3") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "#PWR09") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "power:+3.3V") + (at 161.29 99.06 180) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "d6678757-2ebb-4128-8030-6d8841bb6c75") + (property "Reference" "#PWR08" + (at 161.29 95.25 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Value" "+3.3V" + (at 161.29 102.87 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 161.29 99.06 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 161.29 99.06 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Power symbol creates a global label with name \"+3.3V\"" + (at 161.29 99.06 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "b2fbe9f3-3222-4ae1-b583-970abdfc56ca") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "#PWR08") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "Device:R_Small") + (at 153.67 101.6 0) + (mirror y) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "db6a3718-e8ff-44ae-aa13-b57f45e4bd09") + (property "Reference" "R3" + (at 151.384 100.076 90) + (effects + (font + (size 1.27 1.27) + ) + (justify right) + ) + ) + (property "Value" "51k" + (at 155.956 99.568 90) + (effects + (font + (size 1.27 1.27) + ) + (justify right) + ) + ) + (property "Footprint" "Resistor_SMD:R_0805_2012Metric_Pad1.20x1.40mm_HandSolder" + (at 153.67 101.6 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "~" + (at 153.67 101.6 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Resistor, small symbol" + (at 153.67 101.6 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MANUFACTURER" "" + (at 153.67 101.6 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MAXIMUM_PACKAGE_HEIGHT" "" + (at 153.67 101.6 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "PARTREV" "" + (at 153.67 101.6 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "STANDARD" "" + (at 153.67 101.6 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "6829eee2-48c3-428c-b395-aded09ed3da1") + ) + (pin "2" + (uuid "3f91b68a-7552-416e-b2ab-0e04c521fffd") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "R3") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "power:GND") + (at 113.03 95.25 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "dc1fb61f-7794-44b1-9c3f-9d8160f73445") + (property "Reference" "#PWR03" + (at 113.03 101.6 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Value" "GND" + (at 113.03 99.314 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 113.03 95.25 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 113.03 95.25 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Power symbol creates a global label with name \"GND\" , ground" + (at 113.03 95.25 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "7a1a9ab3-e4f2-4afb-ade6-51fe3c60c5d4") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "#PWR03") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "Device:C") + (at 139.7 115.57 0) + (mirror y) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "df3697c9-ae53-41d5-a282-976508338da5") + (property "Reference" "C3" + (at 143.764 113.03 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Value" "100nF" + (at 146.812 118.364 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Footprint" "Capacitor_SMD:C_0805_2012Metric_Pad1.18x1.45mm_HandSolder" + (at 138.7348 119.38 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "~" + (at 139.7 115.57 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Unpolarized capacitor" + (at 139.7 115.57 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MANUFACTURER" "" + (at 139.7 115.57 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MAXIMUM_PACKAGE_HEIGHT" "" + (at 139.7 115.57 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "PARTREV" "" + (at 139.7 115.57 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "STANDARD" "" + (at 139.7 115.57 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "2" + (uuid "234d1c0f-c5de-4dc7-b16b-da5dc27fab1c") + ) + (pin "1" + (uuid "2b679a4b-9e23-47bd-a24d-6eded824d69c") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "C3") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "Device:C") + (at 99.06 92.71 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "e78fc402-ce31-495a-81f4-a4788b0342b0") + (property "Reference" "C1" + (at 100.33 90.17 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Value" "100nF" + (at 100.584 95.25 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Footprint" "Capacitor_SMD:C_0805_2012Metric_Pad1.18x1.45mm_HandSolder" + (at 100.0252 96.52 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "~" + (at 99.06 92.71 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Unpolarized capacitor" + (at 99.06 92.71 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MANUFACTURER" "" + (at 99.06 92.71 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "MAXIMUM_PACKAGE_HEIGHT" "" + (at 99.06 92.71 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "PARTREV" "" + (at 99.06 92.71 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "STANDARD" "" + (at 99.06 92.71 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "2" + (uuid "30c9c460-abed-45f7-a241-ed63555d80fc") + ) + (pin "1" + (uuid "f986288e-0ac9-4da5-9bcb-ea41a75dd84f") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "C1") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "power:GND") + (at 153.67 120.65 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "eb2dd32b-7175-4175-be49-b823cf64d88f") + (property "Reference" "#PWR07" + (at 153.67 127 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Value" "GND" + (at 153.67 124.714 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 153.67 120.65 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 153.67 120.65 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Power symbol creates a global label with name \"GND\" , ground" + (at 153.67 120.65 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "ea5a2d61-5227-4a80-af48-dc2dd1529c05") + ) + (instances + (project "example_schematic" + (path "/2360a543-140e-4488-b7ed-7d45263c2314" + (reference "#PWR07") + (unit 1) + ) + ) + ) + ) + (sheet_instances + (path "/" + (page "1") + ) + ) +) diff --git a/usermods/Power_Measurement/assets/img/example schematic.png b/usermods/Power_Measurement/assets/img/example schematic.png new file mode 100644 index 0000000000000000000000000000000000000000..2a25116fbd2faab79222618dbe0d58fcd9a5e372 GIT binary patch literal 53358 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 CieuRT literal 0 HcmV?d00001 diff --git a/usermods/Power_Measurement/assets/img/screenshot 1 - info.jpg b/usermods/Power_Measurement/assets/img/screenshot 1 - info.jpg new file mode 100644 index 0000000000000000000000000000000000000000..278d572463d64d8a3555800090a85f93a3c7c883 GIT binary patch literal 48340 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%0DcYFVo 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 Djb;n* literal 0 HcmV?d00001 diff --git a/usermods/Power_Measurement/assets/img/screenshot 2 - settings.png b/usermods/Power_Measurement/assets/img/screenshot 2 - settings.png new file mode 100644 index 0000000000000000000000000000000000000000..bb9a59d08d23589651194f560e655aa461e79b3e GIT binary patch literal 24762 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 literal 0 HcmV?d00001 diff --git a/usermods/Power_Measurement/assets/img/screenshot 3 - settings.png b/usermods/Power_Measurement/assets/img/screenshot 3 - settings.png new file mode 100644 index 0000000000000000000000000000000000000000..96c05e067edfa7f006bf99710edb306e72726208 GIT binary patch literal 35492 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 KsE~af_`d+&RW*D7 literal 0 HcmV?d00001 diff --git a/usermods/Power_Measurement/readme.md b/usermods/Power_Measurement/readme.md new file mode 100644 index 0000000000..4df846179c --- /dev/null +++ b/usermods/Power_Measurement/readme.md @@ -0,0 +1,94 @@ +# Voltage and Current Measurement using ESP's Internal ADC + +This usermod is a proof of concept that measures current and voltage using a voltage divider and a current sensor. It leverages the ESP's internal ADC to read the voltage and current, calculate power and energy consumption, and optionally publish these measurements via MQTT. + +## Features + +- **Voltage and Current Measurement**: Reads voltage and current using ADC pins of the ESP. +- **Power and Energy Calculation**: Calculates power (in watts) and energy consumption (in kilowatt-hours). +- **Calibration Support**: Offers calibration for more accurate measurements. +- **MQTT Publishing**: Publishes voltage, current, power, and energy measurements to an MQTT broker. +- **Debug Information**: Provides debug output via serial for monitoring raw and processed data. + +## Dependencies + +- **ESP32 ADC Calibration Library**: Requires `esp_adc_cal.h` for ADC calibration, which is a standard ESP-IDF library. + +## Configuration + +### Pins + +- `VOLTAGE_PIN`: ADC pin for voltage measurement (default: `0`) +- `CURRENT_PIN`: ADC pin for current measurement (default: `1`) + +### Constants + +- `NUM_READINGS`: Number of readings for moving average (default: `10`) +- `NUM_READINGS_CAL`: Number of readings for calibration (default: `100`) +- `UPDATE_INTERVAL_MAIN`: Main update interval in milliseconds (default: `100`) +- `UPDATE_INTERVAL_MQTT`: MQTT update interval in milliseconds (default: `60000`) + +## Installation + +Add `-D USERMOD_CURRENT_MEASUREMENT` to `build_flags` in `platformio_override.ini`. + +Or copy the example `platformio_override.ini` to the root directory. This file should be placed in the same directory as `platformio.ini`. + +## Hardware Example + +![Example Schematic](./assets/img/example%20schematic.png "Example Schematic") + +## Define Your Options + +- `USERMOD_POWER_MEASUREMENT`: Enable the usermod + +All parameters and calibration variables can be configured at runtime via the Usermods settings page. + +## Calibration + +### Calibration Steps + +1. Enable the `Calibration mode` checkbox. +2. Connect the controller via USB. +3. Disconnect the power supply (Vin) from the LED strip. +4. Select the option to `Calibrate Zero Points`. +5. Reconnect the power supply to the LED strip and set it to white and full brightness. +6. Measure the voltage and current and enter the values into the `Measured Voltage` and `Measured Current` fields. +7. Check the checkboxes for `Calibrate Voltage` and `Calibrate Current`. + +### Advanced + +![Advanced Calibration](./assets/img/screenshot%203%20-%20settings.png "Advanced Calibration") + +## MQTT + +If MQTT is enabled, the module will periodically publish the voltage, current, power, and energy measurements to the configured MQTT broker. + +## Debugging + +Enable `WLED_DEBUG` to print detailed debug information to the serial output, including raw and calculated values for voltage, current, power, and energy. + +## Screenshots + +Info screen | Settings page +:-----------------------------------------------------------------------:|:-------------------------------------------------------------------------------: +![Info screen](./assets/img/screenshot%201%20-%20info.jpg "Info screen") | ![Settings page](./assets/img/screenshot%202%20-%20settings.png "Settings page") + +## To-Do + +- [ ] Pin manager doesn't work properly. +- [ ] Implement a brightness limiter based on current. +- [ ] Make the code use less flash memory. + +## Changelog + +19.8.2024 +- Initial PR + +## License + +This code was created by Tomáš Kuchta. + +## Contributions + +- Tomáš Kuchta (Initial idea) \ No newline at end of file diff --git a/wled00/const.h b/wled00/const.h index 07873deca1..e3bf0247c6 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -203,6 +203,7 @@ #define USERMOD_ID_LD2410 52 //Usermod "usermod_ld2410.h" #define USERMOD_ID_POV_DISPLAY 53 //Usermod "usermod_pov_display.h" #define USERMOD_ID_PIXELS_DICE_TRAY 54 //Usermod "pixels_dice_tray.h" +#define USERMOD_ID_POWER_MEASUREMENT 55 //Usermod "Power_measurement.h" //Access point behavior #define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot diff --git a/wled00/pin_manager.h b/wled00/pin_manager.h index 73a4a36564..fdb942168a 100644 --- a/wled00/pin_manager.h +++ b/wled00/pin_manager.h @@ -67,6 +67,7 @@ enum struct PinOwner : uint8_t { UM_MAX17048 = USERMOD_ID_MAX17048, // 0x2F // Usermod "usermod_max17048.h" UM_BME68X = USERMOD_ID_BME68X, // 0x31 // Usermod "usermod_bme68x.h -- Uses "standard" HW_I2C pins UM_PIXELS_DICE_TRAY = USERMOD_ID_PIXELS_DICE_TRAY // 0x35 // Usermod "pixels_dice_tray.h" -- Needs compile time specified 6 pins for display including SPI. + UM_Power_Measurement = USERMOD_ID_POWER_MEASUREMENT, // 0x32 // Usermod "Power_measurement.h" }; static_assert(0u == static_cast(PinOwner::None), "PinOwner::None must be zero, so default array initialization works as expected"); diff --git a/wled00/usermods_list.cpp b/wled00/usermods_list.cpp index 36bd122a51..5c369d983b 100644 --- a/wled00/usermods_list.cpp +++ b/wled00/usermods_list.cpp @@ -182,6 +182,10 @@ #include "../usermods/Internal_Temperature_v2/usermod_internal_temperature.h" #endif +#ifdef USERMOD_POWER_MEASUREMENT + #include "../usermods/Power_Measurement/Power_Measurement.h" +#endif + #if defined(WLED_USE_SD_MMC) || defined(WLED_USE_SD_SPI) // This include of SD.h and SD_MMC.h must happen here, else they won't be // resolved correctly (when included in mod's header only) @@ -470,4 +474,8 @@ void registerUsermods() #ifdef USERMOD_POV_DISPLAY UsermodManager::add(new PovDisplayUsermod()); #endif + + #ifdef USERMOD_POWER_MEASUREMENT + usermods.add(new UsermodPower_Measurement()); + #endif } From 36dfb5c9cdaf7fbfa1f19125ff3e9a2e974d1243 Mon Sep 17 00:00:00 2001 From: Tomas Kuchta Date: Sun, 18 Aug 2024 22:32:05 +0200 Subject: [PATCH 144/145] Add Power Measurement usermod - Implement functions to measure power consumption --- wled00/pin_manager.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/wled00/pin_manager.h b/wled00/pin_manager.h index fdb942168a..f0cbb04df7 100644 --- a/wled00/pin_manager.h +++ b/wled00/pin_manager.h @@ -63,6 +63,8 @@ enum struct PinOwner : uint8_t { UM_Audioreactive = USERMOD_ID_AUDIOREACTIVE, // 0x20 // Usermod "audio_reactive.h" UM_SdCard = USERMOD_ID_SD_CARD, // 0x25 // Usermod "usermod_sd_card.h" UM_PWM_OUTPUTS = USERMOD_ID_PWM_OUTPUTS, // 0x26 // Usermod "usermod_pwm_outputs.h" + UM_LDR_DUSK_DAWN = USERMOD_ID_LDR_DUSK_DAWN // 0x2B // Usermod "usermod_LDR_Dusk_Dawn_v2.h" + UM_Power_Measurement = USERMOD_ID_POWER_MEASUREMENT // 0x2C // Usermod "Power_measurement.h" UM_LDR_DUSK_DAWN = USERMOD_ID_LDR_DUSK_DAWN, // 0x2B // Usermod "usermod_LDR_Dusk_Dawn_v2.h" UM_MAX17048 = USERMOD_ID_MAX17048, // 0x2F // Usermod "usermod_max17048.h" UM_BME68X = USERMOD_ID_BME68X, // 0x31 // Usermod "usermod_bme68x.h -- Uses "standard" HW_I2C pins From f79d01aae4474942e7b1e20fe33a559d441a749b Mon Sep 17 00:00:00 2001 From: Tomas Kuchta Date: Mon, 11 Nov 2024 15:08:09 +0100 Subject: [PATCH 145/145] Refactor Power Measurement usermod: rename kilowatthours to watthours and update calculations --- .../Power_Measurement/Power_Measurement.h | 35 +++++++++++-------- usermods/Power_Measurement/readme.md | 2 +- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/usermods/Power_Measurement/Power_Measurement.h b/usermods/Power_Measurement/Power_Measurement.h index d54ac790c5..297d007c75 100644 --- a/usermods/Power_Measurement/Power_Measurement.h +++ b/usermods/Power_Measurement/Power_Measurement.h @@ -100,7 +100,7 @@ class UsermodPower_Measurement : public Usermod { float Voltage = 0; float Current = 0; float Power = 0; - unsigned long kilowatthours = 0; + unsigned long watthours = 0; void setup() { analogReadResolution(ADCResolution); @@ -177,6 +177,8 @@ class UsermodPower_Measurement : public Usermod { } + + void pinAlocation() { DEBUG_PRINTLN(F("Allocating power pins...")); if (VoltagePin >= 0 && pinManager.allocatePin(VoltagePin, true, PinOwner::UM_Power_Measurement)) { @@ -222,7 +224,8 @@ class UsermodPower_Measurement : public Usermod { DEBUG_PRINTLN(Power); DEBUG_PRINT("Energy: "); - DEBUG_PRINTLN(kilowatthours); + DEBUG_PRINT(watthours); + DEBUG_PRINTLN(" Wh"); DEBUG_PRINT("Energy Wms: "); DEBUG_PRINTLN(wattmiliseconds); } @@ -287,14 +290,18 @@ class UsermodPower_Measurement : public Usermod { // Calculate energy - dont do it when led is off if (Power > 0) { - unsigned long elapsedTime = millis() - lastTime_energy; - wattmiliseconds += Power * elapsedTime; - } - lastTime_energy = millis(); + unsigned long currentTime = millis(); + unsigned long elapsedTime = currentTime - lastTime_energy; + lastTime_energy = currentTime; - if (wattmiliseconds >= 3600000000) { // 3,600,000 milliseconds = 1 hour - kilowatthours += wattmiliseconds / 3600000000; // Convert watt-milliseconds to kilowatt-hours (1 watt-millisecond = 1/3,600,000,000 kilowatt-hours) - wattmiliseconds = 0; + unsigned long long wattMilliseconds = static_cast(Power) * elapsedTime; + wattmiliseconds += wattMilliseconds; + + // Convert watt-milliseconds to kilowatt-hours + if (wattmiliseconds >= 3600000ULL) { // 3,600,000 milliseconds = 1 hour + watthours += static_cast(wattmiliseconds) / 3600000.0; + wattmiliseconds = 0; + } } } @@ -415,8 +422,8 @@ class UsermodPower_Measurement : public Usermod { Power_json.add(F(" W")); JsonArray Energy_json = user.createNestedArray(FPSTR("Energy")); - Energy_json.add(kilowatthours); - Energy_json.add(F(" kWh")); + Energy_json.add(watthours); + Energy_json.add(F(" Wh")); } void addToConfig(JsonObject& root) { @@ -546,10 +553,10 @@ class UsermodPower_Measurement : public Usermod { dtostrf(Power, 6, 2, payload); // Convert float to string mqtt->publish(subuf, 0, true, payload); - // Publish kilowatthours + // Publish watthours strcpy(subuf, mqttDeviceTopic); - strcat_P(subuf, PSTR("/power_measurement/kilowatthours")); - ultoa(kilowatthours, payload, 10); // Convert unsigned long to string + strcat_P(subuf, PSTR("/power_measurement/watthours")); + ultoa(watthours, payload, 10); // Convert unsigned long to string mqtt->publish(subuf, 0, true, payload); } } diff --git a/usermods/Power_Measurement/readme.md b/usermods/Power_Measurement/readme.md index 4df846179c..edbceb193d 100644 --- a/usermods/Power_Measurement/readme.md +++ b/usermods/Power_Measurement/readme.md @@ -91,4 +91,4 @@ This code was created by Tomáš Kuchta. ## Contributions -- Tomáš Kuchta (Initial idea) \ No newline at end of file +- Tomáš Kuchta (Initial idea and code) \ No newline at end of file