PKG_NAME:=luci-app-advanced-reboot
PKG_LICENSE:=AGPL-3.0-or-later
PKG_MAINTAINER:=Stan Grishin <stangri@melmac.ca>
-PKG_VERSION:=1.1.1
-PKG_RELEASE:=15
-
-PKG_BUILD_DEPENDS:=jq/host
+PKG_VERSION:=1.1.2
+PKG_RELEASE:=6
LUCI_TITLE:=Advanced Linksys Reboot Web UI
-LUCI_URL:=https://github.com/stangri/luci-app-advanced-reboot/
+LUCI_URL:=https://github.com/mossdef-org/luci-app-advanced-reboot/
LUCI_DESCRIPTION:=Provides Web UI (found under System/Advanced Reboot) to reboot supported Linksys and ZyXEL routers to\
an alternative partition. Also provides Web UI to shut down (power off) your device. Supported dual-partition\
- routers are listed at https://docs.openwrt.melmac.ca/luci-app-advanced-reboot/
+ routers are listed at https://docs.mossdef.org/luci-app-advanced-reboot/
LUCI_DEPENDS:=+luci-base +jshn
define Package/$(PKG_NAME)/config
Version: $(PKG_VERSION)-$(PKG_RELEASE)
endef
-include ../../luci.mk
-
-# Prune individual device JSON directories from the package image.
-# The default LuCI install logic from luci.mk copies everything under
-# htdocs/, root/, etc. We let it run, then remove folders we keep only
-# in source control.
-define Package/$(PKG_NAME)/install
- $(call Package/$(PKG_NAME)/install/default,$(1))
- @mkdir -p $(1)/usr/share/advanced-reboot
- @if [ -d $(1)/usr/share/advanced-reboot/devices ] \
- && ls $(1)/usr/share/advanced-reboot/devices/*.json >/dev/null 2>&1; then \
- $(STAGING_DIR_HOST)/bin/jq -s '.' $(1)/usr/share/advanced-reboot/devices/*.json \
- > $(1)/usr/share/advanced-reboot/devices.json; \
+# Consolidate individual device JSON files into devices.json in the build tree.
+# luci.mk's Build/Prepare calls Build/Prepare/$(LUCI_NAME) as a per-package hook.
+# The merged file lands in root/ so luci.mk's install step copies it automatically.
+define Build/Prepare/luci-app-advanced-reboot
+ @if [ -d $(PKG_BUILD_DIR)/root/usr/share/advanced-reboot/devices ] \
+ && ls $(PKG_BUILD_DIR)/root/usr/share/advanced-reboot/devices/*.json >/dev/null 2>&1; then \
+ sep=''; printf '[' > $(PKG_BUILD_DIR)/root/usr/share/advanced-reboot/devices.json; \
+ for f in $(PKG_BUILD_DIR)/root/usr/share/advanced-reboot/devices/*.json; do \
+ printf '%s' "$$$$sep" >> $(PKG_BUILD_DIR)/root/usr/share/advanced-reboot/devices.json; \
+ cat "$$$$f" >> $(PKG_BUILD_DIR)/root/usr/share/advanced-reboot/devices.json; \
+ sep=','; \
+ done; \
+ printf ']' >> $(PKG_BUILD_DIR)/root/usr/share/advanced-reboot/devices.json; \
fi
- @if [ -s $(1)/usr/share/advanced-reboot/devices.json ]; then \
- $(RM) -r $(1)/usr/share/advanced-reboot/devices $(1)/usr/share/advanced-reboot/devices.disabled || true; \
+ @rm -rf $(PKG_BUILD_DIR)/root/usr/share/advanced-reboot/devices.disabled
+ @if grep -q '"device"' $(PKG_BUILD_DIR)/root/usr/share/advanced-reboot/devices.json 2>/dev/null; then \
+ rm -rf $(PKG_BUILD_DIR)/root/usr/share/advanced-reboot/devices; \
+ else \
+ rm -f $(PKG_BUILD_DIR)/root/usr/share/advanced-reboot/devices.json; \
fi
endef
+include ../../luci.mk
+
# call BuildPackage - OpenWrt buildroot signature
## Documentation
Full documentation is available at:
-[https://docs.openwrt.melmac.ca/luci-app-advanced-reboot/](https://docs.openwrt.melmac.ca/luci-app-advanced-reboot/)
+[https://docs.mossdef.org/luci-app-advanced-reboot/](https://docs.mossdef.org/luci-app-advanced-reboot/)
## License
return "luci-app-advanced-reboot";
},
get URL() {
- return "https://docs.openwrt.melmac.ca/" + pkg.Name + "/";
+ return "https://docs.mossdef.org/" + pkg.Name + "/";
},
};
ERR_SAVE_ENV: function (args) {
return _("Unable to save environment changes.");
},
+ ERR_SET_HELPER: function (args) {
+ return _(
+ "Unable to run device helper: %s for partition: %s."
+ ).format(args[0], args[1]);
+ },
NO_TARGET_FLAG: function (args) {
return _("Target partition flag is not defined for this device.");
},
"partitions": [
{
"number": 1,
- "param_values": [1],
- "mtd": "mtd3",
- "labelOffsetBytes": 28
+ "param_values": [
+ 1
+ ],
+ "firmware": {
+ "kernel": "mtd3",
+ "kernel_label_offset": 28,
+ "rootfs": "mtd4"
+ }
},
{
"number": 2,
- "param_values": [2],
- "mtd": "mtd6",
- "labelOffsetBytes": 28
+ "param_values": [
+ 2
+ ],
+ "firmware": {
+ "kernel": "mtd6",
+ "kernel_label_offset": 28,
+ "rootfs": "mtd7"
+ }
}
]
-}
\ No newline at end of file
+}
"partitions": [
{
"number": 1,
- "param_values": [0, 3800000],
- "mtd": "mtd9",
- "labelOffsetBytes": null
+ "param_values": [
+ 0,
+ 3800000
+ ],
+ "firmware": {
+ "rootfs": "mtd10"
+ }
},
{
"number": 2,
- "param_values": [3800000, 0],
- "mtd": "mtd10",
- "labelOffsetBytes": null
+ "param_values": [
+ 3800000,
+ 0
+ ],
+ "firmware": {
+ "rootfs": "mtd11"
+ }
}
]
}
0,
0
],
- "mtd": "mtd12",
- "labelOffsetBytes": 266432
+ "firmware": {
+ "kernel": "mtd12",
+ "kernel_label_offset": 266432,
+ "rootfs": "mtd13"
+ }
},
{
"number": 2,
1,
1
],
- "mtd": "mtd13",
- "labelOffsetBytes": 266432
+ "firmware": {
+ "kernel": "mtd13",
+ "kernel_label_offset": 266432,
+ "rootfs": "mtd14"
+ }
}
]
}
0,
0
],
- "mtd": "mtd20",
- "labelOffsetBytes": 266432
+ "firmware": {
+ "kernel": "mtd20",
+ "kernel_label_offset": 266432,
+ "rootfs": "mtd21"
+ }
},
{
"number": 2,
1,
1
],
- "mtd": "mtd21",
- "labelOffsetBytes": 266432
+ "firmware": {
+ "kernel": "mtd21",
+ "kernel_label_offset": 266432,
+ "rootfs": "mtd22"
+ }
}
]
}
+++ /dev/null
-[
- {
- "device": {
- "vendor": "D-Link",
- "model": "DGS-1210-28",
- "board": [
- "d-link,dgs-1210-28"
- ]
- },
- "commands": {
- "params": [
- "bootcmd",
- "image"
- ],
- "get": "fw_printenv",
- "set": "fw_setenv",
- "save": null
- },
- "partitions": [
- {
- "number": 1,
- "param_values": [],
- "mtd": "mtd5",
- "labelOffsetBytes": null
- },
- {
- "number": 2,
- "param_values": [
- "run addargs\\; bootm 0xb4e80000",
- "/dev/mtdblock7"
- ],
- "mtd": "mtd9",
- "labelOffsetBytes": null
- }
- ]
- },
- {
- "device": {
- "vendor": "Linksys",
- "model": "E4200v2/EA4500",
- "board": [
- "linksys-viper",
- "linksys,viper"
- ]
- },
- "commands": {
- "params": [
- "boot_part",
- "bootcmd"
- ],
- "get": "fw_printenv",
- "set": "fw_setenv",
- "save": null
- },
- "partitions": [
- {
- "number": 1,
- "param_values": [
- 1,
- "run nandboot"
- ],
- "mtd": "mtd3",
- "labelOffsetBytes": 32
- },
- {
- "number": 2,
- "param_values": [
- 2,
- "run altnandboot"
- ],
- "mtd": "mtd5",
- "labelOffsetBytes": 32
- }
- ]
- },
- {
- "device": {
- "vendor": "Linksys",
- "model": "E4200v2",
- "board": [
- "linksys-e4200v2",
- "linksys,e4200-v2"
- ]
- },
- "commands": {
- "params": [
- "boot_part",
- "bootcmd"
- ],
- "get": "fw_printenv",
- "set": "fw_setenv",
- "save": null
- },
- "partitions": [
- {
- "number": 1,
- "param_values": [
- 1,
- "run nandboot"
- ],
- "mtd": "mtd3",
- "labelOffsetBytes": 32
- },
- {
- "number": 2,
- "param_values": [
- 2,
- "run altnandboot"
- ],
- "mtd": "mtd5",
- "labelOffsetBytes": 32
- }
- ]
- },
- {
- "device": {
- "vendor": "Linksys",
- "model": "E7350",
- "board": [
- "linksys,e7350"
- ]
- },
- "commands": {
- "params": [
- "bootimage"
- ],
- "get": "fw_printenv",
- "set": "fw_setenv",
- "save": null
- },
- "partitions": [
- {
- "number": 1,
- "param_values": [
- 1
- ],
- "mtd": "mtd3",
- "labelOffsetBytes": 192
- },
- {
- "number": 2,
- "param_values": [
- 2
- ],
- "mtd": "mtd6",
- "labelOffsetBytes": 192
- }
- ]
- },
- {
- "device": {
- "vendor": "Linksys",
- "model": "EA3500",
- "board": [
- "linksys-audi",
- "linksys,audi",
- "linksys-ea3500",
- "linksys,ea3500"
- ]
- },
- "commands": {
- "params": [
- "boot_part",
- "bootcmd"
- ],
- "get": "fw_printenv",
- "set": "fw_setenv",
- "save": null
- },
- "partitions": [
- {
- "number": 1,
- "param_values": [
- 1,
- "run nandboot"
- ],
- "mtd": "mtd3",
- "labelOffsetBytes": 32
- },
- {
- "number": 2,
- "param_values": [
- 2,
- "run altnandboot"
- ],
- "mtd": "mtd5",
- "labelOffsetBytes": 32
- }
- ]
- },
- {
- "device": {
- "vendor": "Linksys",
- "model": "EA4500",
- "board": [
- "linksys-ea4500",
- "linksys,ea4500"
- ]
- },
- "commands": {
- "params": [
- "boot_part",
- "bootcmd"
- ],
- "get": "fw_printenv",
- "set": "fw_setenv",
- "save": null
- },
- "partitions": [
- {
- "number": 1,
- "param_values": [
- 1,
- "run nandboot"
- ],
- "mtd": "mtd3",
- "labelOffsetBytes": 32
- },
- {
- "number": 2,
- "param_values": [
- 2,
- "run altnandboot"
- ],
- "mtd": "mtd5",
- "labelOffsetBytes": 32
- }
- ]
- },
- {
- "device": {
- "vendor": "Linksys",
- "model": "EA6350v3",
- "board": [
- "linksys-ea6350v3",
- "linksys,ea6350v3"
- ]
- },
- "commands": {
- "params": [
- "boot_part"
- ],
- "get": "fw_printenv",
- "set": "fw_setenv",
- "save": null
- },
- "partitions": [
- {
- "number": 1,
- "param_values": [
- 1
- ],
- "mtd": "mtd10",
- "labelOffsetBytes": 192
- },
- {
- "number": 2,
- "param_values": [
- 2
- ],
- "mtd": "mtd12",
- "labelOffsetBytes": 192
- }
- ]
- },
- {
- "device": {
- "vendor": "Linksys",
- "model": "EA6350v4",
- "board": [
- "linksys,ea6350-v4"
- ]
- },
- "commands": {
- "params": [
- "boot_part"
- ],
- "get": "fw_printenv",
- "set": "fw_setenv",
- "save": null
- },
- "partitions": [
- {
- "number": 1,
- "param_values": [
- 1
- ],
- "mtd": "mtd5",
- "labelOffsetBytes": 32
- },
- {
- "number": 2,
- "param_values": [
- 2
- ],
- "mtd": "mtd7",
- "labelOffsetBytes": 32
- }
- ]
- },
- {
- "device": {
- "vendor": "Linksys",
- "model": "EA7300v1",
- "board": [
- "linksys,ea7300-v1"
- ]
- },
- "commands": {
- "params": [
- "boot_part",
- "bootcmd"
- ],
- "get": "fw_printenv",
- "set": "fw_setenv",
- "save": null
- },
- "partitions": [
- {
- "number": 1,
- "param_values": [
- 1,
- "run nandboot"
- ],
- "mtd": "mtd5",
- "labelOffsetBytes": 32
- },
- {
- "number": 2,
- "param_values": [
- 2,
- "run altnandboot"
- ],
- "mtd": "mtd7",
- "labelOffsetBytes": 32
- }
- ]
- },
- {
- "device": {
- "vendor": "Linksys",
- "model": "EA7300v2",
- "board": [
- "linksys,ea7300-v2"
- ]
- },
- "commands": {
- "params": [
- "boot_part",
- "bootcmd"
- ],
- "get": "fw_printenv",
- "set": "fw_setenv",
- "save": null
- },
- "partitions": [
- {
- "number": 1,
- "param_values": [
- 1,
- "run nandboot"
- ],
- "mtd": "mtd5",
- "labelOffsetBytes": 32
- },
- {
- "number": 2,
- "param_values": [
- 2,
- "run altnandboot"
- ],
- "mtd": "mtd7",
- "labelOffsetBytes": 32
- }
- ]
- },
- {
- "device": {
- "vendor": "Linksys",
- "model": "EA7500v1",
- "board": [
- "linksys,ea7500-v1"
- ]
- },
- "commands": {
- "params": [
- "boot_part"
- ],
- "get": "fw_printenv",
- "set": "fw_setenv",
- "save": null
- },
- "partitions": [
- {
- "number": 1,
- "param_values": [
- 1
- ],
- "mtd": "mtd13",
- "labelOffsetBytes": 32
- },
- {
- "number": 2,
- "param_values": [
- 2
- ],
- "mtd": "mtd15",
- "labelOffsetBytes": 32
- }
- ]
- },
- {
- "device": {
- "vendor": "Linksys",
- "model": "EA7500v2",
- "board": [
- "linksys,ea7500-v2"
- ]
- },
- "commands": {
- "params": [
- "boot_part",
- "bootcmd"
- ],
- "get": "fw_printenv",
- "set": "fw_setenv",
- "save": null
- },
- "partitions": [
- {
- "number": 1,
- "param_values": [
- 1,
- "run nandboot"
- ],
- "mtd": "mtd5",
- "labelOffsetBytes": 32
- },
- {
- "number": 2,
- "param_values": [
- 2,
- "run altnandboot"
- ],
- "mtd": "mtd7",
- "labelOffsetBytes": 32
- }
- ]
- },
- {
- "device": {
- "vendor": "Linksys",
- "model": "EA8100v1",
- "board": [
- "linksys,ea8100-v1"
- ]
- },
- "commands": {
- "params": [
- "boot_part"
- ],
- "get": "fw_printenv",
- "set": "fw_setenv",
- "save": null
- },
- "partitions": [
- {
- "number": 1,
- "param_values": [
- 1
- ],
- "mtd": "mtd5",
- "labelOffsetBytes": 32
- },
- {
- "number": 2,
- "param_values": [
- 2
- ],
- "mtd": "mtd7",
- "labelOffsetBytes": 32
- }
- ]
- },
- {
- "device": {
- "vendor": "Linksys",
- "model": "EA8100v2",
- "board": [
- "linksys,ea8100-v2"
- ]
- },
- "commands": {
- "params": [
- "boot_part"
- ],
- "get": "fw_printenv",
- "set": "fw_setenv",
- "save": null
- },
- "partitions": [
- {
- "number": 1,
- "param_values": [
- 1
- ],
- "mtd": "mtd5",
- "labelOffsetBytes": 32
- },
- {
- "number": 2,
- "param_values": [
- 2
- ],
- "mtd": "mtd7",
- "labelOffsetBytes": 32
- }
- ]
- },
- {
- "device": {
- "vendor": "Linksys",
- "model": "EA8300",
- "board": [
- "linksys-ea8300",
- "linksys,ea8300"
- ]
- },
- "commands": {
- "params": [
- "boot_part"
- ],
- "get": "fw_printenv",
- "set": "fw_setenv",
- "save": null
- },
- "partitions": [
- {
- "number": 1,
- "param_values": [
- 1
- ],
- "mtd": "mtd10",
- "labelOffsetBytes": 192
- },
- {
- "number": 2,
- "param_values": [
- 2
- ],
- "mtd": "mtd12",
- "labelOffsetBytes": 192
- }
- ]
- },
- {
- "device": {
- "vendor": "Linksys",
- "model": "EA8500",
- "board": [
- "linksys-ea8500",
- "linksys,ea8500"
- ]
- },
- "commands": {
- "params": [
- "boot_part"
- ],
- "get": "fw_printenv",
- "set": "fw_setenv",
- "save": null
- },
- "partitions": [
- {
- "number": 1,
- "param_values": [
- 1
- ],
- "mtd": "mtd13",
- "labelOffsetBytes": 32
- },
- {
- "number": 2,
- "param_values": [
- 2
- ],
- "mtd": "mtd15",
- "labelOffsetBytes": 32
- }
- ]
- },
- {
- "device": {
- "vendor": "Linksys",
- "model": "MR5500",
- "board": [
- "linksys,mr5500"
- ]
- },
- "commands": {
- "params": [
- "boot_part"
- ],
- "get": "fw_printenv",
- "set": "fw_setenv",
- "save": null
- },
- "partitions": [
- {
- "number": 1,
- "param_values": [
- 1
- ],
- "mtd": "mtd12",
- "labelOffsetBytes": 192
- },
- {
- "number": 2,
- "param_values": [
- 2
- ],
- "mtd": "mtd14",
- "labelOffsetBytes": 192
- }
- ]
- },
- {
- "device": {
- "vendor": "Linksys",
- "model": "MR7350",
- "board": [
- "linksys,mr7350"
- ]
- },
- "commands": {
- "params": [
- "boot_part"
- ],
- "get": "fw_printenv",
- "set": "fw_setenv",
- "save": null
- },
- "partitions": [
- {
- "number": 1,
- "param_values": [
- 1
- ],
- "mtd": "mtd14",
- "labelOffsetBytes": 192
- },
- {
- "number": 2,
- "param_values": [
- 2
- ],
- "mtd": "mtd16",
- "labelOffsetBytes": 192
- }
- ]
- },
- {
- "device": {
- "vendor": "Linksys",
- "model": "MR7500",
- "board": [
- "linksys,mr7500"
- ]
- },
- "commands": {
- "params": [
- "boot_part"
- ],
- "get": "fw_printenv",
- "set": "fw_setenv",
- "save": null
- },
- "partitions": [
- {
- "number": 1,
- "param_values": [
- 1
- ],
- "mtd": "mtd13",
- "labelOffsetBytes": 192
- },
- {
- "number": 2,
- "param_values": [
- 2
- ],
- "mtd": "mtd15",
- "labelOffsetBytes": 192
- }
- ]
- },
- {
- "device": {
- "vendor": "Linksys",
- "model": "MR8300",
- "board": [
- "linksys-mr8300",
- "linksys,mr8300"
- ]
- },
- "commands": {
- "params": [
- "boot_part"
- ],
- "get": "fw_printenv",
- "set": "fw_setenv",
- "save": null
- },
- "partitions": [
- {
- "number": 1,
- "param_values": [
- 1
- ],
- "mtd": "mtd10",
- "labelOffsetBytes": 192
- },
- {
- "number": 2,
- "param_values": [
- 2
- ],
- "mtd": "mtd12",
- "labelOffsetBytes": 192
- }
- ]
- },
- {
- "device": {
- "vendor": "Linksys",
- "model": "MR9000 (Dallas)",
- "board": [
- "linksys,mr9000"
- ]
- },
- "commands": {
- "params": [
- "boot_part"
- ],
- "get": "fw_printenv",
- "set": "fw_setenv",
- "save": null
- },
- "partitions": [
- {
- "number": 1,
- "param_values": [
- 1
- ],
- "mtd": "mtd10",
- "labelOffsetBytes": 192
- },
- {
- "number": 2,
- "param_values": [
- 2
- ],
- "mtd": "mtd12",
- "labelOffsetBytes": 192
- }
- ]
- },
- {
- "device": {
- "vendor": "Linksys",
- "model": "MX2000",
- "board": [
- "linksys,mx2000"
- ]
- },
- "commands": {
- "params": [
- "boot_part"
- ],
- "get": "fw_printenv",
- "set": "fw_setenv",
- "save": null
- },
- "partitions": [
- {
- "number": 1,
- "param_values": [
- 1
- ],
- "mtd": "mtd12",
- "labelOffsetBytes": 192
- },
- {
- "number": 2,
- "param_values": [
- 2
- ],
- "mtd": "mtd14",
- "labelOffsetBytes": 192
- }
- ]
- },
- {
- "device": {
- "vendor": "Linksys",
- "model": "MX4200v1",
- "board": [
- "linksys,mx4200v1"
- ]
- },
- "commands": {
- "params": [
- "boot_part"
- ],
- "get": "fw_printenv",
- "set": "fw_setenv",
- "save": null
- },
- "partitions": [
- {
- "number": 1,
- "param_values": [
- 1
- ],
- "mtd": "mtd21",
- "labelOffsetBytes": 192
- },
- {
- "number": 2,
- "param_values": [
- 2
- ],
- "mtd": "mtd23",
- "labelOffsetBytes": 192
- }
- ]
- },
- {
- "device": {
- "vendor": "Linksys",
- "model": "MX4200v2",
- "board": [
- "linksys,mx4200v2"
- ]
- },
- "commands": {
- "params": [
- "boot_part"
- ],
- "get": "fw_printenv",
- "set": "fw_setenv",
- "save": null
- },
- "partitions": [
- {
- "number": 1,
- "param_values": [
- 1
- ],
- "mtd": "mtd21",
- "labelOffsetBytes": 192
- },
- {
- "number": 2,
- "param_values": [
- 2
- ],
- "mtd": "mtd23",
- "labelOffsetBytes": 192
- }
- ]
- },
- {
- "device": {
- "vendor": "Linksys",
- "model": "MX4300",
- "board": [
- "linksys,mx4300"
- ]
- },
- "commands": {
- "params": [
- "boot_part"
- ],
- "get": "fw_printenv",
- "set": "fw_setenv",
- "save": null
- },
- "partitions": [
- {
- "number": 1,
- "param_values": [
- 1
- ],
- "mtd": "mtd21",
- "labelOffsetBytes": 192
- },
- {
- "number": 2,
- "param_values": [
- 2
- ],
- "mtd": "mtd23",
- "labelOffsetBytes": 192
- }
- ]
- },
- {
- "device": {
- "vendor": "Linksys",
- "model": "MX5300",
- "board": [
- "linksys,mx5300"
- ]
- },
- "commands": {
- "params": [
- "boot_part"
- ],
- "get": "fw_printenv",
- "set": "fw_setenv",
- "save": null
- },
- "partitions": [
- {
- "number": 1,
- "param_values": [
- 1
- ],
- "mtd": "mtd21",
- "labelOffsetBytes": 192
- },
- {
- "number": 2,
- "param_values": [
- 2
- ],
- "mtd": "mtd23",
- "labelOffsetBytes": 192
- }
- ]
- },
- {
- "device": {
- "vendor": "Linksys",
- "model": "MX5500",
- "board": [
- "linksys,mx5500"
- ]
- },
- "commands": {
- "params": [
- "boot_part"
- ],
- "get": "fw_printenv",
- "set": "fw_setenv",
- "save": null
- },
- "partitions": [
- {
- "number": 1,
- "param_values": [
- 1
- ],
- "mtd": "mtd12",
- "labelOffsetBytes": 192
- },
- {
- "number": 2,
- "param_values": [
- 2
- ],
- "mtd": "mtd14",
- "labelOffsetBytes": 192
- }
- ]
- },
- {
- "device": {
- "vendor": "Linksys",
- "model": "MX8500",
- "board": [
- "linksys,mx8500"
- ]
- },
- "commands": {
- "params": [
- "boot_part"
- ],
- "get": "fw_printenv",
- "set": "fw_setenv",
- "save": null
- },
- "partitions": [
- {
- "number": 1,
- "param_values": [
- 1
- ],
- "mtd": "mtd21",
- "labelOffsetBytes": 192
- },
- {
- "number": 2,
- "param_values": [
- 2
- ],
- "mtd": "mtd23",
- "labelOffsetBytes": 192
- }
- ]
- },
- {
- "device": {
- "vendor": "Linksys",
- "model": "SPNMX56",
- "board": [
- "linksys,spnmx56"
- ]
- },
- "commands": {
- "params": [
- "boot_part"
- ],
- "get": "fw_printenv",
- "set": "fw_setenv",
- "save": null
- },
- "partitions": [
- {
- "number": 1,
- "param_values": [
- 1
- ],
- "mtd": "mtd12",
- "labelOffsetBytes": 192
- },
- {
- "number": 2,
- "param_values": [
- 2
- ],
- "mtd": "mtd14",
- "labelOffsetBytes": 192
- }
- ]
- },
- {
- "device": {
- "vendor": "Linksys",
- "model": "WHW01 V1 (Velop)",
- "board": [
- "linksys,whw01"
- ]
- },
- "commands": {
- "params": [
- "boot_part"
- ],
- "get": "fw_printenv",
- "set": "fw_setenv",
- "save": null
- },
- "partitions": [
- {
- "number": 1,
- "param_values": [
- 1
- ],
- "mtd": "mtd9",
- "labelOffsetBytes": 192
- },
- {
- "number": 2,
- "param_values": [
- 2
- ],
- "mtd": "mtd11",
- "labelOffsetBytes": 192
- }
- ]
- },
- {
- "device": {
- "vendor": "Linksys",
- "model": "WHW03 (Velop)",
- "board": [
- "linksys,whw03"
- ]
- },
- "commands": {
- "params": [
- "boot_part"
- ],
- "get": "fw_printenv",
- "set": "fw_setenv",
- "save": null
- },
- "partitions": [
- {
- "number": 1,
- "param_values": [
- 1
- ],
- "mtd": "mmcblk0p14",
- "labelOffsetBytes": 192
- },
- {
- "number": 2,
- "param_values": [
- 2
- ],
- "mtd": "mmcblk0p16",
- "labelOffsetBytes": 192
- }
- ]
- },
- {
- "device": {
- "vendor": "Linksys",
- "model": "WHW03 V2 (Velop)",
- "board": [
- "linksys-whw03v2",
- "linksys,whw03v2"
- ]
- },
- "commands": {
- "params": [
- "boot_part"
- ],
- "get": "fw_printenv",
- "set": "fw_setenv",
- "save": null
- },
- "partitions": [
- {
- "number": 1,
- "param_values": [
- 1
- ],
- "mtd": "mtd9",
- "labelOffsetBytes": 192
- },
- {
- "number": 2,
- "param_values": [
- 2
- ],
- "mtd": "mtd11",
- "labelOffsetBytes": 192
- }
- ]
- },
- {
- "device": {
- "vendor": "Linksys",
- "model": "WRT1200AC",
- "board": [
- "linksys-caiman",
- "linksys,caiman",
- "linksys,wrt1200ac"
- ]
- },
- "commands": {
- "params": [
- "boot_part",
- "bootcmd"
- ],
- "get": "fw_printenv",
- "set": "fw_setenv",
- "save": null
- },
- "partitions": [
- {
- "number": 1,
- "param_values": [
- 1,
- "run nandboot"
- ],
- "mtd": "mtd4",
- "labelOffsetBytes": 32
- },
- {
- "number": 2,
- "param_values": [
- 2,
- "run altnandboot"
- ],
- "mtd": "mtd6",
- "labelOffsetBytes": 32
- }
- ]
- },
- {
- "device": {
- "vendor": "Linksys",
- "model": "WRT1900ACv1",
- "board": [
- "linksys-mamba",
- "linksys,mamba",
- "linksys,wrt1900ac-v1"
- ]
- },
- "commands": {
- "params": [
- "boot_part",
- "bootcmd"
- ],
- "get": "fw_printenv",
- "set": "fw_setenv",
- "save": null
- },
- "partitions": [
- {
- "number": 1,
- "param_values": [
- 1,
- "run nandboot"
- ],
- "mtd": "mtd4",
- "labelOffsetBytes": 32
- },
- {
- "number": 2,
- "param_values": [
- 2,
- "run altnandboot"
- ],
- "mtd": "mtd6",
- "labelOffsetBytes": 32
- }
- ]
- },
- {
- "device": {
- "vendor": "Linksys",
- "model": "WRT1900ACS",
- "board": [
- "linksys-shelby",
- "linksys,shelby",
- "linksys,wrt1900acs"
- ]
- },
- "commands": {
- "params": [
- "boot_part",
- "bootcmd"
- ],
- "get": "fw_printenv",
- "set": "fw_setenv",
- "save": null
- },
- "partitions": [
- {
- "number": 1,
- "param_values": [
- 1,
- "run nandboot"
- ],
- "mtd": "mtd4",
- "labelOffsetBytes": 32
- },
- {
- "number": 2,
- "param_values": [
- 2,
- "run altnandboot"
- ],
- "mtd": "mtd6",
- "labelOffsetBytes": 32
- }
- ]
- },
- {
- "device": {
- "vendor": "Linksys",
- "model": "WRT1900ACv2",
- "board": [
- "linksys-cobra",
- "linksys,cobra",
- "linksys,wrt1900ac-v2"
- ]
- },
- "commands": {
- "params": [
- "boot_part",
- "bootcmd"
- ],
- "get": "fw_printenv",
- "set": "fw_setenv",
- "save": null
- },
- "partitions": [
- {
- "number": 1,
- "param_values": [
- 1,
- "run nandboot"
- ],
- "mtd": "mtd4",
- "labelOffsetBytes": 32
- },
- {
- "number": 2,
- "param_values": [
- 2,
- "run altnandboot"
- ],
- "mtd": "mtd6",
- "labelOffsetBytes": 32
- }
- ]
- },
- {
- "device": {
- "vendor": "Linksys",
- "model": "WRT3200ACM",
- "board": [
- "linksys-rango",
- "linksys,rango",
- "linksys,wrt3200acm"
- ]
- },
- "commands": {
- "params": [
- "boot_part",
- "bootcmd"
- ],
- "get": "fw_printenv",
- "set": "fw_setenv",
- "save": null
- },
- "partitions": [
- {
- "number": 1,
- "param_values": [
- 1,
- "run nandboot"
- ],
- "mtd": "mtd5",
- "labelOffsetBytes": 32
- },
- {
- "number": 2,
- "param_values": [
- 2,
- "run altnandboot"
- ],
- "mtd": "mtd7",
- "labelOffsetBytes": 32
- }
- ]
- },
- {
- "device": {
- "vendor": "Linksys",
- "model": "WRT32X",
- "board": [
- "linksys-venom",
- "linksys,venom",
- "linksys,wrt32x"
- ]
- },
- "commands": {
- "params": [
- "boot_part",
- "bootcmd"
- ],
- "get": "fw_printenv",
- "set": "fw_setenv",
- "save": null
- },
- "partitions": [
- {
- "number": 1,
- "param_values": [
- 1,
- "run nandboot"
- ],
- "mtd": "mtd5",
- "labelOffsetBytes": null
- },
- {
- "number": 2,
- "param_values": [
- 2,
- "run altnandboot"
- ],
- "mtd": "mtd7",
- "labelOffsetBytes": null
- }
- ]
- },
- {
- "device": {
- "vendor": "MERCUSYS",
- "model": "MR90X v1",
- "board": [
- "mercusys,mr90x-v1"
- ]
- },
- "commands": {
- "params": [
- "tp_boot_idx"
- ],
- "get": "fw_printenv",
- "set": "fw_setenv",
- "save": null
- },
- "partitions": [
- {
- "number": 1,
- "param_values": [
- 0
- ],
- "mtd": "mtd2",
- "labelOffsetBytes": null,
- "altMountOptions": {
- "mtdOffset": 0,
- "ubiVolume": 2
- }
- },
- {
- "number": 2,
- "param_values": [
- 1
- ],
- "mtd": "mtd3",
- "labelOffsetBytes": null,
- "altMountOptions": {
- "mtdOffset": 0,
- "ubiVolume": 2
- }
- }
- ]
- },
- {
- "device": {
- "vendor": "Netgear",
- "model": "GS308T v1",
- "board": [
- "netgear,gs308t-v1"
- ]
- },
- "commands": {
- "params": [
- "bootpartition"
- ],
- "get": "fw_printsys",
- "set": "fw_setsys",
- "save": null
- },
- "partitions": [
- {
- "number": 1,
- "param_values": [
- 0
- ],
- "mtd": "mtd6",
- "labelOffsetBytes": null
- },
- {
- "number": 2,
- "param_values": [
- 1
- ],
- "mtd": "mtd9",
- "labelOffsetBytes": null
- }
- ]
- },
- {
- "device": {
- "vendor": "ZyXEL",
- "model": "NBG6817",
- "board": [
- "nbg6817",
- "zyxel,nbg6817"
- ]
- },
- "commands": {
- "params": [],
- "get": null,
- "set": null,
- "save": null
- },
- "partitions": [
- {
- "number": 1,
- "param_values": [
- "ff"
- ],
- "mtd": "mmcblk0p4",
- "labelOffsetBytes": 32
- },
- {
- "number": 2,
- "param_values": [
- "01"
- ],
- "mtd": "mmcblk0p7",
- "labelOffsetBytes": 32
- }
- ]
- }
-]
+++ /dev/null
-# Tabs only for indentation, spaces ok in comments.
-
-# jq -f _device_json_transform.jq old.json
-# Transform from the old schema:
-# {
-# "vendorName": "...",
-# "deviceName": "...",
-# "boardNames": ["..."],
-# "partition1MTD": "mtdX",
-# "partition2MTD": "mtdY",
-# "labelOffset": 32,
-# "bootEnv1": "boot_part",
-# "bootEnv1Partition1Value": 1,
-# "bootEnv1Partition2Value": 2,
-# "bootEnv2": "bootcmd",
-# "bootEnv2Partition1Value": "run nandboot",
-# "bootEnv2Partition2Value": "run altnandboot",
-# "opOffset": 0,
-# "ubiVolume": 2
-# }
-#
-# …to the new schema:
-# {
-# "device": { "vendor": "...", "model": "...", "board": ["..."] },
-# "commands": { "params": [...], "get": "fw_printenv", "set": "fw_setenv", "save": null },
-# "partitions": [
-# { "number": 1, "param_values": [...], "mtd": "mtdX", "labelOffsetBytes": <int|null>, "altMountOptions": {...|null} },
-# { "number": 2, "param_values": [...], "mtd": "mtdY", "labelOffsetBytes": <int|null>, "altMountOptions": {...|null} }
-# ]
-# }
-
-. as $in
-| ([$in.bootEnv1, $in.bootEnv2] | map(select(. != null))) as $params
-| {
- device: {
- vendor: $in.vendorName,
- model: $in.deviceName,
- board: (if ($in.boardNames | type) == "array" then $in.boardNames else [$in.boardNames] end)
- },
- commands: {
- params: $params,
- get: "fw_printenv",
- set: "fw_setenv",
- save: null
- },
- partitions: [
- (if $in.partition1MTD != null then
- {
- number: 1,
- param_values: (
- # Align to commands.params: value for param[0], param[1], ...
- # Only include entries that exist (skip nulls)
- [
- (if ($params | length) > 0 then $in.bootEnv1Partition1Value else empty end),
- (if ($params | length) > 1 then $in.bootEnv2Partition1Value else empty end)
- ] | map(select(. != null))
- ),
- mtd: $in.partition1MTD,
- labelOffsetBytes: ($in.labelOffset // null)
- } + (
- if ($in.opOffset != null or $in.ubiVolume != null) then
- { altMountOptions: { mtdOffset: ($in.opOffset // null), ubiVolume: ($in.ubiVolume // null) } }
- else
- {}
- end
- )
- else empty end),
- (if $in.partition2MTD != null then
- {
- number: 2,
- param_values: (
- [
- (if ($params | length) > 0 then $in.bootEnv1Partition2Value else empty end),
- (if ($params | length) > 1 then $in.bootEnv2Partition2Value else empty end)
- ] | map(select(. != null))
- ),
- mtd: $in.partition2MTD,
- labelOffsetBytes: ($in.labelOffset // null)
- } + (
- if ($in.opOffset != null or $in.ubiVolume != null) then
- { altMountOptions: { mtdOffset: ($in.opOffset // null), ubiVolume: ($in.ubiVolume // null) } }
- else
- {}
- end
- )
- else empty end)
- ]
-}
{
"number": 1,
"param_values": [],
- "mtd": "mtd5",
- "labelOffsetBytes": null
+ "firmware": {
+ "rootfs": "mtd6"
+ }
},
{
"number": 2,
"run addargs\\; bootm 0xb4e80000",
"/dev/mtdblock7"
],
- "mtd": "mtd9",
- "labelOffsetBytes": null
+ "firmware": {
+ "rootfs": "mtd10"
+ }
}
]
}
1,
"run nandboot"
],
- "mtd": "mtd3",
- "labelOffsetBytes": 32
+ "firmware": {
+ "kernel": "mtd3",
+ "kernel_label_offset": 32,
+ "rootfs": "mtd4"
+ }
},
{
"number": 2,
2,
"run altnandboot"
],
- "mtd": "mtd5",
- "labelOffsetBytes": 32
+ "firmware": {
+ "kernel": "mtd5",
+ "kernel_label_offset": 32,
+ "rootfs": "mtd6"
+ }
}
]
}
1,
"run nandboot"
],
- "mtd": "mtd3",
- "labelOffsetBytes": 32
+ "firmware": {
+ "kernel": "mtd3",
+ "kernel_label_offset": 32,
+ "rootfs": "mtd4"
+ }
},
{
"number": 2,
2,
"run altnandboot"
],
- "mtd": "mtd5",
- "labelOffsetBytes": 32
+ "firmware": {
+ "kernel": "mtd5",
+ "kernel_label_offset": 32,
+ "rootfs": "mtd6"
+ }
}
]
}
"param_values": [
1
],
- "mtd": "mtd3",
- "labelOffsetBytes": 192
+ "firmware": {
+ "kernel": "mtd3",
+ "kernel_label_offset": 192,
+ "rootfs": "mtd4"
+ }
},
{
"number": 2,
"param_values": [
2
],
- "mtd": "mtd6",
- "labelOffsetBytes": 192
+ "firmware": {
+ "kernel": "mtd6",
+ "kernel_label_offset": 192,
+ "rootfs": "mtd7"
+ }
}
]
}
1,
"run nandboot"
],
- "mtd": "mtd3",
- "labelOffsetBytes": 32
+ "firmware": {
+ "kernel": "mtd3",
+ "kernel_label_offset": 32,
+ "rootfs": "mtd4"
+ }
},
{
"number": 2,
2,
"run altnandboot"
],
- "mtd": "mtd5",
- "labelOffsetBytes": 32
+ "firmware": {
+ "kernel": "mtd5",
+ "kernel_label_offset": 32,
+ "rootfs": "mtd6"
+ }
}
]
}
1,
"run nandboot"
],
- "mtd": "mtd3",
- "labelOffsetBytes": 32
+ "firmware": {
+ "kernel": "mtd3",
+ "kernel_label_offset": 32,
+ "rootfs": "mtd4"
+ }
},
{
"number": 2,
2,
"run altnandboot"
],
- "mtd": "mtd5",
- "labelOffsetBytes": 32
+ "firmware": {
+ "kernel": "mtd5",
+ "kernel_label_offset": 32,
+ "rootfs": "mtd6"
+ }
}
]
}
"param_values": [
1
],
- "mtd": "mtd10",
- "labelOffsetBytes": 192
+ "firmware": {
+ "kernel": "mtd10",
+ "kernel_label_offset": 192,
+ "rootfs": "mtd11"
+ }
},
{
"number": 2,
"param_values": [
2
],
- "mtd": "mtd12",
- "labelOffsetBytes": 192
+ "firmware": {
+ "kernel": "mtd12",
+ "kernel_label_offset": 192,
+ "rootfs": "mtd13"
+ }
}
]
}
"param_values": [
1
],
- "mtd": "mtd5",
- "labelOffsetBytes": 32
+ "firmware": {
+ "kernel": "mtd5",
+ "kernel_label_offset": 32,
+ "rootfs": "mtd6"
+ }
},
{
"number": 2,
"param_values": [
2
],
- "mtd": "mtd7",
- "labelOffsetBytes": 32
+ "firmware": {
+ "kernel": "mtd7",
+ "kernel_label_offset": 32,
+ "rootfs": "mtd8"
+ }
}
]
}
1,
"run nandboot"
],
- "mtd": "mtd5",
- "labelOffsetBytes": 32
+ "firmware": {
+ "kernel": "mtd5",
+ "kernel_label_offset": 32,
+ "rootfs": "mtd6"
+ }
},
{
"number": 2,
2,
"run altnandboot"
],
- "mtd": "mtd7",
- "labelOffsetBytes": 32
+ "firmware": {
+ "kernel": "mtd7",
+ "kernel_label_offset": 32,
+ "rootfs": "mtd8"
+ }
}
]
}
1,
"run nandboot"
],
- "mtd": "mtd5",
- "labelOffsetBytes": 32
+ "firmware": {
+ "kernel": "mtd5",
+ "kernel_label_offset": 32,
+ "rootfs": "mtd6"
+ }
},
{
"number": 2,
2,
"run altnandboot"
],
- "mtd": "mtd7",
- "labelOffsetBytes": 32
+ "firmware": {
+ "kernel": "mtd7",
+ "kernel_label_offset": 32,
+ "rootfs": "mtd8"
+ }
}
]
}
"param_values": [
1
],
- "mtd": "mtd13",
- "labelOffsetBytes": 32
+ "firmware": {
+ "kernel": "mtd13",
+ "kernel_label_offset": 32,
+ "rootfs": "mtd14"
+ }
},
{
"number": 2,
"param_values": [
2
],
- "mtd": "mtd15",
- "labelOffsetBytes": 32
+ "firmware": {
+ "kernel": "mtd15",
+ "kernel_label_offset": 32,
+ "rootfs": "mtd16"
+ }
}
]
}
1,
"run nandboot"
],
- "mtd": "mtd5",
- "labelOffsetBytes": 32
+ "firmware": {
+ "kernel": "mtd5",
+ "kernel_label_offset": 32,
+ "rootfs": "mtd6"
+ }
},
{
"number": 2,
2,
"run altnandboot"
],
- "mtd": "mtd7",
- "labelOffsetBytes": 32
+ "firmware": {
+ "kernel": "mtd7",
+ "kernel_label_offset": 32,
+ "rootfs": "mtd8"
+ }
}
]
}
--- /dev/null
+{
+ "device": {
+ "vendor": "Linksys",
+ "model": "EA7500v3",
+ "board": [
+ "linksys,ea7500-v3"
+ ]
+ },
+ "commands": {
+ "params": [
+ "boot_part",
+ "bootimage"
+ ],
+ "get": "fw_printenv",
+ "set": "fw_setenv",
+ "save": null
+ },
+ "partitions": [
+ {
+ "number": 1,
+ "param_values": [
+ 1,
+ 1
+ ],
+ "firmware": {
+ "kernel": "mtd3",
+ "kernel_label_offset": 32,
+ "rootfs": "mtd4"
+ }
+ },
+ {
+ "number": 2,
+ "param_values": [
+ 2,
+ 2
+ ],
+ "firmware": {
+ "kernel": "mtd5",
+ "kernel_label_offset": 32,
+ "rootfs": "mtd6"
+ }
+ }
+ ]
+}
"param_values": [
1
],
- "mtd": "mtd5",
- "labelOffsetBytes": 32
+ "firmware": {
+ "kernel": "mtd5",
+ "kernel_label_offset": 32,
+ "rootfs": "mtd6"
+ }
},
{
"number": 2,
"param_values": [
2
],
- "mtd": "mtd7",
- "labelOffsetBytes": 32
+ "firmware": {
+ "kernel": "mtd7",
+ "kernel_label_offset": 32,
+ "rootfs": "mtd8"
+ }
}
]
}
"param_values": [
1
],
- "mtd": "mtd5",
- "labelOffsetBytes": 32
+ "firmware": {
+ "kernel": "mtd5",
+ "kernel_label_offset": 32,
+ "rootfs": "mtd6"
+ }
},
{
"number": 2,
"param_values": [
2
],
- "mtd": "mtd7",
- "labelOffsetBytes": 32
+ "firmware": {
+ "kernel": "mtd7",
+ "kernel_label_offset": 32,
+ "rootfs": "mtd8"
+ }
}
]
}
"param_values": [
1
],
- "mtd": "mtd10",
- "labelOffsetBytes": 192
+ "firmware": {
+ "kernel": "mtd10",
+ "kernel_label_offset": 192,
+ "rootfs": "mtd11"
+ }
},
{
"number": 2,
"param_values": [
2
],
- "mtd": "mtd12",
- "labelOffsetBytes": 192
+ "firmware": {
+ "kernel": "mtd12",
+ "kernel_label_offset": 192,
+ "rootfs": "mtd13"
+ }
}
]
}
"param_values": [
1
],
- "mtd": "mtd13",
- "labelOffsetBytes": 32
+ "firmware": {
+ "kernel": "mtd13",
+ "kernel_label_offset": 32,
+ "rootfs": "mtd14"
+ }
},
{
"number": 2,
"param_values": [
2
],
- "mtd": "mtd15",
- "labelOffsetBytes": 32
+ "firmware": {
+ "kernel": "mtd15",
+ "kernel_label_offset": 32,
+ "rootfs": "mtd16"
+ }
}
]
}
"param_values": [
1
],
- "mtd": "mtd12",
- "labelOffsetBytes": 192
+ "firmware": {
+ "kernel": "mtd12",
+ "kernel_label_offset": 192,
+ "rootfs": "mtd13"
+ }
},
{
"number": 2,
"param_values": [
2
],
- "mtd": "mtd14",
- "labelOffsetBytes": 192
+ "firmware": {
+ "kernel": "mtd14",
+ "kernel_label_offset": 192,
+ "rootfs": "mtd15"
+ }
}
]
}
"param_values": [
1
],
- "mtd": "mtd14",
- "labelOffsetBytes": 192
+ "firmware": {
+ "kernel": "mtd14",
+ "kernel_label_offset": 192,
+ "rootfs": "mtd15"
+ }
},
{
"number": 2,
"param_values": [
2
],
- "mtd": "mtd16",
- "labelOffsetBytes": 192
+ "firmware": {
+ "kernel": "mtd16",
+ "kernel_label_offset": 192,
+ "rootfs": "mtd17"
+ }
}
]
}
"param_values": [
1
],
- "mtd": "mtd13",
- "labelOffsetBytes": 192
+ "firmware": {
+ "kernel": "mtd13",
+ "kernel_label_offset": 192,
+ "rootfs": "mtd14"
+ }
},
{
"number": 2,
"param_values": [
2
],
- "mtd": "mtd15",
- "labelOffsetBytes": 192
+ "firmware": {
+ "kernel": "mtd15",
+ "kernel_label_offset": 192,
+ "rootfs": "mtd16"
+ }
}
]
}
"param_values": [
1
],
- "mtd": "mtd10",
- "labelOffsetBytes": 192
+ "firmware": {
+ "kernel": "mtd10",
+ "kernel_label_offset": 192,
+ "rootfs": "mtd11"
+ }
},
{
"number": 2,
"param_values": [
2
],
- "mtd": "mtd12",
- "labelOffsetBytes": 192
+ "firmware": {
+ "kernel": "mtd12",
+ "kernel_label_offset": 192,
+ "rootfs": "mtd13"
+ }
}
]
}
"param_values": [
1
],
- "mtd": "mtd10",
- "labelOffsetBytes": 192
+ "firmware": {
+ "kernel": "mtd10",
+ "kernel_label_offset": 192,
+ "rootfs": "mtd11"
+ }
},
{
"number": 2,
"param_values": [
2
],
- "mtd": "mtd12",
- "labelOffsetBytes": 192
+ "firmware": {
+ "kernel": "mtd12",
+ "kernel_label_offset": 192,
+ "rootfs": "mtd13"
+ }
}
]
}
"param_values": [
1
],
- "mtd": "mtd12",
- "labelOffsetBytes": 192
+ "firmware": {
+ "kernel": "mtd12",
+ "kernel_label_offset": 192,
+ "rootfs": "mtd13"
+ }
},
{
"number": 2,
"param_values": [
2
],
- "mtd": "mtd14",
- "labelOffsetBytes": 192
+ "firmware": {
+ "kernel": "mtd14",
+ "kernel_label_offset": 192,
+ "rootfs": "mtd15"
+ }
}
]
}
"param_values": [
1
],
- "mtd": "mtd21",
- "labelOffsetBytes": 192
+ "firmware": {
+ "kernel": "mtd21",
+ "kernel_label_offset": 192,
+ "rootfs": "mtd22"
+ }
},
{
"number": 2,
"param_values": [
2
],
- "mtd": "mtd23",
- "labelOffsetBytes": 192
+ "firmware": {
+ "kernel": "mtd23",
+ "kernel_label_offset": 192,
+ "rootfs": "mtd24"
+ }
}
]
}
"param_values": [
1
],
- "mtd": "mtd21",
- "labelOffsetBytes": 192
+ "firmware": {
+ "kernel": "mtd21",
+ "kernel_label_offset": 192,
+ "rootfs": "mtd22"
+ }
},
{
"number": 2,
"param_values": [
2
],
- "mtd": "mtd23",
- "labelOffsetBytes": 192
+ "firmware": {
+ "kernel": "mtd23",
+ "kernel_label_offset": 192,
+ "rootfs": "mtd24"
+ }
}
]
}
"param_values": [
1
],
- "mtd": "mtd21",
- "labelOffsetBytes": 192
+ "firmware": {
+ "kernel": "mtd21",
+ "kernel_label_offset": 192,
+ "rootfs": "mtd22"
+ }
},
{
"number": 2,
"param_values": [
2
],
- "mtd": "mtd23",
- "labelOffsetBytes": 192
+ "firmware": {
+ "kernel": "mtd23",
+ "kernel_label_offset": 192,
+ "rootfs": "mtd24"
+ }
}
]
}
"param_values": [
1
],
- "mtd": "mtd21",
- "labelOffsetBytes": 192
+ "firmware": {
+ "kernel": "mtd21",
+ "kernel_label_offset": 192,
+ "rootfs": "mtd22"
+ }
},
{
"number": 2,
"param_values": [
2
],
- "mtd": "mtd23",
- "labelOffsetBytes": 192
+ "firmware": {
+ "kernel": "mtd23",
+ "kernel_label_offset": 192,
+ "rootfs": "mtd24"
+ }
}
]
}
"param_values": [
1
],
- "mtd": "mtd12",
- "labelOffsetBytes": 192
+ "firmware": {
+ "kernel": "mtd12",
+ "kernel_label_offset": 192,
+ "rootfs": "mtd13"
+ }
},
{
"number": 2,
"param_values": [
2
],
- "mtd": "mtd14",
- "labelOffsetBytes": 192
+ "firmware": {
+ "kernel": "mtd14",
+ "kernel_label_offset": 192,
+ "rootfs": "mtd15"
+ }
}
]
}
--- /dev/null
+{
+ "device": {
+ "vendor": "Linksys",
+ "model": "MX6200",
+ "board": ["linksys,mx6200"]
+ },
+ "commands": {
+ "params": ["boot_part"],
+ "get": "fw_printenv",
+ "set": "fw_setenv",
+ "save": null,
+ "set_helper": "/usr/share/advanced-reboot/helpers/linksys-mx6200.sh"
+ },
+ "partitions": [
+ {
+ "number": 1,
+ "param_values": [1],
+ "firmware": {
+ "rootfs": "mtd19",
+ "rootfs_ubi_volume": 1
+ }
+ },
+ {
+ "number": 2,
+ "param_values": [2],
+ "firmware": {
+ "rootfs": "mtd19",
+ "rootfs_ubi_volume": 1
+ }
+ }
+ ]
+}
"param_values": [
1
],
- "mtd": "mtd21",
- "labelOffsetBytes": 192
+ "firmware": {
+ "kernel": "mtd21",
+ "kernel_label_offset": 192,
+ "rootfs": "mtd22"
+ }
},
{
"number": 2,
"param_values": [
2
],
- "mtd": "mtd23",
- "labelOffsetBytes": 192
+ "firmware": {
+ "kernel": "mtd23",
+ "kernel_label_offset": 192,
+ "rootfs": "mtd24"
+ }
}
]
}
"param_values": [
1
],
- "mtd": "mtd12",
- "labelOffsetBytes": 192
+ "firmware": {
+ "kernel": "mtd12",
+ "kernel_label_offset": 192,
+ "rootfs": "mtd13"
+ }
},
{
"number": 2,
"param_values": [
2
],
- "mtd": "mtd14",
- "labelOffsetBytes": 192
+ "firmware": {
+ "kernel": "mtd14",
+ "kernel_label_offset": 192,
+ "rootfs": "mtd15"
+ }
}
]
}
"param_values": [
1
],
- "mtd": "mtd9",
- "labelOffsetBytes": 192
+ "firmware": {
+ "kernel": "mtd9",
+ "kernel_label_offset": 192,
+ "rootfs": "mtd10"
+ }
},
{
"number": 2,
"param_values": [
2
],
- "mtd": "mtd11",
- "labelOffsetBytes": 192
+ "firmware": {
+ "kernel": "mtd11",
+ "kernel_label_offset": 192,
+ "rootfs": "mtd12"
+ }
}
]
}
"param_values": [
1
],
- "mtd": "mmcblk0p14",
- "labelOffsetBytes": 192
+ "firmware": {
+ "kernel": "mmcblk0p14",
+ "kernel_label_offset": 192
+ }
},
{
"number": 2,
"param_values": [
2
],
- "mtd": "mmcblk0p16",
- "labelOffsetBytes": 192
+ "firmware": {
+ "kernel": "mmcblk0p16",
+ "kernel_label_offset": 192
+ }
}
]
}
"param_values": [
1
],
- "mtd": "mtd9",
- "labelOffsetBytes": 192
+ "firmware": {
+ "kernel": "mtd9",
+ "kernel_label_offset": 192,
+ "rootfs": "mtd10"
+ }
},
{
"number": 2,
"param_values": [
2
],
- "mtd": "mtd11",
- "labelOffsetBytes": 192
+ "firmware": {
+ "kernel": "mtd11",
+ "kernel_label_offset": 192,
+ "rootfs": "mtd12"
+ }
}
]
}
1,
"run nandboot"
],
- "mtd": "mtd4",
- "labelOffsetBytes": 32
+ "firmware": {
+ "kernel": "mtd4",
+ "kernel_label_offset": 32,
+ "rootfs": "mtd5"
+ }
},
{
"number": 2,
2,
"run altnandboot"
],
- "mtd": "mtd6",
- "labelOffsetBytes": 32
+ "firmware": {
+ "kernel": "mtd6",
+ "kernel_label_offset": 32,
+ "rootfs": "mtd7"
+ }
}
]
}
1,
"run nandboot"
],
- "mtd": "mtd4",
- "labelOffsetBytes": 32
+ "firmware": {
+ "kernel": "mtd4",
+ "kernel_label_offset": 32,
+ "rootfs": "mtd5"
+ }
},
{
"number": 2,
2,
"run altnandboot"
],
- "mtd": "mtd6",
- "labelOffsetBytes": 32
+ "firmware": {
+ "kernel": "mtd6",
+ "kernel_label_offset": 32,
+ "rootfs": "mtd7"
+ }
}
]
}
1,
"run nandboot"
],
- "mtd": "mtd4",
- "labelOffsetBytes": 32
+ "firmware": {
+ "kernel": "mtd4",
+ "kernel_label_offset": 32,
+ "rootfs": "mtd5"
+ }
},
{
"number": 2,
2,
"run altnandboot"
],
- "mtd": "mtd6",
- "labelOffsetBytes": 32
+ "firmware": {
+ "kernel": "mtd6",
+ "kernel_label_offset": 32,
+ "rootfs": "mtd7"
+ }
}
]
}
1,
"run nandboot"
],
- "mtd": "mtd4",
- "labelOffsetBytes": 32
+ "firmware": {
+ "kernel": "mtd4",
+ "kernel_label_offset": 32,
+ "rootfs": "mtd5"
+ }
},
{
"number": 2,
2,
"run altnandboot"
],
- "mtd": "mtd6",
- "labelOffsetBytes": 32
+ "firmware": {
+ "kernel": "mtd6",
+ "kernel_label_offset": 32,
+ "rootfs": "mtd7"
+ }
}
]
}
1,
"run nandboot"
],
- "mtd": "mtd5",
- "labelOffsetBytes": 32
+ "firmware": {
+ "kernel": "mtd5",
+ "kernel_label_offset": 32,
+ "rootfs": "mtd6"
+ }
},
{
"number": 2,
2,
"run altnandboot"
],
- "mtd": "mtd7",
- "labelOffsetBytes": 32
+ "firmware": {
+ "kernel": "mtd7",
+ "kernel_label_offset": 32,
+ "rootfs": "mtd8"
+ }
}
]
}
1,
"run nandboot"
],
- "mtd": "mtd5",
- "labelOffsetBytes": null
+ "firmware": {
+ "rootfs": "mtd6"
+ }
},
{
"number": 2,
2,
"run altnandboot"
],
- "mtd": "mtd7",
- "labelOffsetBytes": null
+ "firmware": {
+ "rootfs": "mtd8"
+ }
}
]
}
"param_values": [
0
],
- "mtd": "mtd2",
- "labelOffsetBytes": null,
- "altMountOptions": {
- "mtdOffset": 0,
- "ubiVolume": 2
+ "firmware": {
+ "rootfs": "mtd2",
+ "rootfs_ubi_volume": 2
}
},
{
"param_values": [
1
],
- "mtd": "mtd3",
- "labelOffsetBytes": null,
- "altMountOptions": {
- "mtdOffset": 0,
- "ubiVolume": 2
+ "firmware": {
+ "rootfs": "mtd3",
+ "rootfs_ubi_volume": 2
}
}
]
"param_values": [
0
],
- "mtd": "mtd6",
- "labelOffsetBytes": null
+ "firmware": {
+ "rootfs": "mtd7"
+ }
},
{
"number": 2,
"param_values": [
1
],
- "mtd": "mtd9",
- "labelOffsetBytes": null
+ "firmware": {
+ "rootfs": "mtd10"
+ }
}
]
}
"param_values": [
"ff"
],
- "mtd": "mmcblk0p4",
- "labelOffsetBytes": 32
+ "firmware": {
+ "kernel": "mmcblk0p4",
+ "kernel_label_offset": 32
+ }
},
{
"number": 2,
"param_values": [
"01"
],
- "mtd": "mmcblk0p7",
- "labelOffsetBytes": 32
+ "firmware": {
+ "kernel": "mmcblk0p7",
+ "kernel_label_offset": 32
+ }
}
]
}
--- /dev/null
+{
+ "device": {
+ "vendor": "ZyXEL",
+ "model": "WSM20",
+ "board": [
+ "zyxel,wsm20"
+ ]
+ },
+ "commands": {
+ "params": [],
+ "get": null,
+ "set": null,
+ "save": null,
+ "dual_flag": {
+ "mtd": "persist",
+ "offset": 4
+ }
+ },
+ "partitions": [
+ {
+ "number": 1,
+ "param_values": [
+ "01"
+ ],
+ "firmware": {
+ "kernel": "mtd4",
+ "kernel_label_offset": 32,
+ "rootfs": "mtd5"
+ }
+ },
+ {
+ "number": 2,
+ "param_values": [
+ "02"
+ ],
+ "firmware": {
+ "kernel": "mtd6",
+ "kernel_label_offset": 32
+ }
+ }
+ ]
+}
--- /dev/null
+#!/bin/sh
+# advanced-reboot helper for Linksys MX6200
+# Sets bootconfig partitions after fw_setenv boot_part has been called.
+# Usage: linksys-mx6200.sh <partition_number>
+
+. /lib/upgrade/platform.sh
+
+case "$1" in
+ 1)
+ linksys_bootconfig_set_primaryboot "0:bootconfig" 0
+ linksys_bootconfig_set_primaryboot "0:bootconfig1" 0
+ ;;
+ 2)
+ linksys_bootconfig_set_primaryboot "0:bootconfig" 1
+ linksys_bootconfig_set_primaryboot "0:bootconfig1" 1
+ ;;
+ *)
+ exit 1
+ ;;
+esac
{
"luci-app-advanced-reboot": {
- "description": "Grant file access for luci-app-advanced-reboot",
+ "description": "Grant access for luci-app-advanced-reboot",
"read": {
"ubus": {
- "luci.advanced-reboot": [ "obtain_device_info", "boot_partition" ],
+ "luci.advanced-reboot": [ "obtain_device_info" ]
+ }
+ },
+ "write": {
+ "ubus": {
+ "luci.advanced-reboot": [ "boot_partition" ],
"system": [ "reboot" ]
},
"file": {
- "/bin/dd": [ "list", "exec" ],
- "/bin/grep": [ "list", "exec" ],
- "/bin/mount": [ "list", "exec" ],
- "/bin/sed": [ "list", "exec" ],
- "/bin/sh": [ "list", "exec" ],
- "/bin/umount": [ "list", "exec" ],
- "/bin/uname": [ "list", "exec" ],
- "/etc/os-release": [ "read" ],
- "/lib/functions.sh": [ "read" ],
- "/proc/mounts": [ "read" ],
- "/proc/version": [ "read" ],
- "/sbin/poweroff": [ "list", "exec" ],
- "/tmp/sysinfo/board_name": [ "read" ],
- "/usr/bin/hexdump": [ "list", "exec" ],
- "/usr/bin/logger": [ "list", "exec" ],
- "/usr/bin/printf": [ "list", "exec" ],
- "/usr/bin/tr": [ "list", "exec" ],
- "/usr/sbin/fw_printenv": [ "list", "exec" ],
- "/usr/sbin/fw_printsys": [ "list", "exec" ],
- "/usr/sbin/fw_setenv": [ "list", "exec" ],
- "/usr/sbin/fw_setsys": [ "list", "exec" ],
- "/usr/sbin/ubiblock": [ "list", "exec" ],
- "/usr/sbin/ubiattach": [ "list", "exec" ],
- "/usr/sbin/ubidetach": [ "list", "exec" ]
+ "/sbin/poweroff": [ "list", "exec" ]
}
}
}
* "params": ["<env1>", "<env2>"],
* "get": "fw_printenv",
* "set": "fw_setenv",
- * "save": null
+ * "save": null,
+ * "set_helper": "/usr/share/advanced-reboot/helpers/<device>.sh",
+ * "dual_flag": { "mtd": "<partition-name>", "offset": <int> }
* },
* "partitions": [
- * { "number": 1, "param_values": [v1, w1], "mtd": "mtdX", "labelOffsetBytes": <int|null>, "altMountOptions": { "mtdOffset": <int>, "ubiVolume": <int> } },
- * { "number": 2, "param_values": [v2, w2], "mtd": "mtdY", "labelOffsetBytes": <int|null> }
+ * { "number": 1, "param_values": [v1, w1], "firmware": { "kernel": "mtdX", "rootfs": "mtdY", "kernel_label_offset": <int>, "rootfs_ubi_volume": <int> } },
+ * { "number": 2, "param_values": [v2, w2], "firmware": { ... } }
* ]
* }
*
* Notes:
- * - `altMountOptions` is optional; when absent, defaults are mtdOffset=1, ubiVolume=0.
* - `param_values[i]` aligns with `commands.params[i]`.
+ * - `set_helper` is optional; when present, the script is called with the partition
+ * number as its sole argument after the standard params have been set.
+ * - When `commands.params` is empty the dual-flag method is used: a single byte in
+ * an MTD partition selects the boot slot. `commands.dual_flag` is optional and
+ * customizes this: `mtd` is the partition name to locate via find_mtd_part
+ * (default: "0:dual_flag"/"0:DUAL_FLAG"), `offset` is the byte offset of the flag
+ * (default: 0). `param_values[0]` for each partition is the matching flag byte as
+ * two lowercase hex digits (e.g. "01"). Example: ZyXEL WSM20 stores the boot
+ * number at offset 4 of the "persist" partition (1 => first OS, 2 => second OS).
+ * - `firmware.kernel` is optional; the device to read raw label from at kernel_label_offset.
+ * - `firmware.rootfs` is optional; the device to UBI-mount for alt partition info.
+ * - `firmware.kernel_label_offset` is optional; byte offset for label probe (requires kernel).
+ * - `firmware.rootfs_ubi_volume` is optional; UBI volume index within rootfs (default 0).
*/
"use strict";
import * as fs from "fs";
let DEVICES_DIR = "/usr/share/advanced-reboot/devices/";
let DEVICES_JSON = "/usr/share/advanced-reboot/devices.json";
+/* Reset in alt_partition_mount() on each call; tracks whether the UBI device
+ * was already attached (e.g. by the kernel on IPQ5018) so alt_partition_unmount() knows
+ * not to detach it. */
+let alt_ubi_pre_attached = false;
function file_exists(p) {
try {
// Read one big JSON file instead of many small ones
let txt = fs.readfile(DEVICES_JSON);
- if (!txt) return null;
-
- // devices.json : [ { device: {...} }, { device: {...} }, ... ]
- let arr = json(txt);
- if (!arr || type(arr) != "array") return null;
-
- // Loop over each object in the array
- for (let obj in arr) {
- if (!obj || !obj.device) continue;
-
- let boards = obj.device.board;
- if (!boards) continue;
-
- // normalise to array
- if (type(boards) != "array") boards = ["" + boards];
-
- for (let i = 0; i < length(boards); i++) {
- let s = "" + boards[i];
- if (s == rb) return obj;
+ if (txt) {
+ // devices.json : [ { device: {...} }, { device: {...} }, ... ]
+ let arr = json(txt);
+ if (arr && type(arr) == "array") {
+ // Loop over each object in the array
+ for (let obj in arr) {
+ if (!obj || !obj.device) continue;
+
+ let boards = obj.device.board;
+ if (!boards) continue;
+
+ // normalise to array
+ if (type(boards) != "array") boards = ["" + boards];
+
+ for (let i = 0; i < length(boards); i++) {
+ let s = "" + boards[i];
+ if (s == rb) return obj;
+ }
+ }
}
}
- // Fallback: old schema with many small files
+ // Fallback: individual device JSON files
let list = fs.glob(DEVICES_DIR + "*.json");
for (let f in list) {
let ftxt = fs.readfile(f);
return get_volume_info("/");
}
+/* Find the UBI device number for a given MTD index via sysfs.
+ *
+ * Checks /sys/class/ubi/ubiN/mtd_num entries to find a match.
+ *
+ * @param {Number|String} mtd_idx MTD index to look up.
+ * @returns {Number|null} UBI device number, or null if not found.
+ */
+function find_ubi_dev_by_mtd(mtd_idx) {
+ let out = command(
+ "for d in /sys/class/ubi/ubi[0-9]*; do " +
+ "[ \"$(cat \"$d/mtd_num\" 2>/dev/null)\" = \"" + mtd_idx + "\" ] && " +
+ "echo \"${d##*/ubi}\" && break; done"
+ );
+ return out && length(out) ? int(out) : null;
+}
+
/* Determine if an alternate partition can be mounted.
*
* Requires: ubiattach, ubiblock, and mount commands available.
op_ubi +
" 2>/dev/null | sed -n 's/^UBI device number\\s*\\([0-9]*\\),.*$/\\1/p'"
);
- log("mounted alternative partition UBI device number: " + out);
let dev = out && length(out) ? int(out) : null;
+ alt_ubi_pre_attached = false;
+ if (dev == null) {
+ /* ubiattach failed — MTD may already be attached (e.g. by kernel).
+ * Look up the existing UBI device number via sysfs. */
+ dev = find_ubi_dev_by_mtd(op_ubi);
+ if (dev != null) {
+ alt_ubi_pre_attached = true;
+ log("found pre-attached UBI device " + dev + " for mtd" + op_ubi);
+ }
+ }
+ log("alternative partition UBI device number: " + dev);
if (dev == null) {
command("ubidetach -m " + op_ubi + " >/dev/null 2>&1");
return false;
/* Unmount and detach any UBI devices associated with a given MTD index.
*
* Also removes possible ubiblock nodes for the device.
+ * Skips ubidetach when the UBI was pre-attached (not attached by us).
*
* @param {Number|String} op_ubi UBI mtd index previously attached.
* @param {Number|String} ubi_vol UBI volume index (ignored during cleanup).
* @sideeffects umounts /var/alt_rom, ubidetach, removes ubiblock nodes.
*/
function alt_partition_unmount(op_ubi, ubi_vol) {
- let mtdCount = command(
- 'ubinfo | grep "Present UBI devices" | tr "," "\\n" | grep -c "ubi"'
- );
- mtdCount = int(mtdCount) || 10;
command("umount /var/alt_rom");
- for (let i = 0; i <= mtdCount; i++) {
- let mtd = fs.readfile("/sys/devices/virtual/ubi/ubi" + i + "/mtd_num");
- if (!mtd) break;
- mtd = trim(mtd);
- if (mtd == "" + op_ubi) {
- /* remove any ubiblock nodes on this ubi dev */
- for (let vid = 0; vid < 16; vid++)
- command(
- "ubiblock --remove /dev/ubi" + i + "_" + vid + " >/dev/null 2>&1"
- );
+ let dev = find_ubi_dev_by_mtd(op_ubi);
+ if (dev != null) {
+ /* remove any ubiblock nodes on this ubi dev */
+ for (let vid = 0; vid < 16; vid++)
+ command(
+ "ubiblock --remove /dev/ubi" + dev + "_" + vid + " >/dev/null 2>&1"
+ );
+ if (!alt_ubi_pre_attached)
command("ubidetach -m " + op_ubi + " >/dev/null 2>&1");
- command("rm -rf /var/alt_rom");
- }
}
+ command("rm -rf /var/alt_rom");
}
/* Get label/OS info from an alternative partition, using UBI mount flow.
return { label: label, os: os };
}
-function read_dual_flag_mtd() {
- for (let name in ["0:dual_flag", "0:DUAL_FLAG"]) {
+/* Locate the MTD block device holding the dual-boot flag.
+ *
+ * @param {String|null} mtd_name Partition name to look up via find_mtd_part.
+ * When null, falls back to the conventional
+ * "0:dual_flag"/"0:DUAL_FLAG" names.
+ * @returns {String|null} Device path (e.g., /dev/mtdblockN) or null if not found.
+ */
+function read_dual_flag_mtd(mtd_name) {
+ let names = mtd_name ? ["" + mtd_name] : ["0:dual_flag", "0:DUAL_FLAG"];
+ for (let name in names) {
let dev = command(". /lib/functions.sh; find_mtd_part " + shellquote(name));
if (dev) return dev;
}
/* Read the single-byte dual-boot flag value from a block device.
*
- * @param {String} dev Path to the block (e.g., /dev/mtdX) returned by find_mtd_part.
+ * @param {String} dev Path to the block (e.g., /dev/mtdX) returned by find_mtd_part.
+ * @param {Number} offset Byte offset of the flag (default 0).
* @returns {String|null} Lowercase hex byte (e.g., "00", "01") or null on error.
*/
-function read_dual_flag_value(dev) {
+function read_dual_flag_value(dev, offset) {
if (!dev || dev == "" || !file_exists(dev)) return null;
+ let off = offset != null ? int(offset) : 0;
let r = command(
"dd if=" +
shellquote(dev) +
- " bs=1 count=1 2>/dev/null | hexdump -n 1 -e '" +
+ " bs=1 count=1 skip=" + off +
+ " 2>/dev/null | hexdump -n 1 -e '" +
'1/1 "%02x"' +
"'"
);
/* Write a single-byte dual-boot flag value to a block device.
*
- * @param {String} dev Device path (e.g., /dev/mtdX).
- * @param {String} cur Hex byte without prefix (e.g., "01"); will be written as \\x01.
+ * @param {String} dev Device path (e.g., /dev/mtdX).
+ * @param {String} cur Hex byte without prefix (e.g., "01"); written as \\x01.
+ * @param {Number} offset Byte offset of the flag (default 0).
* @returns {Boolean} true if write succeeded, false otherwise.
* @sideeffects Overwrites one byte on the target device.
*/
-function write_dual_flag_value(dev, cur) {
+function write_dual_flag_value(dev, cur, offset) {
+ let off = offset != null ? int(offset) : 0;
cur = "\\x" + cur;
- let rc = command(
- "printf %b " +
- shellquote(cur) +
- " > " +
- shellquote(dev) +
- " 2>/dev/null; echo $?"
- );
+ let rc;
+ if (off == 0) {
+ rc = command(
+ "printf %b " +
+ shellquote(cur) +
+ " > " +
+ shellquote(dev) +
+ " 2>/dev/null; echo $?"
+ );
+ } else {
+ /* Patch a single byte in place without truncating the rest of the
+ * partition (matches upstream zyxel_mstc_upgrade_prepare). */
+ rc = command(
+ "printf %b " +
+ shellquote(cur) +
+ " | dd of=" +
+ shellquote(dev) +
+ " bs=1 seek=" + off +
+ " count=1 conv=notrunc 2>/dev/null; echo $?"
+ );
+ }
return rc == "0";
}
push(curvals, v);
}
} else {
- let df = read_dual_flag_mtd();
+ let dual = cmds.dual_flag ?? {};
+ let df = read_dual_flag_mtd(dual.mtd);
let v = null;
if (!df) return { error: "NO_DUAL_FLAG", rom_board_name: board };
if (!file_exists(df))
return { error: "NO_DUAL_FLAG_BLOCK", rom_board_name: board };
- v = read_dual_flag_value(df);
+ v = read_dual_flag_value(df, dual.offset);
push(curvals, v);
}
for (let i = 0; i < length(parts); i++) {
let p = parts[i] ?? {};
let num = p.number != null ? p.number : i;
- let mtd = p.mtd ?? null;
+ let fw = p.firmware ?? {};
+ let kernel = fw.kernel ?? null;
+ let rootfs = fw.rootfs ?? null;
+ let label_offset = fw.kernel_label_offset ?? null;
+ let ubi_vol = fw.rootfs_ubi_volume ?? 0;
let info = {};
- let v;
if ("" + curvals[0] == "" + p.param_values[0]) {
/* current partition */
active_num = num;
info = get_partition_info_current();
- } else if (mtd && is_alt_mountable([mtd])) {
- /* attempt alt mount if we have mount options (or defaults) and tools */
- let amo = p.altMountOptions ?? {};
- let mtdOff = amo.mtdOffset == null ? 1 : int(amo.mtdOffset);
- let ubiVol = amo.ubiVolume == null ? 0 : int(amo.ubiVolume);
- let idx = mtd_index(mtd);
+ } else if (rootfs && is_alt_mountable([rootfs])) {
+ /* attempt alt mount for rootfs */
+ let idx = mtd_index(rootfs);
if (idx != null) {
- let op_ubi = idx + mtdOff;
- info = get_partition_info_alt(op_ubi, dev.vendor ?? null, ubiVol);
+ info = get_partition_info_alt(idx, dev.vendor ?? null, ubi_vol);
if (info && info.label == null && info.os == null) info = {};
}
}
- /* raw-label fallback if needed */
- if (mtd && p.labelOffsetBytes != null) {
+ /* raw-label fallback from kernel partition */
+ if (kernel && label_offset != null) {
if (!info?.label || !info?.os) {
let fb = get_partition_info_fallback(
- mtd,
- p.labelOffsetBytes,
+ kernel,
+ label_offset,
dev.vendor
);
if (!info) {
number: "" + num,
label: info ? info.label : null,
os: info ? info.os : null,
- mtd: mtd,
+ mtd: kernel ?? rootfs,
});
}
}
if (rc != "0") return { error: "ERR_SAVE_ENV", rom_board_name: board };
}
+
+ /* Run device-specific helper if defined */
+ let helper = cmds.set_helper;
+ if (helper && helper != "" &&
+ match(helper, /^\/usr\/share\/advanced-reboot\/helpers\/[^\/]+$/)) {
+ let cmd = shellquote(helper) +
+ " " + shellquote("" + number) +
+ " 2>/dev/null; echo $?";
+ log("boot_partition running helper: " + cmd);
+ let rc = command(cmd);
+ if (rc != "0")
+ return {
+ error: "ERR_SET_HELPER",
+ args: [helper, "" + number],
+ rom_board_name: board,
+ };
+ }
} else {
- let df = read_dual_flag_mtd();
+ let dual = cmds.dual_flag ?? {};
+ let df = read_dual_flag_mtd(dual.mtd);
if (!df) return { error: "NO_DUAL_FLAG", rom_board_name: board };
if (!file_exists(df))
return { error: "NO_DUAL_FLAG_BLOCK", rom_board_name: board };
log("boot_partition setting dual flag: " + df + " to " + target.param_values[0]);
- if (!write_dual_flag_value(df, target.param_values[0]))
+ if (!write_dual_flag_value(df, target.param_values[0], dual.offset))
return { error: "ERR_SET_DUAL_FLAG", args: [df], rom_board_name: board };
}
return {};
--- /dev/null
+Test obtain_device_info when board_name file is missing.
+
+-- Testcase --
+let ar = require('advanced_reboot');
+let api = ar["luci.advanced-reboot"];
+let result = api.obtain_device_info.call();
+printf("%J\n", result);
+-- End --
+
+-- Expect stdout --
+{ "error": "NO_BOARD_NAME" }
+-- End --
--- /dev/null
+Test obtain_device_info when board name has no matching device config.
+
+-- File fs/open~_tmp_sysinfo_board_name.txt --
+unknown,device
+-- End --
+
+-- Testcase --
+let ar = require('advanced_reboot');
+let api = ar["luci.advanced-reboot"];
+let result = api.obtain_device_info.call();
+printf("%J\n", result);
+-- End --
+
+-- Expect stdout --
+{ "error": "NO_BOARD_NAME_MATCH", "rom_board_name": "unknown,device" }
+-- End --
--- /dev/null
+Test boot_partition with missing or non-numeric number argument.
+
+-- File fs/open~_tmp_sysinfo_board_name.txt --
+linksys,e7350
+-- End --
+
+-- Testcase --
+let ar = require('advanced_reboot');
+let api = ar["luci.advanced-reboot"];
+
+// No args at all
+let r1 = api.boot_partition.call();
+printf("no_args: %s\n", r1.error);
+
+// Empty object
+let r2 = api.boot_partition.call({ args: {} });
+printf("empty: %s\n", r2.error);
+
+// Non-numeric
+let r3 = api.boot_partition.call({ args: { number: "abc" } });
+printf("abc: %s\n", r3.error);
+-- End --
+
+-- Expect stdout --
+no_args: INVALID_ARG
+empty: INVALID_ARG
+abc: INVALID_ARG
+-- End --
--- /dev/null
+Test boot_partition with a valid numeric partition that does not exist on the device.
+
+-- File fs/open~_tmp_sysinfo_board_name.txt --
+linksys,e7350
+-- End --
+
+-- File fs/popen~fw_printenv_-n_bootimage_2_dev_null.txt --
+1
+-- End --
+
+-- Testcase --
+let ar = require('advanced_reboot');
+let api = ar["luci.advanced-reboot"];
+let result = api.boot_partition.call({ args: { number: "9" } });
+printf("%J\n", result);
+-- End --
+
+-- Expect stdout --
+{ "error": "PARTITION_NOT_FOUND", "args": [ "9" ] }
+-- End --
--- /dev/null
+Test obtain_device_info for Linksys E7350 with partition 1 active.
+Uses fw_printenv params and kernel label fallback for alt partition.
+
+-- File fs/open~_tmp_sysinfo_board_name.txt --
+linksys,e7350
+-- End --
+
+-- File fs/popen~fw_printenv_-n_bootimage_2_dev_null.txt --
+1
+-- End --
+
+-- File fs/popen~_etc_os-release_2_dev_null_echo_OPENWRT_RELEASE_2_dev_null.txt --
+OpenWrt SNAPSHOT r28105-abcdef0123
+-- End --
+
+-- File fs/open~_proc_version.txt --
+Linux version 6.6.63 (builder@buildhost) (aarch64-openwrt-linux-musl-gcc) #0
+-- End --
+
+-- File fs/popen~dd_if_dev_mtd3_bs_1_skip_192_count_64.txt --
+Linux version 6.6.63 OpenWrt
+-- End --
+
+-- File fs/popen~dd_if_dev_mtd6_bs_1_skip_192_count_64.txt --
+Linux version 6.6.50 OpenWrt
+-- End --
+
+-- Testcase --
+let ar = require('advanced_reboot');
+let api = ar["luci.advanced-reboot"];
+let result = api.obtain_device_info.call();
+
+// Verify device info
+printf("vendor: %s\n", result.device.vendor);
+printf("model: %s\n", result.device.model);
+printf("board: %s\n", result.device.board);
+printf("active: %s\n", result.device.partition_active);
+printf("partitions: %d\n", length(result.partitions));
+
+// Partition 1 (current)
+let p1 = result.partitions[0];
+printf("p1: number=%s label=%s os=%s\n", p1.number, p1.label, p1.os);
+
+// Partition 2 (alternate — label from dd fallback)
+let p2 = result.partitions[1];
+printf("p2: number=%s label=%s os=%s\n", p2.number, p2.label, p2.os);
+-- End --
+
+-- Expect stdout --
+vendor: Linksys
+model: E7350
+board: linksys,e7350
+active: 1
+partitions: 2
+p1: number=1 label=OpenWrt SNAPSHOT r28105-abcdef0123 os=6.6.63
+p2: number=2 label=OpenWrt os=6.6.50
+-- End --
--- /dev/null
+Test obtain_device_info for ZyXEL NBG6817 using the dual-flag method.
+Partition 1 active (dual flag reads "ff" which matches partition 1's param_values).
+
+-- File fs/open~_tmp_sysinfo_board_name.txt --
+zyxel,nbg6817
+-- End --
+
+-- File fs/popen~_lib_functions_sh_find_mtd_part_0_dual_flag_.txt --
+/dev/mtdblock12
+-- End --
+
+-- File fs/popen~dd_if_dev_mtdblock12_bs_1_count_1_skip_0_2_dev_null_hexdump_-n_1_-e_1_1_02x_.txt --
+ff
+-- End --
+
+-- File fs/popen~_etc_os-release_2_dev_null_echo_OPENWRT_RELEASE_2_dev_null.txt --
+OpenWrt 24.10.0 r28427-6df0e3d02a
+-- End --
+
+-- File fs/open~_proc_version.txt --
+Linux version 6.6.63 (builder@buildhost) (arm-openwrt-linux-muslgnueabi-gcc) #0
+-- End --
+
+-- File fs/popen~dd_if_dev_mmcblk0p4_bs_1_skip_32_count_64.txt --
+Linux version 6.6.63 OpenWrt
+-- End --
+
+-- File fs/popen~dd_if_dev_mmcblk0p7_bs_1_skip_32_count_64.txt --
+Linux-6.6.50 ZyXEL
+-- End --
+
+-- Testcase --
+let ar = require('advanced_reboot');
+let api = ar["luci.advanced-reboot"];
+let result = api.obtain_device_info.call();
+
+printf("vendor: %s\n", result.device.vendor);
+printf("model: %s\n", result.device.model);
+printf("board: %s\n", result.device.board);
+printf("active: %s\n", result.device.partition_active);
+printf("partitions: %d\n", length(result.partitions));
+
+let p1 = result.partitions[0];
+printf("p1: number=%s label=%s os=%s\n", p1.number, p1.label, p1.os);
+
+let p2 = result.partitions[1];
+printf("p2: number=%s label=%s os=%s\n", p2.number, p2.label, p2.os);
+-- End --
+
+-- Expect stdout --
+vendor: ZyXEL
+model: NBG6817
+board: zyxel,nbg6817
+active: 1
+partitions: 2
+p1: number=1 label=OpenWrt 24.10.0 r28427-6df0e3d02a os=6.6.63
+p2: number=2 label=ZyXEL os=6.6.50
+-- End --
--- /dev/null
+Test obtain_device_info for ZyXEL WSM20 using the dual-flag method with a
+custom MTD partition ("persist") and byte offset (4).
+Partition 1 active (the flag byte at offset 4 reads "01", matching partition 1).
+
+-- File fs/open~_tmp_sysinfo_board_name.txt --
+zyxel,wsm20
+-- End --
+
+-- File fs/popen~_lib_functions_sh_find_mtd_part_persist_.txt --
+/dev/mtdblock7
+-- End --
+
+-- File fs/popen~dd_if_dev_mtdblock7_bs_1_count_1_skip_4_2_dev_null_hexdump_-n_1_-e_1_1_02x_.txt --
+01
+-- End --
+
+-- File fs/popen~_etc_os-release_2_dev_null_echo_OPENWRT_RELEASE_2_dev_null.txt --
+OpenWrt 25.12.2 r32802-f505120278
+-- End --
+
+-- File fs/open~_proc_version.txt --
+Linux version 6.12.74 (builder@buildhost) (mipsel-openwrt-linux-musl-gcc) #0
+-- End --
+
+-- File fs/popen~dd_if_dev_mtd6_bs_1_skip_32_count_64.txt --
+Linux-6.6.50 ZyXEL
+-- End --
+
+-- Testcase --
+let ar = require('advanced_reboot');
+let api = ar["luci.advanced-reboot"];
+let result = api.obtain_device_info.call();
+
+printf("vendor: %s\n", result.device.vendor);
+printf("model: %s\n", result.device.model);
+printf("board: %s\n", result.device.board);
+printf("active: %s\n", result.device.partition_active);
+printf("partitions: %d\n", length(result.partitions));
+
+let p1 = result.partitions[0];
+printf("p1: number=%s label=%s os=%s\n", p1.number, p1.label, p1.os);
+
+let p2 = result.partitions[1];
+printf("p2: number=%s label=%s os=%s\n", p2.number, p2.label, p2.os);
+-- End --
+
+-- Expect stdout --
+vendor: ZyXEL
+model: WSM20
+board: zyxel,wsm20
+active: 1
+partitions: 2
+p1: number=1 label=OpenWrt 25.12.2 r32802-f505120278 os=6.12.74
+p2: number=2 label=ZyXEL os=6.6.50
+-- End --
--- /dev/null
+Test boot_partition for Linksys E7350: switch to partition 2 (success).
+
+-- File fs/open~_tmp_sysinfo_board_name.txt --
+linksys,e7350
+-- End --
+
+-- File fs/popen~fw_printenv_-n_bootimage_2_dev_null.txt --
+1
+-- End --
+
+-- File fs/popen~fw_setenv_bootimage_2_2_dev_null_echo_.txt --
+0
+-- End --
+
+-- Testcase --
+let ar = require('advanced_reboot');
+let api = ar["luci.advanced-reboot"];
+let result = api.boot_partition.call({ args: { number: "2" } });
+printf("%J\n", result);
+-- End --
+
+-- Expect stdout --
+{ }
+-- End --
--- /dev/null
+Test boot_partition when fw_setenv fails (non-zero exit code).
+
+-- File fs/open~_tmp_sysinfo_board_name.txt --
+linksys,e7350
+-- End --
+
+-- File fs/popen~fw_printenv_-n_bootimage_2_dev_null.txt --
+1
+-- End --
+
+-- File fs/popen~fw_setenv_bootimage_2_2_dev_null_echo_.txt --
+1
+-- End --
+
+-- Testcase --
+let ar = require('advanced_reboot');
+let api = ar["luci.advanced-reboot"];
+let result = api.boot_partition.call({ args: { number: "2" } });
+printf("error: %s\n", result.error);
+printf("args: %J\n", result.args);
+-- End --
+
+-- Expect stdout --
+error: ERR_SET_ENV
+args: [ "bootimage", "2" ]
+-- End --
--- /dev/null
+Test boot_partition for ZyXEL WSM20: switch to partition 2 (success).
+Writes the boot-number byte "02" at offset 4 of the "persist" partition.
+
+-- File fs/open~_tmp_sysinfo_board_name.txt --
+zyxel,wsm20
+-- End --
+
+-- File fs/popen~_lib_functions_sh_find_mtd_part_persist_.txt --
+/dev/mtdblock7
+-- End --
+
+-- File fs/popen~printf_b_x02_dd_of_dev_mtdblock7_bs_1_seek_4_count_1_conv_notrunc_2_dev_null_echo_.txt --
+0
+-- End --
+
+-- Testcase --
+let ar = require('advanced_reboot');
+let api = ar["luci.advanced-reboot"];
+let result = api.boot_partition.call({ args: { number: "2" } });
+printf("%J\n", result);
+-- End --
+
+-- Expect stdout --
+{ }
+-- End --
--- /dev/null
+/* strict mode compliance: ensure that global variables are defined */
+if (!exists(global, 'REQUIRE_SEARCH_PATH'))
+ global.REQUIRE_SEARCH_PATH = [];
+
+if (!exists(global, 'MOCK_SEARCH_PATH'))
+ global.MOCK_SEARCH_PATH = [];
+
+if (!exists(global, 'TRACE_CALLS'))
+ global.TRACE_CALLS = null;
+
+let _fs = require("fs");
+
+/* Force reloading fs module on next require */
+delete global.modules.fs;
+
+let _log = (level, fmt, ...args) => {
+ let color, prefix;
+
+ switch (level) {
+ case 'info':
+ color = 34;
+ prefix = '!';
+ break;
+
+ case 'warn':
+ color = 33;
+ prefix = 'W';
+ break;
+
+ case 'error':
+ color = 31;
+ prefix = 'E';
+ break;
+
+ default:
+ color = 0;
+ prefix = 'I';
+ }
+
+ let f = sprintf("\u001b[%d;1m[%s] %s\u001b[0m", color, prefix, fmt);
+ warn(replace(sprintf(f, ...args), "\n", "\n "), "\n");
+};
+
+let read_data_file = (path) => {
+ for (let dir in MOCK_SEARCH_PATH) {
+ let fd = _fs.open(dir + '/' + path, "r");
+
+ if (fd) {
+ let data = fd.read("all");
+ fd.close();
+
+ return data;
+ }
+ }
+
+ return null;
+};
+
+let read_json_file = (path) => {
+ let data = read_data_file(path);
+
+ if (data != null) {
+ try {
+ return json(data);
+ }
+ catch (e) {
+ _log('error', "Unable to parse JSON data in %s: %s", path, e);
+
+ return NaN;
+ }
+ }
+
+ return null;
+};
+
+let format_json = (data) => {
+ let rv;
+
+ let format_value = (value) => {
+ switch (type(value)) {
+ case "object":
+ return sprintf("{ /* %d keys */ }", length(value));
+
+ case "array":
+ return sprintf("[ /* %d items */ ]", length(value));
+
+ case "string":
+ if (length(value) > 64)
+ value = substr(value, 0, 64) + "...";
+
+ /* fall through */
+ return sprintf("%J", value);
+
+ default:
+ return sprintf("%J", value);
+ }
+ };
+
+ switch (type(data)) {
+ case "object":
+ rv = "{";
+
+ let k = sort(keys(data));
+
+ for (let i, n in k)
+ rv += sprintf("%s %J: %s", i ? "," : "", n, format_value(data[n]));
+
+ rv += " }";
+ break;
+
+ case "array":
+ rv = "[";
+
+ for (let i, v in data)
+ rv += (i ? "," : "") + " " + format_value(v);
+
+ rv += " ]";
+ break;
+
+ default:
+ rv = format_value(data);
+ }
+
+ return rv;
+};
+
+let trace_call = (ns, func, args) => {
+ let msg = "[call] " +
+ (ns ? ns + "." : "") +
+ func;
+
+ for (let k, v in args) {
+ msg += ' ' + k + ' <';
+
+ switch (type(v)) {
+ case "array":
+ case "object":
+ msg += format_json(v);
+ break;
+
+ default:
+ msg += v;
+ }
+
+ msg += '>';
+ }
+
+ switch (TRACE_CALLS) {
+ case '1':
+ case 'stdout':
+ _fs.stdout.write(msg + "\n");
+ break;
+
+ case 'stderr':
+ _fs.stderr.write(msg + "\n");
+ break;
+ }
+};
+
+/* Captured file contents from mock writefile */
+let _captured = {};
+
+/* Prepend mocklib to REQUIRE_SEARCH_PATH */
+for (let pattern in REQUIRE_SEARCH_PATH) {
+ if (!match(pattern, /\*\.uc$/))
+ continue;
+
+ let path = replace(pattern, /\*/, 'mocklib'),
+ stat = _fs.stat(path);
+
+ if (!stat || stat.type != 'file')
+ continue;
+
+ if (!length(global.MOCK_SEARCH_PATH))
+ global.MOCK_SEARCH_PATH = [ replace(path, /mocklib\.uc$/, '../mocks') ];
+
+ unshift(REQUIRE_SEARCH_PATH, replace(path, /mocklib\.uc$/, 'mocklib/*.uc'));
+ break;
+}
+
+if (!length(global.MOCK_SEARCH_PATH))
+ global.MOCK_SEARCH_PATH = [ './mocks' ];
+
+/* Register global mocklib namespace */
+global.mocklib = {
+ require: function(module) {
+ let path, res, ex;
+
+ if (type(REQUIRE_SEARCH_PATH) == "array" && index(REQUIRE_SEARCH_PATH[0], 'mocklib/*.uc') != -1)
+ path = shift(REQUIRE_SEARCH_PATH);
+
+ try {
+ res = require(module);
+ }
+ catch (e) {
+ ex = e;
+ }
+
+ if (path)
+ unshift(REQUIRE_SEARCH_PATH, path);
+
+ if (ex)
+ die(ex);
+
+ return res;
+ },
+
+ I: (...args) => _log('info', ...args),
+ N: (...args) => _log('notice', ...args),
+ W: (...args) => _log('warn', ...args),
+ E: (...args) => _log('error', ...args),
+
+ format_json,
+ read_data_file,
+ read_json_file,
+ trace_call,
+
+ capture: (path, data) => { _captured[path] = data; },
+ read_captured: (path) => _captured[path],
+ has_captured: (path) => exists(_captured, path),
+ delete_captured: (path) => { delete _captured[path]; },
+};
+
+/* Override stdlib functions */
+global.system = function(argv, timeout) {
+ trace_call(null, "system", { command: argv, timeout });
+
+ return 0;
+};
+
+global.time = function() {
+ trace_call(null, "time");
+
+ return 1615382640;
+};
+
+global.print = ((_orig) => function(...args) {
+ if (length(args) == 1 && type(args[0]) in ["array", "object"])
+ printf("%s\n", format_json(args[0]));
+ else
+ _orig(...args);
+})(global.print);
+
+global.getenv = function(key) {
+ return null;
+};
+
+return global.mocklib;
--- /dev/null
+let mocklib = global.mocklib; // ucode-lsp disable
+
+return {
+ readfile: function(path, limit) {
+ /* Check captured content first (from mock writefile) */
+ if (mocklib.has_captured(path)) {
+ mocklib.trace_call("fs", "readfile", { path, limit, source: "captured" });
+ let data = mocklib.read_captured(path);
+ return limit ? substr(data, 0, limit) : data;
+ }
+
+ let file = sprintf("fs/open~%s.txt", replace(path, /[^A-Za-z0-9_-]+/g, '_')),
+ mock = mocklib.read_data_file(file);
+
+ if (mock == null) {
+ /* Silently return null for missing fixtures */
+ return null;
+ }
+
+ mocklib.trace_call("fs", "readfile", { path, limit });
+
+ return limit ? substr(mock, 0, limit) : mock;
+ },
+
+ writefile: function(path, data) {
+ mocklib.capture(path, data);
+ mocklib.trace_call("fs", "writefile", { path, length: length(data) });
+
+ return length(data);
+ },
+
+ popen: (cmdline, mode) => {
+ let read = (!mode || index(mode, "r") != -1),
+ path = sprintf("fs/popen~%s.txt", replace(cmdline, /[^A-Za-z0-9_-]+/g, '_')),
+ mock = mocklib.read_data_file(path);
+
+ if (read && mock == null) {
+ /* Silently return empty-read handle for missing fixtures */
+ return {
+ read: function(amount) { return ''; },
+ write: function() {},
+ close: function() {},
+ error: function() { return null; }
+ };
+ }
+
+ mocklib.trace_call("fs", "popen", { cmdline, mode });
+
+ return {
+ read: function(amount) {
+ let rv;
+
+ switch (amount) {
+ case "all":
+ rv = mock;
+ mock = "";
+ break;
+
+ case "line":
+ let i = index(mock, "\n");
+ i = (i > -1) ? i + 1 : length(mock);
+ rv = substr(mock, 0, i);
+ mock = substr(mock, i);
+ break;
+
+ default:
+ let n = +amount;
+ n = (n > 0) ? n : 0;
+ rv = substr(mock, 0, n);
+ mock = substr(mock, n);
+ break;
+ }
+
+ return rv;
+ },
+
+ write: function() {},
+ close: function() {},
+
+ error: function() {
+ return null;
+ }
+ };
+ },
+
+ stat: function(path) {
+ /* Check captured content first */
+ if (mocklib.has_captured(path)) {
+ mocklib.trace_call("fs", "stat", { path, source: "captured" });
+ return { type: "file" };
+ }
+
+ let file = sprintf("fs/stat~%s.json", replace(path, /[^A-Za-z0-9_-]+/g, '_')),
+ mock = mocklib.read_json_file(file);
+
+ if (!mock || mock != mock) {
+ /* No fixture: fall back to reasonable defaults */
+ if (match(path, /\/$/))
+ mock = { type: "directory" };
+ else
+ mock = { type: "file" };
+ }
+
+ mocklib.trace_call("fs", "stat", { path });
+
+ return mock;
+ },
+
+ glob: function(pattern) {
+ mocklib.trace_call("fs", "glob", { pattern });
+
+ /* Return empty array — tests use devices.json fixture directly */
+ return [];
+ },
+
+ unlink: function(path) {
+ mocklib.delete_captured(path);
+ mocklib.trace_call("fs", "unlink", { path });
+
+ return true;
+ },
+
+ error: () => "Unspecified error"
+};
--- /dev/null
+[
+ {
+ "device": {
+ "vendor": "Linksys",
+ "model": "E7350",
+ "board": ["linksys,e7350"]
+ },
+ "commands": {
+ "params": ["bootimage"],
+ "get": "fw_printenv",
+ "set": "fw_setenv",
+ "save": null
+ },
+ "partitions": [
+ {
+ "number": 1,
+ "param_values": [1],
+ "firmware": {
+ "kernel": "mtd3",
+ "kernel_label_offset": 192,
+ "rootfs": "mtd4"
+ }
+ },
+ {
+ "number": 2,
+ "param_values": [2],
+ "firmware": {
+ "kernel": "mtd6",
+ "kernel_label_offset": 192,
+ "rootfs": "mtd7"
+ }
+ }
+ ]
+ },
+ {
+ "device": {
+ "vendor": "ZyXEL",
+ "model": "NBG6817",
+ "board": ["zyxel,nbg6817"]
+ },
+ "commands": {
+ "params": [],
+ "get": null,
+ "set": null,
+ "save": null
+ },
+ "partitions": [
+ {
+ "number": 1,
+ "param_values": ["ff"],
+ "firmware": {
+ "kernel": "mmcblk0p4",
+ "kernel_label_offset": 32
+ }
+ },
+ {
+ "number": 2,
+ "param_values": ["01"],
+ "firmware": {
+ "kernel": "mmcblk0p7",
+ "kernel_label_offset": 32
+ }
+ }
+ ]
+ },
+ {
+ "device": {
+ "vendor": "ZyXEL",
+ "model": "WSM20",
+ "board": ["zyxel,wsm20"]
+ },
+ "commands": {
+ "params": [],
+ "get": null,
+ "set": null,
+ "save": null,
+ "dual_flag": {
+ "mtd": "persist",
+ "offset": 4
+ }
+ },
+ "partitions": [
+ {
+ "number": 1,
+ "param_values": ["01"],
+ "firmware": {
+ "kernel": "mtd4",
+ "kernel_label_offset": 32,
+ "rootfs": "mtd5"
+ }
+ },
+ {
+ "number": 2,
+ "param_values": ["02"],
+ "firmware": {
+ "kernel": "mtd6",
+ "kernel_label_offset": 32
+ }
+ }
+ ]
+ }
+]
--- /dev/null
+#!/usr/bin/env bash
+# Functional test runner for luci-app-advanced-reboot.
+#
+# Tests:
+# 00: Device JSON validation (structure, required fields)
+# 01: Error handling (missing board, unknown device, bad args)
+# 02: obtain_device_info (fw_printenv, dual-flag)
+# 03: boot_partition (switch, failure)
+#
+# Usage: cd source.mossdef.org/luci-app-advanced-reboot && bash tests/run_tests.sh
+
+set -o pipefail
+
+line='........................................'
+
+# ── Patch phase ─────────────────────────────────────────────────────
+# Convert ES namespace import to require() so the mock framework can
+# intercept the fs module through the module cache.
+
+patch_dir="/tmp/ar_test_modules.$$"
+mkdir -p "$patch_dir"
+
+sed \
+ -e 's|import \* as fs from "fs";|let fs = require("fs");|' \
+ ./root/usr/share/rpcd/ucode/luci.advanced-reboot > "$patch_dir/advanced_reboot.uc"
+
+trap "rm -rf '$patch_dir'" EXIT
+
+# Search paths: patched module first, then tests/lib (for mocklib)
+ucode="ucode -S -L$patch_dir -L./tests/lib"
+
+# ── Device JSON validation ──────────────────────────────────────────
+
+DEVICES_DIR="./root/usr/share/advanced-reboot/devices"
+
+n_tests=0
+n_fails=0
+
+pass() {
+ printf " PASS: %s\n" "$1"
+}
+fail() {
+ printf " FAIL: %s\n" "$1"
+ [ -n "$2" ] && printf " %s\n" "$2"
+ n_fails=$((n_fails + 1))
+}
+
+if [ -d "$DEVICES_DIR" ]; then
+ printf "\n##\n## 00: Device JSON validation\n##\n\n"
+
+ device_count=0
+ for f in "$DEVICES_DIR"/*.json; do
+ [ -f "$f" ] || continue
+ device_count=$((device_count + 1))
+ base="$(basename "$f")"
+
+ # Valid JSON
+ n_tests=$((n_tests + 1))
+ if ! jq empty "$f" 2>/dev/null; then
+ fail "$base: invalid JSON"
+ continue
+ fi
+ pass "$base: valid JSON"
+
+ # Required: device.vendor
+ n_tests=$((n_tests + 1))
+ vendor="$(jq -r '.device.vendor // empty' "$f")"
+ if [ -n "$vendor" ]; then
+ pass "$base: has device.vendor '$vendor'"
+ else
+ fail "$base: missing device.vendor"
+ fi
+
+ # Required: device.model
+ n_tests=$((n_tests + 1))
+ model="$(jq -r '.device.model // empty' "$f")"
+ if [ -n "$model" ]; then
+ pass "$base: has device.model '$model'"
+ else
+ fail "$base: missing device.model"
+ fi
+
+ # Required: device.board (must be array)
+ n_tests=$((n_tests + 1))
+ board_type="$(jq -r '.device.board | type' "$f")"
+ if [ "$board_type" = "array" ]; then
+ board_len="$(jq '.device.board | length' "$f")"
+ if [ "$board_len" -gt 0 ]; then
+ pass "$base: device.board is array with $board_len entries"
+ else
+ fail "$base: device.board is empty array"
+ fi
+ else
+ fail "$base: device.board is not an array (got $board_type)"
+ fi
+
+ # Required: partitions (must be array with >= 2 entries)
+ n_tests=$((n_tests + 1))
+ part_type="$(jq -r '.partitions | type' "$f")"
+ if [ "$part_type" = "array" ]; then
+ part_len="$(jq '.partitions | length' "$f")"
+ if [ "$part_len" -ge 2 ]; then
+ pass "$base: has $part_len partitions"
+ else
+ fail "$base: need >= 2 partitions (got $part_len)"
+ fi
+ else
+ fail "$base: partitions is not an array"
+ fi
+
+ # Each partition must have number and param_values
+ for i in $(seq 0 $((part_len - 1))); do
+ n_tests=$((n_tests + 1))
+ has_num="$(jq ".partitions[$i] | has(\"number\")" "$f")"
+ has_pv="$(jq ".partitions[$i] | has(\"param_values\")" "$f")"
+ if [ "$has_num" = "true" ] && [ "$has_pv" = "true" ]; then
+ pass "$base: partition $i has number and param_values"
+ else
+ fail "$base: partition $i missing number ($has_num) or param_values ($has_pv)"
+ fi
+ done
+ done
+
+ printf "\n Validated %d device files\n" "$device_count"
+fi
+
+# ── Mock-based functional tests ────────────────────────────────────
+
+extract_sections() {
+ local file=$1
+ local dir=$2
+ local count=0
+ local tag line outfile
+
+ while IFS= read -r line; do
+ case "$line" in
+ "-- Testcase --")
+ tag="test"
+ count=$((count + 1))
+ outfile=$(printf "%s/%03d.in" "$dir" $count)
+ printf "" > "$outfile"
+ ;;
+ "-- Environment --")
+ tag="env"
+ count=$((count + 1))
+ outfile=$(printf "%s/%03d.env" "$dir" $count)
+ printf "" > "$outfile"
+ ;;
+ "-- Expect stdout --"|"-- Expect stderr --"|"-- Expect exitcode --")
+ tag="${line#-- Expect }"
+ tag="${tag% --}"
+ count=$((count + 1))
+ outfile=$(printf "%s/%03d.%s" "$dir" $count "$tag")
+ printf "" > "$outfile"
+ ;;
+ "-- File "*" --")
+ tag="file"
+ outfile="${line#-- File }"
+ outfile="$(echo "${outfile% --}" | xargs)"
+ outfile="$dir/files$(readlink -m "/${outfile:-file}")"
+ mkdir -p "$(dirname "$outfile")"
+ printf "" > "$outfile"
+ ;;
+ "-- End --")
+ tag=""
+ outfile=""
+ ;;
+ *)
+ if [ -n "$tag" ]; then
+ printf "%s\\n" "$line" >> "$outfile"
+ fi
+ ;;
+ esac
+ done < "$file"
+
+ return $(ls -l "$dir/"*.in 2>/dev/null | wc -l)
+}
+
+run_testcase() {
+ local num=$1
+ local dir=$2
+ local in=$3
+ local env=$4
+ local out=$5
+ local err=$6
+ local code=$7
+ local fail=0
+
+ $ucode \
+ -D MOCK_SEARCH_PATH='["'"$dir"'/files", "./tests/mocks"]' \
+ ${env:+-F "$env"} \
+ -l mocklib \
+ - <"$in" >"$dir/res.out" 2>"$dir/res.err"
+
+ printf "%d\n" $? > "$dir/res.code"
+
+ touch "$dir/empty"
+
+ if ! cmp -s "$dir/res.err" "${err:-$dir/empty}"; then
+ [ $fail = 0 ] && printf "!\n"
+ printf "Testcase #%d: Expected stderr did not match:\n" $num
+ diff -u --color=always --label="Expected stderr" --label="Resulting stderr" "${err:-$dir/empty}" "$dir/res.err"
+ printf -- "---\n"
+ fail=1
+ fi
+
+ if ! cmp -s "$dir/res.out" "${out:-$dir/empty}"; then
+ [ $fail = 0 ] && printf "!\n"
+ printf "Testcase #%d: Expected stdout did not match:\n" $num
+ diff -u --color=always --label="Expected stdout" --label="Resulting stdout" "${out:-$dir/empty}" "$dir/res.out"
+ printf -- "---\n"
+ fail=1
+ fi
+
+ if [ -n "$code" ] && ! cmp -s "$dir/res.code" "$code"; then
+ [ $fail = 0 ] && printf "!\n"
+ printf "Testcase #%d: Expected exit code did not match:\n" $num
+ diff -u --color=always --label="Expected code" --label="Resulting code" "$code" "$dir/res.code"
+ printf -- "---\n"
+ fail=1
+ fi
+
+ return $fail
+}
+
+run_test() {
+ local file=$1
+ local name=${file##*/}
+ local res ecode eout eerr ein eenv tests
+ local testcase_first=0 failed=0 count=0
+
+ printf "%s %s " "$name" "${line:${#name}}"
+
+ mkdir "/tmp/test.$$"
+
+ extract_sections "$file" "/tmp/test.$$"
+ tests=$?
+
+ [ -f "/tmp/test.$$/001.in" ] && testcase_first=1
+
+ for res in "/tmp/test.$$/"[0-9]*; do
+ case "$res" in
+ *.in)
+ count=$((count + 1))
+
+ if [ $testcase_first = 1 ]; then
+ # Flush previous test
+ if [ -n "$ein" ]; then
+ run_testcase $count "/tmp/test.$$" "$ein" "$eenv" "$eout" "$eerr" "$ecode" || failed=$((failed + 1))
+
+ eout=""
+ eerr=""
+ ecode=""
+ eenv=""
+ fi
+
+ ein=$res
+ else
+ run_testcase $count "/tmp/test.$$" "$res" "$eenv" "$eout" "$eerr" "$ecode" || failed=$((failed + 1))
+
+ eout=""
+ eerr=""
+ ecode=""
+ eenv=""
+ fi
+
+ ;;
+ *.env) eenv=$res ;;
+ *.stdout) eout=$res ;;
+ *.stderr) eerr=$res ;;
+ *.exitcode) ecode=$res ;;
+ esac
+ done
+
+ # Flush last test
+ if [ $testcase_first = 1 ] && [ -n "$ein" ]; then
+ run_testcase $count "/tmp/test.$$" "$ein" "$eenv" "$eout" "$eerr" "$ecode" || failed=$((failed + 1))
+ fi
+
+ rm -r "/tmp/test.$$"
+
+ if [ $failed = 0 ]; then
+ printf "OK\n"
+ else
+ printf "%s %s FAILED (%d/%d)\n" "$name" "${line:${#name}}" $failed $tests
+ fi
+
+ return $failed
+}
+
+
+select_tests="$@"
+
+use_test() {
+ local input="$(readlink -f "$1")"
+ local test
+
+ [ -f "$input" ] || return 1
+ [ -n "$select_tests" ] || return 0
+
+ for test in $select_tests; do
+ test="$(readlink -f "$test")"
+
+ [ "$test" != "$input" ] || return 0
+ done
+
+ return 1
+}
+
+for catdir in tests/[0-9][0-9]_*; do
+ [ -d "$catdir" ] || continue
+
+ printf "\n##\n## Running %s tests\n##\n\n" "${catdir##*/[0-9][0-9]_}"
+
+ for testfile in "$catdir/"[0-9][0-9]_*; do
+ use_test "$testfile" || continue
+
+ n_tests=$((n_tests + 1))
+ run_test "$testfile" || n_fails=$((n_fails + 1))
+ done
+done
+
+# ── Shell script syntax checks ──────────────────────────────────────
+
+printf "\n##\n## Checking shell script syntax\n##\n\n"
+for shellscript in \
+ root/etc/uci-defaults/* \
+ root/usr/share/advanced-reboot/helpers/*.sh; do
+ [ -f "$shellscript" ] || continue
+ head -1 "$shellscript" | grep -q '^#!/bin/sh' || continue
+ name="${shellscript#root/}"
+ n_tests=$((n_tests + 1))
+ printf "%s %s " "$name" "${line:${#name}}"
+ if sh -n "$shellscript" 2>/dev/null; then
+ printf "OK\n"
+ else
+ printf "FAIL\n"
+ sh -n "$shellscript"
+ n_fails=$((n_fails + 1))
+ fi
+done
+
+printf "\nRan %d tests, %d okay, %d failures\n" $n_tests $((n_tests - n_fails)) $n_fails
+exit $n_fails