+#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}}
-
+
+
| Model | {{.Board.Model}} |
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 [32m59.5 °C[39m ok
pwmfan 3200 RPM ok
+Soc Temperature [32m41.2 °C[39m 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"