Friday, May 22, 2020

ATOM Matrix: Onboard IR LED and Remote Control Emulator

The ATOM Matrix has an onboard IR LED. To toggle the IR LED, use the same code as from the last post, but change the outputPin from 26 to 12:

 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
/*
 * IR-LED-01.ino
 * 
 * By: Mike Klepper
 * Date: 26 April 2020
 * 
 * Demonstrates how to turn the IR LED on and off
 */

#include "M5Atom.h"

int outputPin = 12;

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

void loop() 
{
  digitalWrite(12, HIGH);
  delay(500);
  M5.update();

  digitalWrite(12, LOW);
  delay(500);
  M5.update();
}

When you run this program and point the ATOM Matrix at your webcam, you will notice that the IR LED's output is not bright at all. This will cause problems for our remote control emulator! :-(


IR Codes for a Particular Remote Control

I will be using the IR remote control that came with a Sony Blu-ray player.

Using the IRrecvDumpV2 program from the last blog post, the codes for some of the buttons on that remote control are as follows:

Button Code
On/Off 0xA8B47
Open/Close 0x68B47
Popup Menu 0x94B47
Left Arrow 0xDCB47
Right Arrow 0x3CB47
Up Arrow 0x9CB47
Down Arrow 0x5CB47
Select 0xBCB47
Play 0x58B47
Pause 0x98B47
Stop 0x18B47
Reverse 0xD8B47
Forward 0x38B47
Previous Scene 0xEAB47
Next Scene 0x6AB47


IR Remote Control Emulator

Now combine these codes with the one-button menu system found in a previous blog post, and the result is an IR remote control emulator! The secret sauce is in the IRremoteESP8266 library installed in the last blog post. This library has a IRsend class with format-specific commands, like sendNEC, sendSony, etc., for transmitting IR commands.

  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
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
/*
 * Sony-IR-Remote-Emulator.ino
 * 
 * By: Mike Klepper
 * Date: 26 April 2020
 * 
 * Emulates a Sony Blu-ray Player IR Remote Control
 * 
 * See post on patriot-geek.blogspot.com
 * for details
 */


#include "M5Atom.h"
#include <IRremoteESP8266.h>
#include <IRsend.h>

// const uint16_t kIrLed = 12;  // Internal IR LED
const uint16_t kIrLed = 26;  // IR Remote Grove Module IR LED

IRsend irsend(kIrLed);  // Set the GPIO to be used to sending the message.

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 colorList[] = {GRB_COLOR_BLACK, GRB_COLOR_WHITE, GRB_COLOR_RED, GRB_COLOR_GREEN};

int empty[25] = 
{
  0,0,0,0,0,
  0,0,0,0,0,
  0,0,0,0,0,
  0,0,0,0,0,
  0,0,0,0,0
};

int onOff[25] = 
{
  0,2,3,2,0,
  2,0,3,0,2,
  2,0,3,0,2,
  2,0,0,0,2,
  0,2,2,2,0
};

int onOffCode = 0xA8B47;


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

int openCloseCode = 0x68B47;


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

int popUpMenuCode = 0x94B47;


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

int leftArrowCode = 0xDCB47;


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

int rightArrowCode = 0x3CB47;


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

int upArrowCode = 0x9CB47;


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

int downArrowCode = 0x5CB47;


int select[25] = 
{
  0,0,3,0,0,
  0,3,3,3,0,
  3,3,3,3,3,
  0,3,3,3,0,
  0,0,3,0,0
};

int selectCode = 0xBCB47;


int playBtn[25] = 
{
  0,0,3,0,0,
  0,0,3,3,0,
  0,0,3,3,3,
  0,0,3,3,0,
  0,0,3,0,0
};

int playBtnCode = 0x58B47;


int pauseBtn[25] = 
{
  0,0,0,0,0,
  0,2,0,2,0,
  0,2,0,2,0,
  0,2,0,2,0,
  0,0,0,0,0
};

int pauseBtnCode = 0x98B47;


int stopBtn[25] = 
{
  0,0,0,0,0,
  0,2,2,2,0,
  0,2,2,2,0,
  0,2,2,2,0,
  0,0,0,0,0
};

