Litter tray TwitterBot - BeemoPoops!

The Internet of cat litter trays - TwitterTray

The world almost certainly doesn't need an internet-enabled litter tray that sends an amusing tweet every time my cat uses it.  But the world wasn't consulted, and got it anyway.  Follow Beemo the cat's poops at https://twitter.com/BeemoPoops.


Above: "You've put a Twitter-what on my litter tray now?!" - Beemo, the cat in the hat.

I've recently blogged about sending a tweet from an ESP8266 controller.  What if we hooked the ESP8266 up to some kind of sensor, woke it up, generated a random tweet and tweeted it?  Running any sort of electronic device for months-year(s) solely on battery power is a challenge in low-power electronic design which means judicious choice of passive components and utilising the deep sleep mode of our ESP8266.

You can skip to final result on Twitter here.

I wanted the tweets to be random (but not repeat the same one twice in a row), have an element of randomness with emojis and hashtags to give potential for unexpected humour and add variety.  The tweet structure I settled on is below:


Sending the first part of the text is somewhat straightforward and something I've covered in my previous post.  The text is chosen at random, using a random number generator whose seed is saved each time to allow new random numbers every time the ESP8266 is reset.  The slight complication is that we need to "percent encode" the text in a standard way.  For that purpose I wrote this function:

uint8_t encodeURL(char *dest, char *src)
{
  char *encoded = dest;
  char *hex = "0123456789ABCDEF";
  char *ptr = src;
  char c=1;
  uint8_t i_dest=0, i_src=0; // i is number of c

  while (c!='\0')
  {
    c = *ptr;

    if (c!='\0')
    {
      if( ('a' <= c && c <= 'z')
          || ('A' <= c && c <= 'Z')
          || ('0' <= c && c <= '9') )
          {
            *encoded++ = c; i_dest++;
          }
          else
          {
            *encoded++ = '%';
            *encoded++ = hex[c >> 4];
            *encoded++ = hex[c & 15];
            i_dest+=3;
          }
    }
    ptr++;
  }
  *encoded='\0'; //  Append a NULL as a courtesy, but don't increase length.
  return i_dest; // returns encoded length
}
The function, rather than returning a String, takes a pointer to a byte array, progresses through and encodes any non-alphanumeric characters.  Usefully, the length of the new encoded string is returned, which we can use as a start position to append more text.

Emojis

What tweet would be complete without a string of amusing emojis to inject a bit of life?  This took a bit of figuring out, but it is easy to add emojis programmatically to a text.  It turns out emojis are represented in unicode, which Twitter will recognise as such if we send the right sequence of bytes to represent the unicode and display the emoji appropriately.  First we need a list of unicode emojis, and the corresponding byte sequences - fortunately we can find such a list here; https://apps.timwhitlock.info/emoji/tables/unicode 

I've taken the trouble to pick out a list of fun emojis and tabulated them below.  Note that I've replicated the "poop" emoji a few times as I want it to be chosen more often than the others!  From the 2D char array, it is evident I have chosen only 4-byte emojis - others emojis (flags for example), are other lengths.  For now I'm happy with this constraint.
#define NUM_EMOJIS 34

