first attempt at ui

This commit is contained in:
Miguel Fuertes committed 2025-10-15 23:51:08 +02:00
1 parent f8b978df15
commit 45c9f55473
6 files changed
+382 -1

No files matched your search

+1 -1
View File
@@ -47,7 +47,7 @@ define Device/generic-mf68e
CMDLINE := "earlycon console=ttyMSM0,115200 quiet loglevel=0 vt.global_cursor_default=0 vt.cur_default=0 root=/dev/mmcblk0p14 rootfstype=squashfs rootwait"
DEVICE_PACKAGES := configs-mf68e wpad-basic-wolfssl rmtfs uci-usb-gadget \
block-mount f2fs-tools prepare-rootfs-data \
msm-firmware-dumper power-button-daemon kmod-fbtft-gc9107 display-monitor
msm-firmware-dumper power-button-daemon kmod-fbtft-gc9107 display-monitor router-display
endef
TARGET_DEVICES += generic-mf68e
+71
View File
@@ -0,0 +1,71 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=router-display
PKG_VERSION:=1.0
PKG_RELEASE:=1
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION)
PKG_QRGEN_NAME:=QR-Code-generator
PKG_QRGEN_VERSION:=v1.8.0
PKG_QRGEN_SOURCE:=$(PKG_QRGEN_VERSION).tar.gz
PKG_QRGEN_URL:=https://github.com/nayuki/QR-Code-generator/archive/refs/tags
PKG_QRGEN_HASH:=skip
include $(INCLUDE_DIR)/package.mk
define Package/router-display
SECTION:=utils
CATEGORY:=Utilities
TITLE:=Framebuffer router status display
DEPENDS:=+libfreetype +dejavu-fonts-ttf-DejaVuSansMono
endef
define Package/router-display/description
Display router status on RGB565 framebuffer
endef
define Download/qrcodegen
URL:=$(PKG_QRGEN_URL)
FILE:=qrcodegen-$(PKG_QRGEN_VERSION).tar.gz
URL_FILE:=$(PKG_QRGEN_VERSION).tar.gz
HASH:=$(PKG_QRGEN_HASH)
endef
define Build/Prepare
mkdir -p $(PKG_BUILD_DIR)
# Copiar nuestro código
$(CP) ./src/main.c $(PKG_BUILD_DIR)/
# Descargar y extraer qrcodegen
$(eval $(call Download,qrcodegen))
tar -xzf $(DL_DIR)/qrcodegen-$(PKG_QRGEN_VERSION).tar.gz -C $(PKG_BUILD_DIR)
# Copiar solo los archivos C necesarios
$(CP) $(PKG_BUILD_DIR)/$(PKG_QRGEN_NAME)-*/c/qrcodegen.c $(PKG_BUILD_DIR)/
$(CP) $(PKG_BUILD_DIR)/$(PKG_QRGEN_NAME)-*/c/qrcodegen.h $(PKG_BUILD_DIR)/
endef
define Build/Compile
$(TARGET_CC) $(TARGET_CFLAGS) $(TARGET_CPPFLAGS) \
-I$(STAGING_DIR)/usr/include/freetype2 \
$(TARGET_LDFLAGS) \
-o $(PKG_BUILD_DIR)/router-display \
$(PKG_BUILD_DIR)/main.c \
$(PKG_BUILD_DIR)/qrcodegen.c \
-lfreetype -lm
endef
define Package/router-display/install
$(INSTALL_DIR) $(1)/usr/bin
$(INSTALL_BIN) $(PKG_BUILD_DIR)/router-display $(1)/usr/bin/
# $(INSTALL_DIR) $(1)/etc/init.d
# $(INSTALL_BIN) ./files/router-display.init $(1)/etc/init.d/router-display
# $(INSTALL_DIR) $(1)/usr/sbin
# $(INSTALL_BIN) ./files/update-display.sh $(1)/usr/sbin/update-display
endef
$(eval $(call BuildPackage,router-display))
+40
View File
@@ -0,0 +1,40 @@
┌────────────────────────────┐
│ MF68E │
│ │
│ ██ ██ ████ ██ ██ │
│ ██ ██ ██ ██ ██ ██ │
│ ██ ██ ██████ ██ ██ │
│ ██ ██████ ██ │
│ ████ ██ ████ │
│ ██ ██ ██ ██ ██ ██ │
│ ██ ██ ████ ██ ██ │
│ │
│ │
│ Orange ES 4G │
│ ██████████████████░░░░ 85% │
└────────────────────────────┘
void draw_display() {
// Hostname derecha
draw_text("MF68E", 120, 2, FONT_10PX, ALIGN_RIGHT);
// QR centrado: (128 - 96) / 2 = 16px offset
draw_qr_code(16, 16, 96); // x=16 para centrar
// Operador + Red
draw_text("Orange ES", 2, 112, FONT_8PX, ALIGN_LEFT);
draw_text("4G", 122, 112, FONT_8PX, ALIGN_RIGHT);
// Batería
char bar[32];
int percent = get_battery_percent();
int filled = 18 * percent / 100;
for(int i = 0; i < filled; i++)
bar[i] = 0x2588;
for(int i = filled; i < 18; i++)
bar[i] = 0x2591;
sprintf(bar + 18, " %d%%", percent);
draw_text(bar, 0, 120, FONT_8PX);
}
+241
View File
@@ -0,0 +1,241 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <getopt.h>
#include <unistd.h>
#include <ft2build.h>
#include FT_FREETYPE_H
#include "qrcodegen.h"
#define WIDTH 128
#define HEIGHT 128
#define RGB565(r,g,b) (((r & 0xF8) << 8) | ((g & 0xFC) << 3) | ((b & 0xF8) >> 3))
#define COLOR_WHITE RGB565(255, 255, 255)
#define COLOR_BLACK RGB565(0, 0, 0)
typedef struct {
uint16_t data[WIDTH * HEIGHT];
} Framebuffer;
typedef struct {
int battery;
char operator[32];
char network_type[8];
char ssid[64];
char password[64];
char hostname[32];
} DisplayConfig;
void fb_init(Framebuffer *fb) {
memset(fb->data, 0, sizeof(fb->data));
}
void fb_put_pixel(Framebuffer *fb, int x, int y, uint16_t color) {
if (x >= 0 && x < WIDTH && y >= 0 && y < HEIGHT) {
fb->data[y * WIDTH + x] = color;
}
}
void fb_draw_rect(Framebuffer *fb, int x, int y, int w, int h, uint16_t color) {
for (int j = 0; j < h; j++) {
for (int i = 0; i < w; i++) {
fb_put_pixel(fb, x + i, y + j, color);
}
}
}
void fb_draw_char(Framebuffer *fb, FT_Face face, char c, int x, int y, int size) {
FT_Set_Pixel_Sizes(face, 0, size);
if (FT_Load_Char(face, c, FT_LOAD_RENDER)) {
return;
}
FT_GlyphSlot slot = face->glyph;
FT_Bitmap bitmap = slot->bitmap;
for (unsigned int row = 0; row < bitmap.rows; row++) {
for (unsigned int col = 0; col < bitmap.width; col++) {
int px = x + slot->bitmap_left + col;
int py = y - slot->bitmap_top + row;
unsigned char gray = bitmap.buffer[row * bitmap.pitch + col];
if (gray > 128) {
fb_put_pixel(fb, px, py, COLOR_WHITE);
}
}
}
}
void fb_draw_text(Framebuffer *fb, FT_Face face, const char *text, int x, int y, int size) {
int pen_x = x;
for (const char *p = text; *p; p++) {
FT_Set_Pixel_Sizes(face, 0, size);
if (FT_Load_Char(face, *p, FT_LOAD_RENDER)) {
continue;
}
FT_GlyphSlot slot = face->glyph;
FT_Bitmap bitmap = slot->bitmap;
for (unsigned int row = 0; row < bitmap.rows; row++) {
for (unsigned int col = 0; col < bitmap.width; col++) {
int px = pen_x + slot->bitmap_left + col;
int py = y - slot->bitmap_top + row;
unsigned char gray = bitmap.buffer[row * bitmap.pitch + col];
if (gray > 128) {
fb_put_pixel(fb, px, py, COLOR_WHITE);
}
}
}
pen_x += slot->advance.x >> 6;
}
}
void fb_draw_qr(Framebuffer *fb, const char *text, int x, int y, int size) {
uint8_t qr_data[qrcodegen_BUFFER_LEN_MAX];
uint8_t temp_buffer[qrcodegen_BUFFER_LEN_MAX];
bool ok = qrcodegen_encodeText(text, temp_buffer, qr_data,
qrcodegen_Ecc_LOW,
qrcodegen_VERSION_MIN,
qrcodegen_VERSION_MAX,
qrcodegen_Mask_AUTO, true);
if (!ok) {
fprintf(stderr, "QR generation failed\n");
return;
}
int qr_size = qrcodegen_getSize(qr_data);
int module_size = size / qr_size;
for (int row = 0; row < qr_size; row++) {
for (int col = 0; col < qr_size; col++) {
if (qrcodegen_getModule(qr_data, col, row)) {
fb_draw_rect(fb,
x + col * module_size,
y + row * module_size,
module_size, module_size,
COLOR_WHITE);
}
}
}
}
void generate_display(Framebuffer *fb, DisplayConfig *cfg, FT_Face face) {
fb_init(fb);
// 1. Hostname arriba derecha
int text_w = strlen(cfg->hostname) * 6;
fb_draw_text(fb, face, cfg->hostname, WIDTH - text_w - 2, 10, 9);
// 2. QR WiFi centrado
char wifi_qr[256];
snprintf(wifi_qr, sizeof(wifi_qr),
"WIFI:T:WPA;S:%s;P:%s;;", cfg->ssid, cfg->password);
fb_draw_qr(fb, wifi_qr, 16, 16, 96);
// 3. Operador + tipo red
fb_draw_text(fb, face, cfg->operator, 2, 118, 8);
int net_w = strlen(cfg->network_type) * 5;
fb_draw_text(fb, face, cfg->network_type, WIDTH - net_w - 2, 118, 8);
// 4. Barra batería
char bar[64];
int bar_chars = 18;
int filled = (bar_chars * cfg->battery) / 100;
for (int i = 0; i < filled; i++) {
bar[i] = 0x2588; // █
}
for (int i = filled; i < bar_chars; i++) {
bar[i] = 0x2591; // ░
}
sprintf(bar + bar_chars, " %d%%", cfg->battery);
fb_draw_text(fb, face, bar, 0, 126, 8);
}
void print_usage(const char *prog) {
fprintf(stderr, "Usage: %s [OPTIONS]\n", prog);
fprintf(stderr, " -b NUM Battery (0-100)\n");
fprintf(stderr, " -n NAME Operator name\n");
fprintf(stderr, " -t TYPE Network type (4G, LTE)\n");
fprintf(stderr, " -s SSID WiFi SSID\n");
fprintf(stderr, " -p PASS WiFi password\n");
fprintf(stderr, " -h HOST Hostname\n");
}
int main(int argc, char *argv[]) {
DisplayConfig cfg = {
.battery = 100,
.operator = "Unknown",
.network_type = "4G",
.ssid = "WiFi",
.password = "password",
.hostname = "Router"
};
int opt;
while ((opt = getopt(argc, argv, "b:n:t:s:p:h:")) != -1) {
switch (opt) {
case 'b': cfg.battery = atoi(optarg); break;
case 'n': strncpy(cfg.operator, optarg, 31); break;
case 't': strncpy(cfg.network_type, optarg, 7); break;
case 's': strncpy(cfg.ssid, optarg, 63); break;
case 'p': strncpy(cfg.password, optarg, 63); break;
case 'h': strncpy(cfg.hostname, optarg, 31); break;
default:
print_usage(argv[0]);
return 1;
}
}
// Init FreeType
FT_Library ft;
if (FT_Init_FreeType(&ft)) {
fprintf(stderr, "FreeType init failed\n");
return 1;
}
FT_Face face;
const char *fonts[] = {
"/usr/share/fonts/dejavu/DejaVuSansMono.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
NULL
};
int found = 0;
for (int i = 0; fonts[i]; i++) {
if (FT_New_Face(ft, fonts[i], 0, &face) == 0) {
found = 1;
break;
}
}
if (!found) {
fprintf(stderr, "No font found\n");
FT_Done_FreeType(ft);
return 1;
}
// Generate
Framebuffer fb;
generate_display(&fb, &cfg, face);
// Output raw RGB565
fwrite(fb.data, sizeof(uint16_t), WIDTH * HEIGHT, stdout);
// Cleanup
FT_Done_Face(face);
FT_Done_FreeType(ft);
return 0;
}
@@ -0,0 +1,13 @@
#!/bin/sh /etc/rc.common
START=99
STOP=10
USE_PROCD=1
start_service() {
procd_open_instance
procd_set_param command /bin/sh -c "while true; do /usr/sbin/update-display; sleep 30; done"
procd_set_param respawn
procd_close_instance
}
@@ -0,0 +1,16 @@
#!/bin/sh
BATTERY=$(cat /sys/class/power_supply/battery/capacity 2>/dev/null || echo 100)
OPERATOR=$(uci get network.modem.operator 2>/dev/null || echo "Unknown")
NETWORK="4G"
SSID=$(uci get wireless.@wifi-iface[0].ssid 2>/dev/null || echo "WiFi")
PASSWORD=$(uci get wireless.@wifi-iface[0].key 2>/dev/null || echo "")
HOSTNAME=$(uci get system.@system[0].hostname 2>/dev/null || echo "Router")
/usr/bin/router-display \
-b "$BATTERY" \
-n "$OPERATOR" \
-t "$NETWORK" \
-s "$SSID" \
-p "$PASSWORD" \
-h "$HOSTNAME" > /dev/fb0