int stopBtnCode = 0x18B47;


int reverseBtn[25] = 
{
  0,0,0,0,0,
  0,3,0,0,3,
  3,0,0,3,0,
  0,3,0,0,3,
  0,0,0,0,0
};

int reverseBtnCode = 0xD8B47;


int forwardBtn[25] = 
{
  0,0,0,0,0,
  3,0,0,3,0,
  0,3,0,0,3,
  3,0,0,3,0,
  0,0,0,0,0
};

int forwardBtnCode = 0x38B47;


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

int prevSceneCode = 0xEAB47;


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

int nextSceneCode = 0x6AB47;

const int numChoices = 15;
int *iconList[numChoices] = { onOff, openClose, popUpMenu, leftArrow, rightArrow, upArrow, downArrow, select, playBtn, pauseBtn, stopBtn, reverseBtn, forwardBtn, prevScene, nextScene };
int codeList[numChoices] = { onOffCode, openCloseCode, popUpMenuCode, leftArrowCode, rightArrowCode, upArrowCode, downArrowCode, selectCode, playBtnCode, pauseBtnCode, stopBtnCode, reverseBtnCode, forwardBtnCode, prevSceneCode, nextSceneCode };

int currentState = 0;
bool isAsleep = false;
bool wasLongPressed = false;
bool commandExecuted = false;


void setup() 
{
  M5.begin(true, false, true);
  delay(20);
  
  irsend.begin();
  
  drawArray2(iconList[currentState], colorList, 10);
}

void loop() 
{
  if(M5.Btn.wasPressed())
  {
    // Display icon
    drawArray2(iconList[currentState], colorList, 10);

    commandExecuted = false;
  }
  
  if(M5.Btn.wasReleased())
  {
    Serial.print("isAsleep = ");
    Serial.println(isAsleep);
    Serial.print("wasLongPressed = ");
    Serial.println(wasLongPressed);
    
    if(wasLongPressed == false)
    {
      if(isAsleep == false)
      {
        // Advance to next state in the cycle
        currentState = (currentState + 1) % numChoices;
        
      }
      isAsleep = false;
      commandExecuted = false;
      Serial.println(currentState);
  
      // Display icon
      drawArray2(iconList[currentState], colorList, 10);
    }

    wasLongPressed = false;
  }
  
  // Long press
  if(M5.Btn.pressedFor(1000))
  {
    wasLongPressed = true;
    
    // Display icon as active
    drawArray2(iconList[currentState], colorList, 25);
    
    // Perform corresponding action
    if(commandExecuted == false)
    {
      irsend.sendSony(codeList[currentState], 20, 2);
      commandExecuted = true;
      Serial.println("pressedFor");
    }
  }
  
  // Clear display after period of inactivity
  if(M5.Btn.releasedFor(2000))
  {
    M5.dis.clear();
    isAsleep = true;
    commandExecuted = false;
  }

  // Reset to initial state after LONG period of inactivity
  if(M5.Btn.releasedFor(30000))
  {
    currentState = 0;
    M5.dis.clear();
    isAsleep = true;
    commandExecuted = false;
    //Serial.println("Reset currentState to 0");
  }
  
  delay(50);
  M5.update();
}

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

The code is almost identical to the one-button menu application, and this line-by-line commentary will focus only on the differences:

Line Comment
18 - 19 Select the desired IR LED - the onboard IR LED won't work beyond moderate distances
21 Create an IRsend object for transmitting IR codes
32 The colorList is longer than usual since we'll be using multi-color icons!
34 - 218 Icons and the IR codes
222 Number of choices in our menu system
223 - 224 List of icons and IR codes
237 Initialize the irsend object created in line 21
239 Draw the current icon using the colorList and at brightness level 10
289 Send the IR code in the Sony format, padding to 20 bits and repeating twice
272 Draw icon at brightness level 10
284 When button is long-pressed, draw icon at brightness level 25
317 - 325 New, improved, drawArray2 function that lets us control brightness


About the Onboard IR...

