From 5afd816197cf3c1c9f350b8029b9fbbafe85e916 Mon Sep 17 00:00:00 2001 From: ZodiusInfuser Date: Thu, 3 Aug 2023 17:22:11 +0100 Subject: [PATCH] Work on LED strip functionality --- examples/yukon/yukon_simple_enable.cpp | 3 +- libraries/yukon/logging.cpp | 5 + libraries/yukon/logging.hpp | 38 ++++ libraries/yukon/modules/common.hpp | 140 +++++++++++++- .../yukon/modules/led_strip/led_strip.hpp | 178 ++++++++++++++++++ libraries/yukon/yukon.cmake | 3 +- libraries/yukon/yukon.cpp | 62 +++--- libraries/yukon/yukon.hpp | 78 ++------ 8 files changed, 413 insertions(+), 94 deletions(-) create mode 100644 libraries/yukon/logging.cpp create mode 100644 libraries/yukon/logging.hpp diff --git a/examples/yukon/yukon_simple_enable.cpp b/examples/yukon/yukon_simple_enable.cpp index bf91b056..5ed12073 100644 --- a/examples/yukon/yukon_simple_enable.cpp +++ b/examples/yukon/yukon_simple_enable.cpp @@ -42,7 +42,7 @@ int main() { //y.find_slots_with_module(LEDStripModule::info()); //y.find_slots_with_module(DualSwitchedModule::info()); //y.find_slots_with_module(BenchPowerModule::info()); - y.register_with_slot(&strip, 2); + y.register_with_slot(&strip, 3); y.initialise_modules(); //y.detect_module(Yukon::SLOT1); @@ -53,6 +53,7 @@ int main() { //y.detect_module(Yukon::SLOT6); y.enable_main_output(); + strip.enable(); while(!y.is_boot_pressed()) { y.monitored_sleep_ms(100); diff --git a/libraries/yukon/logging.cpp b/libraries/yukon/logging.cpp new file mode 100644 index 00000000..89737258 --- /dev/null +++ b/libraries/yukon/logging.cpp @@ -0,0 +1,5 @@ +#include "logging.hpp" + +namespace pimoroni { + logger logging; +} \ No newline at end of file diff --git a/libraries/yukon/logging.hpp b/libraries/yukon/logging.hpp new file mode 100644 index 00000000..21da5a1d --- /dev/null +++ b/libraries/yukon/logging.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include + +namespace pimoroni { + + enum LoggingLevel { + LOG_NONE = 0, + LOG_WARN, + LOG_INFO, + LOG_DEBUG + }; + + + struct logger { + uint level = LOG_INFO; + + void warn(std::string message) { + if(level >= LOG_WARN) + std::cout << message; + } + + void info(std::string message) { + if(level >= LOG_INFO) { + std::cout << message; + } + } + + void debug(std::string message) { + if(level >= LOG_DEBUG) { + std::cout << message; + } + } + }; + + extern logger logging; + +} diff --git a/libraries/yukon/modules/common.hpp b/libraries/yukon/modules/common.hpp index 53c981de..d3988e8e 100644 --- a/libraries/yukon/modules/common.hpp +++ b/libraries/yukon/modules/common.hpp @@ -3,9 +3,34 @@ #include "pico/stdlib.h" #include #include - +#include +#include +#include namespace pimoroni { + struct TCA { + uint CHIP; + uint GPIO; + }; + + struct SLOT { + uint ID; + uint FAST1; + uint FAST2; + uint FAST3; + uint FAST4; + TCA SLOW1; + TCA SLOW2; + TCA SLOW3; + uint ADC1_ADDR; + uint ADC2_TEMP_ADDR; + + // Needed for use with std::map + bool operator<(const SLOT& o) const { + return (ID < o.ID); + } + }; + enum ADC { ADC_LOW = 0, ADC_HIGH = 1, @@ -18,6 +43,21 @@ namespace pimoroni { HIGH = true }; + class SlotAccessor { + public: + virtual bool get_slow_input(TCA gpio) = 0; + virtual bool get_slow_output(TCA gpio) = 0; + virtual bool get_slow_config(TCA gpio) = 0; + virtual bool get_slow_polarity(TCA gpio) = 0; + + virtual void set_slow_output(TCA gpio, bool value) = 0; + virtual void set_slow_config(TCA gpio, bool output) = 0; + virtual void set_slow_polarity(TCA gpio, bool polarity) = 0; + + virtual float read_slot_adc1(SLOT slot) = 0; + virtual float read_slot_adc2(SLOT slot) = 0; + }; + class YukonModule { public: static constexpr float ROOM_TEMP = 273.15f + 25.0f; @@ -25,14 +65,106 @@ namespace pimoroni { static constexpr float BETA = 3435; virtual std::string name() = 0; + + SLOT slot; + SlotAccessor* __accessor; + bool initialised; + + YukonModule() : + slot(), + __accessor(nullptr), + initialised(false) { + + clear_readings(); + + // TODO + //self.__monitor_action_callback = None + } + + virtual void initialise(const SLOT& slot, SlotAccessor& accessor) { + // Record the slot we are in, and the ADC functions to call + this->slot = slot; + this->__accessor = &accessor; + initialised = true; + } + + bool is_initialised() { + return initialised; + } + + void deregister() { + initialised = false; + __accessor = nullptr; + } + + virtual void configure() { + // Function for (re)configuring pins etc to their default states needed by the module + } + + float __read_adc1() { + if(!is_initialised()) { + throw std::runtime_error("Module not initialised\n"); + } + return __accessor->read_slot_adc1(slot); + } + + float __read_adc2() { + if(!is_initialised()) { + throw std::runtime_error("Module not initialised\n"); + } + return __accessor->read_slot_adc2(slot); + } + + float __read_adc2_as_temp() { + if(!is_initialised()) { + throw std::runtime_error("Module not initialised\n"); + } + float sense = __accessor->read_slot_adc2(slot); + float r_thermistor = sense / ((3.3f - sense) / 5100.0f); + float t_kelvin = (BETA * ROOM_TEMP) / (BETA + (ROOM_TEMP * log(r_thermistor / RESISTOR_AT_ROOM_TEMP))); + float t_celsius = t_kelvin - 273.15f; + // https://www.allaboutcircuits.com/projects/measuring-temperature-with-an-ntc-thermistor/ + return t_celsius; + } + + void assign_monitor_action() { //TODO callback_function): + //if not None and not callable(callback_function): + // raise TypeError("callback is not callable or None") + + //self.__monitor_action_callback = callback_function + } + + virtual void monitor() { + } + + virtual std::vector> get_readings() { + std::vector> values; + return values; + } + + virtual void process_readings() { + // Use this to calculate averages, or do other post-processing on readings after monitor + } + + virtual void clear_readings() { + // Clear any readings that may accumulate, such as min, max, or average + } + + std::string __message_header() { + if(!is_initialised()) { + throw std::runtime_error("Module not initialised\n"); + } + return "[Slot" + std::to_string(slot.ID) + " '" + name() + "'] "; + } + }; typedef bool (&func_is_module)(uint, bool, bool, bool); struct ModuleType { - const std::type_index TYPE; - const std::string &NAME; - func_is_module is_module; + const std::type_index TYPE; // The typeid of the module class, used for checking if a detected module is a registered module + const std::string& NAME; // The name of the module class to display to the user + func_is_module is_module; // A reference to the function to call to detect a module of this type }; #define TYPE_FUNCTION(module_class) \ diff --git a/libraries/yukon/modules/led_strip/led_strip.hpp b/libraries/yukon/modules/led_strip/led_strip.hpp index 77aed1d1..4f51faa2 100644 --- a/libraries/yukon/modules/led_strip/led_strip.hpp +++ b/libraries/yukon/modules/led_strip/led_strip.hpp @@ -1,6 +1,13 @@ #pragma once #include "../common.hpp" +#include +#include "../../logging.hpp" +#include "../../errors.hpp" +#include "apa102.hpp" +#include "ws2812.hpp" + +using namespace plasma; namespace pimoroni { @@ -8,6 +15,13 @@ namespace pimoroni { public: static const std::string NAME; + enum strip_type { + NEOPIXEL = 0, + DOTSTAR = 1 + }; + + static constexpr float TEMPERATURE_THRESHOLD = 50.0f; + static bool is_module(uint adc_level, bool slow1, bool slow2, bool slow3); virtual std::string name() { @@ -15,6 +29,170 @@ namespace pimoroni { } TYPE_FUNCTION(LEDStripModule) + + bool halt_on_not_pgood; + bool __last_pgood; + + bool __power_good_throughout; + float __max_temperature; + float __min_temperature; + float __avg_temperature; + float __count_avg; + + WS2812* led_strip; + uint __power_good; + uint __power_en; + + + + LEDStripModule(bool halt_on_not_pgood = false) : + YukonModule(), + halt_on_not_pgood(halt_on_not_pgood), + __last_pgood(false), + led_strip(nullptr) { //TODO strip_type, num_pixels, brightness=1.0, halt_on_not_pgood=False): + //self.__strip_type = strip_type + //if self.__strip_type == self.NEOPIXEL: + // self.NAME += " (NeoPixel)" + //else: + // self.NAME += " (DotStar)" + + //self.__num_pixels = num_pixels + //self.__brightness = brightness + } + + ~LEDStripModule() { + delete(led_strip); + } + + virtual void initialise(const SLOT& slot, SlotAccessor& accessor) { + /* + // Create the strip driver object + if self.__strip_type == self.NEOPIXEL: + from neopixel import NeoPixel + self.pixels = NeoPixel(slot.FAST4, self.__num_pixels, brightness=self.__brightness, auto_write=False) + else: + from adafruit_dotstar import DotStar + self.pixels = DotStar(slot.FAST3, slot.FAST4, self.__num_pixels, brightness=self.__brightness, auto_write=False) + */ + led_strip = new WS2812(60, pio0, 0, slot.FAST4); + led_strip->start(60); + for(auto i = 0u; i < led_strip->num_leds; ++i) { + float hue = float(i) / led_strip->num_leds; + led_strip->set_hsv(i, hue, 1.0f, 1.0f); + } + + __power_good = slot.FAST1; + __power_en = slot.FAST2; + + // Create the power control pin objects + gpio_init(__power_good); // self.__power_good = DigitalInOut(slot.FAST1) + gpio_init(__power_en); // self.__power_en = DigitalInOut(slot.FAST2) + + + // Configure strip and power pins + configure(); + + // Pass the slot and adc functions up to the parent now that module specific initialisation has finished + YukonModule::initialise(slot, accessor); + } + + virtual void configure() { + //self.__power_en.switch_to_output(False) + gpio_set_function(__power_en, GPIO_FUNC_SIO); + gpio_set_dir(__power_en, GPIO_OUT); + gpio_put(__power_en, false); + + //self.__power_good.switch_to_input(Pull.UP) + gpio_set_function(__power_good, GPIO_FUNC_SIO); + gpio_set_dir(__power_good, GPIO_IN); + gpio_set_pulls(__power_good, true, false); + printf(("LED Strip Configured, apparently: " + std::to_string(__power_en) + "\n").c_str()); + } + + void enable() { + if(!is_initialised()) { + throw std::runtime_error("Module not initialised\n"); + } + gpio_put(__power_en, true); //self.__power_en.value = True + + printf(("LED Strip Enabled, apparently: " + std::to_string(__power_en) + "\n").c_str()); + } + + void disable() { + if(!is_initialised()) { + throw std::runtime_error("Module not initialised\n"); + } + gpio_put(__power_en, false); //self.__power_en.value = False + } + + bool is_enabled() { + // Documentation has note that this is just for debug use! + return gpio_get_out_level(__power_en); //return self.__power_en.value + } + + bool read_power_good() { + return gpio_get(__power_good); //return self.__power_en.value + } + + float read_temperature() { + return __read_adc2_as_temp(); + } + + virtual void monitor() { + bool pgood = read_power_good(); + if(!pgood) { + if(halt_on_not_pgood) { + throw FaultError(__message_header() + "Power is not good! Turning off output\n"); + } + } + + float temperature = read_temperature(); + if(temperature > TEMPERATURE_THRESHOLD) { + throw OverTemperatureError(__message_header() + "Temperature of " + std::to_string(temperature) + "°C exceeded the user set level of " + std::to_string(TEMPERATURE_THRESHOLD) + "°C! Turning off output\n"); + } + + if(__last_pgood && !pgood) { + logging.warn(__message_header() + "Power is not good\n"); + } + else if(!__last_pgood && pgood) { + logging.warn(__message_header() + "Power is good\n"); + } + + // Run some user action based on the latest readings + //if self.__monitor_action_callback is not None: + // self.__monitor_action_callback(pgood, temperature) + + __last_pgood = pgood; + __power_good_throughout = __power_good_throughout && pgood; + + __max_temperature = MAX(temperature, __max_temperature); + __min_temperature = MIN(temperature, __min_temperature); + __avg_temperature += temperature; + __count_avg += 1; + } + + virtual std::vector> get_readings() { + std::vector> values; + values.push_back(std::pair("PGood", __power_good_throughout)); + values.push_back(std::pair("T_max", __max_temperature)); + values.push_back(std::pair("T_min", __min_temperature)); + values.push_back(std::pair("T_avg", __avg_temperature)); + return values; + } + + virtual void process_readings() { + if(__count_avg > 0) { + __avg_temperature /= __count_avg; + } + } + + virtual void clear_readings() { + __power_good_throughout = true; + __max_temperature = -std::numeric_limits::infinity(); + __min_temperature = std::numeric_limits::infinity(); + __avg_temperature = 0; + __count_avg = 0; + } }; } diff --git a/libraries/yukon/yukon.cmake b/libraries/yukon/yukon.cmake index 12d8c75e..3a0e843d 100644 --- a/libraries/yukon/yukon.cmake +++ b/libraries/yukon/yukon.cmake @@ -2,6 +2,7 @@ add_library(yukon INTERFACE) target_sources(yukon INTERFACE ${CMAKE_CURRENT_LIST_DIR}/yukon.cpp + ${CMAKE_CURRENT_LIST_DIR}/logging.cpp ${CMAKE_CURRENT_LIST_DIR}/modules/led_strip/led_strip.cpp ${CMAKE_CURRENT_LIST_DIR}/modules/quad_servo/quad_servo_direct.cpp ${CMAKE_CURRENT_LIST_DIR}/modules/quad_servo/quad_servo_reg.cpp @@ -16,4 +17,4 @@ target_sources(yukon INTERFACE target_include_directories(yukon INTERFACE ${CMAKE_CURRENT_LIST_DIR}) # Pull in pico libraries that we need -target_link_libraries(yukon INTERFACE pico_stdlib pico_graphics tca9555 hardware_adc) +target_link_libraries(yukon INTERFACE pico_stdlib pico_graphics tca9555 hardware_adc plasma) diff --git a/libraries/yukon/yukon.cpp b/libraries/yukon/yukon.cpp index 519d3512..db90690f 100644 --- a/libraries/yukon/yukon.cpp +++ b/libraries/yukon/yukon.cpp @@ -143,6 +143,11 @@ namespace pimoroni { } void Yukon::reset() { + // Only disable the output if enabled (avoids duplicate messages) + if(is_main_output()) { + disable_main_output(); + } + // Set the first IO expander's initial state tca0.set_output_port(0x0000); tca0.set_polarity_port(0x0000); @@ -309,7 +314,7 @@ namespace pimoroni { YukonModule* module = slot_assignments[slot]; if(module != nullptr) { - //module.deregister() //TODO + module->deregister(); slot_assignments[slot] = nullptr; } } @@ -317,10 +322,6 @@ namespace pimoroni { const ModuleType* Yukon::__match_module(uint adc_level, bool slow1, bool slow2, bool slow3) { for(uint i = 0; i < count_of(KNOWN_MODULES); i++) { const ModuleType& m = KNOWN_MODULES[i]; - //printf("%s\n", std::get<0>(KNOWN_MODULES[i]).name()); - //printf("%s\n", std::get<1>(KNOWN_MODULES[i]).c_str()); - //printf("%d\n", std::get<2>(KNOWN_MODULES[i])(adc_level, slow1, slow2, slow3)); - //printf("%s\n", KNOWN_MODULES[i]..name()); if(m.is_module(adc_level, slow1, slow2, slow3)) { return &m; } @@ -473,7 +474,7 @@ namespace pimoroni { if(module != nullptr) { logging.info("[Slot" + std::to_string(slot.ID) + " '" + module->name() + "'] Initialising ... "); - //TODO module.initialise(slot, self.read_slot_adc1, self.read_slot_adc2) + module->initialise(slot, *this); logging.info("done\n"); } } @@ -687,13 +688,18 @@ namespace pimoroni { //if __monitor_action_callback is not None: // __monitor_action_callback(voltage, current, temperature); - //for module in self.__slot_assignments.values(): - // if module is not None: - // try: - // module.monitor() - // except Exception: - // self.disable_main_output() - // raise # Now the output is off, let the exception continue into user code + for(auto it = slot_assignments.begin(); it != slot_assignments.end(); it++) { + YukonModule* module = it->second; + if(module != nullptr) { + try { + module->monitor(); + } + catch(const std::exception& e) { + disable_main_output(); + throw; // Now the output is off, let the exception continue into user code + } + } + } readings.max_voltage = MAX(voltage, readings.max_voltage); readings.min_voltage = MIN(voltage, readings.min_voltage); @@ -760,9 +766,14 @@ namespace pimoroni { void Yukon::print_readings() { __print_named_readings("[Yukon]", get_readings()); - //for slot, module in self.__slot_assignments.items(): - // if module is not None: - // self.__print_dict(f"[Slot{slot.ID}]", module.get_readings(), allowed, excluded) + for(auto it = slot_assignments.begin(); it != slot_assignments.end(); it++) { + SLOT slot = it->first; + YukonModule* module = it->second; + + if(module != nullptr) { + __print_named_readings("[Slot" + std::to_string(slot.ID) + "]", module->get_readings()); + } + } std::cout << std::endl; //printf("\n"); } @@ -789,9 +800,12 @@ namespace pimoroni { readings.avg_temperature /= count_avg; } - //for module in self.__slot_assignments.values(): - // if module is not None: - // module.process_readings() + for(auto it = slot_assignments.begin(); it != slot_assignments.end(); it++) { + YukonModule* module = it->second; + if(module != nullptr) { + module->process_readings(); + } + } } void Yukon::__clear_readings() { @@ -811,9 +825,13 @@ namespace pimoroni { } void Yukon::clear_readings() { __clear_readings(); - //for module in self.__slot_assignments.values(): - // if module is not None: - // module.clear_readings() + + for(auto it = slot_assignments.begin(); it != slot_assignments.end(); it++) { + YukonModule* module = it->second; + if(module != nullptr) { + module->clear_readings(); + } + } } void Yukon::allow_reading(std::string name) { diff --git a/libraries/yukon/yukon.hpp b/libraries/yukon/yukon.hpp index b49cdc8b..93b95a87 100644 --- a/libraries/yukon/yukon.hpp +++ b/libraries/yukon/yukon.hpp @@ -5,65 +5,11 @@ #include "drivers/tca9555/tca9555.hpp" #include "errors.hpp" #include -#include #include "modules.hpp" +#include "logging.hpp" namespace pimoroni { - struct TCA { - uint CHIP; - uint GPIO; - }; - - struct SLOT { - uint ID; - uint FAST1; - uint FAST2; - uint FAST3; - uint FAST4; - TCA SLOW1; - TCA SLOW2; - TCA SLOW3; - uint ADC1_ADDR; - uint ADC2_TEMP_ADDR; - - // Needed for use with std::map - bool operator<(const SLOT& o) const { - return (ID < o.ID); - } - }; - - - enum LoggingLevel { - LOG_NONE = 0, - LOG_WARN, - LOG_INFO, - LOG_DEBUG - }; - - - struct logger { - uint level = LOG_INFO; - - void warn(std::string message) { - if(level >= LOG_WARN) - std::cout << message; - } - - void info(std::string message) { - if(level >= LOG_INFO) { - std::cout << message; - } - } - - void debug(std::string message) { - if(level >= LOG_DEBUG) { - std::cout << message; - } - } - }; - - - class Yukon { + class Yukon : public SlotAccessor { public: static const SLOT SLOT1; static const SLOT SLOT2; @@ -137,7 +83,7 @@ namespace pimoroni { float voltage_limit; float current_limit; float temperature_limit; - logger logging; + //logger logging; std::map slot_assignments; void* monitor_action_callback; @@ -173,14 +119,14 @@ namespace pimoroni { TCA9555& get_tca_chip(uint chip); public: - bool get_slow_input(TCA gpio); - bool get_slow_output(TCA gpio); - bool get_slow_config(TCA gpio); - bool get_slow_polarity(TCA gpio); + virtual bool get_slow_input(TCA gpio); + virtual bool get_slow_output(TCA gpio); + virtual bool get_slow_config(TCA gpio); + virtual bool get_slow_polarity(TCA gpio); - void set_slow_output(TCA gpio, bool value); - void set_slow_config(TCA gpio, bool output); - void set_slow_polarity(TCA gpio, bool polarity); + virtual void set_slow_output(TCA gpio, bool value); + virtual void set_slow_config(TCA gpio, bool output); + virtual void set_slow_polarity(TCA gpio, bool polarity); void change_output_mask(uint8_t chip, uint16_t mask, uint16_t state); //-------------------------------------------------- @@ -225,8 +171,8 @@ namespace pimoroni { float read_current(); float read_temperature(); float read_expansion(); - float read_slot_adc1(SLOT slot); - float read_slot_adc2(SLOT slot); + virtual float read_slot_adc1(SLOT slot); + virtual float read_slot_adc2(SLOT slot); float time(); void assign_monitor_action(void* callback_function);