934 lines
28 KiB
C
934 lines
28 KiB
C
/*
|
|
* kscribe.c
|
|
*
|
|
* Simple native notepad for Kindle Scribe:
|
|
* - Reads stylus events from /dev/input/stylus
|
|
* - Draws into an 8bpp /dev/fb0 framebuffer
|
|
* - Maintains a 1-bit offscreen bitmap (g_bitmap) for PBM export
|
|
* - Also exports PNG via LodePNG from the same bitmap
|
|
* - Double-tap gestures:
|
|
* top-left = save (PBM + PNG) to /mnt/us/simple_notes
|
|
* top-right = exit
|
|
* bottom-right = reset page (clear) without saving
|
|
* - Optional: load a P1 PBM passed as argv[1] into framebuffer + g_bitmap
|
|
*
|
|
* Coordinate mapping:
|
|
* - Stylus X: RAW_X_MIN..RAW_X_MAX mapped to 0..(g_fb_width-1), reversed axis
|
|
* - Stylus Y: RAW_Y_MIN..RAW_Y_MAX mapped to 0..(g_fb_height-1), inverted axis
|
|
*/
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <stdint.h>
|
|
#include <unistd.h>
|
|
#include <fcntl.h>
|
|
#include <linux/input.h>
|
|
#include <sys/types.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/ioctl.h>
|
|
#include <sys/mman.h>
|
|
#include <string.h>
|
|
#include <errno.h>
|
|
#include <time.h>
|
|
#include <sys/time.h>
|
|
#include <linux/fb.h>
|
|
|
|
// ---- LodePNG for PNG export ----
|
|
#include "lodepng.h"
|
|
|
|
#include "kscribe.h"
|
|
|
|
// ============================================================================
|
|
// Time helpers
|
|
// ============================================================================
|
|
|
|
static uint64_t now_ms(void) {
|
|
struct timeval tv;
|
|
gettimeofday(&tv, NULL);
|
|
return (uint64_t)tv.tv_sec * 1000ULL + tv.tv_usec / 1000ULL;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Framebuffer helpers
|
|
// ============================================================================
|
|
|
|
static void framebuffer_clear_white(uint8_t *fbp, long screensize) {
|
|
memset(fbp, 0xFF, screensize); // white = 0xFF
|
|
}
|
|
|
|
// Initialize framebuffer context for /dev/fb0:
|
|
// - opens device
|
|
// - mmaps framebuffer to ctx->fbp
|
|
// - clears screen to white
|
|
// - allocates 1-bit g_bitmap backing store
|
|
static int fb_init(fb_ctx_t *ctx, const char *path) {
|
|
memset(ctx, 0, sizeof(*ctx));
|
|
ctx->fd = open(path, O_RDWR);
|
|
if (ctx->fd < 0) {
|
|
perror("open fb");
|
|
return -1;
|
|
}
|
|
if (ioctl(ctx->fd, FBIOGET_FSCREENINFO, &ctx->finfo) < 0) {
|
|
perror("FBIOGET_FSCREENINFO");
|
|
close(ctx->fd);
|
|
ctx->fd = -1;
|
|
return -1;
|
|
}
|
|
if (ioctl(ctx->fd, FBIOGET_VSCREENINFO, &ctx->vinfo) < 0) {
|
|
perror("FBIOGET_VSCREENINFO");
|
|
close(ctx->fd);
|
|
ctx->fd = -1;
|
|
return -1;
|
|
}
|
|
ctx->screensize = ctx->finfo.line_length * ctx->vinfo.yres;
|
|
ctx->fbp = mmap(NULL, ctx->screensize, PROT_READ | PROT_WRITE,
|
|
MAP_SHARED, ctx->fd, 0);
|
|
if (ctx->fbp == MAP_FAILED) {
|
|
perror("mmap fb");
|
|
ctx->fbp = NULL;
|
|
close(ctx->fd);
|
|
ctx->fd = -1;
|
|
return -1;
|
|
}
|
|
|
|
// Initial blank overlay: clear whole framebuffer to white once
|
|
framebuffer_clear_white(ctx->fbp, ctx->screensize);
|
|
msync(ctx->fbp, ctx->screensize, MS_SYNC);
|
|
|
|
fprintf(stderr, "fb: %ux%u, bpp=%u, line_length=%u\n",
|
|
ctx->vinfo.xres, ctx->vinfo.yres,
|
|
ctx->vinfo.bits_per_pixel, ctx->finfo.line_length);
|
|
|
|
if (ctx->vinfo.bits_per_pixel != 8) {
|
|
fprintf(stderr, "WARNUNG: bits_per_pixel != 8, Code erwartet 8bpp!\n");
|
|
}
|
|
|
|
g_fb_width = ctx->vinfo.xres;
|
|
g_fb_height = ctx->vinfo.yres;
|
|
|
|
g_bitmap = calloc(g_fb_width * g_fb_height, 1);
|
|
if (!g_bitmap) {
|
|
perror("calloc bitmap");
|
|
munmap(ctx->fbp, ctx->screensize);
|
|
ctx->fbp = NULL;
|
|
close(ctx->fd);
|
|
ctx->fd = -1;
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// Clear framebuffer and logical bitmap
|
|
static void fb_clear(fb_ctx_t *ctx, uint8_t value) {
|
|
if (!ctx->fbp) return;
|
|
memset(ctx->fbp, value, ctx->screensize);
|
|
if (g_bitmap) {
|
|
memset(g_bitmap, 0, g_fb_width * g_fb_height);
|
|
}
|
|
}
|
|
|
|
static void fb_close(fb_ctx_t *ctx) {
|
|
if (g_bitmap) {
|
|
free(g_bitmap);
|
|
g_bitmap = NULL;
|
|
}
|
|
|
|
if (ctx->fbp && ctx->fbp != MAP_FAILED) {
|
|
munmap(ctx->fbp, ctx->screensize);
|
|
ctx->fbp = NULL;
|
|
}
|
|
if (ctx->fd >= 0) {
|
|
close(ctx->fd);
|
|
ctx->fd = -1;
|
|
}
|
|
}
|
|
|
|
// Global eInk refresh
|
|
static void fb_flush(void) {
|
|
system("eips -r 2>/dev/null");
|
|
}
|
|
|
|
// ============================================================================
|
|
// Drawing primitives
|
|
// ============================================================================
|
|
|
|
// Set a single pixel in framebuffer + g_bitmap
|
|
static void fb_set_pixel(fb_ctx_t *ctx, int x, int y, uint8_t value) {
|
|
if (!ctx->fbp) return;
|
|
if (x < 0 || x >= g_fb_width || y < 0 || y >= g_fb_height) return;
|
|
|
|
size_t offset = (size_t)y * ctx->finfo.line_length + (size_t)x;
|
|
if (offset >= ctx->screensize) return;
|
|
ctx->fbp[offset] = value;
|
|
|
|
if (g_bitmap) {
|
|
size_t idx = (size_t)y * g_fb_width + (size_t)x;
|
|
if (idx < (size_t)g_fb_width * g_fb_height) {
|
|
g_bitmap[idx] = (value == 0x00) ? 1 : 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Draw a thick block around (cx, cy) more efficiently
|
|
static void fb_draw_thick_point(fb_ctx_t *ctx, int cx, int cy, uint8_t value) {
|
|
if (!ctx || !ctx->fbp) return;
|
|
|
|
// Fast path: 1-pixel stroke → behave exactly like before
|
|
if (STROKE_PX <= 1) {
|
|
fb_set_pixel(ctx, cx, cy, value);
|
|
return;
|
|
}
|
|
|
|
int half = STROKE_PX / 2;
|
|
|
|
int x0 = cx - half;
|
|
int x1 = cx + half;
|
|
int y0 = cy - half;
|
|
int y1 = cy + half;
|
|
|
|
// Completely outside the screen?
|
|
if (x1 < 0 || x0 >= g_fb_width || y1 < 0 || y0 >= g_fb_height) {
|
|
return;
|
|
}
|
|
|
|
// Clamp to framebuffer bounds
|
|
if (x0 < 0) x0 = 0;
|
|
if (y0 < 0) y0 = 0;
|
|
if (x1 >= g_fb_width) x1 = g_fb_width - 1;
|
|
if (y1 >= g_fb_height) y1 = g_fb_height - 1;
|
|
|
|
const size_t total_bits = (size_t)g_fb_width * g_fb_height;
|
|
|
|
for (int y = y0; y <= y1; ++y) {
|
|
// Framebuffer row start
|
|
size_t fb_off = (size_t)y * ctx->finfo.line_length + (size_t)x0;
|
|
if (fb_off >= ctx->screensize) {
|
|
// Safety; should not happen after clamping
|
|
break;
|
|
}
|
|
|
|
uint8_t *fb_row = ctx->fbp + fb_off;
|
|
|
|
// Bitmap row start
|
|
size_t bmp_idx = (size_t)y * g_fb_width + (size_t)x0;
|
|
|
|
for (int x = x0; x <= x1; ++x) {
|
|
*fb_row++ = value;
|
|
|
|
if (g_bitmap && bmp_idx < total_bits) {
|
|
// Keep same semantics as fb_set_pixel: 0x00 = black = 1
|
|
g_bitmap[bmp_idx] = (value == 0x00) ? 1 : 0;
|
|
}
|
|
++bmp_idx;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Bresenham in full-res pixel space
|
|
static void fb_draw_line(fb_ctx_t *ctx, int x0, int y0, int x1, int y1, uint8_t value) {
|
|
int dx = abs(x1 - x0);
|
|
int sx = (x0 < x1) ? 1 : -1;
|
|
int dy = -abs(y1 - y0);
|
|
int sy = (y0 < y1) ? 1 : -1;
|
|
int err = dx + dy;
|
|
|
|
int x = x0;
|
|
int y = y0;
|
|
|
|
while (1) {
|
|
fb_draw_thick_point(ctx, x, y, value);
|
|
if (x == x1 && y == y1) break;
|
|
int e2 = 2 * err;
|
|
if (e2 >= dy) { err += dy; x += sx; }
|
|
if (e2 <= dx) { err += dx; y += sy; }
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Tiny 5x7 bitmap font for PNG titlebar
|
|
// ============================================================================
|
|
//
|
|
// Each glyph: 5 pixels wide, 7 pixels high, stored in 7 bytes.
|
|
// Bit 0 = leftmost pixel. 1 = black, 0 = white.
|
|
|
|
static const uint8_t font5x7[96][TITLE_FONT_H] = {
|
|
// index = ch - 32 (' ' is index 0)
|
|
|
|
// ' ' (space)
|
|
[0] = { 0x00,0x00,0x00,0x00,0x00,0x00,0x00 },
|
|
|
|
// '0'
|
|
['0' - 32] = { 0x1E,0x21,0x23,0x25,0x29,0x31,0x1E },
|
|
// '1'
|
|
['1' - 32] = { 0x04,0x0C,0x14,0x04,0x04,0x04,0x1F },
|
|
// '2'
|
|
['2' - 32] = { 0x1E,0x21,0x01,0x0E,0x10,0x20,0x3F },
|
|
// '3'
|
|
['3' - 32] = { 0x1E,0x21,0x01,0x0E,0x01,0x21,0x1E },
|
|
// '4'
|
|
['4' - 32] = { 0x02,0x06,0x0A,0x12,0x3F,0x02,0x02 },
|
|
// '5'
|
|
['5' - 32] = { 0x3F,0x20,0x3E,0x01,0x01,0x21,0x1E },
|
|
// '6'
|
|
['6' - 32] = { 0x0E,0x10,0x20,0x3E,0x21,0x21,0x1E },
|
|
// '7'
|
|
['7' - 32] = { 0x3F,0x01,0x02,0x04,0x08,0x10,0x10 },
|
|
// '8'
|
|
['8' - 32] = { 0x1E,0x21,0x21,0x1E,0x21,0x21,0x1E },
|
|
// '9'
|
|
['9' - 32] = { 0x1E,0x21,0x21,0x1F,0x01,0x02,0x1C },
|
|
|
|
// 'A'
|
|
['A' - 32] = { 0x0E,0x11,0x11,0x1F,0x11,0x11,0x11 },
|
|
// 'B'
|
|
['B' - 32] = { 0x1E,0x11,0x11,0x1E,0x11,0x11,0x1E },
|
|
// 'C'
|
|
['C' - 32] = { 0x0E,0x11,0x10,0x10,0x10,0x11,0x0E },
|
|
// 'D'
|
|
['D' - 32] = { 0x1C,0x12,0x11,0x11,0x11,0x12,0x1C },
|
|
// 'E'
|
|
['E' - 32] = { 0x1F,0x10,0x10,0x1E,0x10,0x10,0x1F },
|
|
// 'F'
|
|
['F' - 32] = { 0x1F,0x10,0x10,0x1E,0x10,0x10,0x10 },
|
|
// 'G'
|
|
['G' - 32] = { 0x0E,0x11,0x10,0x17,0x11,0x11,0x0F },
|
|
// 'H'
|
|
['H' - 32] = { 0x11,0x11,0x11,0x1F,0x11,0x11,0x11 },
|
|
// 'I'
|
|
['I' - 32] = { 0x0E,0x04,0x04,0x04,0x04,0x04,0x0E },
|
|
// 'J'
|
|
['J' - 32] = { 0x07,0x02,0x02,0x02,0x02,0x12,0x0C },
|
|
// 'K'
|
|
['K' - 32] = { 0x11,0x12,0x14,0x18,0x14,0x12,0x11 },
|
|
// 'L'
|
|
['L' - 32] = { 0x10,0x10,0x10,0x10,0x10,0x10,0x1F },
|
|
// 'M'
|
|
['M' - 32] = { 0x11,0x1B,0x15,0x15,0x11,0x11,0x11 },
|
|
// 'N'
|
|
['N' - 32] = { 0x11,0x19,0x15,0x13,0x11,0x11,0x11 },
|
|
// 'O'
|
|
['O' - 32] = { 0x0E,0x11,0x11,0x11,0x11,0x11,0x0E },
|
|
// 'P'
|
|
['P' - 32] = { 0x1E,0x11,0x11,0x1E,0x10,0x10,0x10 },
|
|
// 'Q'
|
|
['Q' - 32] = { 0x0E,0x11,0x11,0x11,0x15,0x12,0x0D },
|
|
// 'R'
|
|
['R' - 32] = { 0x1E,0x11,0x11,0x1E,0x14,0x12,0x11 },
|
|
// 'S'
|
|
['S' - 32] = { 0x0F,0x10,0x10,0x0E,0x01,0x01,0x1E },
|
|
// 'T'
|
|
['T' - 32] = { 0x1F,0x04,0x04,0x04,0x04,0x04,0x04 },
|
|
// 'U'
|
|
['U' - 32] = { 0x11,0x11,0x11,0x11,0x11,0x11,0x0E },
|
|
// 'V'
|
|
['V' - 32] = { 0x11,0x11,0x11,0x11,0x11,0x0A,0x04 },
|
|
// 'W'
|
|
['W' - 32] = { 0x11,0x11,0x11,0x15,0x15,0x1B,0x11 },
|
|
// 'X'
|
|
['X' - 32] = { 0x11,0x11,0x0A,0x04,0x0A,0x11,0x11 },
|
|
// 'Y'
|
|
['Y' - 32] = { 0x11,0x11,0x0A,0x04,0x04,0x04,0x04 },
|
|
// 'Z'
|
|
['Z' - 32] = { 0x1F,0x01,0x02,0x04,0x08,0x10,0x1F },
|
|
|
|
// '.'
|
|
['.' - 32] = { 0x00,0x00,0x00,0x00,0x00,0x18,0x18 },
|
|
// '-'
|
|
['-' - 32] = { 0x00,0x00,0x00,0x1F,0x00,0x00,0x00 },
|
|
// ':'
|
|
[':' - 32] = { 0x00,0x18,0x18,0x00,0x18,0x18,0x00 },
|
|
};
|
|
|
|
|
|
|
|
// Lowercase: reuse uppercase glyphs
|
|
static const uint8_t *get_glyph_5x7(char ch) {
|
|
unsigned char c = (unsigned char)ch;
|
|
if (c < 32 || c > 127) return font5x7[0]; // space
|
|
if (c >= 'a' && c <= 'z') {
|
|
c = (unsigned char)(c - 'a' + 'A');
|
|
}
|
|
return font5x7[c - 32];
|
|
}
|
|
|
|
static void rgba_set_pixel(unsigned char *image, int x, int y, uint8_t c) {
|
|
if (!image) return;
|
|
if (x < 0 || x >= g_fb_width || y < 0 || y >= g_fb_height) return;
|
|
|
|
size_t idx = ((size_t)y * (size_t)g_fb_width + (size_t)x) * 4;
|
|
image[idx + 0] = c; // R
|
|
image[idx + 1] = c; // G
|
|
image[idx + 2] = c; // B
|
|
image[idx + 3] = 0xFF; // A
|
|
}
|
|
|
|
static void draw_char_5x7_rgba(unsigned char *image, int x, int y, char ch) {
|
|
const uint8_t *glyph = get_glyph_5x7(ch);
|
|
for (int row = 0; row < TITLE_FONT_H; ++row) {
|
|
uint8_t bits = glyph[row];
|
|
for (int col = 0; col < TITLE_FONT_W; ++col) {
|
|
if (bits & (1u << col)) {
|
|
rgba_set_pixel(image, x + col, y + row, 0x00); // black
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Draw one-line title text into the same region as the on-screen topbar.
|
|
static void draw_titlebar_rgba(unsigned char *image, const char *title) {
|
|
if (!image || !title) return;
|
|
|
|
int margin_x = 40; // roughly matches "eips 12 1" horizontal start
|
|
int margin_y = 20; // safely inside TOPBAR_H
|
|
int x = margin_x;
|
|
int y = margin_y;
|
|
|
|
while (*title && x + TITLE_FONT_W < g_fb_width) {
|
|
if (*title == '\n') {
|
|
// (optional) support a second line inside TOPBAR_H
|
|
x = margin_x;
|
|
y += TITLE_FONT_H + 2;
|
|
if (y + TITLE_FONT_H >= TOPBAR_H) break;
|
|
++title;
|
|
continue;
|
|
}
|
|
draw_char_5x7_rgba(image, x, y, *title);
|
|
x += TITLE_FONT_ADVANCE;
|
|
++title;
|
|
if (y + TITLE_FONT_H >= TOPBAR_H) {
|
|
// stop if we would leave the topbar vertically
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// ============================================================================
|
|
// PBM load
|
|
// ============================================================================
|
|
|
|
// Load a full-screen P1 PBM into:
|
|
// - g_bitmap (1=black, 0=white)
|
|
// - framebuffer (0x00=black, 0xFF=white)
|
|
static int load_pbm_into_fb(fb_ctx_t *ctx, const char *path) {
|
|
if (!ctx || !ctx->fbp) {
|
|
fprintf(stderr, "load_pbm_into_fb: framebuffer not initialized\n");
|
|
return -1;
|
|
}
|
|
|
|
FILE *f = fopen(path, "r");
|
|
if (!f) {
|
|
perror("fopen load_pbm_into_fb");
|
|
return -1;
|
|
}
|
|
|
|
// Read magic "P1"
|
|
char magic[3] = {0};
|
|
if (!fgets(magic, sizeof(magic), f)) {
|
|
fprintf(stderr, "load_pbm_into_fb: failed to read magic\n");
|
|
fclose(f);
|
|
return -1;
|
|
}
|
|
if (magic[0] != 'P' || magic[1] != '1') {
|
|
fprintf(stderr, "load_pbm_into_fb: not a P1 PBM file\n");
|
|
fclose(f);
|
|
return -1;
|
|
}
|
|
|
|
// Read width/height (no comments in our own PBMs)
|
|
int w = 0, h = 0;
|
|
if (fscanf(f, "%d %d", &w, &h) != 2) {
|
|
fprintf(stderr, "load_pbm_into_fb: failed to read width/height\n");
|
|
fclose(f);
|
|
return -1;
|
|
}
|
|
|
|
if (w != g_fb_width || h != g_fb_height) {
|
|
fprintf(stderr,
|
|
"load_pbm_into_fb: size mismatch (pbm=%dx%d, fb=%dx%d)\n",
|
|
w, h, g_fb_width, g_fb_height);
|
|
fclose(f);
|
|
return -1;
|
|
}
|
|
|
|
// Clear current content (framebuffer + g_bitmap)
|
|
fb_clear(ctx, 0xFF); // white background
|
|
|
|
// Read each pixel (0/1) and map: 1 -> black (0x00), 0 -> white (0xFF)
|
|
for (int y = 0; y < h; y++) {
|
|
for (int x = 0; x < w; x++) {
|
|
int bit = 0;
|
|
if (fscanf(f, "%d", &bit) != 1) {
|
|
fprintf(stderr,
|
|
"load_pbm_into_fb: unexpected EOF or parse error\n");
|
|
fclose(f);
|
|
return -1;
|
|
}
|
|
uint8_t v = bit ? 0x00 : 0xFF;
|
|
fb_set_pixel(ctx, x, y, v);
|
|
}
|
|
}
|
|
|
|
fclose(f);
|
|
|
|
// Ensure data is pushed out to the device
|
|
msync(ctx->fbp, ctx->screensize, MS_SYNC);
|
|
|
|
return 0;
|
|
}
|
|
|
|
// ============================================================================
|
|
// PNG export using LodePNG
|
|
// ============================================================================
|
|
|
|
// Convert g_bitmap (1-bit as bytes) to RGBA and write PNG.
|
|
static int save_page_png(const char *png_path, const char *title_text) {
|
|
if (!g_bitmap || g_fb_width <= 0 || g_fb_height <= 0) return -1;
|
|
|
|
size_t w = (size_t)g_fb_width;
|
|
size_t h = (size_t)g_fb_height;
|
|
size_t num_pixels = w * h;
|
|
size_t buf_size = num_pixels * 4;
|
|
|
|
unsigned char *image = (unsigned char *)malloc(buf_size);
|
|
if (!image) {
|
|
perror("malloc PNG buffer");
|
|
return -1;
|
|
}
|
|
|
|
for (size_t i = 0; i < num_pixels; i++) {
|
|
uint8_t bit = g_bitmap[i]; // 1=black, 0=white
|
|
uint8_t c = bit ? 0x00 : 0xFF; // black or white
|
|
size_t idx = i * 4;
|
|
image[idx + 0] = c; // R
|
|
image[idx + 1] = c; // G
|
|
image[idx + 2] = c; // B
|
|
image[idx + 3] = 0xFF; // A
|
|
}
|
|
|
|
// Overlay titlebar text in the same region as the on-screen topbar
|
|
if (title_text && *title_text) {
|
|
draw_titlebar_rgba(image, title_text);
|
|
}
|
|
|
|
unsigned error = lodepng_encode32_file(png_path, image, (unsigned)w, (unsigned)h);
|
|
free(image);
|
|
|
|
if (error) {
|
|
fprintf(stderr, "LodePNG error %u: %s\n", error, lodepng_error_text(error));
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// ============================================================================
|
|
// PBM save
|
|
// ============================================================================
|
|
|
|
|
|
// Save current g_bitmap as a P1 PBM in /mnt/us/simple_notes with timestamped name.
|
|
// Additionally write a PNG with the same basename via LodePNG.
|
|
static void save_page(void) {
|
|
if (!g_bitmap || g_fb_width <= 0 || g_fb_height <= 0) return;
|
|
|
|
time_t now = time(NULL);
|
|
struct tm *tm = localtime(&now);
|
|
|
|
mkdir(US_SAVE_PATH, 0777);
|
|
|
|
char file_ts[50];
|
|
char pbm_path[160];
|
|
char png_path[160];
|
|
|
|
// Base name with .pbm
|
|
snprintf(file_ts, sizeof(file_ts),
|
|
"%04d%02d%02d-%02d%02d%02d",
|
|
tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday,
|
|
tm->tm_hour, tm->tm_min, tm->tm_sec);
|
|
|
|
//build pbm file path -> only one pbm ist stored because of its size atm so filename latest.fb
|
|
snprintf(pbm_path, sizeof(pbm_path), "%s/bin/latest.fb", US_SAVE_PATH);
|
|
|
|
//build png fale using timestamp and global save path
|
|
snprintf(png_path, sizeof(png_path), "%s/note-%s.png", US_SAVE_PATH, file_ts);
|
|
|
|
|
|
// --- PBM write as before ---
|
|
FILE *f = fopen(pbm_path, "w");
|
|
if (!f) {
|
|
perror("fopen save_page (PBM)");
|
|
return;
|
|
}
|
|
|
|
fprintf(f, "P1\n");
|
|
fprintf(f, "%d %d\n", g_fb_width, g_fb_height);
|
|
for (int y = 0; y < g_fb_height; y++) {
|
|
for (int x = 0; x < g_fb_width; x++) {
|
|
size_t idx = (size_t)y * g_fb_width + (size_t)x;
|
|
int v = (idx < (size_t)g_fb_width * g_fb_height && g_bitmap[idx]) ? 1 : 0;
|
|
fputc(v ? '1' : '0', f);
|
|
if (x < g_fb_width - 1) fputc(' ', f);
|
|
}
|
|
fputc('\n', f);
|
|
}
|
|
fclose(f);
|
|
|
|
// --- PNG write via LodePNG ---
|
|
char title_text[256];
|
|
snprintf(title_text, sizeof(title_text), "note-%s", file_ts);
|
|
int png_ok = save_page_png(png_path, title_text);
|
|
|
|
char msg[256];
|
|
if (png_ok == 0) {
|
|
snprintf(msg, sizeof(msg),
|
|
"eips 7 1 \"Saved: note-%s.png\"",
|
|
file_ts);
|
|
} else {
|
|
snprintf(msg, sizeof(msg),
|
|
"eips 7 1 \"Failed saving PNG - %s\"",
|
|
file_ts);
|
|
}
|
|
system(msg);
|
|
}
|
|
|
|
// ============================================================================
|
|
// UI / title
|
|
// ============================================================================
|
|
|
|
// Draw a small "X" icon in the topbar (currently unused, kept for reference)
|
|
static void draw_topbar_icons(fb_ctx_t *ctx) {
|
|
int margin = 10;
|
|
int size = TOPBAR_H - 5 * margin;
|
|
if (size <= 0) return;
|
|
|
|
int x1 = g_fb_width - margin - size;
|
|
int y1 = margin;
|
|
int x2 = g_fb_width - margin;
|
|
int y2 = margin + size;
|
|
|
|
fb_draw_line(ctx, x1, y1, x2, y2, 0x00);
|
|
fb_draw_line(ctx, x1, y2, x2, y1, 0x00);
|
|
}
|
|
|
|
// Draw a small PNG icon using eips at absolute pixel coordinates.
|
|
static void draw_icon_png(const char *file, int x, int y)
|
|
{
|
|
char path[160];
|
|
|
|
// Build: /mnt/us/penpad/assets/<file>
|
|
snprintf(path, sizeof(path), "%s/assets/%s", US_HOME_PATH, file);
|
|
|
|
char cmd[512];
|
|
// -g : PNG
|
|
// -x, -y : position
|
|
snprintf(cmd, sizeof(cmd),
|
|
"eips -g %s -x %d -y %d",
|
|
path, x, y);
|
|
|
|
system(cmd);
|
|
}
|
|
|
|
static void printTitle(void) {
|
|
system("eips -a 40");
|
|
system("eips 0 0 \" \"");
|
|
system("eips 12 1 \"simplenotes by maru21\"");
|
|
|
|
draw_icon_png("exit.png", 1800, 1);
|
|
draw_icon_png("reset.png", 1795, 2410);
|
|
draw_icon_png("save.png", 100, 1);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Mapping raw stylus → screen pixels
|
|
// ============================================================================
|
|
|
|
// X: raw left ~15624, right ~0 → reversed axis.
|
|
// raw range RAW_X_MIN..RAW_X_MAX mapped to pixel 0..(xres-1).
|
|
static int map_raw_to_px(int rawx, const fb_ctx_t *ctx) {
|
|
if (rawx < RAW_X_MIN) rawx = RAW_X_MIN;
|
|
if (rawx > RAW_X_MAX) rawx = RAW_X_MAX;
|
|
int range = RAW_X_MAX - RAW_X_MIN;
|
|
if (range <= 0) return 0;
|
|
|
|
// reverse axis: RAW_X_MAX → x=0, RAW_X_MIN → x=max
|
|
int reversed = RAW_X_MAX - rawx;
|
|
int px = reversed * (int)(ctx->vinfo.xres - 1) / (range + 1);
|
|
if (px < 0) px = 0;
|
|
if (px >= (int)ctx->vinfo.xres) px = ctx->vinfo.xres - 1;
|
|
return px;
|
|
}
|
|
|
|
// Y: raw top ~20832, bottom ~0 → inverted.
|
|
// raw range RAW_Y_MIN..RAW_Y_MAX mapped to pixel 0..(yres-1).
|
|
static int map_raw_to_py(int rawy, const fb_ctx_t *ctx) {
|
|
if (rawy < RAW_Y_MIN) rawy = RAW_Y_MIN;
|
|
if (rawy > RAW_Y_MAX) rawy = RAW_Y_MAX;
|
|
int range = RAW_Y_MAX - RAW_Y_MIN;
|
|
if (range <= 0) return 0;
|
|
|
|
int inverted = RAW_Y_MAX - rawy;
|
|
int py = inverted * (int)(ctx->vinfo.yres - 1) / (range + 1);
|
|
if (py < 0) py = 0;
|
|
if (py >= (int)ctx->vinfo.yres) py = ctx->vinfo.yres - 1;
|
|
return py;
|
|
}
|
|
|
|
static int u_map_raw_to_px(int rawx, const fb_ctx_t *ctx) {
|
|
if (rawx < RAW_X_MIN) rawx = RAW_X_MIN;
|
|
if (rawx > RAW_X_MAX) rawx = RAW_X_MAX;
|
|
int range = RAW_X_MAX - RAW_X_MIN;
|
|
if (range <= 0) return 0;
|
|
|
|
// direct mapping, no reversed axis
|
|
int px = (rawx - RAW_X_MIN) * (int)(ctx->vinfo.xres - 1) / range;
|
|
|
|
if (px < 0) px = 0;
|
|
if (px >= (int)ctx->vinfo.xres) px = ctx->vinfo.xres - 1;
|
|
return px;
|
|
}
|
|
|
|
static int u_map_raw_to_py(int rawy, const fb_ctx_t *ctx) {
|
|
if (rawy < RAW_Y_MIN) rawy = RAW_Y_MIN;
|
|
if (rawy > RAW_Y_MAX) rawy = RAW_Y_MAX;
|
|
int range = RAW_Y_MAX - RAW_Y_MIN;
|
|
if (range <= 0) return 0;
|
|
|
|
// direct mapping, no inversion
|
|
int py = (rawy - RAW_Y_MIN) * (int)(ctx->vinfo.yres - 1) / range;
|
|
|
|
if (py < 0) py = 0;
|
|
if (py >= (int)ctx->vinfo.yres) py = ctx->vinfo.yres - 1;
|
|
return py;
|
|
}
|
|
|
|
|
|
// ============================================================================
|
|
// Double-tap handling
|
|
// ============================================================================
|
|
//
|
|
// Zones:
|
|
// top-left (py < TOPBAR_H, px < g_fb_width/2) → save
|
|
// top-right (py < TOPBAR_H, px >= g_fb_width/2) → exit
|
|
// bottom-right (py > g_fb_height - TOPBAR_H,
|
|
// px >= g_fb_width/2) → reset (no save)
|
|
//
|
|
// Returns:
|
|
// 0 = no action
|
|
// 1 = double-tap top-left (save)
|
|
// 2 = double-tap top-right (exit)
|
|
// 3 = double-tap bottom-right (reset page, no save)
|
|
|
|
static int handle_tap(int px, int py) {
|
|
tap_zone_t zone = TAP_NONE;
|
|
|
|
if (py < TOPBAR_H) {
|
|
zone = (px < g_fb_width / 2) ? TAP_LEFT : TAP_RIGHT;
|
|
} else if (py > g_fb_height - TOPBAR_H && px >= g_fb_width / 2) {
|
|
zone = TAP_BOTTOM_RIGHT;
|
|
} else {
|
|
// outside gesture regions: reset tap tracking
|
|
g_tap_count = 0;
|
|
g_last_tap_zone = TAP_NONE;
|
|
return 0;
|
|
}
|
|
|
|
time_t now_t = time(NULL);
|
|
|
|
if (zone == g_last_tap_zone &&
|
|
(now_t - g_last_tap_timestamp) <= DOUBLE_TAP_WINDOW_SEC) {
|
|
g_tap_count++;
|
|
} else {
|
|
g_tap_count = 1;
|
|
g_last_tap_zone = zone;
|
|
}
|
|
g_last_tap_timestamp = now_t;
|
|
|
|
if (g_tap_count >= 2) {
|
|
g_tap_count = 0;
|
|
g_last_tap_zone = TAP_NONE;
|
|
if (zone == TAP_LEFT) return 1;
|
|
else if (zone == TAP_RIGHT) return 2;
|
|
else if (zone == TAP_BOTTOM_RIGHT) return 3;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// ============================================================================
|
|
// main
|
|
// ============================================================================
|
|
|
|
int main(int argc, char **argv) {
|
|
fb_ctx_t fb;
|
|
if (fb_init(&fb, DEV_PATH_FB) != 0) {
|
|
fprintf(stderr, "fb_init failed\n");
|
|
return 1;
|
|
}
|
|
|
|
int stylus_fd = open(DEV_PATH_STYLUS, O_RDONLY);
|
|
if (stylus_fd < 0) {
|
|
perror("open stylus");
|
|
fb_close(&fb);
|
|
return 1;
|
|
}
|
|
|
|
// Exclusive grab so the framework cannot see stylus events
|
|
if (ioctl(stylus_fd, EVIOCGRAB, (void *)1) < 0) {
|
|
perror("EVIOCGRAB stylus");
|
|
close(stylus_fd);
|
|
fb_close(&fb);
|
|
return 1;
|
|
}
|
|
|
|
int touch_fd = open(DEV_PATH_TOUCH, O_RDONLY);
|
|
if (touch_fd < 0) {
|
|
perror("open touch");
|
|
touch_fd = -1; // weiterlaufen ohne Touch-Grab
|
|
} else {
|
|
// Exclusive grab so the framework cannot see touch events
|
|
if (ioctl(touch_fd, EVIOCGRAB, (void *)1) < 0) {
|
|
perror("EVIOCGRAB touch");
|
|
// trotzdem weiterlaufen, touch_fd bleibt offen
|
|
}
|
|
}
|
|
|
|
// Give the framework time to draw its last stuff, then wipe it away.
|
|
sleep(1);
|
|
fb_clear(&fb, 0xFF); // all white (also clears g_bitmap)
|
|
|
|
system("eips -c");
|
|
|
|
// ---- Load PBM if provided as first argument ----
|
|
if (argc > 1) {
|
|
const char *pbm_path = argv[1];
|
|
fprintf(stderr, "Loading PBM: %s\n", pbm_path);
|
|
|
|
if (load_pbm_into_fb(&fb, pbm_path) != 0) {
|
|
fprintf(stderr, "Failed to load PBM from %s\n", pbm_path);
|
|
}
|
|
}
|
|
|
|
printTitle();
|
|
//draw_topbar_icons(&fb);
|
|
|
|
fb_flush();
|
|
|
|
struct input_event ev;
|
|
int32_t stylus_raw_x = 0;
|
|
int32_t stylus_raw_y = 0;
|
|
int pen_is_down = 0;
|
|
int last_pen_px = -1;
|
|
int last_pen_py = -1;
|
|
|
|
uint64_t last_flush = now_ms();
|
|
int dirty_since_flush = 0;
|
|
|
|
while (1) {
|
|
ssize_t r = read(stylus_fd, &ev, sizeof(ev));
|
|
if (r < (ssize_t)sizeof(ev)) {
|
|
if (r < 0 && errno == EINTR) continue;
|
|
perror("read stylus");
|
|
break;
|
|
}
|
|
|
|
if (ev.type == EV_ABS) {
|
|
if (ev.code == ABS_X) stylus_raw_x = ev.value;
|
|
else if (ev.code == ABS_Y) stylus_raw_y = ev.value;
|
|
} else if (ev.type == EV_KEY && ev.code == 0x14A) {
|
|
// Pen down / up
|
|
pen_is_down = (ev.value == 1);
|
|
if (!pen_is_down) {
|
|
last_pen_px = last_pen_py = -1;
|
|
}
|
|
|
|
// Double-tap detection ONLY on pen-down events
|
|
if (ev.value == 1) {
|
|
int px = map_raw_to_px(stylus_raw_x, &fb);
|
|
int py = map_raw_to_py(stylus_raw_y, &fb);
|
|
|
|
int tap_result = handle_tap(px, py);
|
|
if (tap_result == 1) {
|
|
// top-left: save (PBM + PNG)
|
|
fb_flush();
|
|
save_page();
|
|
sleep(SAVE_FEEDBACK_SLEEP_SEC);
|
|
fb_clear(&fb, 0xFF);
|
|
system("eips -c");
|
|
printTitle();
|
|
//draw_topbar_icons(&fb);
|
|
fb_flush();
|
|
|
|
// reset stroke state and skip drawing for this event
|
|
pen_is_down = 0;
|
|
last_pen_px = last_pen_py = -1;
|
|
continue;
|
|
} else if (tap_result == 2) {
|
|
// top-right: EXIT
|
|
fb_flush();
|
|
//save_page();
|
|
fb_clear(&fb, 0xFF);
|
|
system("eips 1 1 \"bye bye\"");
|
|
break; // aus while(1)
|
|
} else if (tap_result == 3) {
|
|
// bottom-right: RESET (no save)
|
|
fb_flush();
|
|
fb_clear(&fb, 0xFF);
|
|
system("eips -c");
|
|
printTitle();
|
|
//draw_topbar_icons(&fb);
|
|
fb_flush();
|
|
|
|
// reset stroke state and skip drawing for this event
|
|
pen_is_down = 0;
|
|
last_pen_px = last_pen_py = -1;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Drawing while pen is down
|
|
if (pen_is_down) {
|
|
int px = map_raw_to_px(stylus_raw_x, &fb);
|
|
int py = map_raw_to_py(stylus_raw_y, &fb);
|
|
|
|
// Do not draw in top bar
|
|
if (py >= TOPBAR_H) {
|
|
if (last_pen_px >= 0 && last_pen_py >= 0) {
|
|
fb_draw_line(&fb, last_pen_px, last_pen_py, px, py, 0x00);
|
|
} else {
|
|
fb_draw_thick_point(&fb, px, py, 0x00);
|
|
}
|
|
last_pen_px = px;
|
|
last_pen_py = py;
|
|
dirty_since_flush = 1;
|
|
} else {
|
|
// If pen moves into top bar, break the stroke
|
|
last_pen_px = last_pen_py = -1;
|
|
}
|
|
}
|
|
|
|
uint64_t now_m = now_ms();
|
|
if (dirty_since_flush && (now_m - last_flush >= REFRESH_INTERVAL_MS)) {
|
|
fb_flush();
|
|
last_flush = now_m;
|
|
dirty_since_flush = 0;
|
|
}
|
|
}
|
|
|
|
// On normal loop break: release stylus/touch grab and clean up
|
|
ioctl(stylus_fd, EVIOCGRAB, (void *)0);
|
|
close(stylus_fd);
|
|
|
|
if (touch_fd >= 0) {
|
|
ioctl(touch_fd, EVIOCGRAB, (void *)0);
|
|
close(touch_fd);
|
|
}
|
|
|
|
fb_close(&fb);
|
|
return 0;
|
|
}
|