Upload files to "/"

This commit is contained in:
maru21 2025-12-08 22:30:32 +01:00
parent f24c7c9cbd
commit b509368098
3 changed files with 1008 additions and 0 deletions

9
compile.sh Normal file
View File

@ -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

933
kscribe.c Normal file
View File

@ -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 <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;
}

66
kscribe.h Normal file
View File

@ -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;