Sunday, March 14, 2021

Easy Multitasking on the Arduino - Finite State Machines

Unlike a PC, microcontrollers like the Arduino or ESP32 are not able to run multiple programs in parallel. But that doesn't mean that you wouldn't be able to run multiple tasks in parallel. This article shows how to achieve this in a simple way.

The Concept

The main idea is that most code pieces do not fully require the full processor power. Therefore we can alternate and let the processor work on each piece of code for a limited amount of time (typically less than a millisecond). In this case, each piece of code needs to remember where it stopped, so it knows where to continue the next time. This requires to define (and to number) all the possible "states" of the code, so it can memorize this "state number".

This approach is called a Finite State Machine (FSM). If this sounds complex to you, let me assure you: it isn't! Probably, you have used the "Blink" example from the Arduino library before (I always use this to test any new Arduino board). Under "02.Digital" is another example "BlinkWithoutDelay". Please have a look at that one. The difference between these two examples explains basically all the ingredients that we need to understand a FSM - and to do multitasking.

A Simple Example

Before we build a slightly more complex example below, let's study the main features by comparing slightly modified versions of the two blink sketches.

Here is the main loop (which is the same for both):

void loop() {
  checkButton();
  blink();
}

Let us assume that "checkButton()" is a subroutine that checks if a button has been pressed - it does not matter for our purposes how this is done in detail, and what the code will do else. It only matters here, that we want to ensure that the code will be able recognize if a user pushes a button, which requires it to be called in sufficiently small time intervals. For the moment, we just create an empty subroutine.

void checkButton() {
}

The first (simple and inefficient) version of the blink sketch is this:

void blink() {
  digitalWrite(LED_BUILTIN, HIGH);   // turn the LED on
  delay(1000);                       // wait for a second
  digitalWrite(LED_BUILTIN, LOW);    // turn the LED off
  delay(1000);                       // wait for a second
}

In this case, the main loop is doing the following: it enters "checkButton()" to quickly check if the button was pressed (this is extremely fast - much faster than 1 millisecond), and then it enters "blink()". In "blink()", it turns on the LED, waits one second, turns off the LED, and waits another second - so two seconds are spent in each call of "blink()". Then the loop repeats and it checks the button again.

The obvious problem is that two seconds pass in between subsequent readings of the button. In many cases, when a user presses the button for a typical time of 1/2 second, the code will not recognize this, since it may just be waiting in "blink()" inside one of the two "delay(1000)" statements. 

So, the culprit is the "delay()" statement - which essentially prevents our Arduino sketch from doing anything during this time. 

A better, more efficient code would memorize whether the LED is on or off, and also the time when it was set into this state. Then it would exit the blink() subroutine, so that the main loop() could go back and call checkButton() again after a much shorter time. Whenever blink() is called again, it checks if one second has already expired. If not, it exits - and if yes, it turns the LED on or off (depending on its previous state) and, again, memorizes the new on/off state and the time when this happened. Together, the information whether the LED is on or off plus the time information define the state of the updated blink() subroutine. This is what a Finite State Machine (FSM) is about.

A FSM can be in a certain (finite) number of possible states. The state changes whenever it receives input (examples are signals from other code pieces or here, from a timer). The new state is defined based on the type of input and on the current state. 

Let's implement everything that we stated above in a new, FSM version of the blink() code. It is almost equal to the "BlinkWithoutDelay" example - just slightly re-organized, so that the FSM feature becomes more clear. 

