Hans-Günther Nusseck
Published © MIT

M5Stack Screen Capture and remote control

A simple example of how to save a screenshot of the display on the SD card or show it on a web page - Including button remote control!

BeginnerFull instructions provided2 hours2,640
M5Stack Screen Capture and remote control

Things used in this project

Hardware components

M5Stack FIRE IoT Development Kit (PSRAM 2.0)
M5Stack FIRE IoT Development Kit (PSRAM 2.0)
×1

Software apps and online services

PlatformIO IDE
PlatformIO IDE

Story

Read more

Code

main.cpp

C/C++
The main program
/******************************************************************************
 * M5Stack Screen-Capture
 * Software routines to save a screenshot of the display to the SD card 
 * or SPIFFS. The image can also be sent to a client via WiFi (view in web browser).
 * The image can be saved in two formats: PPM or BMP.
 * 
 * Description:
 * After the device has booted up, the web page of the device can be called up 
 * via the displayed IP address. The screenshot is then displayed on that page. 
 * After 20 seconds, the gauge is automatically shown. The pointer arrow  moves 
 * back and forth randomly. The three buttons set the pointer either to 0%, 
 * to 50% or to 100%. Each time the button is pressed, a screenshot in BMP format 
 * is saved to the SD card. 
 * 
 * Hague Nusseck @ electricidea 
 * v1.0 | 28.November.2021
 * https://github.com/electricidea/M5Stack-Screen-Capture
 * 
 * 
 * to generate the gauge image in 565 color format:
 * https://github.com/m5stack/M5Stack/blob/master/examples/Advanced/Display/TFT_Flash_Bitmap/TFT_Flash_Bitmap.ino
 * https://github.com/mysensors/MySensorsArduinoExamples/blob/master/libraries/UTFT/Tools/ImageConverter565.exe
 * 
 * Distributed as-is; no warranty is given.
 ******************************************************************************/
#include <Arduino.h>


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

// 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"

// WIFI and https client librarys:
#include "WiFi.h"
#include <WiFiClientSecure.h>

// WiFi network configuration:
char wifi_ssid[33];
char wifi_key[65];
const char* ssid     = "YourWiFi";
const char* password = "YourPassword";

WiFiClient myclient;
WiFiServer server(80);

// GET request indication
#define GET_unknown 0
#define GET_index_page  1
#define GET_favicon  2
#define GET_logo  3
#define GET_refresh_img  4
#define GET_button_img  5
#define GET_screenshot  6
int html_get_request;

// website stuff
#include "index.h"
#include "electric_logo.h"
#include "favicon.h"
#include "button.h"
#include "refresh.h"

unsigned long next_millis;
// Flags for button presses via Web interface
bool Control_A_pressed = false;
bool Control_B_pressed = false;
bool Control_C_pressed = false;

// image for gauge display
#include "gauge.h"
// RAD = DEG * (pi/180).
#define DEG2RAD 0.01745329251994;
// the value for the gauge display
float gauge_val = 50.0;

// forward declarations:
void check_webserver();
boolean connect_Wifi();
bool M5Screen2bmp(WiFiClient &client);
bool M5Screen2bmp(fs::FS &fs, const char * path);
bool M5Screen2ppm(fs::FS &fs, const char * path);
void draw_gauge(float val_1, float val_2);


void setup() {
  M5.begin();
  M5.Power.begin();
  //Brightness (0: Off - 255: Full)
  M5.Lcd.setBrightness(100); 
  // draw start screen  
  M5.Lcd.fillScreen(BLACK);
  // draw logo in the center of the screen
  M5.Lcd.drawXBitmap((int)(320-logoWidth)/2, (int)(240-logoHeight)/2, logo, logoWidth, logoHeight, TFT_WHITE);
  // configure centered String output (Centre centre)
  M5.Lcd.setTextDatum(CC_DATUM);
  // select a nice font
  // FF4 : large (FreeMono24pt7b)
  // FF3 : medium (FreeMono18pt7b)
  // FF2 : normal (FreeMono12pt7b)
  // FF1 : small (FreeMono9pt7b)
  M5.Lcd.setFreeFont(FF2);
  M5.Lcd.setTextColor(TFT_LIGHTGREY);
  M5.Lcd.drawString("Screen Capture", (int)(M5.Lcd.width()/2), 20, 1);
  Serial.println("M5 Screen capture");
  Serial.println("v1.0 | 27.11.2021");
  // Byte Order for pushImage()
  // need to be set "true" to get the right color coding
  M5.Lcd.setSwapBytes(true);
  // Set WiFi to station mode and disconnect
  // from an AP if it was previously connected
  WiFi.mode(WIFI_STA);
  WiFi.disconnect();
  delay(1000);
  // connect to the configured AP
  connect_Wifi();
  // print the IP-Adress
  char String_buffer[128]; 
  snprintf(String_buffer, sizeof(String_buffer), "IP: %s\n",WiFi.localIP().toString().c_str());
  M5.Lcd.setFreeFont(FF1);
  M5.Lcd.setTextColor(TFT_WHITE);
  M5.Lcd.drawString(String_buffer, (int)(M5.Lcd.width()/2), M5.Lcd.height()-20, 1);
  // Start TCP/IP-Server
  server.begin();     
  // start gauge display after 20 seconds (or button press)
  next_millis = millis() + 20000;
}

