Photon going in endless RGB loop, and not able to be factory restored

I bought 2 particle photon’s earlier this week from Adafruit, and received it yesterday. I was creating some Globally Synced Wifi Touch lights, the code was verified and I sent it off to both of them. 1 accepted the flash and has been working so well, the other one has been going in an infinite loop of connecting to the cloud, and then disconnecting from it. Basically the main LED indicator breathes cyan for a few seconds, then it switches to a hard green, breathes on that for a few seconds, then flashes cyan quickly then reconnects to the cloud.

I’ve thought it was something wrong with the flash, or something must’ve happened to the memory (it still operates which I am still happy about) but even when I try to connect it to both my laptop and pc, its not being recognized as a USB device, even when I put it in DFU mode. At one point in time with trying to put it in listening mode, it was in it for a few seconds, then started flashing SOS, then it rebooted and continued the loop

I would like to mainly figure out why my Windows 10 laptop and PC is not recognizing it as a USB device in both listening mode, and DFU mode, as well as figure out why the endless loop is happening.

Can you get it into Safe Mode successfully?

Yes I can, and it does hold in Safe Mode

Could you try flashing Tinker to it again, while in Safe mode?

Ive tried with the Android App and it failed, and Ive tried with the Command Tool CLI and it still failed, it doesnt recognize my particle as a USB device

If your computer is not recognizing your Particle device as a USB device, make sure you’re using either the Particle USB cable or a USB cable with a known data line. Some USB cables only have power and not data–and as such, won’t be usable to flash your device over the CLI.

Why does it continue to loop between LED statuses if i may ask

The RGB statuses are normal. Green indicates no connectivity. Blinking cyan is negotiating with the cloud. Breathing cyan is successfully connected. The behavior pattern is odd and I cannot immediately say why that is. The first recommendation is always to try and put it in a known good state by putting Tinker and the latest Device OS on there. If it continues to have issues after, we can do more comprehensive debugging.

I got part of the code to work, but im still having issues with it looping between breathing cyan, breathing green, and flashing cyan. like every 10 seconds it reloops

Your code is probably misbehaving.
If you can show it we may be able to spot the issue.

This is the code I am using in the Web IDE

This file is called touch_light.io

#include <neopixel.h>

// This #include statement was automatically added by the Particle IDE.
#include "wifi_creds.h"


/*
 * Project: touch_light
 * Description: A touch light that syncs with other touch lights. Adapted from
 *              http://www.instructables.com/id/Networked-RGB-Wi-Fi-Decorative-Touch-Lights/
 * Author: 
 * Date: 
 *
 */

//#include "application.h"

//#include "wifi_creds.h"


// CONFIGURATION SETTINGS START
// DEBUG SETTINGS:
#define D_SERIAL false
#define D_WIFI false

String touchEventName = "touch_event";

#define NUM_PARTICLES 2 // number of touch lights in your group
// Number each Filimin starting at 1.
String particleId[] = {
  "",                         // 0
  "4c0036000751373238323937", // Alex (Light_1)
  "400058001151373331333230", // Sarah (Light_2)
  
};

int particleColors[] = {
  0,   // Green
  90,  // Magenta
  170, // Blue
  79,  // Orange
  131  // Purple
};

// TWEAKABLE VALUES FOR CAP SENSING. THE BELOW VALUES WORK WELL AS A STARTING PLACE:
// BASELINE_VARIANCE: The higher the number the less the baseline is affected by
// current readings. (was 4)
#define BASELINE_VARIANCE 512.0
// SENSITIVITY: Integer. Higher is more sensitive (was 8)
#define SENSITIVITY 8
// BASELINE_SENSITIVITY: Integer. A trigger point such that values exceeding this point
// will not affect the baseline. Higher values make the trigger point sooner. (was 16)
#define BASELINE_SENSITIVITY 16
// SAMPLE_SIZE: Number of samples to take for one reading. Higher is more accurate
// but large values cause some latency.(was 32)
#define SAMPLE_SIZE 512
#define SAMPLES_BETWEEN_PIXEL_UPDATES 32
#define LOOPS_TO_FINAL_COLOR 150

const int minMaxColorDiffs[2][2] = {
  {5,20},   // min/Max if color change last color change from same touch light
  {50,128}  // min/Max if color change last color change from different touch light
};

