Hans-Günther Nusseck
Published © MIT

M5Stack Christmas Snow Globe

Modern times make modern solutions possible. Therefore, the step to a digital snow globe is not that far.

BeginnerFull instructions provided1 hour2,492

Things used in this project

Hardware components

M5Stack FIRE IoT Development Kit (PSRAM 2.0)
M5Stack FIRE IoT Development Kit (PSRAM 2.0)
×1
ESP32 GREY Development Kit with 9Axis Sensor
M5Stack ESP32 GREY Development Kit with 9Axis Sensor
×1
M5Stack CORE2
×1

Software apps and online services

PlatformIO IDE
PlatformIO IDE
The R Project

Story

Read more

Code

main.cpp

C/C++
The code to turn the M5Stack into a digital Snow-Globe
/**************************************************************************
 * M5Stack IMU Snow-Globe
 * 
 * A simple program that turns an M5Stack into a digital snow globe.
 * Works with M5Stack Gray, FIRE and CORE2
 * 
 * Hague Nusseck @ electricidea
 * v1.20 25.December.2020
 * https://github.com/electricidea/M5Stack-Snow-Globe
 * 
 * Changelog:
 * v1.01 = - initial version for the M5Stack-fire
 * v1.20 = - 8 bit Sprite instead of 16 bit sprite
 *         - NO PSRAM needed anymore
 *         - works with M5Stack Gray and FIRE
 *         - pushing the sprite without transparency color
 * v1.21 = - compiler options for the different M5Stack devices
 * 
 * Distributed as-is; no warranty is given.
**************************************************************************/

//Select the device for which the code should be compiled and build:
//#define M5STACK_GRAY
#define M5STACK_FIRE
//#define M5STACK_CORE2

// REMEMBER:
// Also change the "board" option in the platformio.ini:
//
// board = m5stack-grey
// board = m5stack-fire
// board = m5stack-core-esp32

#include <Arduino.h>

#if defined (M5STACK_CORE2)
  #include <M5Core2.h>
  // install the library:
  // pio lib install "m5stack/M5Core2"

#else // M5STACK_GRAY or M5STACK_FIRE

  // For the M5Stack FIRE and M5Stack-gray, this definition 
  // must be placed before the #include <M5Stack.h>:
  // #define M5STACK_MPU6886 
  #define M5STACK_MPU9250 
  // #define M5STACK_MPU6050
  // #define M5STACK_200Q

  #include <M5Stack.h>
  // install the library:
  // pio lib install "M5Stack"

#endif

// Free Fonts for nice looking fonts on the screen
#include "Free_Fonts.h"

// logo with 150x150 pixel size in XBM format
// check the file header for more information
#include "electric-idea_logo.h"

// Stuff for the Graphical output
// The M5Stack screen pixel is 320x240, with the top left corner of the screen as the origin (0,0)
#define SCREEN_WIDTH 320
#define SCREEN_HEIGHT 240

// acceleration values
float accX = 0.0F;
float accY = 0.0F;
float accZ = 0.0F;
/*  Direction of acceleration:

      . down = +z
  - - - - -
  |       |
  |       | --> -x
  | O O O |
  - - - - - 
      |
      V
     +y 
*/

// bitmap of one snowflake (XBM Format)
#include "snowflake.h"

// background image as uint16_t RGB565 array
#include "background_image.h"

// structure for the position and speed of every snow flake
const int flake_max = 250;
struct flakeObject
{
  int32_t x;
  int32_t y;
  float speed;
};

flakeObject flakeArray[flake_max];

// Sprite object "img" with pointer to "M5.Lcd" object
// the pointer is used by pushSprite() to push it onto the LCD
TFT_eSprite img = TFT_eSprite(&M5.Lcd);  

// color depth of the sprite
// can be: 1, 8 or 16
// NOTE: 16bit fullscreen sprites require more memory. 
// They only work with the M5Stack Fire or Core2 with activated PSRAM.
#define SPRITE_COLOR_DEPTH 8