As mentioned above, the onboard IR is fairly dim, and it isn't sufficiently bright to control my Blu-ray player at a distance of 6 feet. For now, we will leave the Grove IR Remote module plugged in. A workaround will be implemented in a future post!

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

ATOM Matrix: Advanced Button Usage

With only one user button, it seems to be impossible to build a full application on the ATOM Matrix! The purpose of this post is demonstrate one way of doing so, by making use of the available button events beyond the onRelease event.

The goal of this project is to build a menu system. Here are the desired properties of our application:

  1. The menu choices will be displayed as the digits 0 - 9
  2. The only action each of those choices will perform is to print the digit in the Serial monitor window
  3. When the button is pressed, the app "wakes up" and displays the current choice
  4. To advance to the next choice, release the button
  5. Advancing past choice 9 sets the choice back to 0
  6. To select the current choice, hold the button down for 2 seconds
  7. The action for the selected choice must fire only once - no repeated actions when we hold it down for more than 2 seconds!
  8. After the button has been released for 2 seconds, the display goes "to sleep"
  9. If the button has been released for 30 seconds, the current choice returns to 0
  10. Otherwise, the choice is unchanged

How do we implement this? We have all the concepts and tools from the previous posts in this series. Briefly, here is how we will implement each of the above requirements:

  1. Store the digits in a length 25 array, and use the drawArray to display these single characters
  2. Serial.println statements will do this
  3. Use the wasPressed event to "wake up" the device and turn on the display
  4. The wasReleased event will advance to the next choice
  5. Use modular arithmetic to cycle through the choices
  6. To select the current choice, use the pressedFor(2000) method
  7. Use a boolean to prevent pressedFor from firing if the button is held down for an extended time
  8. Use releasedFor(2000) to turn-off display
  9. Resetting the current choice to 0 is done using releasedFor(30000)
  10. The choice is unchanged otherwise, since we'll store it in a variable

Here is the code, in all its detail!

  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
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
/*
 * Button04.ino
 * 
 * By: Mike Klepper
 * Date: 26 April 2020
 * 
 * Demonstrates how to build a menu system with just one button!
 * 
 * See post on patriot-geek.blogspot.com
 * for details
 */


#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 colorList[] = {GRB_COLOR_BLACK, GRB_COLOR_BLUE};
int activeColorList[] = {GRB_COLOR_BLACK, GRB_COLOR_GREEN};

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


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

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

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

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

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

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

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

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

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

int *displayNumbers[10] = { zero, one, two, three, four, five, six, seven, eight, nine };

int currentState = 0;
bool isAsleep = false;
bool wasLongPressed = false;
bool commandExecuted = false;

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

  drawArray(displayNumbers[currentState], colorList);
}

void loop() 
{
  if(M5.Btn.wasPressed())
  {
    // Display state number
    drawArray(displayNumbers[currentState], colorList);

    commandExecuted = false;
  }
  
  if(M5.Btn.wasReleased())
  {
    Serial.print("isAsleep = ");
    Serial.println(isAsleep);
    Serial.print("wasLongPressed = ");
    Serial.println(wasLongPressed);
    
    if(wasLongPressed == false)
    {
      if(isAsleep == false)
      {
        // Advance to next state in the cycle
        currentState = (currentState + 1) % 10;
      }
      
      isAsleep = false;
      commandExecuted = false;
      Serial.println(currentState);
  
      // Display state number
      drawArray(displayNumbers[currentState], colorList);
    }

    wasLongPressed = false;
  }

  // Long press
  if(M5.Btn.pressedFor(1000))
  {
    wasLongPressed = true;
    
    // Display state number as active
    drawArray(displayNumbers[currentState], activeColorList);
    
    // Perform corresponding action
    if(commandExecuted == false)
    {
      switch(currentState)
      {
        case 0:
        {
          performActionZero();
          break;
        }
        case 1:
        {
          performActionOne();
          break;
        }
        case 2:
        {
          performActionTwo();
          break;
        }
        case 3:
        {
          performActionThree();
          break;
        }
        case 4:
        {
          performActionFour();
          break;
        }
        case 5:
        {
          performActionFive();
          break;
        }
        case 6:
        {
          performActionSix();
          break;
        }
        case 7:
        {
          performActionSeven();
          break;
        }
        case 8:
        {
          performActionEight();
          break;
        }
        case 9:
        {
          performActionNine();
          break;
        }
      }

      commandExecuted = true;

      Serial.println("pressedFor");
    }
  }

  // Clear display after period of inactivity
  if(M5.Btn.releasedFor(2000))
  {
    M5.dis.clear();
    isAsleep = true;
    commandExecuted = false;
  }

  // Reset to initial state after LONG period of inactivity
  if(M5.Btn.releasedFor(30000))
  {
    currentState = 0;
    M5.dis.clear();
    isAsleep = true;
    commandExecuted = false;
    // Serial.println("Reset currentState to 0");
  }

  delay(50);
  M5.update();
}

