Showing posts with label RGB LED. Show all posts
Showing posts with label RGB LED. Show all posts

Friday, May 22, 2020

ATOM Matrix: Advanced Display Usage

As we saw in a previous tutorial, the ATOM Matrix's display is nothing but an array of WS2812B LEDs, commonly called NeoPixels. As such, we can use various libraries developed by Adafruit.

First, install the following libraries using the Arduino IDE's Library Manager:

  • Adafruit GFX Library - foundation of the other graphics libraries
  • Adafruit NeoPixel - for controlling NeoPixel strips
  • Adafruit NeoMatrix - for controlling NeoPixel grids
  • FastLED - includes various effect and noise functions for making interesting animations

These libraries all come with examples, especially the FastLED library. Before examining those examples, here is an example of displaying strings.


Scrolling Text Messages

To display text messages, we must first have a font! The Adafruit GFX library includes many fonts (found in the Adafruit_GFX/Fonts folder, which on the Mac is at ~/Documents/Arduino/libraries/Adafruit_GFX/Fonts), unfortunately they are all of size 8 x 8. A 5 x 5 font can be found in Lucasmaximus89's Github page at https://github.com/lukasmaximus89/M5Atom-Resources. The specific URL for the font is https://github.com/lukasmaximus89/M5Atom-Resources/blob/master/glcdfont.c. This font has character definitions for uppercase letters and digits, plus other characters - but those other characters are mostly unreadable.

Download this font and move it into the Adafruit_GFX/Fonts folder. By default, the GFX library looks for a file called "glcfont.c" for the fonts, so there is no need to specify that file in our code.

Note: when updating the GFX library, the glcfont.c file will be overwritten with the default 8 x 8 font!

Documentation for the three Adafruit libraries can be found at:

The methods in the NeoMatrix library we'll be using are mostly self explanatory, the exception being the constructor, which looks like this:
Adafruit_NeoMatrix(width, height, dataPin, matrixType, neoPixelType)

width and height
size of display, so in our case those numbers will be 5 and 5
dataPin
the pin that the NeoPixel display uses for data, which is 27 for the ATOM Matrix
matrixType
what is the physical position of the pixel at (0, 0), and are the subsequent pixels arranged in rows or columns
neoPixelType
the color order (GRB) and data rate

As we'll see, the library contains constants that make specifying the last two arguments easy.

The whole trick is to imagine the display to be a "window" showing part of the the text. The scrolling results by moving the window, not moving the message. Here is the code, with explanation below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/*
 * Text-Display-01.ino
 * 
 * By: Mike Klepper
 * Date: 26 April 2020
 * 
 * Displays text on the M5 ATOM Matrix using the Adafruit GFX library
 */

#include <Adafruit_GFX.h>
#include <Adafruit_NeoMatrix.h>
#include <Adafruit_NeoPixel.h>

#define DISPLAY_PIN 27

String msg = F("MAKE AMERICA GREAT AGAIN");
int msgLength = msg.length();
int charWidth = 5;
int numTrailingSpaces = 3;
int maxLeftPosition = (msgLength + numTrailingSpaces) * (charWidth + 1);

int scrollDelay = 100;

// Horizontal, left to right with bottom closest to USB-C port
int directionAndOrientation = NEO_MATRIX_TOP + NEO_MATRIX_LEFT + NEO_MATRIX_ROWS;

// Vertical, top to bottom with top closest to reset button
// int directionAndOrientation = NEO_MATRIX_TOP + NEO_MATRIX_RIGHT + NEO_MATRIX_COLUMNS;

int pixelType = NEO_GRB + NEO_KHZ800;

Adafruit_NeoMatrix matrix = Adafruit_NeoMatrix(5, 5, DISPLAY_PIN,
  directionAndOrientation + NEO_MATRIX_PROGRESSIVE,
  pixelType);

int xPos  = matrix.width();

void setup() 
{
  matrix.begin();
  matrix.setTextWrap(false);
  matrix.setBrightness(40);
  matrix.setTextColor(matrix.Color(80, 0, 80));
}

