esp8266 / Arduino

ESP8266 core for Arduino
GNU Lesser General Public License v2.1
15.98k stars 13.33k forks source link

Version 3.0.0 up break my92xx library #8400

Open CBMalloch opened 2 years ago

CBMalloch commented 2 years ago

Basic Infos

Platform

Settings in IDE

Problem Description

Sending code to an Itead SONOFF B1 light bulb, which uses a my9231 chip to control the 5 channels of LED. When I use ESP8266 board definitions 2.7.4, everything works as planned. If I update to 3.0.0 or above, the bulb flashes more or less at random as I adjust values. The brightnesses no longer scale smoothly with the commands, nor go to the proper channels. Seems funny, since the my92xx library isn't using much - only digitalWrites and the c code for os_delay_us. I tried replacing the os_delay_us definition with delayMicroseconds, without any better luck.

MCVE Sketch

#define PROGNAME "ESP8266_B1_bulb"
#define VERSION  "1.1.0"
#define VERDATE  "2021-12-06"

  /*
    ESP8266_B1_bulb.ino
    2021-02-15
    Charles B. Malloch, PhD

    Program to control a Sonoff B1 Bulb
    Target hardware: ESP8266 on a Sonoff B1 RGB WW CW LED light bulb on Edison base

    v0.0.1  2021-02-13 cloned from ESP8266_Sonoff v1.1.1
    v1.2.1  2021-02-16 cbm repaired timer
    v0.2.4  2021-04-06 cbm ready for production test
    v0.2.15 2021-04-27 cbm added veil of indication
    v0.3.0  2021-11-29 cbm modified to alternately start up with red or white
    v0.3.7  2021-12-02 cbm removed timed auto-reboot, 'cause we don't retain color over
                           a soft restart. Could, with work, retrieve it from MQTT "status"
    v0.4.0  2021-12-02 cbm now fixed; writing and reading soft-restart file
    v1.0.0  2021-12-02 cbm calling it production!
    v1.1.0  2021-12-06 cbm substituting on-time for reset reason to keep old color

    Sonoff B1 Bulb Pinout, CW from CCW, for wiring to an ESP-01 programming adapter:
      1 Vcc (3.3) red
      2 RX        yellow NOTE this is the RX of the ESP, so RX header hole ( next to 3V3 )
      3 TX        green
      4 GND       black
      5 GPIO0     purple

    Use adapter from Sonoff box

    Schematic:
      https://user-images.githubusercontent.com/17343162/29202638-6b2c39ee-7e6a-11e7-93eb-727d7efaa24c.png
      3-color LEDs are controlled by my9231 chip

      http://www.bitfracture.com/pages/techarticles/squirrel-esp8266-esp8285-wifi-lightbulb-control-project
      has picture of my9231 setup (colors not right, though)

    MQTT: can send messages like home/bulb/b01/command -> {"color":[32,0,0,0,0]}
      [ R, G, B, WW, CW ]

    The current two bulbs are designated b01 (hall), b02 (office)

    Plans:
      Turn on full warm white, alternately non-max red
        thus acting like a little-bit-smart bulb

      MQTT changes: send timeout_ms only if testing
                    send reset, on, off messages

      Appears to work using upstairs computer and a05 with VERBOSE set to 12
      and looking at MQTT telemetry.

      --BUT--

      When compiled and sent from pro2, IT DOESN'T WORK RIGHT - the colors are screwed
      up. When compiled and sent from Hack, it work(ed) OK.
      Checked the my92xx library revisions - identical Hack and pro2.
      Testing with board revisions; broken ones are at 3.0.2; office OK at 2.7.x
      broken at 3.0.1, trying 3.0.0; still NG
      tried using delayMicroseconds instead of os_delay_us; still NG
      trying 2.7.4, just prior to 3.0.0: WORKS!!!

*/

#include <ESP8266WiFi.h>
#include <ArduinoJson.h>          //https://github.com/bblanchon/ArduinoJson

#include <WiFiClient.h>
#include <PubSubClient.h>
#include <ESP8266WebServer.h>
#include <WebSocketsServer.h>
// mDNS is now furnished by ArduinoOTA
// see https://tttapa.github.io/ESP8266/Chap08%20-%20mDNS.html
// ESP8266mDNS is multicast DNS - responds to <whatever>.local
// #include <ESP8266mDNS.h>
// ESP8266WiFiMulti looks at multiple access points and chooses the strongest
// #include <ESP8266WiFiMulti.h>
#include <WiFiUDP.h>
#include <TimeLib.h>

#include <LittleFS.h>

#define ALLOW_OTA
#ifdef ALLOW_OTA
  #include <ArduinoOTA.h>
#endif

#include <my92xx.h>

#include <cbmNetworkInfo.h>

#define SECOND_ms  ( 1000UL )
#define MINUTE_ms  ( 60UL * SECOND_ms )
#define HOUR_ms    ( 60UL * MINUTE_ms )

// R, G, B,  WW, CW
#define RED_color    0x80, 0x00, 0x00,  0x00, 0x00
#define WHITE_color  0x00, 0x00, 0x00,  0x80, 0x80

// ***************************************
// ***************************************
#pragma mark -> vars MAIN PARAMETERS

#define mqtt_baseTopic "home/bulb"

#define BAUDRATE    115200
#define VERBOSE          2 

// auto reboot will have side effect of turning bulb off! <- now fixed
const unsigned long rebootInterval_ms         = 24UL * HOUR_ms;

#define TESTING 0

// ***************************************
// ***************************************

/********************************** GPIO **************************************/
#pragma mark -> vars GPIO

/*
  I2C cbm standard colors: yellow for SDA, blue for SCL
  Blue onboard LED is inverse logic, connected as:
    ESP-01:
      GPIO0 for SDA
      GPIO1 ( TX ) is blue, inverse logic
      GPIO2 (next to GND) for SCL; 1K pullup on my boards
      GPIO3 ( RX ) is red
    Adafruit Huzzah: 
      GPIO0 is red
      GPIO2 is blue, inverse logic
      GPIO4 is SDA; GPIO5 is SCL
    Amica NodeMCU
      GPIO4 (D2) is SDA; GPIO5 (D1) is SCL
      GPIO16 (D0) is red but this is usually used to drive reset 
        LOW to pin RST (CH_PD / chip_enable)
      blue is GPIO1 TX conflicts with the use of Serial
    WeMos-WROOM-02: D16
    Sonoff
      pagoda:
        GPIO4 is red (after board modification) (inverse logic, hardware PWM)
        GPIO13 is green (inverse logic, no PWM)
      switch block:
        red LED associated with relay
        GPIO13 is blue (inverse logic, PWM)
      GPIO0 is the pushbutton (has pullup resistor)
      GPIO12 is the relay output
      GPIO14 is J1 Pin 5 (not used in this program)
    ITEAD Sonoff S31:
      GPIO0 is the pushbutton (has pullup resistor)
      GPIO12 is the relay output
      GPIO13 is green
    Sonoff Bulb
      GPIO12 (SDA) and GPIO14 (SCL) to two cascaded MY9231
      controlling Blue Red Green | Warm NC Cool
*/

#define INVERSE_LOGIC_ON    0
#define INVERSE_LOGIC_OFF   1

const int pd_LED_white   =  4;  // hardware PWM

/********************************* Network ************************************/
#pragma mark -> vars Network

// locales include M5, CBMIoT, CBMDATACOL, CBMDDWRT, CBMDDWRT3, CBMDDWRT3GUEST,
//   CBMBELKIN, CBM_RASPI_MOSQUITTO
// need to use CBMDDWRT3 for my own network access
// can use CBMIoT or CBMDDWRT3GUEST for Sparkfun etc.

#define WIFI_LOCALE CBMDATACOL
const unsigned int port_UDP = 9250;

/*
  The cbmNetworkInfo object has (at least) the following instance variables:
    .ip
    .gateway
    .mask
    .ssid
    .password
    .chipID    ( GUID assigned by manufacturer, often the last three octets
                 of the MAC address, e.g. 5c:cf:7f:xx:xx:xx )
    .chipName  ( globally-unique name assigned by cbm and marked on PCB
*/

cbmNetworkInfo Network;

// Create an ESP8266 WiFiClient class to connect to the MQTT server.
WiFiClient conn_TCP;
WiFiUDP conn_UDP;
PubSubClient conn_MQTT ( conn_TCP );
ESP8266WebServer htmlServer ( 80 );
WebSocketsServer webSocket = WebSocketsServer(81);

/************************************ mDNS ************************************/
#pragma mark -> vars mDNS

const int mdnsOtaIdLen = 40;
char mdnsOtaId [ mdnsOtaIdLen ];

/************************************ MQTT ************************************/
#pragma mark -> vars MQTT

const int mqttClientIDLen = 50;
char mqtt_clientID [ mqttClientIDLen ] = "whatever";

const int mqttTopicLen = 50;
char mqttCmdTopic [ mqttTopicLen ];
char mqttStatusTopic [ mqttTopicLen ];
char mqttTimeoutTopic [ mqttTopicLen ];
char mqttResetReasonTopic [ mqttTopicLen ];

/********************************** NTP ***************************************/
#pragma mark -> vars NTP

IPAddress timeServer ( 17, 253, 14, 253 );
const char* NTPServerName = "time.apple.com";

const int NTP_PACKET_SIZE = 48;   // NTP time stamp is in the first 48 bytes of the message
byte NTPBuffer [NTP_PACKET_SIZE]; // buffer to hold incoming and outgoing packets

const int timeZone = 0;   // GMT
// const int timeZone = -5;  // Eastern Standard Time (USA)
// const int timeZone = -4;  // Eastern Daylight Time (USA)
unsigned long lastNTPResponseAt_ms = millis();
// uint32_t time_unix_s               = 0;
bool ntpTimeGoodP = false;

/********************************** bulb **************************************/
#pragma mark -> vars bulb

#define MY92XX_MODEL        MY92XX_MODEL_MY9231
#define MY92XX_CHIPS        2
#define MY92XX_DI_PIN       12
#define MY92XX_DCKI_PIN     14