// END VALUE, TIME
// 160 is approximately 1 second
const long envelopes[6][2] = {
  {0, 0},      // NOT USED
  {255, 30},   // ATTACK
  {200, 240},  // DECAY
  {200, 1000}, // SUSTAIN
  {150, 60},   // RELEASE1
  {0, 1000000} // RELEASE2 (65535 is about 6'45")
};

#define PERIODIC_UPDATE_TIME 5 // seconds
#define COLOR_CHANGE_WINDOW 10 // seconds

// CONFIGURATION SETTINGS END

// STATES:
#define ATTACK 1
#define DECAY 2
#define SUSTAIN 3
#define RELEASE1 4
#define RELEASE2 5
#define OFF 6

#define LOCAL_CHANGE 0
#define REMOTE_CHANGE 1

#define END_VALUE 0
#define TIME 1

#define tEVENT_NONE 0
#define tEVENT_TOUCH 1
#define tEVENT_RELEASE 2

String eventTypes[] = {
  "None",
  "Touch",
  "Release"
};

int sPin = D4;
int rPin = D3;

// NEOPIXEL
#define PIXEL_PIN D2
#define PIXEL_COUNT 24
#define PIXEL_TYPE WS2812B

// STATE
unsigned char myId = 0;

int currentEvent = tEVENT_NONE;
int eventTime = Time.now();
int eventTimePrecision = random(INT_MAX);

int initColor = 0;
int currentColor = 0; // 0 to 255
int finalColor = 0;   // 0 to 255
int lastLocalColorChangeTime = Time.now();

int initBrightness = 0;    // 0 to 255
int currentBrightness = 0; // 0 to 255

unsigned char prevState = OFF;
unsigned char state = OFF;

unsigned char lastColorChangeDeviceId = 1;

long loopCount = 0;
long colorLoopCount = 0;
int lastPeriodicUpdate = Time.now();

// timestamps
unsigned long tS;
volatile unsigned long tR;

// reading and baseline
float tBaseline;

double tDelayExternal = 0;
double tBaselineExternal = 0;

Adafruit_NeoPixel strip = Adafruit_NeoPixel(PIXEL_COUNT, PIXEL_PIN, PIXEL_TYPE);

#ifdef WIFI_CREDENTIALS_SPECIFIED
SYSTEM_MODE(SEMI_AUTOMATIC);
#endif

void setup()
{
#ifdef WIFI_CREDENTIALS_SPECIFIED
  setupWifi();
#endif

  Particle.subscribe(touchEventName, handleTouchEvent, MY_DEVICES);

  if (D_SERIAL) Serial.begin(9600);
  if (D_WIFI) {
    Particle.variable("tDelay", &tDelayExternal, DOUBLE);
    Particle.variable("tBaseline", &tBaselineExternal, DOUBLE);
  }

  strip.begin();
  strip.show(); // Initialize all pixels to 'off'

  pinMode(sPin, OUTPUT);
  attachInterrupt(rPin, touchSense, RISING);

  myId = getMyId(particleId, NUM_PARTICLES);

  flashWhite(&strip);

  // Calibrate touch sensor- Keep hands off!!!
  tBaseline = touchSampling(); // initialize to first reading
  if (D_WIFI) tBaselineExternal = tBaseline;

  traverseColorWheel(&strip);
  fade(&strip);
}

void loop() {
  int touchEvent = touchEventCheck();

  if (touchEvent == tEVENT_NONE) {
    // Publish periodic updates to synchronize state
    bool touchedBefore = currentEvent != tEVENT_NONE;
    if (lastPeriodicUpdate < Time.now() - PERIODIC_UPDATE_TIME && touchedBefore) {
      publishTouchEvent(currentEvent, finalColor, eventTime, eventTimePrecision);
      lastPeriodicUpdate = Time.now();
    }
    return;
  }

  // Random eventTimePrecision prevents ties with other
  // server events. This allows us to determine dominant
  // color in the event of ties.
  setEvent(touchEvent, Time.now(), random(INT_MAX));

  if (D_SERIAL) Serial.println(eventTypes[touchEvent]);
  if (touchEvent == tEVENT_TOUCH) {
    int newColor = generateColor(finalColor, prevState, lastColorChangeDeviceId);
    setColor(newColor, prevState, myId);
    changeState(ATTACK, LOCAL_CHANGE);
  }
}