void loop() {
  M5.update();  
  // get actual time in miliseconds
  unsigned long current_millis = millis();

  // left Button
  if (M5.BtnA.wasPressed() || Control_A_pressed){  
    Control_A_pressed = false;
    gauge_val = 0.0;
    draw_gauge(gauge_val, 50);
    M5Screen2bmp(SD, "/gauge_0.bmp");
    next_millis = millis() + 1000;
  }

  // center Button
  if (M5.BtnB.wasPressed() || Control_B_pressed){
    Control_B_pressed = false;
    gauge_val = 50.0;
    draw_gauge(gauge_val, 50);
    M5Screen2bmp(SD, "/gauge_50.bmp");
    next_millis = millis() + 1000;
  }

  // right Button
  if (M5.BtnC.wasPressed() || Control_C_pressed){
    Control_C_pressed = false;
    gauge_val = 100.0;
    draw_gauge(gauge_val, 50);
    M5Screen2bmp(SD, "/gauge_100.bmp");
    next_millis = millis() + 1000;
  }

  // check if next measure interval is reached
  if(current_millis > next_millis){
    // ramdom movements for gauge display
    gauge_val += random(0, 11)-5;
    if(gauge_val < 0) gauge_val = 0.0;
    if(gauge_val > 100) gauge_val = 100.0;
    draw_gauge(gauge_val, 50);
    next_millis = millis() + 1000;
  }

  // check for new clients and handle responses
  check_webserver();
  // The delay is important
  // otherwise ghost key presses of the A key may occur.
  delay(20);
}


