diff --git a/compile.sh b/compile.sh new file mode 100644 index 0000000..16d328f --- /dev/null +++ b/compile.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +IN="kscribe.c" +OUT="kscribe" + +echo "---------- COMPILING: ${IN} TO ${OUT} ----------" +rm -i $OUT +arm-linux-gnueabihf-gcc -O2 -static -o $OUT $IN lodepng.c -lm -ldl +file $OUT diff --git a/kscribe.c b/kscribe.c new file mode 100644 index 0000000..04dc3d7 --- /dev/null +++ b/kscribe.c @@ -0,0 +1,933 @@ +/* + * 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// ---- 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/ + 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; +} diff --git a/kscribe.h b/kscribe.h new file mode 100644 index 0000000..266e298 --- /dev/null +++ b/kscribe.h @@ -0,0 +1,66 @@ +// DEV devices +#define DEV_PATH_STYLUS "/dev/input/stylus" +#define DEV_PATH_TOUCH "/dev/input/touch" +#define DEV_PATH_FB "/dev/fb0" + +// Path definitions +#define US_HOME_PATH "/mnt/us/extensions/simplenotes" +#define US_SAVE_PATH "/mnt/us/simplenotes" + +// Raw stylus ranges (from full calibration) +#define RAW_X_MIN 0 +#define RAW_X_MAX 15624 // left ~15624, right ~0 +#define RAW_Y_MIN 0 +#define RAW_Y_MAX 20832 // top ~20832, bottom ~0 + + +// Stroke thickness in pixels +#define STROKE_PX 3 + +// Height of the top/bottom gesture bar in pixels +#define TOPBAR_H 100 + +// Refresh interval in ms +#define REFRESH_INTERVAL_MS 45 + +// Double-tap window and feedback delay +#define DOUBLE_TAP_WINDOW_SEC 2 +#define SAVE_FEEDBACK_SLEEP_SEC 1 + + +// Tiny 5x7 bitmap font +#define TITLE_FONT_W 5 +#define TITLE_FONT_H 7 +#define TITLE_FONT_ADVANCE 6 // 5px glyph + 1px space + +// ============================================================================ +// Framebuffer context +// ============================================================================ + +typedef struct { + int fd; + struct fb_fix_screeninfo finfo; + struct fb_var_screeninfo vinfo; + uint8_t *fbp; + size_t screensize; +} fb_ctx_t; + +// ============================================================================ +// Global drawing / state +// ============================================================================ + +static uint8_t *g_bitmap = NULL; // 1-bit (stored as bytes) full-screen: 1=black, 0=white +static int g_fb_width = 0; +static int g_fb_height = 0; + +typedef enum { + TAP_NONE = 0, + TAP_LEFT, + TAP_RIGHT, + TAP_BOTTOM_RIGHT +} tap_zone_t; + +static tap_zone_t g_last_tap_zone = TAP_NONE; +static int g_tap_count = 0; +static time_t g_last_tap_timestamp = 0; +