<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Topics tagged with sdk]]></title><description><![CDATA[A list of topics that have been tagged with sdk]]></description><link>https://community.flic.io/tags/sdk</link><generator>RSS for Node</generator><lastBuildDate>Wed, 20 May 2026 20:08:19 GMT</lastBuildDate><atom:link href="https://community.flic.io/tags/sdk.rss" rel="self" type="application/rss+xml"/><pubDate>Invalid Date</pubDate><ttl>60</ttl><item><title><![CDATA[Flic duo gesture and twist sensitivity]]></title><description><![CDATA[<p dir="auto"><a class="plugin-mentions-user plugin-mentions-a" href="https://community.flic.io/uid/5330">@rickard</a> If anyone else wants me a chatgpt (98% chatgpt) did this</p>
<p dir="auto">This basically uses a twist motion as a variable speed adjuster that keeps going after hitting value from flic (intended). For that reason the virtual volume/lights/blinds should not have a limiter, instead you configure the limiter here in main.js</p>
<p dir="auto">All you need to do is set up a virtual adjuster in the flic app and this will log the output. You can always then send this over mqtt or such</p>
// rateDetentController.js
//
// Key fixes in this version:
// 1) Soft recenter while neutral-latched (prevents UP/DOWN asymmetry)
// 2) Debounced "ease into fine mode" so jitter does NOT trigger fine mode
//
// No behavior changes elsewhere.

function clamp(x, min, max) {
  return x &lt; min ? min : (x &gt; max ? max : x);
}
function sign(x) {
  return x &gt; 0 ? 1 : (x &lt; 0 ? -1 : 0);
}

class RateDetentController {
  constructor(cfg) {
    this.cfg = cfg || {};

    this.tickMs = this.cfg.tickMs || 333;

    // Neutral hysteresis
    this.deadbandEnter = typeof this.cfg.deadbandEnter === "number" ? this.cfg.deadbandEnter : 5;
    this.deadbandExit  = typeof this.cfg.deadbandExit  === "number" ? this.cfg.deadbandExit  : 9;

    // Speed tiers
    this.tier1MaxOff = typeof this.cfg.tier1MaxOff === "number" ? this.cfg.tier1MaxOff : 25;
    this.tier2MaxOff = typeof this.cfg.tier2MaxOff === "number" ? this.cfg.tier2MaxOff : 50;
    this.tierHys     = typeof this.cfg.tierHys === "number"     ? this.cfg.tierHys     : 3;

    // Output clamp
    this.minOutPct = typeof this.cfg.minOutPct === "number" ? this.cfg.minOutPct : 0;
    this.maxOutPct = typeof this.cfg.maxOutPct === "number" ? this.cfg.maxOutPct : 100;

    // --- NEW: soft recenter tuning ---
    this.neutralRecenterAlpha =
      typeof this.cfg.neutralRecenterAlpha === "number"
        ? this.cfg.neutralRecenterAlpha
        : 0.08; // 8% per update while resting

    // --- NEW: debounce for easing into fine mode ---
    this.easeConfirmMs =
      typeof this.cfg.easeConfirmMs === "number"
        ? this.cfg.easeConfirmMs
        : 200;

    // State
    this.centerInPct = null;
    this.actualOutPct = typeof this.cfg.initialOutPct === "number" ? this.cfg.initialOutPct : null;

    this.fineMode = false;

    this.lastDir = 0;
    this.lastSpeed = 0;

    this.currentDir = 0;
    this.currentSpeed = 0;

    this.neutralLatched = false;
    this.speedLatched = 0;

    // For debouncing easing
    this._easeCandidateSince = null;
    this._easeCandidateDir = 0;

    this._lastIntentKey = null;

    this._timer = setInterval(() =&gt; this._tick(), this.tickMs);
  }

  _desiredSpeed(absOff) {
    if (absOff &lt;= this.tier1MaxOff) return 1;
    if (absOff &lt;= this.tier2MaxOff) return 2;
    return 3;
  }