void loop() 
{
  matrix.fillScreen(0);
  matrix.setCursor(xPos, 0);
  matrix.print(msg);

  if(--xPos < -maxLeftPosition)
  {
    xPos = matrix.width();
  }
  
  matrix.show();
  delay(scrollDelay);
}


Line Comment
14 The dataPin, which will be third argument of the constructor (line 30)
16 The message that will be displayed
17 Number of characters in the message
18 How wide each character in our fixed-width font is
19 These trailing spaces result in a delay between the end of the message and the start of the next cycle
20 The total width of the message (including trailing spaces) in pixels
25 (0, 0) is located at the top-left, and the subsequent pixels are arranged in rows
28 Uncomment this line (and comment line 25) for a different scroll direction
30 The NeoPixels use GRB color order, with data rate of 800 KHz
32 The constructor!
33 NEO_MATRIX_PROGRESSIVE means that the pixels in the rows all have same order (as opposed to zig-zag)
36 How much the "window" is offset
38 Start of the setup function; all this is self explanatory, except...
43 The matrix.Color method takes values in RGB order, so in this case the text will be purple
49 Set the position of the "window"
50 Draw the message
52 Move the window to the left by one; if the window has moved pass the end of the message...
54 Reset the window to the right edge of the display


Animation Effects

The FastLED library contains numerous examples that were, I believe, originally intended for NeoPixel strips instead of matrices. They still look good, however!

To use the examples, the following three changes must be made to the code:

  • Set the data pin to 27
  • The number of LEDs must be set to 25
  • The brightness should be set to a lower value, like 60, for example.
The ATOM Matrix product description page recommends a max brightness of 20, but higher numbers seem harmless.

Here is one of the examples, called "Fire2012WithPalette," with the above three modifications made. Comments from the original are retained.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
#include <FastLED.h>

#define LED_PIN     27
#define COLOR_ORDER GRB
#define CHIPSET     WS2811
#define NUM_LEDS    25

#define BRIGHTNESS  60
#define FRAMES_PER_SECOND 60

bool gReverseDirection = false;

CRGB leds[NUM_LEDS];

// Fire2012 with programmable Color Palette
//
// This code is the same fire simulation as the original "Fire2012",
// but each heat cell's temperature is translated to color through a FastLED
// programmable color palette, instead of through the "HeatColor(...)" function.
//
// Four different static color palettes are provided here, plus one dynamic one.
// 
// The three static ones are: 
//   1. the FastLED built-in HeatColors_p -- this is the default, and it looks
//      pretty much exactly like the original Fire2012.
//
//  To use any of the other palettes below, just "uncomment" the corresponding code.
//
//   2. a gradient from black to red to yellow to white, which is
//      visually similar to the HeatColors_p, and helps to illustrate
//      what the 'heat colors' palette is actually doing,
//   3. a similar gradient, but in blue colors rather than red ones,
//      i.e. from black to blue to aqua to white, which results in
//      an "icy blue" fire effect,
//   4. a simplified three-step gradient, from black to red to white, just to show
//      that these gradients need not have four components; two or
//      three are possible, too, even if they don't look quite as nice for fire.
//
// The dynamic palette shows how you can change the basic 'hue' of the
// color palette every time through the loop, producing "rainbow fire".

CRGBPalette16 gPal;

void setup() {
  delay(3000); // sanity delay
  FastLED.addLeds<CHIPSET, LED_PIN, COLOR_ORDER>(leds, NUM_LEDS).setCorrection( TypicalLEDStrip );
  FastLED.setBrightness( BRIGHTNESS );

  // This first palette is the basic 'black body radiation' colors,
  // which run from black to red to bright yellow to white.
  gPal = HeatColors_p;
  
  // These are other ways to set up the color palette for the 'fire'.
  // First, a gradient from black to red to yellow to white -- similar to HeatColors_p
  //   gPal = CRGBPalette16( CRGB::Black, CRGB::Red, CRGB::Yellow, CRGB::White);
  
  // Second, this palette is like the heat colors, but blue/aqua instead of red/yellow
  //   gPal = CRGBPalette16( CRGB::Black, CRGB::Blue, CRGB::Aqua,  CRGB::White);
  
  // Third, here's a simpler, three-step gradient, from black to red to white
  //   gPal = CRGBPalette16( CRGB::Black, CRGB::Red, CRGB::White);

}