#define MY92XX_COLD         0
#define MY92XX_WARM         1
#define MY92XX_RED          4
#define MY92XX_GREEN        3
#define MY92XX_BLUE         5

my92xx * _my92xx;

byte currentR, currentG, currentB, currentW, currentC;
// output states: 0 -> off; 1 -> on; 2 -> on timer;
int outputState = 0;

const char * outputStateString [3] = { "OFF", "ON", "TIMER" };
#if TESTING
  const unsigned long turnOffTimerDuration_ms = 6UL * SECOND_ms;
#else
  const unsigned long turnOffTimerDuration_ms =        30UL * MINUTE_ms;
#endif

unsigned long timerStartedAt_ms = 0UL;
unsigned long timeoutValue_ms = 0UL;
unsigned long settingsDirtyAt_ms = 0UL;

/******************************* Global Vars **********************************/
#pragma mark -> vars global

char uniqueToken [ 9 ];
unsigned long lastRebootAt_nts                 = 0UL;

const size_t pBufLen = 128;
char pBuf [ pBufLen ];
const int htmlMessageLen = 3072;
char htmlMessage [htmlMessageLen];
const int timeStringLen = 32;
char timeString [ timeStringLen ] = "<unset>";
char bootTimeString [ timeStringLen ] = "<unset>";

const size_t jsonStrSize = 1000; 
char jsonString [ jsonStrSize ];  // needs to be global to be on heap!

bool forceWSUpdate = false;

/**************************** Function Prototypes *****************************/
#pragma mark -> function prototypes

void POST ();

void initializeGPIO ();
void initializeBulb ();
void initializeLittleFS ();
void initializeLampColor ();
void lampColorInitialize_powercycle ();
void lampColorInitialize_restorePrevious ();
void initializeWebServer ();

bool connect_WiFi ();
bool connect_MQTT ();
void handleReceivedMQTTMessage ( char * topic, byte * payload, unsigned int length );
void sendSettingsToMQTT ( bool force = false );
int sendValueToMQTT ( const char * topic, int value, const char * name, bool retainP = false );
int sendValueToMQTT ( const char * topic, const char * value, const char * name, bool retainP = false );

void interpretNewCommandString ( char * theTopic, char * thePayload );

void htmlResponse_root ();
void webSocketEvent ( uint8_t num, WStype_t type, uint8_t * payload, size_t length );
void update_WebSocket ();

time_t getUnixTime();
void sendNTPpacket ( IPAddress& address );
void formatTimeString ( char * result, int resultLen, unsigned long time );
void formatIntervalString ( char * result, int resultLen, unsigned long time );

void saveSettingsToFile ();

int setOutputState ( int state, bool force = false );
void sendOutput ();
void setOutput ( byte R, byte G, byte B, byte W, byte C );
void toggleOutput ();
void setTimer ( unsigned long p_timeoutValue_ms = turnOffTimerDuration_ms );
unsigned long timeRemaining_ms ();

// magic juju to return array size
// see http://forum.arduino.cc/index.php?topic=157398.0
template< typename T, size_t N > size_t ArraySize (T (&) [N]){ return N; }

/******************************************************************************/

void setup ( void ) {

  if ( VERBOSE >= 12 ) delay ( 10000 );

  initializeGPIO ();
  initializeBulb ();

  #if TESTING
    Serial.printf ( "\n\nTESTING mode %d\n", TESTING );
    POST();
  #endif

  /*********************** LittleFS File System Setup *************************/

  initializeLittleFS ();

  lampColorInitialize_restorePrevious ();

  /****************************** WiFi Setup **********************************/

  // for security reasons, the network settings are stored in a private library
  Network.init ( WIFI_LOCALE );

  if ( ! strncmp ( Network.chipName, "unknown", 12 ) ) {
    Network.describeESP ( Network.chipName );
  }

  // Connect to WiFi access point.
  Serial.printf ( "\nESP8266 device '%s' connecting to %s\n", Network.chipName, Network.ssid );
  yield ();

  WiFi.config ( Network.ip, Network.gw, Network.mask, Network.dns );
  // for exception 3 problem, use erase wifi parameters once
  WiFi.begin ( Network.ssid, Network.password );

Serial.println ( F ( "about to connect_WiFi" ) );
  connect_WiFi ();

  // while ( WiFi.status() != WL_CONNECTED ) {
  //   Serial.print ( F ( "v" ) );
  //   delay ( 500 );  // implicitly yields but may not pet the nice doggy
  // }
  // Serial.println ();

  if ( VERBOSE >= 15 ) WiFi.printDiag ( Serial );

  if ( VERBOSE >= 4 ) {
    Serial.println ( F ( "WiFi connected with IP address: " ) );
    Serial.println ( WiFi.localIP() );
  }

  /************************** Random Token Setup ******************************/

  // create a hopefully-unique string to identify this program instance
  // REQUIREMENT: must have initialized the network for chipName

  #ifdef cbmnetworkinfo_h
    // Chuck-only
    strncpy ( uniqueToken, Network.chipName, 9 );
  #else
    snprintf ( uniqueToken, 9, "%08x", ESP.getChipId() );
  #endif

  snprintf ( mdnsOtaId, mdnsOtaIdLen, "esp8266-%s", uniqueToken );
  Serial.printf ( "mDNS host name: %s.local\n", mdnsOtaId );

  /******************************* UDP Setup **********************************/

  conn_UDP.begin ( port_UDP );

  if ( VERBOSE >= 4 ) Serial.println ( F ( "UDP connected" ) );
  delay ( 50 );

  /****************************** MQTT Setup **********************************/

  snprintf ( mqtt_clientID, mqttClientIDLen, "%s_%s", PROGNAME, uniqueToken );

  snprintf ( mqttCmdTopic, mqttTopicLen, "%s/%s/%s", mqtt_baseTopic, uniqueToken, "command" );
  snprintf ( mqttStatusTopic, mqttTopicLen, "%s/%s/%s", mqtt_baseTopic, uniqueToken, "status" );
  snprintf ( mqttTimeoutTopic, mqttTopicLen, "%s/%s/%s", mqtt_baseTopic, uniqueToken, "timeout_ms" );
  snprintf ( mqttResetReasonTopic, mqttTopicLen, "%s/%s/%s", mqtt_baseTopic, uniqueToken, "reset_reason" );

  conn_MQTT.setCallback ( handleReceivedMQTTMessage );

  /*

  // testing because it wouldn't find the DNS -- it needs the Network.dns arg
  // when we connect!!! See above:
  // WiFi.config ( Network.ip, Network.gw, Network.mask, Network.dns );

  IPAddress mosq;
  WiFi.hostByName( CBM_MQTT_SERVER, mosq );
  Serial.print ( F ( "Here's what we found for the IP of '" ) );
  Serial.print ( CBM_MQTT_SERVER );
  Serial.print ( F ( "': " ) );
  Serial.println ( mosq );

  */

  conn_MQTT.setServer ( CBM_MQTT_SERVER, CBM_MQTT_SERVERPORT );  
  if ( VERBOSE >= 5 ) {
    Serial.print ( F ( "Connecting to MQTT at " ) );
    Serial.println ( CBM_MQTT_SERVER );
  }
  connect_MQTT ();

  yield ();

  /******************************* NTP Setup **********************************/

  setSyncProvider ( getUnixTime );
  setSyncInterval ( 300 );          // seconds
  unsigned long beganWaitingAt_ms = millis();

  while ( ( timeStatus() != timeSet ) && ( ( millis () - beganWaitingAt_ms ) < 10000UL ) ) {
    now ();
    Serial.print ( F ( "t" ) );
    delay ( 100 );
    yield();
  }

  snprintf ( pBuf, pBufLen, "%s/%s/time/startup", PROGNAME, uniqueToken );
  if ( timeStatus() == timeSet ) {
    lastRebootAt_nts = now();
    formatTimeString ( bootTimeString, timeStringLen, now () );
    sendValueToMQTT ( pBuf, bootTimeString, "Startup Time" );
    Serial.printf ( "Starting at %s\n", bootTimeString );
  } else {
    Serial.println ( F ( "WARNING: No NTP response within 10 seconds..." ) );
    sendValueToMQTT ( pBuf, "WARNING no NTP within 10 seconds", "Startup Time" );
  }
  yield();

  /***************************** OTA Update Setup *****************************/

  #ifdef ALLOW_OTA

    // Port defaults to 8266
    // ArduinoOTA.setPort(8266);

    // Hostname defaults to esp8266-[ChipID]
    ArduinoOTA.setHostname ( (const char *) mdnsOtaId );

    // No authentication by default
    ArduinoOTA.setPassword ( (const char *) CBM_OTA_KEY );

    ArduinoOTA.onStart([]() {
      Serial.println ( F ( "Start" ) );
    });

    ArduinoOTA.onEnd([]() {
      Serial.println ( F ( "\nEnd" ) );
    });

    ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
      Serial.printf ( "Progress: %u%%\r", ( progress / ( total / 100 ) ) );
    });

    ArduinoOTA.onError([](ota_error_t error) {
      Serial.printf("Error[%u]: ", error);
      if (error == OTA_AUTH_ERROR) Serial.println ( F ( "Auth Failed" ) );
      else if (error == OTA_BEGIN_ERROR) Serial.println ( F ( "Begin Failed" ) );
      else if (error == OTA_CONNECT_ERROR) Serial.println ( F ( "Connect Failed" ) );
      else if (error == OTA_RECEIVE_ERROR) Serial.println ( F ( "Receive Failed" ) );
      else if (error == OTA_END_ERROR) Serial.println ( F ( "End Failed" ) );
    });

    ArduinoOTA.begin();
    Serial.println ( F ( "ArduinoOTA running" ) );

    yield();

  #endif

  /**************************** HTML Server Setup *****************************/

  initializeWebServer ();

  // /**************************** mDNS Server Setup *****************************/
  // 
  // snprintf ( mdnsOtaId, mdnsOtaIdLen, "%s_%s", PROGNAME, uniqueToken );
  // if (!MDNS.begin ( mdnsOtaId ) ) {             // Start the mDNS responder for <PROGNAME>.local
  //   Serial.println ( F ( "Error setting up MDNS responder!" ) );
  // }
  // Serial.printf ( "mDNS responder '%s.local' started", mdnsOtaId );

  /************************* Power-on Color Setting ***************************/

  initializeLampColor ();

  /************************** Report successful init **************************/

  Serial.printf ( "\n%s v%s %s cbm", PROGNAME, VERSION, VERDATE );
  #ifdef TESTING
    Serial.printf ( " TESTING level %d\n", TESTING );
  #else
    Serial.println ();
  #endif

}

