Tuesday, 28 May 2019

Pan/Tilt Head Using the NodeMCU L293D shield and TowerPro MG-996R Servomotor

Left: Dual TowerPro MG-996R, right: L293D NodeMCU ESp-12E Shield Click on picture for youtube video
I had always wanted a pan/tilt head. There is some deterrence value to the baleful gaze of a pan/tilt camera following your every move. While pan/tilt cameras are cheap and easily available, they are less useful at night: their range is a lot shorter.  It would be nice to move an LED torchlight together with the camera.

You could tape an LED torch onto your favorite IP camera, but the mechanism is often made of  plastic and the extra weight wears it out fast. A homebrew pan/tilt head would be nice ...

The L293D NodeMCU ESP-12E motor shield was such an easy build (see part 1, part 2, and part 3) I quickly bought two Towerpro MG-996R servomotors for just RM22.90 (about USD5) each and matching mounting brackets for RM10.30.

Towerpro MG-996R Servomotor

And matching accessories

The brackets were listed in lelong.com as:

"Arduino Multi Purpose Servo Motor Holder Bracket MG995 MG996"


Arduino Multi Purpose Servo Motor Holder Bracket MG995 MG996

I found the mechanical assembly work quite mystifying, but Starmaxtrek's youtube video was a godsend:

Click on picture for Youtube video on mechanical assembly
Documentation for the MG-996R took a little digging, but you can find a copy here.

For testing, I shorted VM to VCC on the L293D and powered everything from VCC using a USB smartphone charger.  The only change in the ESP-12E program was to change the PWM frequency to 50Hz:
  analogWriteFreq(50);

For my setup I found the servomotors reacted only to PWM values between 1-25%. The shaft rotated some 120 degrees and the pan/tilt head whirred about with a gentle menace.

The youtube video is here.

Life is good - Happy Trails.

Thursday, 16 May 2019

PWMing the ESP8266 - Programming: Part 3 of 3

L293D motor shield for NodeMCU ESP-12E. Note green 5V jumper supplying test power to L293D IC. Click on the picture for youtube video. 
In Part 2 I built a DC motor speed controller cum LED light dimmer with the parts I had on hand, mainly so I did not have to wait to test my software. In due course the part arrived: the L293 motor control shield for the ESP8266 NodeMCU ESP-12E development board. It cost me only RM10 (about USD2). The build quality was a little rough; I had some difficulty mounting the ESP-12E into the socket, but it worked as advertised.

L293D motor control shield


Tony Kambourakis has an excellent writeup on the L293D, so please head over there for details.

While testing the program I set the 5V USB power to drive the L293D by shorting VM to VIN, and connecting VIN to my Android smartphone charger. Try not to use the laptop/desktop USB port- it minimizes the damage should things go wrong. Be sure to remove the Vin-VM short when you progress to 12-36VDC. VIN still needs to be at 5VDC.

I only had to make a slight change to my program: the ULN2003 used different pins for PWM and added 2 GPIO pins for motor direction control. We do not really need to change the fan rotation direction, plus reversing power to LED lights may cause them not to turn on, so the additional GPIO pins are used to lock in the output pin assignments on the shield.

The http interface allows for easy webhooking into Google Home/Google Assistant for Voice Control.

#include <ESP8266WiFi.h>

const char* ssid = "YourAccessPoint";    
const char* password = "YourPassword";
IPAddress staticIP(123,45,67,89); // Change IP to suit your AP
IPAddress gateway(123,45,67,1);  
IPAddress subnet(255,255,255,0);
WiFiServer server (8080);
uint8_t PWMpin1 = D1;
uint8_t PWMpin2 = D2;
uint8_t Dirpin1 = D3;
uint8_t Dirpin2 = D4;

void setup ()
{
  delay (10);
  Serial.begin (115200);
  analogWrite(PWMpin1, 0);  /* set initial duty cycle */
  analogWrite(PWMpin2, 0);  /* set initial duty cycle */
  pinMode(Dirpin1, OUTPUT);
  pinMode(Dirpin2, OUTPUT);
  digitalWrite(Dirpin1, 1); // 2019-05-15 set output polarities to conform to shield
  digitalWrite(Dirpin2, 1);
  analogWriteFreq(10);    /* default is 1KHz range is 1Hz-1000kHz */

  // Connect to WiFi network
  Serial.println();
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);

  WiFi.mode(WIFI_OFF); // wifi reconnect workaround
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  WiFi.config(staticIP, gateway, subnet); // Static IP. Not required for dhcp
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi connected");
  Serial.print("Static IP address: ");
  Serial.println(WiFi.localIP());

  // Start the server
  server.begin();
  delay(50);
}

static int dots = 0;
static int val = 0; // 2018-10-25
static int val2 = 0; // 2019-04-19
static int commas = 0;
static unsigned long timeNow = 0; // 2019-03-17
static unsigned long timeLast = 0;
static int seconds = 0;

uint16_t dutycycle = 0; /* Set PWM duty cycle */