void performActionZero()
{
  Serial.println("In performActionZero");
}

void performActionOne()
{
  Serial.println("In performActionOne");
}

void performActionTwo()
{
  Serial.println("In performActionTwo");
}

void performActionThree()
{
  Serial.println("In performActionThree");
}

void performActionFour()
{
  Serial.println("In performActionFour");
}

void performActionFive()
{
  Serial.println("In performActionFive");
}

void performActionSix()
{
  Serial.println("In performActionSix");
}

void performActionSeven()
{
  Serial.println("In performActionSeven");
}

void performActionEight()
{
  Serial.println("In performActionEight");
}

void performActionNine()
{
  Serial.println("In performActionNine");
}

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

Here's the walk-through:

Line Comment
16 - 23 GRB color constants
25 Color scheme for the unselected menu item
26 Color scheme for when the menu item is selected
28 - 117 Definition of the digits corresponding to each menu item
119 Pointer to all those digit arrays for easy reference
121 The current menu item, will be an integer between 0 and 9 inclusive
122 Is the display off?
123 Has the menu item been selected?
124 Has the corresponding menu command been executed?
131 Show the initial value of the currentState, which is zero
136 When button is pressed...
139 Show the current state
141 The command has not YET been executed
242 - 247 If there has been 2 seconds of inactivity, clear the display
250 - 257 If there has been 30 seconds of inactivity, reset currentState to 0
171 If there was a long press (1 second)...
176 Draw the digit as "selected"
179 If the corresponding command has not yet been executed...
181 - 233 Perform the corresponding command
235 Set flag indicating that the command has been performed
260 Poll for button events
263 - 311 Command implementations (currently trivial)
313 - 318 Our friend, the drawArray function

So, that is how to implement a menu system using only one button! We will use this framework to build a DVD IR remote control in the next post!

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

ATOM Matrix: Grove IR Remote Module

The goal of this post and the next post is to reverse engineer the IR signals sent by a DVD remote control. We will use the Grove IR Remote module to read the signals that correspond to a few select buttons. The project will be completed in the next post.

While all of the code here is not specific to the ATOM Matrix, the IR codes transmitted by the remote control are HIGHLY specific to the particular remote!


IR Remote Module

The Grove IR Remote module is different from the PIR or potentiometer modules in that the IR Remote performs both output and input - it can both emit (pin 32) and receive (pin 26) IR signals.

First, here is a simple app that flashes the IR LED on the Remote. In order to see it, use either a computer's webcam, or the camera on an older phone.

 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
/*
 * Grove-IR-Remote-01.ino
 * 
 * By: Mike Klepper
 * Date: 26 April 2020
 * 
 * Demonstrates how to turn on/off the IR LED on the IR Remote
 */

#include "M5Atom.h"

int outputPin = 26;

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

void loop() 
{
  digitalWrite(outputPin, HIGH);
  delay(500);
  M5.update();

  digitalWrite(outputPin, LOW);
  delay(500);
  M5.update();
}


Application: Grove-IR-Remote-02 (Same as IRrecvDumpV2)

To read IR signals, first install the "IRremoteESP8266" library using the Library Manager - it works with the ESP32 as well. One of the example sketches is called IRrecvDumpV2, reproduced as-is with comments and all, except that kRecvPin has been set to 32.

  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
#include "M5Atom.h"

