#include "FxCyanF.hpp" // C++ system level #include <string> #include <cstdio> #include <cstdlib> #include <algorithm> #include <functional> // ESP32 specific #include "esp_err.h" #include "esp_log.h" #include "driver/ledc.h" #include "tcpip_adapter.h" // project specific #include "FxVSync.hpp" #include "CyanBus.hpp" #include "Metrics.hpp" // qthing stuff #include <qthing> #include <qthing/mqtt_common.hpp> // misc #include <nlohmann/json.hpp> using namespace qthing; using json = nlohmann::json; namespace SiliconTorch { namespace FxCyanF { const char* TAG = "FxCyanF"; const char* nvsNameSpace = TAG; const std::string HEADER("fxCyanF"); const uint8_t MAX_CHANNELS = 8; // Maybe 16…? const std::string delimiter = ":"; static float bytes2float(const uint8_t* bytes) { float f; memcpy(&f, bytes, sizeof(f)); return f; } FxCyanF::FxCyanF(uint32_t baseChannel) : metrics(Metrics::Metrics("fxCyan")), baseChannel(baseChannel) { this->metrics.registerMetric("frames", "frameCounter"); this->metrics.registerMetric("errors", "errorCounter"); this->frameCntInc = this->metrics.generateMetricIncrementer("frames"); this->errorCntInc = this->metrics.generateMetricIncrementer("errors"); // TODO: make vector! this->channels = new Impl::PWMChannel*[MAX_CHANNELS]; this->timer_cfg.duty_resolution = (ledc_timer_bit_t)this->resolution; this->timer_cfg.freq_hz = this->frequency; this->timer_cfg.speed_mode = LEDC_HIGH_SPEED_MODE; this->timer_cfg.timer_num = LEDC_TIMER_0; this->timer_cfg.clk_cfg = LEDC_AUTO_CLK; ledc_timer_config(&this->timer_cfg); ledc_fade_func_install(0); add_binary_message_callback(this->genDeviceTopic("pwm/$all"), [&](qthing::multipart_message_t message) { if (message.offset == 0) this->handleUnicast((const uint8_t*)message.payload, message.length); else ESP_LOGE(TAG, "Invalid message format: Fragmentation is unsupported"); }); std::string header("fxCyanF"); uint8_t headerLength = header.length(); qthing::addUDPPacketCallback(header, [&, headerLength](udpPacket packet) { this->handleUnicast((const uint8_t*)packet.payload + headerLength, packet.length - headerLength); }); std::function<void(const std::string&)> setCh = [&](const std::string& message) { long int ch = strtol(message.c_str(), NULL, 0); this->setBaseChannel(ch); }; std::function<void(const std::string&)> setFrq = [&](const std::string& message) { long int frq = strtol(message.c_str(), NULL, 0); this->setFrequency(frq); }; std::function<void(const std::string&)> setRes = [&](const std::string& message) { long int res = strtol(message.c_str(), NULL, 0); this->setResolution(res); }; std::function<void(const std::string&)> setFrqRes = [&](const std::string& message) { std::string::size_type found = message.find(delimiter); if (found == std::string::npos) { ESP_LOGE(TAG, "Invalid message format[ %s ]", message.c_str()); return; } std::string _frq = message.substr(0, found); std::string _res = message.substr(found + delimiter.length()); long int frq = strtol(_frq.c_str(), NULL, 0); long int res = strtol(_res.c_str(), NULL, 0); if (frq == 0 || res == 0) { ESP_LOGE(TAG, "Invalid message format[ '%s' ]", message.c_str()); return; } this->setFrqRes(frq, res); }; std::function<void(const std::string&)> getBCh = [&](const std::string& ignored) { this->publishBaseChannel(); }; std::function<void(const std::string&)> getChs = [&](const std::string& ignored) { this->publishChannelCount(); }; std::function<void(const std::string&)> getFrq = [&](const std::string& ignored) { this->publishFrequency(); }; std::function<void(const std::string&)> getRes = [&](const std::string& ignored) { this->publishResolution(); }; std::function<void(const std::string&)> getFrqRes = [&](const std::string& ignored) { this->publishFrqRes(); }; std::function<void(const std::string&)> getListener = [&](const std::string& ignored) { this->publishListenerInfo(); }; // device-local setters add_message_callback(this->genDeviceTopic("frqres/set"), setFrqRes); add_message_callback(this->genDeviceTopic("channel/set"), setCh); add_message_callback(this->genDeviceTopic("frequency/set"), setFrq); add_message_callback(this->genDeviceTopic("resolution/set"), setRes); // device-lokal getters add_message_callback(this->genDeviceTopic("frequency/get"), getFrq); add_message_callback(this->genDeviceTopic("resolution/get"), getRes); add_message_callback(this->genDeviceTopic("frqres/get"), getFrqRes); add_message_callback(this->genDeviceTopic("channel/get"), getBCh); add_message_callback(this->genDeviceTopic("channelCnt/get"), getChs); // global setters add_message_callback(this->genServiceTopic("frqres/set"), setFrqRes); add_message_callback(this->genServiceTopic("channel/set"), setCh); add_message_callback(this->genServiceTopic("frequency/set"), setFrq); add_message_callback(this->genServiceTopic("resolution/set"), setRes); // global getters add_message_callback(this->genServiceTopic("frequency/get"), getFrq); add_message_callback(this->genServiceTopic("resolution/get"), getRes); add_message_callback(this->genServiceTopic("frqres/get"), getFrqRes); add_message_callback(this->genServiceTopic("channel/get"), getBCh); add_message_callback(this->genServiceTopic("channelCnt/get"), getChs); // Listener info getters and auto-publish add_message_callback(this->genDeviceTopic("listener/get"), getListener); add_message_callback(this->genServiceTopic("listener/get"), getListener); add_mqtt_connected_callback(std::bind(&FxCyanF::publishListenerInfo, this)); } bool FxCyanF::addChannel(uint8_t gpio) { if (this->channelsConfigured >= MAX_CHANNELS) { ESP_LOGE(TAG, "Cannot create channel#[ %i ]! ESP32 hardware limit is #[ %i ]", this->channelsConfigured, MAX_CHANNELS); return false; } uint8_t channel = this->channelsConfigured; // TODO: catch channel creation errors from IDF (e.g. on input-only GPIO) this->channels[channel] = new Impl::PWMChannel(channel, gpio); char topic[32]; snprintf(topic, sizeof(topic), "pwm/$%i", channel); add_message_callback(this->genDeviceTopic(topic), [&, channel](const std::string& message) { this->setPWM(channel, strtof(message.c_str(), NULL)); this->callPacketCallback(); }); this->channelsConfigured++; return true; } bool FxCyanF::handleUnicast(const uint8_t* data, std::size_t length) { std::size_t size = this->getChannelCount() * sizeof(float); int32_t diff = length - size; if (diff < 0) { ESP_LOGE(TAG, "Invalid |data|[ %d ]: Received ΔB[ %d ] bytes too few", length, -diff); this->errorCntInc(); return false; } for (uint8_t ch = 0; ch < this->getChannelCount(); ch++) { float f = bytes2float(data + ch * sizeof(float)); this->setPWM(ch, f); } this->frameCntInc(); this->callPacketCallback(); return true; } bool FxCyanF::handleBroadcast(const uint8_t* data, std::size_t length) { std::size_t size = this->getChannelCount() * sizeof(float); std::size_t offset = this->getBaseChannel() * sizeof(float); int32_t diff = length - offset - size; if (diff < 0) { // TODO: test thoroughly! ESP_LOGE(TAG, "Invalid data length[ %i ]: Received ΔB = %i bytes too few", length, -diff); this->errorCntInc(); return false; } for (uint8_t ch = 0; ch < this->getChannelCount(); ch++) { float f = bytes2float(data + offset + ch * sizeof(float)); this->setPWM(ch, f); } this->frameCntInc(); this->callPacketCallback(); return true; } void FxCyanF::setPWM(uint8_t channel, float value) { if (channel >= this->channelsConfigured) { ESP_LOGW(TAG, "ChannelID out of range: channel[ %i ]", channel); return; } value = this->gammaCorrector(value); if (value < 0.0f) value = 0.0f; if (value > 1.0f) value = 1.0f; uint32_t maxPWM = 1 << (this->resolution - 1); uint32_t pwm = (uint32_t)( value * (maxPWM - 1) ); this->channels[channel]->setPWM(pwm); } void FxCyanF::setGammaCorrector(GammaCorrector gammaCorrector) { this->gammaCorrector = gammaCorrector; } bool FxCyanF::setFrqRes(uint32_t frq_hz, uint8_t res_bits) { this->timer_cfg.duty_resolution = (ledc_timer_bit_t)res_bits; this->timer_cfg.freq_hz = frq_hz; if (ledc_timer_config(&this->timer_cfg) == ESP_OK) { this->frequency = frq_hz; this->resolution = res_bits; ESP_LOGI(TAG, "frequency[ %i Hz ] resolution[ %i bit ]", frq_hz, res_bits); return true; } else { ESP_LOGW(TAG, "Invalid frequency[ %i Hz ] and resolution[ %i bit ] setting!", frq_hz, res_bits); return false; } } void FxCyanF::registerAtCyanBus(CyanBus::CyanBus& cyanBus) { /* uint8_t headerLength = HEADER.length(); cyanBus.packetCallback.add(HEADER, [&, headerLength](const CyanBus::PacketData& packet) { this->handlePacket((uint8_t*)(packet.payload + headerLength), packet.length - headerLength); }); FxVSync::registerAtCyanBus(cyanBus, [&]() { //requestBufferSwap(); }); */ } void FxCyanF::setBaseChannel(uint16_t baseChannel) { this->baseChannel = baseChannel; } uint16_t FxCyanF::getBaseChannel() { return this->baseChannel; } uint8_t FxCyanF::getChannelCount() { return this->channelsConfigured; } bool FxCyanF::setFrequency(uint32_t frq_hz) { return this->setFrqRes(frq_hz, this->resolution); } uint32_t FxCyanF::getFrequency() { return this->frequency; } bool FxCyanF::setResolution(uint8_t res_bits) { return this->setFrqRes(this->frequency, res_bits); } uint8_t FxCyanF::getResolution() { return this->resolution; } void FxCyanF::setPacketHandledCallback(PacketHandledCallback callback) { this->packetCallback = callback; } std::string FxCyanF::genDeviceTopic(const char *suffix) { return std::string(DEVICE_NAMESPACE + "fxCyan/") + std::string(suffix); } std::string FxCyanF::genServiceTopic(const char *suffix) { return std::string("service/fxCyan/") + std::string(suffix); } void FxCyanF::callPacketCallback() { this->packetCallback(); } void FxCyanF::publishBaseChannel() { char tmp[16]; snprintf(tmp, sizeof(tmp), "%i", this->getBaseChannel()); publish_message(this->genDeviceTopic("channel"), tmp); } void FxCyanF::publishChannelCount() { char tmp[16]; snprintf(tmp, sizeof(tmp), "%i", this->getChannelCount()); publish_message(this->genDeviceTopic("channelCnt"), tmp); } void FxCyanF::publishFrequency() { char tmp[16]; snprintf(tmp, sizeof(tmp), "%i", this->getFrequency()); publish_message(this->genDeviceTopic("frequency"), tmp); } void FxCyanF::publishResolution() { char tmp[16]; snprintf(tmp, sizeof(tmp), "%i", this->getResolution()); publish_message(this->genDeviceTopic("resolution"), tmp); } void FxCyanF::publishFrqRes() { char tmp[32]; snprintf(tmp, sizeof(tmp), "%i%s%i", this->getFrequency(), delimiter.c_str(), this->getResolution()); publish_message(this->genDeviceTopic("frqres"), tmp); } void FxCyanF::publishListenerInfo() { if (qthing::is_mqtt_connected()) { // TODO: get IP Address of ethernet adapter tcpip_adapter_ip_info_t ipInfo; esp_err_t err; err = tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_STA, &ipInfo); if (err == ESP_OK) { char ip[16]; snprintf(ip, 16, "%d.%d.%d.%d", ipInfo.ip.addr & 0xFF, (ipInfo.ip.addr >> 8) & 0xFF, (ipInfo.ip.addr >> 16) & 0xFF, ipInfo.ip.addr >> 24); json j; j["IPv4"] = ip; j["port"] = "4213"; // TODO: get from qthing (currently not possible) publish_message(this->genDeviceTopic("listener"), j.dump().c_str()); } else { ESP_LOGW(TAG, "Can't determine IP"); } } } } }