//============================================================
//	Setup functions
//============================================================
//------------------------------------------------------------
// Functions used during setup
//------------------------------------------------------------
void setupWifi() {
  WiFi.on();
  WiFi.disconnect();
  WiFi.clearCredentials();
  int numWifiCreds = sizeof(wifiCreds) / sizeof(*wifiCreds);
  for (int i = 0; i < numWifiCreds; i++) {
    credentials creds = wifiCreds[i];
    WiFi.setCredentials(creds.ssid, creds.password, creds.authType, creds.cipher);
  }
  WiFi.connect();
  waitUntil(WiFi.ready);
  Particle.connect();
}

int getMyId(String particleId[], int numParticles) {
  int id = 0;
  for (int i = 1; i <= numParticles; i++) {
    if (!particleId[i].compareTo(Particle.deviceID())) {
      id = i;
      break;
    }
  }
  return id;
}

void flashWhite(Adafruit_NeoPixel* strip) {
  int numPixels = strip->numPixels();
  for (byte j = 0; j < numPixels; j++) {
    strip->setPixelColor(j, 255, 255, 255);
  }
  strip->show();
  delay(250);
  for (byte j = 0; j < numPixels; j++) {
    strip->setPixelColor(j, 0, 0, 0);
  }
  strip->show();
  delay(250);
}

void traverseColorWheel(Adafruit_NeoPixel* strip) {
  int numPixels = strip->numPixels();
  for (int i = 0; i < 256; i++) {
    uint32_t color = wheelColor(i, 255);
    for (byte j = 0; j < numPixels; j++) {
      strip->setPixelColor(j, color);
      strip->show();
    }
    delay(1);
  }
}

void fade(Adafruit_NeoPixel* strip) {
  int numPixels = strip->numPixels();
  for (int j = 255; j >= 0; j--) {
    uint32_t color = wheelColor(255, j);
    for (byte k = 0; k < numPixels; k++) {
      strip->setPixelColor(k, color);
      strip->show();
    }
    delay(1);
  }
}

//------------------------------------------------------------
// touch sampling
//
// sample touch sensor SAMPLE_SIZE times and get average RC delay [usec]
//------------------------------------------------------------
long touchSampling() {
  long tDelay = 0;
  int mSample = 0;
  static int timeout = 10000; // Timeout after 10000 failed readings
  int num_readings = 0;

  for (int i = 0; i < SAMPLE_SIZE; i++) {
    if (!(i % SAMPLES_BETWEEN_PIXEL_UPDATES)) {
      updateState();
    }
    pinMode(rPin, OUTPUT); // discharge capacitance at rPin
    digitalWrite(sPin,LOW);
    digitalWrite(rPin,LOW);
    pinMode(rPin,INPUT); // revert to high impedance input
    // timestamp & transition sPin to HIGH and wait for interrupt in a read loop
    num_readings = 0;
    tS = micros();
    tR = tS;
    digitalWrite(sPin,HIGH);
    do {
      // wait for transition
      num_readings++;
    } while (digitalRead(rPin)==LOW && num_readings < timeout);

    // accumulate the RC delay samples
    // ignore readings when micros() overflows
    if (tR > tS) {
      tDelay = tDelay + (tR - tS);
      mSample++;
    }
  }

  // calculate average RC delay [usec]
  if ((tDelay > 0) && (mSample > 0)) {
    tDelay = tDelay/mSample;
  } else {
    tDelay = 0;     // this is an error condition!
  }
  if (D_SERIAL) Serial.println(tDelay);
  if (D_WIFI) tDelayExternal = tDelay;
  // autocalibration using exponential moving average on data below specified point
  if (tDelay < (tBaseline + tBaseline/BASELINE_SENSITIVITY)) {
    tBaseline = tBaseline + (tDelay - tBaseline)/BASELINE_VARIANCE;
    if (D_WIFI) tBaselineExternal = tBaseline;
  }
  return tDelay;
}

//============================================================
//	Touch UI
//============================================================
//------------------------------------------------------------
// ISR for touch sensing
//------------------------------------------------------------
void touchSense() {
  tR = micros();
}

