525 lines
17 KiB
Plaintext
525 lines
17 KiB
Plaintext
|
// This is a Processing sketch, see https://processing.org/ to download the IDE
|
||
|
|
||
|
// The sketch is a client that requests TFT screenshots from an Arduino board.
|
||
|
// The Arduino must call a screenshot server function to respond with pixels.
|
||
|
|
||
|
// It has been created to work with the TFT_eSPI library here:
|
||
|
// https://github.com/Bodmer/TFT_eSPI
|
||
|
|
||
|
// The sketch must only be run when the designated serial port is available and enumerated
|
||
|
// otherwise the screenshot window may freeze and that process will need to be terminated
|
||
|
// This is a limitation of the Processing environment and not the sketch.
|
||
|
// If anyone knows how to determine if a serial port is available at start up the PM me
|
||
|
// on (Bodmer) the Arduino forum.
|
||
|
|
||
|
// The block below contains variables that the user may need to change for a particular setup
|
||
|
// As a minimum set the serial port and baud rate must be defined. The capture window is
|
||
|
// automatically resized for landscape, portrait and different TFT resolutions.
|
||
|
|
||
|
// Captured images are stored in the sketch folder, use the Processing IDE "Sketch" menu
|
||
|
// option "Show Sketch Folder" or press Ctrl+K
|
||
|
|
||
|
// Created by: Bodmer 5/3/17
|
||
|
// Updated by: Bodmer 12/3/17
|
||
|
// Version: 0.07
|
||
|
|
||
|
// MIT licence applies, all text above must be included in derivative works
|
||
|
|
||
|
|
||
|
// ###########################################################################################
|
||
|
// # These are the values to change for a particular setup #
|
||
|
// #
|
||
|
int serial_port = 0; // Use enumerated value from list provided when sketch is run #
|
||
|
// #
|
||
|
// On an Arduino Due Programming Port use a baud rate of:115200) #
|
||
|
// On an Arduino Due Native USB Port use a baud rate of any value #
|
||
|
int serial_baud_rate = 921600; // #
|
||
|
// #
|
||
|
// Change the image file type saved here, comment out all but one #
|
||
|
//String image_type = ".jpg"; // #
|
||
|
String image_type = ".png"; // Lossless compression #
|
||
|
//String image_type = ".bmp"; // #
|
||
|
//String image_type = ".tif"; // #
|
||
|
// #
|
||
|
boolean save_border = true; // Save the image with a border #
|
||
|
int border = 5; // Border pixel width #
|
||
|
boolean fade = false; // Fade out image after saving #
|
||
|
// #
|
||
|
int max_images = 100; // Maximum of numbered file images before over-writing files #
|
||
|
// #
|
||
|
int max_allowed = 1000; // Maximum number of save images allowed before a restart #
|
||
|
// #
|
||
|
// # End of the values to change for a particular setup #
|
||
|
// ###########################################################################################
|
||
|
|
||
|
// These are default values, this sketch obtains the actual values from the Arduino board
|
||
|
int tft_width = 480; // default TFT width (automatic - sent by Arduino)
|
||
|
int tft_height = 480; // default TFT height (automatic - sent by Arduino)
|
||
|
int color_bytes = 2; // 2 for 16 bit, 3 for three RGB bytes (automatic - sent by Arduino)
|
||
|
|
||
|
import processing.serial.*;
|
||
|
|
||
|
Serial serial; // Create an instance called serial
|
||
|
|
||
|
int serialCount = 0; // Count of colour bytes arriving
|
||
|
|
||
|
// Stage window graded background colours
|
||
|
color bgcolor1 = color(0, 100, 104); // Arduino IDE style background color 1
|
||
|
color bgcolor2 = color(77, 183, 187); // Arduino IDE style background color 2
|
||
|
//color bgcolor2 = color(255, 255, 255); // White
|
||
|
|
||
|
// TFT image frame greyscale value (dark grey)
|
||
|
color frameColor = 42;
|
||
|
|
||
|
color buttonStopped = color(255, 0, 0);
|
||
|
color buttonRunning = color(128, 204, 206);
|
||
|
color buttonDimmed = color(180, 0, 0);
|
||
|
boolean dimmed = false;
|
||
|
boolean running = true;
|
||
|
boolean mouseClick = false;
|
||
|
|
||
|
int[] rgb = new int[3]; // Buffer for the colour bytes
|
||
|
int indexRed = 0; // Colour byte index in the array
|
||
|
int indexGreen = 1;
|
||
|
int indexBlue = 2;
|
||
|
|
||
|
int n = 0;
|
||
|
|
||
|
int x_offset = (500 - tft_width) /2; // Image offsets in the window
|
||
|
int y_offset = 20;
|
||
|
|
||
|
int xpos = 0, ypos = 0; // Current pixel position
|
||
|
|
||
|
int beginTime = 0;
|
||
|
int pixelWaitTime = 1000; // Maximum 1000ms wait for image pixels to arrive
|
||
|
int lastPixelTime = 0; // Time that "image send" command was sent
|
||
|
|
||
|
int requestTime = 0;
|
||
|
int requestCount = 0;
|
||
|
|
||
|
int state = 0; // State machine current state
|
||
|
|
||
|
int progress_bar = 0; // Console progress bar dot count
|
||
|
int pixel_count = 0; // Number of pixels read for 1 screen
|
||
|
float percentage = 0; // Percentage of pixels received
|
||
|
|
||
|
int saved_image_count = 0; // Stats - number of images processed
|
||
|
int bad_image_count = 0; // Stats - number of images that had lost pixels
|
||
|
String filename = "";
|
||
|
|
||
|
int drawLoopCount = 0; // Used for the fade out
|
||
|
|
||
|
void setup() {
|
||
|
|
||
|
size(500, 540); // Stage size, can handle 480 pixels wide screen
|
||
|
noStroke(); // No border on the next thing drawn
|
||
|
noSmooth(); // No anti-aliasing to avoid adjacent pixel colour merging
|
||
|
|
||
|
// Graded background and title
|
||
|
drawWindow();
|
||
|
|
||
|
frameRate(2000); // High frame rate so draw() loops fast
|
||
|
|
||
|
// Print a list of the available serial ports
|
||
|
println("-----------------------");
|
||
|
println("Available Serial Ports:");
|
||
|
println("-----------------------");
|
||
|
printArray(Serial.list());
|
||
|
println("-----------------------");
|
||
|
|
||
|
print("Port currently used: [");
|
||
|
print(serial_port);
|
||
|
println("]");
|
||
|
|
||
|
String portName = Serial.list()[serial_port];
|
||
|
|
||
|
serial = new Serial(this, portName, serial_baud_rate);
|
||
|
|
||
|
state = 99;
|
||
|
}
|
||
|
|
||
|
void draw() {
|
||
|
|
||
|
if (mouseClick) buttonClicked();
|
||
|
|
||
|
switch(state) {
|
||
|
|
||
|
case 0: // Init varaibles, send start request
|
||
|
if (running) {
|
||
|
tint(0, 0, 0, 255);
|
||
|
flushBuffer();
|
||
|
println("");
|
||
|
print("Ready: ");
|
||
|
|
||
|
xpos = 0;
|
||
|
ypos = 0;
|
||
|
serialCount = 0;
|
||
|
progress_bar = 0;
|
||
|
pixel_count = 0;
|
||
|
percentage = 0;
|
||
|
drawLoopCount = frameCount;
|
||
|
lastPixelTime = millis() + 1000;
|
||
|
|
||
|
state = 1;
|
||
|
} else {
|
||
|
if (millis() > beginTime) {
|
||
|
beginTime = millis() + 500;
|
||
|
dimmed = !dimmed;
|
||
|
if (dimmed) drawButton(buttonDimmed);
|
||
|
else drawButton(buttonStopped);
|
||
|
}
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
case 1: // Console message, give server some time
|
||
|
print("requesting image ");
|
||
|
serial.write("S");
|
||
|
delay(10);
|
||
|
beginTime = millis();
|
||
|
requestTime = millis() + 1000;
|
||
|
requestCount = 1;
|
||
|
state = 2;
|
||
|
break;
|
||
|
|
||
|
case 2: // Get size and set start time for rendering duration report
|
||
|
if (millis() > requestTime) {
|
||
|
requestCount++;
|
||
|
print("*");
|
||
|
serial.clear();
|
||
|
serial.write("S");
|
||
|
if (requestCount > 32) {
|
||
|
requestCount = 0;
|
||
|
System.err.println(" - no response!");
|
||
|
state = 0;
|
||
|
}
|
||
|
requestTime = millis() + 1000;
|
||
|
}
|
||
|
if ( getSize() == true ) { // Go to next state when we have the size and bits per pixel
|
||
|
getFilename();
|
||
|
flushBuffer(); // Precaution in case image header size increases in later versions
|
||
|
lastPixelTime = millis() + 1000;
|
||
|
beginTime = millis();
|
||
|
state = 3;
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
case 3: // Request pixels and render returned RGB values
|
||
|
state = renderPixels(); // State will change when all pixels are rendered
|
||
|
|
||
|
// Request more pixels, changing the number requested allows the average transfer rate to be controlled
|
||
|
// The pixel transfer rate is dependant on four things:
|
||
|
// 1. The frame rate defined in this Processing sketch in setup()
|
||
|
// 2. The baud rate of the serial link (~10 bit periods per byte)
|
||
|
// 3. The number of request bytes 'R' sent in the lines below
|
||
|
// 4. The number of pixels sent in a burst by the server sketch (defined via NPIXELS)
|
||
|
|
||
|
//serial.write("RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR"); // 32 x NPIXELS more
|
||
|
serial.write("RRRRRRRRRRRRRRRR"); // 16 x NPIXELS more
|
||
|
//serial.write("RRRRRRRR"); // 8 x NPIXELS more
|
||
|
//serial.write("RRRR"); // 4 x NPIXELS more
|
||
|
//serial.write("RR"); // 2 x NPIXELS more
|
||
|
//serial.write("R"); // 1 x NPIXELS more
|
||
|
if (!running) state = 4;
|
||
|
break;
|
||
|
|
||
|
case 4: // Pixel receive time-out, flush serial buffer
|
||
|
flushBuffer();
|
||
|
state = 6;
|
||
|
break;
|
||
|
|
||
|
case 5: // Save the image to the sketch folder (Ctrl+K to access)
|
||
|
saveScreenshot();
|
||
|
saved_image_count++;
|
||
|
println("Saved image count = " + saved_image_count);
|
||
|
if (bad_image_count > 0) System.err.println(" Bad image count = " + bad_image_count);
|
||
|
drawLoopCount = frameCount; // Reset value ready for counting in step 6
|
||
|
state = 6;
|
||
|
break;
|
||
|
|
||
|
case 6: // Fade the old image if enabled
|
||
|
if ( fadedImage() == true ) state = 0; // Go to next state when image has faded
|
||
|
break;
|
||
|
|
||
|
case 99: // Draw image viewer window
|
||
|
drawWindow();
|
||
|
delay(50); // Delay here seems to be required for the IDE console to get ready
|
||
|
state = 0;
|
||
|
break;
|
||
|
|
||
|
default:
|
||
|
println("");
|
||
|
System.err.println("Error state reached - check sketch!");
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void drawWindow()
|
||
|
{
|
||
|
// Graded background in Arduino colours
|
||
|
for (int i = 0; i < height - 25; i++) {
|
||
|
float inter = map(i, 0, height - 25, 0, 1);
|
||
|
color c = lerpColor(bgcolor1, bgcolor2, inter);
|
||
|
stroke(c);
|
||
|
line(0, i, 500, i);
|
||
|
}
|
||
|
fill(bgcolor2);
|
||
|
rect( 0, height-25, width-1, 24);
|
||
|
textAlign(CENTER);
|
||
|
textSize(20);
|
||
|
fill(0);
|
||
|
text("Bodmer's TFT image viewer", width/2, height-6);
|
||
|
|
||
|
if (running) drawButton(buttonRunning);
|
||
|
else drawButton(buttonStopped);
|
||
|
}
|
||
|
|
||
|
void flushBuffer()
|
||
|
{
|
||
|
//println("Clearing serial pipe after a time-out");
|
||
|
int clearTime = millis() + 50;
|
||
|
while ( millis() < clearTime ) serial.clear();
|
||
|
}
|
||
|
|
||
|
boolean getSize()
|
||
|
{
|
||
|
if ( serial.available() > 6 ) {
|
||
|
println();
|
||
|
char code = (char)serial.read();
|
||
|
if (code == 'W') {
|
||
|
tft_width = serial.read()<<8 | serial.read();
|
||
|
}
|
||
|
code = (char)serial.read();
|
||
|
if (code == 'H') {
|
||
|
tft_height = serial.read()<<8 | serial.read();
|
||
|
}
|
||
|
code = (char)serial.read();
|
||
|
if (code == 'Y') {
|
||
|
int bits_per_pixel = (char)serial.read();
|
||
|
if (bits_per_pixel == 24) color_bytes = 3;
|
||
|
else color_bytes = 2;
|
||
|
}
|
||
|
code = (char)serial.read();
|
||
|
if (code == '?') {
|
||
|
drawWindow();
|
||
|
|
||
|
x_offset = (500 - tft_width) /2;
|
||
|
tint(0, 0, 0, 255);
|
||
|
noStroke();
|
||
|
fill(frameColor);
|
||
|
rect((width - tft_width)/2 - border, y_offset - border, tft_width + 2 * border, tft_height + 2 * border);
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
void saveScreenshot()
|
||
|
{
|
||
|
println();
|
||
|
if (saved_image_count < max_allowed)
|
||
|
{
|
||
|
if (filename == "") filename = "tft_screen_" + (n++);
|
||
|
filename = filename + image_type;
|
||
|
println("Saving image as \"" + filename + "\"");
|
||
|
if (save_border)
|
||
|
{
|
||
|
PImage partialSave = get(x_offset - border, y_offset - border, tft_width + 2*border, tft_height + 2*border);
|
||
|
partialSave.save(filename);
|
||
|
} else {
|
||
|
PImage partialSave = get(x_offset, y_offset, tft_width, tft_height);
|
||
|
partialSave.save(filename);
|
||
|
}
|
||
|
|
||
|
if (n>=max_images) n = 0;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
System.err.println(max_allowed + " saved image count exceeded, restart the sketch");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void getFilename()
|
||
|
{
|
||
|
int readTime = millis() + 20;
|
||
|
int inByte = 0;
|
||
|
filename = "";
|
||
|
while ( serial.available() > 0 && millis() < readTime && inByte != '.')
|
||
|
{
|
||
|
inByte = serial.read();
|
||
|
if (inByte == ' ') inByte = '_';
|
||
|
if ( unicodeCheck(inByte) ) filename += (char)inByte;
|
||
|
}
|
||
|
|
||
|
inByte = serial.read();
|
||
|
if (inByte == '@') filename += "_" + timeCode();
|
||
|
else if (inByte == '#') filename += "_" + saved_image_count%100;
|
||
|
else if (inByte == '%') filename += "_" + millis();
|
||
|
else if (inByte != '*') filename = "";
|
||
|
|
||
|
inByte = serial.read();
|
||
|
if (inByte == 'j') image_type =".jpg";
|
||
|
else if (inByte == 'b') image_type =".bmp";
|
||
|
else if (inByte == 'p') image_type =".png";
|
||
|
else if (inByte == 't') image_type =".tif";
|
||
|
}
|
||
|
|
||
|
boolean unicodeCheck(int unicode)
|
||
|
{
|
||
|
if ( unicode >= '0' && unicode <= '9' ) return true;
|
||
|
if ( (unicode >= 'A' && unicode <= 'Z' ) || (unicode >= 'a' && unicode <= 'z')) return true;
|
||
|
if ( unicode == '_' || unicode == '/' ) return true;
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
String timeCode()
|
||
|
{
|
||
|
String timeCode = (int)year() + "_" + (int)month() + "_" + (int)day() + "_";
|
||
|
timeCode += (int)hour() + "_" + (int)minute() + "_" + (int)second();
|
||
|
return timeCode;
|
||
|
}
|
||
|
|
||
|
int renderPixels()
|
||
|
{
|
||
|
if ( serial.available() > 0 ) {
|
||
|
|
||
|
// Add the latest byte from the serial port to array:
|
||
|
while (serial.available()>0)
|
||
|
{
|
||
|
rgb[serialCount++] = serial.read();
|
||
|
|
||
|
// If we have 3 colour bytes:
|
||
|
if ( serialCount >= color_bytes ) {
|
||
|
serialCount = 0;
|
||
|
pixel_count++;
|
||
|
if (color_bytes == 3)
|
||
|
{
|
||
|
stroke(rgb[indexRed], rgb[indexGreen], rgb[indexBlue], 1000);
|
||
|
} else
|
||
|
{ // Can cater for various byte orders
|
||
|
//stroke( (rgb[0] & 0x1F)<<3, (rgb[0] & 0xE0)>>3 | (rgb[1] & 0x07)<<5, (rgb[1] & 0xF8));
|
||
|
//stroke( (rgb[1] & 0x1F)<<3, (rgb[1] & 0xE0)>>3 | (rgb[0] & 0x07)<<5, (rgb[0] & 0xF8));
|
||
|
stroke( (rgb[0] & 0xF8), (rgb[1] & 0xE0)>>3 | (rgb[0] & 0x07)<<5, (rgb[1] & 0x1F)<<3);
|
||
|
//stroke( (rgb[1] & 0xF8), (rgb[0] & 0xE0)>>3 | (rgb[1] & 0x07)<<5, (rgb[0] & 0x1F)<<3);
|
||
|
}
|
||
|
// We get some pixel merge aliasing if smooth() is defined, so draw pixel twice
|
||
|
point(xpos + x_offset, ypos + y_offset);
|
||
|
//point(xpos + x_offset, ypos + y_offset);
|
||
|
|
||
|
lastPixelTime = millis();
|
||
|
xpos++;
|
||
|
if (xpos >= tft_width) {
|
||
|
xpos = 0;
|
||
|
progressBar();
|
||
|
ypos++;
|
||
|
if (ypos>=tft_height) {
|
||
|
ypos = 0;
|
||
|
if ((int)percentage <100) {
|
||
|
while (progress_bar++ < 64) print(" ");
|
||
|
percent(100);
|
||
|
}
|
||
|
println("Image fetch time = " + (millis()-beginTime)/1000.0 + " s");
|
||
|
return 5;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
} else
|
||
|
{
|
||
|
if (millis() > (lastPixelTime + pixelWaitTime))
|
||
|
{
|
||
|
println("");
|
||
|
System.err.println(pixelWaitTime + "ms time-out for pixels exceeded...");
|
||
|
if (pixel_count > 0) {
|
||
|
bad_image_count++;
|
||
|
System.err.print("Pixels missing = " + (tft_width * tft_height - pixel_count));
|
||
|
System.err.println(", corrupted image not saved");
|
||
|
System.err.println("Good image count = " + saved_image_count);
|
||
|
System.err.println(" Bad image count = " + bad_image_count);
|
||
|
}
|
||
|
return 4;
|
||
|
}
|
||
|
}
|
||
|
return 3;
|
||
|
}
|
||
|
|
||
|
void progressBar()
|
||
|
{
|
||
|
progress_bar++;
|
||
|
print(".");
|
||
|
if (progress_bar >63)
|
||
|
{
|
||
|
progress_bar = 0;
|
||
|
percentage = 0.5 + 100 * pixel_count/(0.001 + tft_width * tft_height);
|
||
|
percent(percentage);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void percent(float percentage)
|
||
|
{
|
||
|
if (percentage > 100) percentage = 100;
|
||
|
println(" [ " + (int)percentage + "% ]");
|
||
|
textAlign(LEFT);
|
||
|
textSize(16);
|
||
|
noStroke();
|
||
|
fill(bgcolor2);
|
||
|
rect(10, height - 25, 70, 20);
|
||
|
fill(0);
|
||
|
text(" [ " + (int)percentage + "% ]", 10, height-8);
|
||
|
}
|
||
|
|
||
|
boolean fadedImage()
|
||
|
{
|
||
|
int opacity = frameCount - drawLoopCount; // So we get increasing fade
|
||
|
if (fade)
|
||
|
{
|
||
|
tint(255, opacity);
|
||
|
//image(tft_img, x_offset, y_offset);
|
||
|
noStroke();
|
||
|
fill(50, 50, 50, opacity);
|
||
|
rect( (width - tft_width)/2, y_offset, tft_width, tft_height);
|
||
|
delay(10);
|
||
|
}
|
||
|
if (opacity > 50) // End fade after 50 cycles
|
||
|
{
|
||
|
return true;
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
void drawButton(color buttonColor)
|
||
|
{
|
||
|
stroke(0);
|
||
|
fill(buttonColor);
|
||
|
rect(500 - 100, 540 - 26, 80, 24);
|
||
|
textAlign(CENTER);
|
||
|
textSize(20);
|
||
|
fill(0);
|
||
|
if (running) text(" Pause ", 500 - 60, height-7);
|
||
|
else text(" Run ", 500 - 60, height-7);
|
||
|
}
|
||
|
|
||
|
void buttonClicked()
|
||
|
{
|
||
|
mouseClick = false;
|
||
|
if (running) {
|
||
|
running = false;
|
||
|
drawButton(buttonStopped);
|
||
|
System.err.println("");
|
||
|
System.err.println("Stopped - click 'Run' button: ");
|
||
|
//noStroke();
|
||
|
//fill(50);
|
||
|
//rect( (width - tft_width)/2, y_offset, tft_width, tft_height);
|
||
|
beginTime = millis() + 500;
|
||
|
dimmed = false;
|
||
|
state = 4;
|
||
|
} else {
|
||
|
running = true;
|
||
|
drawButton(buttonRunning);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void mousePressed() {
|
||
|
if (mouseX > (500 - 100) && mouseX < (500 - 20) && mouseY > (540 - 26) && mouseY < (540 - 2)) {
|
||
|
mouseClick = true;
|
||
|
}
|
||
|
}
|