  _updateLatchedSpeed(desired, absOff) {
    if (this.speedLatched === 0) {
      this.speedLatched = desired;
      return;
    }

    if (this.speedLatched === 1) {
      if (desired &gt;= 2 &amp;&amp; absOff &gt;= (this.tier1MaxOff + this.tierHys)) this.speedLatched = 2;
      if (desired === 3 &amp;&amp; absOff &gt;= (this.tier2MaxOff + this.tierHys)) this.speedLatched = 3;
      return;
    }

    if (this.speedLatched === 2) {
      if (absOff &lt;= (this.tier1MaxOff - this.tierHys)) { this.speedLatched = 1; return; }
      if (desired === 3 &amp;&amp; absOff &gt;= (this.tier2MaxOff + this.tierHys)) { this.speedLatched = 3; return; }
      return;
    }

    if (this.speedLatched === 3) {
      if (absOff &lt;= (this.tier2MaxOff - this.tierHys)) this.speedLatched = 2;
    }
  }

  _baseIntent(rawInPct) {
    if (this.centerInPct === null) {
      this.centerInPct = rawInPct;
      this.neutralLatched = true;
      this.speedLatched = 0;
      return { dir: 0, speed: 0, desiredSpeed: 0, reason: "center set" };
    }

    const off = rawInPct - this.centerInPct;
    const absOff = Math.abs(off);

    // --- Sticky neutral with SOFT RECENTER ---
    if (this.neutralLatched) {
      if (absOff &lt;= this.deadbandExit) {
        // soft recenter while resting
        this.centerInPct =
          this.centerInPct +
          this.neutralRecenterAlpha * (rawInPct - this.centerInPct);

        this.speedLatched = 0;
        return { dir: 0, speed: 0, desiredSpeed: 0, reason: "deadband (latched)" };
      }
      this.neutralLatched = false;
    } else {
      if (absOff &lt;= this.deadbandEnter) {
        this.neutralLatched = true;
        this.speedLatched = 0;
        return { dir: 0, speed: 0, desiredSpeed: 0, reason: "deadband (enter)" };
      }
    }

    const dir = sign(off);
    const desiredSpeed = this._desiredSpeed(absOff);
    this._updateLatchedSpeed(desiredSpeed, absOff);

    const speed = (this.speedLatched === 0) ? 1 : this.speedLatched;
    return { dir, speed, desiredSpeed, reason: "detent" };
  }

  _applyFineMode(base) {
    const { dir, speed, desiredSpeed } = base;
    const now = Date.now();

    const directionChanged =
      (this.lastDir !== 0 &amp;&amp; dir !== 0 &amp;&amp; dir !== this.lastDir);

    const hitNeutralFromIntent =
      (this.lastSpeed &gt; 0 &amp;&amp; speed === 0);

    // --- Debounced easing detection ---
    let easedConfirmed = false;

    const easingCandidate =
      (this.lastSpeed &gt;= 2 &amp;&amp;
       desiredSpeed &gt; 0 &amp;&amp;
       desiredSpeed &lt; this.lastSpeed &amp;&amp;
       dir === this.lastDir);

    if (easingCandidate) {
      if (this._easeCandidateSince === null) {
        this._easeCandidateSince = now;
        this._easeCandidateDir = dir;
      } else if (
        this._easeCandidateDir === dir &amp;&amp;
        (now - this._easeCandidateSince) &gt;= this.easeConfirmMs
      ) {
        easedConfirmed = true;
      }
    } else {
      this._easeCandidateSince = null;
      this._easeCandidateDir = 0;
    }

    if (!this.fineMode &amp;&amp; (directionChanged || hitNeutralFromIntent || easedConfirmed)) {
      this.fineMode = true;
      this._easeCandidateSince = null;

      if (speed === 0) return { dir: 0, speed: 0, note: "enter fine (neutral)" };
      if (directionChanged) return { dir, speed: 1, note: "enter fine (turn)" };
      if (easedConfirmed) return { dir, speed: 1, note: "enter fine (ease)" };
      return { dir, speed: 1, note: "enter fine" };
    }

    if (this.fineMode) {
      if (speed === 0) return { dir: 0, speed: 0, note: "fine mode" };
      return { dir, speed: 1, note: "fine mode" };
    }

    return { dir, speed, note: null };
  }

