diff --git a/CLC-qthing/CMakeLists.txt b/CLC-qthing/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..2dd04d521bbadac7a320499c479725e8470367a5
--- /dev/null
+++ b/CLC-qthing/CMakeLists.txt
@@ -0,0 +1,11 @@
+idf_component_register(
+  SRC_DIRS
+  "."
+
+  INCLUDE_DIRS
+  "."
+
+  REQUIRES
+  main
+)
+component_compile_options(-std=c++17)
diff --git a/CLC-qthing/Controller.cpp b/CLC-qthing/Controller.cpp
index 30495beab4bca082dc437d435cfd59e34b0b0dbd..dbc5ebb63c2a3ffe5bfb4652fb951a02c8d23eda 100644
--- a/CLC-qthing/Controller.cpp
+++ b/CLC-qthing/Controller.cpp
@@ -1,25 +1,27 @@
 #include "CyanLightControl.hpp"
 
-#include "qthing.h"
-#include "mqtt_common.h"
+#include <qthing>
+#include <qthing/mqtt_common.hpp>
 
 #include <string>
 #include <cstdio>
 #include <cstdlib>
-#include <function>
 #include <algorithm>
+#include <functional>
 
 #include "esp_err.h"
 #include "esp_log.h"
 #include "driver/ledc.h"
 
 
+using namespace qthing;
+
 
 const char *TAG = "CLC";
 const std::string delimiter = ":";
 
 