void loop ( void ) {

  const unsigned long wsSendInterval_ms       =   5UL * SECOND_ms;
  static unsigned long lastWSSendAt_ms        =      0UL;

  /****************************************************************************/

  if ( ! connect_WiFi () ) return;
  yield();

  #ifdef ALLOW_OTA
    ArduinoOTA.handle();
    yield ();
  #endif

  /****************************************************************************/

  // Ensure the connection to the MQTT server is alive (this will make the first
  // connection and automatically reconnect when disconnected).  See the MQTT_connect
  // function definition below.

  if ( ! connect_MQTT () ) return;

  conn_MQTT.loop();
  delay ( 10 );  // fix some issues with WiFi stability
  yield();

  /****************************************************************************/

  htmlServer.handleClient();
  webSocket.loop();
  yield ();

  /****************************************************************************/

  if ( ( forceWSUpdate ) 
      || ( lastWSSendAt_ms == 0UL )                                       // initial update
      || ( ( millis() - lastWSSendAt_ms ) > wsSendInterval_ms )           // regular update
      || ( timerStartedAt_ms && ( ( millis() - lastWSSendAt_ms ) > 1000UL ) )  // update faster while counting down
     ) {

    if ( ( VERBOSE >= 15 ) && forceWSUpdate ) Serial.println ( F ( "forced WS update" ) );

    // dtostrf ( temperature_degC, 0, 1, fBuf );
    // snprintf ( pBuf, pBufLen, "%s/%s", "raw/temperature_degC/air", sensorId );
    // sendValueToMQTT ( pBuf, fBuf, "Temperature" );

    if ( timerStartedAt_ms ) {
      // update if clock is ticking
      sendValueToMQTT ( mqttTimeoutTopic, timeRemaining_ms(), "time remaining" );
    }

    update_WebSocket ();
    lastWSSendAt_ms = millis();

  }

  yield ();

  // if settings have changed, send them to MQTT?
  // if changed by web page, yes
  // if changed by power-on, yes
  // if soft reset, it screws up the settings?
  sendSettingsToMQTT ();  

  /****************************************************************************/  

  if ( rebootInterval_ms && ( millis() > rebootInterval_ms ) ) {
    snprintf ( pBuf, pBufLen, "%s/%s/time/restart", PROGNAME, uniqueToken );
    sendValueToMQTT ( pBuf, bootTimeString, "Restart Time" );
    Serial.printf ( "Rebooting at %s\n", bootTimeString );
    delay ( 1000 );

    ESP.restart();  // soft reset;  ESP.reset() is a hard reset leaving regs unknown...

  }

  if ( outputState ) {
    // on
    if ( timerStartedAt_ms != 0UL ) {
      // timer is running
      if ( timeRemaining_ms() == 0UL ) {
        // timed out
        Serial.printf ( "Timeout!\n" );
        setOutputState ( 0 );
        timerStartedAt_ms = 0UL;
      }
    }
  }

  /****************************************************************************/

  yield ();

}

// *****************************************************************************
// ***************************** Initializations *******************************
// *****************************************************************************

void POST () {

  Serial.print ( F ( "\n\nPOST\n\n" ) );

  int oldVal = setOutputState ( 1 );

  sendOutput ( 0x00, 0x00, 0x00,  0x00, 0x00 );
  delay ( 100 );

  sendOutput ( 0x80, 0x00, 0x00,  0x00, 0x00 );
  delay ( 100 );
  sendOutput ( 0xff, 0x00, 0x00,  0x00, 0x00 );
  delay ( 250 );
  sendOutput ( 0x00, 0x00, 0x00,  0x00, 0x00 );
  delay ( 10 ); yield();

  sendOutput ( 0x00, 0x80, 0x00,  0x00, 0x00 );
  delay ( 100 );
  sendOutput ( 0x00, 0xff, 0x00,  0x00, 0x00 );
  delay ( 250 );
  sendOutput ( 0x00, 0x00, 0x00,  0x00, 0x00 );
  delay ( 10 ); yield();

  sendOutput ( 0x00, 0x00, 0x80,  0x00, 0x00 );
  delay ( 100 );
  sendOutput ( 0x00, 0x00, 0xff,  0x00, 0x00 );
  delay ( 250 );
  sendOutput ( 0x00, 0x00, 0x00,  0x00, 0x00 );
  delay ( 10 ); yield();

  sendOutput ( 0x00, 0x00, 0x00,  0x80, 0x00 );
  delay ( 100 );
  sendOutput ( 0x00, 0x00, 0x00,  0xff, 0x00 );
  delay ( 250 );
  sendOutput ( 0x00, 0x00, 0x00,  0x00, 0x00 );
  delay ( 10 ); yield();

  sendOutput ( 0x00, 0x00, 0x00,  0x00, 0x80 );
  delay ( 100 );
  sendOutput ( 0x00, 0x00, 0x00,  0x00, 0xff );
  delay ( 250 );
  sendOutput ( 0x00, 0x00, 0x00,  0x00, 0x00 );

  for ( int i = 0; i < 2 * 5; i++ ) {
    for ( int j = 0; j < 256; j += 10 ) {
      int b = ( i % 2 ) ? 255 - j : j;
      sendOutput ( i * 20, 0x00, 0x00,  j, j );
      delay ( 20 );
    }
  }

  sendOutput ( 0x00, 0x00, 0x00,  0x80, 0x80 );
  for ( int i = 0; i < 5; i++ ) {
    setOutputState ( 1 );
    delay ( 100 );
    setOutputState ( 0 );
    delay ( 100 );
  }

  // restore bulb settings
  setOutput ( currentR, currentG, currentB, currentW, currentC );
  setOutputState ( oldVal );

  yield();

}

void initializeGPIO () {

/*
  pinMode ( pd_LED_green, OUTPUT ); digitalWrite ( pd_LED_green, INVERSE_LOGIC_OFF );
  pinMode ( pd_LED_red,   OUTPUT ); digitalWrite ( pd_LED_red,   INVERSE_LOGIC_OFF );
  pinMode ( pdOutput,     OUTPUT ); digitalWrite ( pdOutput,     LOW  );
  pinMode ( pdC5,         OUTPUT ); digitalWrite ( pdC5,         LOW  );
  pinMode ( pdButton,      INPUT );  // has built-in pullup

*/

  Serial.begin ( BAUDRATE );
  while ( ! Serial && ( millis() < 2000 ) ) {
    // int newStatus = 1 - digitalRead ( pd_LED_red );
    // digitalWrite ( pd_LED_green, newStatus );
    // digitalWrite ( pd_LED_red,   newStatus );
    // #ifdef TESTING
    //   digitalWrite ( pdOutput, newStatus );
    // #endif
    delay ( 200 );  // wait a little more, if necessary, for serial to come up
    yield ();
  }
  if ( VERBOSE >= 10 ) while ( ! Serial && ( millis() < 20000 ) );
  Serial.print ( F ( "\n\n" ) );
  // digitalWrite ( pd_LED_green, INVERSE_LOGIC_OFF );
  // digitalWrite ( pd_LED_red,   INVERSE_LOGIC_OFF );

}

void initializeBulb () {

//  _my92xx = new my92xx ( MY92XX_MODEL, MY92XX_CHIPS, 
//                         MY92XX_DI_PIN, MY92XX_DCKI_PIN, 
//                         MY92XX_COMMAND_DEFAULT );

/*
  freq divide 4 breaks
  one shot enable no diff
  bit width 16 breaks
  cmd reaction slow breaks
*/

  _my92xx = new my92xx ( MY92XX_MODEL, MY92XX_CHIPS, 
                         MY92XX_DI_PIN, MY92XX_DCKI_PIN, 
                         { \
                            .scatter = MY92XX_CMD_SCATTER_APDM, \
                            .frequency = MY92XX_CMD_FREQUENCY_DIVIDE_1, \
                            .bit_width = MY92XX_CMD_BIT_WIDTH_8, \
                            .reaction = MY92XX_CMD_REACTION_FAST, \
                            .one_shot = MY92XX_CMD_ONE_SHOT_DISABLE, \
                            .resv = 0 \
                         } 
                       );

  _my92xx->setState(true);

  currentR = 0;
  currentG = 0;
  currentB = 0;
  currentW = 0;
  currentC = 0;

}

void initializeLittleFS () {

  if ( ! LittleFS.begin() ) {
    Serial.println ( F ( "INFO: LittleFS.begin() failed" ) );
    Serial.print ( F ("  formatting...") );
    if ( LittleFS.format() ) {
      Serial.println ( F (" ok") );
    } else {
      Serial.println ( F (" FAILED") );
    }
  }

}