/***************************************************************************************
* Function name:          check_webserver
* Description:            check for new clients and handle response generation
***************************************************************************************/
void check_webserver(){
  // check if WIFI is still connected
  // if the WIFI is not connected (anymore)
  // a reconnect is triggert
  wl_status_t wifi_Status = WiFi.status();
  if(wifi_Status != WL_CONNECTED){
    // reconnect if the connection get lost
    Serial.println("[ERR] Lost WiFi connection, reconnecting...");
    if(connect_Wifi()){
      Serial.println("[OK] WiFi reconnected");
    } else {
      Serial.println("[ERR] unable to reconnect");
    }
  }
  // check if WIFI is connected
  // needed because of the above mentioned reconnection attempt
  wifi_Status = WiFi.status();
  if(wifi_Status == WL_CONNECTED){
    // check for incoming clients
    WiFiClient client = server.available(); 
    if (client) {  
      // force a disconnect after 2 seconds
      unsigned long timeout_millis = millis()+2000;
      Serial.println("New Client.");  
      // a String to hold incoming data from the client line by line        
      String currentLine = "";                
      // loop while the client's connected
      while (client.connected()) { 
        // if the client is still connected after 2 seconds,
        // something is wrong. So kill the connection
        if(millis() > timeout_millis){
          Serial.println("Force Client stop!");  
          client.stop();
        } 
        // if there's bytes to read from the client,
        if (client.available()) {             
          char c = client.read();            
          Serial.write(c);    
          // if the byte is a newline character             
          if (c == '\n') {    
            // two newline characters in a row (empty line) are indicating
            // the end of the client HTTP request, so send a response:
            if (currentLine.length() == 0) {
              // HTTP headers always start with a response code (e.g. HTTP/1.1 200 OK)
              // and a content-type so the client knows what's coming, then a blank line,
              // followed by the content:
              switch (html_get_request)
              {
                case GET_index_page: {
                  client.println("HTTP/1.1 200 OK");
                  client.println("Content-type:text/html");
                  client.println();
                  client.write_P(index_html, sizeof(index_html));
                  break;
                }
                case GET_favicon: {
                  client.println("HTTP/1.1 200 OK");
                  client.println("Content-type:image/x-icon");
                  client.println();
                  client.write_P(electric_favicon, sizeof(electric_favicon));
                  break;
                }
                case GET_logo: {
                  client.println("HTTP/1.1 200 OK");
                  client.println("Content-type:image/jpeg");
                  client.println();
                  client.write_P(electric_logo, sizeof(electric_logo));
                  break;
                }
                case GET_screenshot: {              
                  client.println("HTTP/1.1 200 OK");
                  client.println("Content-type:image/bmp");
                  client.println();
                  M5Screen2bmp(client);
                  break;
                }
                case GET_refresh_img: {              
                  client.println("HTTP/1.1 200 OK");
                  client.println("Content-type:image/png");
                  client.println();
                  client.write_P(refresh_img, sizeof(refresh_img));
                  break;
                }
                case GET_button_img: {              
                  client.println("HTTP/1.1 200 OK");
                  client.println("Content-type:image/png");
                  client.println();
                  client.write_P(control_button_img, sizeof(control_button_img));
                  break;
                }
                default:
                  client.println("HTTP/1.1 404 Not Found");
                  client.println("Content-type:text/html");
                  client.println();
                  client.print("404 Page not found.<br>");
                  break;
              }
              // The HTTP response ends with another blank line:
              client.println();
              // break out of the while loop:
              break;
            } else {    // if a newline is found
              // Analyze the currentLine:
              // detect the specific GET requests:
              if(currentLine.startsWith("GET /")){
                html_get_request = GET_unknown;
                // if no specific target is requested
                if(currentLine.startsWith("GET / ")){
                  html_get_request = GET_index_page;
                }
                // if the logo image is requested
                if(currentLine.startsWith("GET /electric-idea_100x100.jpg")){
                  html_get_request = GET_logo;
                }
                // if the favicon icon is requested
                if(currentLine.startsWith("GET /favicon.ico")){
                  html_get_request = GET_favicon;
                }
                // if the screenshot image is requested
                if(currentLine.startsWith("GET /screenshot.bmp")){
                  html_get_request = GET_screenshot;
                }
                // if the refresh image is requested
                if(currentLine.startsWith("GET /refresh-40x30.png")){
                  html_get_request = GET_refresh_img;
                }
                // if the control-button image is requested
                if(currentLine.startsWith("GET /button.png")){
                  html_get_request = GET_button_img;
                }
                // if the control-button A was pressed on the HTML page
                if(currentLine.startsWith("GET /button-A")){
                  Control_A_pressed = true;
                  html_get_request = GET_index_page;
                }
                // if the control-button B was pressed on the HTML page
                if(currentLine.startsWith("GET /button-B")){
                  Control_B_pressed = true;
                  html_get_request = GET_index_page;
                }
                // if the control-button C was pressed on the HTML page
                if(currentLine.startsWith("GET /button-C")){
                  Control_C_pressed = true;
                  html_get_request = GET_index_page;
                }
              }
              currentLine = "";
            }
          } else if (c != '\r') {  
            // add anything else than a carriage return
            // character to the currentLine 
            currentLine += c;      
          }
        }
      }
      // close the connection:
      client.stop();
      Serial.println("Client Disconnected.");
    }
  }
}


/***************************************************************************************
* Function name:          M5Screen2ppm
* Description:            Dump the screen to a ppm image File
* Image file format:      .ppm
* return value:           true:  succesfully wrote screen to file
*                         false: unabel to open file for writing
* example for screen capture onto SD-Card: 
*                         M5Screen2ppm(SD, "/screen.ppm");
***************************************************************************************/
bool M5Screen2ppm(fs::FS &fs, const char * path){
  // Open file for writing
  // The existing image file will be replaced
  File file = fs.open(path, FILE_WRITE);
  if(file){
    int image_height = M5.Lcd.height();
    int image_width = M5.Lcd.width();
    // write PPM file header
    //    P6 - magical numer = file format indicator
    //          P6 =  Binary (raw) format
    //                16777216 colors (0–255 for each RGB channel)
    //    \n - CR = Blank space (Spaceholder)
    //    w h - width and heigt decimal in ASCII (Space-seperated)
    //    \n - CR = Blank space (Spaceholder)
    //    cmax - maximum color value (decimal in ASCII)
    //    \n - CR = Blank space (Spaceholder)
    file.printf("P6\n%d %d\n255\n", image_width, image_height);
    // To keep the required memory low, the image is captured line by line
    unsigned char line_data[image_width*3];
    // The function readRectRGB reads a screen area and returns the 
    // RGB 8 bit colour values of each pixel
    for(int y=0; y<image_height; y++){
      // get one line of the screen content
      M5.Lcd.readRectRGB(0, y, image_width, 1, line_data);
      // write the line to the file
      file.write(line_data, image_width*3);
    }
    file.close();
    return true;
  }
  return false;
}