void setup(void) {
  M5.begin();
  #if defined (M5STACK_FIRE)
    M5.Power.begin();
  #endif
  M5.IMU.Init();
  // int the starting position of all snowflakes
  for(int i=0; i < flake_max; i++){
    // horizontally distributed
    flakeArray[i].x = random(SCREEN_WIDTH);
    // at the bottom of the screen
    flakeArray[i].y = SCREEN_HEIGHT-random(20);
    // individual speed for each snowflake
    flakeArray[i].speed = (random(80)+20)/100.0;
  }
  // electric-idea logo
  M5.Lcd.fillScreen(TFT_BLACK);
  M5.Lcd.drawXBitmap((int)(320-logoWidth)/2, (int)(240-logoHeight)/2, logo, logoWidth, logoHeight, TFT_WHITE);
  delay(1500);
  // Create a sprite
  img.setColorDepth(SPRITE_COLOR_DEPTH);
  img.createSprite(SCREEN_WIDTH, SCREEN_HEIGHT);
  // welcome text
  M5.Lcd.setTextColor(TFT_WHITE);
  M5.Lcd.setTextSize(1);
  M5.Lcd.fillScreen(TFT_BLACK);
  // configure centered String output
  M5.Lcd.setTextDatum(CC_DATUM);
  M5.Lcd.setFreeFont(FF2);
  M5.Lcd.drawString("IMU Snow Globe", (int)(M5.Lcd.width()/2), (int)(M5.Lcd.height()/2), 1);
  M5.Lcd.setFreeFont(FF1);
  M5.Lcd.drawString("Version 1.21 | 25.12.2020", (int)(M5.Lcd.width()/2), M5.Lcd.height()-20, 1);
  delay(1500);
}

void loop() {
  M5.update();
  // mute speaker
  dacWrite (25,0);
  // push the background image to the sprite
  img.pushImage(0, 0, 320, 240, (uint16_t *)background_image);
  // get the acceleration data
  // values are in g (9.81 m/s2)
  M5.IMU.getAccelData(&accX,&accY,&accZ);
  // Draw the snowflakes
  for (int i=0; i < flake_max; i++)
  {
    // detect shaking
    if(fabs(accX) > 2 || fabs(accY) > 2){
      flakeArray[i].x = random(SCREEN_WIDTH);
      flakeArray[i].y = random(SCREEN_HEIGHT);
    }
    else {
      // use gravity vector for movement
      float dx = (accX*-10.0) + (round(accX)*random(5)) + (round(accY)*(random(10)-5));
      float dy = (accY*10.0) +  (round(accX)*random(5)) + (round(accY)*(random(10)-5));
      flakeArray[i].x = flakeArray[i].x + round(dx*flakeArray[i].speed);
      flakeArray[i].y = flakeArray[i].y + round(dy*flakeArray[i].speed);
    }
    // keep the snowflakes on the screen
    if(flakeArray[i].x < 0)
      flakeArray[i].x = 0;
    if(flakeArray[i].x > SCREEN_WIDTH)
      flakeArray[i].x = SCREEN_WIDTH;
    if(flakeArray[i].y < 0)
      flakeArray[i].y = 0;
    if(flakeArray[i].y > SCREEN_HEIGHT)
      flakeArray[i].y = SCREEN_HEIGHT;
    // push the snowflake to the sprite on top of the background image
    img.drawXBitmap((int)(flakeArray[i].x-flakeWidth), 
                       (int)(flakeArray[i].y-flakeHeight), 
                       snowflake, flakeWidth, flakeHeight, TFT_WHITE);
  }
  // After all snowflakes are drawn, pus the sprite
  // to TFT screen CGRAM at coordinate x,y (top left corner)
  img.pushSprite(0, 0);
  delay(5);
}

Image-to-RGB565-Array.R

R
R script to convert an image to a uint16_t RGB565 array
# =============================================================================
# Image-to-RGB565-Array.R
# 
# R script to convert an image to a uint16_t RGB565 array
# 
# As output, a C-header file is created
# 
# Hague Nusseck @ electricidea
# v1.1 25.December.2020
#
# Windows command line call:
# "C:\Program Files\R\R-4.0.2\bin\Rscript.exe" Image-to-RGB565-Array.R
# 
# for details about using the imager library see:
# https://cran.r-project.org/web/packages/imager/vignettes/gettingstarted.html
# https://metacpan.org/pod/distribution/Imager/lib/Imager/Files.pod
#
# for details about RGB565 see:
# http://www.barth-dev.de/online/rgb565-color-picker/
# online RGB565 Color calculator:
# http://www.rinkydinkelectronics.com/calc_rgb565.php
# =============================================================================