#include <IRrecv.h>
#include <IRremoteESP8266.h>
#include <IRac.h>
#include <IRtext.h>
#include <IRutils.h>

// ==================== start of TUNEABLE PARAMETERS ====================
// An IR detector/demodulator is connected to GPIO pin 14
// e.g. D5 on a NodeMCU board.
// Note: GPIO 16 won't work on the ESP8266 as it does not have interrupts.
const uint16_t kRecvPin = 32;

// The Serial connection baud rate.
// i.e. Status message will be sent to the PC at this baud rate.
// Try to avoid slow speeds like 9600, as you will miss messages and
// cause other problems. 115200 (or faster) is recommended.
// NOTE: Make sure you set your Serial Monitor to the same speed.
const uint32_t kBaudRate = 115200;

// As this program is a special purpose capture/decoder, let us use a larger
// than normal buffer so we can handle Air Conditioner remote codes.
const uint16_t kCaptureBufferSize = 1024;

// kTimeout is the Nr. of milli-Seconds of no-more-data before we consider a
// message ended.
// This parameter is an interesting trade-off. The longer the timeout, the more
// complex a message it can capture. e.g. Some device protocols will send
// multiple message packets in quick succession, like Air Conditioner remotes.
// Air Coniditioner protocols often have a considerable gap (20-40+ms) between
// packets.
// The downside of a large timeout value is a lot of less complex protocols
// send multiple messages when the remote's button is held down. The gap between
// them is often also around 20+ms. This can result in the raw data be 2-3+
// times larger than needed as it has captured 2-3+ messages in a single
// capture. Setting a low timeout value can resolve this.
// So, choosing the best kTimeout value for your use particular case is
// quite nuanced. Good luck and happy hunting.
// NOTE: Don't exceed kMaxTimeoutMs. Typically 130ms.
#if DECODE_AC
// Some A/C units have gaps in their protocols of ~40ms. e.g. Kelvinator
// A value this large may swallow repeats of some protocols
const uint8_t kTimeout = 50;
#else   // DECODE_AC
// Suits most messages, while not swallowing many repeats.
const uint8_t kTimeout = 15;
#endif  // DECODE_AC
// Alternatives:
// const uint8_t kTimeout = 90;
// Suits messages with big gaps like XMP-1 & some aircon units, but can
// accidentally swallow repeated messages in the rawData[] output.
//
// const uint8_t kTimeout = kMaxTimeoutMs;
// This will set it to our currently allowed maximum.
// Values this high are problematic because it is roughly the typical boundary
// where most messages repeat.
// e.g. It will stop decoding a message and start sending it to serial at
//      precisely the time when the next message is likely to be transmitted,
//      and may miss it.

// Set the smallest sized "UNKNOWN" message packets we actually care about.
// This value helps reduce the false-positive detection rate of IR background
// noise as real messages. The chances of background IR noise getting detected
// as a message increases with the length of the kTimeout value. (See above)
// The downside of setting this message too large is you can miss some valid
// short messages for protocols that this library doesn't yet decode.
//
// Set higher if you get lots of random short UNKNOWN messages when nothing
// should be sending a message.
// Set lower if you are sure your setup is working, but it doesn't see messages
// from your device. (e.g. Other IR remotes work.)
// NOTE: Set this value very high to effectively turn off UNKNOWN detection.
const uint16_t kMinUnknownSize = 12;

// Legacy (No longer supported!)
//
// Change to `true` if you miss/need the old "Raw Timing[]" display.
#define LEGACY_TIMING_INFO false
// ==================== end of TUNEABLE PARAMETERS ====================

// Use turn on the save buffer feature for more complete capture coverage.
IRrecv irrecv(kRecvPin, kCaptureBufferSize, kTimeout, true);
decode_results results;  // Somewhere to store the results