void loop() {

  // 2019-03-17
  timeNow = millis()/1000; // the number of milliseconds that have passed since boot
  if (timeLast==0 || timeNow<timeLast)
    timeLast = timeNow; // Start with time since bootup. Note millis() number overflows after 50 days
  seconds = timeNow - timeLast;//the number of seconds that have passed since the last time 60 seconds was reached.

  // 2019-02-24 Check for broken wifi links
  if (WiFi.status() != WL_CONNECTED) {
    delay(10);
    // Connect to WiFi network
    Serial.println();
    delay(10);
    Serial.print("Reconnecting WiFi ");
    delay(10);
    WiFi.mode(WIFI_OFF); // 2018-10-09 workaround
    WiFi.mode(WIFI_STA);
    WiFi.begin(ssid, password);
    WiFi.config(staticIP, gateway, subnet); // Static IP. Not required for dhcp

    while (WiFi.status() != WL_CONNECTED) {
      Serial.print("x");
      delay(500);
      dots++;
      if (dots >= 1000)
      {
        dots = 0;
        Serial.println("Giving up! Waiting for WDT reset ...");
        delay(20);
        while (1)
        {}
      }
    }
    Serial.println("");
    delay(10);
    Serial.println("WiFi reconnected");

    // Start the server
    server.begin();
    Serial.println("Server restarted");
    delay(10);
    // Print the IP address
    Serial.print("IP address: ");
    delay(10);
    Serial.println(WiFi.localIP());
    delay(10);
  }

  // Check if a client has connected
  WiFiClient client = server.available();
  if ( ! client ) {
    return;
  }

  //Wait until the client sends some data
  while ( ! client.available () )
  {
    delay (10); // 2019-02-28 reduced from 100
    Serial.print(".");
    if (commas++ > 5) {
      commas = 0;
      Serial.println("Terminating client connecting without reading");
      delay(20);
      client.stop();
      return;
    }
  }

  client.setTimeout(3000); // 2019-03-19
  Serial.println("new client connect, waiting for data ");

  // Read the first line of the request
  String req = client.readStringUntil ('\r');
  client.flush ();

  // Match the request
  String s = "";
  // Prepare the response
  if (req.indexOf ("/pwm1") != -1) // first channel
  {
    Serial.println("Command for PWM1 received");  

    int value_index = req.indexOf("/pwm1/");// Here you get the index to split the response string where it says "pwm1"
    String value_string = req.substring(value_index);
    String value = value_string.substring(6, value_string.indexOf("\r"));// Skip first 6 characters, get value
    Serial.println(value);
 
    s = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<!DOCTYPE HTML>\r\n<html>\r\nValue entered ";
    s += value;
    s += "\n\r";
    val = value.toInt();
 
    if (val < 0) // 2019-04-19 value must be 0-100%
      val = 0;
    else if (val > 100)
      val = 100;
    s += "Duty cycle (percent) used is: ";  
    s += String(val);
    s += "\n\r</html>\n";
    client.print (s);
 
    Serial.print("Duty cycle (percent) used is: ");
    Serial.println(val);

    dutycycle = (val * 1023) / 100; // Convert to 16-bit unsigned
    analogWrite(PWMpin1, dutycycle);
 
  } else if (req.indexOf ("/pwm2") != -1) {
      Serial.println("PWM2 command received");  

      int value_index = req.indexOf("/pwm2/");// Here you get the index to split the response string where it says "pwm1"
      String value_string = req.substring(value_index);
      String value = value_string.substring(6, value_string.indexOf("\r"));// Skip first 6 characters, get value
      Serial.println(value);

      s = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<!DOCTYPE HTML>\r\n<html>\r\nValue entered ";
      s += value;
      s += "\n\r";
      val2 = value.toInt();
 
      if (val2 < 0) // 2019-04-19 value must be 0-100%
        val2 = 0;
      else if (val2 > 100)
        val2 = 100;
      s += "Duty cycle (percent) used is: ";  
      s += String(val2);
      s += "\n\r</html>\n";
      client.print (s);
 
      Serial.print("Duty cycle (percent) used is: ");
      Serial.println(val2);

      dutycycle = (val2 * 1023) / 100; // Convert to 16-bit unsigned
      analogWrite(PWMpin2, dutycycle);
  } else if (req.indexOf("/index.html") != -1) {
      s = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<!DOCTYPE HTML>\r\n<html>\r\nStatus: PWM1 ";
      s += String(val);
      s += "% PWM2 ";
      s += String(val2);
      s += "%\r\n</html>\n";
      client.print (s);
      Serial.println("Status command received");
  }  
  else {
    Serial.println("invalid request");
    s = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<!DOCTYPE HTML>\r\n<html>\r\nInvalid request</html>\n";
    client.print (s);
  }

  delay (10);
  client.stop(); // 2019-02-24
  Serial.println("Client disonnected");
  delay(10);
}

And that was it- it worked first time. Having said that do test your system is stages: at first with no power to the L293D while you worked out the bugs in the program, and then with low (5V current-limited) power from the USB power supply. You can check the motor output pins with a voltmeter for polarity before connecting up the fan and LED lamp. 

I also use a 60W 230Vac mains isolation transformer for safety - it limits mains power in case of catastrophic failures. 

My test setup: clockwise from top left: isolation transformer, LED lamp, phone charger as 5V supply, ancient 12VDC power adapter, smartphone with chrome browser, ESP-12E with L293D motor shield, and 12VDC fan. Click on picture for youtube video.

On the smartphone browser, I used:
http://123.45.67.89:8080/pwm1/100

On my Linux desktop I used 
lynx -dump -read_timeout=5 -cmd_script=./lynx_script http://123.45.67.89:8080/pwm2/0

Make sure the phone or desktop is logged into the same WiFi access point. Notice the IP address here does not really work- they are not even Class C (ie your AP will not use them) so substitute your own.

There you have it: IoT PWM DC fan and LED lamp controller, all set for Google Home Voice Control. It cost me only RM30 (about USD8) per unit. Making electronics stuff can be cheap, fun and fast.

Happy Trails.

Wednesday, 24 April 2019

PWMing the ESP8266 Part 2 of 3: Building the Board

ESP-12E ESP8266 NodeMCU with ULN2003 Darlington IC provides PWM for 12Vdc devices like LED Lantern and DC Fan
Link to youtube video.

In Part 1, we saw that it was fast and easy to test Pulse Width Modulation (PWM) using an ESP8266-based NodeMCU ESP-12E development kit. To control a real device we increase the power rating of the ESP-12E's output pins D5 and D6 from 3.3V to 12V using darlington transistors.

Since we are all about fast and cheap prototyping, a handy darlington is the ULN2003. It is actually 7 darlingtons in a 16-pin package but is rated 50Vdc at 500mA and is cheaper than many single (ie discrete) darlington transistors.

ULN2003 is actually 7 Darlington Transistors
We will need a blank solder-able prototyping PCB - veroboard, stripboard, breadboard. Don't be afraid to try a little soldering- all you need is practice. Bob Pease, the legendary electronics guru, used to say, "My favorite programming language is solder".

Prototyping PCB
Bob "My favorite programming language is solder" Pease