-float CyanLight::bytes2float(uint8_t *bytes) {
+static float bytes2float(const char *bytes) {
   float f;
 
   memcpy(&f, bytes, sizeof(f));
@@ -28,12 +30,12 @@ float CyanLight::bytes2float(uint8_t *bytes) {
 }
 
 
-CyanLight::Controller::Controller(uint8_t networkChannel, uint8_t channelsConfigured) : networkChannel{networkChannel} {
+CyanLight::Controller::Controller(uint16_t baseChannel, uint8_t channelsConfigured) : baseChannel{baseChannel} {
 
-  this->channelsConfigured = std::max(channelsConfigured, CyanLight::MAX_CHANNELS);
+  this->channelsConfigured = std::min(channelsConfigured, CyanLight::MAX_CHANNELS);
 
   this->channels = new CyanLight::PWMChannel*[this->channelsConfigured];
-  //this->channels = (CyanLight::PWMChannel **) malloc(this->channelsConfigured * sizeof(CyanLight::PWMChannel*));
+
 
   this->timer_cfg.duty_resolution = (ledc_timer_bit_t)this->resolution;
   this->timer_cfg.freq_hz = this->frequency;
@@ -46,22 +48,32 @@ CyanLight::Controller::Controller(uint8_t networkChannel, uint8_t channelsConfig
   ledc_fade_func_install(0);
 
 
-  for (uint8_t i = 0; i < channelsConfigured; i++) {
+  for (uint8_t i = 0; i < this->channelsConfigured; i++) {
     this->channels[i] = new CyanLight::PWMChannel(i, CyanLight::channelGPIOS[i]);
 
-    char topic[256];
-    snprintf(topic, sizeof(topic), DEVICE_NAMESPACE "CyanLight/pwm/$%i", i);
-
-    add_message_callback(topic, [&, i](std::string message) {
-
-      this->setChannel(i, strtof(message.c_str(), NULL));
+    char topic[32];
+    snprintf(topic, sizeof(topic), "pwm/$%i", i);
 
-      // float f = strtof(message.c_str(), NULL);
-      // ESP_LOGW(TAG, "i: %d   msg: %s   f: %f", i, message.c_str(), f);
+    add_message_callback(this->genDeviceTopic(topic), [&, i](std::string message) {
+      this->setPWM(i, strtof(message.c_str(), NULL));
+      this->callPacketCallback();
     });
   }
 
 
+  std::string header("fxCyanF");
+  uint8_t headerLength = header.length();
+
+  qthing::addUDPPacketCallback(header, [&, headerLength](udpPacket packet) {
+    this->handleUnicast(packet.payload + headerLength, packet.length - headerLength);
+  });
+
+
+  std::function<void(std::string)> setCh = [&](std::string message) {
+    long int ch = strtol(message.c_str(), NULL, 0);
+    this->setBaseChannel(ch);
+  };
+
   std::function<void(std::string)> setFrq = [&](std::string message) {
     long int frq = strtol(message.c_str(), NULL, 0);
     this->setFrequency(frq);
@@ -95,6 +107,14 @@ CyanLight::Controller::Controller(uint8_t networkChannel, uint8_t channelsConfig
   };
 
 
+  std::function<void(std::string)> getBCh = [&](std::string ignored) {
+    this->publishBaseChannel();
+  };
+
+  std::function<void(std::string)> getChs = [&](std::string ignored) {
+    this->publishChannelCount();
+  };
+
   std::function<void(std::string)> getFrq = [&](std::string ignored) {
     this->publishFrequency();
   };
@@ -109,34 +129,82 @@ CyanLight::Controller::Controller(uint8_t networkChannel, uint8_t channelsConfig
 
 
   // device-local setters
-  add_message_callback(DEVICE_NAMESPACE "CyanLight/frqres/set", setFrqRes);
-  add_message_callback(DEVICE_NAMESPACE "CyanLight/frequency/set", setFrq);
-  add_message_callback(DEVICE_NAMESPACE "CyanLight/resolution/set", setRes);
+  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(DEVICE_NAMESPACE "CyanLight/frequency/get", getFrq);
-  add_message_callback(DEVICE_NAMESPACE "CyanLight/resolution/get", getRes);
-  add_message_callback(DEVICE_NAMESPACE "CyanLight/frqres/get", getFrqRes);
+  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("service/CyanLight/frqres/set", setFrqRes);
-  add_message_callback("service/CyanLight/frequency/set", setFrq);
-  add_message_callback("service/CyanLight/resolution/set", setRes);
+  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("service/CyanLight/frequency/get", getFrq);
-  add_message_callback("service/CyanLight/resolution/get", getRes);
-  add_message_callback("service/CyanLight/frqres/get", getFrqRes);
+  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);
+}
+
+
+bool CyanLight::Controller::handleUnicast(const char *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 length[ %i ]: Received ΔB = %i bytes too few", length, -diff);
+    return false;
+  }
+
+  for (uint8_t ch = 0; ch < this->getChannelCount(); ch++) {
+    float f = bytes2float(data + ch * sizeof(float));
+    this->setPWM(ch, f);
+  }
+
+  this->callPacketCallback();
+
+  return true;
+}
+
+bool CyanLight::Controller::handleBroadcast(const char *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);
+    return false;
+  }
+
+  for (uint8_t ch = 0; ch < this->getChannelCount(); ch++) {
+    float f = bytes2float(data + offset + ch * sizeof(float));
+    this->setPWM(ch, f);
+  }
+
+  this->callPacketCallback();
+
+  return true;
 }
 
 
-void CyanLight::Controller::setChannel(uint8_t channel, float value) {
+void CyanLight::Controller::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;
 
@@ -147,6 +215,11 @@ void CyanLight::Controller::setChannel(uint8_t channel, float value) {
 }
 
 
+void CyanLight::Controller::setGammaCorrector(CyanLight::GammaCorrector gammaCorrector) {
+  this->gammaCorrector = gammaCorrector;
+}
+
+
 bool CyanLight::Controller::setFrqRes(uint32_t frq_hz, uint8_t res_bits) {
 
   this->timer_cfg.duty_resolution = (ledc_timer_bit_t)res_bits;
@@ -167,6 +240,19 @@ bool CyanLight::Controller::setFrqRes(uint32_t frq_hz, uint8_t res_bits) {
   }
 }
 
+
+void CyanLight::Controller::setBaseChannel(uint16_t baseChannel) {
+  this->baseChannel = baseChannel;
+}
+
+uint16_t CyanLight::Controller::getBaseChannel() {
+  return this->baseChannel;
+}
+
+uint8_t CyanLight::Controller::getChannelCount() {
+  return this->channelsConfigured;
+}
+
 bool CyanLight::Controller::setFrequency(uint32_t frq_hz) {
   return this->setFrqRes(frq_hz, this->resolution);
 }
@@ -183,21 +269,52 @@ uint8_t CyanLight::Controller::getResolution() {
   return this->resolution;
 }
 
+void CyanLight::Controller::setPacketHandledCallback(CyanLight::PacketHandledCallback callback) {
+  this->packetCallback = callback;
+}
+
+
+std::string CyanLight::Controller::genDeviceTopic(const char *suffix) {
+  return std::string(DEVICE_NAMESPACE + "fxCyanF/") + std::string(suffix);
+}
+
+std::string CyanLight::Controller::genServiceTopic(const char *suffix) {
+  return std::string("service/fxCyanF/") + std::string(suffix);
+}
+
+
+void CyanLight::Controller::callPacketCallback() {
+  this->packetCallback();
+}
+
+
+void CyanLight::Controller::publishBaseChannel() {
+  char tmp[16];
+  snprintf(tmp, sizeof(tmp), "%i", this->getBaseChannel());
+  publish_message(DEVICE_NAMESPACE + "CyanLight/channel", tmp);
+}
+
+void CyanLight::Controller::publishChannelCount() {
+  char tmp[16];
+  snprintf(tmp, sizeof(tmp), "%i", this->getChannelCount());
+  publish_message(DEVICE_NAMESPACE + "CyanLight/channelCnt", tmp);
+}
+
 void CyanLight::Controller::publishFrequency() {
   char tmp[16];
   snprintf(tmp, sizeof(tmp), "%i", this->getFrequency());
-  publish_message(DEVICE_NAMESPACE "CyanLight/frequency", tmp);
+  publish_message(DEVICE_NAMESPACE + "CyanLight/frequency", tmp);
 }
 
 void CyanLight::Controller::publishResolution() {
   char tmp[16];
   snprintf(tmp, sizeof(tmp), "%i", this->getResolution());
-  publish_message(DEVICE_NAMESPACE "CyanLight/resolution", tmp);
+  publish_message(DEVICE_NAMESPACE + "CyanLight/resolution", tmp);
 }
 
 void CyanLight::Controller::publishFrqRes() {
   char tmp[32];
   snprintf(tmp, sizeof(tmp), "%i%s%i", this->getFrequency(), delimiter.c_str(), this->getResolution());
-  publish_message(DEVICE_NAMESPACE "CyanLight/frqres", tmp);
+  publish_message(DEVICE_NAMESPACE + "CyanLight/frqres", tmp);
 }
 
diff --git a/CLC-qthing/CyanLightControl.hpp b/CLC-qthing/CyanLightControl.hpp
index 1afdd3e46a5071892355c90f08c725f02a5cbc8a..650b598a777c0dd58529fb904c8d537dcf1a8502 100644
--- a/CLC-qthing/CyanLightControl.hpp
+++ b/CLC-qthing/CyanLightControl.hpp
@@ -15,11 +15,21 @@ namespace CyanLight {
   
   const uint8_t channelGPIOS[MAX_CHANNELS] = { 27, 16, 17, 18, 19, 21 };
 
+  typedef std::function<void()> PacketHandledCallback;
+  typedef std::function<float(float)> GammaCorrector;
+
   class Controller {
     public:
-      Controller(uint8_t networkChannel, uint8_t channelsConfigured = 1);
+      Controller(uint16_t baseChannel, uint8_t channelsConfigured = 1);
+
+      void setPWM(uint8_t channel, float value);
+
+      void setGammaCorrector(GammaCorrector gammaCorrector);
 
-      void setChannel(uint8_t channel, float value);
+      uint8_t getChannelCount();
+
+      void setBaseChannel(uint16_t baseChannel);
+      uint16_t getBaseChannel();
 
       bool setFrequency(uint32_t frq_hz);
       uint32_t getFrequency();
@@ -29,19 +39,34 @@ namespace CyanLight {
 
       bool setFrqRes(uint32_t frq_hz, uint8_t res_bits);
 
+      void setPacketHandledCallback(PacketHandledCallback callback);
+
+      bool handleUnicast(const char *data, std::size_t length);
+      bool handleBroadcast(const char *data, std::size_t length);
+
+      std::string genDeviceTopic(const char *suffix);
+      std::string genServiceTopic(const char *suffix);
+
       void publishFrqRes();
       void publishFrequency();
       void publishResolution();
+      void publishBaseChannel();
+      void publishChannelCount();
+
     private:
       uint32_t frequency = 1000;  // Hz
-      uint8_t resolution =  10;  // bits
+      uint8_t resolution =   10;  // bits
 
-      uint8_t networkChannel;
+      uint16_t baseChannel;
       uint8_t channelsConfigured;
       PWMChannel **channels;
 
       ledc_timer_config_t timer_cfg;
+
+      GammaCorrector gammaCorrector = [](float f) { return f*f; };
+
+      PacketHandledCallback packetCallback = []() {};
+      void callPacketCallback();
   };
 
-  float bytes2float(uint8_t *bytes);
 }
diff --git a/CLC-qthing/PWMChannel.hpp b/CLC-qthing/PWMChannel.hpp
index 125c7d086e8b0a30b4bc9e3ef7f4e36f9c9294a4..61b57f13a8fb4f42ffba31edc1672a7f7fe05975 100644
--- a/CLC-qthing/PWMChannel.hpp
+++ b/CLC-qthing/PWMChannel.hpp
@@ -1,6 +1,6 @@
 #pragma once
 
-#include <qthing.h>
+#include <qthing>
 
 #include "driver/ledc.h"
 
diff --git a/CLC-qthing/device_main.cpp b/CLC-qthing/device_main.cpp
index e4eeb514109b5df8e5dedcc43b8dbbfee9ead109..04681872833be6092356cf822b16cbd97566c8fc 100644
--- a/CLC-qthing/device_main.cpp
+++ b/CLC-qthing/device_main.cpp
@@ -1,18 +1,23 @@
-#include <qthing.h>
+#include <qthing>
 
+#include "esp_log.h"
 #include "driver/gpio.h"
 
+#include "freertos/FreeRTOS.h"
+#include "freertos/task.h"
+
+
 #include "CyanLightControl.hpp"
 
 
 
-CyanLight::Controller ctrl(0, 3);
+CyanLight::Controller ctrl(0, 1);
 
 
 void device_main() {
 
   // ground the pin beneath our channel 0 IO
-  uint8_t GND = 23;
+  /* uint8_t GND = 23;
 
   gpio_config_t conf;
 
@@ -23,12 +28,31 @@ void device_main() {
   conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
 
   gpio_config(&conf);
-  gpio_set_level((gpio_num_t)GND, 0);
+  gpio_set_level((gpio_num_t)GND, 0); */
+
+
+  qthing::Config cfg;
+  cfg.apply();
 
 
+  qthing::enable_wifi();
 
+  return;
 
+  auto delay = 10 / portTICK_PERIOD_MS;
+  float limit = 50.0f;
 
+  while (true) {
+    for (uint8_t i = 0; i < limit; i++) {
+      float pwm = i / limit;
+      ctrl.setPWM(0, pwm*pwm);
+      vTaskDelay(delay);
+    }
 
-  enable_wlan();
+    for (uint8_t i = 0; i < limit; i++) {
+      float pwm = i / limit;
+      ctrl.setPWM(0, 1.0f - pwm*pwm);
+      vTaskDelay(delay);
+    }
+  }
 }
diff --git a/CLC-qthing/floatOut.py b/CLC-qthing/floatOut.py
index 96cd7967aaa398a9c740147c89d908450cead8d3..0b995f6822ad8b3ce41a5e798c5d95f01cac378f 100755
--- a/CLC-qthing/floatOut.py
+++ b/CLC-qthing/floatOut.py
@@ -1,16 +1,53 @@
 #!/usr/bin/env nix-shell
 #!nix-shell -i python -p python3
 
+import time
 import struct
+from socket import *
 
 
-value = 13.37
+port = 4213
+hosts = ['192.168.0.115']
 
+fps = 60
+step = 1 / 32
+
+
+s = socket(AF_INET, SOCK_DGRAM)
 
 def f2b(f):
   return bytearray(struct.pack("f", f))
 
 
-ba = f2b(value)
-print(ba)
-print([ "0x%02x" % b for b in ba ])
+x = 0.0
+goUp = True
+
+while True:
+
+  if goUp:
+    x += step
+  else:
+    x -= step
+
+  if x > 1:
+    x = 1.0
+    goUp = not goUp
+
+  if x < 0:
+    x = 0.0
+    goUp = not goUp
+
+
+  data = f2b(x)
+
+  for _ in range(5):
+    data += f2b(0.0)
+
+  data = bytearray(b'fxCyanF') + data
+
+  for host in hosts:
+    try:
+      s.sendto(data, (host, port))
+    except:
+      pass
+  time.sleep(1 / fps)