void loop()
{
  // Add entropy to random number generator; we use a lot of it.
  random16_add_entropy( rand());

  // Fourth, the most sophisticated: this one sets up a new palette every
  // time through the loop, based on a hue that changes every time.
  // The palette is a gradient from black, to a dark color based on the hue,
  // to a light color based on the hue, to white.
  //
  //   static uint8_t hue = 0;
  //   hue++;
  //   CRGB darkcolor  = CHSV(hue,255,192); // pure hue, three-quarters brightness
  //   CRGB lightcolor = CHSV(hue,128,255); // half 'whitened', full brightness
  //   gPal = CRGBPalette16( CRGB::Black, darkcolor, lightcolor, CRGB::White);


  Fire2012WithPalette(); // run simulation frame, using palette colors
  
  FastLED.show(); // display this frame
  FastLED.delay(1000 / FRAMES_PER_SECOND);
}


// Fire2012 by Mark Kriegsman, July 2012
// as part of "Five Elements" shown here: http://youtu.be/knWiGsmgycY
//// 
// This basic one-dimensional 'fire' simulation works roughly as follows:
// There's a underlying array of 'heat' cells, that model the temperature
// at each point along the line.  Every cycle through the simulation, 
// four steps are performed:
//  1) All cells cool down a little bit, losing heat to the air
//  2) The heat from each cell drifts 'up' and diffuses a little
//  3) Sometimes randomly new 'sparks' of heat are added at the bottom
//  4) The heat from each cell is rendered as a color into the leds array
//     The heat-to-color mapping uses a black-body radiation approximation.
//
// Temperature is in arbitrary units from 0 (cold black) to 255 (white hot).
//
// This simulation scales it self a bit depending on NUM_LEDS; it should look
// "OK" on anywhere from 20 to 100 LEDs without too much tweaking. 
//
// I recommend running this simulation at anywhere from 30-100 frames per second,
// meaning an interframe delay of about 10-35 milliseconds.
//
// Looks best on a high-density LED setup (60+ pixels/meter).
//
//
// There are two main parameters you can play with to control the look and
// feel of your fire: COOLING (used in step 1 above), and SPARKING (used
// in step 3 above).
//
// COOLING: How much does the air cool as it rises?
// Less cooling = taller flames.  More cooling = shorter flames.
// Default 55, suggested range 20-100 
#define COOLING  55

// SPARKING: What chance (out of 255) is there that a new spark will be lit?
// Higher chance = more roaring fire.  Lower chance = more flickery fire.
// Default 120, suggested range 50-200.
#define SPARKING 120


void Fire2012WithPalette()
{
// Array of temperature readings at each simulation cell
  static byte heat[NUM_LEDS];

  // Step 1.  Cool down every cell a little
    for( int i = 0; i < NUM_LEDS; i++) {
      heat[i] = qsub8( heat[i],  random8(0, ((COOLING * 10) / NUM_LEDS) + 2));
    }
  
    // Step 2.  Heat from each cell drifts 'up' and diffuses a little
    for( int k= NUM_LEDS - 1; k >= 2; k--) {
      heat[k] = (heat[k - 1] + heat[k - 2] + heat[k - 2] ) / 3;
    }
    
    // Step 3.  Randomly ignite new 'sparks' of heat near the bottom
    if( random8() < SPARKING ) {
      int y = random8(7);
      heat[y] = qadd8( heat[y], random8(160,255) );
    }

    // Step 4.  Map from heat cells to LED colors
    for( int j = 0; j < NUM_LEDS; j++) {
      // Scale the heat value from 0-255 down to 0-240
      // for best results with color palettes.
      byte colorindex = scale8( heat[j], 240);
      CRGB color = ColorFromPalette( gPal, colorindex);
      int pixelnumber;
      if( gReverseDirection ) {
        pixelnumber = (NUM_LEDS-1) - j;
      } else {
        pixelnumber = j;
      }
      leds[pixelnumber] = color;
    }
}



