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..0c0dfef3 --- /dev/null +++ b/meta-opencentauri/recipes-apps/helixscreen/files/helixscreen.init @@ -0,0 +1,80 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-3.0-or-later +### 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@" + +# 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 + +# 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" +if [ -f "$PLATFORM_HOOKS" ]; then + # shellcheck disable=SC1090 + . "$PLATFORM_HOOKS" +fi + +# 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" + +case "$1" in + start) + echo "Starting HelixScreen..." + platform_pre_start + if [ -e /sys/class/graphics/fbcon/cursor_blink ]; then + echo 0 > /sys/class/graphics/fbcon/cursor_blink + fi + if [ -x "$HELIX_LAUNCHER" ]; then + start-stop-daemon -S -b -m -p "$PIDFILE" -x "$HELIX_LAUNCHER" + else + start-stop-daemon -S -b -m -p "$PIDFILE" -x "$HELIX_BIN" + fi + ;; + stop) + echo "Stopping HelixScreen..." + start-stop-daemon -K -p "$PIDFILE" -R 10 + rm -f "$PIDFILE" + platform_post_stop + ;; + restart|force-reload) + $0 stop + sleep 1 + $0 start + ;; + 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" + exit 1 + fi + ;; + *) + echo "Usage: $0 {start|stop|restart|status}" + exit 1 + ;; +esac + +exit 0 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..55c46363 --- /dev/null +++ b/meta-opencentauri/recipes-apps/helixscreen/helixscreen_0.99.bb @@ -0,0 +1,193 @@ +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" +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 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" + +inherit pkgconfig update-rc.d + +DEPENDS = " \ + libhv \ + openssl \ + spdlog \ + fmt \ + alsa-lib \ + libusb1 \ + wpa-supplicant \ + libnl \ + zlib \ +" + +# 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}' \ + CXX='${CXX}' \ + AR='${AR}' \ + LD='${LD}' \ + STRIP='${STRIP}' \ + RANLIB='${RANLIB}' \ + OBJCOPY='${OBJCOPY}' \ +" + +# 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() { + # --- 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 + if [ -x ${B}/build/yocto/bin/$exe ]; then + install -m 0755 ${B}/build/yocto/bin/$exe ${D}${bindir}/$exe + fi + done + 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 \ + -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} = " \ + ${bindir}/helix-screen \ + ${bindir}/helix-splash \ + ${bindir}/helix-watchdog \ + ${bindir}/helix-launcher.sh \ + ${HELIX_DATA_DIR} \ + ${sysconfdir}/init.d/helixscreen \ + ${HELIX_USER_CONFIG_DIR} \ +" + +CONFFILES:${PN} = "${HELIX_USER_CONFIG_DIR}/settings.json" + +INITSCRIPT_NAME = "helixscreen" +# 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. 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 \ + 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-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/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 new file mode 100644 index 00000000..40ce800e --- /dev/null +++ b/meta-opencentauri/recipes-support/libhv/libhv_1.3.4.bb @@ -0,0 +1,46 @@ +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 \ + file://libhv-set-soversion.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. 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