/***************************************************************************************
* Function name:          M5Screen2bmp
* Description:            Dump the screen to a bmp image File
* Image file format:      .bmp
* return value:           true:  succesfully wrote screen to file
*                         false: unabel to open file for writing
* example for screen capture onto SD-Card: 
*                         M5Screen2bmp(SD, "/screen.bmp");
* inspired by: https://stackoverflow.com/a/58395323
***************************************************************************************/
bool M5Screen2bmp(fs::FS &fs, const char * path){
  // Open file for writing
  // The existing image file will be replaced
  File file = fs.open(path, FILE_WRITE);
  if(file){
    // M5Stack:      TFT_WIDTH = 240 / TFT_HEIGHT = 320
    // M5StickC:     TFT_WIDTH =  80 / TFT_HEIGHT = 160
    // M5StickCplus: TFT_WIDTH =  135 / TFT_HEIGHT = 240
    int image_height = M5.Lcd.height();
    int image_width = M5.Lcd.width();
    // horizontal line must be a multiple of 4 bytes long
    // add padding to fill lines with 0
    const uint pad=(4-(3*image_width)%4)%4;
    // header size is 54 bytes:
    //    File header = 14 bytes
    //    Info header = 40 bytes
    uint filesize=54+(3*image_width+pad)*image_height; 
    unsigned char header[54] = { 
      'B','M',  // BMP signature (Windows 3.1x, 95, NT, …)
      0,0,0,0,  // image file size in bytes
      0,0,0,0,  // reserved
      54,0,0,0, // start of pixel array
      40,0,0,0, // info header size
      0,0,0,0,  // image width
      0,0,0,0,  // image height
      1,0,      // number of color planes
      24,0,     // bits per pixel
      0,0,0,0,  // compression
      0,0,0,0,  // image size (can be 0 for uncompressed images)
      0,0,0,0,  // horizontal resolution (dpm)
      0,0,0,0,  // vertical resolution (dpm)
      0,0,0,0,  // colors in color table (0 = none)
      0,0,0,0 };// important color count (0 = all colors are important)
    // fill filesize, width and heigth in the header array
    for(uint i=0; i<4; i++) {
        header[ 2+i] = (char)((filesize>>(8*i))&255);
        header[18+i] = (char)((image_width   >>(8*i))&255);
        header[22+i] = (char)((image_height  >>(8*i))&255);
    }
    // write the header to the file
    file.write(header, 54);
    
    // To keep the required memory low, the image is captured line by line
    unsigned char line_data[image_width*3+pad];
    // initialize padded pixel with 0 
    for(int i=(image_width-1)*3; i<(image_width*3+pad); i++){
      line_data[i]=0;
    }
    // The coordinate origin of a BMP image is at the bottom left.
    // Therefore, the image must be read from bottom to top.
    for(int y=image_height; y>0; y--){
      // get one line of the screen content
      M5.Lcd.readRectRGB(0, y-1, image_width, 1, line_data);
      // BMP color order is: Blue, Green, Red
      // return values from readRectRGB is: Red, Green, Blue
      // therefore: R und B need to be swapped
      for(int x=0; x<image_width; x++){
        unsigned char r_buff = line_data[x*3];
        line_data[x*3] = line_data[x*3+2];
        line_data[x*3+2] = r_buff;
      }
      // write the line to the file
      file.write(line_data, (image_width*3)+pad);
    }
    file.close();
    return true;
  }
  return false;
}

