Merge pull request #16 from hkfuertes/mf68e_display_controller_logo

MF68E Display Monitor and Boot Logo
This commit is contained in:
Miguel Fuertes authored and GitHub committed 2025-10-16 21:57:11 +02:00
commit 48ade803a0
18 files changed
+751 -202

No files matched your search

+1
View File
@@ -3,6 +3,7 @@ CONFIG_TARGET_msm89xx_msm8916=y
CONFIG_TARGET_msm89xx_msm8916_DEVICE_generic-mf68e=y
CONFIG_BUSYBOX_CUSTOM=y
CONFIG_BUSYBOX_CONFIG_FBSET=y
CONFIG_BUSYBOX_CONFIG_USLEEP=y
CONFIG_LIBQMI_COLLECTION_BASIC=y
CONFIG_LIBQMI_WITH_MBIM_QMUX=y
CONFIG_LIBQMI_WITH_QRTR_GLIB=y
+2 -1
View File
@@ -44,9 +44,10 @@ define Device/generic-mf68e
DEVICE_VENDOR := Generic
DEVICE_MODEL := MF68E
FILESYSTEMS := squashfs
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
msm-firmware-dumper kmod-fbtft-gc9107 router-display
endef
TARGET_DEVICES += generic-mf68e
@@ -1,6 +1,6 @@
--- /dev/null
+++ b/arch/arm64/boot/dts/qcom/msm8916-generic-mf68e.dts
@@ -0,0 +1,158 @@
@@ -0,0 +1,165 @@
+// SPDX-License-Identifier: GPL-2.0-only
+
+/dts-v1/;
@@ -8,154 +8,161 @@
+#include "msm8916-ufi.dtsi"
+#include <dt-bindings/gpio/gpio.h>
+#include <dt-bindings/pinctrl/qcom,pmic-mpp.h>
+#include <dt-bindings/pwm/pwm.h>
+
+/ {
+ model = "MF68E 4G LTE Mobile WiFi Router";
+ compatible = "generic,mf68e", "qcom,msm8916";
+ model = "MF68E 4G LTE Mobile WiFi Router";
+ compatible = "generic,mf68e", "qcom,msm8916";
+
+ bat: battery {
+ compatible = "simple-battery";
+ voltage-min-design-microvolt = <3400000>;
+ voltage-max-design-microvolt = <4350000>;
+ energy-full-design-microwatt-hours = <11100000>;
+ charge-full-design-microamp-hours = <3000000>;
+ bat: battery {
+ compatible = "simple-battery";
+ voltage-min-design-microvolt = <3400000>;
+ voltage-max-design-microvolt = <4350000>;
+ energy-full-design-microwatt-hours = <11100000>;
+ charge-full-design-microamp-hours = <3000000>;
+
+ ocv-capacity-celsius = <25>;
+ ocv-capacity-table-0 = <4330000 100>, <4265000 95>,
+ <4208000 90>, <4154000 85>, <4102000 80>,
+ <4063000 75>, <4006000 70>, <3972000 65>,
+ <3931000 60>, <3880000 55>, <3841000 50>,
+ <3817000 45>, <3798000 40>, <3783000 35>,
+ <3767000 30>, <3747000 25>, <3729000 20>,
+ <3700000 16>, <3687000 13>, <3683000 11>,
+ <3681000 10>, <3680000 9>, <3678000 8>,
+ <3674000 7>, <3666000 6>, <3641000 5>,
+ <3597000 4>, <3537000 3>, <3457000 2>,
+ <3336000 1>, <3000000 0>;
+ };
+ ocv-capacity-celsius = <25>;
+ ocv-capacity-table-0 = <4330000 100>, <4265000 95>,
+ <4208000 90>, <4154000 85>, <4102000 80>,
+ <4063000 75>, <4006000 70>, <3972000 65>,
+ <3931000 60>, <3880000 55>, <3841000 50>,
+ <3817000 45>, <3798000 40>, <3783000 35>,
+ <3767000 30>, <3747000 25>, <3729000 20>,
+ <3700000 16>, <3687000 13>, <3683000 11>,
+ <3681000 10>, <3680000 9>, <3678000 8>,
+ <3674000 7>, <3666000 6>, <3641000 5>,
+ <3597000 4>, <3537000 3>, <3457000 2>,
+ <3336000 1>, <3000000 0>;
+ };
+
+ usb_id: usb-id {
+ compatible = "linux,extcon-usb-gpio";
+ id-gpios = <&tlmm 110 GPIO_ACTIVE_HIGH>;
+ pinctrl-names = "default";
+ pinctrl-0 = <&usb_id_default>;
+ };
+ usb_id: usb-id {
+ compatible = "linux,extcon-usb-gpio";
+ id-gpios = <&tlmm 110 GPIO_ACTIVE_HIGH>;
+ pinctrl-names = "default";
+ pinctrl-0 = <&usb_id_default>;
+ };
+
+ backlight: backlight {
+ compatible = "pwm-backlight";
+ pwms = <&pm8916_pwm 0 100000>;
+ brightness-levels = <0 4 8 16 32 64 128 255>;
+ num-interpolated-steps = <255>;
+ default-brightness-level = <6>;
+ };
+ backlight: backlight {
+ compatible = "pwm-backlight";
+ pwms = <&pm8916_pwm 0 100000>;
+ brightness-levels = <255 128 64 32 16 8 4 0>;
+ num-interpolated-steps = <255>;
+ default-brightness-level = <6>;
+ status = "okay";
+ };
+};
+
+/* GC9107 Panel on SPI5 (0x78b9000) */
+&blsp_spi5 {
+ status = "okay";
+
+ pinctrl-names = "default", "sleep";
+ pinctrl-0 = <&spi4_default>;
+ pinctrl-1 = <&spi4_sleep>;
+ status = "okay";
+
+ pinctrl-names = "default", "sleep";
+ pinctrl-0 = <&spi5_default>;
+ pinctrl-1 = <&spi5_sleep>;
+
+ panel@0 {
+ compatible = "galaxycore,gc9107";
+ reg = <0>;
+ spi-max-frequency = <16000000>;
+ panel@0 {
+ compatible = "galaxycore,gc9107";
+ reg = <0>;
+ spi-max-frequency = <16000000>;
+
+ reset-gpios = <&tlmm 11 GPIO_ACTIVE_LOW>;
+ dc-gpios = <&tlmm 30 GPIO_ACTIVE_HIGH>;
+ backlight = <&backlight>;
+ reset-gpios = <&tlmm 11 GPIO_ACTIVE_LOW>;
+ dc-gpios = <&tlmm 30 GPIO_ACTIVE_HIGH>;
+ backlight = <&backlight>;
+
+ vdd-supply = <&pm8916_l17>;
+ vddio-supply = <&pm8916_l6>;
+ vdd-supply = <&pm8916_l17>;
+ vddio-supply = <&pm8916_l6>;
+
+ width-mm = <23>;
+ height-mm = <23>;
+ width-mm = <23>;
+ height-mm = <23>;
+
+ buswidth = <8>;
+ width = <128>;
+ height = <128>;
+ rotate = <0>;
+ bgr;
+ fps = <27>;
+ debug = <1>;
+ };
+ buswidth = <8>;
+ width = <128>;
+ height = <128>;
+ rotate = <0>;
+ bgr;
+ fps = <27>;
+ debug = <1>;
+ };
+};
+
+&pm8916_l6 {
+ regulator-always-on;
+ regulator-always-on;
+};
+
+&pm8916_l13 {
+ regulator-boot-on;
+ regulator-allow-set-load;
+};
+
+&pm8916_rpm_regulators {
+ pm8916_l17: l17 {
+ regulator-min-microvolt = <2850000>;
+ regulator-max-microvolt = <2850000>;
+ regulator-always-on;
+ };
+ pm8916_l17: l17 {
+ regulator-min-microvolt = <2850000>;
+ regulator-max-microvolt = <2850000>;
+ regulator-always-on;
+ };
+};
+
+&pm8916_pwm {
+ status = "okay";
+ pinctrl-names = "default";
+ pinctrl-0 = <&pwm_out>;
+ status = "okay";
+ pinctrl-names = "default";
+ pinctrl-0 = <&pwm_out>;
+};
+
+&pm8916_mpps {
+ pwm_out: mpp4-state {
+ pins = "mpp4";
+ function = "digital";
+ output-low;
+ qcom,dtest = <1>;
+ power-source = <PM8916_MPP_VPH>;
+ };
+ pwm_out: mpp4-state {
+ pins = "mpp4";
+ function = "digital";
+ output-low;
+ qcom,dtest = <1>;
+ power-source = <PM8916_MPP_VPH>;
+ };
+};
+
+&pm8916_bms {
+ status = "okay";
+ monitored-battery = <&bat>;
+ power-supplies = <&pm8916_charger>;
+ status = "okay";
+ monitored-battery = <&bat>;
+ power-supplies = <&pm8916_charger>;
+};
+
+&pm8916_charger {
+ status = "okay";
+ monitored-battery = <&bat>;
+ qcom,fast-charge-safe-current = <900000>;
+ qcom,fast-charge-safe-voltage = <4350000>;
+ status = "okay";
+ monitored-battery = <&bat>;
+ qcom,fast-charge-safe-current = <900000>;
+ qcom,fast-charge-safe-voltage = <4350000>;
+};
+
+&usb {
+ status = "okay";
+ extcon = <&usb_id>, <&usb_id>;
+ status = "okay";
+ extcon = <&usb_id>, <&usb_id>;
+};
+
+&usb_hs_phy {
+ extcon = <&usb_id>;
+ extcon = <&usb_id>;
+};
+
+&sdhc_1 {
+ status = "okay";
+ status = "okay";
+};
+
+&tlmm {
+ spi4_default: spi4-default-state {
+ pins = "gpio16", "gpio17", "gpio19";
+ function = "blsp_spi5";
+ drive-strength = <12>;
+ bias-disable;
+ };
+ spi5_default: spi5-default-state {
+ pins = "gpio16", "gpio17", "gpio19";
+ function = "blsp_spi5";
+ drive-strength = <12>;
+ bias-disable;
+ };
+
+ spi4_sleep: spi4-sleep-state {
+ pins = "gpio16", "gpio17", "gpio19";
+ function = "gpio";
+ drive-strength = <2>;
+ bias-pull-down;
+ };
+ spi5_sleep: spi5-sleep-state {
+ pins = "gpio16", "gpio17", "gpio19";
+ function = "gpio";
+ drive-strength = <2>;
+ bias-pull-down;
+ };
+
+ usb_id_default: usb-id-default-state {
+ pins = "gpio110";
+ function = "gpio";
+ drive-strength = <8>;
+ bias-pull-up;
+ };
+ usb_id_default: usb-id-default-state {
+ pins = "gpio110";
+ function = "gpio";
+ drive-strength = <8>;
+ bias-pull-up;
+ };
+};
+17
View File
@@ -27,4 +27,21 @@ uci commit
wifi reload
/etc/init.d/network restart
cat > /etc/rc.button/power << 'EOF'
#!/bin/sh
SECONDS=3
[ "${ACTION}" = "pressed" ] && exit ${SECONDS}
[ "${ACTION}" = "timeout" ] && {
logger "Power held for ${SECONDS}: Shutting down system"
poweroff
exit 0
}
exit 0
EOF
chmod +x /etc/rc.button/power
exit 0
+17 -3
View File
@@ -23,9 +23,13 @@ define KernelPackage/fbtft-gc9107
CONFIG_BACKLIGHT_PWM=y \
CONFIG_FONTS=y \
CONFIG_FONT_8x16=y \
CONFIG_FRAMEBUFFER_CONSOLE=y \
CONFIG_FRAMEBUFFER_CONSOLE_DEFERRED_TAKEOVER=n \
CONFIG_VT_HW_CONSOLE_BINDING=y \
CONFIG_VT_CONSOLE_SLEEP=y \
CONFIG_STAGING=y \
CONFIG_FB_TFT=m \
CONFIG_FB_TFT_GC9107=m
CONFIG_FB_TFT=y \
CONFIG_FB_TFT_GC9107=y
FILES:= \
$(LINUX_DIR)/drivers/staging/fbtft/fbtft.ko \
$(LINUX_DIR)/drivers/staging/fbtft/fb_gc9107.ko
@@ -33,10 +37,20 @@ define KernelPackage/fbtft-gc9107
endef
define KernelPackage/fbtft-gc9107/description
GC9107 framebuffer driver for MF68E device.
GC9107 framebuffer driver for MF68E device with boot logo support.
endef
define Build/Compile
endef
define KernelPackage/fbtft-gc9107/install
$(INSTALL_DIR) $(1)/etc
$(INSTALL_DIR) $(1)/etc/init.d
$(INSTALL_DIR) $(1)/lib/preinit
$(INSTALL_DATA) ./files/boot_logo.fb $(1)/etc/boot_logo.fb
$(INSTALL_DATA) ./files/poweroff_logo.fb $(1)/etc/poweroff_logo.fb
$(INSTALL_BIN) ./files/05_display_logo $(1)/lib/preinit/05_display_logo
$(INSTALL_BIN) ./files/01_poweroff_logo $(1)/etc/init.d/01_poweroff_logo
endef
$(eval $(call KernelPackage,fbtft-gc9107))
@@ -0,0 +1,13 @@
#!/bin/sh /etc/rc.common
START=99
STOP=01
start() {
return 0
}
stop() {
cat /etc/poweroff_logo.fb > /dev/fb0 2>/dev/null
sleep 2
}
@@ -0,0 +1,30 @@
#!/bin/sh
show_logo() {
(
local BACKLIGHT="/sys/class/backlight/backlight"
local i=0
while [ ! -c /dev/fb0 ] && [ $i -lt 100 ]; do
usleep 100000 # 100ms = 0.1s
i=$((i + 1))
done
if [ -c /dev/fb0 ] && [ -f /etc/boot_logo.fb ]; then
cat /etc/boot_logo.fb > /dev/fb0 2>/dev/null
fi
i=0
while [ ! -d "$BACKLIGHT" ] && [ $i -lt 50 ]; do
usleep 100000 # 100ms = 0.1s
i=$((i + 1))
done
if [ -d "$BACKLIGHT" ]; then
echo 0 > $BACKLIGHT/bl_power 2>/dev/null
echo 1 > $BACKLIGHT/brightness 2>/dev/null
fi
) &
}
boot_hook_add preinit_main show_logo
Binary file not shown.
Binary file not shown.
+5
View File
@@ -0,0 +1,5 @@
## Create boot image for framebuffer
We are converting the image using `ffmpeg`. Ensure you have it installed.
```
ffmpeg -i <image> -vf "scale=128:128" -pix_fmt rgb565le -f rawvideo <output>.fb
```
-35
View File
@@ -1,35 +0,0 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=power-button-daemon
PKG_VERSION:=1.0
PKG_RELEASE:=1
include $(INCLUDE_DIR)/package.mk
define Package/$(PKG_NAME)
SECTION:=utils
CATEGORY:=Utilities
TITLE:=Power button monitor daemon
DEPENDS:=+evtest
endef
define Package/$(PKG_NAME)/description
Monitors power button and triggers poweroff on long press (3s).
Designed for MSM8916-based devices with PM8916 PON.
endef
define Build/Configure
endef
define Build/Compile
endef
define Package/$(PKG_NAME)/install
$(INSTALL_DIR) $(1)/usr/sbin
$(INSTALL_BIN) ./files/power_button_monitor.sh $(1)/usr/sbin/power_button_monitor
$(INSTALL_DIR) $(1)/etc/init.d
$(INSTALL_BIN) ./files/power_button.init $(1)/etc/init.d/power_button
endef
$(eval $(call BuildPackage,$(PKG_NAME)))
@@ -1,15 +0,0 @@
#!/bin/sh /etc/rc.common
START=99
STOP=10
USE_PROCD=1
start_service() {
procd_open_instance
procd_set_param command /usr/sbin/power_button_monitor
procd_set_param respawn
procd_set_param stdout 1
procd_set_param stderr 1
procd_close_instance
}
@@ -1,44 +0,0 @@
#!/bin/sh
DEVICE="/dev/input/event0"
LONG_PRESS_TIME=3
echo "Power button monitor started - Hold ${LONG_PRESS_TIME}s to shutdown"
logger -t pwrkey "Daemon started"
# Monitor button events and check duration in real-time
evtest "$DEVICE" 2>/dev/null | while read -r line; do
# Button pressed - start background timer
if echo "$line" | grep -q "code 116.*value 1"; then
PRESS_START=$(date +%s)
logger -t pwrkey "Button pressed"
# Background timer to trigger poweroff
(
sleep "$LONG_PRESS_TIME"
# Check if button is still pressed (no release event yet)
echo "Long press ${LONG_PRESS_TIME}s reached - Shutting down!"
logger -t pwrkey "Long press ${LONG_PRESS_TIME}s: poweroff triggered"
poweroff
) &
TIMER_PID=$!
fi
# Button released - cancel timer
if echo "$line" | grep -q "code 116.*value 0"; then
if [ -n "$TIMER_PID" ]; then
# Kill the background timer if button released early
kill "$TIMER_PID" 2>/dev/null
CURRENT_TIME=$(date +%s)
PRESS_DURATION=$((CURRENT_TIME - PRESS_START))
if [ "$PRESS_DURATION" -lt "$LONG_PRESS_TIME" ]; then
echo "Button released after ${PRESS_DURATION}s - Ignored"
logger -t pwrkey "Short press ${PRESS_DURATION}s - Ignored"
fi
TIMER_PID=""
fi
fi
done
+67
View File
@@ -0,0 +1,67 @@
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 +kmod-button-hotplug +dejavu-fonts-ttf-DejaVuSans-Bold +dejavu-fonts-ttf-DejaVuSans
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)
$(CP) ./src/main.c $(PKG_BUILD_DIR)/
$(eval $(call Download,qrcodegen))
tar -xzf $(DL_DIR)/qrcodegen-$(PKG_QRGEN_VERSION).tar.gz -C $(PKG_BUILD_DIR)
$(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) -O3 -ffast-math \
$(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)/usr/sbin
$(INSTALL_BIN) ./files/update-display.sh $(1)/usr/sbin/update-display
$(INSTALL_DIR) $(1)/etc/hotplug.d/button
$(INSTALL_BIN) ./files/hotplugs/20_power_dimm $(1)/etc/hotplug.d/button/20_power_dimm
$(INSTALL_BIN) ./files/hotplugs/30_power_update $(1)/etc/hotplug.d/button/30_power_update
endef
$(eval $(call BuildPackage,router-display))
@@ -0,0 +1,91 @@
#!/bin/sh
# /etc/hotplug.d/button/20_power_dimm
# Only process power button events
[ "$BUTTON" = "power" ] || exit 0
# Configuration variables
TIMEOUT_DIM=5
TIMEOUT_OFF=3
BACKLIGHT_PATH="/sys/class/backlight/backlight"
BRIGHTNESS_DIM_DIVISOR=8
BRIGHTNESS_OFF=1
# Calculated variables
MAX_BRIGHTNESS=$(cat $BACKLIGHT_PATH/max_brightness 2>/dev/null || echo 1785)
BRIGHTNESS_FULL=$MAX_BRIGHTNESS
BRIGHTNESS_DIM=$((MAX_BRIGHTNESS / BRIGHTNESS_DIM_DIVISOR))
# PID file for timer management
TIMER_PID_FILE="/tmp/screen_timer.pid"
maybe_set_lockscreen(){
# Check if system is shutting down
if pgrep poweroff >/dev/null 2>&1 || \
pgrep halt >/dev/null 2>&1 || \
pgrep reboot >/dev/null 2>&1; then
logger -t display "System shutting down - skipping lockscreen"
return 0
fi
# Show lockscreen if brightness is OFF
[ -n "$1" ] && [ "$1" = "$BRIGHTNESS_OFF" ] && [ -f /etc/boot_logo.fb ] && \
cat /etc/boot_logo.fb > /dev/fb0 2>/dev/null
}
# Set brightness level
set_brightness() {
echo "$1" > "$BACKLIGHT_PATH/brightness"
# Check if we should show lockscreen
maybe_set_lockscreen "$1"
}
# Function to start a timer
# Parameters: $1=timeout (seconds), $2=brightness level, $3=log message
start_timer() {
local timeout=$1
local brightness=$2
local log_msg=$3
(
sleep "$timeout"
logger -t display "$log_msg"
set_brightness "$brightness"
) >/dev/null 2>&1 </dev/null &
echo $!
}
# Cancel all active timers
cancel_timers() {
if [ -f "$TIMER_PID_FILE" ]; then
while read pid; do
kill "$pid" 2>/dev/null
done < "$TIMER_PID_FILE"
rm -f "$TIMER_PID_FILE"
fi
}
# Only act on button release
[ "${ACTION}" = "released" ] && {
logger -t display "Button event detected"
# Cancel previous timers if any
cancel_timers
# Set full brightness immediately
logger -t display "Backlight: FULL"
set_brightness $BRIGHTNESS_FULL
# Start timer 1: DIM after TIMEOUT_DIM seconds
pid1=$(start_timer $TIMEOUT_DIM $BRIGHTNESS_DIM "Backlight: DIM")
echo "$pid1" >> "$TIMER_PID_FILE"
# Start timer 2: OFF after (TIMEOUT_DIM + TIMEOUT_OFF) seconds
TOTAL_TIMEOUT=$((TIMEOUT_DIM + TIMEOUT_OFF))
pid2=$(start_timer $TOTAL_TIMEOUT $BRIGHTNESS_OFF "Backlight: OFF")
echo "$pid2" >> "$TIMER_PID_FILE"
}
exit 0
@@ -0,0 +1,20 @@
#!/bin/sh
# /etc/hotplug.d/button/30_power_update
# Only process power button events
[ "$BUTTON" = "power" ] || exit 0
# Execute update-display on button release
[ "${ACTION}" = "released" ] && {
# Check if system is shutting down
if pgrep poweroff >/dev/null 2>&1 || \
pgrep halt >/dev/null 2>&1 || \
pgrep reboot >/dev/null 2>&1; then
logger -t display "System shutting down - skipping update-display"
exit 0
fi
/usr/sbin/update-display
}
exit 0
@@ -0,0 +1,85 @@
#!/bin/sh
# /usr/sbin/update-display
# Optimized display update script - caches operator name (requires reboot to change SIM)
# === BATTERY ===
BMS_PATH="/sys/class/power_supply/pm8916-bms-vm"
VOLTAGE=$(cat "$BMS_PATH/voltage_now" 2>/dev/null)
VMAX=$(cat "$BMS_PATH/voltage_max_design" 2>/dev/null)
VMIN=$(cat "$BMS_PATH/voltage_min_design" 2>/dev/null)
if [ -n "$VOLTAGE" ] && [ -n "$VMAX" ] && [ -n "$VMIN" ]; then
BATTERY=$(awk "BEGIN {pct = (($VOLTAGE - $VMIN) / ($VMAX - $VMIN)) * 100; if(pct < 0) pct=0; if(pct > 100) pct=100; printf \"%.0f\", pct}")
else
BATTERY=100
fi
# === WiFi AP ===
SHOW_QR=0
if ip link show phy0-ap0 2>/dev/null | grep -q "state UP"; then
SHOW_QR=1
fi
# === MODEM (OPTIMIZED - single mmcli call + operator cache) ===
OPERATOR_CACHE="/tmp/modem_operator.cache"
MODEM_IDX=$(mmcli -L 2>/dev/null | grep -oE 'Modem/[0-9]+' | head -1 | cut -d'/' -f2)
if [ -n "$MODEM_IDX" ]; then
# Single mmcli call - cache all modem data
MODEM_DATA=$(mmcli -m "$MODEM_IDX" -K 2>/dev/null)
# Try to get operator from cache first (SIM doesn't change without reboot)
if [ -f "$OPERATOR_CACHE" ]; then
OPERATOR=$(cat "$OPERATOR_CACHE")
else
# Cache miss - fetch operator from SIM
SIM_IDX=$(echo "$MODEM_DATA" | grep 'modem.generic.sim' | awk -F': ' '{print $2}' | grep -oE '[0-9]+$')
if [ -n "$SIM_IDX" ]; then
OPERATOR=$(mmcli -i "$SIM_IDX" -K 2>/dev/null | grep 'sim.properties.operator-name' | awk -F': ' '{print $2}')
fi
# Fallback: operator from network (use cached modem data)
if [ -z "$OPERATOR" ]; then
OPERATOR=$(echo "$MODEM_DATA" | grep 'modem.3gpp.operator-name' | awk -F': ' '{print $2}')
fi
# Cache operator for future calls
if [ -n "$OPERATOR" ]; then
echo "$OPERATOR" > "$OPERATOR_CACHE"
fi
fi
# Access technology (use cached modem data)
ACCESS_TECH=$(echo "$MODEM_DATA" | grep 'modem.generic.access-technologies' | awk -F': ' '{print $2}')
# Determine network type only if connected
if [ -n "$ACCESS_TECH" ] && [ "$ACCESS_TECH" != "unknown" ]; then
case "$ACCESS_TECH" in
*lte*) NETWORK="4G" ;;
*umts*|*hspa*|*hsupa*|*hsdpa*) NETWORK="3G" ;;
*edge*|*gprs*|*gsm*) NETWORK="2G" ;;
*) NETWORK="$ACCESS_TECH" ;;
esac
else
# No connection: show operator but no network type
NETWORK=""
fi
else
# No modem
OPERATOR=""
NETWORK=""
fi
# === WiFi Info ===
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")
# === Update Display ===
if [ "$SHOW_QR" -eq 1 ]; then
/usr/bin/router-display -q -b "$BATTERY" -n "$OPERATOR" -t "$NETWORK" -s "$SSID" -p "$PASSWORD" -h "$HOSTNAME" > /dev/fb0
else
/usr/bin/router-display -b "$BATTERY" -n "$OPERATOR" -t "$NETWORK" -s "$SSID" -p "$PASSWORD" -h "$HOSTNAME" > /dev/fb0
fi
+292
View File
@@ -0,0 +1,292 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <getopt.h>
#include <unistd.h>
#include <ctype.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)
#define QR_SIZE 108
#define QR_TOP_MARGIN 6
#define TEXT_HEIGHT 13
#define MIN_MODULE_SIZE 3 // Minimum pixels per QR module for readability
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];
int show_qr;
int uppercase;
} 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_blend_pixel(Framebuffer *fb, int x, int y, unsigned char gray) {
if (x < 0 || x >= WIDTH || y < 0 || y >= HEIGHT) return;
if (gray == 0) return;
uint16_t bg_color = fb->data[y * WIDTH + x];
uint8_t bg_r = (bg_color >> 11) << 3;
uint8_t bg_g = ((bg_color >> 5) & 0x3F) << 2;
uint8_t bg_b = (bg_color & 0x1F) << 3;
float alpha = gray / 255.0f;
uint8_t r = (uint8_t)(255 * alpha + bg_r * (1.0f - alpha));
uint8_t g = (uint8_t)(255 * alpha + bg_g * (1.0f - alpha));
uint8_t b = (uint8_t)(255 * alpha + bg_b * (1.0f - alpha));
fb->data[y * WIDTH + x] = RGB565(r, g, b);
}
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_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 | FT_LOAD_TARGET_NORMAL)) {
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 > 0) {
fb_blend_pixel(fb, px, py, gray);
}
}
}
pen_x += slot->advance.x >> 6;
}
}
int fb_get_text_width(FT_Face face, const char *text, int size) {
int width = 0;
FT_Set_Pixel_Sizes(face, 0, size);
for (const char *p = text; *p; p++) {
if (FT_Load_Char(face, *p, FT_LOAD_RENDER)) {
continue;
}
width += face->glyph->advance.x >> 6;
}
return width;
}
int 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 0;
}
int qr_modules = qrcodegen_getSize(qr_data);
int module_size = size / qr_modules;
// Check if QR modules are too small (minimum pixels per module)
if (module_size < MIN_MODULE_SIZE) {
fprintf(stderr, "QR too large for display area (needs %d modules, only fits %dpx modules)\n",
qr_modules, module_size);
return 0; // Don't draw, QR would be unreadable
}
// Calculate actual QR size and center horizontally, align top vertically
int actual_qr_size = qr_modules * module_size;
int offset_x = (size - actual_qr_size) / 2; // Center horizontally
int offset_y = 0; // Top aligned
for (int row = 0; row < qr_modules; row++) {
for (int col = 0; col < qr_modules; col++) {
if (qrcodegen_getModule(qr_data, col, row)) {
fb_draw_rect(fb,
x + offset_x + col * module_size,
y + offset_y + row * module_size,
module_size, module_size,
COLOR_WHITE);
}
}
}
return 1; // QR drawn successfully
}
void str_to_upper(char *str) {
for (char *p = str; *p; p++) {
*p = toupper((unsigned char)*p);
}
}
void generate_display(Framebuffer *fb, DisplayConfig *cfg, FT_Face face) {
fb_init(fb);
char operator_text[32], network_text[8], hostname_text[32];
strncpy(operator_text, cfg->operator, 31);
strncpy(network_text, cfg->network_type, 7);
strncpy(hostname_text, cfg->hostname, 31);
if (cfg->uppercase) {
str_to_upper(operator_text);
str_to_upper(network_text);
str_to_upper(hostname_text);
}
// Calculate QR position: centered horizontally, at top with margin
int qr_x = (WIDTH - QR_SIZE) / 2;
int qr_y = QR_TOP_MARGIN;
if (cfg->show_qr) {
char wifi_qr[256];
snprintf(wifi_qr, sizeof(wifi_qr),
"WIFI:T:WPA;S:%s;P:%s;;", cfg->ssid, cfg->password);
// Try to draw QR, if it doesn't fit (returns 0), screen stays black
fb_draw_qr(fb, wifi_qr, qr_x, qr_y, QR_SIZE);
}
// If no QR or QR too large: stay black (fb_init already cleared to black)
int fontsz = 12;
int line1_y = HEIGHT - fontsz - 3;
int line2_y = HEIGHT - 3;
fb_draw_text(fb, face, operator_text, 2, line1_y, fontsz);
int net_width = fb_get_text_width(face, network_text, fontsz);
fb_draw_text(fb, face, network_text, WIDTH - net_width - 2, line1_y, fontsz);
fb_draw_text(fb, face, hostname_text, 2, line2_y, fontsz);
char battery_text[8];
snprintf(battery_text, sizeof(battery_text), "%d%%", cfg->battery);
int battery_width = fb_get_text_width(face, battery_text, fontsz);
fb_draw_text(fb, face, battery_text, WIDTH - battery_width - 2, line2_y, fontsz);
}
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");
fprintf(stderr, " -q Show QR code (default: show logo)\n");
fprintf(stderr, " -c Convert text to UPPERCASE\n");
}
int main(int argc, char *argv[]) {
DisplayConfig cfg = {
.battery = 100,
.operator = "Unknown",
.network_type = "4G",
.ssid = "WiFi",
.password = "password",
.hostname = "Router",
.show_qr = 0,
.uppercase = 0
};
int opt;
while ((opt = getopt(argc, argv, "b:n:t:s:p:h:qc")) != -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;
case 'q': cfg.show_qr = 1; break;
case 'c': cfg.uppercase = 1; break;
default:
print_usage(argv[0]);
return 1;
}
}
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/ttf-dejavu/DejaVuSans.ttf",
"/usr/share/fonts/ttf-dejavu/DejaVuSans-Bold.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;
}
Framebuffer fb;
generate_display(&fb, &cfg, face);
fwrite(fb.data, sizeof(uint16_t), WIDTH * HEIGHT, stdout);
FT_Done_Face(face);
FT_Done_FreeType(ft);
return 0;
}