void initializeLampColor () {

//   enum rst_reason {
//     REASON_DEFAULT_RST      = 0, // normal startup by power on
//     REASON_WDT_RST          = 1, // hardware watch dog reset
//     REASON_EXCEPTION_RST    = 2, // exception reset, GPIO status won’t change
//     REASON_SOFT_WDT_RST     = 3, // software watch dog reset, GPIO status won’t change
//     REASON_SOFT_RESTART     = 4, // software restart ,system_restart , GPIO status won’t change
//     REASON_DEEP_SLEEP_AWAKE = 5, // wake up from deep-sleep
//     REASON_EXT_SYS_RST      = 6  // external system reset
//   };
// 
//   char * reasons [    ]      = { "REASON_DEFAULT",
//                                  "REASON_WDT",
//                                  "REASON_EXCEPTION",
//                                  "REASON_SOFT_WDT",
//                                  "REASON_SOFT_RESTART",
//                                  "REASON_DEEP_SLEEP_AWAKE",
//                                  "REASON_EXT_SYS_RST "
//                                };
// 
// 
//   // byte reset_reason_0 = rtc_get_reset_reason ( 0 );
//   // Serial.print ( F ( "Reset reason 0: " ) ); Serial.println ( reset_reason_0, HEX );
//   // byte reset_reason_1 = rtc_get_reset_reason ( 1 );
//   // Serial.print ( F ( "Reset reason 1: " ) ); Serial.println ( reset_reason_1, HEX );
//   rst_info *resetInfo;
//   resetInfo = ESP.getResetInfoPtr();
//   byte reset_reason = resetInfo->reason;
// 
//   Serial.print ( F ( "Reset reason: " ) ); Serial.println ( reasons [ reset_reason ] );
//   sendValueToMQTT ( mqttResetReasonTopic, reasons [ reset_reason ], "Reset reason", true );

  // calculate time since previous power-on

  // read previous power-on time 
  //   in UNIX time, seconds since 12:00 AM Jan 1, 1970 as time_t
  // note: time_t is defined as unsigned long

  while ( ! ntpTimeGoodP && ( millis() < 30000UL ) ) {
    Serial.println ( F ( "Trying to get a goot time from NTP" ) );
    getUnixTime ();
    delay ( 5000 );
  }

  time_t interval_secs = 1000000UL;

  if ( ntpTimeGoodP ) {

    File f;

    f = LittleFS.open ( "/start_UNIX_time.txt", "r" );
    if ( VERBOSE >= 4 ) Serial.printf ( "INFO: LittleFS file%s opened\n", f ? "" : " not" );

    unsigned long lastStartAt_unixtime;
    if  ( f ) {
      // file will contain one line, with either "white" or "red"
      // to identify the color it will next start with
      lastStartAt_unixtime = f.parseInt ();
      f.close();

      if ( VERBOSE >= 5 ) {
        Serial.print ( F ( "  ... file contained: " ) );
        Serial.println ( lastStartAt_unixtime );
      }
    } else {
      Serial.println ( F ( "WARNING: start_UNIX_time.txt file not available" ) );
    }

    // now rewrite that file with the current time

    f = LittleFS.open ( "/start_UNIX_time.txt", "w" );
    f.print ( now() );
    f.close();

    interval_secs = now() - lastStartAt_unixtime;
    if ( VERBOSE >= 3 ) {
      Serial.print ( F ( "Seconds since last startup: " ) );
      Serial.println ( interval_secs );
    }

  } else {
    // don't have a good time from NTP
  }

  // if ( true || reset_reason == ESP_RST_POWERON ) {
  // if (    ( reset_reason == REASON_DEFAULT_RST ) 
  //      || ( reset_reason == REASON_EXT_SYS_RST ) ) {

  if ( interval_secs < 60UL ) {
    lampColorInitialize_powercycle ();
  } else {
    // reboot interval restart -- probably leave settings to what's in MQTT
    // but MQTT doesn't provide a "color" command - colors individually stored, under "status"
    // so no "command"
    lampColorInitialize_restorePrevious ();
  }
  sendSettingsToMQTT ( 1 );
}

void lampColorInitialize_powercycle () {

  File f;

  // power on, but not soft reset
  f = LittleFS.open ( "/power_on_color.txt", "r" );
  Serial.printf ( "INFO: LittleFS file%s opened\n", f ? "" : " not" );

  #define colorStringLen 10
  char colorString [ colorStringLen ];
  if  ( f && f.available() ) {
    // file will contain one line, with either "white" or "red"
    // to identify the color it will next start with
    f.readString().toCharArray( colorString, colorStringLen );
    f.close();

    if ( VERBOSE >= 5 ) {
      Serial.print ( F ( "  ... file contained: " ) );
      Serial.println ( colorString );
    }
  } else {
    Serial.println ( F ( "WARNING: power_on_color.txt file not available" ) );
  }

  // regardless of read status, try to write a file

  f = LittleFS.open ( "/power_on_color.txt", "w" );
  if ( ! strncmp ( colorString, "red", 6 ) ) {
    if ( VERBOSE >= 5 ) Serial.println ( F ( "was red; make white" ) );
    f.print ( F ( "white" ) );
    setOutput ( WHITE_color );  // white
  } else {
    if ( VERBOSE >= 5 ) Serial.println ( F ( "was white; make red" ) );
    f.print ( F ( "red" ) );
    setOutput ( RED_color );  // red
  }
  f.close();
  setOutputState ( 1 );
}

void lampColorInitialize_restorePrevious () {

  File f;

  // soft reset
  f = LittleFS.open ( "/soft_reset_color.txt", "r" );
  Serial.printf ( "INFO: LittleFS file%s opened\n", f ? "" : " not" );

  int stateVector [ 6 ] = { -1, -1, -1,  -1, -1,  -1 } ;
  if  ( f && f.available() ) {
    /*
      file will contain a comma-separated list of 6 items:
        5 color values
        1 0-or-1 "on" value
    */ 

    Serial.print ( F ( "  ... file contained: " ) );
    for ( int i = 0; i < 6; i++ ) {
      stateVector [ i ] = f.parseInt();
      Serial.print ( stateVector [ i ] );
      Serial.print ( ( i < 5 ) ? ", " : "\n" );
    }
    f.close();

    // set stateVector if we got a valid line
    if ( stateVector [ 5 ] >= 0 ) {
      // these values should never all be zero
      setOutput ( stateVector [ 0 ], 
                  stateVector [ 1 ], 
                  stateVector [ 2 ], 
                  stateVector [ 3 ], 
                  stateVector [ 4 ] );
      // this value will be zero of lamp was off
      setOutputState ( stateVector [ 5 ], 1 );
    } else {
      Serial.println ( F ( "Incomplete data from soft-restart file" ) );
    }

  } else {
    Serial.println ( F ( "WARNING: soft_reset_color.txt file not available" ) );
  }

}

void initializeWebServer () {

  /**************************** HTML Server Setup *****************************/

  // for html, do the on's before server begin

  htmlServer.on ( "/", htmlResponse_root );
  htmlServer.onNotFound([](){
    htmlServer.send(404, "text/plain", "404: Not found");
  });
  htmlServer.begin();  

  webSocket.begin();
  webSocket.onEvent ( webSocketEvent );
  // the below are applicable to a WebSocket CLIENT...
  // webSocket.setReconnectInterval ( 15000 );  // ms to reconnect after failure
  // ms; <ping interval> <response timeout> <disconnect if n failures>
  // webSocket.enableHeartbeat ( 30000, 3000, 2 );

}

// *****************************************************************************
// ********************************** WiFi *************************************
// *****************************************************************************

bool connect_WiFi () {

  if ( WiFi.status() == WL_CONNECTED ) return true;

  const unsigned long oldConnectionTimeout_ms = 10UL * SECOND_ms;

  unsigned long startTime_ms   = millis();
  unsigned long lastDotAt_ms   = millis();
  unsigned long lastBlinkAt_ms = millis();
  static bool newConnection = true;
  const int bufLen = 25;
  char strBuf [ bufLen ];
  bool printed = false;

  sendOutput ( 0x00, 0x00, 0x40,  0x00, 0x00 );  // blue while connecting

  while ( WiFi.status() != WL_CONNECTED ) {

    if ( VERBOSE > 4 && ! printed ) { 
      Serial.print ( F ( "\nChecking wifi..." ) );
      printed = true;
      newConnection = true;
    }

    if ( ( millis() - lastDotAt_ms ) > 1000 ) {
      Serial.print ( F ( "." ) );
      lastDotAt_ms = millis();
    }

    if ( ( millis() - startTime_ms ) > oldConnectionTimeout_ms ) {

      Serial.printf ( "About to smart config...\n" );

      WiFi.beginSmartConfig();

      while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print ( WiFi.smartConfigDone() );
      }

      Serial.printf ( "Done smart config.\n" );

    }

    delay ( 1000 );
  }

  if ( newConnection && VERBOSE > 4 ) {
    Serial.print ( F ( "\n  WiFi connected as " ) ); 
    Serial.print ( WiFi.localIP() );
    Serial.print ( F ( " in " ) );
    Serial.print ( millis() - startTime_ms );
    Serial.println ( F ( " ms" ) );    
  }

  startTime_ms = millis();
  printed = false;

  newConnection = false;

  sendOutput  ( currentR, currentG, currentB,  currentW, currentC );  // restore

  return true;
}

bool connect_MQTT () { 

  if ( conn_MQTT.connected() ) return true;

  unsigned long startTime_ms   = millis();
  unsigned long lastDotAt_ms   = millis();
  unsigned long lastBlinkAt_ms = millis();
  static bool newConnection = true;
  const int bufLen = 25;
  char strBuf [ bufLen ];
  bool printed = false;

  sendOutput ( 0x40, 0x00, 0x40,  0x00, 0x00 );  // violet while connecting

  while ( ! conn_MQTT.connected () ) {
    conn_MQTT.connect ( mqtt_clientID );
    if ( VERBOSE > 4 && ! printed ) { 
      Serial.print ( F ( "  Connecting to MQTT as '" ) ); Serial.print ( mqtt_clientID );
      Serial.print ( F ( "'; status: " ) );
      Serial.print ( conn_MQTT.state () );
      Serial.print ( F ( "..." ) );
      /*
        -4 : MQTT_CONNECTION_TIMEOUT - the server didn't respond within the keepalive time
        -3 : MQTT_CONNECTION_LOST - the network connection was broken
        -2 : MQTT_CONNECT_FAILED - the network connection failed - perhaps the IP is bad!!!
        -1 : MQTT_DISCONNECTED - the client is disconnected cleanly
         0 : MQTT_CONNECTED - the cient is connected
         1 : MQTT_CONNECT_BAD_PROTOCOL - the server doesn't support the requested version of MQTT
         2 : MQTT_CONNECT_BAD_CLIENT_ID - the server rejected the client identifier
         3 : MQTT_CONNECT_UNAVAILABLE - the server was unable to accept the connection
         4 : MQTT_CONNECT_BAD_CREDENTIALS - the username/password were rejected
         5 : MQTT_CONNECT_UNAUTHORIZED - the client was not authorized to connect_WiFi
      */
      printed = true;
      newConnection = true;
    }
    if ( ( millis() - lastDotAt_ms ) > 1000 ) {
      Serial.print ( F ( "." ) );
      lastDotAt_ms = millis();
    }
    delay ( 50 );
  }

  if ( newConnection ) {
    if ( VERBOSE > 4 ) {
      Serial.print ( F ( "\n  MQTT connected in " ) );
      Serial.print ( millis() - startTime_ms );
      Serial.println ( F ( " ms" ) );
    }
    conn_MQTT.subscribe ( mqttCmdTopic, 1 );  // topic[, QoS]
    // it looks like trying to subscribe with QoS 2 silently fails!
    if ( VERBOSE > 4 ) {
      Serial.print ( F ( "  Subscribed to MQTT feed '" ) );
      Serial.print ( mqttCmdTopic );
      Serial.println ( F ( "'" ) );
    }
  }
  // client.unsubscribe("/example");
  newConnection = false;

  sendOutput  ( currentR, currentG, currentB,  currentW, currentC );  // restore

  return true;

}

