diff --git a/CLC-qthing/SiliconTorch/FxCyanF.cpp b/CLC-qthing/SiliconTorch/FxCyanF.cpp
index 4730d174efaff642b47e6187c88bfe13a9a4523e..9805186b34b248d9115a85ce2b606dac6f861298 100644
--- a/CLC-qthing/SiliconTorch/FxCyanF.cpp
+++ b/CLC-qthing/SiliconTorch/FxCyanF.cpp
@@ -3,6 +3,9 @@
 #include "esp_err.h"
 #include "esp_log.h"
 #include "driver/ledc.h"
+#include "tcpip_adapter.h"
+
+#include <nlohmann/json.hpp>
 
 #include <qthing>
 #include <qthing/mqtt_common.hpp>
@@ -17,6 +20,7 @@
 
 
 using namespace qthing;
+using json = nlohmann::json;
 
 
 const char* SiliconTorch::TAG = "fxCyan";  // TODO: maybe suffix with exact protocol…?
@@ -35,10 +39,13 @@ static float bytes2float(const char *bytes) {
 }
 
 
-SiliconTorch::FxCyanF::FxCyanF(uint32_t baseChannel) : metrics(Metrics::Metrics("fxCyan", 60*60)), baseChannel(baseChannel) {
+SiliconTorch::FxCyanF::FxCyanF(uint32_t baseChannel) : metrics(Metrics::Metrics("fxCyan")), baseChannel(baseChannel) {
+
+  this->metrics.registerMetric("frames", "frameCounter");
+  this->metrics.registerMetric("errors", "errorCounter");
 
-  metrics.registerMetric("frames", "FrameCounter");
-  metrics.registerMetric("errors", "ErrorCounter");
+  this->frameCntInc = this->metrics.generateMetricIncrementer("frames");
+  this->errorCntInc = this->metrics.generateMetricIncrementer("errors");
 
 
   // TODO: make vector!
@@ -127,6 +134,10 @@ SiliconTorch::FxCyanF::FxCyanF(uint32_t baseChannel) : metrics(Metrics::Metrics(
     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);
@@ -153,6 +164,11 @@ SiliconTorch::FxCyanF::FxCyanF(uint32_t baseChannel) : metrics(Metrics::Metrics(
   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(&SiliconTorch::FxCyanF::publishListenerInfo, this));
 }
 
 
@@ -186,6 +202,9 @@ bool SiliconTorch::FxCyanF::handleUnicast(const char *data, std::size_t length)
   int32_t diff = length - size;
   if (diff < 0) {
     ESP_LOGE(TAG, "Invalid data length[ %i ]: Received ΔB = %i bytes too few", length, -diff);
+
+    this->errorCntInc();
+
     return false;
   }
 
@@ -194,6 +213,7 @@ bool SiliconTorch::FxCyanF::handleUnicast(const char *data, std::size_t length)
     this->setPWM(ch, f);
   }
 
+  this->frameCntInc();
   this->callPacketCallback();
 
   return true;
@@ -206,6 +226,9 @@ bool SiliconTorch::FxCyanF::handleBroadcast(const char *data, std::size_t length
   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;
   }
 
@@ -214,6 +237,7 @@ bool SiliconTorch::FxCyanF::handleBroadcast(const char *data, std::size_t length
     this->setPWM(ch, f);
   }
 
+  this->frameCntInc();
   this->callPacketCallback();
 
   return true;
@@ -342,3 +366,26 @@ void SiliconTorch::FxCyanF::publishFrqRes() {
   publish_message(this->genDeviceTopic("frqres"), tmp);
 }
 
+void SiliconTorch::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");
+    }
+  }
+}
diff --git a/CLC-qthing/SiliconTorch/FxCyanF.hpp b/CLC-qthing/SiliconTorch/FxCyanF.hpp
index 5d410cca3f27d1f3b6c0c3337ee51e3f604378b4..d1f805bf89819a25de1a768253c62d0685ecf405 100644
--- a/CLC-qthing/SiliconTorch/FxCyanF.hpp
+++ b/CLC-qthing/SiliconTorch/FxCyanF.hpp
@@ -54,6 +54,7 @@ namespace SiliconTorch {
       void publishResolution();
       void publishBaseChannel();
       void publishChannelCount();
+      void publishListenerInfo();
 
     private:
 
@@ -73,6 +74,8 @@ namespace SiliconTorch {
       void callPacketCallback();
 
       Metrics::Metrics metrics;
+      Metrics::Incrementer frameCntInc;
+      Metrics::Incrementer errorCntInc;
   };
 
 }