Notice I have used an IC socket for the ULN2003- when dealing with motors and high voltages, mishaps are to be expected; it may need to be changed in a hurry. Notice the socket is only a 6-pin DIP originally meant for optical isolator ICs. This means I am only using 2 out of the 7 darlingtons. Feel free to cut off the dangling IC legs or even better simply turn them up out of harm's way. The extra metal helps to dissipate heat from the darlingtons.

Live bugs: ULN2003 and ESP-12E assembled on prototype board
Same board, solder-side. 

Also I only soldered 3 pins of the ESP-12E board, plus an extra 3 pins at each corner for mechanical strength. This makes it easy to de-solder the ESP-12E for other projects.

There are 3 pairs of wires: one for 12V DC power supply, and one each for each of the two PWM outputs. The idea is to tie one end of the load (fan, LED, etc) to 12V and the other to the darlington output so that the ESP8266 and turn it off and on at will.

I really should have used different colored wires for each pair but I did not have enough wires of the correct gauge, so had to make do with a little duct tape and marker pen.

PWM output wires are 12V (red) and darlington collector (black)
 For a long and happy life for the ULN2003 while driving inductive loads like DC motors, I have also connected a 12V 1W zener diode, forward-biased from 12V to pin 9. It is a little counter-intuitive but you are strongly encouraged to read Douglas W Jones' venerable work on stepper motor drivers, using among others the ULN2003. 

Douglas W Jones: his kung-fu is strong ...

To minimize damage from wiring mistakes and the like, first check the unpowered, unconnected board for short-circuits, especially between 3.3V and GND, 5V and GND and 12V and GND. Next check for ESP-12E and ULN2003 shorts to 3.3V, GND and 12V. 

When testing, it is advisable to first not to connect to 12V power and just test with a power bank. Then test with 12V on. If it works well enough test with a powered USB hub rather than directly from your laptop/desktop. If you must test with a desktop try using a USB port from a PCI daughter card rather than from the mainboard. This is to avoid damaging expensive kit like laptops, desktops and servers.

There you have it, a PWM board you can build in half a day. It can control DC devices up to 50V at 500mA like CPU cooling fans, LED lights, and alarm sirens. 

Happy trails.

Friday, 19 April 2019

PWMing the ESP8266 Part 1 of 3

Pulse Width Modulation can be used to dim LED lights and set DC motor speed
Link to youtube video.

Some time ago, the dual fans on my laptop stand froze solid. I had been re-purposing old laptops as IoT servers and running them 24/7 wore out the internal CPU fans, so I ventilated the bottom with a dual-fan laptop stand.

Laptop stand with dual slow fans
That lasted only a few months. Fans have a surprisingly low temperature rating, often lower than that of a CPU. A failing fan will slow down causing the CPU temperature to rise, which further shortens fan life. So the trick is not to overheat the fan (the CPU is usually tougher).  I would rather replace the fans in the stand - it is much harder to replace the one in the laptop. So, I replaced the weedy 5V USB fans and shoehorned in two 12V 80mm x 800mm power supply ball-bearing fans.

Take this, hot stuff
This sorted the immediate problem: the laptop runs a lot cooler but it is also a lot noisier. If I left the study door open the wife would think the kettle has been left on. And, the increased vibration cannot be good for the laptop.

Pulse-Width Modulation can be used to control fan speed by turning off its power some of the time. And making an IoT PWM device would be a nice project.

Using the Linux command sensors I can even detect the CPU temperature and speed up the fan accordingly with a bash shell:

$sensors
coretemp-isa-0000
Adapter: ISA adapter
Package id 0:  +63.0°C  (high = +100.0°C, crit = +100.0°C)
Core 0:        +63.0°C  (high = +100.0°C, crit = +100.0°C)
Core 1:        +60.0°C  (high = +100.0°C, crit = +100.0°C)

nouveau-pci-0100
Adapter: PCI adapter
GPU core:     +0.60 V  (min =  +0.60 V, max =  +1.20 V)

With DevOps, continuous improvement is the thing, so there is no need to have an all-singing all-dancing system done in one go. Instead we aim always for a working set of hardware and software, and to make sure little can go wrong, we implement the barest minimum, ie we start off with the smallest set of features we can.This means just the PWM.

ElectronicWings has a good writeup, and I will not replicate it here. There is minimal hardware (wiring) involved. The PWM output is indicated by the brightness of the LED.

Look, Ma, no soldering: Eazyhooks and PVC terminals are used to connect an LED and 330R resistor to the ESP-12E's D6 pin


The program is equally simple:

uint8_t LEDpin = D6; /* By default PWM frequency is 1000Hz and we are using same for this application hence no need to set */ void setup(){ Serial.begin(9600); analogWrite(LEDpin, 512); /* set initial 50% duty cycle */ } void loop(){ uint16_t dutycycle = analogRead(A0); /* read continuous POT and set PWM duty cycle according */ if(dutycycle > 1023) dutycycle = 1023;/* limit dutycycle to 1023 if POT read cross it */ Serial.print("Duty Cycle: "); Serial.println(dutycycle); analogWrite(LEDpin, dutycycle); delay(100); }

Instead of obtaining dutycycle from analogRead() I simply hardcoded a value between 0 and 1023 and reprogrammed and re-ran the ESP-12E a few times. This meant I did not need to wire up the trimpot. And since the difference in brightness can be a little hard to distinguish between reprograms, I modified it slightly so it changed dutycycle every 1.5s:

uint8_t LEDpin = D6;

/*
 *
   By default PWM frequency is 1000Hz and we are using same
   for this application hence no need to set
 
   https://www.electronicwings.com/nodemcu/nodemcu-pwm-with-arduino-ide
 */

void setup(){
  Serial.begin(115200);
  analogWrite(LEDpin, 1);  /* set initial duty cycle */
  analogWriteFreq(10);    /* default is 1KHz range is 1Hz-1000kHz */
}

uint16_t dutycycle = 0; /* Set PWM duty cycle */

void loop(){
  if(dutycycle > 1023) dutycycle = 1023;/* limit dutycycle to 1023 if POT read cross it */
  Serial.print("Duty Cycle: ");  Serial.println(dutycycle);
  analogWrite(LEDpin, dutycycle);
  delay(10000);
  if (dutycycle < 1023)
    dutycycle += 100;
  else
    dutycycle = 0;
}