Application: Controlling a NeoPixel Strip

The same FireLED examples can be used to control an external NeoPixel strip.

In order to connect a strip to the Atom's Grove port, Dupont jumper wires cannot be used - the pins are too close! Instead, use the type with the thinner ends. Connect ground to ground, power to power, and (for example) G32 to the data line on the NeoPixel strip.

The spacing between the pins on the back of the ATOM Matrix is sufficient to use ordinary Dupont jumpers.

Ordinary NeoPixel strips can operate at full brightness (255), but the other two changes (dataPin and number of pixels) must still be made. Here is a FastLED example called "Pride2015," with original comments in place.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#include "FastLED.h"

// Pride2015
// Animated, ever-changing rainbows.
// by Mark Kriegsman

#if FASTLED_VERSION < 3001000
#error "Requires FastLED 3.1 or later; check github for latest code."
#endif

#define DATA_PIN    32
//#define CLK_PIN   4
#define LED_TYPE    WS2811
#define COLOR_ORDER GRB
#define NUM_LEDS    30
#define BRIGHTNESS  255

CRGB leds[NUM_LEDS];


void setup() {
  delay(3000); // 3 second delay for recovery
  
  // tell FastLED about the LED strip configuration
  FastLED.addLeds<LED_TYPE,DATA_PIN,COLOR_ORDER>(leds, NUM_LEDS)
    .setCorrection(TypicalLEDStrip)
    .setDither(BRIGHTNESS < 255);

  // set master brightness control
  FastLED.setBrightness(BRIGHTNESS);
}


void loop()
{
  pride();
  FastLED.show();  
}


// This function draws rainbows with an ever-changing,
// widely-varying set of parameters.
void pride() 
{
  static uint16_t sPseudotime = 0;
  static uint16_t sLastMillis = 0;
  static uint16_t sHue16 = 0;
 
  uint8_t sat8 = beatsin88( 87, 220, 250);
  uint8_t brightdepth = beatsin88( 341, 96, 224);
  uint16_t brightnessthetainc16 = beatsin88( 203, (25 * 256), (40 * 256));
  uint8_t msmultiplier = beatsin88(147, 23, 60);

  uint16_t hue16 = sHue16;//gHue * 256;
  uint16_t hueinc16 = beatsin88(113, 1, 3000);
  
  uint16_t ms = millis();
  uint16_t deltams = ms - sLastMillis ;
  sLastMillis  = ms;
  sPseudotime += deltams * msmultiplier;
  sHue16 += deltams * beatsin88( 400, 5,9);
  uint16_t brightnesstheta16 = sPseudotime;
  
  for( uint16_t i = 0 ; i < NUM_LEDS; i++) {
    hue16 += hueinc16;
    uint8_t hue8 = hue16 / 256;

    brightnesstheta16  += brightnessthetainc16;
    uint16_t b16 = sin16( brightnesstheta16  ) + 32768;

    uint16_t bri16 = (uint32_t)((uint32_t)b16 * (uint32_t)b16) / 65536;
    uint8_t bri8 = (uint32_t)(((uint32_t)bri16) * brightdepth) / 65536;
    bri8 += (255 - brightdepth);
    
    CRGB newcolor = CHSV( hue8, sat8, bri8);
    
    uint16_t pixelnumber = i;
    pixelnumber = (NUM_LEDS-1) - pixelnumber;
    
    nblend( leds[pixelnumber], newcolor, 64);
  }
}


Click here to go to the table of contents for this series.

ATOM Matrix: Basic Display Usage

One of the best things about the boards from M5Stack is their API, which handles display, button debounce, and other hardware interfaces. Documents and source code for the ATOM's API can be found here: https://github.com/m5stack/M5Atom

To include the API in a sketch, use the following:

#include "M5Atom.h"
This will define a class called M5Atom, and create an instance of that class called M5.

Here are the most common API methods we'll be using:

To initialize the ATOM, use this:

M5.begin(bool SerialEnable , bool I2CEnable , bool DisplayEnable)
The first argument should be set to true when we want to do serial output
The second argument enables I2C, so would be set to true when using some Grove sensors
The third argument, when set to true, enables the 5x5 RGB LED display.