// This section of code runs only once at start-up.
void setup() {
M5.begin(true, false, true);
  delay(10);
  
#if defined(ESP8266)
  Serial.begin(kBaudRate, SERIAL_8N1, SERIAL_TX_ONLY);
#else  // ESP8266
  Serial.begin(kBaudRate, SERIAL_8N1);
#endif  // ESP8266
  while (!Serial)  // Wait for the serial connection to be establised.
    delay(50);
  Serial.printf("\n" D_STR_IRRECVDUMP_STARTUP "\n", kRecvPin);
#if DECODE_HASH
  // Ignore messages with less than minimum on or off pulses.
  irrecv.setUnknownThreshold(kMinUnknownSize);
#endif  // DECODE_HASH
  irrecv.enableIRIn();  // Start the receiver
}

// The repeating section of the code
void loop() {
  // Check if the IR code has been received.
  if (irrecv.decode(&results)) {
    // Display a crude timestamp.
    uint32_t now = millis();
    Serial.printf(D_STR_TIMESTAMP " : %06u.%03u\n", now / 1000, now % 1000);
    // Check if we got an IR message that was to big for our capture buffer.
    if (results.overflow)
      Serial.printf(D_WARN_BUFFERFULL "\n", kCaptureBufferSize);
    // Display the library version the message was captured with.
    Serial.println(D_STR_LIBRARY "   : v" _IRREMOTEESP8266_VERSION_ "\n");
    // Display the basic output of what we found.
    Serial.print(resultToHumanReadableBasic(&results));
    // Display any extra A/C info if we have it.
    String description = IRAcUtils::resultAcToString(&results);
    if (description.length()) Serial.println(D_STR_MESGDESC ": " + description);
    yield();  // Feed the WDT as the text output can take a while to print.
#if LEGACY_TIMING_INFO
    // Output legacy RAW timing info of the result.
    Serial.println(resultToTimingInfo(&results));
    yield();  // Feed the WDT (again)
#endif  // LEGACY_TIMING_INFO
    // Output the results as source code
    Serial.println(resultToSourceCode(&results));
    Serial.println();    // Blank line between entries
    yield();             // Feed the WDT (again)
  }
}


Reading IR Codes

To use this application, first open the serial monitor. Then point your remote at it and press one of the remote's buttons. Information about the IR signal sent by the remote will be displayed. The number we are interested in is called uint64_t data, like in the following screenshot. Write down the name of the button and the corresponding uint64_t data - that is the info we use for our remote emulator. For example, pressing the on/off button generates 0xA8B47 - your remote will likely be different. The complete code is in a later blog post!

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

ATOM Matrix: Using an I2C Keyboard

Besides supporting analog sensors, the ATOM's Grove port can also be used to interface with I2C sensors. This post demonstrates how to do this using an I2C keyboard. First we read keyboard input into a character array. In the second application we filter and transform incoming characters. Finally, we combine that with the scrolling text message code from an earlier post.

The first two applications should work almost unchanged on regular ESP32 (or even ESP8266) dev boards.

Experimenting (i.e. playing) with the keyboard, there is a problem: the Sym + "." key does NOT return the ">" character.


Store Keyboard Input into a Character Array

The CardKB is handy method of getting user input, but for getting sequences of characters, we must build the result one keypress at a time. The following application does that, and in addition:

  • The maximum length of the character array is enforced
  • When the backspace key is pressed, the last character of the array is removed
  • When the escape key is pressed, the character array is cleared
  • When the return key is pressed, input is "finalized" in that no additional characters are added to the result

 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
/*
 * CardKB-Keyboard-00.ino
 * 
 * By: Mike Klepper
 * Date: 26 April 2020
 * 
 * Read input from an I2C keyboard and store
 * result in a character array
 * 
 * Demonstrates basic char array usage
 * 
 * See post on patriot-geek.blogspot.com
 * for details
 */

#include "M5Atom.h"

#define CARDKB_ADDR 0x5F

#define BKSP 8
#define CR 13
#define ESC 27

const int maxMessageLength = 16;
char msg[maxMessageLength];
uint8_t charIndex = 0;
bool messageEntered = false;

void setup() 
{
  M5.begin(true, true, true);
  delay(20);
  Wire.begin(26, 32);
  Serial.println("");
}