And there you have it, PWM on the ESP8266-based NodeMCU ESP-12E board. Took hardly an hour. In Part 2, we will connect it to the cooling fans and in Part 3 set it up to webhook into Google Assistant.

Happy Trails.

Monday, 8 April 2019

Putting the ESP8266 to Sleep

Your pins are feeling heavy ... you shall enter a Deep Sleep ...
The ESP8266 is not exactly a low-power device and you really feel it when you are running it from a battery. An ESP-01S can draw up to 70mA when connected to its Access Point. A 3000mAh power bank will not last 2 days at that rate. Even doing nothing, the ESP-01S in program mode draws 20mA

Idle ESP-01S draws 20mA


Putting it to sleep can reduce the current to less than 1mA as this excellent link from randomnerd shows.. Now we're talking! but this usually means the bare board - randomnerd even removed the power LED to save those extra mA. Most applications require a little more peripherals so even if my current draw is 7mA my 3000mAh power bank will now last 18 days instead of 2, which is good enough for a start.

randomnerd's writeup is pretty much complete - I will not repeat his work here. The first test using the ESP-12E was done in less than an hour. I only had to connect D0 to RST.

ESP-12E all wired up for deep sleep. Notice the active current draw of 60mA


I used a cheap little USB ammeter for no other reason than it was handy and convenient, but it had a resolution of only 10mA. The ESP-12E current draw dropped from 60mA to 'zero' (ie less than 10mA) in deep sleep.

Thus encouraged, I soldered the wire required for the ESP-01S. But unlike randomnerd, rather than entering deep sleep the ESP-01S continually rebooted. This doesn't mean randomnerd gave bad advice. As usual with these cheap boards, your milage may vary.

Wiring D0/GPIO16 directly to ESP-01S RST pin did not work


The answer here sounded convincing: D0/GPIO16 floats high when in deep sleep and this can trigger RST which wakes up the ESP8266 who then runs its program and goes back to sleep and so on. The recommendation was for a 470Ohm resistor in series. I happened to have a 560Ohm one so in it goes:



Adding a 560-Ohm resistor in series worked

And work it did. I like the ESP-12E well enough but the ESP-01S is just so easy to package. Time to put it to good use.

And immediately found a gotcha- when it wakes from deep sleep it is equivalent to a hard reset. This means it no longer has its volatile memory (RAM). This makes things a little tricky if the ESP-01S had to drive an output like a relay or a transistor.

Now it will be still handy as an IoT input device running on battery. It wakes up, say every 5 minutes, connect to the access point, takes a reading of its sensor, say the room temperature, or a door magnetic switch and publish it to an MQTT server. The server acts as its non-volatile memory and with a little luck we might be even be able to finesse an IoT output by reading the MQTT subscription and re-initializing its output before the load realizes it. This will depend on whether the relay board is able to hold its output with the ESP-01S in deep sleep.

So in a fit of optimism, fired up by success so far, I got out a 1-channel relay PCB.

Notice the maximum current draw is 130mA with the ESP-01S awake and the relay on
The current draw of the ESP-01S with the relay on is 130mA. Now it does not make complete sense to drive an IoT relay output from a battery. Often the load will need power as well and may may draw precious mA from the battery.

The program is:

#include <ESP8266WiFi.h>
#include "RemoteDebug.h" // 2019-04-08 https://github.com/JoaoLopesF/RemoteDebug
RemoteDebug Debug;
#define HOST_NAME "esp01s-1relay"

const char* ssid = "YourAP"; // For testing in study
const char* password = "YourPassword";

IPAddress staticIP(12,34,56,78); // for testing in study only
IPAddress gateway(12,34,56,1); // for testing in study only
IPAddress subnet(255,255,255,0);

// Create an instance of the server
// specify the port to listen on as an argument
WiFiServer server(8080);

static int count = 0;
void setup() {
  Serial.begin(115200);
  delay(10); // 2018-10-08
  Serial.setTimeout(2000); // 2019-04-08
  pinMode(0, OUTPUT); // 2018-10-25 set gpio to o/p
  digitalWrite(0, 0); // Default to 'off' state to save power. relay board starts up with relay 'on'
  delay(10);

  // Connect to WiFi network
  Serial.println();
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);

  WiFi.mode(WIFI_OFF); //workaround 2018-10-08
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  WiFi.config(staticIP, gateway, subnet); // Static IP. Not required for dhcp

  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.print(".");
    count++;
    if (count >= 300) // 5 minutes==300s
    {
      count = 0;
      Serial.println("setup() giving up trying to connect to AP! waiting for WDT reset ... see you in 5min");
      delay(20);
      while (1)
      {}
    }
  }

  Serial.println("");
  Serial.println("WiFi connected");


  String hostNameWifi = HOST_NAME; // 2019-04-08 for remote debug
  hostNameWifi.concat(".local");
  WiFi.hostname(hostNameWifi);
  Debug.begin(HOST_NAME); // Initialize the WiFi server
  Debug.setResetCmdEnabled(true); // Enable the reset command
  Debug.showProfiler(true); // Profiler (Good to measure times, to optimize codes)
  Debug.showColors(true); // Colors

  // Start the server
  server.begin();
  Serial.println("Server started");
  debugI("Starting http server ...");
  delay(50); // 2018-10-25

  // Print the IP address
  Serial.println(WiFi.localIP());
  Debug.handle();
  yield();
}

static int dots = 0;
static int val = 0; // 2018-10-25
static int commas = 0;