We will be using four methods from that API for displaying data:

  • M5.dis.setBrightness(uint8_t brightness)
  • M5.dis.drawpix(uint8_t xpos, uint8_t ypos, CRGB Color)
  • M5.dis.drawpix(uint8_t Number, CRGB Color)
  • M5.dis.clear()

There are other display-related methods that we'll not be using.

Finally, use this method to poll for button clicks:

M5.update()


Activating a Single Pixel

To introduce the M5Atom API, here is a simple sketch that causes the RGB LED in the top-left corner to blink. Thrilling, no?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/*
 * SinglePixel.ino
 * 
 * By: Mike Klepper
 * Date: 16 May 2020
 * 
 * Introduces the M5Atom API
 */

#include "M5Atom.h"

void setup() 
{
  M5.begin(true, false, true);
  delay(20);
}

void loop() 
{
  M5.dis.drawpix(0, 0, 0xffffff);
  Serial.println(F("Pixel is on"));
  delay(250);
  M5.dis.clear();
  Serial.println(F("Pixel is off"));
  delay(250);
  M5.update();
}
Line Comment
10 Include the M5Atom API, which creates an object called M5
14 Initialize the M5 object, enabling serial output, disabling I2C, and enabling the display
15 This delay is not needed, but apps that use the IMU seem to require it
20 Set the pixel at (0, 0) to be white - so we know where the origin is!
23 Clear the display
26 Call update method, which polls for button events (unnecessary for this particular sketch)

Before uploading this program, choose the proper serial port from the Tools > Port menu, and set the board to be "M5Stick-C".

Something to notice is what is missing from this program:

Serial.begin(115200)
This line is included in the M5.begin method when the first argument is set to true.


Color Order

When you attempt to change the color of the pixel's color in line 20, you'll get unexpected results! For example, if you change the line to read:

M5.dis.drawpix(0, 0, 0xff0000);
instead of getting blinking red, you'll get a green blinking pixel!

As it goes, the color order that these LEDs use isn't the RRGGBB found in HTML, but GGRRBB. To demonstrate this (and to demonstrate the axis orientation), this program will plot a red pixel in the top-right corner, a green pixel at bottom-left, and a blue pixel at the bottom-right corner.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/*
 * FourPixels.ino
 * 
 * By: Mike Klepper
 * Date: 16 May 2020
 * 
 * Demonstrates GRB color order for the LEDs as well as axis orientation
 */

#include "M5Atom.h"

void setup() 
{
  M5.begin(true, false, true);
  delay(20);
}

void loop() 
{
  M5.dis.drawpix(0, 0, 0xffffff); // White
  M5.dis.drawpix(4, 0, 0x00ff00); // Red
  M5.dis.drawpix(0, 4, 0xff0000); // Green
  M5.dis.drawpix(4, 4, 0x0000ff); // Blue
  Serial.println(F("Pixels are on"));
  delay(250);
  
  M5.dis.clear();
  Serial.println(F("Pixels are off"));
  delay(250);
  
  M5.update();
}

So we see that the pixels are numbered 0 to 4, inclusive, along each axis, and that the y-axis points down.


Pixel Order

The programs so far has used the drawpix(x, y, c) method. We will have a need for the other drawpix method in the API, the drawpix(n, c). This method counts from the top-left (n = 0) to the bottom-right (n = 24), but are the pixels in the middle ordered like words in an English paragraph (line-by-line, top-to-bottom) or in a zig-zag manner? Let's find out!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/**
 * LED-Order.ino
 *
 * By: Mike Klepper
 * Date: 26 April 2020
 *
 * Demonstrates order that pixels are addressed
 *
 * See blog post on patriot-geek.blogspot.com
 * for instructions.
 */


#include "M5Atom.h"

int GRB_COLOR_WHITE = 0xffffff;
int GRB_COLOR_BLACK = 0x000000;

int delayAmt = 1000;

void setup() 
{
  M5.begin(true, false, true);
  delay(50);
}

