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.