void blink() {
  static byte state = 0;               // state  0:LED-off  1:LED-on
  static unsigned
long previousTime = 0;  // time of the last change
  const unsigned long delayTime = 1000;   // blink-time in millisec
                                         
  
if ((millis()-previousTime) > delayTime) { // one sec expired
    previousTime = millis();            // store current time 
    if (state == 0) {                   // if LED was off
      state = 1;                        //   -> new state is "on" 
      
digitalWrite(LED_BUILTIN, HIGH);  //   -> turn on LED
    } else {                            // if LED was off
      state = 0;                        //   -> new state is "off" 
      digitalWrite(LED_BUILTIN, LOW);   //   -> turn off LED
  }
}

Let's understand this, line by line:

First we define the variable "state" in which we store, well, the current state of the LED - using the type "byte". Then we define the variable "previousTime" to store the last time that the LED was turned on/off - using the type "unsigned long ". Both of these variables are declared as "static", which means that the Arduino will remember their values between subsequent calls of the blink() code (otherwise they would be initialized as zero in every new call).

In the third line we define a constant "delayTime" which we set to 1000. This is the time (in milliseconds) between subsequent on/off changes as used in the first simple and inefficient blink() version. By declaring this as a constant, it will be easier to change this in the future, if you ever decide to change the blink frequency. 

Then we check if one second has passed: This is the case when the difference of the current time and the previous time when the state changed is larger than 1000 ms (the "delayTime"). If not, the code is finished and returns back to the "loop()".
If the code finds that 1000 ms have passed, it will change the state of the LED. Therefore, first, it stores the time when this happened, by updating the variable "previousTime" and setting it to the current time. Then, if the current state of the LED is "off", we set the state variable to "on", and turn on the LED. If the current state of the LED is "on", we set the state variable to "off", and turn off the LED. 

In either case, whether the state of the LED was changed or not, the time spent in the blink routine is very small (much less than one millisecond). Therefore, the loop() code can quickly go back and check the status of the button, so that no push of the button will go unnoticed. Which is exactly what we wanted!

The essential lesson is, not to use any "blocking" code This includes "delay()" statements and also "while" loops that wait for events to occur. If it is necessary for your code to wait for something to happen, then remember (i.e. store) the current state, exit the subroutine, and check again the next time the subroutine is called.

A General Rule: Keep Your Code Local

This means that all operations of a given component (like an LED) are done by a single subroutine which has a state variable to store the current state. If the component can affected by external code, the subroutine also needs an input variable that specifies the request to change the state.

Let's say we have an LED that can be turned on and off. When it's asked to turn on, the LED should fade from zero to the maximum brightness and stay there until requested to turn off. For this case we provide a subroutine "operateLed" with an input variable "input" and a state variable "state".

The "input" variable can have the following values:

  • 0 (means: continue to operate in the current state)
  • 1 (means: turn on the LED)
  • 2 (Means: turn off the LED)

The "state" can either be:

  • 0 (off)
  • 1 (currently ramping up from zero up to max brightness)
  • 2 sitting at max. brightness

The detailed code for "operateLed()" is listed further below as part of ...

A More Interesting and Complex Example

Let's operate a device that has a switch (or a momentary push button), one LED, two servos, and a sound player. The device is supposed to do the following. At startup (when the switch/button is open), the LED is off, the servos sit at their initial angles "minAngle", and no sound is playing.

  • When the switch/button changes from off to on:
    • the servos start to rotate from "minAngle" to "maxAngle" (and stop when they reached the latter)
    • the LED code waits a little, then slowly fades on
    • the sound code waits a little, then starts the sound player
  • The switch/button is ignored for 100-200 ms to avoid bouncing
  • When the switch/button changes from on to off:
    • the servos start to rotate from "maxAngle" back to "minAngle"
    • the LED code turns the LED off
    • the sound code checks if the sound is still playing, and, if yes, it stops.
  • The switch/button is ignored for 100-200 ms to avoid bouncing.
This is a precise, concise, and complete description of the task, based on which we can write the code. The four bullets above correspond to the four different states in which the "checkButton()" can possibly be.

The complete code is provided in seven pieces,

  • a list of constants in which parameters are stored, plus include statements of libraries used,
  • the setup() code in which the Arduino I/O pins are declared,
  • the loop() which is the main program that is executed continuously,
  • the subroutine checkButton() which reads the button status and initiates the requested responses,
  • the subroutine operateLed() which operates the LED,
  • the subroutine operateServos() which operates the servos,
  • the subroutine operateSound() which (you guessed it!) operates the sound output.
Tu run the code, just copy, paste, and append the "code" pieces which follow.

Parameters and Libraries

In the header of the code we define constants that may have to be tuned later plus the pin numbers where the devices are connected.

/*
 * Arduino code for a device with 
 *     push button, LED, 2 servos, sound player
 * - for Arduino Uno, Nano, ProMini
 */

// -----------------------------------------------------------------
// --- constants
//
// --- The angles where the servos start and stop
//     These could be different for both servos
const byte minAngle[2] = {0,0};       // servo start angles
const byte maxAngle[2] = {80, 80};    // servo stop angles
//
// --- delay times for the LED to turn on and the sound to start
//     after the button was pushed
const int ledDelay = 500;  // delay time (in ms) for the LED turn on
const int soundDelay = 450;   // delay time (in ms) for the sound
//
// -----------------------------------------------------------------
// --- pins and libraries
//
// --- button (connected to this pin and GND
const byte pinButton = 2; 
//
// --- LED - (connected to this pin and, via a resistor) to GND
const byte pinLed = 3;     // must be a PWM pin like 3,5,6,9
//
// --- servos
#include <Servo.h>
Servo myServo[2];
const byte pinServo[2] = {9, 10}; 
//
// --- plus variables/pins for the sound player
//     [these are omitted here for brevity]


In the setup() code, the Arduino pins are enabled as required. The button is connected between the Arduino pin and GND. To ensure that the logic level of the pin is well-defined while the pin is open, we use the built-in pull-up resistor.

void setup() {
  Serial.begin(9600);
  pinMode(pinButton, INPUT_PULLUP);     // push button 
  pinMode(pinLed, OUTPUT);              // LED pin
  digitalWrite(pinLed, LOW);            // turn off LED
  for (byte i = 0; i < 2; i++) {     
    myServo[i].attach(pinServo[i]);     // two servos
    myServo[i].write(minAngle[i]);         // rotate to minimum angle
  }
  // --- plus initialization for sound player
}


The loop() code is executed and repeated continuously. It is very simple and consists of four calls. The checkButton() routine has no input - it always does the same: checking for changes of the button status. The other routines each have an input argument. In the loop() code, this input argument is always zero, meaning: "just continue doing what you did".

void loop() {
  checkButton();
  operateLed(0);
  operateServos(0);
  operateSound(0);
}


Now we look at the individual device-specific subroutines, starting with the code to check changes of the button status and to initiate actions. 

// --- check changes of the status of the button
//
//  possible states
//       0:  the button is open - check if the button is closed
//       1:  the button was closed - wait a few milliseconds
//           to ignore possible bounces - then go to state 2
//       2:  the button is closed - wait for button to open
//       3:  button was opened - wait few ms to debounce
// 
void 
checkButton() {
  static byte state = 0;
  static unsigned long previousTime = 0;

  if (state == 0) {
    if (digitalRead(pinButton) == LOW) {    // button was pressed
      operateLed(1);                 // turn on the LED
      operateServos(1);              // rotate servos
      operateSound(1);               // start sound 
      state = 1;                     // change the state
      previousTime = millis();       // store current time
    }
  } else if (state == 1) {
    if ( (millis() - previousTime) > 100) {    // wait 100ms
      state = 2;                               // change state 
    }
  } else if (state == 2) {
    if (digitalRead(pinButton) == HIGH) {   // button was released
      operateLed(2);
      operateServos(2);
      operateSound(2);
      state = 3;                     // change state 
      previousTime = millis();       // store current time
    }

  } else if (state == 3) {
    if ( (millis() - previousTime) > 100) {    // wait 100ms
      state = 0;                               // change state 
    }
  }
}

The LED is operated by the subroutine "operateLed()". It has an input argument for which the meaning of the values are described in the header of the routine.

// --- operate the LED
//     input   0 continue
//             1 turn LED on
//             2 turn LED off
//   
// 
  possible states
//       0:  LED is off
//       1:  LED has received the "turn-on" signal but waits
//       2:  LED is fading on
//       3:  LED is on
//
void operateLed(byte input) {
  static byte state = 0;                   // store the state
  static unsigned long previousTime = 0;   // store the time
  static byte step = 0;                    // steps when fading LED 

  // --- deal with external inputs
  if (input == 1 && state == 0) {     // turn on signal - if LED:off
    state = 1;                        // start turn-on procedure 
    previousTime = millis();          // store current time 
  } else if (input == 2) {            // turn off signal

    digitalWrite(pinLed, LOW);        // turn off LED
    state = 0;                        // set state to 0:off
  }

  // --- timer-based state changes
  if (state == 1) {                   // turn-on was requested
    if ( (millis() - previousTime) > ledDelay) {   // wait
      state = 2;                      // set state to 2:fade-on 
      step = 0;                       // reset step counter
      previousTime = millis();        // store current time 
    }
  } else 
if (state == 2) {            // ramp-up
    if (step == 255) {
      state = 3;
    } else if 
( (millis()-previousTime) > 2){   //
      previousTime = millis();
      step++;
      analogWrite(pinLed,step); 
    }
  }
}

The servos are operated by the subroutine "operateServos()". It has an input argument as described in the header.

// --- operate the servos
//     input   0 continue
//             1 rotate to max angle
//             2 rotate back to min angle
//   
//   possible states
//       0:  servos sit at min angle
//       1:  servos rotate from min to max 
//       2:  servos sit at max angle
//       3:  servos rotate from max to min
//
//
void operateServos(byte input) {
  static byte state = 0;                   // store the state
  static unsigned long previousTime = 0;   // store the time 
  //     store current servo angles
  static byte currentAngle[2] = {minAngle[0],minAngle[1]}; 


  // --- deal with external inputs
  if (input==1 && (state==0 || state==3)) { // go to max angle
    state = 1;               // set state to 1:start rotate to max
    previousTime = millis();          // store current time
  } else if (input==2 && (state==1 || state==2)) { // go to min angle
    state = 3;               // set state to 3:start rotate to min
    previousTime = millis();          // store current time 
  }

  // --- timer-based state changes (rotation of servos)
  if (state == 1) {                   // rotate from min to max
    if ( (millis() - previousTime) > 2) {      // wait
      previousTime = millis();                 // store current time 
      for (byte i = 0; i < 2; i++) {           // do for both servos
        if (currentAngle[i] < maxAngle[i]) {   // if not at max angle
          currentAngle[i]++;                   // increase angle
          myServo[i].write(currentAngle[i]);   // rotate to new angle
        }
      }
      // if both servos reached their max angles -> change to state 2
      if (currentAngle[0]==maxAngle[0] &&
        currentAngle[1]==maxAngle[1]) {  
        state = 2;                    // set state to 2:at max angle
      }
    }
  } else if (state == 3) {                //  rotate from max to min
     if ( (millis() - previousTime) > 2) {     // wait
      previousTime = millis();                 // store current time 
      for (byte i = 0; i < 2; i++) {           // do for both servos
        if (currentAngle[i] > minAngle[i]) {   // if not at min angle
          currentAngle[i]--;                   // decrease angle
          myServo[i].write(currentAngle[i]);   // rotate to new angle
        }
      }
      // if both servos reached their min angles -> change to state 0
      if (currentAngle[0]==minAngle[0] &&
          currentAngle[1]==minAngle[1]) {  
        state = 2;                    // set state to 0:at min angle
      }
    }
  }
}

Finally, the routine to operate the sound player. I am just giving the skeleton - the user needs to add the specific commands for their sound player. Code for the DFPlayerMini (plus a detailed description of the setup) can be found in one of my older blog posts.

// --- operate the sound player
//     input   0 continue
//             1 start sound
//             2 stop sound
//   
//   possible states
//       0:  sound is off
//       1:  waiting for sound to start after a short delay
//       2:  sound has started
//
//
void operateSound(byte input) {
  static byte state = 0;                        // store the state
  static unsigned long previousTime = 0;        // store the time

  // --- deal with external inputs
  if (input == 1 && state == 0) {     // start signal - if:off
    state = 1;                        // go into waiting state 
    previousTime = millis();          // store current time 
  } else if (input == 2) {            // stop signal - if:playing
    // your-code-to-stop-playing();   // stop playing
    state = 0;                        // set state to 0:off
  }

  // --- timer-based state changes
  if (state == 1) {                     // waiting for sound to start
    if ( (millis()-previousTime) > soundDelay) {   // wait
      state = 2;                        // set state to 2:playing
      // your-code-to-start-playing();  // play sound
    }
  }
}

This is it: A complete and detailed example of how to do multitasking with the Arduino using the concept of Finite State Machines. 
It works really well in cases like this, where most code pieces are idle most of the time, only waiting for external inputs (button presses, signals from a motion sensor, timers, etc.). 
If a subroutine is doing extended, complex calculations which each last long (i.e. longer than the time interval at which one e.g. wants to check a button status), then one needs a to look for other solutions (like breaking the calculations into smaller units, or using timer interrupts to execute the button code).

Outlook: Possible Improvements

Most of the above could be coded slightly more efficient, if one understands the concept of a matrix. In the FSM, the new state is uniquely defined from the current state and the input. This can be expressed with a (2-dimensional) matrix, where e.g. the rows (i.e. the first index) correspond to the inputs, the columns (i.e. the second index) to the current state, and the element of the matrix at this position corresponds to the new state.
Here is a rough code sketch using this idea, for a device with four possible states (0-3), and three possible inputs (0-2). The matrix for the FSM is called "fsmMatrix".

void operateMyDevice(byte input) {
  static byte currentState = 0;      // store the current state

  byte newState = 0;                 // store the new state

  // --- response matrix for 3 inputs (i.e. 3 rows 0-2)
  //     and 4 states (i.e. 4 columns 0-3)
  const byte fsmMatrix[3][4] = {   
         {0,1,2,3},            // responses for input==0
         {3,3,1,1},            // responses for input==1
         {0,0,0,0}             // responses for input==2
  };
  newState = fsmMatrix[input][currentState];

  // possible actions, depending on the current state,
  //         the input, and/or the new state
  // further actions (including state changes) may also
  //               be based on timers

  currentState = newState; 
}

Some different cases:

  • If the code is called with "input" zero, the first row of the matrix is evaluated to compute the new state. The first row contains {0, 1, 2, 3}. Since the n-th element is equal to "n", the new state will always be equal to the current state - so the state does not change. (This is the same as what we did in the routines above, where a zero input meant to continue operating in the same state).
  • If the code is called with "input" of one
    • if the current state was zero or one, the new state will become "3"
    • if the current state was two or three, the new state will become "1"
  • if the code is called with input "2", the new state will always be "0" (as the turn-off in the operateLed() code above)

I've been using the concept of FSMs (somehow) for quite some time. But, honestly, never as consequent as described in the example codes presented here. That means, I will the first one to benefit from writing this tutorial for my future code projects. And I hope that one or the other "out there" finds this helpful too!

Wednesday, March 10, 2021

LilyGo T-Watch 2020 goes Back to The Future



The LilyGo T-Watch 2020 hit the market in May 2020. A programmable watch with reasonably small size (looking somehow like the Apple watch), based on the ESP32 processor with an accelerometer, plus Wifi and Bluetooth connectivity at a price of only $30-$40.

When I bought mine in December 2020, I thought I was already late to the party. But then, I started to search the web for great watch faces for the T_Watch and I did not find much. So, I decided to make my own, and share this. Being a fan of the "Back to the Future" trilogy, in which the topic of "time" is omnipresent, I decided to make a Back-to-the-Future fun watch - and to share this.

This watch is not a smart watch! It does neither use the Wifi nor Bluetooth connectivity of the watch (I wouldn't know what I would use this for anyway). This watch will just be fun to look at, and without WiFi and Bluetooth, but with a good power management, the watch will be able to run three or four days without charging.

T-Watch 2020 with Back-to-the-Future Time Circuits watch face

Setting up the programming environment

The very first step is to install and set up the Arduino IDE. I have described this in detail in another blog post: Lilygo TTGO T-Watch 2020: Getting Started / The Software Framework.


References

While I did not find many great watches, I found plenty of good information and example codes that helped me to understand specific details of the watch. The power management, in particular, is very important, in order to get a watch that does not have to be charged every day. Here I'm listing some references that were valuable and from which I benefited a lot when writing my code.

  • The SimpleWatch example that comes with the TTGO T-Watch library
  • A series of articles at diyprojects.io (also check the links to their other articles)
  • The code for "aGoodWatch" from Alex Goodyear
  • The article and code at instructables.com by Dan Geiger
  • An interesting "Wordclock" by instructable.com user ArduinoAndy

Requirements

As mentioned above, I wanted this to be a fun watch that can be used in every day life, which means that it needs a very efficient power management. Therefore, in this project I am not using the WiFi and Bluetooth which helps a lot to save power. It allows to reduce the CPU frequency of the watch (from nominal 80MHz out of a max of 240MHz) down to 30MHz. The graphics are all programmed using the TFT_eSPI library. In contrast to many common example codes that have tiny buttons in their settings menus, the Back-to-the-Future watch has large buttons which can easily accessed. 


Features

My Back-to-the-Future watch code features five different watchfaces, a stop watch, and a settings menu with four screens (two to customize the display, and two to set time and date). The different categories can be thought of as three rows: The middle one has the watch faces, to top one the stopwatch, and the bottom row the settings menu.
The watch can be in three states: 
  1.  Full operation with the display on 
  2.  After a few seconds of full operation, it turns to light sleep from which it can be quickly awakened. This is done either by a press on the button at the top right - a quick(!) double tap - or by rapidly moving the watch upwards and rotating it towards you.
  3. To save power (e.g. over night) the watch can be set to deep sleep, by pressing the button for 5 seconds. It takes another (approx. 2 sec) press of this button to wake it up, which takes about 5 seconds.
In a very bright environment, you can use a double tap on the screen to get the maximum screen brightness. The next time when the watch returns from light sleep, the brightness is back at the normal value.

Watchfaces

The watchfaces, include a fluxing flux capacitor, the time circuits (with the current date/time in the middle row, plus typical movie dates in the top/bottom rows), the speedometer plus the plutonium gauges, the SID spectrometer, and the entrance sign to the Mall (which randomly appears as either "Twin Pines Mall" or "Lone Pine Mall"). 


T-Watch 2020 with Back-to-the-Future watch faces

T-Watch 2020 with Back-to-the-Future watch faces

The user can cycle through those with horizontal swipes. There are three different modes of operation: Whenever the watch returns from light sleep it either presents you with a random face (a: random), the next face (b: cycle), or the same face that was displayed before it went to sleep (c: "fixed").

Stopwatch

If you swipe to the top row, you get the stopwatch, which is modeled after the one Doc Brown uses in his experiment with Einstein (except for the display color, since yellow is much brighter than red).


Menu with Settings

When, starting from the watchfaces, you swipe to the bottom row, you get an "entry" screen: "Enter the Settings - swipe" from which you can swipe into the settings. The additional entry screen prevents you from erroneously entering the actual settings and from unwanted changes of your setup.

Swiping to the right, you get to part one of the display settings where you can select 
  • how the watchfaces are diplayed (random, cycle, fixed - as described above) 
  • the format in which the time is displayed: 12h, 12h plus seconds, or 24h format
  • if the stepcounter is shown in the top left corner (on/off)
  • if the remaining battery percentage is dislayed in the top right corner (on/off) 


On part two of the display settings you can select
  • how long the watch stays on: 7 sec, 12 sec, or 20 sec (I prefer 7 sec which is long enough for me to read the time. I used 20 sec to make the video, without the watch turning off all the time. I also use the 20 sec setting if I just enjoy looking at the watch...)
  • three levels of brightness (low, med, high)
  • if you want the display to dim at night (between 10pm and 7am).
On the next two screens, you can set the time and date. Any changes that are made with the "+" and "-" buttons are only stored when the "SET TIME" and "SET DATE" buttons are pressed.  


Swiping to the row above brings you back to the watchfaces.

Installing the Code

The code can be obtained from my github repository https://github.com/mawob/bttfWatch.
From the green "Code" pulldown menu, select "Download ZiP"


Unzipping the file creates a folder "bttfWatch-main" which has all the relevant files. Rename the folder from "bttfWatch-main" to "bttfWatch" and move this into the Sketchbook folder of your Arduino IDE. Then open it from the File menu, connect your watch, upload, and enjoy your new watchface!
 


Monday, March 8, 2021

Lilygo TTGO T-Watch 2020: Getting Started / The Software Framework


Since quite a few years, I liked the idea of a programmable watch. So far, however, all programmable watches were in a price range beyond what I was willing to pay. Then, ten months ago, in May 2020 the company LilyGo released the T-Watch-2020, which you can get at around $30-$40. This is a game changer!

The T-Watch 2020 is the successor of their 2019 model (which was 20 mm thick) with a width of only 13 mm (= 1/2"), and it somehow resembles the shape of the Apple watch. At this price, of course, you don't expect it to be a competitor for the Apple Watch. But you will find that it's really usable in everyday life. And you can have a lot a fun with it, whether you plan to write your own code to run on the watch, or if you just plan to install other people's code (below, I am offering code for this purpose).

In these instructions, I show you how to set up the programming framework and how to run an example program on the T-Watch.


The Watch

 I ordered my watch at Banggood.com, and this is what I found in the package.


The bottom, with the open lid that covers the micro USB connector.


Removing the bottom plate gives a view on the 3.7V 380mAh battery.


And removing the battery shows some of the electronics.


An Arduino on your Wrist

The heart of the T-Watch is the ESP32 microcontroller. This is kind of a (much!!) improved Arduino. This means that it can be programmed via the Arduino IDE and if you have any experience in programming the Arduino, you can directly apply this to the coding of the T-Watch. Most of your Arduino sketches will also run on it. As compared to the Arduino. the ESP32 is much faster (up to 240MHz), has much more memory (16MB of flash memory), plus it has WiFi and bluethooth connectivity built in. In the T-Watch, the ESP32 is connected to an accelerometer, a 240x240 touchscreen, and a 3.7V Li-Ion battery with a capacity of 380mAh. Some of the simple T-Watch codes out there will empty this battery in a few hours, while some (with better power management) can run for a few days without charging.

Setting up the Programming Environment

The programming environment is set up in four small steps.

1. Installing the Arduino IDE

If you have programmed an Arduino before, you already have this, and you can skip to step 3. 
You download the Arduino IDE at https://www.arduino.cc/en/software using the link on the right side for your operating system (Linux, Windows, or Mac) and follow the simple instructions at https://www.arduino.cc/en/Guide.

2. Creating the Sketchbook Folder

After installing the IDE, create a folder "Sketchbook" on your computer. Inside the "Sketchbook" folder, create another folder "libraries" (which you will need later in step 4). Then you start the Arduino IDE, and in the File pull-down menu under Preferences you enter the location of your Sketchbook folder in the box: "Sketchbook location".

 


3. Adding the ESP32 Board in the Board Manager

Initially, the Arduino IDE only knows the details of all Arduino boards, but not the ESP32. Still in the "Preferences", insert the address "https://dl.espressif.com/dl/package_esp32_index.json" (as in the image above), and press "O.k.".
From the Tools menu, hover over the Boards item, and click on the "Boards Manager".


In the search box on the top, search for "ESP32". The pull down menu offers different versions. Pick the latest one and click "Install".


When the ESP32 board manager is installed, click "close".


Now that the information for the ESP32 board in your watch is installed, let's select it. Under Tools and  Board you find a new category "ESP32 Arduino". In there, scroll down until you find "TTGO T-Watch" and select this.


4. Getting the The TTGO T-Watch Library 

Now we get the library that supports the T-Watch from here: https://github.com/Xinyuan-LilyGO/TTGO_TWatch_Library


A click on the green "code" button give the option to "Download the .zip [file]". That's what we do - and we store the .zip file in the "libraries" folder that we created inside the Sketchbook folder, in step 2. To make it available to the Arduino IDE, we enter the Sketch menu, and under Include Library select Add ZIP Library.


In the following dialogue we select the .zip file that we stored in the "libraries" folder.

Running the First Example: SimpleWatch

Now that the programming framework is set up, we can upload the first example code onto the T-Watch. From the File menu, select Examples, then go all the way down to TTGO TWatch Library, and under LVGL, select the SimpleWatch example.


The SimpleWatch example code opens (probably in a new window). At the top there are four tabs. Click on the "config.h" tab and uncomment the line "#define LILYGO_WATCH_2020_V1" (by "uncomment" we mean to remove the two "//" on the left).


Now, connect your T-Watch with the USB cable to the USB port of your computer. Then, check to see if the Arduino IDE automatically recognized the port to which the watch is connected.
Click on the Tools menu and hold your mouse over the "Port" item. Now you should see in pop-up that a port is selected.



When you are done, click on the "upload" button (the arrow to the right). This compiles the SimpleWatch example code, and uploads it onto your T-Watch (see below how to fix a frequent error on Linux systems). 

If everything worked, this is what you will see:


Or a closer look, revealing the 240x240 pixels (however, please note that under typical viewing conditions, you do not notice the individual pixels).


Don't expect too much. The SimpleWatch example is rather boring (it's mainly intended to demonstrate some of the library functions). 

But now you know how to use the Arduino IDE to upload any code into the LilyGo T-Watch 2020.






Appendix: Fix for a Frequent Error on Linux Systems

If you are a Linux user (like me), it may happen that you get an error message when trying to upload the code onto your watch for the first time. This is what I got on the very first attempt:

avrdude: ser_open(): can't open device "/dev/ttyACM0": Permission denied
ioctl("TIOCMGET"): Inappropriate ioctl for device

The solution is to do the following: 

sudo usermod -a -G dialout <user>
sudo chmod a+rw /dev/ttyACM0

where <user> is replaced with your username and "/dev/ttyACM0" is replaced with the port to which your watch is connected. This worked for me.

Monday, March 1, 2021

Building a Squirrel Picnic Table

 


These Squirrel Picnic Tables were the big 2020 summer hit among woodworkers. Although I'm a little late to the party, I still wanted one. And with my daughter's help, during the Christmas break, we finally built our own.

It is built of 3/4" pine wood and wood glue. The dimensions of the pieces are given
in the first image. The width of the smaller bars doe not really matter, it can be somewhere in the range of 1 1/4" or 1/ 1/2".


The four 6" x 1 1/2" bars on the sides are cut at an angle of 22.5 degrees.

The first two pieces are glued to the bottom of the plate, with 1.5" space (equals to twice the thickness of the wood) to the sides. This space is needed to accommodate the fence mount.







Two holes are drilled into the table plate to hold an additional piece of 3/4" wood that will be screwed into the fence.



The CD case is shown as a reference to judge the size of the table.

And here is the final result, waiting for the squirrels to come. In the future, we plan to screw a little cup or basket onto the plate, where we can place nuts, etc. Alternatively, we may add a larger screw from the bottom of the plate, so we can screw corn cobs onto it.