diff --git a/CLC-qthing/SiliconTorch/Metrics.cpp b/CLC-qthing/SiliconTorch/Metrics.cpp
index 12c2a4291b9e177a8f07abed8751df89c5916bc5..980c44d3724c7d90974eac81df68d87fd234a010 100644
--- a/CLC-qthing/SiliconTorch/Metrics.cpp
+++ b/CLC-qthing/SiliconTorch/Metrics.cpp
@@ -9,11 +9,16 @@
 #include "esp_err.h"
 #include "esp_log.h"
 
-#include <ctime>
+#include <qthing>
+
 #include <climits>
 #include <cinttypes>
 
 
+using namespace qthing;
+using json = nlohmann::json;
+
+
 static const char* TAG = "Metrics";
 
 
@@ -28,11 +33,17 @@ namespace SiliconTorch {
 
   namespace Metrics {
 
-    Metrics::Metrics(const std::string& nameSpace, uint32_t saveInterval) : nameSpace(nameSpace), saveInterval(saveInterval) {
+    uint32_t uptime_ms() {
+      return (uint32_t)(esp_timer_get_time() / 1000L);
+    }
+
+    Metrics::Metrics(const std::string& nameSpace) : nameSpace(nameSpace) {
 
-      nvs_flash_init();
+      nvs_flash_init();  // TODO: WTF does it crash????
       nvs_open(nameSpace.c_str(), NVS_READWRITE, &this->nvs);
 
+      nvs_get_u64(this->nvs, "lifetime", &this->lifeTime);  // read lifetime value from NVS
+      lifeTimeSaved = uptime_ms();
 
 
       char taskName[64];
@@ -40,8 +51,16 @@ namespace SiliconTorch {
 
       std::function<void()> f = std::bind(&SiliconTorch::Metrics::Metrics::tickTask, this);
       std::function<void()> *_f = new std::function<void()>(f);
-      xTaskCreate(methodTaskWrapper, taskName, 8192, (void*)_f, 1, NULL);
+      xTaskCreatePinnedToCore(methodTaskWrapper, taskName, 8192, (void*)_f, 1, NULL, 0);
+
 
+      std::function<void(const std::string&)> getMetrics = [&](const std::string& ignored) {
+        publishData();
+      };
+
+      add_message_callback(genDeviceTopic("listener/get"), getMetrics);
+      add_message_callback(std::string("service/") + nameSpace + std::string("/listener/get"), getMetrics);
+      add_mqtt_connected_callback(std::bind(&SiliconTorch::Metrics::Metrics::publishData, this));
     }
 
 
@@ -51,36 +70,104 @@ namespace SiliconTorch {
       if (result != counters.end()) {
         ESP_LOGW(TAG, "Can't register Metric[ %s ]: Already exists", shortName.c_str());
       } else {
-        counters[shortName] = new CounterMetric(shortName, fullName, 10, 10);
+        CounterMetric* counter = new CounterMetric(shortName, fullName);
+
+        counter->load(&this->nvs);
+        counters[shortName] = counter;
       }
     }
 
+    void Metrics::publishData() {
+      json j;
+
+      for (auto it = counters.begin(); it != counters.end(); ++it) {
+        CounterMetric* metric = it->second;
+        j[metric->fullName.c_str()] = metric->getValue();
+      }
+
+      j["uptime"]   = uptime_ms() / 1000UL;
+      j["lifetime"] = (lifeTime + (uptime_ms() - lifeTimeSaved)) / 1000UL;
+
+      publish_message(genDeviceTopic("metrics"), j.dump().c_str());
+      lastPublished = uptime_ms();
+    }
 
     void Metrics::saveData() {
-      // TODO: implementation!
 
-      lastSaved = std::time(NULL);
+      for (auto it = counters.begin(); it != counters.end(); ++it)
+        it->second->save(&this->nvs);
+
+
+      uint32_t now = uptime_ms();
+
+      lifeTime += now - lifeTimeSaved;
+      lifeTimeSaved = now;
+
+      esp_err_t err = nvs_set_u64(this->nvs, "lifetime", this->lifeTime);
+
+      if (true) {
+      // if (err != ESP_OK) {  // !!!!!!
+        const char* errorName = esp_err_to_name(err);
+        ESP_LOGE(TAG, "Error[ %s ] saving LifeTimeMetric[ %lli ]", errorName, lifeTime);
+      }
+
+      nvs_commit(this->nvs);
+
+      lastSaved = uptime_ms();
     }
 
     void Metrics::tickTask() {
 
+      TickType_t lastWakeTime = xTaskGetTickCount();
+
       while (true) {
 
-        std::time_t now = std::time(NULL);
-        if (now - lastSaved > saveInterval) saveData();
+        uint32_t now = uptime_ms();
+
+        if ((now - lastSaved    ) / 1000 > saveInterval   ) saveData();
+        if ((now - lastPublished) / 1000 > publishInterval) publishData();
+
+        ESP_LOGW(TAG, "TickTack…   now = %i   lastSaved = %i   lastPublished = %i", now, lastSaved, lastPublished);
 
-        vTaskDelay((tickInterval * 1000) / portTICK_PERIOD_MS);
+        vTaskDelayUntil(&lastWakeTime, tickPeriod / portTICK_PERIOD_MS);
       }
     }
 
 
-    uint64_t getCounter(const std::string& shortName) {
-      return 0;
+    uint64_t Metrics::getCounterValue(const std::string& shortName) {
+
+      auto result = counters.find(shortName);
+
+      if (result != counters.end()) {
+        return result->second->getValue();
+      } else if (shortName.compare("uptime") == 0) {
+        return uptime_ms() / 1000UL;
+      } else if (shortName.compare("lifetime") == 0) {
+        return (lifeTime + (uptime_ms() - lifeTimeSaved)) / 1000UL;
+      } else {
+        return 0;  // TODO: log warning…?
+      }
+    }
+
+    std::string Metrics::genDeviceTopic(const char *suffix) {
+      return std::string(DEVICE_NAMESPACE) + nameSpace + std::string("/") + std::string(suffix);
     }
 
+    Incrementer Metrics::generateMetricIncrementer(const std::string& shortName) {
 
-    CounterMetric::CounterMetric(const std::string& shortName, const std::string& fullName, uint32_t saveInterval, uint32_t publishInterval) :
-      shortName(shortName), fullName(fullName), saveInterval(saveInterval), publishInterval(publishInterval) {
+      auto result = counters.find(shortName);
+
+      if (result != counters.end()) {
+        CounterMetric* counter = result->second;
+
+        return [counter]() { counter->inc(); };
+      } else {
+        return [](){};  // TODO: log warning…?
+      }
+    }
+
+
+    CounterMetric::CounterMetric(const std::string& shortName, const std::string& fullName) : shortName(shortName), fullName(fullName) {
 
       // TODO: implement!
 
@@ -89,18 +176,15 @@ namespace SiliconTorch {
 
 
     void CounterMetric::load(nvs_handle_t* nvs) {
-      lastSaved = std::time(NULL);
-
       nvs_get_u64(*nvs, shortName.c_str(), &this->value);
     }
 
     void CounterMetric::save(nvs_handle_t* nvs) {
-      if (std::time(NULL) - lastSaved > saveInterval) forceSave(nvs);
+      if (dirty) forceSave(nvs);
     }
 
     // TODO: synchronize!
     void CounterMetric::forceSave(nvs_handle_t* nvs) {
-      lastSaved = std::time(NULL);
       dirty = false;
 
       esp_err_t err = nvs_set_u64(*nvs, shortName.c_str(), this->value);
@@ -112,6 +196,10 @@ namespace SiliconTorch {
       }
     }
 
+    uint64_t CounterMetric::getValue() {
+      return value;
+    }
+
 
     void CounterMetric::inc() {
       if (value < ULLONG_MAX) {
diff --git a/CLC-qthing/SiliconTorch/Metrics.hpp b/CLC-qthing/SiliconTorch/Metrics.hpp
index b3b81a0d532816c02a312168f11ac47f64da9ca3..c5066e6a6424362c0d358b97a0fde1fcb3264c2d 100644
--- a/CLC-qthing/SiliconTorch/Metrics.hpp
+++ b/CLC-qthing/SiliconTorch/Metrics.hpp
@@ -15,17 +15,23 @@ namespace SiliconTorch {
 
     typedef std::function<void()> Incrementer;
 
+    uint32_t uptime_ms();  // rolls over after about 136 years
+
     class CounterMetric {
       public:
-        CounterMetric(const std::string& shortName, const std::string& fullName, uint32_t saveInterval, uint32_t publishInterval);
+        CounterMetric(const std::string& shortName, const std::string& fullName);
+
+        const std::string fullName;
+        const std::string shortName;
+
+        bool dirty = false;
 
-        const std::string& fullName;
-        const std::string& shortName;
+        uint64_t getValue();
 
         void inc();
         void dec();
 
-        void load(nvs_handle_t* nvs);        // load value from NVS
+        void load(nvs_handle_t* nvs);       // load value from NVS
         void save(nvs_handle_t* nvs);       // saves only if enoughg time elapsed
         void forceSave(nvs_handle_t* nvs);  // saves value immediately
 
@@ -36,39 +42,44 @@ namespace SiliconTorch {
         uint64_t operator--(int);  // post-decrement
 
       private:
-        uint64_t value = 0;
 
-        bool dirty = false;
-
-        uint32_t saveInterval;
-        uint32_t publishInterval;
+        uint64_t value = 0;
 
-        std::time_t lastSaved;
-        std::time_t lastPublished;
     };
 
     class Metrics {
       public:
-        Metrics(const std::string& nameSpace, uint32_t saveInterval = 60*60);
+        Metrics(const std::string& nameSpace);
 
         void registerMetric(const std::string& shortName, const std::string& fullName);
 
-        uint64_t getCounter(const std::string& shortName);  // returns the actual value or 0 for unknown counters
+        /*
+         * returns the actual value or 0 for unknown counters
+         * also supports getting the special uptime and lifetime metrics
+        */
+        uint64_t getCounterValue(const std::string& shortName);
 
         Incrementer generateMetricIncrementer(const std::string& shortName);
 
-
         void saveData();
+        void publishData();
 
+        const std::string nameSpace;
 
-        const std::string& nameSpace;
+        std::string genDeviceTopic(const char *suffix);
 
       private:
 
-        uint32_t saveInterval;       // seconds
-        uint32_t tickInterval = 60;  // seconds; internal tick task interval
+        uint32_t saveInterval    = 60; // *60;   // seconds
+        uint32_t publishInterval = 15; // *60;   // seconds
+
+        uint32_t lastSaved       = uptime_ms();  // unix timestamp
+        uint32_t lastPublished   = uptime_ms();  // unix timestamp
+
+        uint32_t tickPeriod      = 3000;         // milli seconds
 
-        std::time_t lastSaved = std::time(NULL);      // unix timestamp
+        uint64_t lifeTime        = 0;            // milli seconds
+        uint64_t lifeTimeSaved   = 0;            // milli seconds
 
         void tickTask();
 
diff --git a/CLC-qthing/device_main.cpp b/CLC-qthing/device_main.cpp
index 755fce820f58c9203fd91d2f04c32a6271aa24d3..7ff3f2e3fabe28dfea97940acf8efc03929edc48 100644
--- a/CLC-qthing/device_main.cpp
+++ b/CLC-qthing/device_main.cpp
@@ -13,7 +13,7 @@
 
 qthing::Config cfg;
 
-CyanLight::CyanLightControl ctrl(3);
+CyanLight::CyanLightControl* ctrl;
 
 
 
@@ -21,23 +21,24 @@ void device_main() {
 
   cfg.apply();
 
-  ctrl.setFrqRes(100, 19);
+  ctrl = new CyanLight::CyanLightControl(3);
+  ctrl->setFrqRes(100, 19);
 
 
   for (uint8_t i = 0; i < 100; i++) {
 
     bool _f = i % 2 == 0;
 
-    ctrl.setPWM(0,  _f      * 1.0f);
-    ctrl.setPWM(2, (_f ^ 1) * 1.0f);
+    ctrl->setPWM(0,  _f      * 1.0f);
+    ctrl->setPWM(2, (_f ^ 1) * 1.0f);
 
     vTaskDelay(50 / portTICK_PERIOD_MS);
   }
 
 
-  ctrl.setPWM(0, 0.0f);
-  ctrl.setPWM(1, 0.0f);
-  ctrl.setPWM(2, 1.0f);
+  ctrl->setPWM(0, 0.0f);
+  ctrl->setPWM(1, 0.0f);
+  ctrl->setPWM(2, 1.0f);
 
 
   qthing::enable_wifi();
@@ -51,13 +52,13 @@ void device_main() {
   while (true) {
     for (uint8_t i = 0; i < limit; i++) {
       float pwm = i / limit;
-      ctrl.setPWM(0, pwm*pwm);
+      ctrl->setPWM(0, pwm*pwm);
       vTaskDelay(delay);
     }
 
     for (uint8_t i = 0; i < limit; i++) {
       float pwm = i / limit;
-      ctrl.setPWM(0, 1.0f - pwm*pwm);
+      ctrl->setPWM(0, 1.0f - pwm*pwm);
       vTaskDelay(delay);
     }
   }