diff --git a/.github/workflows/build-boot.yml b/.github/workflows/build-boot.yml index 26b62920c..6c3a058c2 100644 --- a/.github/workflows/build-boot.yml +++ b/.github/workflows/build-boot.yml @@ -17,6 +17,7 @@ jobs: matrix: defconfig: - aarch64_qemu_boot + - bpi_m1_boot - bpi_r3_sd_boot - bpi_r3_emmc_boot - bpi_r4_sd_boot diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index d13fe1415..8c0625200 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -10,6 +10,7 @@ on: options: - raspberrypi-rpi2 - raspberrypi-rpi64 + - bananapi-bpi-m1 - bananapi-bpi-r3 - bananapi-bpi-r4 - bananapi-bpi-r64 @@ -69,6 +70,11 @@ jobs: targets="sdcard" bootloader_sdcard=rpi64-boot ;; + bananapi-bpi-m1) + arch=arm + targets="sdcard" + bootloader_sdcard=bpi-m1-boot + ;; bananapi-bpi-r3) arch=aarch64 targets="sdcard emmc" diff --git a/board/arm/Config.in b/board/arm/Config.in index 7d30b9695..d465cee1f 100644 --- a/board/arm/Config.in +++ b/board/arm/Config.in @@ -1,5 +1,6 @@ if BR2_arm +source "$BR2_EXTERNAL_INFIX_PATH/board/arm/bananapi-bpi-m1/Config.in" source "$BR2_EXTERNAL_INFIX_PATH/board/arm/microchip-sama7g54-ek/Config.in" source "$BR2_EXTERNAL_INFIX_PATH/board/arm/raspberrypi-rpi2/Config.in" diff --git a/board/arm/README.md b/board/arm/README.md index 6058d7ee3..0768b6bef 100644 --- a/board/arm/README.md +++ b/board/arm/README.md @@ -4,5 +4,6 @@ Arm 32-bit Board Specific Documentation ---------------------------- +- [Banana Pi BPI-M1 (32-bit)](bananapi-bpi-m1/) - [Microchip SAMA7G54-EK (32-bit)](microchip-sama7g54-ek/) - [Raspberry Pi 2 Model B (32-bit)](raspberrypi-rpi2/) diff --git a/board/arm/bananapi-bpi-m1/Config.in b/board/arm/bananapi-bpi-m1/Config.in new file mode 100644 index 000000000..e9545ba29 --- /dev/null +++ b/board/arm/bananapi-bpi-m1/Config.in @@ -0,0 +1,9 @@ +config BR2_PACKAGE_BANANAPI_BPI_M1 + bool "Banana Pi BPI-M1" + depends on BR2_arm + select SDCARD_AUX + select BR2_PACKAGE_INPUT_EVENT_DAEMON + help + Support for the Banana Pi BPI-M1 single-board computer, the + first Banana Pi, with an Allwinner A20 dual-core Cortex-A7, + 1 GiB DDR3, Gigabit Ethernet, SATA and an SD card slot. diff --git a/board/arm/bananapi-bpi-m1/LICENSE b/board/arm/bananapi-bpi-m1/LICENSE new file mode 100644 index 000000000..8cdb30a3a --- /dev/null +++ b/board/arm/bananapi-bpi-m1/LICENSE @@ -0,0 +1,13 @@ +Copyright (c) 2026 The KernelKit Authors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/board/arm/bananapi-bpi-m1/README.md b/board/arm/bananapi-bpi-m1/README.md new file mode 100644 index 000000000..f9e383566 --- /dev/null +++ b/board/arm/bananapi-bpi-m1/README.md @@ -0,0 +1,114 @@ +Banana Pi BPI-M1 +================ + +The [BPI-M1][1] is the first Banana Pi board, powered by the Allwinner +A20 dual-core Cortex-A7 @ 1 GHz with 1 GB DDR3 RAM. + +The board features: + +- Gigabit Ethernet +- SD card slot (full size) +- SATA 2.0 port +- 2x USB 2.0 host, 1x micro USB OTG +- HDMI, composite video and audio out +- 26-pin GPIO header + +> [!NOTE] +> The BPI-M1+ is a different board with its own device tree and is not +> covered by this BSP. + +How to Build +------------ + +There are no pre-built images for ARM 32-bit, so build both Infix and +the bootloader from source. + +1. Build the bootloader + + make O=x-boot-bpi-m1 bpi_m1_boot_defconfig + make O=x-boot-bpi-m1 + +2. Build Infix + + make O=x-arm arm_defconfig + make O=x-arm + +3. Create the SD card image + + ./utils/mkimage.sh -b x-boot-bpi-m1 -r x-arm bananapi-bpi-m1 + +To test only the bootloader, e.g., to verify the console, create a +boot-only image after step 1: + + ./utils/mkimage.sh -B -b x-boot-bpi-m1 bananapi-bpi-m1 + +Flashing to SD Card +------------------- + +[Flash the image][0] to an SD card (at least 2 GB): + +```bash +sudo bmaptool copy x-boot-bpi-m1/images/infix-bpi-m1-sdcard.img /dev/sdX +``` + +> [!WARNING] +> Ensure `/dev/sdX` is the correct device for your SD card and not used +> by the host system! Use `lsblk` to verify. + +Booting the Board +----------------- + +1. Insert the flashed SD card +2. Connect an Ethernet cable +3. Power up the board, 5V DC via the micro USB port +4. Find the assigned IP and SSH in, default login: `admin` / `admin` + +The board has no user button, so U-Boot always runs in developer mode. +Use `bootmenu` on the console for factory reset. + +LEDs and Buttons +---------------- + +| **Stage** | **USR** (green) | +|----------------|-----------------| +| U-Boot | on | +| System loading | 1 Hz | +| System up | on | +| Locate | 10 Hz | +| Fail safe | 5 Hz | +| Panic | 5 Hz | + +The Ethernet port LEDs show link and activity. + +The power button powers on the board, and a short press when running +powers it off. The reset button is a hardware reset. + +Unsupported Features +-------------------- + +The following hardware has no driver enabled in Infix: + +- Analog audio and composite video out +- HDMI audio, not supported by mainline Linux on the A20 +- IR receiver +- Micro USB OTG port +- Mali-400 GPU and video decoder + +Console Port +------------ + +UART pins + +The debug console is UART0, TX and RX on header J11 and GND on the +neighboring header J12: + +- Baud rate: 115200 +- Data bits: 8 +- Parity: None +- Stop bits: 1 + +> [!WARNING] +> Use only 3.3V serial adapters. + +[0]: https://www.kernelkit.org/posts/flashing-sdcard/ +[1]: https://docs.banana-pi.org/en/BPI-M1/BananaPi_BPI-M1 diff --git a/board/arm/bananapi-bpi-m1/bananapi-bpi-m1.hash b/board/arm/bananapi-bpi-m1/bananapi-bpi-m1.hash new file mode 100644 index 000000000..2b1e74432 --- /dev/null +++ b/board/arm/bananapi-bpi-m1/bananapi-bpi-m1.hash @@ -0,0 +1,2 @@ +# Locally calculated +sha256 d48246c717b505cc11df95171f2fd548b389e1a463f1af4c68d0b69fe0d1009b LICENSE diff --git a/board/arm/bananapi-bpi-m1/bananapi-bpi-m1.mk b/board/arm/bananapi-bpi-m1/bananapi-bpi-m1.mk new file mode 100644 index 000000000..7259b6fa7 --- /dev/null +++ b/board/arm/bananapi-bpi-m1/bananapi-bpi-m1.mk @@ -0,0 +1,84 @@ +# Banana Pi BPI-M1 kernel configuration fixups +define BANANAPI_BPI_M1_LINUX_CONFIG_FIXUPS + # Allwinner A20 SoC + $(call KCONFIG_ENABLE_OPT,CONFIG_ARCH_SUNXI) + $(call KCONFIG_ENABLE_OPT,CONFIG_MACH_SUN7I) + $(call KCONFIG_ENABLE_OPT,CONFIG_SUNXI_SRAM) + + # Serial console + $(call KCONFIG_ENABLE_OPT,CONFIG_SERIAL_8250) + $(call KCONFIG_ENABLE_OPT,CONFIG_SERIAL_8250_CONSOLE) + $(call KCONFIG_ENABLE_OPT,CONFIG_SERIAL_8250_DW) + + # Network: GMAC with Realtek PHY + $(call KCONFIG_ENABLE_OPT,CONFIG_NET_VENDOR_STMICRO) + $(call KCONFIG_ENABLE_OPT,CONFIG_STMMAC_ETH) + $(call KCONFIG_ENABLE_OPT,CONFIG_STMMAC_PLATFORM) + $(call KCONFIG_ENABLE_OPT,CONFIG_DWMAC_SUNXI) + $(call KCONFIG_ENABLE_OPT,CONFIG_REALTEK_PHY) + + # MMC/SD and SATA + $(call KCONFIG_ENABLE_OPT,CONFIG_MMC_SUNXI) + $(call KCONFIG_ENABLE_OPT,CONFIG_AHCI_SUNXI) + + # AXP209 PMIC, supplies the CPU and USB VBUS regulators + $(call KCONFIG_ENABLE_OPT,CONFIG_I2C) + $(call KCONFIG_ENABLE_OPT,CONFIG_I2C_MV64XXX) + $(call KCONFIG_ENABLE_OPT,CONFIG_MFD_AXP20X_I2C) + $(call KCONFIG_ENABLE_OPT,CONFIG_REGULATOR_AXP20X) + + # USB Host, the PHY depends on POWER_SUPPLY, with keyboard and mouse + $(call KCONFIG_ENABLE_OPT,CONFIG_POWER_SUPPLY) + $(call KCONFIG_ENABLE_OPT,CONFIG_PHY_SUN4I_USB) + $(call KCONFIG_SET_OPT,CONFIG_USB_EHCI_HCD,m) + $(call KCONFIG_SET_OPT,CONFIG_USB_EHCI_HCD_PLATFORM,m) + $(call KCONFIG_SET_OPT,CONFIG_USB_OHCI_HCD,m) + $(call KCONFIG_SET_OPT,CONFIG_USB_OHCI_HCD_PLATFORM,m) + $(call KCONFIG_ENABLE_OPT,CONFIG_USB_HID) + $(call KCONFIG_SET_OPT,CONFIG_HID_GENERIC,m) + + # Power button on the AXP209, KEY_POWER for input-event-daemon + $(call KCONFIG_ENABLE_OPT,CONFIG_INPUT_MISC) + $(call KCONFIG_ENABLE_OPT,CONFIG_INPUT_AXP20X_PEK) + + # SPI header + $(call KCONFIG_ENABLE_OPT,CONFIG_SPI_SUN4I) + + # Temperature: SoC sensor in the touchscreen controller, used by + # the cpu-thermal zone to throttle, and AXP209 die temp over IIO + $(call KCONFIG_ENABLE_OPT,CONFIG_TOUCHSCREEN_SUN4I) + $(call KCONFIG_ENABLE_OPT,CONFIG_CPU_THERMAL) + $(call KCONFIG_ENABLE_OPT,CONFIG_IIO) + $(call KCONFIG_ENABLE_OPT,CONFIG_AXP20X_ADC) + $(call KCONFIG_ENABLE_OPT,CONFIG_SENSORS_IIO_HWMON) + + # LEDs: green user LED on GPIO, RTL8211E PHY LEDs offloaded to + # the PHY with the netdev trigger + $(call KCONFIG_ENABLE_OPT,CONFIG_LEDS_GPIO) + $(call KCONFIG_ENABLE_OPT,CONFIG_LEDS_TRIGGER_NETDEV) + + # HDMI output with framebuffer console, no HDMI audio in mainline + $(call KCONFIG_ENABLE_OPT,CONFIG_DRM) + $(call KCONFIG_SET_OPT,CONFIG_DRM_SUN4I,m) + $(call KCONFIG_SET_OPT,CONFIG_DRM_SUN4I_HDMI,m) + $(call KCONFIG_ENABLE_OPT,CONFIG_DRM_SUN4I_HDMI_CEC) + $(call KCONFIG_SET_OPT,CONFIG_DRM_SUN4I_BACKEND,m) + $(call KCONFIG_ENABLE_OPT,CONFIG_DRM_FBDEV_EMULATION) + $(call KCONFIG_ENABLE_OPT,CONFIG_FB) + $(call KCONFIG_ENABLE_OPT,CONFIG_FRAMEBUFFER_CONSOLE) + + # Watchdog, RTC and SID (unique chip ID) + $(call KCONFIG_ENABLE_OPT,CONFIG_WATCHDOG) + $(call KCONFIG_ENABLE_OPT,CONFIG_SUNXI_WATCHDOG) + $(call KCONFIG_ENABLE_OPT,CONFIG_RTC_CLASS) + $(call KCONFIG_ENABLE_OPT,CONFIG_RTC_DRV_SUNXI) + $(call KCONFIG_ENABLE_OPT,CONFIG_NVMEM) + $(call KCONFIG_ENABLE_OPT,CONFIG_NVMEM_SUNXI_SID) + + # Crypto hardware acceleration + $(call KCONFIG_ENABLE_OPT,CONFIG_CRYPTO_DEV_ALLWINNER) + $(call KCONFIG_ENABLE_OPT,CONFIG_CRYPTO_DEV_SUN4I_SS) +endef + +$(eval $(ix-board)) +$(eval $(generic-package)) diff --git a/board/arm/bananapi-bpi-m1/dts/Makefile b/board/arm/bananapi-bpi-m1/dts/Makefile new file mode 100644 index 000000000..eeac3f151 --- /dev/null +++ b/board/arm/bananapi-bpi-m1/dts/Makefile @@ -0,0 +1 @@ +dtb-y += allwinner/sun7i-a20-bananapi.dtb diff --git a/board/arm/bananapi-bpi-m1/dts/allwinner/infix.dtsi b/board/arm/bananapi-bpi-m1/dts/allwinner/infix.dtsi new file mode 100644 index 000000000..16cfafc86 --- /dev/null +++ b/board/arm/bananapi-bpi-m1/dts/allwinner/infix.dtsi @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Common Infix OS defaults + */ + +/ { + chosen { + infix { + /* Default admin user password: 'admin' */ + factory-password-hash = "$5$mI/zpOAqZYKLC2WU$i7iPzZiIjOjrBF3NyftS9CCq8dfYwHwrmUK097Jca9A"; + }; + }; +}; diff --git a/board/arm/bananapi-bpi-m1/dts/allwinner/sun7i-a20-bananapi.dts b/board/arm/bananapi-bpi-m1/dts/allwinner/sun7i-a20-bananapi.dts new file mode 100644 index 000000000..662940942 --- /dev/null +++ b/board/arm/bananapi-bpi-m1/dts/allwinner/sun7i-a20-bananapi.dts @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Infix OS device tree for Banana Pi BPI-M1 + */ + +#include +#include "infix.dtsi" + +/* Only used by arch/arm topology to compare cores, silences boot errors */ +&cpu0 { + clock-frequency = <960000000>; +}; + +&cpu1 { + clock-frequency = <960000000>; +}; + +/* Keep the LED lit by U-Boot until iitod takes over */ +&{/leds/led} { + default-state = "keep"; +}; diff --git a/board/arm/bananapi-bpi-m1/genimage.cfg.in b/board/arm/bananapi-bpi-m1/genimage.cfg.in new file mode 100644 index 000000000..b3a21092f --- /dev/null +++ b/board/arm/bananapi-bpi-m1/genimage.cfg.in @@ -0,0 +1,78 @@ +# Disk image for Banana Pi BPI-M1 (Allwinner A20) + +image cfg.ext4 { + empty = true + temporary = true + size = 16M + + ext4 { + label = "cfg" + use-mke2fs = true + features = "uninit_bg" + extraargs = "-m 0 -i 4096" + } +} + +# The /var partition will be expanded automatically at first boot +# to use the full size of the SD-card or eMMC media. +image var.ext4 { + empty = true + temporary = true + size = 128M + + ext4 { + label = "var" + use-mke2fs = true + features = "uninit_bg" + extraargs = "-m 0 -i 4096" + } +} + +image #IX_ID##VERSION#-bpi-m1-#TARGET#.img { + hdimage { + partition-table-type = "gpt" + # The A20 boot ROM loads the SPL from 8 KiB, move the + # GPT partition entries out of its way + gpt-location = 1M + } + + partition u-boot { + in-partition-table = "no" + image = "u-boot-sunxi-with-spl.bin" + offset = 8K + size = 1016K + } + + partition aux { + partition-uuid = D4EF35A0-0652-45A1-B3DE-D63339C82035 + image = "aux.ext4" + offset = 2M + } + + partition primary { + partition-type-uuid = 0FC63DAF-8483-4772-8E79-3D69D8477DE4 + bootable = true + size = 250M + image = "rootfs.squashfs" + } + + partition secondary { + partition-type-uuid = 0FC63DAF-8483-4772-8E79-3D69D8477DE4 + bootable = true + size = 250M + image = "rootfs.squashfs" + } + + partition cfg { + partition-uuid = 7aa497f0-73b5-47e5-b2ab-8752d8a48105 + image = "cfg.ext4" + } + + partition var { + partition-uuid = 8046A06A-E45A-4A14-A6AD-6684704A393F + image = "var.ext4" + } +} + +# Silence genimage warnings +config {} diff --git a/board/arm/bananapi-bpi-m1/rootfs/usr/share/product/lemaker,bananapi/etc/factory-config.cfg b/board/arm/bananapi-bpi-m1/rootfs/usr/share/product/lemaker,bananapi/etc/factory-config.cfg new file mode 100644 index 000000000..e39e37c1d --- /dev/null +++ b/board/arm/bananapi-bpi-m1/rootfs/usr/share/product/lemaker,bananapi/etc/factory-config.cfg @@ -0,0 +1,311 @@ +{ + "ieee802-dot1ab-lldp:lldp": { + "infix-lldp:enabled": true + }, + "ietf-hardware:hardware": { + "component": [ + { + "name": "USB", + "class": "infix-hardware:usb", + "state": { + "admin-state": "unlocked" + } + } + ] + }, + "ietf-interfaces:interfaces": { + "interface": [ + { + "name": "lo", + "type": "infix-if-type:loopback", + "ietf-ip:ipv4": { + "address": [ + { + "ip": "127.0.0.1", + "prefix-length": 8 + } + ] + }, + "ietf-ip:ipv6": { + "address": [ + { + "ip": "::1", + "prefix-length": 128 + } + ] + } + }, + { + "name": "eth0", + "type": "infix-if-type:ethernet", + "ietf-ip:ipv4": { + "infix-dhcp-client:dhcp": { + "option": [ + { + "id": "netmask" + }, + { + "id": "broadcast" + }, + { + "id": "router" + }, + { + "id": "domain" + }, + { + "id": "hostname", + "value": "auto" + }, + { + "id": "dns-server" + }, + { + "id": "ntp-server" + }, + { + "id": "vendor-class", + "value": "Banana Pi BPI-M1" + } + ] + } + }, + "ietf-ip:ipv6": { + "infix-dhcpv6-client:dhcp": { + "option": [ + { + "id": "ntp-server" + }, + { + "id": "client-fqdn" + }, + { + "id": "domain-search" + }, + { + "id": "dns-server" + } + ] + } + } + } + ] + }, + "ietf-keystore:keystore": { + "asymmetric-keys": { + "asymmetric-key": [ + { + "name": "gencert", + "public-key-format": "infix-crypto-types:x509-public-key-format", + "public-key": "", + "private-key-format": "infix-crypto-types:rsa-private-key-format", + "cleartext-private-key": "", + "certificates": {} + }, + { + "name": "genkey", + "public-key-format": "infix-crypto-types:ssh-public-key-format", + "public-key": "", + "private-key-format": "infix-crypto-types:rsa-private-key-format", + "cleartext-private-key": "", + "certificates": {} + } + ] + } + }, + "ietf-netconf-acm:nacm": { + "enable-nacm": true, + "read-default": "permit", + "write-default": "permit", + "exec-default": "permit", + "groups": { + "group": [ + { + "name": "admin", + "user-name": [ + "admin" + ] + }, + { + "name": "operator", + "user-name": [] + }, + { + "name": "guest", + "user-name": [] + } + ] + }, + "rule-list": [ + { + "name": "admin-acl", + "group": [ + "admin" + ], + "rule": [ + { + "name": "permit-all", + "module-name": "*", + "access-operations": "*", + "action": "permit", + "comment": "Allow 'admin' group complete access to all operations and data." + } + ] + }, + { + "name": "operator-acl", + "group": [ + "operator" + ], + "rule": [ + { + "name": "permit-system-rpcs", + "module-name": "ietf-system", + "rpc-name": "*", + "access-operations": "exec", + "action": "permit", + "comment": "Operators can reboot, shutdown, and set system time." + } + ] + }, + { + "name": "guest-acl", + "group": [ + "guest" + ], + "rule": [ + { + "name": "deny-all-write+exec", + "module-name": "*", + "access-operations": "create update delete exec", + "action": "deny", + "comment": "Guests cannot change anything or exec rpcs." + } + ] + }, + { + "name": "default-deny-all", + "group": [ + "*" + ], + "rule": [ + { + "name": "deny-password-access", + "path": "/ietf-system:system/authentication/user/password", + "access-operations": "*", + "action": "deny", + "comment": "No user except admins can access password hashes." + }, + { + "name": "deny-keystore-access", + "module-name": "ietf-keystore", + "access-operations": "*", + "action": "deny", + "comment": "No user except admins can access cryptographic keys." + }, + { + "name": "deny-truststore-access", + "module-name": "ietf-truststore", + "access-operations": "*", + "action": "deny", + "comment": "No user except admins can access trust store." + } + ] + } + ] + }, + "ietf-system:system": { + "hostname": "bpi-%m", + "infix-system:software": { + "update-url": "https://github.com/kernelkit/infix/releases.atom" + }, + "infix-schedule:schedules": { + "schedule": [ + { + "name": "nightly", + "description": "Every night at 03:00", + "recurrence": { + "frequency": "ietf-schedule:daily", + "byhour": [ + 3 + ] + } + }, + { + "name": "weekly", + "description": "Sunday nights at 03:00", + "recurrence": { + "frequency": "ietf-schedule:weekly", + "byday": [ + { + "weekday": "sunday" + } + ], + "byhour": [ + 3 + ] + } + } + ] + }, + "ntp": { + "enabled": true, + "server": [ + { + "name": "ntp.org", + "udp": { + "address": "pool.ntp.org" + }, + "iburst": true + } + ] + }, + "authentication": { + "user": [ + { + "name": "admin", + "password": "$factory$", + "infix-system:shell": "bash" + } + ] + }, + "infix-system:motd-banner": "Li0tLS0tLS0uCnwgIC4gLiAgfCBJbmZpeCBPUyDigJQgSW1tdXRhYmxlLkZyaWVuZGx5LlNlY3VyZQp8LS4gdiAuLXwgaHR0cHM6Ly9rZXJuZWxraXQub3JnCictJy0tLSctJwo=" + }, + "infix-meta:meta": { + "version": "1.10" + }, + "infix-services:mdns": { + "enabled": true + }, + "infix-services:web": { + "enabled": true, + "certificate": "gencert", + "console": { + "enabled": true + }, + "netbrowse": { + "enabled": true + }, + "restconf": { + "enabled": true + } + }, + "infix-services:ssh": { + "enabled": true, + "hostkey": [ + "genkey" + ], + "listen": [ + { + "name": "ipv4", + "address": "0.0.0.0", + "port": 22 + }, + { + "name": "ipv6", + "address": "::", + "port": 22 + } + ] + } +} diff --git a/board/arm/bananapi-bpi-m1/rootfs/usr/share/product/lemaker,bananapi/etc/finit.d/enabled/input-event-daemon.conf b/board/arm/bananapi-bpi-m1/rootfs/usr/share/product/lemaker,bananapi/etc/finit.d/enabled/input-event-daemon.conf new file mode 120000 index 000000000..8238fd3b4 --- /dev/null +++ b/board/arm/bananapi-bpi-m1/rootfs/usr/share/product/lemaker,bananapi/etc/finit.d/enabled/input-event-daemon.conf @@ -0,0 +1 @@ +../available/input-event-daemon.conf \ No newline at end of file diff --git a/board/arm/bananapi-bpi-m1/rootfs/usr/share/product/lemaker,bananapi/etc/iitod.json b/board/arm/bananapi-bpi-m1/rootfs/usr/share/product/lemaker,bananapi/etc/iitod.json new file mode 100644 index 000000000..287630ac2 --- /dev/null +++ b/board/arm/bananapi-bpi-m1/rootfs/usr/share/product/lemaker,bananapi/etc/iitod.json @@ -0,0 +1,129 @@ +{ + "input": { + "path": { + "locate": { + "path": "/run/led/locate" + }, + "status-prime": { + "path": "/run/led/status-prime" + }, + "status-ok": { + "path": "/run/led/status-ok" + }, + "status-err": { + "path": "/run/led/status-err" + }, + "status-crit": { + "path": "/run/led/status-crit" + }, + "fault-prime": { + "path": "/run/led/fault-prime" + }, + "fault-ok": { + "path": "/run/led/fault-ok" + }, + "fault-err": { + "path": "/run/led/fault-err" + }, + "fault-crit": { + "path": "/run/led/fault-crit" + }, + "startup": { + "path": "/run/finit/cond/usr/startup-config-ok" + }, + "fail-safe": { + "path": "/run/finit/cond/usr/failure-config-ok" + }, + "panic": { + "path": "/run/finit/cond/usr/failure-config-error" + } + }, + "udev": { + "power-a": { + "subsystem": "power_supply" + }, + "power-b": { + "subsystem": "power_supply" + } + } + }, + "output": { + "led-group": { + "port-link-act": { + "match": [ + "*:green:tp", + "*:green:sfp", + "*:green:port" + ], + "rules": [ + { + "if": "true", + "then": { + "trigger": "netdev", + "link": 1, + "rx": 1, + "tx": 1 + } + } + ] + }, + "port-alarm": { + "match": [ + "*:yellow:tp", + "*:yellow:sfp", + "*:yellow:port" + ], + "rules": [] + } + }, + "led": { + "bananapi:green:usr": { + "rules": [ + { + "if": "locate", + "then": "@blink-10hz" + }, + { + "if": "panic", + "then": "@blink-5hz" + }, + { + "if": "fail-safe", + "then": "@blink-5hz" + }, + { + "if": "startup", + "then": "@on" + }, + { + "if": "true", + "then": "@blink-1hz" + } + ] + } + } + }, + "aliases": { + "on": { + "brightness": true + }, + "off": { + "brightness": false + }, + "blink-1hz": { + "trigger": "timer", + "delay_on": 500, + "delay_off": 500 + }, + "blink-5hz": { + "trigger": "timer", + "delay_on": 100, + "delay_off": 100 + }, + "blink-10hz": { + "trigger": "timer", + "delay_on": 50, + "delay_off": 50 + } + } +} diff --git a/board/arm/bananapi-bpi-m1/rootfs/usr/share/product/lemaker,bananapi/etc/input-event-daemon.conf b/board/arm/bananapi-bpi-m1/rootfs/usr/share/product/lemaker,bananapi/etc/input-event-daemon.conf new file mode 100644 index 000000000..eb969fb68 --- /dev/null +++ b/board/arm/bananapi-bpi-m1/rootfs/usr/share/product/lemaker,bananapi/etc/input-event-daemon.conf @@ -0,0 +1,5 @@ +[Global] +listen = /dev/input/power-key + +[Keys] +POWER = poweroff diff --git a/board/arm/bananapi-bpi-m1/rootfs/usr/share/product/lemaker,bananapi/etc/udev/rules.d/70-power-key.rules b/board/arm/bananapi-bpi-m1/rootfs/usr/share/product/lemaker,bananapi/etc/udev/rules.d/70-power-key.rules new file mode 100644 index 000000000..9d1f1b10d --- /dev/null +++ b/board/arm/bananapi-bpi-m1/rootfs/usr/share/product/lemaker,bananapi/etc/udev/rules.d/70-power-key.rules @@ -0,0 +1 @@ +ACTION=="add", SUBSYSTEM=="input", KERNEL=="event*", ATTRS{name}=="axp20x-pek", SYMLINK+="input/power-key" diff --git a/board/arm/bananapi-bpi-m1/uart-pins.png b/board/arm/bananapi-bpi-m1/uart-pins.png new file mode 100644 index 000000000..e2e5b5304 Binary files /dev/null and b/board/arm/bananapi-bpi-m1/uart-pins.png differ diff --git a/board/arm/bananapi-bpi-m1/uboot/bpi-m1-env.dtsi b/board/arm/bananapi-bpi-m1/uboot/bpi-m1-env.dtsi new file mode 100644 index 000000000..02328336b --- /dev/null +++ b/board/arm/bananapi-bpi-m1/uboot/bpi-m1-env.dtsi @@ -0,0 +1,25 @@ +/ { + config { + environment { + vendor = "Banana Pi"; + preboot = "led bananapi:green:usr on; run ixpreboot"; + boot_targets = "mmc0"; + ethprime = "eth0"; + + /* The sunxi board code resets fdtfile after the environment + * is imported, without the vendor directory on 32-bit Arm */ + ixvariant = "setenv fdtfile allwinner/sun7i-a20-bananapi.dtb"; + + /* 1 GiB DRAM at 0x40000000, the squashfs is loaded whole + * at ramdisk_addr_r so keep that last with room above it */ + fdt_addr_r = "0x42000000"; + kernel_addr_r = "0x43000000"; + scriptaddr = "0x48000000"; + ramdisk_addr_r = "0x49000000"; + + /* No user button, this is a development platform */ + ixbtn-devmode = "setenv dev_mode yes; echo Enabled"; + ixbtn-factory = "echo \"No button, use bootmenu\""; + }; + }; +}; diff --git a/board/arm/bananapi-bpi-m1/uboot/extras.config b/board/arm/bananapi-bpi-m1/uboot/extras.config new file mode 100644 index 000000000..e5d221816 --- /dev/null +++ b/board/arm/bananapi-bpi-m1/uboot/extras.config @@ -0,0 +1,6 @@ +CONFIG_DEVICE_TREE_INCLUDES="infix-env.dtsi infix-key.dtsi bpi-m1-env.dtsi" +CONFIG_SYS_PROMPT="(bpi-m1) " + +# CONFIG_ENV_IS_IN_FAT is not set +CONFIG_LED=y +CONFIG_LED_GPIO=y diff --git a/board/arm/linux_defconfig b/board/arm/linux_defconfig index 3c0639ad7..20ff6ac33 100644 --- a/board/arm/linux_defconfig +++ b/board/arm/linux_defconfig @@ -306,6 +306,7 @@ CONFIG_SERIAL_DEV_BUS=y CONFIG_VIRTIO_CONSOLE=y CONFIG_I2C_CHARDEV=y CONFIG_SPI=y +CONFIG_GPIOLIB=y CONFIG_THERMAL=y CONFIG_WATCHDOG=y CONFIG_I6300ESB_WDT=y diff --git a/board/common/uboot/extras.config b/board/common/uboot/extras.config index 0037106a7..d88e46ed6 100644 --- a/board/common/uboot/extras.config +++ b/board/common/uboot/extras.config @@ -19,7 +19,6 @@ CONFIG_CMD_SETEXPR_FMT=y CONFIG_MMC=y -CONFIG_MMC_SDHCI=y CONFIG_CMD_MMC=y CONFIG_FS_SQUASHFS=y diff --git a/configs/arm_defconfig b/configs/arm_defconfig index a48ad1138..c1949506d 100644 --- a/configs/arm_defconfig +++ b/configs/arm_defconfig @@ -10,6 +10,7 @@ BR2_CCACHE_DIR="${BR2_EXTERNAL_INFIX_PATH}/.ccache" BR2_ENABLE_DEBUG=y BR2_GLOBAL_PATCH_DIR="${BR2_EXTERNAL_INFIX_PATH}/patches" BR2_DOWNLOAD_FORCE_CHECK_HASHES=y +BR2_TIME_BITS_64=y BR2_TARGET_GENERIC_HOSTNAME="infix" BR2_TARGET_GENERIC_ISSUE="Infix by KernelKit" BR2_INIT_FINIT=y @@ -33,6 +34,7 @@ BR2_LINUX_KERNEL_CUSTOM_CONFIG_FILE="${BR2_EXTERNAL_INFIX_PATH}/board/arm/linux_ BR2_LINUX_KERNEL_INSTALL_TARGET=y BR2_LINUX_KERNEL_NEEDS_HOST_OPENSSL=y BR2_PACKAGE_BUSYBOX_CONFIG="${BR2_EXTERNAL_INFIX_PATH}/board/common/busybox_defconfig" +BR2_PACKAGE_BUSYBOX_SHOW_OTHERS=y BR2_PACKAGE_STRACE=y BR2_PACKAGE_STRESS_NG=y BR2_PACKAGE_JQ=y @@ -68,7 +70,6 @@ BR2_PACKAGE_CONNTRACK_TOOLS=y BR2_PACKAGE_DNSMASQ=y BR2_PACKAGE_ETHTOOL=y BR2_PACKAGE_FPING=y -BR2_PACKAGE_FRR=y # BR2_PACKAGE_IFUPDOWN_SCRIPTS is not set BR2_PACKAGE_IPERF3=y BR2_PACKAGE_IPROUTE2=y @@ -98,9 +99,6 @@ BR2_PACKAGE_TRACEROUTE=y BR2_PACKAGE_ULOGD=y BR2_PACKAGE_WHOIS=y BR2_PACKAGE_WIREGUARD_TOOLS=y -BR2_PACKAGE_BASH=y -BR2_PACKAGE_BASH_COMPLETION=y -BR2_PACKAGE_NEOFETCH=y BR2_PACKAGE_SUDO=y BR2_PACKAGE_TTYD=y BR2_PACKAGE_GETENT=y @@ -137,6 +135,7 @@ BR2_PACKAGE_HOST_MTOOLS=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y +BR2_PACKAGE_BANANAPI_BPI_M1=y BR2_PACKAGE_MICROCHIP_SAMA7G54_EK=y BR2_PACKAGE_RASPBERRYPI_RPI2=y IX_VENDOR_HOME="https://www.kernelkit.org" diff --git a/configs/arm_minimal_defconfig b/configs/arm_minimal_defconfig index c1b12bca1..3e6d958f8 100644 --- a/configs/arm_minimal_defconfig +++ b/configs/arm_minimal_defconfig @@ -10,6 +10,7 @@ BR2_CCACHE_DIR="${BR2_EXTERNAL_INFIX_PATH}/.ccache" BR2_ENABLE_DEBUG=y BR2_GLOBAL_PATCH_DIR="${BR2_EXTERNAL_INFIX_PATH}/patches" BR2_DOWNLOAD_FORCE_CHECK_HASHES=y +BR2_TIME_BITS_64=y BR2_TARGET_GENERIC_HOSTNAME="ix" BR2_TARGET_GENERIC_ISSUE="Infix by KernelKit" BR2_INIT_FINIT=y @@ -115,6 +116,7 @@ BR2_PACKAGE_HOST_MTOOLS=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y +BR2_PACKAGE_BANANAPI_BPI_M1=y BR2_PACKAGE_MICROCHIP_SAMA7G54_EK=y BR2_PACKAGE_RASPBERRYPI_RPI2=y IX_VENDOR_HOME="https://www.kernelkit.org" diff --git a/configs/bpi_m1_boot_defconfig b/configs/bpi_m1_boot_defconfig new file mode 100644 index 000000000..0d9999ddf --- /dev/null +++ b/configs/bpi_m1_boot_defconfig @@ -0,0 +1,38 @@ +BR2_arm=y +BR2_cortex_a7=y +BR2_ARM_FPU_NEON_VFPV4=y +BR2_TOOLCHAIN_EXTERNAL=y +BR2_TOOLCHAIN_EXTERNAL_BOOTLIN=y +BR2_TOOLCHAIN_EXTERNAL_BOOTLIN_ARMV7_EABIHF_GLIBC_STABLE=y +BR2_DL_DIR="$(BR2_EXTERNAL_INFIX_PATH)/dl" +BR2_CCACHE=y +BR2_CCACHE_DIR="$(BR2_EXTERNAL_INFIX_PATH)/.ccache" +BR2_PACKAGE_OVERRIDE_FILE="$(BR2_EXTERNAL_INFIX_PATH)/local.mk" +BR2_GLOBAL_PATCH_DIR="$(BR2_EXTERNAL_INFIX_PATH)/patches" +BR2_SSP_NONE=y +BR2_INIT_NONE=y +BR2_SYSTEM_BIN_SH_NONE=y +# BR2_PACKAGE_BUSYBOX is not set +# BR2_PACKAGE_IFUPDOWN_SCRIPTS is not set +# BR2_TARGET_ROOTFS_TAR is not set +BR2_TARGET_UBOOT=y +BR2_TARGET_UBOOT_BUILD_SYSTEM_KCONFIG=y +BR2_TARGET_UBOOT_CUSTOM_VERSION=y +BR2_TARGET_UBOOT_CUSTOM_VERSION_VALUE="2025.01" +BR2_TARGET_UBOOT_BOARD_DEFCONFIG="Bananapi" +BR2_TARGET_UBOOT_CONFIG_FRAGMENT_FILES="$(BR2_EXTERNAL_INFIX_PATH)/board/common/uboot/extras.config $(BR2_EXTERNAL_INFIX_PATH)/board/arm/bananapi-bpi-m1/uboot/extras.config" +BR2_TARGET_UBOOT_NEEDS_DTC=y +BR2_TARGET_UBOOT_NEEDS_PYLIBFDT=y +BR2_TARGET_UBOOT_NEEDS_OPENSSL=y +BR2_TARGET_UBOOT_FORMAT_CUSTOM=y +BR2_TARGET_UBOOT_FORMAT_CUSTOM_NAME="u-boot-sunxi-with-spl.bin" +BR2_TARGET_UBOOT_CUSTOM_DTS_PATH="$(BR2_EXTERNAL_INFIX_PATH)/board/arm/bananapi-bpi-m1/uboot/bpi-m1-env.dtsi" +BR2_PACKAGE_HOST_BMAP_TOOLS=y +BR2_PACKAGE_HOST_GENIMAGE=y +BR2_PACKAGE_HOST_RAUC=y +BR2_PACKAGE_HOST_UBOOT_TOOLS=y +BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y +BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y +BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y +IX_TRUSTED_KEYS=y +IX_TRUSTED_KEYS_DEVELOPMENT=y diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md index f176a5b61..60705832a 100644 --- a/doc/ChangeLog.md +++ b/doc/ChangeLog.md @@ -12,7 +12,27 @@ All notable changes to the project are documented in this file. ietf-rip and ietf-ospf models, selected per control-plane-protocol by the `ripng`/`ospfv3` type and the IPv6 address-family - Support building without Frr, which also drops bash. The minimal - defconfigs are now built this way, with static routing only, issue #1670 + defconfigs, and the 32-bit Arm build, are now built this way, with + static routing only, issue #1670 +- Add support for the Banana Pi BPI-M1 (Allwinner A20) to the 32-bit Arm + build, with a new `bpi_m1_boot_defconfig` for its U-Boot bootloader +- New `iitoctl` tool, shows which rule drives each LED and why, and can + start locate, the board's LEDs blink as configured: `iitoctl locate on` +- New `infix-hardware:locate` RPC, blinks the board's locate LEDs to + identify the chassis, by default for 60 seconds. Available from the + CLI, `locate`, and the Hardware page in the web interface. Not allowed + for guest users + +### Fixes + +- Fix FRR mgmtd crash loop on 32-bit Arm, caused by unaligned access to + messages received from other daemons +- Fix mdns-alias failing to publish on 32-bit Arm, every retry ended in + "Memory exhausted". The 32-bit Arm builds now use 64-bit `time_t` +- Fix the LED daemon, iitod, crashing on 32-bit Arm, which left all LEDs + off after boot +- Fix missing hardware status when a sensor label has characters not + allowed in a sensor name, e.g., "SoC temperature" on the Allwinner A20 [v26.09.0][] - 2026-09-30 ------------------------- diff --git a/doc/hardware.md b/doc/hardware.md index c54c22b5a..609db0145 100644 --- a/doc/hardware.md +++ b/doc/hardware.md @@ -31,6 +31,34 @@ Which hwmon device or thermal zone belongs to which component is decided when the data is collected, and is the only place platform specific names are recognized. +## Locate + +To find a chassis, e.g., in a rack, its LEDs can be set to blink. Which +LEDs blink, and how, depends on the device. Locate stops by itself after +60 seconds, unless another duration is given, or when stopped. + +
admin@example:/> locate
+admin@example:/> locate duration 300
+admin@example:/> locate stop
+admin@example:/> no locate
+
+ +The CLI commands call the `infix-hardware:locate` RPC, which takes +`enable` and `duration`, in seconds. Over RESTCONF: + +```sh +curl -su admin:admin -X POST -H "Content-Type: application/yang-data+json" \ + -d '{"infix-hardware:input": {"duration": 300}}' \ + https://example/restconf/operations/infix-hardware:locate +``` + +The web interface has a *Locate* button on the Hardware page. + +Users in the `admin` and `operator` NACM groups may call locate, guest +users may not. On systems where LED control is disabled, the LEDs keep +their hardware default behavior and the RPC fails with *LED control is +not available on this device*. + ## GPS/GNSS Receivers Infix supports GPS/GNSS receivers for hardware status monitoring and NTP diff --git a/package/iito/iito.mk b/package/iito/iito.mk index 0d51f205a..bfc9d3635 100644 --- a/package/iito/iito.mk +++ b/package/iito/iito.mk @@ -9,6 +9,8 @@ IITO_SITE = https://github.com/kernelkit/iito/releases/download/v$(IITO_VERSION) IITO_LICENSE = GPL-2.0 IITO_LICENSE_FILES = COPYING IITO_DEPENDENCIES = jansson libev udev +# Patches touch Makefile.am +IITO_AUTORECONF = YES define IITO_INSTALL_HOOK $(INSTALL) -D -m 0644 $(IITO_PKGDIR)/iitod.svc $(FINIT_D)/available/iitod.conf diff --git a/patches/frr/10.5.5/0005-lib-mgmt_msg-hand-handlers-an-aligned-message.patch b/patches/frr/10.5.5/0005-lib-mgmt_msg-hand-handlers-an-aligned-message.patch new file mode 100644 index 000000000..8ce645cc5 --- /dev/null +++ b/patches/frr/10.5.5/0005-lib-mgmt_msg-hand-handlers-an-aligned-message.patch @@ -0,0 +1,65 @@ +From 6088ca29476f8776426348ab413ddc4eccc6760b Mon Sep 17 00:00:00 2001 +From: Joachim Wiberg +Date: Sun, 4 Oct 2026 15:15:28 +0200 +Subject: [PATCH] lib: mgmt_msg: hand handlers an aligned message +Organization: Wires + +mgmt_msg_procbufs() passes handlers a pointer into the receive stream, +where messages are packed back to back. A message following one whose +length is not a multiple of 8 starts unaligned, and the native message +structs have 64-bit members. On 32-bit Arm GCC may use a VFP store for +those, e.g., `msg->refer_id = 0` in mgmt_fe_adapter_send_notify(), which +the kernel cannot fix up, so mgmtd dies with SIGBUS: + + Alignment trap: not handling instruction edc30b02 at [<00441bfc>] + Unhandled fault: alignment exception (0x801) at 0x023c3f8e + +Copy unaligned messages to an aligned buffer before calling the handler. + +--- + lib/mgmt_msg.c | 27 +++++++++++++++++++++++---- + 1 file changed, 23 insertions(+), 4 deletions(-) + +diff --git a/lib/mgmt_msg.c b/lib/mgmt_msg.c +index 8f22b31231..c341872dc4 100644 +--- a/lib/mgmt_msg.c ++++ b/lib/mgmt_msg.c +@@ -180,15 +180,34 @@ bool mgmt_msg_procbufs(struct mgmt_msg_state *ms, + + for (; left > sizeof(struct mgmt_msg_hdr); + left -= mhdr->len, data += mhdr->len) { ++ uint8_t *msg, *copy = NULL; ++ size_t msglen; ++ + mhdr = (struct mgmt_msg_hdr *)data; + + assert(MGMT_MSG_IS_MARKER(mhdr->marker)); ++ assert(mhdr->len >= sizeof(struct mgmt_msg_hdr)); + assert(left >= mhdr->len); + +- handle_msg(MGMT_MSG_MARKER_VERSION(mhdr->marker), +- (uint8_t *)(mhdr + 1), +- mhdr->len - sizeof(struct mgmt_msg_hdr), +- user); ++ msg = (uint8_t *)(mhdr + 1); ++ msglen = mhdr->len - sizeof(struct mgmt_msg_hdr); ++ ++ /* ++ * Messages are packed back to back, so one following a ++ * message whose length is not a multiple of 8 starts ++ * unaligned. Handlers cast it to structs with 64-bit ++ * members, which traps on 32-bit Arm when the compiler ++ * uses VFP/NEON stores, so give them an aligned copy. ++ */ ++ if ((uintptr_t)msg % sizeof(uint64_t)) { ++ copy = XMALLOC(MTYPE_TMP, msglen); ++ memcpy(copy, msg, msglen); ++ msg = copy; ++ } ++ ++ handle_msg(MGMT_MSG_MARKER_VERSION(mhdr->marker), msg, ++ msglen, user); ++ XFREE(MTYPE_TMP, copy); + ms->nrxm++; + nproc++; + } diff --git a/patches/iito/1.1.0/0001-Include-PID-of-iitod-in-log-messages.patch b/patches/iito/1.1.0/0001-Include-PID-of-iitod-in-log-messages.patch new file mode 100644 index 000000000..df1c73905 --- /dev/null +++ b/patches/iito/1.1.0/0001-Include-PID-of-iitod-in-log-messages.patch @@ -0,0 +1,26 @@ +From eae602bb76ac734bc1415e87581eade573435dea Mon Sep 17 00:00:00 2001 +From: Joachim Wiberg +Date: Mon, 8 Jan 2024 10:21:08 +0100 +Subject: [PATCH 1/8] Include [PID] of iitod in log messages +Organization: Wires + +--- + src/main.c | 3 ++- + 1 file changed, 2 insertions(+), 1 deletion(-) + +diff --git a/src/main.c b/src/main.c +index e98152e..3c035c6 100644 +--- a/src/main.c ++++ b/src/main.c +@@ -89,9 +89,10 @@ int main(int argc, char **argv) + struct ev_loop *loop = ev_default_loop(0); + const char *file = DEFAULT_CONFIG; + struct ev_signal sigusr[2]; +- int err, opt, logopt = 0; ++ int logopt = LOG_PID; + json_t *ins, *outs; + json_error_t jerr; ++ int err, opt; + + while ((opt = getopt_long(argc, argv, sopts, lopts, NULL)) > 0) { + switch (opt) { diff --git a/patches/iito/1.1.0/0002-out-led-unpack-boolean-brightness-into-an-int.patch b/patches/iito/1.1.0/0002-out-led-unpack-boolean-brightness-into-an-int.patch new file mode 100644 index 000000000..33a97291f --- /dev/null +++ b/patches/iito/1.1.0/0002-out-led-unpack-boolean-brightness-into-an-int.patch @@ -0,0 +1,28 @@ +From 1ad066f300ecbc73ef24b0f35e3c278696e5dc3d Mon Sep 17 00:00:00 2001 +From: Joachim Wiberg +Date: Sun, 4 Oct 2026 22:05:45 +0200 +Subject: [PATCH 2/8] out-led: unpack boolean brightness into an int +Organization: Wires + +jansson's "b" format stores an int, but set_max was a bool, so unpacking +"brightness": true or false wrote past it on the stack. On 32-bit Arm +this clobbers other locals and iitod dies with SIGSEGV as soon as a rule +like @on or @off is applied. + +--- + src/out-led.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/src/out-led.c b/src/out-led.c +index ec4f3e6..ed9c173 100644 +--- a/src/out-led.c ++++ b/src/out-led.c +@@ -14,7 +14,7 @@ static int out_led_apply(struct out_dev *odev, struct out_rule *rule) + struct out_led *ol = container_of(odev, struct out_led, odev); + const char *key, *trigger = "none"; + int brightness = 0; +- bool set_max; ++ int set_max; + json_t *val; + + if (!uddev_present(&ol->uddev)) { diff --git a/patches/iito/1.1.0/0003-out-led-print-integer-attributes-with-JSON_INTEGER_F.patch b/patches/iito/1.1.0/0003-out-led-print-integer-attributes-with-JSON_INTEGER_F.patch new file mode 100644 index 000000000..63085f6b9 --- /dev/null +++ b/patches/iito/1.1.0/0003-out-led-print-integer-attributes-with-JSON_INTEGER_F.patch @@ -0,0 +1,30 @@ +From 3b9e7ca6c9dbcf3a87acece4f6a1a4a77fbe059b Mon Sep 17 00:00:00 2001 +From: Joachim Wiberg +Date: Sun, 4 Oct 2026 22:05:56 +0200 +Subject: [PATCH 3/8] out-led: print integer attributes with + JSON_INTEGER_FORMAT +Organization: Wires + +json_integer_value() returns a json_int_t, a long long on most systems, +but it was printed with %d. On 32-bit Arm the variadic 64-bit argument +is aligned in a register pair, so %d picks up the wrong word and timer +attributes like delay_on get garbage, the LED never blinks. + +--- + src/out-led.c | 3 ++- + 1 file changed, 2 insertions(+), 1 deletion(-) + +diff --git a/src/out-led.c b/src/out-led.c +index ed9c173..0b9192b 100644 +--- a/src/out-led.c ++++ b/src/out-led.c +@@ -70,7 +70,8 @@ static int out_led_apply(struct out_dev *odev, struct out_rule *rule) + return -EIO; + break; + case JSON_INTEGER: +- if (uddev_set_sysfs(&ol->uddev, key, "%d", json_integer_value(val))) ++ if (uddev_set_sysfs(&ol->uddev, key, "%" JSON_INTEGER_FORMAT, ++ json_integer_value(val))) + return -EIO; + break; + case JSON_TRUE: diff --git a/patches/iito/1.1.0/0004-main-do-not-release-a-borrowed-reference-when-resolv.patch b/patches/iito/1.1.0/0004-main-do-not-release-a-borrowed-reference-when-resolv.patch new file mode 100644 index 000000000..4462b7d5a --- /dev/null +++ b/patches/iito/1.1.0/0004-main-do-not-release-a-borrowed-reference-when-resolv.patch @@ -0,0 +1,29 @@ +From aae2b54f87170f47452f094cf8dafb1ebf53bd6b Mon Sep 17 00:00:00 2001 +From: Joachim Wiberg +Date: Sun, 4 Oct 2026 22:57:42 +0200 +Subject: [PATCH 4/8] main: do not release a borrowed reference when resolving + an alias +Organization: Wires + +alias_resolve() is handed the rule's "then" value as unpacked from the +config, a borrowed reference, but released it when replacing it with +the alias. That dropped the config's own reference to the string and +freed it while the config still pointed to it. + +--- + src/main.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/src/main.c b/src/main.c +index 3c035c6..2563a87 100644 +--- a/src/main.c ++++ b/src/main.c +@@ -19,7 +19,7 @@ int alias_resolve(json_t **aliasp) + return -ENOENT; + } + +- json_decref(*aliasp); ++ /* Both are borrowed from g_config, which outlives every rule */ + *aliasp = alias; + return 0; + } diff --git a/patches/iito/1.1.0/0005-in-match-input-names-exactly.patch b/patches/iito/1.1.0/0005-in-match-input-names-exactly.patch new file mode 100644 index 000000000..97a6b4905 --- /dev/null +++ b/patches/iito/1.1.0/0005-in-match-input-names-exactly.patch @@ -0,0 +1,28 @@ +From bc27366f010c5e77d7dbb7240a5e7676fb280ac7 Mon Sep 17 00:00:00 2001 +From: Joachim Wiberg +Date: Sun, 4 Oct 2026 22:57:52 +0200 +Subject: [PATCH 5/8] in: match input names exactly +Organization: Wires + +in_dev_find() compared only as many characters as the rule's name has, +so a rule on "fault" bound to whichever input starting with "fault", +e.g. "fault-prime", was probed first. + +--- + src/in.c | 3 ++- + 1 file changed, 2 insertions(+), 1 deletion(-) + +diff --git a/src/in.c b/src/in.c +index 11782ca..2cfe023 100644 +--- a/src/in.c ++++ b/src/in.c +@@ -34,7 +34,8 @@ int in_dev_find(const char *nameprop, struct in_dev **idevp, const char **propp) + sep = index(nameprop, '\0'); + + for (i = 0, idev = g_in_devs; i < g_in_devs_n; i++, idev++) { +- if (!strncmp(nameprop, (*idev)->name, sep - nameprop)) { ++ if (strlen((*idev)->name) == (size_t)(sep - nameprop) && ++ !strncmp(nameprop, (*idev)->name, sep - nameprop)) { + *idevp = *idev; + return 0; + } diff --git a/patches/iito/1.1.0/0006-iitod-add-an-IPC-socket-with-a-status-method.patch b/patches/iito/1.1.0/0006-iitod-add-an-IPC-socket-with-a-status-method.patch new file mode 100644 index 000000000..22e61d8a3 --- /dev/null +++ b/patches/iito/1.1.0/0006-iitod-add-an-IPC-socket-with-a-status-method.patch @@ -0,0 +1,539 @@ +From 3ec2a5059dfc6f3b5fee502e33632329e2f202e1 Mon Sep 17 00:00:00 2001 +From: Joachim Wiberg +Date: Sun, 4 Oct 2026 22:51:27 +0200 +Subject: [PATCH 6/8] iitod: add an IPC socket with a status method +Organization: Wires + +Seeing why an LED is in a given state meant reading the config and +guessing which inputs were set. iitod now listens on /run/iitod.sock, +root:wheel 0660 so admin users need no sudo, and answers a JSON status +request with every input's value, and every output's rules with their +current value, the active one, and the alias each rule was written with. + +The reply is JSON so that tools and management systems can use it +directly, a human friendly view belongs in the client. + +--- + src/Makefile.am | 3 +- + src/iito.h | 12 ++++ + src/in-path.c | 1 + + src/in-udev.c | 1 + + src/in.c | 24 +++++++ + src/ipc.c | 188 ++++++++++++++++++++++++++++++++++++++++++++++++ + src/ipc.h | 16 +++++ + src/main.c | 14 +++- + src/out-led.c | 9 +++ + src/out.c | 52 ++++++++++++++ + 10 files changed, 317 insertions(+), 3 deletions(-) + create mode 100644 src/ipc.c + create mode 100644 src/ipc.h + +diff --git a/src/Makefile.am b/src/Makefile.am +index cb59734..ca194ce 100644 +--- a/src/Makefile.am ++++ b/src/Makefile.am +@@ -10,4 +10,5 @@ iitod_SOURCES = \ + \ + out-led.c \ + \ +- in.c out.c main.c uddev.c iito.h ++ in.c out.c main.c uddev.c iito.h \ ++ ipc.c ipc.h +diff --git a/src/iito.h b/src/iito.h +index 198651d..5fdab0f 100644 +--- a/src/iito.h ++++ b/src/iito.h +@@ -69,6 +69,7 @@ int uddev_init(struct uddev *uddev); + + struct in_dev { + const char *name; ++ const char *type; + + int (*sample)(struct in_dev *dev, const char *prop, bool *state); + }; +@@ -84,6 +85,8 @@ int in_probe(json_t *ins); + + int in_dev_find(const char *nameprop, struct in_dev **idevp, const char **propp); + ++json_t *in_status(void); ++ + + /* output */ + +@@ -92,19 +95,23 @@ struct out_rule { + struct in_dev *idev; + const char *prop; + json_t *state; ++ char *alias; + void *priv; + }; + + struct out_dev { + const char *name; ++ const char *type; + struct out_rule *rules; + size_t n_rules; + + struct out_rule *active_rule; + int (*apply)(struct out_dev *odev, struct out_rule *rule); ++ bool (*present)(struct out_dev *odev); + }; + + void out_dump(void); ++json_t *out_status(void); + + int out_update(const struct in_dev *filter); + +@@ -119,6 +126,11 @@ struct out_drv { + int out_probe(json_t *outs); + + ++/* ipc */ ++ ++int ipc_init(struct ev_loop *loop, const char *path); ++ ++ + /* main */ + + int alias_resolve(json_t **aliasp); +diff --git a/src/in-path.c b/src/in-path.c +index 275a772..2090758 100644 +--- a/src/in-path.c ++++ b/src/in-path.c +@@ -41,6 +41,7 @@ static int in_path_probe(const char *name, json_t *data) + assert(ip); + + ip->dev.name = name; ++ ip->dev.type = "path"; + ip->dev.sample = in_path_sample; + + err = json_unpack(data, "{s:s}", "path", &path); +diff --git a/src/in-udev.c b/src/in-udev.c +index 7600c00..b4c216b 100644 +--- a/src/in-udev.c ++++ b/src/in-udev.c +@@ -64,6 +64,7 @@ static int in_udev_probe(const char *name, json_t *data) + *iu = (struct in_udev) { + .idev = { + .name = name, ++ .type = "udev", + .sample = in_udev_sample, + }, + .uddev = { +diff --git a/src/in.c b/src/in.c +index 2cfe023..9696298 100644 +--- a/src/in.c ++++ b/src/in.c +@@ -14,6 +14,7 @@ static void in_true_probe(void) + assert(tru); + + tru->name = "true"; ++ tru->type = "builtin"; + tru->sample = in_true_sample; + in_dev_add(tru); + } +@@ -56,6 +57,29 @@ void in_dev_add(struct in_dev *idev) + g_in_devs = idevs; + } + ++json_t *in_status(void) ++{ ++ struct in_dev **idev; ++ json_t *ins, *val; ++ bool state; ++ size_t i; ++ ++ ins = json_array(); ++ for (i = 0, idev = g_in_devs; i < g_in_devs_n; i++, idev++) { ++ if ((*idev)->sample(*idev, NULL, &state)) ++ val = json_null(); ++ else ++ val = json_boolean(state); ++ ++ json_array_append_new(ins, json_pack("{s:s, s:s, s:o}", ++ "name", (*idev)->name, ++ "type", (*idev)->type, ++ "value", val)); ++ } ++ ++ return ins; ++} ++ + extern const struct in_drv in_path; + extern const struct in_drv in_udev; + +diff --git a/src/ipc.c b/src/ipc.c +new file mode 100644 +index 0000000..e179c16 +--- /dev/null ++++ b/src/ipc.c +@@ -0,0 +1,188 @@ ++#include "iito.h" ++#include "ipc.h" ++ ++#include ++#include ++#include ++#include ++#include ++#include ++ ++struct ipc_method { ++ const char *name; ++ json_t *(*call)(json_t *params, const char **err); ++}; ++ ++static struct ev_io g_ipc_ev; ++ ++static json_t *ipc_status(json_t *params, const char **err) ++{ ++ return json_pack("{s:o, s:o}", ++ "inputs", in_status(), ++ "outputs", out_status()); ++} ++ ++static const struct ipc_method ipc_methods[] = { ++ { "status", ipc_status }, ++ ++ { NULL } ++}; ++ ++static json_t *ipc_call(const char *req) ++{ ++ const struct ipc_method *m; ++ json_t *msg, *params = NULL, *result = NULL, *reply; ++ const char *method, *err = "unknown method"; ++ json_error_t jerr; ++ ++ msg = json_loads(req, JSON_DISABLE_EOF_CHECK, &jerr); ++ if (!msg) ++ return json_pack("{s:s}", "error", "invalid request"); ++ ++ if (json_unpack(msg, "{s:s, s?o}", "method", &method, "params", ¶ms)) { ++ json_decref(msg); ++ return json_pack("{s:s}", "error", "request lacks a method"); ++ } ++ ++ for (m = ipc_methods; m->name; m++) { ++ if (!strcmp(m->name, method)) { ++ err = "failed"; ++ result = m->call(params, &err); ++ break; ++ } ++ } ++ ++ if (result) ++ reply = json_pack("{s:o}", "result", result); ++ else ++ reply = json_pack("{s:s}", "error", err); ++ ++ json_decref(msg); ++ return reply; ++} ++ ++static void ipc_send(int sd, json_t *reply) ++{ ++ size_t len, off = 0; ++ ssize_t n; ++ char *buf; ++ ++ buf = json_dumps(reply, JSON_COMPACT); ++ json_decref(reply); ++ if (!buf) ++ return; ++ ++ len = strlen(buf); ++ buf[len++] = '\n'; /* json_dumps() leaves room for the NUL */ ++ ++ while (off < len) { ++ n = write(sd, &buf[off], len - off); ++ if (n <= 0) ++ break; ++ off += n; ++ } ++ ++ free(buf); ++} ++ ++/* ++ * Time left until the request deadline, as a receive timeout, or false ++ * once the deadline has passed. ++ */ ++static bool ipc_time_left(const struct timespec *deadline, struct timeval *tv) ++{ ++ struct timespec now; ++ long long us; ++ ++ clock_gettime(CLOCK_MONOTONIC, &now); ++ us = (deadline->tv_sec - now.tv_sec) * 1000000LL + ++ (deadline->tv_nsec - now.tv_nsec) / 1000; ++ if (us <= 0) ++ return false; ++ ++ tv->tv_sec = us / 1000000; ++ tv->tv_usec = us % 1000000; ++ return true; ++} ++ ++static void ipc_cb(struct ev_loop *loop, struct ev_io *w, int revents) ++{ ++ /* ++ * Bound how long a client can hold up the event loop, in total, ++ * a per-read timeout alone lets a trickling client stall us. ++ */ ++ struct timeval tv = { .tv_sec = 1 }; ++ struct timespec deadline; ++ char buf[IPC_MAX_MSG]; ++ size_t len = 0; ++ ssize_t n; ++ int sd; ++ ++ sd = accept4(w->fd, NULL, NULL, SOCK_CLOEXEC); ++ if (sd < 0) { ++ log_err("(ipc) Failed accepting connection: %s", strerror(errno)); ++ return; ++ } ++ ++ setsockopt(sd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); ++ ++ clock_gettime(CLOCK_MONOTONIC, &deadline); ++ deadline.tv_sec += 1; ++ ++ while (len < sizeof(buf) - 1 && ipc_time_left(&deadline, &tv)) { ++ setsockopt(sd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); ++ n = read(sd, &buf[len], sizeof(buf) - 1 - len); ++ if (n <= 0) ++ break; ++ ++ len += n; ++ if (memchr(buf, '\n', len)) ++ break; ++ } ++ buf[len] = 0; ++ ++ ipc_send(sd, ipc_call(buf)); ++ close(sd); ++} ++ ++int ipc_init(struct ev_loop *loop, const char *path) ++{ ++ struct sockaddr_un sun = { .sun_family = AF_UNIX }; ++ struct group *gr; ++ int sd, err; ++ ++ if (strlen(path) >= sizeof(sun.sun_path)) { ++ log_err("(ipc) Socket path too long: %s", path); ++ return -ENAMETOOLONG; ++ } ++ strcpy(sun.sun_path, path); ++ ++ sd = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); ++ if (sd < 0) ++ goto err; ++ ++ unlink(path); ++ if (bind(sd, (struct sockaddr *)&sun, sizeof(sun)) || listen(sd, 8)) ++ goto err; ++ ++ /* Admin users, members of IPC_GROUP, may query and control us */ ++ gr = getgrnam(IPC_GROUP); ++ if (!gr || chown(path, -1, gr->gr_gid)) { ++ log_wrn("(ipc) Unable to grant group %s access to %s", IPC_GROUP, path); ++ gr = NULL; ++ } ++ ++ if (chmod(path, gr ? 0660 : 0600)) ++ goto err; ++ ++ ev_io_init(&g_ipc_ev, ipc_cb, sd, EV_READ); ++ ev_io_start(loop, &g_ipc_ev); ++ return 0; ++ ++err: ++ err = errno; ++ log_err("(ipc) Failed setting up %s: %s", path, strerror(err)); ++ if (sd >= 0) ++ close(sd); ++ return -err; ++} +diff --git a/src/ipc.h b/src/ipc.h +new file mode 100644 +index 0000000..1f4e5fe +--- /dev/null ++++ b/src/ipc.h +@@ -0,0 +1,16 @@ ++#ifndef _IITO_IPC_H ++#define _IITO_IPC_H ++ ++/* ++ * iitod listens on a UNIX stream socket. A client sends one JSON ++ * request terminated by a newline, iitod sends back one JSON reply, ++ * also newline terminated, and closes the connection. ++ * ++ * request: { "method": "status" } ++ * reply: { "result": { ... } } or { "error": "reason" } ++ */ ++#define IPC_SOCKET "/run/iitod.sock" ++#define IPC_GROUP "wheel" ++#define IPC_MAX_MSG 4096 ++ ++#endif /* _IITO_IPC_H */ +diff --git a/src/main.c b/src/main.c +index 2563a87..909492f 100644 +--- a/src/main.c ++++ b/src/main.c +@@ -2,6 +2,7 @@ + + #define SYSLOG_NAMES + #include "iito.h" ++#include "ipc.h" + + static json_t *g_config; + +@@ -58,16 +59,18 @@ static void usage() + " -f, --config=FILE Use configuration from FILE instead of %s\n" + " -h, --help Print usage message and exit\n" + " -l, --loglevel=LVL Log level: none, err, warn, notice*, info, debug\n" ++ " -s, --socket=PATH Listen for iitoctl on PATH instead of %s\n" + " -v, --version Print version information\n", +- DEFAULT_CONFIG); ++ DEFAULT_CONFIG, IPC_SOCKET); + } + +-static const char *sopts = "df:hl:v"; ++static const char *sopts = "df:hl:s:v"; + static struct option lopts[] = { + { "debug", no_argument, 0, 'd' }, + { "config", required_argument, 0, 'f' }, + { "help", no_argument, 0, 'h' }, + { "loglevel", required_argument, 0, 'l' }, ++ { "socket", required_argument, 0, 's' }, + { "version", no_argument, 0, 'v' }, + + { NULL } +@@ -88,6 +91,7 @@ int main(int argc, char **argv) + { + struct ev_loop *loop = ev_default_loop(0); + const char *file = DEFAULT_CONFIG; ++ const char *sock = IPC_SOCKET; + struct ev_signal sigusr[2]; + int logopt = LOG_PID; + json_t *ins, *outs; +@@ -113,6 +117,9 @@ int main(int argc, char **argv) + exit(1); + } + break; ++ case 's': ++ sock = optarg; ++ break; + case 'v': + puts(PACKAGE_STRING); + return 0; +@@ -168,6 +175,9 @@ int main(int argc, char **argv) + return 1; + } + ++ /* LEDs keep working without it, so only log a failure */ ++ ipc_init(loop, sock); ++ + ev_signal_init(&sigusr[0], sigusr1_cb, SIGUSR1); + ev_signal_init(&sigusr[1], sigusr2_cb, SIGUSR2); + ev_signal_start(loop, &sigusr[0]); +diff --git a/src/out-led.c b/src/out-led.c +index 0b9192b..355409d 100644 +--- a/src/out-led.c ++++ b/src/out-led.c +@@ -95,6 +95,13 @@ static int out_led_apply(struct out_dev *odev, struct out_rule *rule) + return 0; + } + ++static bool out_led_present(struct out_dev *odev) ++{ ++ struct out_led *ol = container_of(odev, struct out_led, odev); ++ ++ return uddev_present(&ol->uddev); ++} ++ + static void out_led_set_max(struct out_led *ol) + { + const char *maxstr; +@@ -148,7 +155,9 @@ static int out_led_probe(const char *name, struct out_rule *rules, + *ol = (struct out_led) { + .odev = { + .name = name, ++ .type = "led", + .apply = out_led_apply, ++ .present = out_led_present, + .rules = rules, + .n_rules = n_rules, + }, +diff --git a/src/out.c b/src/out.c +index 715f4d0..681926b 100644 +--- a/src/out.c ++++ b/src/out.c +@@ -23,6 +23,53 @@ void out_dump(void) + } + } + ++static json_t *out_rule_status(struct out_rule *rule) ++{ ++ char cond[128]; ++ json_t *val; ++ bool state; ++ ++ snprintf(cond, sizeof(cond), "%s%s%s%s", rule->invert ? "!" : "", ++ rule->idev->name, rule->prop ? ":" : "", rule->prop ? : ""); ++ ++ if (rule->idev->sample(rule->idev, rule->prop, &state)) ++ val = json_null(); ++ else ++ val = json_boolean(state ^ rule->invert); ++ ++ val = json_pack("{s:s, s:o, s:O}", "if", cond, "value", val, ++ "then", rule->state); ++ if (rule->alias) ++ json_object_set_new(val, "alias", json_string(rule->alias)); ++ ++ return val; ++} ++ ++json_t *out_status(void) ++{ ++ struct out_dev **odev; ++ json_t *outs, *rules; ++ size_t i, j; ++ ++ outs = json_array(); ++ for (i = 0, odev = g_out_devs; i < g_out_devs_n; i++, odev++) { ++ rules = json_array(); ++ for (j = 0; j < (*odev)->n_rules; j++) ++ json_array_append_new(rules, out_rule_status(&(*odev)->rules[j])); ++ ++ json_array_append_new(outs, json_pack("{s:s, s:s, s:b, s:o, s:o}", ++ "name", (*odev)->name, ++ "type", (*odev)->type, ++ "present", (*odev)->present(*odev), ++ "active", (*odev)->active_rule ? ++ json_integer((*odev)->active_rule - (*odev)->rules) : ++ json_null(), ++ "rules", rules)); ++ } ++ ++ return outs; ++} ++ + void out_dev_add(struct out_dev *odev) + { + struct out_dev **odevs; +@@ -140,6 +187,11 @@ static int out_probe_rule(json_t *data, struct out_rule *rule) + if (err) + return err; + ++ if (json_is_string(rule->state)) { ++ rule->alias = strdup(json_string_value(rule->state)); ++ assert(rule->alias); ++ } ++ + err = alias_resolve(&rule->state); + if (err) + return err; diff --git a/patches/iito/1.1.0/0007-iitod-add-locate-controlling-the-config-s-locate-inp.patch b/patches/iito/1.1.0/0007-iitod-add-locate-controlling-the-config-s-locate-inp.patch new file mode 100644 index 000000000..224bba13f --- /dev/null +++ b/patches/iito/1.1.0/0007-iitod-add-locate-controlling-the-config-s-locate-inp.patch @@ -0,0 +1,211 @@ +From eefef9d2d3a748ff33744ff6f5d0656f6d8eed99 Mon Sep 17 00:00:00 2001 +From: Joachim Wiberg +Date: Mon, 5 Oct 2026 18:11:24 +0200 +Subject: [PATCH 7/8] iitod: add locate, controlling the config's locate input +Organization: Wires + +Locating a unit is already expressed in configs as a "locate" path +input, with rules for which LEDs blink and how, but nothing set it. +The new locate method creates or removes that input's file, so tools +and management systems get one API regardless of which file the config +uses. An optional timeout, in seconds, stops locate by itself, and the +status and locate replies show the seconds remaining. + +--- + src/iito.h | 2 ++ + src/in-path.c | 24 +++++++++++++ + src/ipc.c | 99 +++++++++++++++++++++++++++++++++++++++++++++++++-- + src/ipc.h | 1 + + 4 files changed, 123 insertions(+), 3 deletions(-) + +diff --git a/src/iito.h b/src/iito.h +index 5fdab0f..5440014 100644 +--- a/src/iito.h ++++ b/src/iito.h +@@ -87,6 +87,8 @@ int in_dev_find(const char *nameprop, struct in_dev **idevp, const char **propp) + + json_t *in_status(void); + ++int in_path_set(struct in_dev *idev, bool present); ++ + + /* output */ + +diff --git a/src/in-path.c b/src/in-path.c +index 2090758..e9e9a08 100644 +--- a/src/in-path.c ++++ b/src/in-path.c +@@ -1,5 +1,7 @@ + #include "iito.h" + ++#include ++#include + + struct in_path { + struct in_dev dev; +@@ -31,6 +33,28 @@ static int in_path_sample(struct in_dev *dev, const char *prop, bool *state) + return -EINVAL; + } + ++/* ++ * Create, or remove, the file a path input watches. The new state is ++ * applied at once, instead of when ev_stat gets around to noticing it. ++ */ ++int in_path_set(struct in_dev *idev, bool present) ++{ ++ struct in_path *ip = container_of(idev, struct in_path, dev); ++ int fd; ++ ++ if (present) { ++ fd = open(ip->stat.path, O_WRONLY | O_CREAT | O_CLOEXEC, 0644); ++ if (fd < 0) ++ return -errno; ++ close(fd); ++ } else if (unlink(ip->stat.path) && errno != ENOENT) { ++ return -errno; ++ } ++ ++ ev_stat_stat(ev_default_loop(0), &ip->stat); ++ return out_update(idev); ++} ++ + static int in_path_probe(const char *name, json_t *data) + { + struct in_path *ip; +diff --git a/src/ipc.c b/src/ipc.c +index e179c16..9941d95 100644 +--- a/src/ipc.c ++++ b/src/ipc.c +@@ -13,17 +13,107 @@ struct ipc_method { + json_t *(*call)(json_t *params, const char **err); + }; + ++static struct ev_loop *g_loop; + static struct ev_io g_ipc_ev; ++static struct ev_timer g_locate_timer; ++ ++/* ++ * Locate is driven through the configuration's "locate" path input, ++ * the config's rules decide which LEDs blink, and how. ++ */ ++static struct in_dev *ipc_locate_input(void) ++{ ++ const char *prop = NULL; ++ struct in_dev *idev; ++ ++ if (in_dev_find("locate", &idev, &prop) || strcmp(idev->type, "path")) ++ return NULL; ++ ++ return idev; ++} ++ ++static void ipc_locate_timeout(struct ev_loop *loop, struct ev_timer *w, int revents) ++{ ++ struct in_dev *idev = ipc_locate_input(); ++ ++ if (idev && in_path_set(idev, false)) ++ log_err("(ipc) Failed stopping locate"); ++} ++ ++/* "remaining" is only set while a locate timeout is running */ ++static json_t *ipc_locate_state(struct in_dev *idev) ++{ ++ bool state = false; ++ json_t *st; ++ ++ idev->sample(idev, NULL, &state); ++ st = json_pack("{s:b}", "locate", state); ++ if (ev_is_active(&g_locate_timer)) ++ json_object_set_new(st, "remaining", json_integer( ++ (json_int_t)(ev_timer_remaining(g_loop, &g_locate_timer) + 0.999))); ++ ++ return st; ++} + + static json_t *ipc_status(json_t *params, const char **err) + { +- return json_pack("{s:o, s:o}", +- "inputs", in_status(), +- "outputs", out_status()); ++ struct in_dev *idev = ipc_locate_input(); ++ json_t *st; ++ ++ st = idev ? ipc_locate_state(idev) : json_object(); ++ json_object_set_new(st, "inputs", in_status()); ++ json_object_set_new(st, "outputs", out_status()); ++ ++ return st; ++} ++ ++/* ++ * Without params, or without "enable", only report the current state. ++ * An optional "timeout", in seconds, stops locate by itself. ++ */ ++static json_t *ipc_locate(json_t *params, const char **err) ++{ ++ json_t *enable, *timeout; ++ struct in_dev *idev; ++ ++ idev = ipc_locate_input(); ++ if (!idev) { ++ *err = "no locate path input in the configuration"; ++ return NULL; ++ } ++ ++ enable = json_object_get(params, "enable"); ++ timeout = json_object_get(params, "timeout"); ++ ++ if (enable && !json_is_boolean(enable)) { ++ *err = "\"enable\" must be true or false"; ++ return NULL; ++ } ++ if (timeout && (!json_is_integer(timeout) || json_integer_value(timeout) <= 0)) { ++ *err = "\"timeout\" must be a positive number of seconds"; ++ return NULL; ++ } ++ ++ if (enable) { ++ ev_timer_stop(g_loop, &g_locate_timer); ++ ++ if (in_path_set(idev, json_is_true(enable))) { ++ *err = "failed updating the locate input"; ++ return NULL; ++ } ++ ++ if (json_is_true(enable) && timeout) { ++ ev_timer_set(&g_locate_timer, (ev_tstamp)json_integer_value(timeout), 0.); ++ ev_timer_start(g_loop, &g_locate_timer); ++ } ++ } ++ ++ return ipc_locate_state(idev); + } + + static const struct ipc_method ipc_methods[] = { + { "status", ipc_status }, ++ { "locate", ipc_locate }, + + { NULL } + }; +@@ -175,6 +265,9 @@ int ipc_init(struct ev_loop *loop, const char *path) + if (chmod(path, gr ? 0660 : 0600)) + goto err; + ++ g_loop = loop; ++ ev_init(&g_locate_timer, ipc_locate_timeout); ++ + ev_io_init(&g_ipc_ev, ipc_cb, sd, EV_READ); + ev_io_start(loop, &g_ipc_ev); + return 0; +diff --git a/src/ipc.h b/src/ipc.h +index 1f4e5fe..eb7878c 100644 +--- a/src/ipc.h ++++ b/src/ipc.h +@@ -7,6 +7,7 @@ + * also newline terminated, and closes the connection. + * + * request: { "method": "status" } ++ * { "method": "locate", "params": { "enable": true, "timeout": 60 } } + * reply: { "result": { ... } } or { "error": "reason" } + */ + #define IPC_SOCKET "/run/iitod.sock" diff --git a/patches/iito/1.1.0/0008-iitoctl-new-tool-to-query-and-control-iitod.patch b/patches/iito/1.1.0/0008-iitoctl-new-tool-to-query-and-control-iitod.patch new file mode 100644 index 000000000..3e7be3333 --- /dev/null +++ b/patches/iito/1.1.0/0008-iitoctl-new-tool-to-query-and-control-iitod.patch @@ -0,0 +1,434 @@ +From 63dab772b86e3f84b94e2b0bba29d0671f4c0ba3 Mon Sep 17 00:00:00 2001 +From: Joachim Wiberg +Date: Mon, 5 Oct 2026 18:12:37 +0200 +Subject: [PATCH 8/8] iitoctl: new tool to query and control iitod +Organization: Wires + +Talks JSON to iitod over its socket, and prints the reply either as is, +with -j, or as tables for humans: the inputs, then each output's rules +with the active one marked. Also starts and stops locate, optionally +for a number of seconds: "locate on 60". + +--- + ChangeLog.md | 7 ++ + README.md | 49 ++++++++ + src/Makefile.am | 7 +- + src/iitoctl.c | 312 ++++++++++++++++++++++++++++++++++++++++++++++++ + 4 files changed, 374 insertions(+), 1 deletion(-) + create mode 100644 src/iitoctl.c + +diff --git a/ChangeLog.md b/ChangeLog.md +index 59b5986..e2a7b42 100644 +--- a/ChangeLog.md ++++ b/ChangeLog.md +@@ -2,6 +2,13 @@ + + All notable changes to the project are documented in this file. + ++## [UNRELEASED] ++ ++### Added ++ ++- IPC socket, `/run/iitod.sock`, with JSON `status` and `locate` methods ++- `iitoctl` tool to show status and start or stop locate ++ + ## [1.1.0] - 2023-11-18 + + ### Added +diff --git a/README.md b/README.md +index 63d2f04..e0c28e7 100644 +--- a/README.md ++++ b/README.md +@@ -171,6 +171,55 @@ of these rules match, the LED will show a solid green, since an LEDs + default rule is "off", and the green LEDs are hardwired to "on". + + ++## Querying and Controlling iitod ++ ++`iitod` listens on `/run/iitod.sock`, owned by `root:wheel` with mode ++0660, so members of `wheel` need no `sudo`. Use `-s PATH` to listen ++on another path. ++ ++The `iitoctl` tool shows each output's rules, with their current value ++and the active rule marked with `*`: ++ ++```sh ++~$ iitoctl ++Locate: off ++ ++INPUT TYPE VALUE ++true builtin true ++locate path false ++startup path true ++panic path false ++ ++sys:green:status (led) ++ IF VALUE THEN ++ locate false @blink-1hz ++ panic false @blink-5hz ++ * startup true @on ++ true true @blink-1hz ++``` ++ ++`iitoctl locate on` creates the file of the configuration's `locate` ++path input, until `iitoctl locate off`, or with `iitoctl locate on 60`, ++for 60 seconds. Which LEDs blink, and how, is up to the rules using the ++`locate` input, e.g., `{ "if": "locate", "then": "@blink-5hz" }`. ++ ++With `-j` the reply from `iitod` is printed as JSON. The protocol is ++one newline terminated JSON request per connection, answered with one ++JSON reply: ++ ++``` ++{ "method": "status" } ++{ "method": "locate", "params": { "enable": true, "timeout": 60 } } ++ ++{ "result": { ... } } ++{ "error": "reason" } ++``` ++ ++`locate` without `params` reports the current state. While a timeout ++is running, the `locate` and `status` replies include the `remaining` ++seconds. ++ ++ + ## Building and Installing + + iito uses Autotools, so the procdure is hopefully familiar to many. +diff --git a/src/Makefile.am b/src/Makefile.am +index ca194ce..66902c9 100644 +--- a/src/Makefile.am ++++ b/src/Makefile.am +@@ -1,4 +1,4 @@ +-sbin_PROGRAMS = iitod ++sbin_PROGRAMS = iitod iitoctl + + iitod_CPPFLAGS = -include $(top_builddir)/config.h -DSYSCONFDIR=\"$(sysconfdir)\" + iitod_CFLAGS = -Wall -Wextra -Wno-unused-parameter +@@ -12,3 +12,8 @@ iitod_SOURCES = \ + \ + in.c out.c main.c uddev.c iito.h \ + ipc.c ipc.h ++ ++iitoctl_CPPFLAGS = -include $(top_builddir)/config.h ++iitoctl_CFLAGS = -Wall -Wextra $(libjansson_CFLAGS) ++iitoctl_LDADD = $(libjansson_LIBS) ++iitoctl_SOURCES = iitoctl.c ipc.h +diff --git a/src/iitoctl.c b/src/iitoctl.c +new file mode 100644 +index 0000000..738a7d4 +--- /dev/null ++++ b/src/iitoctl.c +@@ -0,0 +1,312 @@ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#include ++ ++#include "ipc.h" ++ ++static const char *g_sock = IPC_SOCKET; ++static int g_json; ++ ++static void usage(void) ++{ ++ fprintf(stderr, ++ "iitoctl - Query and control iitod\n" ++ "\n" ++ "Usage:\n" ++ " iitoctl [options] [COMMAND]\n" ++ "\n" ++ "Commands:\n" ++ " status Show inputs, and each output's rules (default)\n" ++ " locate [on [SEC] | off]\n" ++ " Show, start, or stop locate, the LEDs blink as\n" ++ " configured, with on SEC, locate stops by itself\n" ++ " after SEC seconds\n" ++ "\n" ++ "Options:\n" ++ " -h, --help Print usage message and exit\n" ++ " -j, --json Print the reply from iitod as JSON\n" ++ " -s, --socket=PATH Connect to iitod on PATH instead of %s\n" ++ " -v, --version Print version information\n", ++ IPC_SOCKET); ++} ++ ++static json_t *call(const char *method, json_t *params) ++{ ++ struct sockaddr_un sun = { .sun_family = AF_UNIX }; ++ json_t *req, *reply, *result; ++ char buf[IPC_MAX_MSG], *msg; ++ json_error_t jerr; ++ size_t len = 0; ++ ssize_t n; ++ int sd; ++ ++ if (strlen(g_sock) >= sizeof(sun.sun_path)) { ++ fprintf(stderr, "iitoctl: socket path too long: %s\n", g_sock); ++ return NULL; ++ } ++ strcpy(sun.sun_path, g_sock); ++ ++ sd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); ++ if (sd < 0 || connect(sd, (struct sockaddr *)&sun, sizeof(sun))) { ++ fprintf(stderr, "iitoctl: cannot connect to %s: %s\n", g_sock, strerror(errno)); ++ goto fail; ++ } ++ ++ req = json_pack("{s:s}", "method", method); ++ if (params) ++ json_object_set_new(req, "params", params); ++ msg = json_dumps(req, JSON_COMPACT); ++ json_decref(req); ++ ++ if (!msg || dprintf(sd, "%s\n", msg) < 0) { ++ fprintf(stderr, "iitoctl: failed sending request: %s\n", strerror(errno)); ++ free(msg); ++ goto fail; ++ } ++ free(msg); ++ ++ /* The reply ends when iitod closes the connection */ ++ while (len < sizeof(buf) - 1) { ++ n = read(sd, &buf[len], sizeof(buf) - 1 - len); ++ if (n <= 0) ++ break; ++ len += n; ++ } ++ buf[len] = 0; ++ close(sd); ++ ++ reply = json_loads(buf, 0, &jerr); ++ if (!reply) { ++ fprintf(stderr, "iitoctl: invalid reply from iitod: %s\n", jerr.text); ++ return NULL; ++ } ++ ++ result = json_incref(json_object_get(reply, "result")); ++ if (!result) ++ fprintf(stderr, "iitoctl: %s\n", ++ json_string_value(json_object_get(reply, "error")) ? : "unknown error"); ++ ++ json_decref(reply); ++ return result; ++ ++fail: ++ if (sd >= 0) ++ close(sd); ++ return NULL; ++} ++ ++static const char *value_str(json_t *val) ++{ ++ if (json_is_true(val)) ++ return "true"; ++ if (json_is_false(val)) ++ return "false"; ++ ++ return "error"; ++} ++ ++/* Print a rule's state as its alias, or as key=value pairs */ ++static void print_state(json_t *rule) ++{ ++ const char *alias, *key; ++ json_t *val; ++ ++ alias = json_string_value(json_object_get(rule, "alias")); ++ if (alias) { ++ printf("%s", alias); ++ return; ++ } ++ ++ json_object_foreach(json_object_get(rule, "then"), key, val) { ++ if (json_is_string(val)) ++ printf("%s=%s ", key, json_string_value(val)); ++ else if (json_is_integer(val)) ++ printf("%s=%" JSON_INTEGER_FORMAT " ", key, json_integer_value(val)); ++ else ++ printf("%s=%s ", key, json_is_true(val) ? "true" : "false"); ++ } ++} ++ ++/* No "locate" when the config has no locate input */ ++static void print_locate(json_t *st) ++{ ++ json_t *locate = json_object_get(st, "locate"); ++ json_t *left = json_object_get(st, "remaining"); ++ ++ if (!locate) ++ printf("Locate: not configured\n"); ++ else if (!json_is_true(locate)) ++ printf("Locate: off\n"); ++ else if (json_is_integer(left)) ++ printf("Locate: on, %" JSON_INTEGER_FORMAT " seconds left\n", json_integer_value(left)); ++ else ++ printf("Locate: on\n"); ++} ++ ++static int width(json_t *arr, const char *key, int min) ++{ ++ json_t *obj; ++ size_t i; ++ int len; ++ ++ json_array_foreach(arr, i, obj) { ++ len = strlen(json_string_value(json_object_get(obj, key)) ? : ""); ++ if (len > min) ++ min = len; ++ } ++ ++ return min; ++} ++ ++static int show_status(void) ++{ ++ json_t *st, *ins, *outs, *in, *out, *rules, *rule, *active; ++ int w; ++ size_t i, j; ++ ++ st = call("status", NULL); ++ if (!st) ++ return 1; ++ ++ if (g_json) ++ goto json; ++ ++ print_locate(st); ++ ++ ins = json_object_get(st, "inputs"); ++ w = width(ins, "name", 5); ++ printf("\n%-*s %-7s %s\n", w, "INPUT", "TYPE", "VALUE"); ++ json_array_foreach(ins, i, in) ++ printf("%-*s %-7s %s\n", w, ++ json_string_value(json_object_get(in, "name")), ++ json_string_value(json_object_get(in, "type")), ++ value_str(json_object_get(in, "value"))); ++ ++ outs = json_object_get(st, "outputs"); ++ json_array_foreach(outs, i, out) { ++ printf("\n%s (%s%s)\n", ++ json_string_value(json_object_get(out, "name")), ++ json_string_value(json_object_get(out, "type")), ++ json_is_true(json_object_get(out, "present")) ? "" : ", absent"); ++ ++ rules = json_object_get(out, "rules"); ++ active = json_object_get(out, "active"); ++ w = width(rules, "if", 4); ++ printf(" %-*s %-5s %s\n", w, "IF", "VALUE", "THEN"); ++ json_array_foreach(rules, j, rule) { ++ printf(" %c %-*s %-5s ", ++ json_is_integer(active) && (size_t)json_integer_value(active) == j ? '*' : ' ', ++ w, json_string_value(json_object_get(rule, "if")), ++ value_str(json_object_get(rule, "value"))); ++ print_state(rule); ++ putchar('\n'); ++ } ++ ++ if (!json_is_integer(active)) ++ printf(" * default, no rule matches\n"); ++ } ++ ++ json_decref(st); ++ return 0; ++ ++json: ++ json_dumpf(st, stdout, JSON_INDENT(2)); ++ putchar('\n'); ++ json_decref(st); ++ return 0; ++} ++ ++static int locate(const char *arg, const char *sec) ++{ ++ json_t *params = NULL, *res; ++ char *end; ++ long tmo; ++ ++ if (arg) { ++ if (!strcmp(arg, "on")) ++ params = json_pack("{s:b}", "enable", 1); ++ else if (!strcmp(arg, "off") && !sec) ++ params = json_pack("{s:b}", "enable", 0); ++ else { ++ fprintf(stderr, "iitoctl: locate takes on [SEC] or off\n"); ++ return 1; ++ } ++ } ++ ++ if (sec) { ++ tmo = strtol(sec, &end, 10); ++ if (*end || tmo <= 0) { ++ fprintf(stderr, "iitoctl: invalid number of seconds \"%s\"\n", sec); ++ json_decref(params); ++ return 1; ++ } ++ json_object_set_new(params, "timeout", json_integer(tmo)); ++ } ++ ++ res = call("locate", params); ++ if (!res) ++ return 1; ++ ++ if (g_json) { ++ json_dumpf(res, stdout, JSON_INDENT(2)); ++ putchar('\n'); ++ } else { ++ print_locate(res); ++ } ++ ++ json_decref(res); ++ return 0; ++} ++ ++int main(int argc, char **argv) ++{ ++ static const struct option lopts[] = { ++ { "help", no_argument, 0, 'h' }, ++ { "json", no_argument, 0, 'j' }, ++ { "socket", required_argument, 0, 's' }, ++ { "version", no_argument, 0, 'v' }, ++ { NULL } ++ }; ++ const char *cmd = "status"; ++ int opt; ++ ++ while ((opt = getopt_long(argc, argv, "hjs:v", lopts, NULL)) > 0) { ++ switch (opt) { ++ case 'h': ++ usage(); ++ return 0; ++ case 'j': ++ g_json = 1; ++ break; ++ case 's': ++ g_sock = optarg; ++ break; ++ case 'v': ++ puts(PACKAGE_STRING); ++ return 0; ++ default: ++ usage(); ++ return 1; ++ } ++ } ++ ++ if (optind < argc) ++ cmd = argv[optind++]; ++ ++ if (!strcmp(cmd, "status")) ++ return show_status(); ++ if (!strcmp(cmd, "locate")) ++ return locate(optind < argc ? argv[optind] : NULL, ++ optind + 1 < argc ? argv[optind + 1] : NULL); ++ ++ fprintf(stderr, "iitoctl: unknown command \"%s\"\n\n", cmd); ++ usage(); ++ return 1; ++} diff --git a/src/confd/src/Makefile.am b/src/confd/src/Makefile.am index 6ea4efd2a..69ef9dbea 100644 --- a/src/confd/src/Makefile.am +++ b/src/confd/src/Makefile.am @@ -53,6 +53,7 @@ confd_plugin_la_SOURCES = \ keystore.c \ system.c \ support.c \ + locate.c \ schedule.c \ ntp.c \ ptp.c \ diff --git a/src/confd/src/core.c b/src/confd/src/core.c index 7ebf70d51..b89cf9e02 100644 --- a/src/confd/src/core.c +++ b/src/confd/src/core.c @@ -61,6 +61,24 @@ FILE *fopenfp(mode_t mode, const char *group, const char *fmt, ...) return fp; } +/* The event session runs as confd itself, the caller is only known + * from the originator data: netopeer2 pushes the NETCONF session id + * and then the username, rousette pushes nothing */ +const char *rpc_user(sr_session_ctx_t *session, const char **via) +{ + const char *orig = sr_session_get_orig_name(session); + const void *data; + uint32_t size; + + *via = orig && orig[0] ? orig : "local session"; + + if (orig && !strcmp(orig, "netopeer2") && + !sr_session_get_orig_data(session, 1, &size, &data) && size) + return data; + + return NULL; +} + /* * Touch a Finit service .conf file to schedule a synchronized reload. * Equivalent to 'initctl touch ' but without the fork+exec overhead. @@ -985,6 +1003,10 @@ int sr_plugin_init_cb(sr_session_ctx_t *session, void **priv) if (rc) goto err; + rc = locate_rpc_init(&confd); + if (rc) + goto err; + rc = syslog_rpc_init(&confd); if (rc) goto err; diff --git a/src/confd/src/core.h b/src/confd/src/core.h index 707c6f72a..e784e6a5b 100644 --- a/src/confd/src/core.h +++ b/src/confd/src/core.h @@ -215,6 +215,8 @@ int finit_enablef(const char *fmt, ...) __attribute__((format(printf, 1, 2))); int finit_disablef(const char *fmt, ...) __attribute__((format(printf, 1, 2))); int finit_deletef(const char *fmt, ...) __attribute__((format(printf, 1, 2))); int finit_reloadf(const char *fmt, ...) __attribute__((format(printf, 1, 2))); + +const char *rpc_user(sr_session_ctx_t *session, const char **via); FILE *fopenp(const char *path, mode_t mode, const char *group); FILE *fopenfp(mode_t mode, const char *group, const char *fmt, ...) __attribute__((format(printf, 3, 4))); @@ -278,6 +280,9 @@ int system_sw_rpc_init(struct confd *confd); /* support.c */ int support_rpc_init(struct confd *confd); +/* locate.c */ +int locate_rpc_init(struct confd *confd); + /* services.c */ int services_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff, sr_event_t event, struct confd *confd); diff --git a/src/confd/src/locate.c b/src/confd/src/locate.c new file mode 100644 index 000000000..d28ccdde0 --- /dev/null +++ b/src/confd/src/locate.c @@ -0,0 +1,132 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "core.h" + +#define IITOD_SOCKET "/run/iitod.sock" +#define IITOD_TIMEOUT 2 /* seconds */ + +static int fail(sr_session_ctx_t *session, const char *msg) +{ + sr_session_set_netconf_error(session, "application", "operation-failed", + NULL, NULL, msg, 0); + return SR_ERR_OPERATION_FAILED; +} + +/* + * One request, one reply, over the iitod control socket. Returns the + * parsed reply, or NULL with *err set. iitod is disabled on systems + * where kernel LED support is unreliable, then there is no socket. + */ +static json_t *iitod_call(json_t *req, const char **err) +{ + struct sockaddr_un sun = { .sun_family = AF_UNIX, .sun_path = IITOD_SOCKET }; + struct timeval tv = { .tv_sec = IITOD_TIMEOUT }; + char buf[4096], *msg; + json_t *reply = NULL; + size_t len = 0; + ssize_t n; + int sd; + + sd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); + if (sd < 0) { + *err = "failed creating socket"; + return NULL; + } + + /* Do not let a stuck iitod block confd, this also bounds connect() */ + setsockopt(sd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + setsockopt(sd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); + + if (connect(sd, (struct sockaddr *)&sun, sizeof(sun))) { + if (errno == ENOENT) + *err = "LED control is not available on this device"; + else if (errno == ECONNREFUSED) + *err = "the LED daemon is not running"; + else + *err = "failed connecting to the LED daemon"; + goto done; + } + + msg = json_dumps(req, JSON_COMPACT); + if (!msg || dprintf(sd, "%s\n", msg) < 0) { + *err = "failed sending request to the LED daemon"; + free(msg); + goto done; + } + free(msg); + + while (len < sizeof(buf) - 1) { + n = read(sd, &buf[len], sizeof(buf) - 1 - len); + if (n <= 0) + break; + len += n; + } + buf[len] = 0; + + reply = json_loads(buf, 0, NULL); + if (!reply) + *err = "no valid reply from the LED daemon"; +done: + close(sd); + return reply; +} + +static int rpc_locate(sr_session_ctx_t *session, uint32_t sub_id, const char *path, + const sr_val_t *input, const size_t input_cnt, sr_event_t event, + unsigned request_id, sr_val_t **output, size_t *output_cnt, void *priv) +{ + const char *err, *user, *via; + uint32_t duration = 0; + json_t *req, *reply; + bool enable = false; + int rc = SR_ERR_OK; + + if (event != SR_EV_RPC) + return SR_ERR_OK; + + /* sysrepo passes the YANG defaults for leaves not given */ + for (size_t i = 0; i < input_cnt; i++) { + const char *leaf = strrchr(input[i].xpath, '/'); + + if (!leaf) + continue; + if (!strcmp(leaf, "/enable")) + enable = input[i].data.bool_val; + else if (!strcmp(leaf, "/duration")) + duration = input[i].data.uint32_val; + } + + user = rpc_user(session, &via); + AUDIT("Locate %s by user \"%s\" over %s.", enable ? "started" : "stopped", + user ?: "unknown", via); + + /* iitod only arms the timeout when enabling */ + req = json_pack("{s:s, s:{s:b, s:I}}", "method", "locate", "params", + "enable", enable, "timeout", (json_int_t)duration); + reply = iitod_call(req, &err); + json_decref(req); + if (!reply) + return fail(session, err); + + if (json_object_get(reply, "error")) + rc = fail(session, json_string_value(json_object_get(reply, "error")) ? : "failed"); + + json_decref(reply); + return rc; +} + +int locate_rpc_init(struct confd *confd) +{ + return register_rpc(confd->session, "/infix-hardware:locate", + rpc_locate, NULL, &confd->sub); +} diff --git a/src/confd/src/support.c b/src/confd/src/support.c index 089e0f711..6a5c2d313 100644 --- a/src/confd/src/support.c +++ b/src/confd/src/support.c @@ -171,24 +171,6 @@ static void cleanup(const char *dir) WARN("Cannot remove %s: %s", dir, strerror(errno)); } -/* The event session runs as confd itself, the caller is only known - * from the originator data: netopeer2 pushes the NETCONF session id - * and then the username, rousette pushes nothing */ -static const char *rpc_user(sr_session_ctx_t *session, const char **via) -{ - const char *orig = sr_session_get_orig_name(session); - const void *data; - uint32_t size; - - *via = orig && orig[0] ? orig : "local session"; - - if (orig && !strcmp(orig, "netopeer2") && - !sr_session_get_orig_data(session, 1, &size, &data) && size) - return data; - - return NULL; -} - static int add_str(sr_val_t **output, size_t *cnt, const char *path, const char *leaf, sr_val_type_t type, const char *val) { diff --git a/src/confd/yang/confd.inc b/src/confd/yang/confd.inc index 7363a3abe..71adfd628 100644 --- a/src/confd/yang/confd.inc +++ b/src/confd/yang/confd.inc @@ -27,7 +27,7 @@ MODULES=( "infix-syslog@2026-09-24.yang" "iana-hardware@2018-03-13.yang" "ietf-hardware@2018-03-13.yang -e hardware-state -e hardware-sensor" - "infix-hardware@2026-09-24.yang" + "infix-hardware@2026-10-04.yang" "ieee802-dot1q-types@2022-10-29.yang" "infix-ip@2026-04-28.yang" "infix-if-type@2026-01-07.yang" diff --git a/src/confd/yang/confd/infix-hardware.yang b/src/confd/yang/confd/infix-hardware.yang index 6769774b6..a35c38c23 100644 --- a/src/confd/yang/confd/infix-hardware.yang +++ b/src/confd/yang/confd/infix-hardware.yang @@ -21,6 +21,11 @@ module infix-hardware { contact "kernelkit@googlegroups.com"; description "Vital Product Data augmentation of ieee-hardware and deviations."; + revision 2026-10-04 { + description "Add locate RPC, blinks LEDs to identify the chassis."; + reference "internal"; + } + revision 2026-09-24 { description "Constrain the character set of hardware component names."; reference "internal"; @@ -747,4 +752,32 @@ module infix-hardware { } } } + + rpc locate { + description + "Blink the LEDs of the chassis to physically identify it, e.g., in + a rack. Which LEDs blink, and how, is defined per board. Locate + stops by itself after the given duration, or when called again + with enable set to false. + + Fails on systems where LED control is disabled, the LEDs then keep + their hardware default behavior."; + + input { + leaf enable { + type boolean; + default true; + description "Start, or stop, blinking the LEDs."; + } + + leaf duration { + type uint32 { + range "1..86400"; + } + units "seconds"; + default 60; + description "Stop blinking by itself after this many seconds."; + } + } + } } diff --git a/src/confd/yang/confd/infix-hardware@2026-09-24.yang b/src/confd/yang/confd/infix-hardware@2026-10-04.yang similarity index 100% rename from src/confd/yang/confd/infix-hardware@2026-09-24.yang rename to src/confd/yang/confd/infix-hardware@2026-10-04.yang diff --git a/src/klish-plugin-infix/xml/infix.xml b/src/klish-plugin-infix/xml/infix.xml index 0c17ab978..2932efb1f 100644 --- a/src/klish-plugin-infix/xml/infix.xml +++ b/src/klish-plugin-infix/xml/infix.xml @@ -275,6 +275,24 @@ + + + + + + + + + set -- /infix-hardware:locate + if [ -n "$KLISH_PARAM_stop" ]; then + set -- "$@" enable false + elif [ -n "$KLISH_PARAM_seconds" ]; then + set -- "$@" duration "$KLISH_PARAM_seconds" + fi + doas -u "$KLISH_USER" rpc "$@" + + + /ietf-system:system-shutdown @@ -1099,6 +1117,9 @@ echo "Public: $pub" + + doas -u "$KLISH_USER" rpc /infix-hardware:locate enable false + diff --git a/src/statd/python/yanger/ietf_hardware.py b/src/statd/python/yanger/ietf_hardware.py index b52580924..850c019d5 100644 --- a/src/statd/python/yanger/ietf_hardware.py +++ b/src/statd/python/yanger/ietf_hardware.py @@ -128,6 +128,7 @@ def normalize_sensor_name(name): cpu_thermal -> cpu-thermal s5_temp -> s5-temp pwmfan -> pwmfan + SoC temperature -> SoC-temperature Strategy: 1. Drop the vendor/chipset prefix of a per-port device, where the @@ -151,7 +152,11 @@ def normalize_sensor_name(name): # Remove underscores before trailing numbers (sfp_2 -> sfp2) name = re.sub(r'_(\d+)$', r'\1', name) - return name.replace("_", "-") + # Labels are free text, e.g. "SoC temperature", but a name must + # match the YANG pattern [a-zA-Z0-9_][a-zA-Z0-9_.:+@-]* + name = re.sub(r'[^a-zA-Z0-9.:+@]+', '-', name.replace("_", "-")) + + return name.strip("-") CPU_COMPONENT = "cpu" diff --git a/src/webui/internal/handlers/hardware.go b/src/webui/internal/handlers/hardware.go index 8748926f0..30c679d61 100644 --- a/src/webui/internal/handlers/hardware.go +++ b/src/webui/internal/handlers/hardware.go @@ -4,6 +4,7 @@ package handlers import ( "context" + "fmt" "html/template" "log" "net/http" @@ -96,6 +97,19 @@ type HardwareHandler struct { RC *restconf.Client } +// Locate blinks LEDs to identify the chassis, for the duration +// given by the YANG default of the infix-hardware:locate RPC. +func (h *HardwareHandler) Locate(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html") + if err := h.RC.Post(r.Context(), "/operations/infix-hardware:locate"); err != nil { + log.Printf("locate: %v", err) + fmt.Fprintf(w, `Failed: %s`, + template.HTMLEscapeString(err.Error())) + return + } + fmt.Fprint(w, `✓ Locating, LEDs are blinking`) +} + func (h *HardwareHandler) Overview(w http.ResponseWriter, r *http.Request) { data := hardwarePageData{ PageData: newPageData(w, r, "hardware", "Hardware"), diff --git a/src/webui/internal/server/server.go b/src/webui/internal/server/server.go index e2f58199a..0d065954e 100644 --- a/src/webui/internal/server/server.go +++ b/src/webui/internal/server/server.go @@ -308,6 +308,7 @@ func New( mux.HandleFunc("GET /routing", routing.Overview) mux.HandleFunc("GET /wifi", wifi.Overview) mux.HandleFunc("GET /hardware", hw.Overview) + mux.HandleFunc("POST /hardware/locate", hw.Locate) mux.HandleFunc("GET /vpn", vpn.Overview) mux.HandleFunc("GET /dhcp", dhcp.Overview) mux.HandleFunc("GET /ntp", ntp.Overview) diff --git a/src/webui/templates/pages/hardware.html b/src/webui/templates/pages/hardware.html index cdc598d6e..7983180d7 100644 --- a/src/webui/templates/pages/hardware.html +++ b/src/webui/templates/pages/hardware.html @@ -12,7 +12,13 @@ {{/* ── Board ─────────────────────────────────────────────────────────── */}} {{if .Board.Model}}
-
Board
+
Board + +
+
diff --git a/test/case/statd/sensors/cli/show-hardware b/test/case/statd/sensors/cli/show-hardware index fb22cbb1e..d906fa0a6 100644 --- a/test/case/statd/sensors/cli/show-hardware +++ b/test/case/statd/sensors/cli/show-hardware @@ -13,3 +13,4 @@ cpu: S5 Temp 59.5 °C ok pwmfan 3200 RPM ok +Soc Temperature 41.2 °C ok diff --git a/test/case/statd/sensors/ietf-hardware.json b/test/case/statd/sensors/ietf-hardware.json index 144df05d7..fd93712b3 100644 --- a/test/case/statd/sensors/ietf-hardware.json +++ b/test/case/statd/sensors/ietf-hardware.json @@ -68,6 +68,19 @@ "value-timestamp": "2026-01-02T20:43:23+00:00", "oper-status": "ok" } + }, + { + "name": "sun4i-ts-SoC-temperature", + "class": "iana-hardware:sensor", + "sensor-data": { + "value": 41200, + "value-type": "celsius", + "value-scale": "milli", + "value-precision": 0, + "value-timestamp": "2026-01-02T20:43:23+00:00", + "oper-status": "ok" + }, + "description": "Soc Temperature" } ] } diff --git a/test/case/statd/sensors/operational.json b/test/case/statd/sensors/operational.json index 144df05d7..fd93712b3 100644 --- a/test/case/statd/sensors/operational.json +++ b/test/case/statd/sensors/operational.json @@ -68,6 +68,19 @@ "value-timestamp": "2026-01-02T20:43:23+00:00", "oper-status": "ok" } + }, + { + "name": "sun4i-ts-SoC-temperature", + "class": "iana-hardware:sensor", + "sensor-data": { + "value": 41200, + "value-type": "celsius", + "value-scale": "milli", + "value-precision": 0, + "value-timestamp": "2026-01-02T20:43:23+00:00", + "oper-status": "ok" + }, + "description": "Soc Temperature" } ] } diff --git a/test/case/statd/sensors/system/rootfs/sys/class/hwmon/hwmon3/name b/test/case/statd/sensors/system/rootfs/sys/class/hwmon/hwmon3/name new file mode 100644 index 000000000..80a569b99 --- /dev/null +++ b/test/case/statd/sensors/system/rootfs/sys/class/hwmon/hwmon3/name @@ -0,0 +1 @@ +sun4i_ts diff --git a/test/case/statd/sensors/system/rootfs/sys/class/hwmon/hwmon3/temp1_input b/test/case/statd/sensors/system/rootfs/sys/class/hwmon/hwmon3/temp1_input new file mode 100644 index 000000000..1a112cfa2 --- /dev/null +++ b/test/case/statd/sensors/system/rootfs/sys/class/hwmon/hwmon3/temp1_input @@ -0,0 +1 @@ +41200 diff --git a/test/case/statd/sensors/system/rootfs/sys/class/hwmon/hwmon3/temp1_label b/test/case/statd/sensors/system/rootfs/sys/class/hwmon/hwmon3/temp1_label new file mode 100644 index 000000000..8d9a877a5 --- /dev/null +++ b/test/case/statd/sensors/system/rootfs/sys/class/hwmon/hwmon3/temp1_label @@ -0,0 +1 @@ +SoC temperature diff --git a/test/case/statd/sensors/system/run/ls_+sys+class+hwmon b/test/case/statd/sensors/system/run/ls_+sys+class+hwmon index b51997471..324efcb17 100644 --- a/test/case/statd/sensors/system/run/ls_+sys+class+hwmon +++ b/test/case/statd/sensors/system/run/ls_+sys+class+hwmon @@ -1,3 +1,4 @@ hwmon0 hwmon1 hwmon2 +hwmon3 diff --git a/test/case/statd/sensors/system/run/ls_+sys+class+hwmon+hwmon3 b/test/case/statd/sensors/system/run/ls_+sys+class+hwmon+hwmon3 new file mode 100644 index 000000000..27c20f9a3 --- /dev/null +++ b/test/case/statd/sensors/system/run/ls_+sys+class+hwmon+hwmon3 @@ -0,0 +1,3 @@ +name +temp1_input +temp1_label diff --git a/utils/mkimage.sh b/utils/mkimage.sh index 7738e4277..c942744e9 100755 --- a/utils/mkimage.sh +++ b/utils/mkimage.sh @@ -185,6 +185,9 @@ get_bootloader_name() acer-connect-vero-w6m) echo "bpi_r3_emmc_boot" ;; + bananapi-bpi-m1) + echo "bpi_m1_boot" + ;; bananapi-bpi-r3) if [ "$target" = "emmc" ]; then echo "bpi_r3_emmc_boot"
Model{{.Board.Model}}