/* * 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; }