/***************************************************************************************
* Function name:          M5Screen2bmp
* Description:            Dump the screen to a WiFi client
* Image file format:      Content-type:image/bmp
* return value:           always true
***************************************************************************************/
bool M5Screen2bmp(WiFiClient &client){
  int image_height = M5.Lcd.height();
  int image_width = M5.Lcd.width();
  const uint pad=(4-(3*image_width)%4)%4;
  uint filesize=54+(3*image_width+pad)*image_height; 
  unsigned char header[54] = { 
    'B','M',  // BMP signature (Windows 3.1x, 95, NT, …)
    0,0,0,0,  // image file size in bytes
    0,0,0,0,  // reserved
    54,0,0,0, // start of pixel array
    40,0,0,0, // info header size
    0,0,0,0,  // image width
    0,0,0,0,  // image height
    1,0,      // number of color planes
    24,0,     // bits per pixel
    0,0,0,0,  // compression
    0,0,0,0,  // image size (can be 0 for uncompressed images)
    0,0,0,0,  // horizontal resolution (dpm)
    0,0,0,0,  // vertical resolution (dpm)
    0,0,0,0,  // colors in color table (0 = none)
    0,0,0,0 };// important color count (0 = all colors are important)
  // fill filesize, width and heigth in the header array
  for(uint i=0; i<4; i++) {
      header[ 2+i] = (char)((filesize>>(8*i))&255);
      header[18+i] = (char)((image_width   >>(8*i))&255);
      header[22+i] = (char)((image_height  >>(8*i))&255);
  }
  // write the header to the file
  client.write(header, 54);
  
  // To keep the required memory low, the image is captured line by line
  unsigned char line_data[image_width*3+pad];
  // initialize padded pixel with 0 
  for(int i=(image_width-1)*3; i<(image_width*3+pad); i++){
    line_data[i]=0;
  }
  // The coordinate origin of a BMP image is at the bottom left.
  // Therefore, the image must be read from bottom to top.
  for(int y=image_height; y>0; y--){
    // get one line of the screen content
    M5.Lcd.readRectRGB(0, y-1, image_width, 1, line_data);
    // BMP color order is: Blue, Green, Red
    // return values from readRectRGB is: Red, Green, Blue
    // therefore: R und B need to be swapped
    for(int x=0; x<image_width; x++){
      unsigned char r_buff = line_data[x*3];
      line_data[x*3] = line_data[x*3+2];
      line_data[x*3+2] = r_buff;
    }
    // write the line to the file
    client.write(line_data, (image_width*3)+pad);
  }
  return true;
}


// =============================================================
// connect_Wifi()
// connect to configured Wifi Access point
// returns true if the connection was successful otherwise false
// =============================================================
boolean connect_Wifi(){
  // Establish connection to the specified network until success.
  // Important to disconnect in case that there is a valid connection
  WiFi.disconnect();
  Serial.print("Connecting to ");
  Serial.println(ssid);
  delay(1500);
  //Start connecting (done by the ESP in the background)
  WiFi.begin(ssid, password);
  // read wifi Status
  wl_status_t wifi_Status = WiFi.status();
  int n_trials = 0;
  // loop while Wifi is not connected
  // run only for 20 trials.
  while (wifi_Status != WL_CONNECTED && n_trials < 20) {
    // Check periodicaly the connection status using WiFi.status()
    // Keep checking until ESP has successfuly connected
    wifi_Status = WiFi.status();
    n_trials++;
    switch(wifi_Status){
      case WL_NO_SSID_AVAIL:
          Serial.println("[ERR] WIFI SSID not available");
          break;
      case WL_CONNECT_FAILED:
          Serial.println("[ERR] WIFI Connection failed");
          break;
      case WL_CONNECTION_LOST:
          Serial.println("[ERR] WIFI Connection lost");
          break;
      case WL_DISCONNECTED:
          Serial.println("[STATE] WiFi disconnected");
          break;
      case WL_IDLE_STATUS:
          Serial.println("[STATE] WiFi idle status");
          break;
      case WL_SCAN_COMPLETED:
          Serial.println("[OK] WiFi scan completed");
          break;
      case WL_CONNECTED:
          Serial.println("[OK] WiFi connected");
          break;
      default:
          Serial.println("[ERR] WIFI unknown Status");
          break;
    }
    delay(500);
  }
  if(wifi_Status == WL_CONNECTED){
    // if connected
    Serial.print("IP address: ");
    Serial.println(WiFi.localIP());
    return true;
  } else {
    // if not connected
    Serial.println("[ERR] unable to connect Wifi");
    return false;
  }
}