//------------------------------------------------------------
// touch event check
//
// check touch sensor for events:
//      tEVENT_NONE     no change
//      tEVENT_TOUCH    sensor is touched (Low to High)
//      tEVENT_RELEASE  sensor is released (High to Low)
//
//------------------------------------------------------------
int touchEventCheck() {
  int touchSense;                     // current reading
  static int touchSenseLast = LOW;    // last reading

  static unsigned long touchDebounceTimeLast = 0; // debounce timer
  int touchDebounceTime = 50;                     // debounce time

  static int touchNow = LOW;  // current debounced state
  static int touchLast = LOW; // last debounced state

  int tEvent = tEVENT_NONE;   // default event

  // read touch sensor
  long tReading = touchSampling();

  // touch sensor is HIGH if trigger point some threshold above Baseline
  if (tReading > (tBaseline + tBaseline / SENSITIVITY)) {
    touchSense = HIGH;
  } else {
    touchSense = LOW;
  }

  // debounce touch sensor
  // if state changed then reset debounce timer
  if (touchSense != touchSenseLast) {
    touchDebounceTimeLast = millis();
  }
  touchSenseLast = touchSense;

  // accept as a stable sensor reading if the debounce time is exceeded without reset
  if (millis() > touchDebounceTimeLast + touchDebounceTime) {
    touchNow = touchSense;
  }

  // set events based on transitions between readings
  if (!touchLast && touchNow) {
    tEvent = tEVENT_TOUCH;
  }

  if (touchLast && !touchNow) {
    tEvent = tEVENT_RELEASE;
  }

  // update last reading
  touchLast = touchNow;
  return tEvent;
}

void setEvent(int event, int timeOfEvent, int timePrecision) {
  currentEvent = event;
  eventTime = timeOfEvent;
  eventTimePrecision = timePrecision;
}

void handleTouchEvent(const char *event, const char *data) {
  String eventData = String(data);
  int deviceIdEnd = eventData.indexOf(',');
  int deviceId = eventData.substring(0, deviceIdEnd).toInt();
  int eventEnd = eventData.indexOf(',', deviceIdEnd + 1);
  int serverEvent = eventData.substring(deviceIdEnd + 1, eventEnd).toInt();
  int colorEnd = eventData.indexOf(',', eventEnd + 1);
  int serverColor = eventData.substring(eventEnd + 1, colorEnd).toInt();
  int eventTimeEnd = eventData.indexOf(',', colorEnd + 1);
  int serverEventTime = eventData.substring(colorEnd + 1, eventTimeEnd).toInt();
  int serverEventTimePrecision = eventData.substring(eventTimeEnd + 1).toInt();

  if (false) {
    String response = "deviceId: " + String(deviceId) + " " +
                      "serverEvent: " + String(serverEvent) + " " +
                      "serverColor: " + String(serverColor) + " " +
                      "localEventTime: " + String(eventTime) + " " +
                      "serverEventTime: " + String(serverEventTime) + " " +
                      "serverEventTimePrecision: " + String(serverEventTimePrecision);
    Particle.publish("touch_response", response, 61, PRIVATE);
  }

  if (deviceId == myId) return;
  if (serverEventTime < eventTime) return;
  // Race condition brought colors out of sync
  if (
    serverEventTime == eventTime &&
    serverEventTimePrecision == eventTimePrecision &&
    serverColor != finalColor &&
    myId < deviceId
  ) {
    setColor(serverColor, prevState, deviceId);
    changeState(ATTACK, REMOTE_CHANGE);
    return;
  }
  if (serverEventTime == eventTime && serverEventTimePrecision <= eventTimePrecision) return;

  // Valid remote update
  setEvent(serverEvent, serverEventTime, serverEventTimePrecision);

  if (serverEvent == tEVENT_TOUCH) {
    setColor(serverColor, prevState, deviceId);
    changeState(ATTACK, REMOTE_CHANGE);
  } else {
    changeState(RELEASE1, REMOTE_CHANGE);
  }
}

void setColor(int color, unsigned char prevState, unsigned char deviceId) {
  lastColorChangeDeviceId = deviceId;
  if (prevState == OFF) currentColor = color;
  initColor = currentColor;
  finalColor = color;
  colorLoopCount = 0;
  if (D_SERIAL) {
    Serial.print("get Color From Server Final color: ");
    Serial.print(initColor);
    Serial.print(", ");
    Serial.print(finalColor);
    Serial.print(", ");
  }
}