char emoji[NUM_EMOJIS][4]={{0xF0, 0x9F, 0x92, 0xA9}, // poop - 0
                           {0xF0, 0x9F, 0x92, 0xA9}, // poop - 1
                           {0xF0, 0x9F, 0x92, 0xA9}, // poop - 2
                           {0xF0, 0x9F, 0x92, 0xA9}, // poop - 3
                           {0xF0, 0x9F, 0x98, 0x81}, // grinning face = \xF0\x9F\x98\x81  - 4
                           {0xF0, 0x9F, 0x98, 0x82}, // crying with laughter = \xF0\x9F\x98\x82 -5
                           {0xF0, 0x9F, 0x98, 0x86}, // tightly closed eyes = \xF0\x9F\x98\x86 - 6
                           {0xF0, 0x9F, 0x98, 0x8D}, // smiling face with heart-shaped eyes =  \xF0\x9F\x98\x8D - 7
                           {0xF0, 0x9F, 0x98, 0x9C}, // face with stuck-out tongue and winking eye = \xF0\x9F\x98\x9C - 8
                           {0xF0, 0x9F, 0x98, 0xBB}, // smiling cat face with heart-shaped eyes = \xF0\x9F\x98\xBB - 9
                           {0xF0, 0x9F, 0x98, 0xB9}, // cat face with tears of joy = \xF0\x9F\x98\xB9 - 10
                           {0xF0, 0x9F, 0x99, 0x8A}, // speak-no-evil monkey = \xF0\x9F\x99\x8A - 11
                           {0xF0, 0x9F, 0x99, 0x8B}, // person raising both hands in celebration = xF0\x9F\x99\x8B -12
                           {0xF0, 0x9F, 0x92, 0x96}, // sparkling heart - \xF0\x9F\x92\x96 - 13
                           {0xF0, 0x9F, 0x9A, 0xA8}, // police cars revolving light = \xF0\x9F\x9A\xA8
                           {0xF0, 0x9F, 0x9A, 0xBD}, // toilet = \xF0\x9F\x9A\xBD
                           {0xF0, 0x9F, 0x8C, 0x88}, // rainbow = \xF0\x9F\x8C\x88
                           {0xF0, 0x9F, 0x8C, 0xB7}, // tulip = \xF0\x9F\x8C\xB7  
                           {0xF0, 0x9F, 0x8D, 0x86}, // aubergine = \xF0\x9F\x8D\x86
                           {0xF0, 0x9F, 0x8D, 0x8C}, // banana = \xF0\x9F\x8D\x8C
                           {0xF0, 0x9F, 0x8D, 0x91}, // peach = \xF0\x9F\x8D\x91
                           {0xF0, 0x9F, 0x8D, 0x92}, // cherries = \xF0\x9F\x8D\x92
                           {0xF0, 0x9F, 0x8E, 0x89}, // party popper = \xF0\x9F\x8E\x89
                           {0xF0, 0x9F, 0x8E, 0x8A}, // confetti ball =  \xF0\x9F\x8E\x8A
                           {0xF0, 0x9F, 0x90, 0xB1}, // cat face = \xF0\x9F\x90\xB1
                           {0xF0, 0x9F, 0x91, 0x8D}, // thumbs up sign = \xF0\x9F\x91\x8D
                           {0xF0, 0x9F, 0x92, 0xA6}, // splashing sweat symbol = \xF0\x9F\x92\xA6
                           {0xF0, 0x9F, 0x92, 0xA8}, // dash symbol = \xF0\x9F\x92\xA8
                           {0xF0, 0x9F, 0x98, 0x8E}, // smiling face with sunglasses = \xF0\x9F\x98\x8E
                           {0xF0, 0x9F, 0x98, 0x80}, // grinning face = \xF0\x9F\x98\x80
                           {0xF0, 0x9F, 0x98, 0x87}, // halo face = \xF0\x9F\x98\x87 
                           {0xF0, 0x9F, 0x98, 0x90}, // neutral face = \xF0\x9F\x98\x90
                           {0xF0, 0x9F, 0x98, 0x97}, // kissing face = \xF0\x9F\x98\x97
                           {0xF0, 0x9F, 0x8C, 0x8D}, // earth globe europe-africa = \xF0\x9F\x8C\x8D
                          }; 

These are then chosen at random in the code that assembles the final tweet.  We don't need to percent-encode them, so they are added to the end of the main text of the tweet

Adding hashtags and assembling the final tweet

Adding hashtags is easy - we just have an array of hashtags, and choose one at random.  I want them to not be repeated, so I have to keep a list of hashtag indexes that I've already chosen.  Finally because of the '#' character and the spaces between the hashtags, this portion of the tweet must be percent-encoded.
void SendTweet(uint8_t tweet_index) // pass on index in char *tweets[NUM_TWEETS] array
{
  char final_tweet[MAX_TWEET_LEN];
  uint8_t len;

  len = encodeURL(&final_tweet[0],&tweets[tweet_index][0]); // encode tweet_index straight into final_tweet
 
// Add emojis ///////////////////////////////////////

   uint8_t n_emojis = 3 + nextrandom() % 3;
   for (uint8_t j = 0; j<n_emojis; j++)
   {
    uint8_t id = nextrandom() % NUM_EMOJIS;
    memcpy(&final_tweet[len], &emoji[id][0], 4);
    len+=4;
   }

// Add HASHTAGS ///////////////////////////////

  // Set up unique hashtag list
  uint8_t num_tags = SEND_HASHTAGS_MIN + (nextrandom()% (1+ SEND_HASHTAGS_MAX - SEND_HASHTAGS_MIN));
  uint8_t hash_list[SEND_HASHTAGS_MAX];

  // Generate unique hashtag list
  uint8_t isnovel, temp_tag;

  // manual add a leading space
  final_tweet[len++] = '%'; final_tweet[len++] = '2'; final_tweet[len++] = '0';

  for (uint8_t i=0; i<num_tags;i++)
  {
    isnovel=0; // Entry condition for while loop

    while (isnovel==0) // If temp_tag is found in previous list, then try another one.
    {
      temp_tag = nextrandom()% NUM_HASHTAGS; // 0 --> NUM_HASHTAGS-1
      isnovel=1; // Assume is novel, unless found in list.
      
      for (uint8_t j=0;j<i;j++) // go from 0 to i-1 (previous hashtags and check novelty)
      {
        if (hash_list[j]==temp_tag) isnovel=0; // found in list so not novel.
      } // end for
    } // end while
    hash_list[i] = temp_tag; // commit temp_tag to list.

    // Add to encoded_text:
    len+= encodeURL(&final_tweet[len], &hashtags[temp_tag][0]); 
    if (i!=num_tags-1)  // Don't add a space character if on last hash_tag
    {
      final_tweet[len++] = '%';   // manual add an encoded space
      final_tweet[len++] = '2'; 
      final_tweet[len++] = '0'; 
    }
  }

 Serial.print("Encoded tweet:"); Serial.println(final_tweet);
 String TweetString = String(final_tweet);

if (client.connect("184.106.153.149", 80))
{
  client.print("GET /apps/thingtweet/1/statuses/update?key=" + API + "&status=" + TweetString + " HTTP/1.1\r\n");
  client.print("Host: api.thingspeak.com\r\n"); 
  client.print("Accept: */*\r\n"); 
  client.print("User-Agent: Mozilla/4.0 (compatible; esp8266 Lua; Windows NT 5.1)\r\n");
  client.print("\r\n");
}
else Serial.println("Could not connect to ThingSpeak!");
}