void loop() {
  // 2019-02-27 Check for broken wifi links
  if (WiFi.status() != WL_CONNECTED) {
    delay(10);
    // Connect to WiFi network
    Serial.println();
    delay(10);
    Serial.print("Reconnecting WiFi ");
    debugI("Reconnecting WiFi ");
    delay(10);
    WiFi.mode(WIFI_OFF); // 2018-10-09 workaround
    WiFi.mode(WIFI_STA);
    WiFi.begin(ssid, password);

    while (WiFi.status() != WL_CONNECTED) {
      Serial.print("x");
      debugI("x");
      delay(500);
      dots++;
      if (dots >= 1000)
      {
        dots = 0;
        Serial.println("Giving up! Waiting for WDT reset ...");
        debugI("Giving up! Waiting for WDT reset ...");
        delay(20);
        yield();
        delay(20);
        while (1)
        {}
      }
      yield();
    }
    Serial.println("");
    delay(10);
    Serial.println("WiFi reconnected");
    debugI("WiFi reconnected");
 
    // Start the server
    server.begin();
    Serial.println("Server restarted");
    debugI("Server restarted");
    delay(10);
    // Print the IP address
    Serial.print("IP address: ");
    delay(10);
    Serial.println(WiFi.localIP());
    delay(10);
    Debug.handle();
  }

  // Check if a client has connected
  WiFiClient client = server.available();
  if (!client) {
    delay (10); // 2018-10-25
    //Serial.println("server not available!");
    // Serial.print("."); // no harm but too many dots
    delay(20);
    Debug.handle();
    return; // 2019-02-27 missing return
  }

  // Wait until the client sends some data
  Serial.println("new client");
  debugI("new client");
  while(!client.available()){
    delay(10); // 2019-02-28
    //Serial.println("client not available!");
    Serial.print(",");
    debugI(",");
   if (commas++ > 5) { // 2019-02-28
      commas = 0;
      Serial.println("Terminating client connecting without reading");
      debugI("Terminating client connecting without reading");
      delay(20);
      client.stop();
      Debug.handle();
      yield();
      return;
    }
    Debug.handle();
  }

  // Read the first line of the request
  String req = client.readStringUntil('\r');
  Serial.println(req);
  client.flush();

  // Match the request
  String s = "";
  // Prepare the response
  if (req.indexOf("/gpio/0") != -1) {
    val = 0;
    // Set GPIO2 according to the request
    digitalWrite(0, val);

    s = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<!DOCTYPE HTML>\r\n<html>\r\nRelay is now ";
    s += (val)?"on":"off";
    s += "</html>\n";
    debugI("relay off");
  }
  else if (req.indexOf("/gpio/1") != -1) {
    val = 1;
    // Set GPIO2 according to the request
    digitalWrite(0, val);

    s = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<!DOCTYPE HTML>\r\n<html>\r\nRelay is now ";
    s += (val)?"on":"off";
    s += "</html>\n";
    debugI("relay on");
  }
  else if (req.indexOf("/sleep/") != -1) {
    int value_index = req.indexOf("/sleep/");    // Here you get the index to split the response string right where it says "value"
    String value_string = req.substring(value_index);
    String value = value_string.substring(7, value_string.indexOf("\r"));   // Here you ignore the first nine characters of the substring ("-v-a-l-u-e-"-: ), then get the value, then ignore the rest of the string after the comma.
    Serial.println(value);
    s = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<!DOCTYPE HTML>\r\n<html>\r\nValue is now ";
    s += value;
    s += "</html>\n";
    val = value.toInt();
    debugI("Sleeping for %ds", val);
    //char valueless[80];
    //value.toCharArray(valueless, 80);
    //debugI("Sleeping for %s\n", valueless);
  }
  else if (req.indexOf("/index.html") != -1) {
    s = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<!DOCTYPE HTML>\r\n<html>\r\nRelay is ";
    s += (val)?"on":"off";
    s += "</html>\n";
    debugI("relay state is %d\n", val);
  }
  else {
    Serial.println("invalid request");
    s = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<!DOCTYPE HTML>\r\n<html>\r\nInvalid request</html>\n";
    debugI("invalid request");
  }
  Debug.handle();
  yield();
  client.flush();

  // Send the response to the client
  client.print(s);
  delay(10);
  client.stop(); // 2019-02-27
  Serial.println("Client disonnected");
  delay(10);
  // The client will actually be disconnected
  // when the function returns and 'client' object is detroyed
  if (val>1) {
    debugI("Sleeping for %ds", val);
    Serial.print("Sleeping for ");
    Serial.println(val);
    Debug.handle();
    delay(20);
    ESP.deepSleep(val); // 2019-04-08
  }
}



I have included Joao Lopes' Remote Debug code, as once mounted on the relay board it was difficult to get debug messages. You just do:

telnet 12.34.56.78

The program starts up with the relay off and initially draws 60mA. When the relay is turned on the system draws another 60mA. The deep sleep function is ESP.deepSleep(val) . On calling it, the current draw went from 120mA to 60mA.  To turn on the relay, you use a browser or the lynx command:

$lynx -read_timeout=5 -dump -cmd_script=./lynx_script http://12.34.56.78:8080/gpio/1
   Relay is now on

To sleep 30 seconds you specify the time in microseconds:

$lynx -read_timeout=5 -dump -cmd_script=./IoT/lynx_script http://12.34.56.78:8080/sleep/30000000
   Value is now 30000000 HTTP/1.0

The relay board proved unable to hold its last state when the ESP-01S goes into deep sleep. When deepSleep() is called the relay turns on. This resulted in the same current draw of 60mA. The 60mA saved by the deep sleep is taken up by the relay coil current. But dropping the relay state on deep sleep is a deal breaker for most applications.

Time to try the 2-channel relay. You can get it on ebay here. It has its own microprocessor, which should mean it can hold its state without the ESP-01S. The ESP-01S turns on the relays using the serial port. In the program you just replace code like:
    digitalWrite(0, val);
with

//Hex command to send to serial for close relay
byte relON[]  = {0xA0, 0x01, 0x01, 0xA2};
//Hex command to send to serial for open relay
byte relOFF[] = {0xA0, 0x01, 0x00, 0xA1};
//Hex command to send to serial for close relay
byte rel2ON[]  = {0xA0, 0x02, 0x01, 0xA3};
//Hex command to send to serial for open relay
byte rel2OFF[] = {0xA0, 0x02, 0x00, 0xA2};

    Serial.write (relON, sizeof(relON));

And sure enough it works. With the ESP-01S running and the relays off, it draws 70mA, about the same as the 1-channel relay board. With the ESP-01S in sleep mode it draws less than 10mA.