  updateRaw(rawInPct) {
    if (typeof rawInPct !== "number") return null;

    if (this.actualOutPct === null) this.actualOutPct = this.minOutPct;
    if (this.centerInPct === null) this.centerInPct = rawInPct;

    const base = this._baseIntent(rawInPct);
    const applied = this._applyFineMode(base);

    this.currentDir = applied.dir;
    this.currentSpeed = applied.speed;

    const note = applied.note || base.reason || null;

    const key =
      this.currentDir + "|" +
      this.currentSpeed + "|" +
      (this.fineMode ? "F" : "-") + "|" +
      (this.neutralLatched ? "N" : "-") + "|" +
      (note || "");

    const intentChanged = (key !== this._lastIntentKey);
    this._lastIntentKey = key;

    this.lastDir = this.currentDir;
    this.lastSpeed = this.currentSpeed;

    return {
      intentChanged,
      rawInPct,
      dir: this.currentDir,
      speed: this.currentSpeed,
      fineMode: this.fineMode,
      note
    };
  }

  _tick() {
    if (this.actualOutPct === null) return;
    if (this.currentDir === 0 || this.currentSpeed === 0) return;

    this.actualOutPct = clamp(
      this.actualOutPct + (this.currentDir * this.currentSpeed),
      this.minOutPct,
      this.maxOutPct
    );
  }

  getActualOutPct() {
    return (this.actualOutPct === null) ? null : Math.round(this.actualOutPct);
  }

  stop() {
    if (this._timer) {
      clearInterval(this._timer);
      this._timer = null;
    }
    return this.getActualOutPct();
  }
}

module.exports = RateDetentController;


<p dir="auto">That can be tested with a main.js like this</p>
var buttons = require("buttons");
var flicapp = require("flicapp");
var RateDetentController = require("./rateDetentController");

var ROTATE_ARM_MS = 700;
var CLICK_SUPPRESS_AFTER_ROTATE_MS = 800;

var speakerVirtualDeviceId = null;
var lightVirtualDeviceId = null;
var blindVirtualDeviceId = null;

var stateByKey = {};
var rotateSessionByBdaddr = {};
var suppressClicksUntilByBdaddr = {};

var ctrlByKey = {};
var lastFinalOutByDevKey = {};

function keyOf(obj) { return obj.bdaddr + ":" + obj.buttonNumber; }
function sizeLabel(buttonNumber) { return buttonNumber === 0 ? "BIG" : "small"; }
function clamp01(x) { return x &lt; 0 ? 0 : (x &gt; 1 ? 1 : x); }
function toPct01(x01) { return typeof x01 === "number" ? Math.round(clamp01(x01) * 100) : null; }
function pctTo01(pct) { return clamp01(pct / 100); }
function isInRotateMode(bdaddr) { return !!rotateSessionByBdaddr[bdaddr]; }
function dirText(d) { return d &gt; 0 ? "UP" : (d &lt; 0 ? "DOWN" : "NEUTRAL"); }

function clearArmTimer(st) {
  if (st &amp;&amp; st.armTimer) { clearTimeout(st.armTimer); st.armTimer = null; }
}
function startRotateMode(st) {
  if (!st || st.rotateArmed) return;
  st.rotateArmed = true;
  rotateSessionByBdaddr[st.bdaddr] = { key: st.key, sizeLabel: st.sizeLabel, startedPrinted: false };
}

function inToOut(inPct, minOut, maxOut) {
  var range = maxOut - minOut;
  if (range &lt;= 0) return minOut;
  return minOut + (inPct / 100) * range;
}
function outToIn(outPct, minOut, maxOut) {
  var range = maxOut - minOut;
  if (range &lt;= 0) return 0;
  var v = ((outPct - minOut) / range) * 100;
  if (v &lt; 0) v = 0;
  if (v &gt; 100) v = 100;
  return Math.round(v);
}
function syncVirtualFromOut(type, id, outPct, minOut, maxOut) {
  var inPct = outToIn(outPct, minOut, maxOut);
  var v01 = pctTo01(inPct);

  if (type === "Speaker") flicapp.virtualDeviceUpdateState("Speaker", id, { volume: v01 });
  else if (type === "Light") flicapp.virtualDeviceUpdateState("Light", id, { brightness: v01 });
  else if (type === "Blind") flicapp.virtualDeviceUpdateState("Blind", id, { position: v01 });
}

