Adding WiFi to an alarm clock for the hearing impaired

Introduction: The ClearSounds SW200 (motto: “shakeup to wakeup”) is an $80 alarm clock designed for those of us with hearing loss. It has hearing impairment-friendly features such as the ability to flash lights, an adjustable-frequency tone, and a bed shaker.

This is an older version
This is an older version

I’ve found the bed shaker is really effective – even when I had perfect hearing, the loudest screeching alarm sometimes wouldn’t roust me, but any sort of movement or vibration wakes me up immediately.

They even sell devices that use such a shaker (which is just a DC motor with an offset weight – like your cell phone has but bigger) to alert the hearing-impaired of a fire. These devices cost a lot ($250) – much more than I’m willing to spend. After all, in intro to circuit analysis class, the topic of my filter design project was to isolate and recognize a smoke detector beeping using an RLC filter. But similar devices exist for much cheaper even, such as this night light ($8) which illuminates brightly if there’s a fire. I’ve actually had one of these gutted for awhile, waiting for the day I resumed my hobby.

But lets not get ahead of ourselves. In the previous project I posted about, I left some future-use functionality to be used with my clearsounds alarm clock.

Goal: Reverse engineer a ClearSounds SW200 enough to have its own circuitry used to power an ESP8266 and shake the attached bed shaker on some usable signal.

Details: Upon disassembling and poking around inside, I found the device has an internal transformer to make 9V DC, with an additional 7805 LDO to further step the voltage down to 5V for the brains of the clock. I managed to blow both the PNP transistors (type S8550) prodding around, but during the course of this, discovered how it works (or at least, the part I need):

The LDO and PNP switching circuits are highlighted
The LDO and PNP switching circuits are highlighted
schemeit-project-1
Shaker switching circuit, simplified

The brains of the alarm triggers a small surface-mount NPN transistor which then switches a larger PNP to give the vibrator motor about 9V. From this design I determined a second NPN circuit similar to the one above could be added for my purposes, and it would behave in an “OR” fashion, like this:

schemeit-project-2
I used a BC549 NPN

The ESP-01 module will drive that second switching circuit, and will accept packets describing the temporal pattern. For minor notifications, I decided on three positions for a short or long pulse, so that up to 14 distinct pulses could be feasibly generated. There is also a fire alarm state, where the unit will be on-1700ms-off-300ms for five minutes or until instructed to stop. This should allow enough diversity to permit my alarm clock of the house burning down, someone creeping about outside, or impending zombie apocalypse.

ESP-01 with a 3.3V LDO board
ESP-01 with a 3.3V LDO board

After getting it connected to power and confirming operation, I decided to add female jumper wires to ground, RX, TX, and GPIO0 in case reprogramming is needed in the future. The NPN’s base has a male jumper, so that the RX pin (doubling as GPIO3) can be connected before re-assembly.

Programming the device
Programming the device

In dealing with the ESP-01’s finnicky bootloader: It seems impossible to tie GPIO0 or GPIO2 to anything which might make it appear to be grounded, lest a mode other than our intended operation mode, be triggered. I also noticed pulling TX to ground with an LED caused the device to fail to start up (this may explain why the ESP-01’s TX light is connected to an LED via 3.3V). After the fact I found the internet-consensus is, pull-downs greater than 33k are OK. What I ended up doing was using the RX pin as GPIO3, since serial communication isn’t required.

Appearances can be deceiving - this alarm clock has an IP address.
Appearances can be deceiving – this alarm clock has an IP address.

The code:

/*  This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.

Description: software for an ESP-01 with pin 3 attached to the vibrate trigger of
  a clearsounds SW200 alarm clock. the server listens on port 42001 and
  packets are of the format: ###
  100 = short blip, nothing, nothing.
  121 = short (pause) long (pause) short
  9?? = fire condition, vibrate on-off-on-off for 5 minutes with
        temporal pattern 1700ms on, 300ms off
  x?? = reset fire condition */

#include <ESP8266WiFi.h>
#include <WiFiUdp.h>

const char* ssid = "thisismyssid";
const char* password = "hopefullynotthewordpassword";
WiFiUDP Udp;
int fireState = 0;
char packetBuffer[255];
const int outputPin = 3;

void pulseByTime(int pulseLen) {
  digitalWrite(outputPin,HIGH);
  delay(pulseLen);
  digitalWrite(outputPin,LOW);
  delay(300);
}

void setup() {
  pinMode(outputPin, OUTPUT);
  pulseByTime(200);  // a pulse indicates a successful startup
  WiFi.begin(ssid, password);
  WiFi.config(IPAddress(172,21,42,18),IPAddress(172,21,42,1),IPAddress(255,255,255,0)); // set a static IP
  while (WiFi.status() != WL_CONNECTED) delay(50); // wait for connection
  Udp.begin(42001);  // start listening
  pulseByTime(200);  // a second pulse indicates successful connection
}

void loop() {
  int packetSize = Udp.parsePacket();
  if (packetSize)
  {
    IPAddress remoteIp = Udp.remoteIP();
    int len = Udp.read(packetBuffer, 255);
    if (len > 0) packetBuffer[len] = 0;
    if (len == 3) {
      for (int pCount = 0; pCount<3; pCount++) {
        switch (packetBuffer[pCount]) {
          case '1':
            pulseByTime(300);
            break;
          case '2':
            pulseByTime(700);
            break;
          case '9':
            digitalWrite(outputPin,HIGH);
            fireState = millis();
            break;
          case 'x':
            digitalWrite(outputPin,LOW);
            fireState = 0;
            break;
        }
      }
    }
  }
  if (fireState>0) {
    if ((millis()-fireState)>300000) {
      fireState=0;
      digitalWrite(outputPin,LOW);
    } else if (((millis()-fireState)%2000)>1700) {
      digitalWrite(outputPin,LOW);
    } else digitalWrite(outputPin,HIGH);
  }
}