void loop() 
{
  for(int i = 0; i < 25; i++)
  {
    M5.dis.drawpix(i, GRB_COLOR_WHITE);
    delay(50);
  }
  delay(delayAmt);

  for(int i = 0; i < 25; i++)
  {
    M5.dis.drawpix(i, GRB_COLOR_BLACK);
    delay(50);
  }
  
  delay(delayAmt);
  M5.update();
}

Thus the pixel order with the drawpix(n, c) method is the same as the reading direction in English.


Controlling Brightness

Something unexpected is that changing the overall brightness of the display does NOT require a redraw! Here's a demo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/*
 * BrightnessDemo.ino
 * 
 * By: Mike Klepper
 * Date: 16 May 2020
 * 
 * Demonstrates that changing brightness does NOT require a redraw!
 */

#include "M5Atom.h"

void setup() 
{
  M5.begin(true, false, true);
  delay(20);

  for(int i = 0; i < 25; i++)
  {
    M5.dis.drawpix(i, 0xffffff);
  }
}

void loop() 
{
  for(int i = 60; i >= 0; i--)
  {
    M5.dis.setBrightness(i);
    delay(50);
  }

  for (int i = 0; i < 60; i++)
  {
    M5.dis.setBrightness(i);
    delay(50);
  }
  
  M5.update();
}

The documents for the ATOM Matrix state that the maximum brightness for the pixels should be 60, hence the limits on the for-loops in lines 25 and 31.


Displaying Single Characters

In future parts of this series, we will frequently need to display single characters or other patterns. This can be accomplished by specifying the desired colors of the pixels in a 1-dimensional array, and looping over that array. The last program in this post displays a Morse code message, thus demonstrating how to display dash and dot characters. As it goes, this program mostly shows how to handle character arrays and strings in C.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
/**
 * MorseCode.ino
 *
 * By: Mike Klepper
 * Date: 26 April 2020
 *
 * Converts string stored in message into Morse code
 *
 * See blog post on patriot-geek.blogspot.com
 * for instructions.
 */

#include "M5Atom.h"

int GRB_COLOR_WHITE = 0xffffff;
int GRB_COLOR_BLACK = 0x000000;
int GRB_COLOR_RED = 0x00ff00;
int GRB_COLOR_ORANGE = 0xa5ff00;
int GRB_COLOR_YELLOW = 0xffff00;
int GRB_COLOR_GREEN = 0xff0000;
int GRB_COLOR_BLUE = 0x0000ff;
int GRB_COLOR_PURPLE = 0x008080;

int activeColor = GRB_COLOR_RED;

int colorList[] = {GRB_COLOR_BLACK, activeColor};

// Constants used in Farnsworth timing
// See https://morsecode.world/international/timing.html for details
int scale = 200;
int dotDuration = 1 * scale;
int dashDuration = 3 * scale;
int timeBetweenDotsAndDashes = 1 * scale;
int timeBetweenCharacters = 3 * scale;
int spaceDuration = 7 * scale;

String message = F("Trump 2020 Keep America Great");



int dot[25] = 
{
  0,0,0,0,0,
  0,0,0,0,0,
  0,0,1,1,0,
  0,0,1,1,0,
  0,0,0,0,0
};

int dash[25] = 
{
  0,0,0,0,0,
  0,0,0,0,0,
  1,1,1,1,1,
  1,1,1,1,1,
  0,0,0,0,0
};


char* letters[] = {
  ".-", "-...", "-.-.", "-..", ".", "..-.", "--.", "....", "..",    // A-I
  ".---", "-.-", ".-..", "--", "-.", "---", ".--.", "--.-", ".-.",  // J-R 
  "...", "-", "..-", "...-", ".--", "-..-", "-.--", "--.."          // S-Z
};

char* digits[] = {
  "-----", ".----", "..---", "...--", "....-", ".....",             // 0-5
  "-....", "--...", "---..", "----."                                // 6-9
};


void setup() 
{
  M5.begin(true, false, true);
  delay(20);
}


void loop() 
{
  displayMessageInMorseCode(message);
  finalAnimation();
  delay(2000);
}