int generateColor(int currentFinalColor, unsigned char prevState, int lastColorChangeDeviceId) {
  int color = 0;
  int now = Time.now();
  Serial.println("generating color...");
  if (prevState == OFF || lastLocalColorChangeTime < now - COLOR_CHANGE_WINDOW) {
    color = particleColors[myId];
  } else {
    bool foreignId = (lastColorChangeDeviceId != myId);
    int minChange = minMaxColorDiffs[foreignId][0];
    int maxChange = minMaxColorDiffs[foreignId][1];
    int direction = random(2) * 2 - 1;
    int magnitude = random(minChange, maxChange + 1);
    color = currentFinalColor + direction * magnitude;
    color = (color + 256) % 256;
    // color = 119; // FORCE A COLOR
  }
  lastLocalColorChangeTime = now;
  if (D_SERIAL) { Serial.print("final color: "); Serial.println(finalColor); }
  return color;
}

void changeState(unsigned char newState, int remoteChange) {
  prevState = state;
  state = newState;
  initBrightness = currentBrightness;
  loopCount = 0;
  if (D_SERIAL) { Serial.print("state: "); Serial.println(newState); }

  if (remoteChange) return;

  if (newState == ATTACK || newState == RELEASE1) {
    publishTouchEvent(currentEvent, finalColor, eventTime, eventTimePrecision);
  }
}

void publishTouchEvent(int event, int color, int time, int timePrecision) {
  String response = String(myId)  + "," +
                    String(event) + "," +
                    String(color) + "," +
                    String(time)  + "," +
                    String(timePrecision);
  Particle.publish(touchEventName, response, 60, PRIVATE);
}

void updateState() {
  switch (state) {
    case ATTACK:
      if (loopCount >= envelopes[ATTACK][TIME]) {
        changeState(DECAY, LOCAL_CHANGE);
      }
      break;
    case DECAY:
      if ((loopCount >= envelopes[DECAY][TIME]) || (currentEvent == tEVENT_RELEASE)) {
        changeState(SUSTAIN, LOCAL_CHANGE);
      }
      break;
    case SUSTAIN:
      if ((loopCount >= envelopes[SUSTAIN][TIME]) || (currentEvent == tEVENT_RELEASE)) {
        changeState(RELEASE1, LOCAL_CHANGE);
      }
      break;
    case RELEASE1:
      if (loopCount >= envelopes[RELEASE1][TIME]) {
        changeState(RELEASE2, LOCAL_CHANGE);
      }
      break;
    case RELEASE2:
      if (loopCount >= envelopes[RELEASE2][TIME]) {
        changeState(OFF, LOCAL_CHANGE);
      }
      break;
  }

  currentBrightness = getCurrentBrightness(state, initBrightness, loopCount);
  if (currentColor != finalColor) {
    currentColor = getCurrentColor(finalColor, initColor, colorLoopCount);
  }

  uint32_t colorAndBrightness = wheelColor(currentColor, currentBrightness);
  updateNeoPixels(colorAndBrightness);
  loopCount++;
  colorLoopCount++;
}

int getCurrentBrightness(unsigned char state, int initBrightness, int loopCount) {
  if (state == OFF) return 0;
  int brightnessDistance = envelopes[state][END_VALUE] - initBrightness;
  int brightnessDistanceXElapsedTime = brightnessDistance * loopCount / envelopes[state][TIME];
  return min(255, max(0, initBrightness + brightnessDistanceXElapsedTime));
}

int getCurrentColor(int finalColor, int initColor, int colorLoopCount) {
  if (colorLoopCount > LOOPS_TO_FINAL_COLOR) return finalColor;
  int colorDistance = calcColorChange(initColor, finalColor);
  int colorDistanceXElapsedTime = colorDistance * colorLoopCount / LOOPS_TO_FINAL_COLOR;
  return (256 + initColor + colorDistanceXElapsedTime) % 256;
}

int calcColorChange(int currentColor, int finalColor) {
  int colorChange = finalColor - currentColor;
  int direction = (colorChange < 0) * 2 - 1;
  colorChange += direction * (abs(colorChange) > 127) * 256;
  return colorChange;
}

void updateNeoPixels(uint32_t color) {
  for(char i = 0; i < strip.numPixels(); i++) {
    strip.setPixelColor(i, color);
  }
  strip.show();
}

//============================================================
//	NEOPIXEL
//============================================================
//------------------------------------------------------------
// Wheel
//------------------------------------------------------------