# load library "imager"
# This call automatically loads and installs the required packages 
# if they are not already installed.
if (!require("imager")) {
  install.packages("imager", dependencies = TRUE)
  library(imager)
}

# ------------------------------------------------------------------------------

# open the "file open"-Dialog
ImageFileName <- file.choose()
# extract the file path
ImageFilePath <- dirname(ImageFileName)
# load image file
InputImage <- load.image(paste(ImageFilePath, basename(ImageFileName), sep="/"))

# display the loaded image file
# Note: If the script is started from the command line, then the image is 
# saved as a PDF in the same folder. Cool feature!
plot(InputImage)

# convert the image as data frame
ImageData <- as.data.frame(InputImage,wide="c") 
# c.1 = Red color scaled from 0.0 to 1.0
# c.2 = Green color scaled from 0.0 to 1.0
# c.3 = Blue color scaled from 0.0 to 1.0

# convert color values to RGB565
ImageData$RGB565 <- 
  bitwShiftL(bitwAnd(ImageData$c.1*0xFF, 0xF8), 8) + # Red   0b11111000 shl 8
  bitwShiftL(bitwAnd(ImageData$c.2*0xFF, 0xFC), 3) + # Green 0b11111100 shl 3
  bitwShiftR(bitwAnd(ImageData$c.3*0xFF, 0xF8), 3)   # Blue  0b11111000 shr 3
# prepare the string with leading "0x"
ImageData$RGB565str <- paste("0x", 
                             as.character.hexmode(ImageData$RGB565, 
                                                  width = 4, 
                                                  upper.case = TRUE), 
                             sep="")


# ------------------------------------------------------------------------------

# extract the image filename without extension
# this will be used as the name of the image
ImageName <- sub(pattern = "(.*)\\..*$", replacement = "\\1", basename(ImageFileName))

# replace each occurrence of dash and space characters with an underscore
ImageName <- gsub("-", "_", ImageName)
ImageName <- gsub(" ", "_", ImageName)

# build the output filename for the RGB565-Array
ArrayFileName <- paste(ImageFilePath, "/", ImageName, ".h", sep="")

# write the header information in the file
cat(file = ArrayFileName,
    "// ", basename(ArrayFileName), "\n",
    "// \n",
    "// uint16_t RGB565 image array\n",
    "// created by Image-to-RGB565-Array.R (R-Script)\n\n",
    "// Hague Nusseck @ electricidea\n",
    "// v1.1 25.December.2020\n",
    "// \n\n\n",
    "// original image:\n",
    "// ", basename(ImageFileName), "\n\n",
    "#define ", ImageName, "_WIDTH ", dim(InputImage)[1], "\n",
    "#define ", ImageName, "_HEIGHT ", dim(InputImage)[2], "\n\n",
    "// image array:\n", 
    "const uint16_t ", ImageName, "[", length(ImageData$RGB565str), "] = {\n", 
    sep = "", fill = FALSE, labels = NULL,
    append = FALSE)

# write the formatted RGB565 data to the output file
write.table(t(matrix(ImageData$RGB565str, 
                   nrow = dim(InputImage)[1],ncol = dim(InputImage)[2])), 
            ArrayFileName, append = TRUE, quote = FALSE, sep = ", ",
            eol = ",\n", na = "NA", dec = ".", row.names = FALSE,
            col.names = FALSE, qmethod = c("escape", "double"),
            fileEncoding = "")

# write the closing "}" to the file
cat(file = ArrayFileName, "};\n",
    sep = "", fill = FALSE, labels = NULL, append = TRUE)

# ------------------------------------------------------------------------------

github repository of the Snow-Globe Code

Credits

Hans-Günther Nusseck

Hans-Günther Nusseck

8 projects • 17 followers

Comments