From bb778530ec56e6c7bb6b3dfcbe97ff0d8a4e4ae6 Mon Sep 17 00:00:00 2001 From: Preston Brown Date: Wed, 15 Apr 2026 16:02:59 -0400 Subject: [PATCH 1/4] meta-opencentauri: flesh out helixscreen recipe + add libhv support Replaces the 9-line helixscreen stub with a full recipe that builds, links against system libraries (libhv, openssl, spdlog, fmt, alsa-lib, libusb1, wpa-supplicant, libnl), and installs a runnable .ipk with SysV init hook. Recipe files: - recipes-support/libhv/libhv_1.3.4.bb cmake-inherit recipe for ithewei/libhv tagged release. Applies a dns-resolver fallback patch helixscreen requires (provides dns_resolv_resolve symbol for statically-linked edge cases). SOVERSION fixup in do_install:append renames the unversioned libhv.so so it lands in the runtime package with proper symlinks. - recipes-connectivity/wpa-supplicant/wpa-supplicant_%.bbappend Clears DISABLE_STATIC so upstream wpa-supplicant_2.10.bb actually builds libwpa_client.a. Without this, any screen UI that links wpa_ctrl (helixscreen, guppyscreen) fails at link time with 'cannot find -lwpa_client'. Only changes one variable, no other recipe behavior touched. - recipes-apps/helixscreen/helixscreen_0.99.bb (replaces _0.1.bb) DEPENDS on system libs + EXTRA_OEMAKE wires PLATFORM_TARGET=yocto into helixscreen's Makefile. do_install places the binary + supervisor helpers + runtime assets under /usr/share/helixscreen/, with /usr/bin/helix-screen symlink and /etc/init.d/helixscreen hooked via update-rc.d (S80/S20). Matches the sibling-recipe convention used by guppyscreen/grumpyscreen. - recipes-apps/helixscreen/files/helixscreen.init SysV init script from helixscreen's tree with HELIX_CONFIG_DIR export added up-front so user settings persist into klipper's ~/printer_data/config/helixscreen regardless of the RO squashfs install tree. DAEMON_DIR is sed-rewritten to the packaged path at install time. Tested: bitbake helixscreen produces a clean .ipk (~93 MB) for cortexa7t2hf-neon-vfpv4 with GNU_HASH present and split-debug extraction working. Recipe CFLAGS/LDFLAGS flow through from bitbake into the Makefile's YOCTO_BUILD path, which requires helixscreen main at ea728a294 or newer. Refs #145. --- .../helixscreen/files/helixscreen.init | 277 +++++++++++++++ .../helixscreen/helixscreen_0.1.bb | 12 - .../helixscreen/helixscreen_0.99.bb | 126 +++++++ .../wpa-supplicant/wpa-supplicant_%.bbappend | 6 + .../files/libhv-dns-resolver-fallback.patch | 327 ++++++++++++++++++ .../recipes-support/libhv/libhv_1.3.4.bb | 53 +++ 6 files changed, 789 insertions(+), 12 deletions(-) create mode 100755 meta-opencentauri/recipes-apps/helixscreen/files/helixscreen.init delete mode 100644 meta-opencentauri/recipes-apps/helixscreen/helixscreen_0.1.bb create mode 100644 meta-opencentauri/recipes-apps/helixscreen/helixscreen_0.99.bb create mode 100644 meta-opencentauri/recipes-connectivity/wpa-supplicant/wpa-supplicant_%.bbappend create mode 100644 meta-opencentauri/recipes-support/libhv/files/libhv-dns-resolver-fallback.patch create mode 100644 meta-opencentauri/recipes-support/libhv/libhv_1.3.4.bb diff --git a/meta-opencentauri/recipes-apps/helixscreen/files/helixscreen.init b/meta-opencentauri/recipes-apps/helixscreen/files/helixscreen.init new file mode 100755 index 00000000..da4d05e3 --- /dev/null +++ b/meta-opencentauri/recipes-apps/helixscreen/files/helixscreen.init @@ -0,0 +1,277 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-3.0-or-later +# +# SysV init script for HelixScreen +# For use on systems without systemd (e.g., FlashForge AD5M with BusyBox init) +# +# Installation varies by firmware: +# Forge-X: /etc/init.d/S90helixscreen -> /opt/helixscreen +# Klipper Mod: /etc/init.d/S80helixscreen -> /opt/helixscreen (v00.06+) +# or /root/printer_software/helixscreen (v00.05) +# +# The install.sh script automatically selects the correct paths. +# DAEMON_DIR below is updated by the installer to match the install location. +# +# Usage: +# /etc/init.d/S*helixscreen start|stop|restart|status +# +# Note: S80/S90 prefix ensures we start after network and Moonraker + +NAME="helixscreen" +DESC="HelixScreen 3D Printer Touch UI" +DAEMON_DIR="/opt/helixscreen" # Updated by installer for different firmware +DAEMON="${DAEMON_DIR}/bin/helix-screen" +SPLASH="${DAEMON_DIR}/bin/helix-splash" +WATCHDOG="${DAEMON_DIR}/bin/helix-watchdog" +LAUNCHER="${DAEMON_DIR}/bin/helix-launcher.sh" +PIDFILE="/var/run/${NAME}.pid" +LOGFILE="/tmp/helixscreen.log" + +# Override asset directory if ui_xml/assets/config are not under DAEMON_DIR +# Export so helix-screen picks it up at startup +# HELIX_DATA_DIR="/mnt/data/helixscreen" +# export HELIX_DATA_DIR + +# Ensure HOME is set (BusyBox init may not set it). +# Without HOME, config backup falls back to volatile /tmp/ and is lost on reboot. +: "${HOME:=/root}" +export HOME + +# Point user-writable settings at klipper's printer_data so they persist even +# when the baseline install lives on a read-only rootfs (e.g., the cosmos +# squashfs). Honored by helix::Config::init in src/system/config.cpp. +: "${HELIX_CONFIG_DIR:=${HOME}/printer_data/config/helixscreen}" +export HELIX_CONFIG_DIR + +# Ensure config backup directory exists (survives Moonraker's shutil.rmtree wipe). +# On systemd systems StateDirectory= creates /var/lib/helixscreen/; on SysV we do it here. +mkdir -p /var/lib/helixscreen 2>/dev/null || mkdir -p "${HOME}/.helixscreen" 2>/dev/null || true + +# Check if binary exists +[ -x "$DAEMON" ] || exit 0 + +# Platform hook defaults (no-ops). Platform-specific hook files override these. +platform_stop_competing_uis() { :; } +platform_enable_backlight() { :; } +platform_wait_for_services() { :; } +platform_pre_start() { :; } +platform_post_stop() { :; } +platform_wait_for_boot_complete() { :; } + +# Source platform hooks if available +PLATFORM_HOOKS="${DAEMON_DIR}/platform/hooks.sh" +if [ -f "$PLATFORM_HOOKS" ]; then + # shellcheck disable=SC1090 # path depends on DAEMON_DIR set by installer + . "$PLATFORM_HOOKS" +fi + +# Stop splash screen +stop_splash() { + if [ -f /var/run/helix-splash.pid ]; then + SPLASH_PID=$(cat /var/run/helix-splash.pid) + kill "$SPLASH_PID" 2>/dev/null || true + rm -f /var/run/helix-splash.pid + fi +} + +start() { + # Ensure valid CWD before anything else. + # The installer or boot system may leave us in a deleted/unmounted directory, + # which causes "getcwd: cannot access parent directories" errors from sh and chroot. + cd / 2>/dev/null || true + + echo "Starting $DESC..." + + # Platform-specific pre-start (e.g., flag files for firmware coordination) + platform_pre_start + + # Stop any competing UIs first + platform_stop_competing_uis + + # Enable backlight (some firmware may leave it off) + platform_enable_backlight + + # Check if already running. The PIDFILE may store a recycled PID after a + # reboot or crash — confirm by looking for the actual helix-screen process + # before trusting it. Otherwise a stale PID owned by an unrelated process + # makes us return "already running" while no UI is actually up. + DAEMON_NAME=$(basename "$DAEMON") + if [ -f "$PIDFILE" ]; then + PID=$(cat "$PIDFILE") + if kill -0 "$PID" 2>/dev/null && pidof "$DAEMON_NAME" >/dev/null 2>&1; then + echo "$NAME is already running (PID $PID)" + return 0 + fi + rm -f "$PIDFILE" + fi + # Catch the no-PIDFILE-but-running case (e.g., started outside this script) + if pidof "$DAEMON_NAME" >/dev/null 2>&1; then + RUNNING_PID=$(pidof "$DAEMON_NAME" | awk '{print $1}') + echo "$NAME is already running (PID $RUNNING_PID, no PID file)" + return 0 + fi + + cd "$DAEMON_DIR" || exit 1 + + # Start splash screen early (before launcher) so it's visible during load + # Set HELIX_NO_SPLASH=1 to disable splash (for debugging backlight issues) + # Splash auto-detects resolution from display hardware + EARLY_SPLASH_PID="" + if [ -x "$SPLASH" ] && [ "${HELIX_NO_SPLASH:-0}" != "1" ]; then + echo "Starting splash screen..." + "$SPLASH" >/dev/null 2>&1 & + EARLY_SPLASH_PID=$! + echo "$EARLY_SPLASH_PID" > /var/run/helix-splash.pid + elif [ "${HELIX_NO_SPLASH:-0}" = "1" ]; then + echo "Splash screen disabled (HELIX_NO_SPLASH=1)" + fi + + # Fork the service wait + helix-screen launch into background + # This lets the init script exit so later scripts can run (e.g., S99 starts moonraker) + # Without this, we'd deadlock: S90 waits for services, S99 starts them but can't run until S90 finishes + ( + # Wait for required services before starting the main UI + # Platform hooks control what (if anything) to wait for + if [ "${HELIX_NO_SERVICE_WAIT:-0}" != "1" ]; then + platform_wait_for_services + fi + + # Wait for platform boot sequence to finish (e.g., ForgeX S99root) + # Ensures no other process is writing to the framebuffer when helix-screen starts + if [ "${HELIX_NO_BOOT_WAIT:-0}" != "1" ]; then + platform_wait_for_boot_complete + fi + + # Use launcher script if available (handles watchdog) + # Pass early splash PID so watchdog doesn't start another splash + if [ -x "$LAUNCHER" ]; then + if [ -n "$EARLY_SPLASH_PID" ]; then + export HELIX_SPLASH_PID="$EARLY_SPLASH_PID" + fi + cd "$DAEMON_DIR" && exec $LAUNCHER + else + # Fallback: start daemon directly + cd "$DAEMON_DIR" && exec "$DAEMON" --splash-pid="$EARLY_SPLASH_PID" + fi + ) >> "$LOGFILE" 2>&1 & + echo $! > "$PIDFILE" + + # Wait briefly and check if started + sleep 2 + if [ -f "$PIDFILE" ]; then + PID=$(cat "$PIDFILE") + if kill -0 "$PID" 2>/dev/null; then + echo "$NAME started (PID $PID)" + return 0 + fi + fi + + echo "Failed to start $NAME" + stop_splash + return 1 +} + +stop() { + echo "Stopping $DESC..." + + # Stop splash first + stop_splash + + if [ -f "$PIDFILE" ]; then + PID=$(cat "$PIDFILE") + if kill -0 "$PID" 2>/dev/null; then + kill "$PID" + # Wait for process to exit + for _ in 1 2 3 4 5; do + if ! kill -0 "$PID" 2>/dev/null; then + break + fi + sleep 1 + done + # Force kill if still running + if kill -0 "$PID" 2>/dev/null; then + kill -9 "$PID" 2>/dev/null || true + fi + fi + rm -f "$PIDFILE" + fi + + # Also kill by name in case PID file is stale or the launcher died before + # signaling its children. Kill watchdog first to prevent crash dialog flash + # during clean shutdown. SIGTERM then SIGKILL: helix-watchdog and helix-screen + # may catch SIGTERM without exiting (splash handoff, blocked I/O). + WATCHDOG_NAME=$(basename "$WATCHDOG") + DAEMON_NAME=$(basename "$DAEMON") + SPLASH_NAME=$(basename "$SPLASH") + for proc in "$WATCHDOG_NAME" "$DAEMON_NAME" "$SPLASH_NAME"; do + pids=$(pidof "$proc" 2>/dev/null || true) + [ -n "$pids" ] || continue + # shellcheck disable=SC2086 + kill $pids 2>/dev/null || true + done + # Give caught-SIGTERM handlers a moment, then SIGKILL any survivors. + sleep 1 + for proc in "$WATCHDOG_NAME" "$DAEMON_NAME" "$SPLASH_NAME"; do + pids=$(pidof "$proc" 2>/dev/null || true) + [ -n "$pids" ] || continue + # shellcheck disable=SC2086 + kill -9 $pids 2>/dev/null || true + done + + # Platform-specific post-stop cleanup (e.g., remove flag files) + # Runs after daemon is fully stopped so platform can safely reclaim resources + platform_post_stop + + echo "$NAME stopped" +} + +restart() { + stop + sleep 1 + start +} + +status() { + if [ -f "$PIDFILE" ]; then + PID=$(cat "$PIDFILE") + if kill -0 "$PID" 2>/dev/null; then + echo "$NAME is running (PID $PID)" + return 0 + fi + echo "$NAME is not running (stale PID file)" + return 1 + fi + + # Check if running without PID file + if command -v pidof >/dev/null 2>&1; then + PID=$(pidof "$(basename "$DAEMON")" 2>/dev/null) + if [ -n "$PID" ]; then + echo "$NAME is running (PID $PID) [no PID file]" + return 0 + fi + fi + + echo "$NAME is not running" + return 1 +} + +case "$1" in + start) + start + ;; + stop) + stop + ;; + restart|reload) + restart + ;; + status) + status + ;; + *) + echo "Usage: $0 {start|stop|restart|status}" + exit 1 + ;; +esac + +exit $? diff --git a/meta-opencentauri/recipes-apps/helixscreen/helixscreen_0.1.bb b/meta-opencentauri/recipes-apps/helixscreen/helixscreen_0.1.bb deleted file mode 100644 index 80d4b92d..00000000 --- a/meta-opencentauri/recipes-apps/helixscreen/helixscreen_0.1.bb +++ /dev/null @@ -1,12 +0,0 @@ -SUMMARY = "HelixScreen - Interface for Klipper" -DESCRIPTION = "A modern, lightweight touchscreen interface for Klipper 3D printers" -HOMEPAGE = "https://github.com/prestonbrown/helixscreen" -LICENSE = "GPL-3.0-only" -LIC_FILES_CHKSUM = "file://LICENSE;md5=1ebbd3e34237af26da5dc08a4e440464" - -SRC_URI = "gitsm://github.com/prestonbrown/helixscreen.git;protocol=https;branch=main" -SRCREV = "3aa8d5f6d904e7e66bfd0d647802a85f49317e02" - -inherit pkgconfig - -S = "${WORKDIR}/git" diff --git a/meta-opencentauri/recipes-apps/helixscreen/helixscreen_0.99.bb b/meta-opencentauri/recipes-apps/helixscreen/helixscreen_0.99.bb new file mode 100644 index 00000000..f09f82cc --- /dev/null +++ b/meta-opencentauri/recipes-apps/helixscreen/helixscreen_0.99.bb @@ -0,0 +1,126 @@ +SUMMARY = "HelixScreen - Interface for Klipper" +DESCRIPTION = "A modern, lightweight touchscreen interface for Klipper 3D printers" +HOMEPAGE = "https://github.com/prestonbrown/helixscreen" +LICENSE = "GPL-3.0-only" +LIC_FILES_CHKSUM = "file://LICENSE;md5=1ebbd3e34237af26da5dc08a4e440464" + +FILESEXTRAPATHS:prepend := "${THISDIR}/files:" + +SRC_URI = "gitsm://github.com/prestonbrown/helixscreen.git;protocol=https;branch=main \ + file://helixscreen.init \ +" +# Main-branch commit that carries PLATFORM_TARGET=yocto support (Makefile +# YOCTO_BUILD mode, splash/watchdog LDFLAGS preservation, Config::init +# HELIX_CONFIG_DIR override, docker-based dev loop). +SRCREV = "cde268af7a8ea1af4ebab9cad0a57b7ef31a87a1" + +S = "${WORKDIR}/git" + +inherit pkgconfig update-rc.d + +DEPENDS = " \ + libhv \ + openssl \ + spdlog \ + fmt \ + alsa-lib \ + libusb1 \ + wpa-supplicant \ + libnl \ + zlib \ +" + +# HelixScreen has a PLATFORM_TARGET=yocto mode in its Makefile that honors +# the bitbake-provided toolchain + CFLAGS env and skips all in-tree submodule +# dep builds (deps come via DEPENDS above). +EXTRA_OEMAKE = " \ + PLATFORM_TARGET=yocto \ + CC='${CC}' \ + CXX='${CXX}' \ + AR='${AR}' \ + LD='${LD}' \ + STRIP='${STRIP}' \ + RANLIB='${RANLIB}' \ + OBJCOPY='${OBJCOPY}' \ +" + +# Install layout: ${datadir}/helixscreen, with the binary under a bin/ +# subdir so helix-screen's data_root_resolver (see +# src/application/data_root_resolver.cpp) can locate ui_xml/, assets/, +# config/ as siblings of bin/ just like the install.sh deployment does. +# A symlink at ${bindir}/helix-screen keeps the binary on $PATH. +# +# /opt is not a viable target on cosmos — cosmos's rootfs is a read-only +# squashfs with no /opt mount point. /user-resource/ (where install.sh +# places the self-installed binary at runtime) is a separate ext4 partition +# and not where Yocto packages belong; Yocto packages are baked into the +# squashfs via ${datadir}/${bindir}. +HELIX_DIR = "${datadir}/helixscreen" + +do_install() { + install -d ${D}${HELIX_DIR}/bin + install -m 0755 ${B}/build/yocto/bin/helix-screen ${D}${HELIX_DIR}/bin/helix-screen + + # Optional supervisor binaries — ship if the build produced them (splash + # + watchdog are targets built by the Makefile on embedded platforms). + for exe in helix-splash helix-watchdog; do + [ -x ${B}/build/yocto/bin/$exe ] && install -m 0755 ${B}/build/yocto/bin/$exe ${D}${HELIX_DIR}/bin/ + done + + # Launcher wrapper (invokes watchdog or bare binary depending on which + # supervisors are present). + [ -x ${S}/scripts/helix-launcher.sh ] && install -m 0755 ${S}/scripts/helix-launcher.sh ${D}${HELIX_DIR}/bin/ + + # Asset + layout trees (loaded at runtime; no rebuild required to modify). + install -d ${D}${HELIX_DIR}/ui_xml + cp -r ${S}/ui_xml/. ${D}${HELIX_DIR}/ui_xml/ + + install -d ${D}${HELIX_DIR}/assets + cp -r ${S}/assets/. ${D}${HELIX_DIR}/assets/ + + # Default/seed config — runtime config is written to HOME by the app, so + # these ship as reference files only. + install -d ${D}${HELIX_DIR}/config + for f in default_layout.json helix_macros.cfg helixscreen.env \ + printer_database.json printing_tips.json settings.json.template; do + [ -f ${S}/config/$f ] && install -m 0644 ${S}/config/$f ${D}${HELIX_DIR}/config/ + done + + for d in presets sounds themes print_start_profiles printer_database.d platform; do + if [ -d ${S}/config/$d ]; then + install -d ${D}${HELIX_DIR}/config/$d + cp -r ${S}/config/$d/. ${D}${HELIX_DIR}/config/$d/ + fi + done + + # Convenience symlink so helix-screen is on $PATH. + install -d ${D}${bindir} + ln -sf ${HELIX_DIR}/bin/helix-screen ${D}${bindir}/helix-screen + + # SysV init hook (update-rc.d wires it into the right rc*.d dirs). The + # shipped init script in files/ is a cosmos-specific variant that points + # HELIX_CONFIG_DIR at ~/printer_data so user settings persist even when + # the baseline install is on the read-only squashfs rootfs. Only + # DAEMON_DIR differs by packaging site, so we patch it here. + install -d ${D}${sysconfdir}/init.d + install -m 0755 ${WORKDIR}/helixscreen.init ${D}${sysconfdir}/init.d/helixscreen + sed -i 's|^DAEMON_DIR=.*|DAEMON_DIR="${HELIX_DIR}"|' ${D}${sysconfdir}/init.d/helixscreen +} + +FILES:${PN} = " \ + ${HELIX_DIR} \ + ${bindir}/helix-screen \ + ${sysconfdir}/init.d/helixscreen \ +" + +# Runtime deps — Klipper + Moonraker are the upstream ecosystem; without them +# helix-screen launches but has nothing to talk to. +RDEPENDS:${PN} = " \ + klipper \ + moonraker \ + wpa-supplicant \ +" + +INITSCRIPT_NAME = "helixscreen" +# S80 places us after network / Moonraker. +INITSCRIPT_PARAMS = "start 80 2 3 4 5 . stop 20 0 1 6 ." diff --git a/meta-opencentauri/recipes-connectivity/wpa-supplicant/wpa-supplicant_%.bbappend b/meta-opencentauri/recipes-connectivity/wpa-supplicant/wpa-supplicant_%.bbappend new file mode 100644 index 00000000..5c76b065 --- /dev/null +++ b/meta-opencentauri/recipes-connectivity/wpa-supplicant/wpa-supplicant_%.bbappend @@ -0,0 +1,6 @@ +# helixscreen (and guppyscreen-style screens) link against libwpa_client.a +# for WiFi control via wpa_ctrl. Yocto defaults DISABLE_STATIC=" --disable-static", +# which skips the libwpa_client.a build in upstream wpa-supplicant_2.10.bb. +# Clear it so the static library and the wpa_ctrl.h header both get installed +# into ${D}${libdir} / ${D}${includedir}. +DISABLE_STATIC = "" diff --git a/meta-opencentauri/recipes-support/libhv/files/libhv-dns-resolver-fallback.patch b/meta-opencentauri/recipes-support/libhv/files/libhv-dns-resolver-fallback.patch new file mode 100644 index 00000000..1441a531 --- /dev/null +++ b/meta-opencentauri/recipes-support/libhv/files/libhv-dns-resolver-fallback.patch @@ -0,0 +1,327 @@ +diff --git a/base/dns_resolv.c b/base/dns_resolv.c +new file mode 100644 +index 0000000..28c1c98 +--- /dev/null ++++ b/base/dns_resolv.c +@@ -0,0 +1,233 @@ ++// SPDX-License-Identifier: GPL-3.0-or-later ++// ++// Minimal DNS resolver fallback for statically-linked glibc builds. ++// Provides direct UDP DNS resolution when getaddrinfo() fails. ++ ++#include "dns_resolv.h" ++ ++#include "hsocket.h" ++ ++#include ++#include ++#include ++#include ++ ++#ifdef OS_WIN ++#include ++#define getpid _getpid ++#else ++#include ++#endif ++ ++// Skip a DNS name in wire format. Returns new offset, or -1 on error. ++static int dns_resolv_skip_name(const uint8_t* buf, int off, int len) { ++ while (off < len) { ++ uint8_t label_len = buf[off]; ++ if (label_len == 0) { ++ return off + 1; ++ } ++ if ((label_len & 0xC0) == 0xC0) { ++ // Compression pointer: 2 bytes ++ return (off + 1 < len) ? off + 2 : -1; ++ } ++ if (label_len > 63) { ++ return -1; // Invalid: labels max 63 bytes, reserved bits set ++ } ++ off += label_len + 1; ++ } ++ return -1; ++} ++ ++int dns_resolv_build_query(const char* hostname, uint8_t* buf, int buflen) { ++ if (!hostname || !buf || hostname[0] == '\0') return -1; ++ ++ int hostname_len = (int)strlen(hostname); ++ if (hostname_len > 253) return -1; ++ ++ // Strip trailing dot ++ char name[256]; ++ memcpy(name, hostname, hostname_len + 1); ++ if (hostname_len > 0 && name[hostname_len - 1] == '.') { ++ name[--hostname_len] = '\0'; ++ } ++ if (hostname_len == 0) return -1; ++ ++ // Estimate: header(12) + name(hostname_len+2) + type(2) + class(2) ++ int est_len = 12 + hostname_len + 2 + 4; ++ if (est_len > buflen) return -1; ++ ++ memset(buf, 0, 12); ++ ++ // Transaction ID ++ uint16_t txid = (uint16_t)((unsigned)getpid() ^ (unsigned)time(NULL)); ++ buf[0] = (uint8_t)(txid >> 8); ++ buf[1] = (uint8_t)(txid & 0xFF); ++ // Flags: RD=1 (recursion desired) ++ buf[2] = 0x01; ++ // Questions: 1 ++ buf[5] = 0x01; ++ ++ // Encode hostname into DNS wire format ++ uint8_t* p = buf + 12; ++ const char* start = name; ++ const char* dot; ++ while ((dot = strchr(start, '.')) != NULL) { ++ int label_len = (int)(dot - start); ++ if (label_len > 63 || label_len == 0) return -1; ++ *p++ = (uint8_t)label_len; ++ memcpy(p, start, label_len); ++ p += label_len; ++ start = dot + 1; ++ } ++ // Last label ++ int last_len = (int)strlen(start); ++ if (last_len > 63 || last_len == 0) return -1; ++ *p++ = (uint8_t)last_len; ++ memcpy(p, start, last_len); ++ p += last_len; ++ *p++ = 0; // Root label ++ ++ // Type A (1) ++ *p++ = 0x00; *p++ = 0x01; ++ // Class IN (1) ++ *p++ = 0x00; *p++ = 0x01; ++ ++ return (int)(p - buf); ++} ++ ++int dns_resolv_parse_response(const uint8_t* buf, int len, struct in_addr* addr) { ++ if (!buf || !addr || len < 12) return -1; ++ ++ // Must be a response (QR=1) ++ if (!(buf[2] & 0x80)) return -1; ++ ++ // Check RCODE is 0 (no error) ++ if ((buf[3] & 0x0F) != 0) return -1; ++ ++ uint16_t nquestion = (uint16_t)((buf[4] << 8) | buf[5]); ++ uint16_t nanswer = (uint16_t)((buf[6] << 8) | buf[7]); ++ if (nanswer == 0) return -1; ++ ++ int off = 12; ++ ++ // Skip question section ++ for (uint16_t i = 0; i < nquestion; i++) { ++ off = dns_resolv_skip_name(buf, off, len); ++ if (off < 0) return -1; ++ off += 4; // type + class ++ if (off > len) return -1; ++ } ++ ++ // Parse answer section, looking for first A record ++ for (uint16_t i = 0; i < nanswer; i++) { ++ off = dns_resolv_skip_name(buf, off, len); ++ if (off < 0) return -1; ++ if (off + 10 > len) return -1; ++ ++ uint16_t rtype = (uint16_t)((buf[off] << 8) | buf[off + 1]); ++ uint16_t rdlength = (uint16_t)((buf[off + 8] << 8) | buf[off + 9]); ++ off += 10; ++ ++ if (off + rdlength > len) return -1; ++ ++ if (rtype == 1 && rdlength == 4) { ++ // A record: 4 bytes of IPv4 address ++ memcpy(&addr->s_addr, buf + off, 4); ++ return 0; ++ } ++ ++ off += rdlength; ++ } ++ ++ return -1; // No A record found ++} ++ ++int dns_resolv_get_nameservers_from(const char* path, ++ char nameservers[][DNS_RESOLV_NAMESERVER_LEN], ++ int max) { ++ if (!path || !nameservers || max <= 0) return 0; ++ ++ FILE* fp = fopen(path, "r"); ++ if (!fp) return 0; ++ ++ int count = 0; ++ char line[256]; ++ while (fgets(line, sizeof(line), fp) && count < max) { ++ // Skip leading whitespace ++ char* p = line; ++ while (*p == ' ' || *p == '\t') p++; ++ ++ // Skip comments and empty lines ++ if (*p == '#' || *p == ';' || *p == '\n' || *p == '\0') continue; ++ ++ char ns[64]; ++ if (sscanf(p, "nameserver %63s", ns) == 1) { ++ strncpy(nameservers[count], ns, DNS_RESOLV_NAMESERVER_LEN - 1); ++ nameservers[count][DNS_RESOLV_NAMESERVER_LEN - 1] = '\0'; ++ count++; ++ } ++ } ++ ++ fclose(fp); ++ return count; ++} ++ ++int dns_resolv_get_nameservers(char nameservers[][DNS_RESOLV_NAMESERVER_LEN], int max) { ++ return dns_resolv_get_nameservers_from("/etc/resolv.conf", nameservers, max); ++} ++ ++int dns_resolv_resolve(const char* hostname, struct in_addr* addr) { ++ if (!hostname || !addr) return -1; ++ ++ uint8_t query[DNS_RESOLV_MAX_PACKET_LEN]; ++ int query_len = dns_resolv_build_query(hostname, query, sizeof(query)); ++ if (query_len < 0) return -1; ++ ++ // Get nameservers from resolv.conf ++ char nameservers[DNS_RESOLV_MAX_NAMESERVERS][DNS_RESOLV_NAMESERVER_LEN]; ++ int ns_count = dns_resolv_get_nameservers(nameservers, DNS_RESOLV_MAX_NAMESERVERS); ++ ++ // Fallback to public DNS ++ if (ns_count == 0) { ++ strncpy(nameservers[0], DNS_RESOLV_FALLBACK_NS, DNS_RESOLV_NAMESERVER_LEN); ++ ns_count = 1; ++ } ++ ++ int sockfd = socket(AF_INET, SOCK_DGRAM, 0); ++ if (sockfd < 0) return -1; ++ ++ so_sndtimeo(sockfd, DNS_RESOLV_TIMEOUT_MS); ++ so_rcvtimeo(sockfd, DNS_RESOLV_TIMEOUT_MS); ++ ++ int result = -1; ++ ++ for (int i = 0; i < ns_count; i++) { ++ struct sockaddr_in ns_addr; ++ memset(&ns_addr, 0, sizeof(ns_addr)); ++ ns_addr.sin_family = AF_INET; ++ ns_addr.sin_port = htons(53); ++ if (inet_pton(AF_INET, nameservers[i], &ns_addr.sin_addr) != 1) { ++ continue; // Skip invalid nameserver IPs (e.g. IPv6) ++ } ++ ++ int sent = sendto(sockfd, (const char*)query, query_len, 0, ++ (struct sockaddr*)&ns_addr, sizeof(ns_addr)); ++ if (sent != query_len) continue; ++ ++ uint8_t response[DNS_RESOLV_MAX_PACKET_LEN]; ++ int received = recvfrom(sockfd, (char*)response, sizeof(response), 0, ++ NULL, NULL); ++ if (received <= 0) continue; ++ ++ // Verify transaction ID ++ if (received >= 2 && response[0] == query[0] && response[1] == query[1]) { ++ if (dns_resolv_parse_response(response, received, addr) == 0) { ++ result = 0; ++ break; ++ } ++ } ++ } ++ ++ closesocket(sockfd); ++ return result; ++} +diff --git a/base/dns_resolv.h b/base/dns_resolv.h +new file mode 100644 +index 0000000..e4403a1 +--- /dev/null ++++ b/base/dns_resolv.h +@@ -0,0 +1,45 @@ ++// SPDX-License-Identifier: GPL-3.0-or-later ++// ++// Minimal DNS resolver fallback for statically-linked glibc builds. ++// When getaddrinfo() fails (can't load NSS modules), this provides ++// direct UDP DNS resolution as a fallback. ++ ++#ifndef DNS_RESOLV_H_ ++#define DNS_RESOLV_H_ ++ ++#include "hexport.h" ++#include "hplatform.h" ++ ++#define DNS_RESOLV_MAX_NAMESERVERS 3 ++#define DNS_RESOLV_NAMESERVER_LEN 64 ++#define DNS_RESOLV_MAX_PACKET_LEN 512 ++#define DNS_RESOLV_TIMEOUT_MS 2000 ++#define DNS_RESOLV_FALLBACK_NS "8.8.8.8" ++ ++BEGIN_EXTERN_C ++ ++// Build a DNS A-record query packet for the given hostname. ++// Returns packet length on success, -1 on error. ++HV_EXPORT int dns_resolv_build_query(const char* hostname, uint8_t* buf, int buflen); ++ ++// Parse a DNS response and extract the first A-record IPv4 address. ++// Returns 0 on success, -1 on error. ++HV_EXPORT int dns_resolv_parse_response(const uint8_t* buf, int len, struct in_addr* addr); ++ ++// Parse nameservers from a resolv.conf-format file. ++// Returns number of nameservers found (up to max). ++HV_EXPORT int dns_resolv_get_nameservers_from(const char* path, ++ char nameservers[][DNS_RESOLV_NAMESERVER_LEN], ++ int max); ++ ++// Parse nameservers from /etc/resolv.conf. ++HV_EXPORT int dns_resolv_get_nameservers(char nameservers[][DNS_RESOLV_NAMESERVER_LEN], int max); ++ ++// Resolve hostname to IPv4 via direct UDP DNS queries. ++// Tries nameservers from /etc/resolv.conf, falls back to 8.8.8.8. ++// Returns 0 on success, -1 on error. ++HV_EXPORT int dns_resolv_resolve(const char* hostname, struct in_addr* addr); ++ ++END_EXTERN_C ++ ++#endif // DNS_RESOLV_H_ +diff --git a/base/hsocket.c b/base/hsocket.c +index ed87a8c..4c13ccd 100644 +--- a/base/hsocket.c ++++ b/base/hsocket.c +@@ -2,6 +2,10 @@ + + #include "hdef.h" + ++#if defined(__GLIBC__) ++#include "dns_resolv.h" ++#endif ++ + #ifdef OS_WIN + #include "hatomic.h" + static hatomic_flag_t s_wsa_initialized = HATOMIC_FLAG_INIT; +@@ -64,6 +68,21 @@ int ResolveAddr(const char* host, sockaddr_u* addr) { + addr->sa.sa_family = AF_INET6; // host is ipv6 + } + ++#if defined(__GLIBC__) ++ // Try direct UDP DNS resolution FIRST on glibc builds. ++ // Static glibc can't load NSS modules (libnss_dns.so) at runtime, ++ // so getaddrinfo() can CRASH (SIGSEGV in gaih_inet) on ARM platforms ++ // like AD5M/SonicPad/K1. Avoid calling getaddrinfo() entirely. (#700) ++ { ++ struct in_addr resolved; ++ if (dns_resolv_resolve(host, &resolved) == 0) { ++ memset(addr, 0, sizeof(sockaddr_u)); ++ addr->sa.sa_family = AF_INET; ++ addr->sin.sin_addr = resolved; ++ return 0; ++ } ++ } ++#endif + struct addrinfo* ais = NULL; + int ret = getaddrinfo(host, NULL, NULL, &ais); + if (ret != 0 || ais == NULL || ais->ai_addr == NULL || ais->ai_addrlen == 0) { diff --git a/meta-opencentauri/recipes-support/libhv/libhv_1.3.4.bb b/meta-opencentauri/recipes-support/libhv/libhv_1.3.4.bb new file mode 100644 index 00000000..f60006ee --- /dev/null +++ b/meta-opencentauri/recipes-support/libhv/libhv_1.3.4.bb @@ -0,0 +1,53 @@ +SUMMARY = "libhv — event loop, HTTP, WebSocket, MQTT library" +DESCRIPTION = "A c/c++ network library for developing TCP/UDP/SSL/HTTP/WebSocket/MQTT \ + client/server. Used by helixscreen for Moonraker WebSocket + HTTP clients." +HOMEPAGE = "https://github.com/ithewei/libhv" +LICENSE = "BSD-3-Clause" +LIC_FILES_CHKSUM = "file://LICENSE;md5=122a2f8324611e54381a1de69934481b" + +SRC_URI = "git://github.com/ithewei/libhv.git;protocol=https;branch=master \ + file://libhv-dns-resolver-fallback.patch \ +" +SRCREV = "71770e04becaa149e0ef8ffc4d3900c5466ddddb" + +FILESEXTRAPATHS:prepend := "${THISDIR}/files:" + +S = "${WORKDIR}/git" + +DEPENDS = "openssl zlib" + +inherit cmake pkgconfig + +EXTRA_OECMAKE = " \ + -DBUILD_SHARED=ON \ + -DBUILD_STATIC=OFF \ + -DBUILD_EXAMPLES=OFF \ + -DBUILD_UNITTEST=OFF \ + -DWITH_OPENSSL=ON \ + -DWITH_HTTP=ON \ + -DWITH_HTTP_SERVER=ON \ + -DWITH_HTTP_CLIENT=ON \ + -DWITH_EVPP=ON \ + -DWITH_MQTT=OFF \ +" + +# libhv generates hv/json.hpp etc. into include/hv at configure time; +# cmake install copies them to ${includedir}/hv. Also ship the CMake +# package configuration so downstream cmake projects can find_package(hv). +FILES:${PN} += "${libdir}/cmake/hv" + +# Suppress "file /usr/include/hv/... not shipped" warnings by making -dev pick +# up all public headers (default behavior, but be explicit for clarity). +# libhv's CMakeLists doesn't set SOVERSION, so `make install` produces +# /usr/lib/libhv.so as a plain ELF rather than the usual +# libhv.so -> libhv.so.1 -> libhv.so.1.3.4 symlink chain. Rename it at install +# time to match the PROJECT_VERSION and create the expected symlinks so the +# shared object lands in the runtime package and -lhv still resolves at build +# time. +do_install:append() { + if [ -f ${D}${libdir}/libhv.so ] && [ ! -L ${D}${libdir}/libhv.so ]; then + mv ${D}${libdir}/libhv.so ${D}${libdir}/libhv.so.${PV} + ln -s libhv.so.${PV} ${D}${libdir}/libhv.so.1 + ln -s libhv.so.${PV} ${D}${libdir}/libhv.so + fi +} From b41954182553ef6d45bdf3a33f066bab38fdfb9a Mon Sep 17 00:00:00 2001 From: Preston Brown Date: Wed, 15 Apr 2026 21:46:47 -0400 Subject: [PATCH 2/4] meta-opencentauri: rework helixscreen recipe per review + RO/RW split MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses all review feedback from #148: helixscreen_0.99.bb: - Binary installed to ${bindir} (FHS), not ${datadir}/bin + symlink - Init script overhauled: guppy-style start-stop-daemon, HELIX_CONFIG_DIR and HELIX_DATA_DIR set via sed placeholders, no /var/lib mkdir (volatile on cosmos), no '[ -x $DAEMON ] || exit 0' check on a file we just installed - RO data (ui_xml, assets including seed configs) at ${datadir}/helixscreen - User-writable config at ${sysconfdir}/klipper/config/helixscreen (CONFFILES — preserved across opkg upgrade), seeded with cc1.json preset - Dev-only test gcodes excluded from .ipk (tar --exclude, saves ~50 MB) - Explicit do_compile() for externalsrc compatibility - RDEPENDS: dropped klipper (transitive via moonraker), kept moonraker + wpa-supplicant - INITSCRIPT_PARAMS = disable (gui-switcher picks the active UI) - pkg_postinst: migrates writable state from /user-resource/helixscreen tarball install (settings.json, telemetry_*.json, tool_spools, themes, custom_images) into the new ${HELIX_USER_CONFIG_DIR}; seeds are NOT copied (find_readable falls back to ${HELIX_DATA_DIR}/assets/config/) helixscreen.init: - Replaced 275-line multi-platform init with 80-line cosmos-specific script using start-stop-daemon. Path placeholders (@HELIX_BINDIR@, @HELIX_DATA_DIR@, @HELIX_USER_CONFIG_DIR@) get sed'd by do_install. libhv_1.3.4.bb: - New patch: libhv-set-soversion.patch adds VERSION/SOVERSION to the cmake shared library target so DT_SONAME=libhv.so.1 is stamped into the ELF. Without this, helix-screen gets DT_NEEDED=libhv.so (unversioned, dev-only) and fails to load at runtime. - Dropped patchelf-native dep + the manual do_install rename/symlink dance — cmake handles the chain natively now. - FILES:${PN}-dev += cmake/hv (was incorrectly in runtime package). Verified on CC1 (192.168.1.52) via manual .ipk extract + env-var launch: app starts, connects to Moonraker, loads theme/tips/printer DB from ${HELIX_DATA_DIR}/assets/config/, writes state to ${HELIX_CONFIG_DIR}, postinst migration copies tarball user state correctly. --- .../helixscreen/files/helixscreen.init | 308 +++--------------- .../helixscreen/helixscreen_0.99.bb | 206 ++++++++---- .../libhv/files/libhv-set-soversion.patch | 23 ++ .../recipes-support/libhv/libhv_1.3.4.bb | 31 +- 4 files changed, 222 insertions(+), 346 deletions(-) create mode 100644 meta-opencentauri/recipes-support/libhv/files/libhv-set-soversion.patch diff --git a/meta-opencentauri/recipes-apps/helixscreen/files/helixscreen.init b/meta-opencentauri/recipes-apps/helixscreen/files/helixscreen.init index da4d05e3..e1b0186a 100755 --- a/meta-opencentauri/recipes-apps/helixscreen/files/helixscreen.init +++ b/meta-opencentauri/recipes-apps/helixscreen/files/helixscreen.init @@ -1,272 +1,70 @@ #!/bin/sh # SPDX-License-Identifier: GPL-3.0-or-later -# -# SysV init script for HelixScreen -# For use on systems without systemd (e.g., FlashForge AD5M with BusyBox init) -# -# Installation varies by firmware: -# Forge-X: /etc/init.d/S90helixscreen -> /opt/helixscreen -# Klipper Mod: /etc/init.d/S80helixscreen -> /opt/helixscreen (v00.06+) -# or /root/printer_software/helixscreen (v00.05) -# -# The install.sh script automatically selects the correct paths. -# DAEMON_DIR below is updated by the installer to match the install location. -# -# Usage: -# /etc/init.d/S*helixscreen start|stop|restart|status -# -# Note: S80/S90 prefix ensures we start after network and Moonraker - -NAME="helixscreen" -DESC="HelixScreen 3D Printer Touch UI" -DAEMON_DIR="/opt/helixscreen" # Updated by installer for different firmware -DAEMON="${DAEMON_DIR}/bin/helix-screen" -SPLASH="${DAEMON_DIR}/bin/helix-splash" -WATCHDOG="${DAEMON_DIR}/bin/helix-watchdog" -LAUNCHER="${DAEMON_DIR}/bin/helix-launcher.sh" -PIDFILE="/var/run/${NAME}.pid" -LOGFILE="/tmp/helixscreen.log" - -# Override asset directory if ui_xml/assets/config are not under DAEMON_DIR -# Export so helix-screen picks it up at startup -# HELIX_DATA_DIR="/mnt/data/helixscreen" -# export HELIX_DATA_DIR - -# Ensure HOME is set (BusyBox init may not set it). -# Without HOME, config backup falls back to volatile /tmp/ and is lost on reboot. -: "${HOME:=/root}" -export HOME - -# Point user-writable settings at klipper's printer_data so they persist even -# when the baseline install lives on a read-only rootfs (e.g., the cosmos -# squashfs). Honored by helix::Config::init in src/system/config.cpp. -: "${HELIX_CONFIG_DIR:=${HOME}/printer_data/config/helixscreen}" -export HELIX_CONFIG_DIR - -# Ensure config backup directory exists (survives Moonraker's shutil.rmtree wipe). -# On systemd systems StateDirectory= creates /var/lib/helixscreen/; on SysV we do it here. -mkdir -p /var/lib/helixscreen 2>/dev/null || mkdir -p "${HOME}/.helixscreen" 2>/dev/null || true - -# Check if binary exists -[ -x "$DAEMON" ] || exit 0 - -# Platform hook defaults (no-ops). Platform-specific hook files override these. -platform_stop_competing_uis() { :; } -platform_enable_backlight() { :; } -platform_wait_for_services() { :; } -platform_pre_start() { :; } -platform_post_stop() { :; } -platform_wait_for_boot_complete() { :; } - -# Source platform hooks if available -PLATFORM_HOOKS="${DAEMON_DIR}/platform/hooks.sh" +### BEGIN INIT INFO +# Provides: helixscreen +# Required-Start: $local_fs $network klipper moonraker +# Required-Stop: $local_fs $network +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Description: HelixScreen - Touch UI for Klipper +### END INIT INFO + +# Path placeholders are populated by the recipe's do_install via sed. +HELIX_BIN="@HELIX_BINDIR@/helix-screen" +HELIX_LAUNCHER="@HELIX_BINDIR@/helix-launcher.sh" +HELIX_DATA_DIR="@HELIX_DATA_DIR@" +HELIX_CONFIG_DIR="@HELIX_USER_CONFIG_DIR@" + +PIDFILE=/var/run/gui.pid + +export HELIX_DATA_DIR HELIX_CONFIG_DIR + +# Pull in cosmos-specific platform hooks (e.g. backlight, framebuffer +# init). Hooks live alongside the read-only data root. +PLATFORM_HOOKS="${HELIX_DATA_DIR}/assets/config/platform/hooks-cc1.sh" if [ -f "$PLATFORM_HOOKS" ]; then - # shellcheck disable=SC1090 # path depends on DAEMON_DIR set by installer + # shellcheck disable=SC1090 . "$PLATFORM_HOOKS" fi +platform_pre_start() { :; } 2>/dev/null +platform_post_stop() { :; } 2>/dev/null -# Stop splash screen -stop_splash() { - if [ -f /var/run/helix-splash.pid ]; then - SPLASH_PID=$(cat /var/run/helix-splash.pid) - kill "$SPLASH_PID" 2>/dev/null || true - rm -f /var/run/helix-splash.pid - fi -} - -start() { - # Ensure valid CWD before anything else. - # The installer or boot system may leave us in a deleted/unmounted directory, - # which causes "getcwd: cannot access parent directories" errors from sh and chroot. - cd / 2>/dev/null || true - - echo "Starting $DESC..." - - # Platform-specific pre-start (e.g., flag files for firmware coordination) - platform_pre_start - - # Stop any competing UIs first - platform_stop_competing_uis +# Make sure the writable user-config dir exists. CONFFILES from the package +# only creates settings.json; runtime state (telemetry_*.json, tool_spools.json, +# custom_images/, etc.) lands here too via $HELIX_CONFIG_DIR. +mkdir -p "$HELIX_CONFIG_DIR" - # Enable backlight (some firmware may leave it off) - platform_enable_backlight - - # Check if already running. The PIDFILE may store a recycled PID after a - # reboot or crash — confirm by looking for the actual helix-screen process - # before trusting it. Otherwise a stale PID owned by an unrelated process - # makes us return "already running" while no UI is actually up. - DAEMON_NAME=$(basename "$DAEMON") - if [ -f "$PIDFILE" ]; then - PID=$(cat "$PIDFILE") - if kill -0 "$PID" 2>/dev/null && pidof "$DAEMON_NAME" >/dev/null 2>&1; then - echo "$NAME is already running (PID $PID)" - return 0 - fi - rm -f "$PIDFILE" - fi - # Catch the no-PIDFILE-but-running case (e.g., started outside this script) - if pidof "$DAEMON_NAME" >/dev/null 2>&1; then - RUNNING_PID=$(pidof "$DAEMON_NAME" | awk '{print $1}') - echo "$NAME is already running (PID $RUNNING_PID, no PID file)" - return 0 - fi - - cd "$DAEMON_DIR" || exit 1 - - # Start splash screen early (before launcher) so it's visible during load - # Set HELIX_NO_SPLASH=1 to disable splash (for debugging backlight issues) - # Splash auto-detects resolution from display hardware - EARLY_SPLASH_PID="" - if [ -x "$SPLASH" ] && [ "${HELIX_NO_SPLASH:-0}" != "1" ]; then - echo "Starting splash screen..." - "$SPLASH" >/dev/null 2>&1 & - EARLY_SPLASH_PID=$! - echo "$EARLY_SPLASH_PID" > /var/run/helix-splash.pid - elif [ "${HELIX_NO_SPLASH:-0}" = "1" ]; then - echo "Splash screen disabled (HELIX_NO_SPLASH=1)" - fi - - # Fork the service wait + helix-screen launch into background - # This lets the init script exit so later scripts can run (e.g., S99 starts moonraker) - # Without this, we'd deadlock: S90 waits for services, S99 starts them but can't run until S90 finishes - ( - # Wait for required services before starting the main UI - # Platform hooks control what (if anything) to wait for - if [ "${HELIX_NO_SERVICE_WAIT:-0}" != "1" ]; then - platform_wait_for_services - fi - - # Wait for platform boot sequence to finish (e.g., ForgeX S99root) - # Ensures no other process is writing to the framebuffer when helix-screen starts - if [ "${HELIX_NO_BOOT_WAIT:-0}" != "1" ]; then - platform_wait_for_boot_complete +case "$1" in + start) + echo "Starting HelixScreen..." + type platform_pre_start >/dev/null 2>&1 && platform_pre_start + if [ -e /sys/class/graphics/fbcon/cursor_blink ]; then + echo 0 > /sys/class/graphics/fbcon/cursor_blink fi - - # Use launcher script if available (handles watchdog) - # Pass early splash PID so watchdog doesn't start another splash - if [ -x "$LAUNCHER" ]; then - if [ -n "$EARLY_SPLASH_PID" ]; then - export HELIX_SPLASH_PID="$EARLY_SPLASH_PID" - fi - cd "$DAEMON_DIR" && exec $LAUNCHER + if [ -x "$HELIX_LAUNCHER" ]; then + start-stop-daemon -S -b -m -p "$PIDFILE" -x "$HELIX_LAUNCHER" else - # Fallback: start daemon directly - cd "$DAEMON_DIR" && exec "$DAEMON" --splash-pid="$EARLY_SPLASH_PID" + start-stop-daemon -S -b -m -p "$PIDFILE" -x "$HELIX_BIN" fi - ) >> "$LOGFILE" 2>&1 & - echo $! > "$PIDFILE" - - # Wait briefly and check if started - sleep 2 - if [ -f "$PIDFILE" ]; then - PID=$(cat "$PIDFILE") - if kill -0 "$PID" 2>/dev/null; then - echo "$NAME started (PID $PID)" - return 0 - fi - fi - - echo "Failed to start $NAME" - stop_splash - return 1 -} - -stop() { - echo "Stopping $DESC..." - - # Stop splash first - stop_splash - - if [ -f "$PIDFILE" ]; then - PID=$(cat "$PIDFILE") - if kill -0 "$PID" 2>/dev/null; then - kill "$PID" - # Wait for process to exit - for _ in 1 2 3 4 5; do - if ! kill -0 "$PID" 2>/dev/null; then - break - fi - sleep 1 - done - # Force kill if still running - if kill -0 "$PID" 2>/dev/null; then - kill -9 "$PID" 2>/dev/null || true - fi - fi - rm -f "$PIDFILE" - fi - - # Also kill by name in case PID file is stale or the launcher died before - # signaling its children. Kill watchdog first to prevent crash dialog flash - # during clean shutdown. SIGTERM then SIGKILL: helix-watchdog and helix-screen - # may catch SIGTERM without exiting (splash handoff, blocked I/O). - WATCHDOG_NAME=$(basename "$WATCHDOG") - DAEMON_NAME=$(basename "$DAEMON") - SPLASH_NAME=$(basename "$SPLASH") - for proc in "$WATCHDOG_NAME" "$DAEMON_NAME" "$SPLASH_NAME"; do - pids=$(pidof "$proc" 2>/dev/null || true) - [ -n "$pids" ] || continue - # shellcheck disable=SC2086 - kill $pids 2>/dev/null || true - done - # Give caught-SIGTERM handlers a moment, then SIGKILL any survivors. - sleep 1 - for proc in "$WATCHDOG_NAME" "$DAEMON_NAME" "$SPLASH_NAME"; do - pids=$(pidof "$proc" 2>/dev/null || true) - [ -n "$pids" ] || continue - # shellcheck disable=SC2086 - kill -9 $pids 2>/dev/null || true - done - - # Platform-specific post-stop cleanup (e.g., remove flag files) - # Runs after daemon is fully stopped so platform can safely reclaim resources - platform_post_stop - - echo "$NAME stopped" -} - -restart() { - stop - sleep 1 - start -} - -status() { - if [ -f "$PIDFILE" ]; then - PID=$(cat "$PIDFILE") - if kill -0 "$PID" 2>/dev/null; then - echo "$NAME is running (PID $PID)" - return 0 - fi - echo "$NAME is not running (stale PID file)" - return 1 - fi - - # Check if running without PID file - if command -v pidof >/dev/null 2>&1; then - PID=$(pidof "$(basename "$DAEMON")" 2>/dev/null) - if [ -n "$PID" ]; then - echo "$NAME is running (PID $PID) [no PID file]" - return 0 - fi - fi - - echo "$NAME is not running" - return 1 -} - -case "$1" in - start) - start ;; stop) - stop + echo "Stopping HelixScreen..." + start-stop-daemon -K -p "$PIDFILE" -R 10 + rm -f "$PIDFILE" + type platform_post_stop >/dev/null 2>&1 && platform_post_stop ;; - restart|reload) - restart + restart|force-reload) + $0 stop + sleep 1 + $0 start ;; status) - status + if [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then + echo "HelixScreen is running (PID $(cat "$PIDFILE"))" + else + echo "HelixScreen is not running" + return 1 + fi ;; *) echo "Usage: $0 {start|stop|restart|status}" @@ -274,4 +72,4 @@ case "$1" in ;; esac -exit $? +exit 0 diff --git a/meta-opencentauri/recipes-apps/helixscreen/helixscreen_0.99.bb b/meta-opencentauri/recipes-apps/helixscreen/helixscreen_0.99.bb index f09f82cc..5334e4f2 100644 --- a/meta-opencentauri/recipes-apps/helixscreen/helixscreen_0.99.bb +++ b/meta-opencentauri/recipes-apps/helixscreen/helixscreen_0.99.bb @@ -1,4 +1,4 @@ -SUMMARY = "HelixScreen - Interface for Klipper" +SUMMARY = "HelixScreen - touch UI for Klipper" DESCRIPTION = "A modern, lightweight touchscreen interface for Klipper 3D printers" HOMEPAGE = "https://github.com/prestonbrown/helixscreen" LICENSE = "GPL-3.0-only" @@ -9,10 +9,10 @@ FILESEXTRAPATHS:prepend := "${THISDIR}/files:" SRC_URI = "gitsm://github.com/prestonbrown/helixscreen.git;protocol=https;branch=main \ file://helixscreen.init \ " -# Main-branch commit that carries PLATFORM_TARGET=yocto support (Makefile -# YOCTO_BUILD mode, splash/watchdog LDFLAGS preservation, Config::init -# HELIX_CONFIG_DIR override, docker-based dev loop). -SRCREV = "cde268af7a8ea1af4ebab9cad0a57b7ef31a87a1" +# Main-branch commit that carries the assets/config split (RO seeds out of +# config/), the find_readable / writable_path / get_data_dir resolver helpers, +# and the PLATFORM_TARGET=yocto Makefile path. +SRCREV = "${AUTOREV}" S = "${WORKDIR}/git" @@ -30,9 +30,16 @@ DEPENDS = " \ zlib \ " -# HelixScreen has a PLATFORM_TARGET=yocto mode in its Makefile that honors -# the bitbake-provided toolchain + CFLAGS env and skips all in-tree submodule -# dep builds (deps come via DEPENDS above). +# helix-screen talks to Klipper exclusively via the Moonraker WebSocket+REST +# API; klipper itself is a transitive dep of moonraker. moonraker is enough. +RDEPENDS:${PN} = " \ + moonraker \ + wpa-supplicant \ +" + +# HelixScreen has a PLATFORM_TARGET=yocto mode in its Makefile that honors the +# bitbake-provided toolchain + CFLAGS env and skips all in-tree submodule dep +# builds (deps come via DEPENDS above). EXTRA_OEMAKE = " \ PLATFORM_TARGET=yocto \ CC='${CC}' \ @@ -44,83 +51,138 @@ EXTRA_OEMAKE = " \ OBJCOPY='${OBJCOPY}' \ " -# Install layout: ${datadir}/helixscreen, with the binary under a bin/ -# subdir so helix-screen's data_root_resolver (see -# src/application/data_root_resolver.cpp) can locate ui_xml/, assets/, -# config/ as siblings of bin/ just like the install.sh deployment does. -# A symlink at ${bindir}/helix-screen keeps the binary on $PATH. -# -# /opt is not a viable target on cosmos — cosmos's rootfs is a read-only -# squashfs with no /opt mount point. /user-resource/ (where install.sh -# places the self-installed binary at runtime) is a separate ext4 partition -# and not where Yocto packages belong; Yocto packages are baked into the -# squashfs via ${datadir}/${bindir}. -HELIX_DIR = "${datadir}/helixscreen" +# Run the upstream Makefile. Required for both gitsm-fetched and externalsrc +# builds — externalsrc's default do_compile is a no-op. +do_compile() { + cd ${S} + oe_runmake +} + +# Layout (FHS-aligned, matches guppyscreen / grumpyscreen conventions): +# ${bindir}/helix-screen — primary binary (real, not symlink) +# ${bindir}/helix-splash — early-boot splash binary +# ${bindir}/helix-watchdog — crash dialog + auto-restart +# ${bindir}/helix-launcher.sh — supervisor wrapper +# ${datadir}/helixscreen/ui_xml/ — runtime-loaded LVGL XML layouts +# ${datadir}/helixscreen/assets/ — fonts, images, sounds, RO seed +# configs (assets/config/printer_database.json, +# presets/, themes/defaults/, etc.) +# ${sysconfdir}/init.d/helixscreen — SysV init script +# ${sysconfdir}/klipper/config/helixscreen/settings.json +# — default user settings (CONFFILES; +# preserved across opkg upgrade). +# Init script exports HELIX_CONFIG_DIR +# pointing at this directory so +# runtime state writes land here too. +HELIX_DATA_DIR = "${datadir}/helixscreen" +HELIX_USER_CONFIG_DIR = "${sysconfdir}/klipper/config/helixscreen" do_install() { - install -d ${D}${HELIX_DIR}/bin - install -m 0755 ${B}/build/yocto/bin/helix-screen ${D}${HELIX_DIR}/bin/helix-screen - - # Optional supervisor binaries — ship if the build produced them (splash - # + watchdog are targets built by the Makefile on embedded platforms). + # --- binaries ----------------------------------------------------------- + install -d ${D}${bindir} + install -m 0755 ${B}/build/yocto/bin/helix-screen ${D}${bindir}/helix-screen for exe in helix-splash helix-watchdog; do - [ -x ${B}/build/yocto/bin/$exe ] && install -m 0755 ${B}/build/yocto/bin/$exe ${D}${HELIX_DIR}/bin/ - done - - # Launcher wrapper (invokes watchdog or bare binary depending on which - # supervisors are present). - [ -x ${S}/scripts/helix-launcher.sh ] && install -m 0755 ${S}/scripts/helix-launcher.sh ${D}${HELIX_DIR}/bin/ - - # Asset + layout trees (loaded at runtime; no rebuild required to modify). - install -d ${D}${HELIX_DIR}/ui_xml - cp -r ${S}/ui_xml/. ${D}${HELIX_DIR}/ui_xml/ - - install -d ${D}${HELIX_DIR}/assets - cp -r ${S}/assets/. ${D}${HELIX_DIR}/assets/ - - # Default/seed config — runtime config is written to HOME by the app, so - # these ship as reference files only. - install -d ${D}${HELIX_DIR}/config - for f in default_layout.json helix_macros.cfg helixscreen.env \ - printer_database.json printing_tips.json settings.json.template; do - [ -f ${S}/config/$f ] && install -m 0644 ${S}/config/$f ${D}${HELIX_DIR}/config/ - done - - for d in presets sounds themes print_start_profiles printer_database.d platform; do - if [ -d ${S}/config/$d ]; then - install -d ${D}${HELIX_DIR}/config/$d - cp -r ${S}/config/$d/. ${D}${HELIX_DIR}/config/$d/ + if [ -x ${B}/build/yocto/bin/$exe ]; then + install -m 0755 ${B}/build/yocto/bin/$exe ${D}${bindir}/$exe fi done - - # Convenience symlink so helix-screen is on $PATH. - install -d ${D}${bindir} - ln -sf ${HELIX_DIR}/bin/helix-screen ${D}${bindir}/helix-screen - - # SysV init hook (update-rc.d wires it into the right rc*.d dirs). The - # shipped init script in files/ is a cosmos-specific variant that points - # HELIX_CONFIG_DIR at ~/printer_data so user settings persist even when - # the baseline install is on the read-only squashfs rootfs. Only - # DAEMON_DIR differs by packaging site, so we patch it here. + if [ -x ${S}/scripts/helix-launcher.sh ]; then + install -m 0755 ${S}/scripts/helix-launcher.sh ${D}${bindir}/helix-launcher.sh + fi + + # --- read-only data root ------------------------------------------------ + install -d ${D}${HELIX_DATA_DIR}/assets + cp -r ${S}/ui_xml ${D}${HELIX_DATA_DIR}/ui_xml + # Exclude dev-only test fixtures (~150 MB of mock-mode gcodes / timelapse + # frames) and macOS metadata. tar+pipe is portable; avoids needing rsync + # in HOSTTOOLS. + (cd ${S} && tar cf - \ + --exclude=test_gcodes \ + --exclude=test_timelapse \ + --exclude='*.gcode' \ + --exclude='.DS_Store' \ + assets) | (cd ${D}${HELIX_DATA_DIR} && tar xf -) + + # --- writable user-config dir + default settings.json -------------------- + # Ship the cosmos preset as the initial settings.json. Marked CONFFILES so + # opkg upgrades leave user edits alone. The app writes runtime state + # (telemetry_*.json, tool_spools.json, custom_images/, etc.) into this same + # directory at runtime via $HELIX_CONFIG_DIR. + install -d ${D}${HELIX_USER_CONFIG_DIR} + if [ -f ${S}/assets/config/presets/cc1.json ]; then + install -m 0644 ${S}/assets/config/presets/cc1.json ${D}${HELIX_USER_CONFIG_DIR}/settings.json + fi + + # --- init script ------------------------------------------------------- + # The shipped helixscreen.init template has DAEMON_DIR / HELIX_CONFIG_DIR / + # HELIX_DATA_DIR placeholders that get rewritten here so the script matches + # this packaging's actual install paths. The same template is the one used + # for tarball installs — only the DAEMON_DIR / env paths differ. install -d ${D}${sysconfdir}/init.d install -m 0755 ${WORKDIR}/helixscreen.init ${D}${sysconfdir}/init.d/helixscreen - sed -i 's|^DAEMON_DIR=.*|DAEMON_DIR="${HELIX_DIR}"|' ${D}${sysconfdir}/init.d/helixscreen + sed -i \ + -e "s|@HELIX_BINDIR@|${bindir}|" \ + -e "s|@HELIX_DATA_DIR@|${HELIX_DATA_DIR}|" \ + -e "s|@HELIX_USER_CONFIG_DIR@|${HELIX_USER_CONFIG_DIR}|" \ + ${D}${sysconfdir}/init.d/helixscreen } FILES:${PN} = " \ - ${HELIX_DIR} \ ${bindir}/helix-screen \ + ${bindir}/helix-splash \ + ${bindir}/helix-watchdog \ + ${bindir}/helix-launcher.sh \ + ${HELIX_DATA_DIR} \ ${sysconfdir}/init.d/helixscreen \ + ${HELIX_USER_CONFIG_DIR} \ " -# Runtime deps — Klipper + Moonraker are the upstream ecosystem; without them -# helix-screen launches but has nothing to talk to. -RDEPENDS:${PN} = " \ - klipper \ - moonraker \ - wpa-supplicant \ -" +CONFFILES:${PN} = "${HELIX_USER_CONFIG_DIR}/settings.json" INITSCRIPT_NAME = "helixscreen" -# S80 places us after network / Moonraker. -INITSCRIPT_PARAMS = "start 80 2 3 4 5 . stop 20 0 1 6 ." +# Don't auto-start: cosmos uses gui-switcher (config-manager ui screen_ui) +# to pick which UI runs. See README in this layer for activation. +INITSCRIPT_PARAMS = "disable" + +# --- post-install: migrate state from a previous tarball install ------------ +# When opkg installs onto a system that already has the legacy +# /user-resource/helixscreen tarball install, copy the user's writable state +# into the new ${HELIX_USER_CONFIG_DIR}. Bundled seeds (printer_database.json, +# themes/defaults/, presets/, etc.) are NOT copied — those now live in +# ${HELIX_DATA_DIR}/assets/config/ and find_readable() falls back to them +# automatically when the user dir doesn't have a per-file override. +pkg_postinst:${PN}() { + legacy_dir=$D/user-resource/helixscreen/config + target_dir=$D${HELIX_USER_CONFIG_DIR} + + if [ -d "$legacy_dir" ] && [ ! -f "$target_dir/.migrated_from_tarball" ]; then + mkdir -p "$target_dir" + + # Writable user state — copy if present, never overwrite existing files + # in the new dir (so a prior opkg install's state wins over an even + # older tarball). + for f in settings.json helixconfig.json telemetry_config.json \ + telemetry_device.json telemetry_queue.json \ + telemetry_snapshot.json tool_spools.json \ + ace_slot_overrides.json pending_remap.json \ + helixscreen.env crash_history.json; do + if [ -f "$legacy_dir/$f" ] && [ ! -f "$target_dir/$f" ]; then + cp "$legacy_dir/$f" "$target_dir/$f" + fi + done + + # User-managed dirs + for d in custom_images printer_database.d themes; do + if [ -d "$legacy_dir/$d" ] && [ ! -d "$target_dir/$d" ]; then + cp -r "$legacy_dir/$d" "$target_dir/$d" + fi + done + + # Drop user themes/defaults — those are shipped now in + # ${HELIX_DATA_DIR}/assets/config/themes/defaults/ and will be + # found via find_readable's data_dir fallback. + rm -rf "$target_dir/themes/defaults" + + touch "$target_dir/.migrated_from_tarball" + fi +} diff --git a/meta-opencentauri/recipes-support/libhv/files/libhv-set-soversion.patch b/meta-opencentauri/recipes-support/libhv/files/libhv-set-soversion.patch new file mode 100644 index 00000000..a44fbf4c --- /dev/null +++ b/meta-opencentauri/recipes-support/libhv/files/libhv-set-soversion.patch @@ -0,0 +1,23 @@ +From: HelixScreen packaging +Subject: [PATCH] CMakeLists: set VERSION/SOVERSION on the shared lib target + +Without this, libhv's shared library gets installed as a plain "libhv.so" +with no DT_SONAME, which means downstream binaries linking against -lhv +end up with DT_NEEDED=libhv.so (the unversioned, dev-only name) rather +than DT_NEEDED=libhv.so.1. They then fail to find the library at runtime +when the dev package isn't installed. + +Setting VERSION + SOVERSION lets CMake produce the standard symlink chain +(libhv.so -> libhv.so.1 -> libhv.so.1.3.4) and stamp the correct DT_SONAME +into the ELF, so the runtime package alone is enough to satisfy the dep. + +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -253,6 +253,7 @@ if(BUILD_SHARED) + target_include_directories(hv PRIVATE ${LIBHV_SRCDIRS} + INTERFACE $ $) + target_link_libraries(hv ${LIBS}) ++ set_target_properties(hv PROPERTIES VERSION ${PROJECT_VERSION} SOVERSION 1) + install(TARGETS hv + EXPORT libhvConfig + ARCHIVE DESTINATION lib diff --git a/meta-opencentauri/recipes-support/libhv/libhv_1.3.4.bb b/meta-opencentauri/recipes-support/libhv/libhv_1.3.4.bb index f60006ee..40ce800e 100644 --- a/meta-opencentauri/recipes-support/libhv/libhv_1.3.4.bb +++ b/meta-opencentauri/recipes-support/libhv/libhv_1.3.4.bb @@ -7,6 +7,7 @@ LIC_FILES_CHKSUM = "file://LICENSE;md5=122a2f8324611e54381a1de69934481b" SRC_URI = "git://github.com/ithewei/libhv.git;protocol=https;branch=master \ file://libhv-dns-resolver-fallback.patch \ + file://libhv-set-soversion.patch \ " SRCREV = "71770e04becaa149e0ef8ffc4d3900c5466ddddb" @@ -32,22 +33,14 @@ EXTRA_OECMAKE = " \ " # libhv generates hv/json.hpp etc. into include/hv at configure time; -# cmake install copies them to ${includedir}/hv. Also ship the CMake -# package configuration so downstream cmake projects can find_package(hv). -FILES:${PN} += "${libdir}/cmake/hv" - -# Suppress "file /usr/include/hv/... not shipped" warnings by making -dev pick -# up all public headers (default behavior, but be explicit for clarity). -# libhv's CMakeLists doesn't set SOVERSION, so `make install` produces -# /usr/lib/libhv.so as a plain ELF rather than the usual -# libhv.so -> libhv.so.1 -> libhv.so.1.3.4 symlink chain. Rename it at install -# time to match the PROJECT_VERSION and create the expected symlinks so the -# shared object lands in the runtime package and -lhv still resolves at build -# time. -do_install:append() { - if [ -f ${D}${libdir}/libhv.so ] && [ ! -L ${D}${libdir}/libhv.so ]; then - mv ${D}${libdir}/libhv.so ${D}${libdir}/libhv.so.${PV} - ln -s libhv.so.${PV} ${D}${libdir}/libhv.so.1 - ln -s libhv.so.${PV} ${D}${libdir}/libhv.so - fi -} +# cmake install copies them to ${includedir}/hv. Headers and the CMake +# package config belong in -dev so downstream consumers can find_package(hv) +# at build time, while the runtime package only ships the .so. +FILES:${PN}-dev += "${libdir}/cmake/hv" + +# With libhv-set-soversion.patch applied, cmake produces the standard +# libhv.so -> libhv.so.1 -> libhv.so.1.3.4 symlink chain natively and stamps +# DT_SONAME=libhv.so.1 into the ELF. Default Yocto FILES split picks up the +# right pieces: +# runtime: libhv.so.1, libhv.so.${PV} +# -dev: libhv.so From 48e25b3535d7c701ea35418045285303bcfefe48 Mon Sep 17 00:00:00 2001 From: Preston Brown Date: Wed, 15 Apr 2026 21:58:14 -0400 Subject: [PATCH 3/4] fix(helixscreen): init script bugs + postinst settings.json ordering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Code review fixes: helixscreen.init: - Move platform_pre_start/platform_post_stop fallback no-op definitions BEFORE the source so hooks-cc1.sh can override them. Previously the unconditional redefinition after the source wiped whatever the hooks file defined. - 'return 1' → 'exit 1' in the status case — return outside a function is a syntax error in dash/ash (BusyBox). - Remove now-redundant 'type ... >/dev/null 2>&1 &&' guards since the fallback functions are always defined. helixscreen_0.99.bb pkg_postinst: - settings.json from the legacy tarball install now OVERWRITES the cc1 preset that do_install placed. The user's tarball settings contain their actual preferences and calibration; the shipped cc1.json default is only appropriate for fresh installs with no prior state. --- .../recipes-apps/helixscreen/files/helixscreen.init | 13 ++++++++----- .../recipes-apps/helixscreen/helixscreen_0.99.bb | 13 +++++++++---- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/meta-opencentauri/recipes-apps/helixscreen/files/helixscreen.init b/meta-opencentauri/recipes-apps/helixscreen/files/helixscreen.init index e1b0186a..b002d51f 100755 --- a/meta-opencentauri/recipes-apps/helixscreen/files/helixscreen.init +++ b/meta-opencentauri/recipes-apps/helixscreen/files/helixscreen.init @@ -19,6 +19,11 @@ PIDFILE=/var/run/gui.pid export HELIX_DATA_DIR HELIX_CONFIG_DIR +# Fallback no-ops — platform hooks override these if present. +# Must be defined BEFORE sourcing so the source overwrites them. +platform_pre_start() { :; } +platform_post_stop() { :; } + # Pull in cosmos-specific platform hooks (e.g. backlight, framebuffer # init). Hooks live alongside the read-only data root. PLATFORM_HOOKS="${HELIX_DATA_DIR}/assets/config/platform/hooks-cc1.sh" @@ -26,8 +31,6 @@ if [ -f "$PLATFORM_HOOKS" ]; then # shellcheck disable=SC1090 . "$PLATFORM_HOOKS" fi -platform_pre_start() { :; } 2>/dev/null -platform_post_stop() { :; } 2>/dev/null # Make sure the writable user-config dir exists. CONFFILES from the package # only creates settings.json; runtime state (telemetry_*.json, tool_spools.json, @@ -37,7 +40,7 @@ mkdir -p "$HELIX_CONFIG_DIR" case "$1" in start) echo "Starting HelixScreen..." - type platform_pre_start >/dev/null 2>&1 && platform_pre_start + platform_pre_start if [ -e /sys/class/graphics/fbcon/cursor_blink ]; then echo 0 > /sys/class/graphics/fbcon/cursor_blink fi @@ -51,7 +54,7 @@ case "$1" in echo "Stopping HelixScreen..." start-stop-daemon -K -p "$PIDFILE" -R 10 rm -f "$PIDFILE" - type platform_post_stop >/dev/null 2>&1 && platform_post_stop + platform_post_stop ;; restart|force-reload) $0 stop @@ -63,7 +66,7 @@ case "$1" in echo "HelixScreen is running (PID $(cat "$PIDFILE"))" else echo "HelixScreen is not running" - return 1 + exit 1 fi ;; *) diff --git a/meta-opencentauri/recipes-apps/helixscreen/helixscreen_0.99.bb b/meta-opencentauri/recipes-apps/helixscreen/helixscreen_0.99.bb index 5334e4f2..55c46363 100644 --- a/meta-opencentauri/recipes-apps/helixscreen/helixscreen_0.99.bb +++ b/meta-opencentauri/recipes-apps/helixscreen/helixscreen_0.99.bb @@ -158,10 +158,15 @@ pkg_postinst:${PN}() { if [ -d "$legacy_dir" ] && [ ! -f "$target_dir/.migrated_from_tarball" ]; then mkdir -p "$target_dir" - # Writable user state — copy if present, never overwrite existing files - # in the new dir (so a prior opkg install's state wins over an even - # older tarball). - for f in settings.json helixconfig.json telemetry_config.json \ + # Writable user state — copy if present. For settings.json specifically, + # OVERWRITE the cc1 preset that do_install placed — the user's tarball + # settings contain their actual preferences/calibration, which matter more + # than the shipped default. For everything else, don't overwrite (so a + # prior opkg install's state wins over an even older tarball). + if [ -f "$legacy_dir/settings.json" ]; then + cp "$legacy_dir/settings.json" "$target_dir/settings.json" + fi + for f in helixconfig.json telemetry_config.json \ telemetry_device.json telemetry_queue.json \ telemetry_snapshot.json tool_spools.json \ ace_slot_overrides.json pending_remap.json \ From a6aa526dd28bce6e7c58922471f10e31864f5b55 Mon Sep 17 00:00:00 2001 From: Preston Brown Date: Wed, 15 Apr 2026 22:00:02 -0400 Subject: [PATCH 4/4] fix(helixscreen): document gui.pid naming convention in init script --- .../recipes-apps/helixscreen/files/helixscreen.init | 2 ++ 1 file changed, 2 insertions(+) diff --git a/meta-opencentauri/recipes-apps/helixscreen/files/helixscreen.init b/meta-opencentauri/recipes-apps/helixscreen/files/helixscreen.init index b002d51f..0c0dfef3 100755 --- a/meta-opencentauri/recipes-apps/helixscreen/files/helixscreen.init +++ b/meta-opencentauri/recipes-apps/helixscreen/files/helixscreen.init @@ -15,6 +15,8 @@ HELIX_LAUNCHER="@HELIX_BINDIR@/helix-launcher.sh" HELIX_DATA_DIR="@HELIX_DATA_DIR@" HELIX_CONFIG_DIR="@HELIX_USER_CONFIG_DIR@" +# gui.pid (not helixscreen.pid): cosmos gui-switcher's stop handler reads +# /var/run/gui.pid to kill the active UI. Must match guppyscreen/grumpyscreen. PIDFILE=/var/run/gui.pid export HELIX_DATA_DIR HELIX_CONFIG_DIR