// ---- buttons ----
buttons.on("buttonDown", function (obj) {
  if (!obj) return;

  var k = keyOf(obj);
  stateByKey[k] = {
    key: k,
    bdaddr: obj.bdaddr,
    buttonNumber: obj.buttonNumber,
    sizeLabel: sizeLabel(obj.buttonNumber),
    rotateArmed: false,
    armTimer: setTimeout(function () { startRotateMode(stateByKey[k]); }, ROTATE_ARM_MS)
  };
});

buttons.on("buttonUp", function (obj) {
  if (!obj) return;

  var k = keyOf(obj);
  var st = stateByKey[k];

  clearArmTimer(st);

  if (isInRotateMode(obj.bdaddr)) {
    delete rotateSessionByBdaddr[obj.bdaddr];
    suppressClicksUntilByBdaddr[obj.bdaddr] = Date.now() + CLICK_SUPPRESS_AFTER_ROTATE_MS;

    var keys = Object.keys(ctrlByKey);
    for (var i = 0; i &lt; keys.length; i++) {
      var entry = ctrlByKey[keys[i&rsqb;&rsqb;;
      if (!entry || entry.bdaddr !== obj.bdaddr) continue;

      var finalOut = entry.ctrl.stop();
      if (finalOut !== null) {
        console.log(entry.label + " - rotate end - " + entry.prettyName + " - final out " + finalOut + "%");
        lastFinalOutByDevKey[entry.devKey] = finalOut;
        syncVirtualFromOut(entry.type, entry.id, finalOut, entry.minOut, entry.maxOut);
      }

      delete ctrlByKey[keys[i&rsqb;&rsqb;;
    }
  }

  if (st &amp;&amp; !st.rotateArmed) {
    if (obj.gesture === "up" || obj.gesture === "down" || obj.gesture === "left" || obj.gesture === "right") {
      console.log(st.sizeLabel + " - " + obj.gesture.toUpperCase());
    }
  }

  delete stateByKey[k];
});

buttons.on("buttonSingleOrDoubleClick", function (obj) {
  if (!obj) return;
  if (isInRotateMode(obj.bdaddr)) return;

  var suppressUntil = suppressClicksUntilByBdaddr[obj.bdaddr] || 0;
  if (Date.now() &lt; suppressUntil) return;

  var lbl = sizeLabel(obj.buttonNumber);
  console.log(lbl + (obj.isDoubleClick === true ? " - DOUBLECLICK" : " - CLICK"));
});

// ---- rotate ----
flicapp.on("virtualDeviceUpdate", function (metaData, values) {
  if (!metaData || !metaData.dimmableType || !metaData.virtualDeviceId) return;

  var bdaddr = metaData.buttonId;
  if (!bdaddr) return;

  var session = rotateSessionByBdaddr[bdaddr];
  if (!session) return;

  var type = metaData.dimmableType;
  var id = metaData.virtualDeviceId;

  if (type === "Speaker" &amp;&amp; speakerVirtualDeviceId === null) speakerVirtualDeviceId = id;
  if (type === "Light" &amp;&amp; lightVirtualDeviceId === null) lightVirtualDeviceId = id;
  if (type === "Blind" &amp;&amp; blindVirtualDeviceId === null) blindVirtualDeviceId = id;

  if (type === "Speaker" &amp;&amp; id !== speakerVirtualDeviceId) return;
  if (type === "Light" &amp;&amp; id !== lightVirtualDeviceId) return;
  if (type === "Blind" &amp;&amp; id !== blindVirtualDeviceId) return;

  var inPct = null;
  var prettyName = null;

  if (type === "Speaker") { inPct = values &amp;&amp; typeof values.volume === "number" ? toPct01(values.volume) : null; prettyName = "tv speaker"; }
  else if (type === "Light") { inPct = values &amp;&amp; typeof values.brightness === "number" ? toPct01(values.brightness) : null; prettyName = "living room lights"; }
  else if (type === "Blind") { inPct = values &amp;&amp; typeof values.position === "number" ? toPct01(values.position) : null; prettyName = "test"; }

  if (inPct === null) return;

  var devKey = bdaddr + "|" + type + "|" + id;
  var entry = ctrlByKey[devKey];

  var minOut = 0, maxOut = 100;
  if (type === "Speaker") { minOut = 0; maxOut = 80; }
  else if (type === "Light") { minOut = 5; maxOut = 100; }
  else if (type === "Blind") { minOut = 0; maxOut = 100; }

  if (!entry) {
    var storedOut = lastFinalOutByDevKey[devKey];
    var startOut = (typeof storedOut === "number") ? storedOut : Math.round(inToOut(inPct, minOut, maxOut));

    syncVirtualFromOut(type, id, startOut, minOut, maxOut);

    var label = session.sizeLabel;

    var ctrl = new RateDetentController({
      initialOutPct: startOut,
      tickMs: 333,

      // STICKY NEUTRAL (tweak here)
      deadbandEnter: 5,
      deadbandExit: 9,

      // SENSITIVITY
      tier1MaxOff: 25,
      tier2MaxOff: 40,
      extremeBandPct: 10,

      minOutPct: minOut,
      maxOutPct: maxOut
    });

    entry = {
      bdaddr: bdaddr,
      devKey: devKey,
      type: type,
      id: id,
      prettyName: prettyName,
      label: label,
      ctrl: ctrl,
      minOut: minOut,
      maxOut: maxOut,
      lastOut: null
    };

    ctrlByKey[devKey] = entry;

    if (!session.startedPrinted) {
      session.startedPrinted = true;
      console.log(label + " - rotate mode - start " + prettyName + " - out " + startOut + "% (in " + outToIn(startOut, minOut, maxOut) + "%)");
    }
  }

  var u = entry.ctrl.updateRaw(inPct);
  if (!u) return;

  var outNow = entry.ctrl.getActualOutPct();

  // intent change line
  if (u.intentChanged) {
    console.log(
      entry.label + " - " + entry.prettyName +
      " - out " + (outNow === null ? "?" : outNow) + "% - " +
      dirText(u.dir) + " " + u.speed +
      " - in " + inPct + "%" +
      (u.note ? " - " + u.note : "")
    );
  }

  // tick/output line (only if output changed)
  if (outNow !== null &amp;&amp; outNow !== entry.lastOut) {
    entry.lastOut = outNow;
    console.log(entry.label + " - " + entry.prettyName + " - out " + outNow + "% - " + dirText(u.dir) + " " + u.speed);
  }
});

console.log("Ready. Short press: click/double + gesture. Hold &gt;= 700ms: rotate-only until release.");


]]></description><link>https://community.flic.io/topic/18611/flic-duo-gesture-and-twist-sensitivity</link><guid isPermaLink="true">https://community.flic.io/topic/18611/flic-duo-gesture-and-twist-sensitivity</guid><dc:creator><![CDATA[rickard]]></dc:creator><pubDate>Invalid Date</pubDate></item><item><title><![CDATA[Flic HUB SDK Error connecting to a remote websocket server (wss)]]></title><description><![CDATA[<p dir="auto"><a class="plugin-mentions-user plugin-mentions-a" href="https://community.flic.io/uid/4245">@antonio-mestre</a> the net module implements just a raw TCP socket. The secure web socket protocol uses the Websocket protocol on top of the TLS protocol on top of the TCP protocol.</p>
<p dir="auto">Right now our sdk unfortunately does not include modules for TLS (but we have https) nor Websocket, so you would have to implement that yourself if you want to use wss, using the net module in the bottom for TCP.</p>
]]></description><link>https://community.flic.io/topic/18062/flic-hub-sdk-error-connecting-to-a-remote-websocket-server-wss</link><guid isPermaLink="true">https://community.flic.io/topic/18062/flic-hub-sdk-error-connecting-to-a-remote-websocket-server-wss</guid><dc:creator><![CDATA[Emil]]></dc:creator><pubDate>Invalid Date</pubDate></item></channel></rss>