Dual Relay PCB with uP serial control. Note less than 10mA current draw in deep sleep

With everything running and both relays on, the current draw is 190mA. With ESP-01S now put to deep sleep, the relays retain their state and the system draws 120mA. More importantly when it wakes up, the relay state is still retained (although the ESP-01S will not remember this and has to subscribe to MQTT to find out).

Maximum power: 190mA with both relay and ESP-01S on

This is actually the ideal setup for my Google Voice activated IoT Autogate. The original version used 230Vac for power. Now I can use the autogate 12V backup battery. In the event of a power cut (happens a lot in my house but that is another story), I was afraid the IoT would drain the battery. Now I can simply put it to sleep. To use voice activation during a power cut, I simply set the sleep timer for 5 minutes. The command would be saved in the MQTT server feed and would be read by the ESP-01S when it wakes up. Even better, the autogate uses command pulses, so the relays are never active for more than 1 second.

There you have it: doing good while in deep sleep. Just goes to show in electronics, sometimes you can have your cake and eat it. Happy Trails.

Monday, 25 March 2019

Hacking the Sony ICF-C205 Radio Clock to synchronize time over the Internet (Updated)

Sony ICF-205 Radio Clock
The wife has an ancient Sony ICF-205 Radio Clock which gave many years of good service, but recently it needed its time corrected more often. She tried changing the 9V backup battery a few time to no avail, and rather than buying a new one I exchanged it for my study clock.

Now having done the ESP8266 NTP WiFi clock using the MAX7129 LED Driver,  I figured it would be a doddle to keep the casing and the LEDs and replace the electronics. I would need to do a good bit of wiring, but it would be light work without much programming or mental effort.  Boy was I mistaken.

Clock IC SC8560
The clock IC is SC8560. Found a copy of the datasheet here. It had a supply voltage of -14V. The LED display, correspondingly needed -14V signals, quite unsuitable for the MAX7219.

Luckily danman had the perfect solution. You set the time by power-cycling it and then shorting the 'Set Hour' button.

Modifications top left: Live wire cut to attach cable to relay1, extra white & blue AC mains cable to 5V power module.  Middle left: button 1 shorted, button 2 attached to cable for relay2.
Rear view showing 5Vdc Power Supply mounted in 9V battery compartment
Ready for ESP8266 programming. Click here for youtube video.
In short, one relay is used to power-cycle the clock radio by disconnecting the AC Live connection. On re-connection, this sets the time to 00:00 or 12 midnight. Another relay is used to short the 'Hour' button. The 'hour' digit advances by 1 every time the Hour Button is shorted. This lets the ESP8266 program set the clock radio to any time.

The time is obtained from the WiFi connection via the NTP servers in pool.ntp.gov.

Since I have only 2 relays, I needed to wait till the exact hour, e.g. 01:00 before I can set the clock. There are 3 ways to set the time:

1. on power up, the programs waits for the minute to be 00 then sets the clock to the exact time of day
2. at 01:00 in the morning the program will set the time again
3. using http://12.34.56.78:8000/reset to power-cycle the clock and http://12.34.56.78:8000/hour to advance the hour display

The libraries used are TimeLib.h, ESP8266WiFi.h, WiFiUdp.h and RemoteDebug.h, all available from the Library Manager downloads in the Adruino IDE.

Since the serial debug monitor is difficult to get to, RemoteDebug.h provides a debug link via telnet, e.g. 'telnet 12.34.56.78'.

Click here for the youtube video.

The program is:

#include <TimeLib.h>
#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
#include "RemoteDebug.h" // 2019-03-22

const char ssid[] = "YourAccessPoint";  //  your network SSID (name)
const char password[] = "YourPassword";       // your network password
IPAddress staticIP(12,34,56,78);
IPAddress gateway(12,34,56,1);  
IPAddress subnet(255,255,255,0);
IPAddress dns(8,8,8,8); // 2019-03-24

WiFiServer server (8000);

// NTP Servers:
static const char ntpServerName[] = "pool.ntp.org"; // 2019-03-24 from us.pool.ntp.org
//static const char ntpServerName[] = "time.nist.gov";
//static const char ntpServerName[] = "time-a.timefreq.bldrdoc.gov";
//static const char ntpServerName[] = "time-b.timefreq.bldrdoc.gov";
//static const char ntpServerName[] = "time-c.timefreq.bldrdoc.gov";

const int timeZone = +8;     // Malaysia Time Time
//const int timeZone = 1;     // Central European Time
//const int timeZone = -5;  // Eastern Standard Time (USA)
//const int timeZone = -4;  // Eastern Daylight Time (USA)
//const int timeZone = -8;  // Pacific Standard Time (USA)
//const int timeZone = -7;  // Pacific Daylight Time (USA)


WiFiUDP Udp;
unsigned int localPort = 8888;  // local port to listen for UDP packets

time_t getNtpTime();
void digitalClockDisplay();
void printDigits(int digits);
void sendNTPpacket(IPAddress &address);

//Hex command to send to serial for close relay
byte relON[]  = {0xA0, 0x01, 0x01, 0xA2};
//Hex command to send to serial for open relay
byte relOFF[] = {0xA0, 0x01, 0x00, 0xA1};
//Hex command to send to serial for close relay
byte rel2ON[]  = {0xA0, 0x02, 0x01, 0xA3};
//Hex command to send to serial for open relay
byte rel2OFF[] = {0xA0, 0x02, 0x00, 0xA2};

#define HOSTNAME "NTP_ClockRadio"
RemoteDebug Debug;

