Skip to content

Commit 4036f95

Browse files
Image_Uploader example added
1 parent 5dbdfd3 commit 4036f95

File tree

18 files changed

+3514
-0
lines changed

18 files changed

+3514
-0
lines changed
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
/*
2+
Inkplate10 Image Uploader Example
3+
Compatible with Soldered Inkplate 10
4+
5+
Getting Started:
6+
For setup and documentation, visit: https://inkplate.readthedocs.io/en/latest/
7+
8+
Overview:
9+
This example demonstrates how to upload an image to a webapp hosted by Inkplate 10
10+
and display it on the e‐ink display. Image will be automatically resized.
11+
*/
12+
13+
// Ensure correct board is selected
14+
#if !defined(ARDUINO_INKPLATE10) && !defined(ARDUINO_INKPLATE10V2)
15+
#error "Wrong board selection for this example, please select e-radionica Inkplate10 or Soldered Inkplate10 in the boards menu."
16+
#endif
17+
18+
#include <WiFi.h> // WiFi support
19+
#include <WebServer.h> // Built-in web server
20+
#include <HTTPClient.h> // HTTP client for image upload
21+
#include <Inkplate.h> // Inkplate display library
22+
#include "src/html.h" // HTML page definitions (INDEX_HTML)
23+
24+
// Wi-Fi Access Point credentials
25+
const char* ap_ssid = "InkplateImage"; // SSID for AP mode
26+
const char* ap_password = "inkplate"; // Password (min 8 chars)
27+
28+
// Initialize Inkplate (3-bit grayscale mode by default)
29+
Inkplate display(INKPLATE_3BIT);
30+
31+
// Create a web server object on port 80
32+
WebServer server(80);
33+
34+
// Buffer to hold the latest uploaded image in RAM
35+
uint8_t* imageBuf = nullptr; // pointer to JPEG data
36+
size_t imageLen = 0; // actual bytes received
37+
size_t imageBufCapacity = 0; // allocated buffer size
38+
39+
// Flag to indicate a completed upload
40+
bool imageUploaded = false;
41+
42+
// Serve the main HTML page when root is accessed
43+
void handleIndex() {
44+
server.send_P(200, "text/html", INDEX_HTML);
45+
}
46+
47+
// Handle file upload in three stages: START, WRITE, END
48+
void handleUpload() {
49+
HTTPUpload& u = server.upload();
50+
51+
if (u.status == UPLOAD_FILE_START) {
52+
// Beginning of upload: allocate buffer based on Content-Length header
53+
imageLen = 0;
54+
if (imageBuf) {
55+
free(imageBuf);
56+
imageBuf = nullptr;
57+
}
58+
imageBufCapacity = server.header("Content-Length").toInt();
59+
if (imageBufCapacity <= 0) {
60+
// Fallback capacity if header is missing or invalid (~1200×825 pixels)
61+
imageBufCapacity = 1200 * 825;
62+
}
63+
imageBuf = (uint8_t*) malloc(imageBufCapacity);
64+
Serial.printf("Upload start, buffer capacity = %u bytes\n",
65+
(unsigned)imageBufCapacity);
66+
}
67+
else if (u.status == UPLOAD_FILE_WRITE) {
68+
// Middle of upload: append received chunk into buffer
69+
if (imageBuf && (imageLen + u.currentSize <= imageBufCapacity)) {
70+
memcpy(imageBuf + imageLen, u.buf, u.currentSize);
71+
imageLen += u.currentSize;
72+
Serial.printf(" received %u bytes (total %u)\n",
73+
(unsigned)u.currentSize, (unsigned)imageLen);
74+
}
75+
}
76+
else if (u.status == UPLOAD_FILE_END) {
77+
// End of upload: mark flag to display the image
78+
Serial.printf("Upload complete, final size = %u bytes\n",
79+
(unsigned)imageLen);
80+
imageUploaded = true;
81+
}
82+
}
83+
84+
// After upload ends, reload the page so the user sees the result
85+
void handleUploadComplete() {
86+
server.send(200, "text/html");
87+
}
88+
89+
// Show a preview page with the last uploaded image
90+
void handlePreview() {
91+
String html = "<html><body><h3>Preview:</h3>"
92+
"<img src=\"/image.jpg?ts=" + String(millis()) +
93+
"\" style=\"max-width:100%;\">"
94+
"</body></html>";
95+
server.send(200, "text/html", html);
96+
}
97+
98+
// Serve the JPEG image bytes directly from RAM
99+
void handleImage() {
100+
if (imageBuf && imageLen > 0) {
101+
// Send appropriate headers for JPEG
102+
server.sendHeader("Content-Type", "image/jpeg");
103+
server.sendHeader("Content-Length", String(imageLen));
104+
server.send(200, "image/jpeg", ""); // end headers
105+
106+
// Write image data to client
107+
WiFiClient client = server.client();
108+
size_t sent = client.write(imageBuf, imageLen);
109+
Serial.printf("Sent %u of %u bytes\n", sent, (unsigned)imageLen);
110+
}
111+
else {
112+
// No image available yet
113+
server.send(404, "text/plain", "No image uploaded");
114+
}
115+
}
116+
117+
// Display the buffered image on the e-ink display
118+
void showImageBuffer() {
119+
if (!imageBuf || imageLen == 0) return;
120+
121+
display.clearDisplay(); // clear existing content
122+
display.setDisplayMode(INKPLATE_3BIT); // ensure correct mode
123+
// Draw JPEG from RAM: full-screen, no dithering
124+
display.drawJpegFromBuffer(imageBuf, imageLen, 0, 0, true, false);
125+
display.display(); // push to panel
126+
}
127+
128+
// Helper: calculate scaled dimensions by the larger original side
129+
void getScaledByLargerSide(int origW, int origH, int maxW, int maxH, int &outW, int &outH) {
130+
float ratio = (origW >= origH) ? (float(maxW) / origW) : (float(maxH) / origH);
131+
outW = round(origW * ratio);
132+
outH = round(origH * ratio);
133+
}
134+
135+
void setup() {
136+
Serial.begin(115200); // initialize serial for debug
137+
138+
// Initialize the display
139+
display.begin();
140+
display.clearDisplay();
141+
142+
// Start Wi-Fi in AP+STA mode
143+
WiFi.mode(WIFI_AP_STA);
144+
WiFi.softAP(ap_ssid, ap_password);
145+
IPAddress apIP = WiFi.softAPIP();
146+
Serial.print("AP IP: "); Serial.println(apIP);
147+
148+
// Show instructions on the e-ink screen
149+
display.setTextSize(4);
150+
display.setTextColor(BLACK);
151+
display.clearDisplay();
152+
display.setCursor(10, 10);
153+
display.print("Welcome to the Inkplate Image Uploader example.");
154+
display.setTextSize(2);
155+
display.setCursor(10, 50);
156+
display.print("Connect to WiFi:");
157+
display.setCursor(30, 70);
158+
display.print("SSID: " + String(ap_ssid));
159+
display.setCursor(30, 90);
160+
display.print("Password: " + String(ap_password));
161+
display.setCursor(10, 110);
162+
display.print("Then visit:");
163+
display.setCursor(10, 130);
164+
display.print(apIP.toString());
165+
display.display();
166+
167+
// Define web routes and handlers
168+
server.on("/", HTTP_GET, handleIndex);
169+
server.on("/upload", HTTP_POST, handleIndex, handleUpload);
170+
server.on("/preview", HTTP_GET, handlePreview);
171+
server.on("/image.jpg", HTTP_GET, handleImage);
172+
173+
server.begin(); // start the HTTP server
174+
Serial.println("HTTP server started");
175+
}
176+
177+
void loop() {
178+
server.handleClient(); // handle incoming HTTP requests
179+
180+
// If an image was uploaded, display it once
181+
if (imageUploaded) {
182+
imageUploaded = false; // reset the flag
183+
delay(100); // small delay to finish POST
184+
showImageBuffer(); // render image on e-ink
185+
}
186+
}
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
#ifndef HTML_H
2+
#define HTML_H
3+
4+
const char INDEX_HTML[] PROGMEM = R"rawliteral(
5+
<!DOCTYPE html>
6+
<html lang="en">
7+
<head>
8+
<meta charset="UTF-8">
9+
<meta name="viewport" content="width=device-width,initial-scale=1.0">
10+
<title>Inkplate Image Uploader</title>
11+
<style>
12+
:root {
13+
--primary: #5e2a8e;
14+
--accent: #00b8d4;
15+
--light: #f5f7fa;
16+
--dark: #333;
17+
}
18+
* {
19+
margin: 0;
20+
padding: 0;
21+
box-sizing: border-box;
22+
font-family: sans-serif;
23+
}
24+
body {
25+
background: var(--light);
26+
color: var(--dark);
27+
padding: 1rem;
28+
text-align: center;
29+
}
30+
.upload-section {
31+
background: #fff;
32+
padding: 1rem;
33+
border-radius: 8px;
34+
max-width: 400px;
35+
width: 100%;
36+
margin: 2rem auto;
37+
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
38+
}
39+
.file-picker-buttons {
40+
display: flex;
41+
flex-wrap: wrap;
42+
gap: .5rem;
43+
justify-content: center;
44+
margin-bottom: 1rem;
45+
}
46+
.file-picker-buttons .upload-btn {
47+
flex: 1 1 45%;
48+
min-width: 120px;
49+
}
50+
#btn-upload {
51+
width: 100%;
52+
margin-top: .5rem;
53+
}
54+
.upload-btn {
55+
background: var(--accent);
56+
color: #fff;
57+
padding: .5rem 1rem;
58+
border: none;
59+
border-radius: 4px;
60+
cursor: pointer;
61+
}
62+
.upload-btn:disabled {
63+
opacity: .5;
64+
cursor: not-allowed;
65+
}
66+
.status {
67+
margin-top: 1rem;
68+
font-weight: bold;
69+
min-height: 1.2em;
70+
}
71+
/* Hide the real file inputs */
72+
input[type=file] {
73+
display: none;
74+
}
75+
</style>
76+
</head>
77+
<body>
78+
<div class="upload-section">
79+
<h2>Inkplate Image Uploader</h2>
80+
<br>
81+
<p>Using this app you can easily upload pictures to your Inkplate board. Take a new photo (like a selfie) or browse from your gallery - then click upload to show the image on your Inkplate 10!</p>
82+
<br>
83+
<form id="uploadForm" method="POST" action="/upload" enctype="multipart/form-data">
84+
<div class="file-picker-buttons">
85+
<!-- Hidden file inputs -->
86+
<input type="file" accept="image/*" capture="environment" id="camera-input" name="image">
87+
<input type="file" accept="image/*" id="gallery-input" name="image">
88+
<!-- Visible pickers -->
89+
<button type="button" class="upload-btn" id="btn-camera">Take a Photo</button>
90+
<button type="button" class="upload-btn" id="btn-gallery">Choose from Gallery</button>
91+
</div>
92+
<button type="submit" class="upload-btn" id="btn-upload" disabled>Upload</button>
93+
</form>
94+
95+
<!-- Status message -->
96+
<div id="statusMessage" class="status" aria-live="polite"></div>
97+
</div>
98+
99+
<script>
100+
document.addEventListener('DOMContentLoaded', () => {
101+
const CANVAS_W = 1200;
102+
const CANVAS_H = 825;
103+
104+
const form = document.getElementById('uploadForm');
105+
const camInput = document.getElementById('camera-input');
106+
const galInput = document.getElementById('gallery-input');
107+
const btnCam = document.getElementById('btn-camera');
108+
const btnGal = document.getElementById('btn-gallery');
109+
const uploadBtn = document.getElementById('btn-upload');
110+
const statusMessage = document.getElementById('statusMessage');
111+
112+
// initial state
113+
uploadBtn.disabled = true;
114+
statusMessage.textContent = '';
115+
116+
// wire picker buttons
117+
btnCam.addEventListener('click', () => camInput.click());
118+
btnGal.addEventListener('click', () => galInput.click());
119+
120+
// enable upload when a file is selected
121+
[camInput, galInput].forEach(input => {
122+
input.addEventListener('change', () => {
123+
const file = camInput.files[0] || galInput.files[0];
124+
if (file) {
125+
statusMessage.textContent = 'Image ready to upload';
126+
uploadBtn.disabled = false;
127+
}
128+
});
129+
});
130+
131+
form.addEventListener('submit', function(e) {
132+
e.preventDefault();
133+
const file = camInput.files[0] || galInput.files[0];
134+
if (!file) return;
135+
136+
// disable controls during upload
137+
uploadBtn.disabled = true;
138+
btnCam.disabled = true;
139+
btnGal.disabled = true;
140+
statusMessage.textContent = 'Uploading...';
141+
142+
// process image
143+
const img = new Image();
144+
img.onload = () => {
145+
// calculate scale to fit entire image into 1200x825
146+
const scale = Math.min(
147+
CANVAS_W / img.width,
148+
CANVAS_H / img.height,
149+
1 // don't scale up
150+
);
151+
const drawW = Math.round(img.width * scale);
152+
const drawH = Math.round(img.height * scale);
153+
const offsetX = Math.round((CANVAS_W - drawW) / 2);
154+
const offsetY = Math.round((CANVAS_H - drawH) / 2);
155+
156+
// prepare canvas
157+
const canvas = document.createElement('canvas');
158+
canvas.width = CANVAS_W;
159+
canvas.height = CANVAS_H;
160+
const ctx = canvas.getContext('2d');
161+
162+
// clear canvas (optional)
163+
ctx.clearRect(0, 0, CANVAS_W, CANVAS_H);
164+
165+
// draw the scaled, centered image
166+
ctx.drawImage(img, offsetX, offsetY, drawW, drawH);
167+
168+
// convert to JPEG and upload
169+
canvas.toBlob(blob => {
170+
const formData = new FormData();
171+
const name = file.name.replace(/\.[^/.]+$/, '') + '.jpg';
172+
formData.append('image', blob, name);
173+
174+
const xhr = new XMLHttpRequest();
175+
xhr.open('POST', '/upload', true);
176+
xhr.onload = () => {
177+
statusMessage.textContent = 'Image uploaded';
178+
// reset for next upload
179+
camInput.value = '';
180+
galInput.value = '';
181+
btnCam.disabled = false;
182+
btnGal.disabled = false;
183+
};
184+
xhr.onerror = () => {
185+
statusMessage.textContent = 'Upload failed';
186+
btnCam.disabled = false;
187+
btnGal.disabled = false;
188+
};
189+
xhr.send(formData);
190+
}, 'image/jpeg', 0.92);
191+
192+
URL.revokeObjectURL(img.src);
193+
};
194+
img.onerror = () => {
195+
statusMessage.textContent = 'Selected file is not a valid image.';
196+
};
197+
img.src = URL.createObjectURL(file);
198+
});
199+
});
200+
</script>
201+
</body>
202+
</html>
203+
)rawliteral";
204+
205+
#endif

0 commit comments

Comments
 (0)