void loop() 
{
  Wire.requestFrom(CARDKB_ADDR, 1);

  while(Wire.available() && !messageEntered)
  {
    char c = Wire.read();
    
    if(c != 0)
    {
      Serial.print(c);
      Serial.print(F(" - "));
      Serial.println(c, DEC);

      if(charIndex >= 0 && charIndex < maxMessageLength || c == BKSP || c == CR || c == ESC)
      {
        if(c != BKSP && c != CR && c != ESC)
        {
          msg[charIndex++] = c;
        }
        else if(c == BKSP && charIndex > 0)
        {
          msg[--charIndex] = '\0';
          Serial.print(F("Backspace! "));
        }
        else if(c == ESC)
        {
          charIndex = 0;
          memset(msg, 0, sizeof(msg));
          Serial.print(F("Resetting input! "));
        }
        else if(c == CR)
        {
          messageEntered = true;
          Serial.print(F("Done! "));
        }
      }
      else
      {
        Serial.print(F("Maximum message length reached! "));
      }

      Serial.print(F("msg = "));
      Serial.println(msg);
      Serial.print(F("charIndex = "));
      Serial.println(charIndex);
    }
  }
}

First thing to notice is that Wire.h is apparently not included. As it goes, when the second argument of the M5.begin command is true (line 31), the M5 API includes Wire.h for us. Here's a line-by-line explanation of the code:

Line Comment
18 I2C bus address of the CardKB
20 - 22 ASCII code for backspace, enter key, and escape key
25 Array of characters, with length specified in line above
26 Current character (think of it as a cursor position)
27 When messageEntered becomes true, we are done getting input!
31 Second argument is true, so Wire.h will be included
33 Initialize the Wire library using pins 26 and 32 for SCL and SDA, respectively
41 If messageEntered is false
43 Read one byte as a character
45 If we have a non-zero character
47 - 49 Print it in the serial monitor
51 Enforce max length requirement, but let BKSP, CR, and ESC through
53 If character is not one of those three
55 Increment charIndex and add c to end of the char array
57 If incoming character is a backspace
59 Decrement charIndex and fill with null terminator
62 If it is the escape character
64 Reset charIndex
65 Empty out the msg char array
68 - 72 If character is carriage return, we are done!
76 Ignore incoming character as msg is as long as we allow
79 - 82 Display the msg and the charIndex after each keypress


Filtering and Transforming Keyboard Input

In order to display the message entered by the keyboard, we must do the following:

  • Lower-case letters are converted to upper case letters (since that is what the 5 x 5 font has available)
  • Characters other than letters and digits are ignored (for the same reason)

It makes sense to do these just before the incoming character is added to the msg char array.

 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
/*
 * CardKB-Keyboard-01.ino
 * 
 * By: Mike Klepper
 * Date: 26 April 2020
 * 
 * Read input from an I2C keyboard, filter and 
 * transform it
 * 
 * See post on patriot-geek.blogspot.com
 * for details
 */

#include "M5Atom.h"

#define CARDKB_ADDR 0x5F

#define BKSP 8
#define CR 13
#define ESC 27

const int maxMessageLength = 16;
char msg[maxMessageLength];
uint8_t charIndex = 0;
bool messageEntered = false;

void setup() 
{
  M5.begin(true, true, true);
  delay(20);
  Wire.begin(26, 32);
  Serial.println("");
}

void loop() 
{
  Wire.requestFrom(CARDKB_ADDR, 1);

  while(Wire.available() && !messageEntered)
  {
    char c = Wire.read();
    
    if(c != 0)
    {
      Serial.print(c);
      Serial.print(F(" - "));
      Serial.println(c, DEC);

      if(charIndex >= 0 && charIndex < maxMessageLength || c == BKSP || c == CR || c == ESC)
      {
        if(c >= '0' && c <= '9' || c >= 'A' && c <= 'Z' || c == ' ')
        {
          msg[charIndex++] = c;
        }
        else if(c >= 'a' && c <= 'z')
        {
          msg[charIndex++] = c - 32;
        }
        else if(c == BKSP && charIndex > 0)
        {
          msg[--charIndex] = '\0';
          Serial.print(F("Backspace! "));
        }
        else if(c == ESC)
        {
          charIndex = 0;
          memset(msg, 0, sizeof(msg));
          Serial.print(F("Resetting input! "));
        }
        else if(c == CR)
        {
          messageEntered = true;
          Serial.print(F("Done! "));
        }
      }
      else
      {
        Serial.print(F("Maximum message length reached! "));
      }

      Serial.print(F("msg = "));
      Serial.println(msg);
      Serial.print(F("charIndex = "));
      Serial.println(charIndex);
    }
  }
}

