#25projectsofchristmas
The M5STickC-plus has a built-in accelerometer. And a display. With this, you can easily program digital dice that generates new values when you shake the device. In addition, the M5STickC-plus also has a WiFi chip that allows it to connect to the Internet. And this is where IOT comes into the game: Imagine playing dice with your friends to see who pays the bill at the pub. The person whose estimate is the furthest away has to pay. You shake the M5Stick and place it face down on the table. Everyone has to guess a value, but you check your smart phone first to see what value was rolled, because you don't have enough money in your wallet to pay the bill.
Dice displayThe values of two dice should be displayed on the display and the dice should look like dice:
To achieve this, first an image of a die without dots is loaded as background image.
Now, according to the randomly generated value between 1 and 6, the points must be drawn on the area of the die. For this purpose, the coordinates of the points for the 6 possible values are stored in a 3 dimensional array:
// Array to define the position of the dots on the dice
int dot_positions[7][6][2] {
{}, // 0
{{55,55}}, // 1
{{25,25},{85,85}}, // 2
{{25,25},{55,55},{85,85}}, // 3
{{25,25},{85,25},{25,85},{85,85}}, // 4
{{25,25},{85,25},{55,55},{25,85},{85,85}},// 5
{{25,25},{25,55},{25,85},{85,25},{85,55},{85,85}}, // 6
};
I think these values need to be explained a little more detailed: The display of the M5StickC-plus has a resolution of 135*240 pixels. Space enough for two die images with a size of 110x110 pixels. With the origin at the top left, the coordinates for each possible point position on the die can be defined like this:
If now a 2 is rolled, then two circles must be drawn on the display at the corresponding positions. And to get the dots at the right position for the die, the coordinates must be shifted by the target position of the die on the display:
The dot_positions[] array contains the corresponding positions of the circles for all 6 values the die can take. This can then be used to program a simple function that draws the corresponding arrangement of dots for each numerical value between 1 and 6:
// function to draw the dice
void draw_dice(int16_t x, int16_t y, int dice_value) {
// M5StickC-plus Display size is 135x240
// Dice size is 110x110
M5.Lcd.pushImage(x, y, 110, 110, (uint16_t *)Dice_background);
if(dice_value > 0 && dice_value < 7){
for(int dot_index = 0; dot_index < dice_value; dot_index++) {
M5.Lcd.fillCircle(x+dot_positions[dice_value][dot_index][0],
y+dot_positions[dice_value][dot_index][1],
DOT_SIZE, TFT_BLACK);
}
}
}
The function call for a die with the value of 2 like described above will look like this:
draw_dice(12,9, 2);
Now we can draw dice but the application still needs to be made to run in a nice process. So we need a "process sequence control stuff functionthing" or in other words: We need a state machine!
Finite State MachineA so called "state machine" is a behavioral model for process sequences. It consists of a finite number of states and is therefore often called a Finite State Machine (FSM). Simply said: Based on a current state and a given input, the state machine performs state transitions and produces outputs. This sounds more complicated than it is. For the dice software we can define three simple states:
The software should remain in the start state until a shake of the device is detected. Then the result should only be displayed when shaking has stopped. therefore, the software should remain in the shaking state until the end of shaking has been detected. At the end, the software should remain in the display state until the button on the M5Stick was pressed. This is the basic principle of a state machine.
To represent the flow control of the dice program the following states are defined in the code:
// 0 = start with printing text
#define START_STATE 0
// 10 = waiting for start shaking
#define WAIT_STATE 10
// 20 = generate random numbers and wait for stop shaking
#define SHAKE_STATE 20
// 30 = display dice #1 value
#define DISPLAY1_STATE 30
// 40 = display dice #2 value
#define DISPLAY2_STATE 40
// 50 = wait for button press to start new game
#define BUTTON_STATE 50
A detailed graphical representation of the sequence of operation looks like this:
There are several ways to implement a state machine. Since only a simple state machine is needed for this software, the implementation can also be realized via a simple switch() function:
// state machine cases
switch(process_State) {
// ******** START_STATE = start with printing text
case START_STATE :
...
// next state
process_State = WAIT_STATE;
break;
// ******** WAIT_STATE = waiting for acceleration (start shaking)
case WAIT_STATE :
...
process_State = SHAKE_STATE;
break;
// ******** SHAKE_STATE = generate random numbers and wait for stop shaking
case SHAKE_STATE :
...
process_State = DISPLAY1_STATE;
break;
// ******** DISPLAY1_STATE = display dice #1
case DISPLAY1_STATE :
...
process_State = DISPLAY2_STATE;
break;
// ******** DISPLAY2_STATE = display dice #2
case DISPLAY2_STATE :
...
process_State = BUTTON_STATE;
break;
// ******** BUTTON_STATE = wait for button press to start new roll
case BUTTON_STATE :
...
process_State = START_STATE;
break;
}
In each "switch state" the corresponding functions are implemented. Please check the full source code for details. Basically that's the whole thing. Not as complicated as it sounded at first, right?
If you want to learn how to deal with background images, please check my "M5Stack Screen Capture" project or my "M5Stack Christmas Snow Globe" project.
Shake detectionThe integrated acceleration sensor (IMU) measures the acceleration in 3 axes (X, Y and Z):
The shaking to the left and right can therefore be determined by the acceleration along the X-axis. But no knocking, hitting or tapping should be detected, only actual shaking. A simple method is to take two measurements within a short time interval. The absolute difference of these values can then be used as an indicator of how much the device was accelerated. The direction does not matter (to the left or to the right), since the absolute value is calculated. However, this value would also detect knocking on the device. To prevent this, a moving average over 10 measurements can be calculated, which filters out high frequency data like knocking or tapping.
If you would write a function that measures the acceleration 10 times with a interval of 100ms to calculate the average value, the whole software would be blocked for 10*100ms = 1 second. This is not good. One should always avoid such blocking functions. Instead of looping inside the function for 10 measurements, it is better to let the function take only one measurement and then store the result in a global variable. Within the state machine, the function is called inside the WAIT_STATE and the SHAKE_STATE, which results in an non-blocking flow of the state machine.
Moving Average filter functionUsually one would calculate the average value by summing up the single values and then dividing the sum by the number of summed values:
This method works fine for block by block calculation of average values: You simply sum up a number of values and divide the sum at the end by the number of values. For the next block, you start again with summing up new values. But if you want to calculate the average for each new value (moving average) you have to delete the first value of the list and add the new value at the end. Programming such a method is cumbersome and mathematically unnecessary.
An alternative (and, by the way, mathematically identical) method is to multiply the last calculated average value by the number of values decremented by 1, then add the new value to it and divide this sum by the number of values to be averaged:
This method is fast an need less memory than storing a list of 10 values.
Technically, this is kind of low-pass filter and the theory behind this can become almost arbitrarily complex, but for this simple program a suitable value can be well determined by trial and error. The higher the value for the number of values to be averaged, the more high-frequency signals are filtered out, but the more latency is created. The implementation of the function to detect shaking looks like this:
int mean_accX_n = 8;
.....
float get_horizontal_shaking(){
// two values of X-acceleration
float accX_1, accX_2;
// Y and Z values are needed for function call
float accY, accZ;
// differential value of X-acceleration
float accX_d = 0;
// get the first and the second sensor data
M5.Imu.getAccelData(&accX_1,&accY,&accZ);
delay(100);
M5.Imu.getAccelData(&accX_2,&accY,&accZ);
// calculate the absolute differential value
accX_d = fabs(accX_2 - accX_1);
// building the mean value (Moving average calculation)
mean_accX_d = ((mean_accX_d * (mean_accX_n-1)) + accX_d)/mean_accX_n;
return mean_accX_d;
}
A return value greater than 3 indicates that the device is shaken. A value less than 1 indicates that shaking has stopped:
With this, the dice software is ready to run and you can roll for the pub bill. The only thing missing is the cheating function.
IOT cheatingWith some simple lines of code, the M5StickC becomes a WiFi access point:
// WiFi network configuration:
char wifi_ssid[] = "M5StickC-plus";
char wifi_key[] = "1234567890";
IPAddress ip(192, 168, 0, 1);
IPAddress gateway(192, 168, 0, 1);
IPAddress subnet(255, 255, 255, 0);
WiFiClient myclient;
WiFiServer server(80);
void web_server_init(){
WiFi.mode(WIFI_AP);
WiFi.softAP(wifi_ssid, wifi_key);
WiFi.softAPConfig(ip, gateway, subnet);
WiFi.begin();
// Start TCP/IP-Server
server.begin();
}
Now you can connect your cell phone to the WiFi access point created by the M5Stick. And at the same time a web server is waiting for you to connect to the IP address with a browser.
I have already described the functionality of the web server in my project "M5ATOM ENV mini data monitor". For the dice web page, I also used dynamic HTML code to include the values of the cubes via an external Java script request.:
<script>
window.onload = function(){
document.getElementById('dice1').innerHTML = dice1value;
document.getElementById('dice2').innerHTML = dice2value;
};
</script>
<script type="text/javascript" src="dicevalue.js"></script>
The values of the two dice rolled are defined as global variables. When the web browser requests the Java script file "dicevalue.js", the web server generates it based on the current values:
case GET_dicevalue: {
client.println("HTTP/1.1 200 OK");
client.println("Content-type:application/javascript");
client.println();
client.printf("var dice1value = %c;\n", dice1_html_value);
client.printf("var dice2value = %c;\n", dice2_html_value);
break;
}
Everything else is exactly as described in my project "M5ATOM ENV mini data monitor". However, in this code I have outsourced the web server functions to an external file. This keeps the code of the dice software simple and clean. The web server with access point only needs to be initialized in the setup() function and called in the loop() function on each run:
void setup() {
M5.begin();
...
// init and start the web-server for the dice-web-page
web_server_init();
delay(3000);
}
void loop() {
M5.update();
...
// check for web browser requests
web_server_update();
delay(20);
}
Now you can go to the pub with an empty wallet and have a drink with your friends. But make sure that the battery of the M5Stick is always well charged, because if someone takes out real dice you have to rely on your luck and not on your smart phone.
FeedbackI hope you like this short fun project and this code can prove to be useful to some of you. Feel free to message me here if you have questions or comments.
Enjoy rolling the dice!
Regards,
Hans-Günther
Comments