void displayMessageInMorseCode(String message)
{
  message.toUpperCase();
  
  int msgLength = message.length();

  Serial.println("");

  for(int i = 0; i < msgLength; i++)
  {
    char currentChar = message.charAt(i);
    String morseCodeForCurrentChar;
    Serial.print(currentChar);
    Serial.print(" ");
    
    if(isAlpha(currentChar))
    {
      morseCodeForCurrentChar = letters[currentChar - 65];
      Serial.print(letters[currentChar - 65]);
      Serial.println(" - Alpha");

      displaySingleMorseCodeCharacter(morseCodeForCurrentChar);
    }
    else if(isDigit(currentChar))
    {
      morseCodeForCurrentChar = digits[currentChar - 48];
      Serial.print(digits[currentChar - 48]);
      Serial.println(" - Digit");

      displaySingleMorseCodeCharacter(morseCodeForCurrentChar);
    }
    else if(isSpace(currentChar))
    {
      Serial.println(" - Space");
      delay(spaceDuration);
    }
  }
}

void displaySingleMorseCodeCharacter(String morseCodeCharacter)
{
  int morseCodeLength = morseCodeCharacter.length();

  for(int i = 0; i < morseCodeLength; i++)
  {
    char currentDotOrDash = morseCodeCharacter.charAt(i);
    Serial.println(currentDotOrDash);

    if(currentDotOrDash == '.')
    {
      M5.dis.clear();
      drawArray(dot, colorList);
      delay(dotDuration);
    }
    else
    {
      M5.dis.clear();
      drawArray(dash, colorList);
      delay(dashDuration);
    }

    M5.dis.clear();
    delay(timeBetweenDotsAndDashes);
  }

  delay(timeBetweenCharacters);
  Serial.println("---------------------");
}


void drawArray(int arr[], int colors[])
{
  for(int i = 0; i < 25; i++)
  {
      M5.dis.drawpix(i, colors[arr[i]]);
  }
}

void fillDisplay(int fillColor)
{
  for(int i = 0; i < 25; i++)
  {
      M5.dis.drawpix(i, fillColor);
  }
}

void finalAnimation()
{
  delay(2000);
  
  M5.dis.clear();
  fillDisplay(GRB_COLOR_RED);
  delay(2000);
  fillDisplay(GRB_COLOR_WHITE);
  delay(2000);
  fillDisplay(GRB_COLOR_BLUE);
  delay(2000);
  M5.dis.clear();
}

It is easier to understand the code starting from the bottom...

Line Comment
156 - 162 Draws an array using the colors specified in the array of colors
164 - 170 Fills the display with a single color
172 - 184 Animation that is displayed at the end of the Morse code message
15 - 22 Handy color constants!
24 Color used to display the Morse code message
26 Colors used when displaying a character (both off and on colors)
28 - 35 Constants that control the Morse code speed
37 The message string that will be displayed - MAGA!
41 - 48 Array describing the dot character
50 - 57 Array for the dash character
60 - 69 The Morse code for letters and numbers
72 - 76 Usual setup function
79 - 84 The loop function - display the message, do the animation, pause for two seconds
86 - 124 This function looks-up each character in lines 60 - 69 by ASCII code
94 Loop over all characters in the message
96 Get current character
101 If the character is a letter...
103 Look up character in the letters array, offsetting the ASCII code by 65 (the ASCII code for 'A')
107 Display the single letter in Morse code
109 Else if the character is a digit...
111 Look up character in the digits array, offsetting ASCII code by 48 (the ASCII code for '0')
115 Display the single digit in Morse code
117 - 121 A space is just a pause in Morse code
125 - 149 Function for displaying the Morse code equivalent of a single character
129 Loop over the dots and dashes
131 Get current dot or dash
134 - 139 If it is a dot, show the dot character, and delay
140 - 145 Same for dash
151 Pause between characters
156 - 162 Display a character by looping over its array representation
158 Loop over the given array
160 Look up the color in the colorList and plot it!

Not very memory efficient code, but there it is.

The drawArray function is quite flexible! As used here, it displays characters using only one color for all pixels (and the other pixels are black). In later posts in this series, we will use that same function to show characters or icons using more colors!

Click here to go to the table of contents for this series.