int cold_start = 0; // 2019-03-25
void setup()
{
  cold_start = 1; // 2019-03-25
  Serial.begin(115200);
  while (!Serial) ; // Needed for Leonardo only
  delay(250);
  Serial.println("TimeNTP Example");
  Serial.print("Connecting to ");
  Serial.println(ssid);
  WiFi.mode(WIFI_OFF); // 2018-10-09 workaround
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  WiFi.config(staticIP, dns, gateway, subnet); // Static IP. Not required for dhcp

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

  Debug.begin(HOSTNAME);
  Debug.setResetCmdEnabled(true); // Enable the reset command

  Debug.showProfiler(true); // Profiler (Good to measure times, to optimize codes)
  Debug.showColors(true); // Colors
  Serial.println("* Arduino RemoteDebug Library");
  Serial.println("*");
  Serial.print("* WiFI connected. IP address: ");
  Serial.println(WiFi.localIP());
  Serial.println("*");
  Serial.println("* Please use the telnet client (telnet for Mac/Unix or putty and others for Windows)");
  Serial.println("* or the RemoteDebugApp (in browser: http://joaolopesf.net/remotedebugapp)");
  Serial.println("*");
  Serial.println("* This sample will send messages of debug in all levels.");
  Serial.println("*");
  Serial.println("* Please try change debug level in client (telnet or web app), to see how it works");
  Serial.println("*");

  // Start the server
  Serial.println("Starting http server ...");
  debugI("Starting http server ...");
  server.begin();
  delay(50);

  Serial.println("Starting UDP");
  debugI("Starting UDP");
  Udp.begin(localPort);
  Serial.print("Local port");
  debugI("Local port %d", Udp.localPort());
  Serial.println(Udp.localPort());
  Serial.println("waiting for sync");
  debugI("waiting for sync");
  Debug.handle();
  setSyncProvider(getNtpTime);
  setSyncInterval(300);
}

time_t prevDisplay = 0; // when the digital clock was displayed
static int dots = 0;
static int val = 0; // 2018-10-25
static int commas = 0;
static int Minute = 45; // 2019-03-25 Init to nonzero
static int Hour = 0;
static int daily_sync = 0;
#define Minute_SET 0  // should always be zero. Nonzero values for testing only
#define Hour_SET 1    // should always be one. Other values for testing only
void loop()
{
   // 2019-02-24 Check for broken wifi links
  if (WiFi.status() != WL_CONNECTED) {
    delay(10);
    // Connect to WiFi network
    Serial.println();
    delay(10);
    debugE("Connection lost. Reconnecting WiFi ...");
    Serial.print("Connection lost. Reconnecting WiFi ...");
    delay(10);
    WiFi.mode(WIFI_OFF); // 2018-10-09 workaround
    WiFi.mode(WIFI_STA);
    WiFi.begin(ssid, password);
    WiFi.config(staticIP, dns, gateway, subnet); // Static IP. Not required for dhcp

    while (WiFi.status() != WL_CONNECTED) {
      Serial.print("x");
      debugW("x");
      delay(500);
      dots++;
      if (dots >= 1000)
      {
        dots = 0;
        Serial.println("Giving up! Waiting for WDT reset ...");
        debugE("Giving up! Waiting for WDT reset ...");
        delay(20);
        while (1)
        {
            debugE("Death loop waiting for WDT reset ...");
            delay(1000);
            Debug.handle();
        }
      }
      Debug.handle();
    }
    Serial.println("");
    delay(10);
    Serial.println("WiFi reconnected");
    debugI("WiFi reconnected");
    // Start the server
    server.begin();
    Serial.println("Server restarted");
    debugW("Server restarted");
    delay(10);
    // Print the IP address
    Serial.print("IP address: ");
    delay(10);
    Serial.println(WiFi.localIP());
    delay(10);
  }

  // 2019-03-24 Run this first
  if (timeStatus() != timeNotSet) {
    if (now() != prevDisplay) { //update the display only if time has changed
      prevDisplay = now();
      digitalClockDisplay();
    }
    // Code to reset the Sony SC8560 clock radio 2019-03-25
    if (cold_start) {
      switch (cold_start)
      {
        case 1:
          Minute = minute();
          if (Minute == Minute_SET) // Wait for the hour
            cold_start++;
          else {
            Serial.print("Cold start reset & sync: waiting till minute is ");
            Serial.print(Minute_SET);
            Serial.print(" now ");
            Serial.println(Minute);
            debugV("Cold start reset & sync waiting till minute is %d, now %d", Minute_SET, Minute);
            delay(500);
          }
        break;
        case 2:
          Serial.println("Cold start reset & sync started");
          debugI("Cold start reset & sync started %d:%d", hour(), minute());
       
          // On power-up clock is reset but the minute display will be wrong as the esp8266
          // Might need to wait a few minutes for the hour.
          Serial.write (relON, sizeof(relON)); // relay is normally closed
          delay(2000); // It takes awhile for the clock to reset
          Serial.write (relOFF, sizeof(relOFF)); // relay is normally closed
          delay(400);
       
          // Now after a reset the clock time is 00:00
          Hour = hour();        
          for (int i = 0; i<Hour;i++) // Set the hour.
          {
            Serial.write (rel2ON, sizeof(rel2ON)); // relay is normally open
            delay(100);
            Serial.write (rel2OFF, sizeof(rel2OFF)); // relay is normally open
            delay(100);
            debugI("Cold start sync-pulse Hour pulse #%d", i);
          }
          cold_start = 0;
        break;
        default:
          debugE("Error! illegal value cold_start %d", cold_start);
          Serial.print("Error! illegal value cold_start ");
          Serial.println(cold_start);
          delay(1000);
        break;
      }    
    }
    else {
      if (daily_sync == 0)
      {
        if (minute()==Minute_SET && hour()==Hour_SET)
          daily_sync=1;
      }
      else
      {
        Serial.println("Daily sync started");  
        debugI("Daily reset & sync started %d:%d", hour(), minute());  
        Serial.write (relON, sizeof(relON)); // relay is normally closed
        delay(2000);
        Serial.write (relOFF, sizeof(relOFF)); // relay is normally closed
        delay(500);
        // Now after a reset the clock time is 00:00
     
        Serial.write (rel2ON, sizeof(rel2ON)); // relay is normally open
        delay(100);
        Serial.write (rel2OFF, sizeof(rel2OFF)); // relay is normally open
        debugI("Daily sync, 1 Hour pulse");
        daily_sync = 0;
      }
    }
  }
  else {
    debugD("timeStatus() returned timeNotSet!");
    delay(1000);
  }

  Debug.handle();

  // Check if a client has connected
  WiFiClient client = server.available();
  if ( ! client ) {
    return;
  }

  //Wait until the client sends some data
  while ( ! client.available () )
  {
    delay (10); // 2019-02-28 reduced from 100
    Serial.print(".");
    if (commas++ > 5) {
      commas = 0;
      Serial.println("Terminating client connecting without reading");
      debugW("Terminating client connecting without reading");
      delay(20);
      client.stop();
      return;
    }
    Debug.handle();
  }

  client.setTimeout(2000); // 2019-03-24
  Serial.println("new client connect, waiting for data ");
  debugI("new client connect, waiting for data ");

  // Read the first line of the request
  String req = client.readStringUntil ('\r');
  client.flush ();

  // Match the request
  String s = "";
  // Prepare the response
  if (req.indexOf ("/reset") != -1) // Reset power to radio clock
  {
    Serial.println("Reset & sync command received");  
    Serial.write (relON, sizeof(relON)); // relay is normally closed
    val = 1; // if you want feedback see below
    s = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<!DOCTYPE HTML>\r\n<html>\r\nReset";
    s += "</html>\n";
    client.print (s);
    delay(2000);
    Serial.write (relOFF, sizeof(relOFF));
    Serial.println("Reset command executed");          
    debugW("Reset command executed");
  }
  else if (req.indexOf ("/hour") != -1) {
    Serial.println("Set hour command received");  
    debugI("Set hour command received");
    Serial.write (rel2ON, sizeof(rel2ON));
    val = 0; // if you want feedback
    s = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<!DOCTYPE HTML>\r\n<html>\r\nIncremented hour";
    //s += (val)?"on":"off";
    s += "</html>\n";
    client.print (s);
    delay(100);
    Serial.write (rel2OFF, sizeof(rel2OFF));
  } else if (req.indexOf("/index.html") != -1) {
      s = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<!DOCTYPE HTML>\r\n<html>\r\nStatus is ";
      //s += (val)?"on":"off";
      s += val; // 2018-10-25
      s += "</html>\n";
      client.print (s);
      Serial.println("Status command received");
      debugI("Status command received");
  }  
  else {
    Serial.println("invalid request");
    debugW("invalid request");
    s = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<!DOCTYPE HTML>\r\n<html>\r\nInvalid request</html>\n";
    client.print (s);
  }

  delay (10);
  client.stop(); // 2019-02-24
  Serial.println("Client disonnected");
  debugI("Client disonnected");
  delay(10);
  Debug.handle();
}