Finally, after sending the tweet, the device goes into a deep.sleep mode which consumes about 20 uA.  This is done trivially using the Arduino IDE and libraries with a single line of code:
ESP.deepSleep(0);

The circuit

In terms of hardware, the ESP8266 is kept in a deep-sleep mode to conserve battery power, and wakes up when the PIR sensor provides a high pulse - the tweet is sent and the ESP8266 returns to a deep-sleep, low-power mode.  We want the high pulse of the sensor to provide a short (10s ms) pulse that pulls the RESET pin of the ESP8266 low and then lets it return high.    We don't want the continued high signal of the PIR detection signal to continually hold the ESP8266 in a RESET state, hence the wake-up circuity consists of an RC filter which turns the leading edge of the PIR high pulse into a short pulse, and a transistor that inverts the HIGH signal to the desired LOW signal on the RESET pin.  A pull-up resistor is used to hold the ESP8266 RESET pin high otherwise.


The wake-up circuitry is shown above.  I used EasyEDA to design the circuit.  It is free to use and allows simulations to be performed, without limit upon the number.  It is trivial therefore to play around with component values and examine the effect on voltage, RC decay and current.  One issue I had was inserting a simulation-active transistor into the circuit, but it worked well otherwise.

Low power 
The ESP-01S board doesn't have a voltage regulator on-board, meaning we have to supply it with 3.3 volts ourselves.  The advantage of this is that we don't waste energy through a linear regulator, but means we have to be careful to supply the correct voltage.  Infact 2.3-2.5V has been measured as a stable lower voltage power level and even up to 4.7V, though clearly that is not recommended for long periods of time. The PIR sensor is powered by 5V, and uses an LDO to drop to 3.3V volts.  It turns out that it is possible to by-pass the LDO and save ourselves a bit of otherwise wasted power by connecting 3.3V directly to pin  1, see here.  So to provide power to the entire device, I'm using 2 x 1.5V AA batteries to supply 3.0 volts, which can be used without further regulation to the power-bypassed PIR sensor, and the ESP-01S. 

In deep-sleep mode:
ESP-01S : 20 uA
PIR sensor: 50 uA

In active mode:
ESP-01S: 50-100 mA (estimate)
PIR sensor: 200 uA


I soldered it all together, with sockets for the PIR module (left) and the ESP8266 (right), for easy removal.  I put in 2 LEDs for demonstration/debugging purposes.  The blue LED is connected to the PIR SIGNAL pin and goes high when my hand movement is detected.  The green LED is controlled by the ESP8266 and flashes as it attempts to connect to WiFi, is constant when sending the tweet, and goes out when the module goes into deep sleep mode.  Looks like sensed movement correctly resets the chip every time to send out a new tweet.

The Installation

Now to put my device above the cat's litter tray and see if it all works in situ!  My cat is lucky enough to have a litter tray that is enclosed in a little house, and conveniently, I can install the battery pack and circuit board in a compartment above the litter tray that ordinarily holds a poop-scoop.



I noticed that when changing the litter tray, I would sometimes set off the sensor erroneously.  Therefore I put some black insulation tape over the half of the sensor lens that points towards the door of the litter tray, and this low-tech solution seems to work well.

And that's it!  If you want to follow Beemo's poop cuteness, you can follow this device here: https://twitter.com/BeemoPoops

Comments

Popular posts from this blog

Getting started with the Pro Micro Arduino Board

Arduino and Raspberry Pi serial communciation