The only real difference between this code and the previous app's is that instead of indiscriminately adding the incoming character to the end of msg (lines 53-56 in first application) we do the following:

Line Comment
51 If the incoming character is either a digit, an upper-case letter, or a space
53 Increment charIndex and add it to the end of msg
55 Else if the incoming character is a lower-case letter
57 Increment charIndex, convert it to uppercase, and add it to the end of msg


Displaying Input

This last application combines the above keyboard input code with the code for displaying scrolling text from an earlier post, each wrapped in its own function.

  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
/*
 * CardKB-Keyboard-02.ino
 * 
 * By: Mike Klepper
 * Date: 26 April 2020
 * 
 * Read input from an I2C keyboard and display it!
 * 
 * See post on patriot-geek.blogspot.com
 * for details
 */

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

#define DISPLAY_PIN 27
#define CARDKB_ADDR 0x5F

#define BKSP 8
#define CR 13
#define ESC 27

int directionAndOrientation = NEO_MATRIX_TOP + NEO_MATRIX_LEFT + NEO_MATRIX_ROWS;
int pixelType = NEO_GRB + NEO_KHZ800;

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

int xPos  = matrix.width();

const int maxMessageLength = 16;
char msg[maxMessageLength];
uint8_t charIndex = 0;
bool messageEntered = false;

void setup() 
{
  M5.begin(true, true, true);
  delay(20);
  Wire.begin(26, 32);
  Serial.println("");

  matrix.begin();
  matrix.setTextWrap(false);
  matrix.setBrightness(60);
  matrix.setTextColor(matrix.Color(80, 0, 80));
}

void loop()
{
  if(!messageEntered)
  {
    getMessageFromCardKB();
  }
  else
  {
    displayMessage(msg);
  }
}


void getMessageFromCardKB() 
{
  Wire.requestFrom(CARDKB_ADDR, 1);

  while(Wire.available() && !messageEntered)
  {
    char c = Wire.read();
    
    if(c != 0)
    {
      Serial.print(c);
      Serial.print(F(" - "));
      Serial.println(c, DEC);

      if(charIndex >= 0 && charIndex < maxMessageLength || c == BKSP || c == CR || c == ESC)
      {
        if(c >= '0' && c <= '9' || c >= 'A' && c <= 'Z' || c == ' ')
        {
          msg[charIndex++] = c;
        }
        else if(c >= 'a' && c <= 'z')
        {
          msg[charIndex++] = c - 32;
        }
        else if(c == BKSP && charIndex > 0)
        {
          msg[--charIndex] = '\0';
          Serial.print(F("Backspace! "));
        }
        else if(c == ESC)
        {
          charIndex = 0;
          memset(msg, 0, sizeof(msg));
          Serial.print(F("Resetting input! "));
        }
        else if(c == CR)
        {
          messageEntered = true;
          Serial.print(F("Done! "));
        }
      }
      else
      {
        Serial.print(F("Maximum message length reached! "));
      }

      Serial.print(F("msg = "));
      Serial.println(msg);
      Serial.print(F("charIndex = "));
      Serial.println(charIndex);
    }

    delay(1);
  }
}


void displayMessage(char message[])
{
  Serial.print("Displaying ");
  Serial.println(msg);

  String msgString = String(message);
  int msgLength = msgString.length();
  int charWidth = 5;
  int numTrailingSpaces = 3;
  int maxLeftPosition = (msgLength + numTrailingSpaces) * (charWidth + 1);
  
  int scrollDelay = 100;

  while(true)
  {
    matrix.fillScreen(0);
    matrix.setCursor(xPos, 0);
    matrix.print(msgString);
  
    if(--xPos < -maxLeftPosition)
    {
      xPos = matrix.width();
    }
    
    matrix.show();
    delay(scrollDelay);
  }
}

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