uint32_t wheelColor(byte WheelPos, byte iBrightness) {
  float R, G, B;
  float brightness = iBrightness / 255.0;

  if (WheelPos < 85) {
    R = WheelPos * 3;
    G = 255 - WheelPos * 3;
    B = 0;
  } else if (WheelPos < 170) {
    WheelPos -= 85;
    R = 255 - WheelPos * 3;
    G = 0;
    B = WheelPos * 3;
  } else {
    WheelPos -= 170;
    R = 0;
    G = WheelPos * 3;
    B = 255 - WheelPos * 3;
  }
  R = R * brightness + .5;
  G = G * brightness + .5;
  B = B * brightness + .5;
  return strip.Color((byte) R,(byte) G,(byte) B);
}

This file is called wifi_cred.h/.cpp

/*
 * wifi_creds.h
 * Description: Specify any wifi credentials in this file to be automatically
 *              added to your Particle Photon.
 * NOTE: If you specify any credentials here, all other credentials
 *       stored on your photon will be cleared.
 * NOTE: Credentials should never be stored in version control. If you plan
 *       to specify credentials here, you should not commit them to your repo.
 * Author: Alexander IB
 * Date: 2019-08-23
 *
 */

// Uncomment the line below if specifying credentials in this file
// #define WIFI_CREDENTIALS_SPECIFIED

// See https://docs.particle.io/reference/firmware/photon/#setcredentials- for details
struct credentials { char *ssid; char *password; int authType; int cipher; };

const credentials wifiCreds[] = {
  // Set wifi creds here (up to 5) (last entry will be tried first)
  {.ssid="thisisfilledout", .password="thisisalsofilledout", .authType=WPA, .cipher=WLAN_CIPHER_AES_TKIP}
};

There is also a library I am bringing in called neopixel

Alex, A quick observation about your code and using neopixel library. You will need to include the macro SYSTEM_THREAD(ENABLED); otherwise with the long delays in the LED control functions the Particle Cloud connection is not getting attention. You can sprinkle Particle.process() through these functions or enable system threading. The problem then is that you may get some ‘hiccups’ in the data signal to the LEDs whilst the cloud comms are getting attention. There are ways around this but they make for more complex code. For starters I would just try SYSTEM_THREAD(ENABLED);

1 Like

Some more observations that may not solve the issue but make for better style and/or reliable code

  • Adafruit_NeoPixel strip = Adafruit_NeoPixel(PIXEL_COUNT, PIXEL_PIN, PIXEL_TYPE); is better written as Adafruit_NeoPixel strip(PIXEL_COUNT, PIXEL_PIN, PIXEL_TYPE);
  • the syntax for Particle.variable() is outdated, you’d just write Particle.variable("tDelay", tDelayExternal)
  • you should not call strip->show() after each individual pixel was set to a new value. Update all pixels first, then call strip->show() once.
  • try to avoid String and go with C strings (aka. char arrays) instead
  • try using enum or const int instead of loads of #defines. User defined structs are also something to consider.
  • the most time consuming function is touchEventCheck() (avg. 2.5sec - up to 10sec). When getting into the 10sec range that is enough to throw off the cloud connection, and that is what that RGB LED pattern tells you.

@ScruffR & @armor I am honestly not well versed within the language of C… If it would be possible, could I ask for some help on a rewrite of the code then, since you both know where the issues are at? I hate to ask but this project means a lot to me, and having help from you two would mean a lot to me.

I have reworked the code a bit (and removed the WiFi credentials setup as I find it superfluous for your personal units) but I have not changed the behaviour of touchEventCheck() as I’m not building the hardware to test any changes - although this would be the place I’d entirely rework if I were building these for me :wink:
I’d probably opt for a dedicated cap-touch sensor like this or this for 5 channels

However, you can try this
https://go.particle.io/shared_apps/5d6144cad4cdb00016129075

But be aware, once that code is running, updating the device OTA can take quite some time. If you want to re-flash the device you should put it in Safe Mode to prevent the running code from interfering with the update.

If I were to build it, I would ditch the touch sensor entirely, and use an accelerometer, which can be much simpler to implement, along with offering additional functionality :slight_smile: Better yet, don't worry about the hardware, and just use an internet button, which contains everything you need for a project like this. Then again, that's how I built these, so I might be biased:

1 Like

I sadly cant get rid of the touch sensor honestly, but thank you so much for reworking it for me!!!

How is that?
Have you even considered an alternative?
Do you know the differences and how the proposed alternatives could provide a similar UX?