// *****************************************************************************
// ********************************** MQTT *************************************
// *****************************************************************************

void sendSettingsToMQTT ( bool force ) {

  const unsigned long sendSettingsDelay_ms = 2UL * SECOND_ms;
  static byte oldR = 0x00;
  static byte oldG = 0x00;
  static byte oldB = 0x00;
  static byte oldW = 0x00;
  static byte oldC = 0x00;
  static int oldState = -99;

  if (    force
       || (    settingsDirtyAt_ms 
            && ( millis() - settingsDirtyAt_ms ) > sendSettingsDelay_ms 
          )
     ) {

    if ( currentR != oldR ) {
      snprintf ( pBuf, pBufLen, "%s/%s", mqttStatusTopic, "red" );
      sendValueToMQTT ( pBuf, currentR, "red", true );
      oldR = currentR;
    }
    if ( currentG != oldG ) {
      snprintf ( pBuf, pBufLen, "%s/%s", mqttStatusTopic, "green" );
      sendValueToMQTT ( pBuf, currentG, "green", true );
      oldG = currentG;
    }
    if ( currentB != oldB ) {
      snprintf ( pBuf, pBufLen, "%s/%s", mqttStatusTopic, "blue" );
      sendValueToMQTT ( pBuf, currentB, "blue", true );
      oldB = currentB;
    }
    if ( currentW != oldW ) {
      snprintf ( pBuf, pBufLen, "%s/%s", mqttStatusTopic, "warm" );
      sendValueToMQTT ( pBuf, currentW, "warm", true );
      oldW = currentW;
    }
    if ( currentC != oldC ) {
      snprintf ( pBuf, pBufLen, "%s/%s", mqttStatusTopic, "cool" );
      sendValueToMQTT ( pBuf, currentC, "cool", true );
      oldC = currentC;
    }

    if ( outputState != oldState ) {
      snprintf ( pBuf, pBufLen, "%s/%s", mqttStatusTopic, "state" );
      sendValueToMQTT ( pBuf, outputState, "Output state", true );
      oldState = outputState;
    }

    saveSettingsToFile ();    

    settingsDirtyAt_ms = 0UL;
  }
}

int sendValueToMQTT ( const char * topic, int value, const char * name, bool retainP ) {
  const int valLen = 10;
  char val [ valLen ];
  snprintf ( val, valLen, "%d", value );
  return sendValueToMQTT ( topic,  val,  name, retainP );
}

int sendValueToMQTT ( const char * topic, const char * value, const char * name, bool retainP ) {

  /*

    Send data via MQTT to Mosquitto

  */

  int tLen = strlen ( topic );
  int vLen = strlen ( value );
  if ( vLen < 1 ) {
    if ( VERBOSE >= 10 ) {
      Serial.print ( F ( "\nNot sending null payload " ) );
      Serial.println ( name );
    }
    return 0;
  }

  if ( ( tLen + vLen ) > MQTT_MAX_PACKET_SIZE ) {
    if ( VERBOSE >= 10 ) {
      Serial.print ( F ( "\nNot sending oversized ( " ) );
      Serial.print ( tLen + vLen );
      Serial.print ( F ( " > 128 ) packet " ) );
      Serial.println ( name );
    }
    return 0;
  }

  if ( VERBOSE >= 10 ) {
    Serial.print ( F ( "Sending " ) );
    Serial.print ( topic );
    Serial.print ( F ( " ( name = " ) );
    Serial.print ( name );
    Serial.print ( F ( ", len = " ) );
    Serial.print ( tLen + vLen );
    Serial.print ( F ( " ): " ) );
    Serial.print ( value );
    Serial.print ( F ( " ... " ) );
  }

  /*

    Note: each publish call seems to take about a second
    Even though connection remains good
    But only when the MQTT server is overtaxed - probably in need of restart!

  */

  bool success = conn_MQTT.publish ( topic, value, retainP );
  if ( VERBOSE >= 10 ) Serial.println ( success ? "OK!" : "Failed" );

  return success ? ( tLen + vLen ) : -1;
};

void handleReceivedMQTTMessage ( char * topic, byte * payload, unsigned int length ) {

  const size_t pBufLen = 512;
  char pBuf [ pBufLen ];
  memcpy ( pBuf, payload, length );
  pBuf [ length ] = '\0';

  if ( VERBOSE >= 8 ) {
    Serial.print ( F ( "Got: " ) );
    Serial.print ( topic );
    Serial.print ( F ( ": " ) );
    Serial.print ( pBuf );
    Serial.println ();
  }

  interpretNewCommandString ( topic, pBuf );
}

void interpretNewCommandString ( char * theTopic, char * thePayload ) {

  DynamicJsonDocument doc ( 400 );

  if ( VERBOSE >= 4 ) {
    Serial.print ( F ( "thePayload: '" ) ); 
    Serial.print ( thePayload ); 
    Serial.println ( F ( "'" ) );
  }

  DeserializationError error = deserializeJson ( doc, thePayload );

  // Test if parsing succeeds.
  if ( error ) {
    Serial.print(F("deserializeJson() failed: "));
    Serial.println(error.f_str());
    return;
  }

  // Most of the time, you can rely on the implicit casts.
  // In other case, you can do doc["time"].as<long>();

  if ( doc.containsKey ( "color" ) ) {
    byte R, G, B,  W, C;
    R = doc["color"][0]; // || 0x00;
    G = doc["color"][1]; // || 0x00;
    B = doc["color"][2]; // || 0x00;
    W = doc["color"][3]; // || 0x00;
    C = doc["color"][4]; // || 0x00;

    setOutput ( R, G, B,  W, C );
    setOutputState ( 1 );
  }

  if ( doc.containsKey ( "state" ) ) {
    setOutputState ( doc["state"].as<int>() );
  }

  if ( doc.containsKey ( "timer" ) ) {
    setTimer ();
  }

}

// *****************************************************************************
// ********************************* output *************************************
// *****************************************************************************

void toggleOutput () {
  setOutputState ( ! outputState );
}

int setOutputState ( int newSetting, bool force ) {

  // turn on/off without losing settings...
  // force resets the output state and resends the retained brightness values

  if ( VERBOSE >= 4 ) {
    Serial.printf ( "State: %s\n", newSetting ? "ON" : "OFF" );
  }

  int oldVal = outputState;

  if ( ( newSetting != outputState ) || force ) {

    // if not force, then newSetting must be unequal to outputState
    // if force, can't assume anything about the equality
    if ( newSetting >= 0 ) outputState = newSetting;
    if ( outputState ) {
      // turn on; could be timer or just "on"
      if ( ! currentR && ! currentG && ! currentB && ! currentW && ! currentC ) {
        // everything's set to zero brightness
        if ( VERBOSE >= 12 ) Serial.println ( F ( "Setting default nonzero values" ) );
        currentW = 0x80;
        currentC = 0x80;
      }
      sendOutput  ( currentR, currentG, currentB,  currentW, currentC );
    } else {
      // turn off, leaving settings alone
      if ( VERBOSE >= 12 ) Serial.println ( F ( "Turning off, leaving settings" ) );
      sendOutput  ( 0x00, 0x00, 0x00,  0x00, 0x00 );
    }

    if ( VERBOSE > 10 ) Serial.printf ( "  -> output is %s\n", outputStateString [ outputState ] );
    settingsDirtyAt_ms = millis();
    timerStartedAt_ms = 0UL;  // cancel timer on any interaction
  }

  forceWSUpdate = true;
  return ( oldVal );

}

void setOutput ( byte R, byte G, byte B, byte W, byte C ) {

  // turning off doesn't call this routine, so current values should never be 0

  currentR = R;
  currentG = G;
  currentB = B;
  currentW = W;
  currentC = C;

  sendOutput  ( currentR, currentG, currentB,  currentW, currentC );
  settingsDirtyAt_ms = millis();

  forceWSUpdate = true;

}

void sendOutput ( byte R, byte G, byte B, byte W, byte C ) {

  // lowest level send - just send these values, without side effects
  if ( VERBOSE >= 4 ) {
    Serial.printf ( "Color settings: 0x%02x, 0x%02x, 0x%02x,  0x%02x, 0x%02x\n",
      R, G, B,  W, C );
  }

  _my92xx->setChannel ( MY92XX_RED,   (unsigned int) R );
  _my92xx->setChannel ( MY92XX_GREEN, (unsigned int) G );
  _my92xx->setChannel ( MY92XX_BLUE , (unsigned int) B );
  _my92xx->setChannel ( MY92XX_WARM,  (unsigned int) W );
  _my92xx->setChannel ( MY92XX_COLD,  (unsigned int) C );
  _my92xx->update();

}

void setTimer ( unsigned long p_timeoutValue_ms ) {

  if ( VERBOSE >= 4 ) {
    Serial.printf ( "Timer enabled\n" );
  }

  setOutputState ( 2 );
  timerStartedAt_ms = millis();
  timeoutValue_ms = p_timeoutValue_ms;
  // Serial.printf ( "setTimer: setting %ul ms; turnoff at: %ul ms\n", timeoutValue_ms, turnOffAt_ms );
  sendValueToMQTT ( mqttTimeoutTopic, timeoutValue_ms, "Timeout value" );
}

