Compare commits

..

2 commits

Author SHA1 Message Date
d3de814d75 Send index.html to browser when visiting / 2025-10-06 21:36:03 +02:00
3a78fec456 Add index.html with basic documentation 2025-10-06 21:05:24 +02:00
5 changed files with 230 additions and 24 deletions

View file

@ -10,7 +10,7 @@ You need to give it a 2.4Ghz Wifi SSID and password by adjusting `main/settings.
You can send scripts like this:
``` bash
cat assets/post_test.lua | curl -X POST --data-binary @- 10.0.0.100/draw
cat assets/scripts/post_test.lua | curl -X POST --data-binary @- 10.0.0.100/draw
```
The IP address of the display is logged to its serial output and can be read via `idf.py monitor` (see below).
@ -41,7 +41,9 @@ You can stop the monitor by pressing `Ctrl + ]`.
- `main/`
- `inkpot.c`: Main program containing embedded Lua script and file-based Lua execution.
- `paper.c`: Defines the lua bindings to epdiy that can be used for drawing on the screen.
- `assets/`: Directory containing all Lua scripts; some are older example scripts that are not executed
- `assets/`
- `http/`: Static assets to be served by the HTTP server
- `scripts/`: Directory containing all Lua scripts; some are older example scripts that are not executed
- `boot.lua` contains the sketch that runs at startup
- `post_test.lua` contains another sketch

161
assets/http/index.html Normal file
View file

@ -0,0 +1,161 @@
<!doctype html>
<html class="no-js" lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>Inkpot</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="favicon" content="content" />
<link rel="icon" href="data:image/svg+xml,&lt;svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22&gt;&lt;text y=%22.9em%22 font-size=%2290%22&gt;ᝰ&lt;/text&gt;&lt;/svg&gt;">
<!-- Place favicon.ico in the root directory -->
<style>
*, *:before, *:after { box-sizing: border-box; }
html, body { margin: 0; padding: 0 }
body {
min-height: 100vh;
font-family: monospace;
background: #EEE6EA;
color: #246;
padding: 36px 24px;
font-size: 14px;
line-height: 1.5;
max-width: 800px;
}
h1 {
margin: 0 0 24px;
font-size: 1.5rem;
}
h1:after {
content: ' ᝰ';
}
p {
margin: 0 0 12px;
}
form {
margin: 24px 0;
}
textarea {
margin: 0 0 6px;
padding: 6px;
width: 100%;
height: 18rem;
background: #F5F0F2;
border: 2px solid #246;
}
button[type=submit] {
margin: 0 0 6px;
padding: 12px;
background: #F5F0F2;
border: 2px solid #246;
font-weight: bold;
text-transform: uppercase;
}
button[type=submit]:after {
content: ' ⛆';
}
a {
color: inherit;
}
h2 {
margin: 0 0 18px;
font-size: 1.2rem;
}
dl {
line-height: 2;
}
dt {
font-weight: bold;
}
dd {
margin: 0 0 12px;
}
code {
background: #F5F0F2;
padding: 3px 6px;
z-index: -1;
}
dt code {
white-space: nowrap;
}
@media screen and (min-width: 800px) {
body {
padding: 72px;
}
}
</style>
</head>
<body>
<h1>inkblot</h1>
<p>Welcome to your inkblot. You can use the text field below to enter and send Lua scripts that will change what is displayed on your screen. At the bottom of the screen you can get a short list of functions available for drawing.</p>
<form method="POST" id="blot" action="/draw">
<textarea></textarea>
<button type="submit">Draw</button>
</form>
<h2>How to draw</h2>
<p>Lua is a very simple language. Here is a brief overview of the syntax: <a href="https://learnxinyminutes.com/lua/" target="_blank">Learn Lua in Y Minutes.</a> You can find a description of the drawing library below.</p>
<p>The screen is monochrome and knows 16 shades. Whenever you see a <code>color</code> below, it can be set to values from <code>0</code> (black) to <code>255</code> (white). You can also use hexadecimal notation (from <code>0x00</code> to <code>0xFF</code>) which means the same thing.</p>
<p>The coordinate system starts at <code>(0,0)</code> in the top left corner.</p>
<dl>
<dt><code>local paper = require "paper"</code></dt>
<dd>Loads the library to run drawing commands.</dd>
<dt><code>paper.init()</code></dt>
<dd>Initializes the screen. Needs to be called at least once.</dd>
<dt><code>paper.clear()</code></dt>
<dd>Clears the screen. If you don't call this, you will draw on top of your previous drawing.</dd>
<dt><code>paper.update()</code></dt>
<dd>Sends your drawing to the screen. Needs to be called after a sequence of drawing commands to display them..</dd>
<dt><code>paper.set_orientation(orientation)</code></dt>
<dd>Set the screen orientation. Allowed values for <code>orientation</code> are <code>"portrait"</code>, <code>"landscape"</code>, <code>"inverse_portrait"</code> and <code>"inverse_landscape"</code></dd>
<dt><code>paper.get_orientation()</code></dt>
<dd>Get current screen orientation. Returned values are <code>"portrait"</code>, <code>"landscape"</code>, <code>"inverse_portrait"</code> and <code>"inverse_landscape"</code></dd>
<dt><code>paper.get_width()</code></dt>
<dd>Get current screen width, according to orientation.</dd>
<dt><code>paper.get_height()</code></dt>
<dd>Get current screen height, according to orientation.</dd>
<dt><code>paper.draw_pixel(x, y, color)</code></dt>
<dd>Draw a single pixel at <code>(x,y)</code> in the given <code>color</code>.</dd>
<dt><code>paper.draw_line(x1, y1, x2, y2, color)</code></dt>
<dd>Draw a line from <code>(x1,y1)</code> to <code>(x2,y2)</code> in the given <code>color</code>.</dd>
<dt><code>paper.draw_hline(x, y, length, color)</code></dt>
<dd>Draw a horizontal line from <code>(x,y)</code> with the given <code>length</code> in the given <code>color</code>.</dd>
<dt><code>paper.draw_vline(x, y, length, color)</code></dt>
<dd>Draw a vertical line from <code>(x,y)</code> with the given <code>length</code> in the given <code>color</code>.</dd>
<dt><code>paper.draw_circle(x, y, radius, color)</code> / <code>paper.fill_circle(x, y, radius, color)</code></dt>
<dd>Draw an outline around a circle at <code>(x,y)</code> with the given <code>radius</code> in the given <code>color</code>. <code>fill_circle</code> draws the same circle with a solid filling.</dd>
<dt><code>paper.draw_rect(x1, y1, x2, y2, color)</code> / <code>paper.fill_rect(x1, y1, x2, y2, color)</code></dt>
<dd>Draw an outline around a rectangle with the upper left corner at <code>(x1,y1)</code> and lower right corner at <code>(x2,y2)</code> in the given <code>color</code>. <code>fill_rect</code> draws the same rectangle with a solid filling.</dd>
<dt><code>paper.draw_triangle(x1, y1, x2, y2, x3, y3, color)</code> / <code>paper.fill_triangle(x1, y1, x2, y2, x3, y3, color)</code></dt>
<dd>Draw an outline around a triangle with corners <code>(x1,y1)</code>, <code>(x2,y2)</code> and <code>(x3,y3)</code> in the given <code>color</code>. <code>fill_triangle</code> draws the same triangle with a solid filling.</dd>
</dl>
<p>Have fun!</p>
</body>
</html>

View file

@ -18,12 +18,20 @@
#include "server.h"
#define WIFI_SCAN_LIST_SIZE 10
#define LUA_FILE_PATH "/assets"
#define LFS_PATH "/assets"
#ifndef MIN
#define MIN(a, b) (((a) < (b)) ? (a) : (b))
#endif
/* Scratch buffer size for sending files to HTTP clients */
#define SCRATCH_BUFSIZE 8192
struct file_server_data {
/* Scratch buffer for temporary storage during file transfer */
char scratch[SCRATCH_BUFSIZE];
};
static const char *TAG = "inkpot";
// Function to log memory usage with the message at the end
@ -40,7 +48,7 @@ void init_filesystem() {
ESP_LOGI(TAG, "Initializing File System");
esp_vfs_littlefs_conf_t conf = {
.base_path = LUA_FILE_PATH,
.base_path = LFS_PATH,
.partition_label = "assets",
.format_if_mount_failed = false,
.dont_mount = false,
@ -50,7 +58,7 @@ void init_filesystem() {
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to mount or format filesystem");
} else {
ESP_LOGI(TAG, "Filesystem mounted at %s", LUA_FILE_PATH);
ESP_LOGI(TAG, "Filesystem mounted at %s", LFS_PATH);
}
}
@ -84,7 +92,7 @@ void run_lua_file(const char *file_name, const char *test_name) {
// Construct the full file path
char full_path[128];
snprintf(full_path, sizeof(full_path), LUA_FILE_PATH"/%s", file_name);
snprintf(full_path, sizeof(full_path), LFS_PATH"/%s", file_name);
if (luaL_dofile(L, full_path) == LUA_OK) {
lua_pop(L, lua_gettop(L));
@ -184,16 +192,37 @@ void scan_wifi_networks(void) {
// HTTP Server
#define WRITE_HEADER(req, buffer, name, format, src) \
sprintf(buffer, format, src); \
ESP_ERROR_CHECK(httpd_resp_set_hdr(req, name, buffer));
static esp_err_t http_index(httpd_req_t* req) {
// TODO: Serve HTML file with form POSTing to `/draw`
const char* response = "Hello world!\n";
httpd_resp_set_type(req, "text/plain");
httpd_resp_set_status(req, "200");
httpd_resp_send(req, response, HTTPD_RESP_USE_STRLEN);
char index_path[128];
strcpy(index_path, LFS_PATH);
strcat(index_path, "/http/index.html");
FILE *fd = fopen(index_path, "r");
char *chunk = ((struct file_server_data *)req->user_ctx)->scratch;
size_t chunksize;
do {
/* Read file in chunks into the scratch buffer */
chunksize = fread(chunk, 1, SCRATCH_BUFSIZE, fd);
if (chunksize > 0) {
/* Send the buffer contents as HTTP response chunk */
if (httpd_resp_send_chunk(req, chunk, chunksize) != ESP_OK) {
fclose(fd);
ESP_LOGE(TAG, "File sending failed!");
/* Abort sending file */
httpd_resp_sendstr_chunk(req, NULL);
/* Respond with 500 Internal Server Error */
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to send file");
return ESP_FAIL;
}
}
/* Keep looping till the whole file is sent */
} while (chunksize != 0);
fclose(fd);
httpd_resp_set_hdr(req, "Connection", "close");
httpd_resp_send_chunk(req, NULL, 0);
ESP_LOGI(TAG, "Sent index.html");
return ESP_OK;
}
@ -252,17 +281,33 @@ esp_err_t http_draw(httpd_req_t* req) {
return ESP_OK;
}
void register_http_routes(httpd_handle_t server) {
esp_err_t register_http_routes(httpd_handle_t server) {
static struct file_server_data *server_data = NULL;
if (server_data) {
ESP_LOGE(TAG, "File server already started");
return ESP_ERR_INVALID_STATE;
}
// Allocate memory for server data
server_data = calloc(1, sizeof(struct file_server_data));
if (!server_data) {
ESP_LOGE(TAG, "Failed to allocate memory for server data");
return ESP_ERR_NO_MEM;
}
{
httpd_uri_t uri
= { .uri = "/", .method = HTTP_GET, .handler = http_index, .user_ctx = NULL };
= { .uri = "/", .method = HTTP_GET, .handler = http_index, .user_ctx = server_data };
httpd_register_uri_handler(server, &uri);
}
{
httpd_uri_t uri
= { .uri = "/draw", .method = HTTP_POST, .handler = http_draw, .user_ctx = NULL };
= { .uri = "/draw", .method = HTTP_POST, .handler = http_draw, .user_ctx = server_data };
httpd_register_uri_handler(server, &uri);
}
return ESP_OK;
}
// init
@ -279,17 +324,15 @@ void app_main(void) {
}
ESP_ERROR_CHECK(ret);
// FIXME: Wifi server causes some crash, failing to yield to watchdog in time
// Set up HTTP server
httpd_handle_t server = get_server();
if (server != NULL) {
register_http_routes(server);
}
// Run script in assets/boot.lua; this is executed as a FreeRTOS task
// Run script in assets/scripts/boot.lua; this is executed as a FreeRTOS task
// that may not be interrupted
void runLuaFile (void* arg) {
run_lua_file("boot.lua", "E-Paper Startup Script");
run_lua_file("scripts/boot.lua", "E-Paper Startup Script");
vTaskDelete(NULL);
}
xTaskCreate(runLuaFile, "run_lua_file", 4096, NULL, tskIDLE_PRIORITY, NULL);