/***************************************************************************************
* Function name:          draw_gauge
* Description:            Draw a nice gauge on the screen with two indicators
* parameter:              val_1 = value between 0 and 100 for the red arrow
*                         val_2 = value between 0 and 100 for the green line
* Note:                   val_2 is optional
                          values below 0 will not be displayed (hide the arrow)
* example for a gauge with then red arrow at 45% and the freen line at 80%: 
*                         draw_gauge(45,80);
***************************************************************************************/
void draw_gauge(float val_1, float val_2 = -1.0){
  // fill screen with gauge image
  M5.Lcd.pushImage(0, 0, 320, 240, gauge_pic);
  
  // unrotated arrow is pointing on the x-axis to the right
  int xpos1 = 80.0;
  int ypos1 = 0.0;
  // with the origin in the center of the screen
  int xpos0 = (int)(M5.Lcd.width()/2);
  int ypos0 = (int)(M5.Lcd.height()/2);
  // rotate the endpoint for the thin green line
  if(val_2 >= 0 && val_2 <= 100){
    float angle = (239.0-((val_2/100)*298)) * DEG2RAD;
    int xpos2 = (int) roundf(xpos1 * cos(angle) + ypos1 * sin(angle)) + xpos0;
    int ypos2 = (int) roundf(-1.0*xpos1 * sin(angle) + ypos1 * cos(angle)) + ypos0;
    M5.Lcd.drawLine(xpos0, ypos0, xpos2, ypos2, TFT_GREEN);
  }
  if(val_1 >= 0 && val_1 <= 100){
    // calculate the endpoint of the red arrow after rotation
    // 0%   = 239 deg
    // 100% = -60 deg
    float angle = (239.0-((val_1/100)*298)) * DEG2RAD;
    int xpos2 = (int) roundf(xpos1 * cos(angle) + ypos1 * sin(angle)) + xpos0;
    int ypos2 = (int) roundf(-1.0*xpos1 * sin(angle) + ypos1 * cos(angle)) + ypos0;
    // this will be the new origin, so translate the centerpoint
    xpos1 = xpos0 - xpos2;
    ypos1 = ypos0 - ypos2;
    // now rotate the original centerpoint by +4.5 and -4.5 deg to get the triangle
    angle = (float) -4.5 * DEG2RAD;
    int xpos3 = xpos2 + (int) roundf(xpos1 * cos(angle) + ypos1 * sin(angle));
    int ypos3 = ypos2 + (int) roundf(-1.0*xpos1 * sin(angle) + ypos1 * cos(angle));
    angle = (float) 4.5 * DEG2RAD;
    int xpos4 = xpos2 + (int) roundf(xpos1 * cos(angle) + ypos1 * sin(angle));
    int ypos4 = ypos2 + (int) roundf(-1.0*xpos1 * sin(angle) + ypos1 * cos(angle));
    M5.Lcd.fillTriangle(xpos2, ypos2, xpos3, ypos3, xpos4, ypos4, TFT_RED);
    // draw the center circle
    M5.Lcd.fillCircle(xpos0, ypos0, 10, TFT_RED);
    M5.Lcd.fillCircle(xpos0, ypos0, 2, TFT_BLACK);
  }
}

index.html

HTML
The web page for online screenshots and remote control
<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>M5ATOM ENV Monitor</title>
    <style>
      body {
        background: #FFFFFF;
        margin: 0;
      }
      #HeadFont {
        font-family: Impact, Charcoal, sans-serif;
        font-size: 25px;
        letter-spacing: 2px;
        word-spacing: 2px;
        color: #FFFFFF;
        font-weight: normal;
      }
    </style>
  </head>
  <body>
    <table style="background-color: #7f7f7f; border-color: #000000; margin-left: auto; margin-right: auto; cellspacing=10">
    <tbody>
        <tr>
            <td id="HeadFont" style="text-align: center;">
            
            <table style="float: center;" cellspacing="10">
            <tbody id="HeadFont">
            <tr>
              <td style="text-align: center;">M5 Screen Capture</td>
              <td><img alt="" src="electric-idea_100x100.jpg"/></td>
            </tr>
            </tbody>
            </table>
        </tr>
        <tr><td style="text-align: center;">&nbsp;</td></tr>
        <tr>
            <td style="text-align: center;">
            <img alt="" src="screenshot.bmp"/>
            </td>
        </tr>
        <tr><td style="text-align: center;">
          <a href="button-A"><img alt="" src="button.png"></a>
          <a href="button-B"><img alt="" src="button.png"></a>
          <a href="button-C"><img alt="" src="button.png"></a>
        </td></tr>
        <tr><td style="text-align: right;"><a href="/"><img alt="" src="refresh-40x30.png"/></a></td></tr>
    </tbody>
    </table>
  </body>
</html>

github repository

repository with all files and images

Credits

Hans-Günther Nusseck

Hans-Günther Nusseck

17 projects • 55 followers
Just a guy who can't pass by any hardware without wondering how it works. Managing robot based industrial automation projects for living.

Comments