unsigned long timeRemaining_ms () {
  if ( timerStartedAt_ms == 0 ) return 0UL;
  bool timedOut = ( millis() - timerStartedAt_ms ) > timeoutValue_ms;
  return ( timedOut ? 0UL : ( timerStartedAt_ms + timeoutValue_ms ) - millis() );
}

// *****************************************************************************
// ********************************** HTML *************************************
// *****************************************************************************

// { "color": [ 0, 0, 0,  128, 255 ] }
// { "timer": 1 }
// { "state": 1 }

void htmlResponse_root () {

  sendOutput ( 0x40, 0x40, 0x00,  0x00, 0x00 );

  const char * htmlMessage = R"HTML(

    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="UTF-8">
        <link referrerpolicy="no-referrer" rel="icon" href="http://miniTunes.cbmHome/favicon.ico">
        <style> 
          p{} .data { font-weight: bold; }
          .right { text-align: right; }

          .myButton {
            -moz-box-shadow: 3px 4px 0px 0px #899599;
            -webkit-box-shadow: 3px 4px 0px 0px #899599;
            box-shadow: 3px 4px 0px 0px #899599;
            background:-webkit-gradient(linear, left top, left bottom, color-stop(0.05, #ededed), color-stop(1, #bab1ba));
            background:-moz-linear-gradient(top, #ededed 5%, #bab1ba 100%);
            background:-webkit-linear-gradient(top, #ededed 5%, #bab1ba 100%);
            background:-o-linear-gradient(top, #ededed 5%, #bab1ba 100%);
            background:-ms-linear-gradient(top, #ededed 5%, #bab1ba 100%);
            background:linear-gradient(to bottom, #ededed 5%, #bab1ba 100%);
            filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ededed', endColorstr='#bab1ba',GradientType=0);
            background-color:#ededed;
            -moz-border-radius:15px;
            -webkit-border-radius:15px;
            border-radius:15px;
            border:1px solid #d6bcd6;
            display:inline-block;
            cursor:pointer;
            color:#3a8a9e;
            font-family:Arial;
            font-size:17px;
            padding:7px 25px;
            text-decoration:none;
            text-shadow:0px 1px 0px #e1e2ed;
          }
          .myButton .willBe { display:none; }
          .myButton:hover {
            background:-webkit-gradient(linear, left top, left bottom, color-stop(0.05, #bab1ba), color-stop(1, #ededed));
            background:-moz-linear-gradient(top, #bab1ba 5%, #ededed 100%);
            background:-webkit-linear-gradient(top, #bab1ba 5%, #ededed 100%);
            background:-o-linear-gradient(top, #bab1ba 5%, #ededed 100%);
            background:-ms-linear-gradient(top, #bab1ba 5%, #ededed 100%);
            background:linear-gradient(to bottom, #bab1ba 5%, #ededed 100%);
            filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#bab1ba', endColorstr='#ededed',GradientType=0);
            background-color:#bab1ba;
          }
          .myButton:hover .is { display:none; }
          .myButton:hover .willBe { display:inline-block; }
          .myButton:active {
            position:relative;
            top:1px;
          }

          .timerSliderContainer {
            position: relative;
            top: 20px;
            width: 50%;
          }

          .timerSlider,.rSlider,.gSlider,.bSlider,.wSlider,.cSlider {
            -webkit-appearance: none;
            width: 300px;
            height: 25px;
            background: #d3d3d3;
            outline: none;
            opacity: 0.7;
            -webkit-transition: .2s;
            transition: opacity .2s;
          }

          .timerSlider {
            width: 500px;
          }

          .timerSlider:hover,
          .rSlider:hover,
          .gSlider:hover,
          .bSlider:hover,
          .wSlider:hover,
          .cSlider:hover {
            opacity: 1;
          }

          .timerSlider::-moz-range-thumb,
          .rSlider::-moz-range-thumb,
          .gSlider::-moz-range-thumb,
          .bSlider::-moz-range-thumb,
          .wSlider::-moz-range-thumb,
          .cSlider::-moz-range-thumb {
            width: 25px;
            height: 25px;
            cursor: pointer;
          }

          .timerSlider::-webkit-slider-thumb,
          .rSlider::-webkit-slider-thumb,
          .gSlider::-webkit-slider-thumb,
          .bSlider::-webkit-slider-thumb,
          .wSlider::-webkit-slider-thumb,
          .cSlider::-webkit-slider-thumb {
            -webkit-appearance: none;
            appearance: none;
            width: 25px;
            height: 25px;
            cursor: pointer;
          }

          .timerSlider::-moz-range-thumb,
          .timerSlider::-webkit-slider-thumb {
            background: #4CAF50;
          }

          .rSlider::-moz-range-thumb,
          .rSlider::-webkit-slider-thumb {
            background: #FF0000;
          }

          .gSlider::-moz-range-thumb,
          .gSlider::-webkit-slider-thumb {
            background: #00FF00;
          }

          .bSlider::-moz-range-thumb,
          .bSlider::-webkit-slider-thumb {
            background: #0000FF;
          }

          .wSlider::-moz-range-thumb,
          .wSlider::-webkit-slider-thumb {
            background: #ffc0c0;
          }

          .cSlider::-moz-range-thumb,
          .cSlider::-webkit-slider-thumb {
            background: #c0c0ff;
          }

          .timerSliderinfo {
            position: absolute;
            top: -45px;
            left: 150px;
            background: #80d880;
          }

        </style>

        <script type="text/javascript">

          var ws;
          var wsUri = "ws:";
          var loc = window.location;
          if (loc.protocol === "https:") { wsUri = "wss:"; }
          // wsUri += "//" + loc.host + loc.pathname.replace("simple","ws/simple");
          wsUri += "//" + loc.host + ":81/";
          ws = new WebSocket ( wsUri );

          window.addEventListener('load', onLoad);

          function wsConnect() {

            ws.onerror = function (error) {
              document.getElementById ( 'STATUS' ).innerHTML = "ERROR!!";
              document.getElementById ( 'STATUS' ).style = "background:pink; color:red";
              document.getElementById ("overlay").style.display = "block";
            };
            ws.onclose = function() {
              document.getElementById ( 'STATUS' ).innerHTML = "<b>not</b> connected";
              document.getElementById ( 'STATUS' ).style = "background:none; color:red";
              document.getElementById ("overlay").style.display = "block";
              // in case of lost connection tries to reconnect every 3 secs
              setTimeout ( wsConnect, 3000 );
            }
            ws.onopen = function () {
              document.getElementById ( 'STATUS' ).innerHTML = "connected";
              document.getElementById ( 'STATUS' ).style = "background:none; color:black";
              ws.send ( "Open for data" );
              document.getElementById ( 'OUTPUT_CURRENT' ).innerHTML = "..yet unknown..";
              document.getElementById ( 'OUTPUT_ONCHANGE' ).innerHTML = "..yet unknown tobe..";
            }
            ws.onmessage = function ( msg ) {
              var data = msg.data;

              document.getElementById ("overlay").style.display = "none";

              var jsonObj = JSON.parse(data);
              document.getElementById ( 'PROGNAME' ).innerHTML = jsonObj.PROGNAME;
              document.getElementById ( 'VERSION' ).innerHTML = jsonObj.VERSION;
              document.getElementById ( 'VERDATE' ).innerHTML = jsonObj.VERDATE;
              document.getElementById ( 'UNIQUE_TOKEN' ).innerHTML = jsonObj.UNIQUE_TOKEN;
              document.getElementById ( 'IP_STRING' ).innerHTML = jsonObj.IP_STRING;
              var temp = jsonObj.MDNS_ID;
              document.getElementById("MDNS_ID").innerHTML = 
                ( temp == "<failure>" ) ? "<em>mDns failed</em>" : temp.concat ( ".local" );
              document.getElementById ( 'TIME' ).innerHTML = jsonObj.TIME;
              document.getElementById ( 'MQTT_CMD_TOPIC' ).innerHTML = jsonObj.MQTT_CMD_TOPIC;
              document.getElementById ( 'MQTT_STATUS_TOPIC' ).innerHTML = jsonObj.MQTT_STATUS_TOPIC;
              document.getElementById ( 'MQTT_TIMEOUT_TOPIC' ).innerHTML = jsonObj.MQTT_TIMEOUT_TOPIC;

              document.getElementById ( 'R_VALUE' ).innerHTML = jsonObj.R_VALUE;
              document.getElementById ( 'G_VALUE' ).innerHTML = jsonObj.G_VALUE;
              document.getElementById ( 'B_VALUE' ).innerHTML = jsonObj.B_VALUE;
              document.getElementById ( 'W_VALUE' ).innerHTML = jsonObj.W_VALUE;
              document.getElementById ( 'C_VALUE' ).innerHTML = jsonObj.C_VALUE;
              document.getElementById ( 'R_SLIDER' ).value = jsonObj.R_VALUE;
              document.getElementById ( 'G_SLIDER' ).value = jsonObj.G_VALUE;
              document.getElementById ( 'B_SLIDER' ).value = jsonObj.B_VALUE;
              document.getElementById ( 'W_SLIDER' ).value = jsonObj.W_VALUE;
              document.getElementById ( 'C_SLIDER' ).value = jsonObj.C_VALUE;

              document.getElementById ( 'OUTPUT_CURRENT' ).innerHTML = jsonObj.OUTPUT_STATUS;
              document.getElementById ( 'OUTPUT_ONCHANGE' ).innerHTML = jsonObj.OUTPUT_CHANGE_STATUS;
              var secs = jsonObj.TIMER_SECONDS_REMAINING;
              document.getElementById ( "TIMER_SLIDER" ).value = secs;
              // var mins = Math.round ( secs / 6 ) / 10;
              // document.getElementById ( "TIMER_SLIDER_VALUE" ).innerHTML = "";  // jsonObj.TIMER_SECONDS_REMAINING ? ( mins + " minutes" ) : "";
              document.getElementById ( "TIMER_REMAINING" ).innerHTML = jsonObj.TIMER_REMAINING_STRING;

              document.getElementById ( 'LAST_BOOT_AT' ).innerHTML = jsonObj.LAST_BOOT_AT;

            }
          }

          function onLoad(event) {
            initButtons();
            initSliders();
          }
          function initButtons() {
            document.getElementById('MAIN_BUTTON').addEventListener('click', toggle);
          }
          function toggle(){
            ws.send('toggle');
          }

          function pageUnload () {
            document.getElementById ( 'STATUS' ).innerHTML = "page unloaded";
            document.getElementById ( 'STATUS' ).style = "background:none; color:red";
            document.getElementById ("overlay").style.display = "block";
          }

          function initSliders() {
            var timerSlider = document.getElementById("TIMER_SLIDER");
            // document.getElementById("TIMER_SLIDER_VALUE").innerHTML = timerSlider.value;

            timerSlider.oninput = function() {
              // document.getElementById("TIMER_SLIDER_VALUE").innerHTML = this.value;
              ws.send ( 'timer_value:' + this.value );
            }

            var rSlider = document.getElementById("R_SLIDER");
            rSlider.oninput = function() {
              ws.send ( 'red_value:' + this.value );
            }

            var gSlider = document.getElementById("G_SLIDER");
            gSlider.oninput = function() {
              ws.send ( 'green_value:' + this.value );
            }

            var bSlider = document.getElementById("B_SLIDER");
            bSlider.oninput = function() {
              ws.send ( 'blue_value:' + this.value );
            }

            var wSlider = document.getElementById("W_SLIDER");
            wSlider.oninput = function() {
              ws.send ( 'warm_value:' + this.value );
            }

            var cSlider = document.getElementById("C_SLIDER");
            cSlider.oninput = function() {
              ws.send ( 'cool_value:' + this.value );
            }

          }

        </script>

        <title>Sonoff LED Bulb</title>

      </head>

      <body onload="wsConnect()" onunload="pageUnload()">

        <div id="overlay" style="
            position: fixed; /* Sit on top of the page content */
            display: block; /* shown by default */
            width: 100%; /* Full width (cover the whole page) */
            height: 100%; /* Full height (cover the whole page) */
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background-color: rgba(0,0,0,0.25); /* background color, with opacity */
            z-index: 2; /* Specify a stack order in case you're using a different order for other elements */
            cursor: pointer; /* Add a pointer on hover */
        ">  
          <span style="
            position: absolute;
            top: 50%;
            left: 50%;
            font-size: 50px;
            background: none;
            color: red;
            transform: translate(-50%,-50%);
            -ms-transform: translate(-50%,-50%);
          ">
            <span id="STATUS">uninitialized</span>
          </span>
        </div> 

        <h2><span id="PROGNAME">..progname..
          </span>   v<span id="VERSION">..version..
          </span> <span id="VERDATE">..verdate..</span> cbm</h2>

        <h4>    Running on 
                  <span id="UNIQUE_TOKEN">..unique_token..</span> at 
                  <span id="IP_STRING">..ipstring..</span>
                  <span id="MDNS_ID">..mdns..</span>
        </h4>

        <h4>    MQTT command topic: 
                  <span id="MQTT_CMD_TOPIC">..mqtt_cmd_topic..</span> <br/>
                MQTT status topic:
                  <span id="MQTT_STATUS_TOPIC">..mqtt_status_topic..</span> <br/>
                MQTT timeout topic:
                  <span id="MQTT_TIMEOUT_TOPIC">..mqtt_timeout_topic..</span> <br/>
        </h4>

        <br>

        <h4>As of <time id="TIME">..time..:</time></h4>

        <table id="COLOR_TABLE" border="1" style="width: 500px">
          <thead>
            <tr>
              <th colspan="3">Color Values</th>
            </tr>
          </thead>
          <tbody>
            <tr>
             <td style="width: 150px">Red</td>
             <td class="right" id="R_VALUE" style="width: 50px">..R..</td>
             <td><input type="range" min="0" max="255" value="0" class="rSlider" id="R_SLIDER"></td>
            </tr>
            <tr>
            <tr>
             <td>Green</td>
             <td class="right" id="G_VALUE">..G..</td>
             <td><input type="range" min="0" max="255" value="0" class="gSlider" id="G_SLIDER"></td>
            </tr>
            <tr>
            <tr>
             <td>Blue</td>
             <td class="right" id="B_VALUE">..B..</td>
             <td><input type="range" min="0" max="255" value="0" class="bSlider" id="B_SLIDER"></td>
            </tr>
            <tr>
            <tr>
             <td>Warm White</td>
             <td class="right" id="W_VALUE">..W..</td>
             <td><input type="range" min="0" max="255" value="0" class="wSlider" id="W_SLIDER"></td>
            </tr>
            <tr>
            <tr>
             <td>Cool White</td>
             <td class="right" id="C_VALUE">..C..</td>
             <td><input type="range" min="0" max="255" value="0" class="cSlider" id="C_SLIDER"></td>
            </tr>
          </tbody>
        </table>

        <br><br>

        <button id="MAIN_BUTTON" class="myButton">
          <span id="OUTPUT_CURRENT" class="is">..currently..</span>
          <span id="OUTPUT_ONCHANGE" class="willBe">..willbe..</span>
        </button>

        <div class="timerSliderContainer">
          <input type="range" min="0" max="7200" value="0" class="timerSlider" id="TIMER_SLIDER">
          <span class="timerSliderinfo" id="TIMER_REMAINING"></span>
        </div>

        <br><br>Last boot at <span id="LAST_BOOT_AT">..last_boot..</span><br>

      </body>
    </html>
  )HTML";

  yield ();

  htmlServer.send ( 200, "text/html", htmlMessage );

  if ( VERBOSE >= 20 ) {
    Serial.print ( F ( "done\n" ) );
  }

  setOutputState ( -1, true );  // revert output, canceling indicator
  forceWSUpdate = true;

}

void webSocketEvent ( uint8_t num, WStype_t type, uint8_t * payload, size_t length ) {

  // Serial.printf ( "Event %d received\n", type );

    switch ( type ) {
        case WStype_ERROR:  // 0
            if ( VERBOSE >= 10 ) Serial.printf("[WSc] Error!\n");
            break;
        case WStype_DISCONNECTED:  // 1
            if ( VERBOSE >= 10 ) Serial.printf("[WSc] Disconnected!\n");
            break;
        case WStype_CONNECTED:  // 2
          if ( length > 1 ) {
              if ( VERBOSE >= 10 ) Serial.printf ( "[WSc] %s: Connected to url: %s\n", num, payload );
        // bool sendTXT ( uint8_t num, uint8_t * payload, size_t length = 0, bool headerToPayload = false );
        webSocket.sendTXT ( num, "Connected", 9 );
      } else {
        if ( VERBOSE >= 10 ) Serial.printf ( "[WSc] CONNECTED message length 1 (empty!)\n" );
      }
            break;
        case WStype_TEXT:  // 3
          if ( length > 1 ) {
              if ( VERBOSE >= 10 ) Serial.printf ( "[WSc] get text: %s\n", payload );
              if ( ! strcmp ( (const char *) payload, "toggle" ) ) {
                toggleOutput();
              }
              // if ( ! strcmp ( (const char *) payload, "initiate_timer" ) ) {
              //   setTimer();
              // }
              if ( strstr ( (const char *) payload, "timer_value:" ) ) {
                if ( VERBOSE >= 10 ) Serial.println ( (const char *) payload );
                // strstr returns NULL if string not found
          char * pch;
          pch = strtok ( (char *) payload, ":" );
          pch = strtok ( NULL, ":");  // skip the first token "timer_value"
          int seconds = atoi ( pch );
          setTimer ( seconds * 1000UL );
        } else {
          bool change = false;
          if ( strstr ( (const char *) payload, "red_value:" ) ) {
            if ( VERBOSE >= 10 ) Serial.println ( (const char *) payload );
            // strstr returns NULL if string not found
            char * pch;
            pch = strtok ( (char *) payload, ":" );
            pch = strtok ( NULL, ":");  // skip the first token
            currentR = atoi ( pch );
            change = true;
          }
          if ( strstr ( (const char *) payload, "green_value:" ) ) {
            if ( VERBOSE >= 10 ) Serial.println ( (const char *) payload );
            // strstr returns NULL if string not found
            char * pch;
            pch = strtok ( (char *) payload, ":" );
            pch = strtok ( NULL, ":");  // skip the first token
            currentG = atoi ( pch );
            change = true;
          }
          if ( strstr ( (const char *) payload, "blue_value:" ) ) {
            if ( VERBOSE >= 10 ) Serial.println ( (const char *) payload );
            // strstr returns NULL if string not found
            char * pch;
            pch = strtok ( (char *) payload, ":" );
            pch = strtok ( NULL, ":");  // skip the first token
            currentB = atoi ( pch );
            change = true;
          }
          if ( strstr ( (const char *) payload, "warm_value:" ) ) {
            if ( VERBOSE >= 10 ) Serial.println ( (const char *) payload );
            // strstr returns NULL if string not found
            char * pch;
            pch = strtok ( (char *) payload, ":" );
            pch = strtok ( NULL, ":");  // skip the first token
            currentW = atoi ( pch );
            change = true;
          }
          if ( strstr ( (const char *) payload, "cool_value:" ) ) {
            if ( VERBOSE >= 10 ) Serial.println ( (const char *) payload );
            // strstr returns NULL if string not found
            char * pch;
            pch = strtok ( (char *) payload, ":" );
            pch = strtok ( NULL, ":");  // skip the first token
            currentC = atoi ( pch );
            change = true;
          }
          if ( change && outputState ) {
            setOutput  ( currentR, currentG, currentB,  currentW, currentC );
            forceWSUpdate = true;
          }
        }
              // send message to server
        // webSocket.sendTXT ( num, "message here", 12 );
      } else {
        if ( VERBOSE >= 10 ) Serial.printf ( "[WSc] text message length 1 (empty!)\n" );
      }
            break;
        case WStype_BIN:  // 4
            if ( VERBOSE >= 10 ) Serial.printf("[WSc] get binary length: %u\n", length);
//      hexdump(payload, length);
            // send data to server
            // webSocket.sendBIN(payload, length);
            break;
    case WStype_PING:  // 9?
      // pong will be send automatically
      if ( VERBOSE >= 10 ) Serial.printf("[WSc] get ping\n");
      break;
    case WStype_PONG:  // 10
      // answer to a ping we send
      if ( VERBOSE >= 10 ) Serial.printf("[WSc] get pong\n");
      break;
    default:
      Serial.printf ( "[WSc] got unexpected event type %d\n", type );
      break;
  }

  // Serial.println ( F ( "Switch exited" ) );

}

void update_WebSocket () {

  // ArduinoJSON v.6.x

  DynamicJsonDocument doc ( 600 );

  #if TESTING
    doc["PROGNAME"] = PROGNAME " TESTING";
  #else
    doc["PROGNAME"] = PROGNAME;
  #endif
  doc["VERSION"] = VERSION;
  doc["VERDATE"] = VERDATE;
  doc["UNIQUE_TOKEN"] = uniqueToken;

  char ipString[25];
  snprintf ( ipString, 25, "%u.%u.%u.%u", Network.ip[0], Network.ip[1], Network.ip[2], Network.ip[3] );
  doc["IP_STRING"] = ipString;
  doc["MDNS_ID"] = mdnsOtaId;

  if ( timeStatus() == timeSet ) {
    formatTimeString ( timeString, timeStringLen, now() );
  } else {
    snprintf ( timeString, timeStringLen, "<time not set>" );
  }

  doc["TIME"] = timeString;

  doc["MQTT_CMD_TOPIC"] = mqttCmdTopic;
  doc["MQTT_STATUS_TOPIC"] = mqttStatusTopic;
  doc["MQTT_TIMEOUT_TOPIC"] = mqttTimeoutTopic;
  doc["LAST_BOOT_AT"] = bootTimeString;

  // JsonArray dataArray = doc.createNestedArray("data");
  // const size_t pBufLen = 32;
  // char pBuf [ pBufLen ];

  doc["R_VALUE"] = currentR;
  doc["G_VALUE"] = currentG;
  doc["B_VALUE"] = currentB;
  doc["W_VALUE"] = currentW;
  doc["C_VALUE"] = currentC;

  doc["OUTPUT_STATUS"] = outputStateString [ outputState ];
  doc["OUTPUT_CHANGE_STATUS"] = outputState ? "turn OFF" : "turn ON";
  if ( timerStartedAt_ms == 0UL ) {
    doc["TIMER_SECONDS_REMAINING"] = 0;
    doc["TIMER_REMAINING_STRING"] = "";
  } else {
    unsigned long timeRemaining_s = timeRemaining_ms() / SECOND_ms;
    formatIntervalString ( timeString, timeStringLen, timeRemaining_s );
    snprintf ( pBuf, pBufLen, "Time remaining %s\n", timeString );
    doc["TIMER_SECONDS_REMAINING"] = timeRemaining_s;
    doc["TIMER_REMAINING_STRING"] = pBuf;
    // Serial.printf ( "Timer pBuf: %s\n", pBuf );
  }

  serializeJson ( doc, jsonString );

  #if 0 && defined ( TELEMETRY_ON )
    {
      snprintf ( pBuf, pBufLen, "%s/%s/debug/update_Websocket_JSON_string_len", PROGNAME, uniqueToken );
      sendValueToMQTT ( pBuf, strlen ( jsonString ), "update_Websocket JSON string len", MQTT_RETAIN );
    }
  #endif

  if ( VERBOSE >= 20 )
    Serial.printf ( "doc size %d and JSON string length %d\n", doc.size(), measureJson ( doc ) );

  if ( VERBOSE >= 15 ) Serial.println ( jsonString );

  // webSocket.sendTXT ( 0, jsonString, strlen ( jsonString ) );  // both work
  webSocket.broadcastTXT ( jsonString, strlen ( jsonString ) );  // both work

  if ( VERBOSE >= 15 ) Serial.print ( F ("  ... sent\n" ) );

  if ( ( VERBOSE >= 15 ) && forceWSUpdate ) Serial.println ( F ( "forced WS update completing" ) );

  forceWSUpdate = false;

}

// *****************************************************************************
// ********************************** time *************************************
// *****************************************************************************

time_t getUnixTime() {
  while (conn_UDP.parsePacket() > 0) ; // discard any previously received packets
  if ( VERBOSE >= 16 ) Serial.println ( F ( "Transmit NTP Request" ) );
  sendNTPpacket(timeServer);
  uint32_t beginWait = millis();
  while (millis() - beginWait < 1500) {
   int size = conn_UDP.parsePacket();
    if (size >= NTP_PACKET_SIZE) {
      if ( VERBOSE >= 16 ) Serial.println ( F ( "Receive NTP Response" ) );
      ntpTimeGoodP = true;
      conn_UDP.read(NTPBuffer, NTP_PACKET_SIZE);  // read packet into the buffer
      unsigned long secsSince1900;
      // convert four bytes starting at location 40 to a long integer
      secsSince1900 =  (unsigned long)NTPBuffer[40] << 24;
      secsSince1900 |= (unsigned long)NTPBuffer[41] << 16;
      secsSince1900 |= (unsigned long)NTPBuffer[42] << 8;
      secsSince1900 |= (unsigned long)NTPBuffer[43];
      return secsSince1900 - 2208988800UL + timeZone * 1UL * 60UL * 60UL;
    }
  }
  Serial.println ( F ( "No NTP Response :-(" ) );
  return 0; // return 0 if unable to get the time
}

void sendNTPpacket ( IPAddress& address ) {
  if ( VERBOSE >= 16 ) Serial.printf ( "Sending NTP request to %2u.%2u.%2u.%2u\n", 
    address[0], address[1], address[2], address[3] );
  memset(NTPBuffer, 0, NTP_PACKET_SIZE);  // set all bytes in the buffer to 0
  // Initialize values needed to form NTP request
  NTPBuffer[0] = 0b11100011;   // LI, Version, Mode
  NTPBuffer[1] = 0;     // Stratum, or type of clock
  NTPBuffer[2] = 6;     // Polling Interval
  NTPBuffer[3] = 0xEC;  // Peer Clock Precision
  // 8 bytes of zero for Root Delay & Root Dispersion
  NTPBuffer[12]  = 49;
  NTPBuffer[13]  = 0x4E;
  NTPBuffer[14]  = 49;
  NTPBuffer[15]  = 52;
  // all NTP fields have been given values, now
  // you can send a packet requesting a timestamp:                 
   // send a packet requesting a timestamp:
  conn_UDP.beginPacket ( address, 123 ); // NTP requests are to port 123
  conn_UDP.write ( NTPBuffer, NTP_PACKET_SIZE );
  conn_UDP.endPacket();
}

void formatTimeString ( char * result, int resultLen, unsigned long time ) {
  snprintf ( result, resultLen, "%04d-%02d-%02d %02d:%02d:%02dZ",
    year ( time ), month ( time ), day ( time ), hour ( time ), minute ( time ), second ( time ) );
}

void formatIntervalString ( char * result, int resultLen, unsigned long time ) {
  snprintf ( result, resultLen, "%02d:%02d:%02d",
    hour ( time ), minute ( time ), second ( time ) );
}

// *****************************************************************************
// ********************************** util *************************************
// *****************************************************************************

void saveSettingsToFile () {

  // try to write status to LittleFS

  // unsigned long s = millis();

  File f;
  f = LittleFS.open ( "/soft_reset_color.txt", "w" );
  f.printf ( "%d,%d,%d, %d,%d, %d", currentR, currentG, currentB,
                                    currentW, currentC,
                                    outputState );
  f.close();

  // unsigned long interval = millis() - s;
  // Serial.printf ( "LittleFS file write took %lu ms\n", interval );
}

// *****************************************************************************
// *****************************************************************************
// *****************************************************************************

Debug Messages

Debug messages go here
mcspr commented 2 years ago

You sure we are better served with the issue here instead of https://github.com/xoseperez/my92xx/issues?

Is this the actual MCVE, and channels don't properly adjust from 0 to 255?

#include <Arduino.h>
#include <my92xx.h>

#define MY92XX_MODEL        MY92XX_MODEL_MY9231
#define MY92XX_CHIPS        2
#define MY92XX_DI_PIN       12
#define MY92XX_DCKI_PIN     14

#define MY92XX_COLD         0
#define MY92XX_WARM         1
#define MY92XX_RED          4
#define MY92XX_GREEN        3
#define MY92XX_BLUE         5

static constexpr unsigned char MY92XX_CHANNELS[] {
    MY92XX_RED,
    MY92XX_GREEN,
    MY92XX_BLUE,
    MY92XX_WARM,
    MY92XX_COLD
};

static my92xx* _my92xx { nullptr };

void setup() {
    _my92xx = new my92xx ( MY92XX_MODEL, MY92XX_CHIPS,
            MY92XX_DI_PIN, MY92XX_DCKI_PIN,
            { \
            .scatter = MY92XX_CMD_SCATTER_APDM, \
            .frequency = MY92XX_CMD_FREQUENCY_DIVIDE_1, \
            .bit_width = MY92XX_CMD_BIT_WIDTH_8, \
            .reaction = MY92XX_CMD_REACTION_FAST, \
            .one_shot = MY92XX_CMD_ONE_SHOT_DISABLE, \
            .resv = 0 \
            }
        );

    _my92xx->setState(true);

    for (;;) {
        for (unsigned char value = 0; value < 255; ++value) {
            for (auto channel : MY92XX_CHANNELS) {
                _my92xx->setChannel(channel, value);
            }
            _my92xx->update();
            delay(100);
        }
    }
}
                                                                                                                                                                                                                                             void loop() {                                                                                                                                                                                                                                }

delayMicroseconds(...) is os_delay_us(...) so no surprise there :) https://github.com/esp8266/Arduino/blob/c312a2eaf1356ceaafad7c4935fa850e087c84fe/cores/esp8266/core_esp8266_wiring.cpp#L189-L191

Since we are expected to digitalWrite in a tight sequence... Have you tried locking interrupts before doing _my92xx->update() in the sendOutput(), like the library source have tried originally (but commented out)? Something like

 ets_intr_lock();
 _my92xx->update();
 ets_intr_unlock();
CBMalloch commented 2 years ago

I just tried locking interrupts before the update call. No change. :(