/* * penpad_fb2.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" // 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 // ============================================================================ // 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; // ============================================================================ // 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; } } } // ============================================================================ // 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) { 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 } 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 --- int png_ok = save_page_png(png_path); 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; }