void digitalClockDisplay()
{
  // digital clock display of the time
  Serial.print(hour());
  printDigits(minute());
  printDigits(second());
  Serial.print(" ");
  Serial.print(day());
  Serial.print(".");
  Serial.print(month());
  Serial.print(".");
  Serial.print(year());
  Serial.println();
  debugV("%d:%d:%d", hour(), minute(), second());
}

void printDigits(int digits)
{
  // utility for digital clock display: prints preceding colon and leading 0
  Serial.print(":");
  if (digits < 10)
    Serial.print('0');
  Serial.print(digits);
}

/*-------- NTP code ----------*/

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

time_t getNtpTime()
{
  IPAddress ntpServerIP; // NTP server's ip address

  while (Udp.parsePacket() > 0) ; // discard any previously received packets
  Serial.println("Transmit NTP Request");
  // get a random server from the pool
  WiFi.hostByName(ntpServerName, ntpServerIP);
  Serial.print(ntpServerName);
  Serial.print(": ");
  Serial.println(ntpServerIP);
  sendNTPpacket(ntpServerIP);
  uint32_t beginWait = millis();
  while (millis() - beginWait < 1500) {
    int size = Udp.parsePacket();
    if (size >= NTP_PACKET_SIZE) {
      Serial.println("Receive NTP Response");
      Udp.read(packetBuffer, 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)packetBuffer[40] << 24;
      secsSince1900 |= (unsigned long)packetBuffer[41] << 16;
      secsSince1900 |= (unsigned long)packetBuffer[42] << 8;
      secsSince1900 |= (unsigned long)packetBuffer[43];
      return secsSince1900 - 2208988800UL + timeZone * SECS_PER_HOUR;
    }
  }
  Serial.println("No NTP Response :-(");
  return 0; // return 0 if unable to get the time
}

// send an NTP request to the time server at the given address
void sendNTPpacket(IPAddress &address)
{
  // set all bytes in the buffer to 0
  memset(packetBuffer, 0, NTP_PACKET_SIZE);
  // Initialize values needed to form NTP request
  // (see URL above for details on the packets)
  packetBuffer[0] = 0b11100011;   // LI, Version, Mode
  packetBuffer[1] = 0;     // Stratum, or type of clock
  packetBuffer[2] = 6;     // Polling Interval
  packetBuffer[3] = 0xEC;  // Peer Clock Precision
  // 8 bytes of zero for Root Delay & Root Dispersion
  packetBuffer[12] = 49;
  packetBuffer[13] = 0x4E;
  packetBuffer[14] = 49;
  packetBuffer[15] = 52;
  // all NTP fields have been given values, now
  // you can send a packet requesting a timestamp:
  Udp.beginPacket(address, 123); //NTP requests are to port 123
  Udp.write(packetBuffer, NTP_PACKET_SIZE);
  Udp.endPacket();
}

[Update 2019-04-25] After a few weeks monitoring it was time for final assembly. Since the wires from the LED radio clock carry 230Vac mains voltages, a little care is in order. I had a long-deceased TP-LINK TD-8610 modem whose plastic case seemed the right size:

TP-LINK 8610
I ripped out the electronics and just used the plastic case. Note the cable ties: white to provide strain relief for the wires and a small black one to prevent the relay board from flopping around in its case.


If you looked very carefully, you might spot the flash marks from the lightning strike (just to the left of the white cable tie) that killed the original TD-8610.
The relatively flat case lets me place the radio clock on top:

Somehow it did not look too out of place


There you have it, a clock radio that is synchronized to the Internet clock. With a bit of luck that Sony ICF-C205 Radio Clock might last another 20 years. Happy Trails.