ZyXEL GS1900-10HP revision B1 support OpenWrt firmware?

I would like to buy ZyXEL GS1900-10HP revision B1, but I didn't know that this firmware is okay
https://downloads.openwrt.org/releases/21.02.0/targets/realtek/generic/openwrt-21.02.0-realtek-generic-zyxel_gs1900-10hp-initramfs-kernel.bin
Check also https://openwrt.org/toh/zyxel/gs1900-10hp

In this datasheet documentation I see two revisions
GS1900-10HP A1 and GS1900-10 B1

and different hardware comparison

So the question is GS1900-10HP A1 and GS1900-10 B1 can possible the install the same OpenWRT firmware or do I need to wait for a new release?

Thanks in advance
Regards, Georgi

1 Like

You should probably check in Support for RTL838x based managed switches

1 Like

Any new information on this? Did you try it?

No, I bought Zyxel GS1900-8HP v2

I bought a GS1900-10HP unit, and it differs from the "A1" as described in your datasheet (barrel plug, and no ON/OFF switch), and labeled on the device as "Revision: B1".

Over serial it calls itself "v2":

U-Boot Version: 2.0.2.1 (May 21 2021 - 12:21:20)

CPU:   500MHz
DRAM:  128 MB
FLASH: 16 MB
Model: ZyXEL_GS1900_10HPv2

I can confirm, that the install instructions on the OpenWrt wiki you posted above work with self-compiled images from current master (r0-a181b9f0). This is after connecting via SSH:

root@gs190010hp:~# uname -a
Linux gs190010hp 5.15.132 #0 Mon Oct 2 20:13:10 2023 mips GNU/Linux

I haven't tested all functions yet, just flashed it minutes ago, but I think your question can be now answered in the positive "Yes". The Wiki-page is a bit outdated though, and the filename in the tftp command needs to be adapted.

Edit: After testing, it turns out, that the POE-part is different in rev B1: Power delivery works, but no control, or measuring the used power. ubus call poe info returns 'unknown' everywhere.

1 Like

Run dmesg after bootup and check kernel log messages. See if there any any major errors or warnings about hardware missing or changed. The best way to be sure is to open the chassis and see what the chip numbers are (compared to A1).

Overview shot of the Rev B1 board:

The POE part (Nuvoton NUC029ZAN, like in Zyxel GS1900-24EP)

The PCB area surrounding the POE part

For the SFP cages, I think

Yes, I know, I should not quit my day job to become a photographer...

I have not compared them yet to the A1 revision board.

Get close-ups of the 4 chips closest to the SFP cages. Can't read shit from the photos you posted except for the RTL chip. Take photos at an incident angle where light bounces off the chip at an obtuse angle, such that chip numbers are... readable :slight_smile: Rotate the board so that text reads across the photo and is in the depth of focus plane that is in focus.

Take your time and review the photos before you close the chassis. Get as close as you can without using digital zoom (only optical) which quickly produces artifacts; some cameras use AI/weird algos to compress better which can mess numbers/lettering up in weird ways. The 'PoE' chip photo was OK, but it's Nuvoton (an ARM micro-controller, for SPI GPIO I2C UART etc, probably for LED control, and Serial, judging by its connection to J1 and J2). All of those caps in the upper left which feed into the GS6014 Ethernet drivers are likely the PoE part.

It's not a race - clear information helps everyone in the long run.

Uploads photos, or crops of photos, but not screenshots of photos :slight_smile:

Of interest are the WinBond memory, and the 16pin square chip (prob NOR) between Winbond and RTL8231 by the cages.

The PoE chip is likely the one under the aluminium 3-fin heat-sink in the upper-left of the overview photo. You may have to prize the heat-sink off to get a clear picture of it. Only do so if you have some heat-sink paste to put it back on. I would advise the same to see if they refreshed the SoC, but that seems unlikely, since this is not trivial in a refresh. And if it boots, detects, and works, it's probably the same.

The Winbond, if you squint enough and let google autocomplete help a bit, seems to be:
Winbond W631GU6NB which is: 1G bits DDR3L SDRAM 1.35/1.5v.

2 Likes

Also, what does dmesg show (or just copy the text between uboot start and the openwrt text logo) after boot?

Yes, I can/will make better pictures, but the sunlight angle right now make my phone nearly blind, lol.
I posted the rough ones above, so that people could tell me, which parts I should post close-ups from. I'll edit the requested pics into this post sometime later today.

1 Like

I've compared the dmesg outputs:

Differences are few:

A1: Probing RTL838X eth device pdev: 82087600, dev: 82087610
B1: Probing RTL838X eth device pdev: 82087c00, dev: 82087c10

Both have these errors further down:
rtl83xx_fib_event: FIB_RULE ADD/DEL for IPv6 not supported
rtl83xx_fib_event_work_do: FIB4 failed

See here

The same SoC. Just the chip parent device and device address in memory.

1 Like

Has anyone got the GS1900-HP10 revision B1 (V2) to work properly so that PoE can be controlled (on and off via ubus -v call poe port '{"enable":false,"port":4}')?

The "new" Realtek POE chip is in use here apparently. I ran into the same issue.

There are 2 hack patches here(github.com/Hurricos) that may work, but I have not tried them yet.

Also: https://github.com/Hurricos/realtek-poe/issues/25

1 Like

You can use this PoE manager. It is made for the Realtek PoE chip on mainboard revision B1 (GS1900-10HP v2) and works for me.

#!/usr/bin/lua

-------------------------------------------------------------
----  For testing, start in foreground with:
----  $ poemgr-realtek -d -v
----
----  ("-d" is debug, "-v" is verbose)
----
----  While the daemon is running, PoE ports can be manually
----  enabled via ubus:
----  $ ubus call poe manage "{'port':'lan1', 'enable':true}"
----
----  Inspect the per-port consumption to see if power is delivered:
----  $ ubus call poe info
----
----  If everything works, install the PoE manager:
----    1. poe manager (this file)   --> /usr/bin/poemgr-realtek
----    2. service definition file   --> /etc/init.d/poemgr-realtek
----    3. configuration             --> /etc/config/poe
----
----  And install required packages / dependencies:
----  $ opkg install lua-rs232 libuci-lua libubus-lua
----
----  Start the service via GUI (System -> Startup) or CLI:
----  $ service poemgr-realtek start
----
----  In the configuration file, PoE ports can be enabled or
----  disabled with the "enable" property.
----
----  Priority groups can be configured with "priority".
----  Priorities range from 0 (low) to 3 (critical), with
----  1 (normal) and 2 (high) in between.
----
----  Mapping between the PSE power lines and Ethernet ports
----  can be adjusted with the "id" and "name" properties.
----
----  After modifying the configuration file or updating
----  configuration via UCI, perform a commit to push
----  changes to the PoE controller:
----  $ ubus call uci commit '{"config":"poe"}'
----
----  For reading out internal statistics, the following
----  command is available.
----  $ ubus call poe stats
----
----  For experimenting with new or updated opcodes in the
----  controller firmware, the following command is available.
----  $ ubus call poe sendframe '{"frame":"40"}'
-------------------------------------------------------------

require "uci"
require "ubus"
require "uloop"

local rs = require "luars232"
local stderr = io.stderr

-- PSE known power limits for this board (zyxel,gs1900-10hp-v2)
local PSE_WATTS = 77.0
local PSE_GUARD = 11.0

-- SoC serial port for talking to MCU
local SOC_UART = "/dev/ttyS1"

-- MCU baud rate (Realtek RTL8238)
local MCU_RATE = rs.RS232_BAUD_115200

-- How many times to try again in case of timeouts, checksum errors etc.
local MAX_RETRIES = 3

-- MCU error codes (Realtek RTL8238)
local MCU_ERR_CHECKSUM = 0x62
local MCU_ERR_INCOMPLETE = 0xfd
local MCU_ERR_MALFORMED = 0xfe
local MCU_ERR_BUSY = 0xff

-- MCU commands (Realtek RTL8238)
local MCU_CMD_GET_CONTROLLER_STATUS = 0x40
local MCU_CMD_SET_GLOBAL_POWER_LIMITS = 0x04
local MCU_CMD_GET_GLOBAL_POWER_USAGE = 0x41
local MCU_CMD_SET_PORT_PRIORITY = 0x15
local MCU_CMD_SET_PORT_POWER_DELIVERY = 0x01
local MCU_CMD_GET_PORT_POWER_USAGE = 0x44

-- Hardware the above is tested on
local MATCH_BOARDNAME = "ZyXEL_GS1900_10HPv2"
local MATCH_PSEID = "4"

-- Text markings on packages - guesswork, need photos
local CHIPNAME_PSE = "Nuvoton NUC029ZAN"
local CHIPNAME_MCU = "Realtek RTL8238"

-- Filled with defaults (1=lan1, 2=lan2 etc.) during init
local ifname
local portid, portprio, portenable
local budget, guard
local poe_ports

-- Internal statistics counters
local stats_msg = 0
local stats_timeout = 0
local stats_checksum = 0
local stats_error = 0
local stats_notify = 0
local stats_reload = 0

-- Command-line options
local log_verbose = false
local log_debug = false

local seq = 0
local queue = {}

function verbose(s)
	if log_verbose then io.write(s) end
end

function debug(s)
	if log_debug then io.write(s) end
end

function read_fw_env(key)
	local handle = io.popen(string.format("fw_printenv -n %s", key))
	local output = handle:read("*a")
	local res = output:gsub('[\n\r]', '')
	handle:close()
	return res
end

function check_compatible()
	-- mainboard revision "B1" / router model "v2" has the relevant MCU
	local model = read_fw_env("boardmodel")
	local pse_id = read_fw_env("pseId")
	if model ~= MATCH_BOARDNAME or pse_id ~= MATCH_PSEID then
		stderr:write(string.format("ERROR: unknown hardware model or revision"))
		assert(false)
	end
end

function configure_uart()
	local err, handle = rs.open(SOC_UART)
	if err ~= rs.RS232_ERR_NOERROR then
		stderr:write(string.format("ERROR: failed to open('%s'), cause: '%s'\n", SOC_UART, rs.error_tostring(err)))
		assert(false)
	end

	assert(handle:set_baud_rate(MCU_RATE) == rs.RS232_ERR_NOERROR)
	assert(handle:set_data_bits(rs.RS232_DATA_8) == rs.RS232_ERR_NOERROR)
	assert(handle:set_parity(rs.RS232_PARITY_NONE) == rs.RS232_ERR_NOERROR)
	assert(handle:set_stop_bits(rs.RS232_STOP_1) == rs.RS232_ERR_NOERROR)
	assert(handle:set_flow_control(rs.RS232_FLOW_OFF)  == rs.RS232_ERR_NOERROR)
	io.write(string.format("configured %s\n", tostring(handle)))
	synchronize(handle)
	return handle
end

function synchronize(handle)
	local interval = 1000
	local buffer = {}
	local size = 1
	local err, data, i

	io.write(string.format("wait for quiescent %s\n", SOC_UART))
	while size > 0 do
		err, data, size = handle:read(4096, interval)
		if err == rs.RS232_ERR_NOERROR then
			for i = 1, string.len(data) do
				table.insert(buffer, string.byte(string.sub(data, i, i)))
			end
		elseif err == rs.RS232_ERR_TIMEOUT then
			size = 0
		else
			stderr:write(string.format("ERROR: failed to read(), cause: '%s'\n", rs.error_tostring(err)))
			assert(false)
		end
	end

	if #buffer > 0 then
		debug("RX")
		for i = 1, #buffer do
			debug(string.format(" %02x", buffer[i]))
		end
		debug("\n")
	end
end

function try_process_unsolicited()
	-- handle any notifications received asynchronously
	stats_notify = stats_notify + #queue
	if #queue == 0 then return nil end
	verbose(string.format("ignoring %d unknown notifications from controller\n", #queue))
	queue = {}
end

function try_exchange_message(h, xmit)
	local interval = 1000
	local retries = 10
	local recv = {}
	local round = 0
	local sum = 0
	local data = ""
	local err, size, left, i

	if #xmit > 12 then
		verbose(string.format("output frame too large, discard\n", xmit))
		return nil
	end

	if #xmit < 1 then
		verbose("output frame too small, discard\n")
		return nil
	end

	-- reserve all-zeroes and bit 2^7 for controller
	seq = (seq % 127) + 1
	xmit[2] = seq

	-- pad to full frame length
	while #xmit < 12 do
		table.insert(xmit, 0xff)
	end

	-- coalesce to zero to avoid crash on invalid input
	for i = 1, 12 do
		if type(xmit[i]) ~= "number" then xmit[i] = 0 end
		xmit[i] = math.floor(math.abs(xmit[i])) % 256
	end

	-- calculate checksum
	for i = 1, 11 do
		sum = sum + xmit[i]
	end
	xmit[12] = sum % 256

	debug("TX ->")
	for i = 1, 12 do
		debug(string.format(" %02x", xmit[i]))
	end
	debug("\n")

	for i = 1, 12 do
		data = data .. string.char(xmit[i])
	end

	err, size = h:write(data)
	assert(err == rs.RS232_ERR_NOERROR)

	while round < retries do
		while round < retries and #recv < 12 do
			err, data, size = h:read(12 - #recv, interval)
			assert(err == rs.RS232_ERR_NOERROR or err == rs.RS232_ERR_TIMEOUT)
			if err == rs.RS232_ERR_NOERROR then
				assert(#recv + size < 13)
				for i = 1, string.len(data) do
					table.insert(recv, string.byte(string.sub(data, i, i)))
				end
			end
			round = round + 1
		end

		if #recv > 0 then
			stats_msg = stats_msg + 1
			debug("RX <-")
			for i = 1, #recv do
				debug(string.format(" %02x", recv[i]))
			end
			debug("\n")
		end

		if #recv < 12 then
			stats_timeout = stats_timeout + 1
			stderr:write("timeout while waiting for response\n")
			return nil
		end

		sum = 0
		for i = 1, 11 do
			sum = sum + recv[i]
		end

		if sum % 256 ~= recv[12] then
			stats_checksum = stats_checksum + 1
			stderr:write("frame checksum invalid in response\n")
			return nil

		elseif recv[1] ~= xmit[1] and recv[2] ~= xmit[2] then
			verbose("unsolicited frame, add to queue\n");
			table.insert(queue, recv)
			recv = {}
			round = 0

		elseif recv[2] ~= xmit[2] then
			recv = {}
			verbose("wrong sequence number from controller, discard\n")

		elseif recv[1] == MCU_ERR_CHECKSUM then
			stats_error = stats_error + 1
			verbose("controller report: invalid checksum\n")

		elseif recv[1] == MCU_ERR_INCOMPLETE then
			stats_error = stats_error + 1
			verbose("controller report: incomplete request\n")

		elseif recv[1] == MCU_ERR_MALFORMED then
			stats_error = stats_error + 1
			verbose("controller report: malformed request\n")

		elseif recv[1] == MCU_ERR_BUSY then
			stats_error = stats_error + 1
			verbose("controller report: busy\n")

		elseif recv[1] ~= xmit[1] then
			stats_error = stats_error + 1
			verbose(string.format("controller report: (unknown error reply %s)\n", recv[1]));

		else
			return recv
		end
	end
end

function exchange_message_once(h, xmit)
	local recv = try_exchange_message(h, xmit)
	try_process_unsolicited()
	return recv
end

function exchange_message(h, xmit)
	local round
	for round = 1, MAX_RETRIES do
		if round > 1 then verbose("Retrying...\n") end
		local recv = exchange_message_once(h, xmit)
		if recv ~= nil then return recv end
	end
	return recv
end

function uintbe16(nr)
	-- coalesce to zero to avoid crash on invalid input
	if type(nr) ~= "number" then nr = 0 end
	local l = math.floor(nr * 10 / 256)
	local r = math.floor(nr * 10) % 256
	return l, r
end

function get_controller_status(h)
	verbose("request controller status\n")
	local cmd = {MCU_CMD_GET_CONTROLLER_STATUS}
	local reply = exchange_message(h, cmd)
	if type(reply) == "nil" then return nil end
	table.remove(reply, #reply)
	table.remove(reply, 1)
	table.remove(reply, 1)
	return unpack(reply)
end

function set_power_limits(h, total, guard)
	verbose(string.format("set power limits, global budget: %s, port guard: %s\n", total, guard))
	local cmd = {MCU_CMD_SET_GLOBAL_POWER_LIMITS, 0, 0x00}
	cmd[4], cmd[5] = uintbe16(total)
	cmd[6], cmd[7] = uintbe16(guard)
	exchange_message(h, cmd)
end

function get_global_power_usage(h)
	verbose("get global power usage\n")
	local cmd = {MCU_CMD_GET_GLOBAL_POWER_USAGE, 0}
	local reply = exchange_message(h, cmd)
	if type(reply) == "nil" then return nil end
	local watts = (reply[3] * 256 + reply[4]) / 10.0
	verbose(string.format("global watts: %s\n", watts))
	return watts
end

function set_port_priority(h, port, prio)
	verbose(string.format("set priority %s for port %s\n", prio, port))
	local cmd = {MCU_CMD_SET_PORT_PRIORITY, 0, port, prio}
	exchange_message(h, cmd)
end

function disable_port_power(h, port)
	verbose(string.format("disable power on port %s\n", port))
	local cmd = {MCU_CMD_SET_PORT_POWER_DELIVERY, 0, port, 0x00}
	exchange_message(h, cmd)
end

function enable_port_power(h, port)
	verbose(string.format("enable power on port %s\n", port))
	local cmd = {MCU_CMD_SET_PORT_POWER_DELIVERY, 0, port, 0x01}
	exchange_message(h, cmd)
end

function get_port_power_usage(h, port)
	verbose(string.format("get power usage on port %s\n", port))
	local cmd = {MCU_CMD_GET_PORT_POWER_USAGE, 0, port}
	local reply = exchange_message(h, cmd)
	if type(reply) == "nil" then return nil end
	local watts = (reply[10] * 256 + reply[11]) / 10.0
	local milliamps = reply[6] * 256 + reply[7]
	verbose(string.format("port %s watts: %s milliAmps: %s\n", port, watts, milliamps))
	return watts, milliamps
end

function count_ports(h)
	local ports = ({get_controller_status(h)})[2]
	if type(ports) == "nil" then
		stderr:write(string.format("ERROR: failed to get controller global state"))
		assert(false)
	end
	io.write(string.format("controller status request ok, %s ports\n", tostring(ports)))
	poe_ports = ports
end

function ensure_defaults()
	-- make sure port count is complete
	assert(type(poe_ports) ~= "nil")
	-- default available pse port ids
	portid = {}
	local i
	for i = 1, poe_ports do
		table.insert(portid, i)
	end
	-- default global policy
	budget = PSE_WATTS
	guard = PSE_GUARD
	-- default pse port id to interface name mapping is lan1->pse1, lan2->pse2 etc.
	ifname = {}
	for i = 1, poe_ports do
		ifname[i] = "lan" .. tostring(i)
	end
	-- default is empty, so to do nothing
	portprio = {}
	portenable = {}
end

function load_config()
	-- preferred if each poe controller had its own
	-- top-level section (fx. "poe.poectrl0") but alas
	local cfg = uci.cursor()
	local k, v

	local global_budget
	local global_guard
	cfg:foreach("poe", "global", function(s)
		for k, v in pairs(s) do
			if k == "budget" then global_budget = tonumber(v) end
			if k == "guard" then global_guard = tonumber(v) end
		end
	end)
	-- if user supplied budget and/or guard values exist, apply
	if type(global_budget) == "number" then budget = global_budget end
	if type(global_guard) == "number" then guard = global_guard end

	verbose("loaded global policy")
	if type(global_budget) == "number" then verbose(" [budget]") end
	if type(global_guard) == "number" then verbose(" [guard]") end
	verbose("\n")

	local custom_ifname = {}
	local custom_enable = {}
	local custom_prio = {}
	local custom_portid = {}
	cfg:foreach("poe", "port", function(s)
		local p_id
		local p_prio
		local p_enable
		local p_name
		for k, v in pairs(s) do
			if k == "id" then p_id = tonumber(v) end
			if k == "priority" then p_prio = tonumber(v) end
			if k == "enable" then p_enable = tostring(v) end
			if k == "name" then p_name = tostring(v) end
		end
		if type(p_id) == "number" then
			table.insert(custom_portid, p_id)
			if type(p_prio) == "number" then custom_prio[p_id] = p_prio end
			if type(p_enable) == "string" then
				custom_enable[p_id] = (p_enable ~= "0" and p_enable ~= "false" and p_enable ~= "off" and p_enable ~= "no")
			end
			if type(p_name) == "string" then custom_ifname[p_id] = p_name end
		end
	end)
	-- user supplied pse port id values exist, apply
	if #custom_portid > 0 then portid = custom_portid end
	-- user supplied pse port priorities exist, apply
	if #custom_prio > 0 then portprio = custom_prio end
	-- user supplied pse port enable exist, apply
	if #custom_enable > 0 then portenable = custom_enable end
	-- user supplied eth<->pse interface name/id mapping exist, apply
	if #custom_ifname > 0 then ifname = custom_ifname end

	verbose("loaded per-port policy")
	if #custom_portid > 0 then verbose(" [port-ids: " .. tostring(#custom_portid) .. "]") end
	if #custom_prio > 0 then verbose(" [port-prios: " .. tostring(#custom_prio) .. "]") end
	if #custom_enable > 0 then verbose(" [port-enables: " .. tostring(#custom_enable) .. "]") end
	if #custom_ifname > 0 then verbose(" [port-idnames: " .. tostring(#custom_ifname) .. "]") end
	verbose("\n")

	stats_reload = stats_reload + 1
end

function apply_config(h)
	-- set global policy (budget etc.)
	set_power_limits(h, budget, guard)
	-- set policy for each port (priority etc.)
	local port, prio
	for port, prio in pairs(portprio) do
		set_port_priority(h, port - 1, prio)
	end
end

function run_administrative_actions(h)
	-- enable and disable individual ports
	local port, enable
	for port, enable in pairs(portenable) do
		local bit = enable ~= 0
		if bit then
			enable_port_power(h, port - 1)
		else
			disable_port_power(h, port - 1)
		end
	end
end

function parse_cmdline()
	local i
	for i = 1, #arg do
		if arg[i] == "-v" then log_verbose = true end
		if arg[i] == "-d" then log_debug = true end
	end
end

-- adjust verbosity
parse_cmdline()
-- quit early if wrong hardware
check_compatible()
-- make sure we can talk to the MCU
local h = configure_uart()
-- grab a port count used to build defaults
count_ports(h)
-- configure final bits and pieces of configurable settings
ensure_defaults()
-- load user config
load_config()
-- apply loaded configuration
apply_config(h)
-- finally, after applying policies, enable and disable power on the ports
run_administrative_actions(h)

uloop.init()

local conn = ubus.connect()
if not conn then
	stderr:write("ERROR: could not connect to ubus")
	assert(false)
end

local methods = {
	-- could register as eg. poe[0] in case of multiple controllers
	poe = {
		reload = {
			function(req, msg)
				ensure_defaults()
				load_config()
				apply_config(h)
				run_administrative_actions(h)
			end, {}
		},
		stats = {
			function(req, msg)
				local reply = {}
				reply['configuration reloads'] = stats_reload
				reply['messages exchanged'] = stats_msg
				reply['communication timeouts'] = stats_timeout
				reply['frame checksum errors'] = stats_checksum
				reply['notifications received'] = stats_notify
				reply['controller errors'] = stats_error
				conn:reply(req, reply)
			end, {}
		},
		sendframe = {
			function(req, msg)
				local reply = {}
				local hex = tostring(msg.frame)
				if type(hex) ~= "string" then hex = "" end
				hex = string.gsub(hex, '[^%x]', '')
				local xmit = {}
				for k in string.gmatch(hex, "(%x%x)") do table.insert(xmit, tonumber(k, 16)) end
				local recv = exchange_message_once(h, xmit)
				if type(recv) ~= "nil" then reply.frame = string.gsub(table.concat(recv, ' '), '(%d+)', function(k) return string.format('%02x', k) end) end
				conn:reply(req, reply)
			end, {frame = ubus.STRING}
		},
		info = {
			function(req, msg)
				local reply = {}
				local mcu_usage = get_global_power_usage(h)

				-- static values
				reply.mcu = CHIPNAME_MCU
				reply.pse = CHIPNAME_PSE

				-- values applied during last config reload
				reply.budget = string.format("%.1f", budget)
				reply.guard = string.format("%.1f", guard)

				-- values from controller
				reply.poe_ports = poe_ports
				-- seem to report about 4x too high ?
				reply.consumption = string.format("%.1f", mcu_usage)

				reply.ports = {}
				local i
				for i = 1, #portid do
					local item = {}
					local id = portid[i]
					local name = ifname[id]
					local prio = portprio[id]
					local enable = portenable[id]
					local w, ma = get_port_power_usage(h, id - 1)

					-- values applied during last config reload
					if type(name) ~= "nil" then item.name = name end
					if type(prio) ~= "nil" then item.priority = prio end
					if type(enable) ~= "nil" then
						if enable then item.enabled = "yes" else item.enabled = "no" end
					end

					-- values from controller
					item.consumption = string.format("%.1f", w)
					-- figure out command to fetch these
					--item.status = "disable" or "search" or "deliver" or "unknown"
					--item.mode = "poe++" or "poe+" or "poe", alt. "802.3bt" or "802.3at" or "802.3af"

					table.insert(reply.ports, item)
				end
				conn:reply(req, reply)
			end, {}
		},
		manage = {
			function(req, msg)
				local enable = msg.enable
				local portname = msg.port
				if type(enable) == "boolean" and type(portname) == "string" then
					local i
					for i = 1, #portid do
						local id = portid[i]
						local name = ifname[id]
						if name == portname then
							portenable[id] = enable
							if enable then
								enable_port_power(h, id - 1)
							else
								disable_port_power(h, id - 1)
							end
						end
					end
				end
			end, {port = ubus.STRING, enable = ubus.BOOLEAN}
		},
	},
}

conn:add(methods)

uloop.run()

Save as /usr/bin/poemgr-realtek to install

Needs a service definition too:

#!/bin/sh /etc/rc.common

START=80
USE_PROCD=1
PROG=/usr/bin/poemgr-realtek

reload_service() {
	ubus call poe reload
}

service_triggers() {
	procd_add_config_trigger "config.change" "poe" ubus call poe reload
}

start_service() {
	procd_open_instance
	procd_set_param command "$PROG" -d
	procd_set_param respawn
	procd_set_param stdout 1
	procd_set_param stderr 1
	procd_close_instance
}

Save as /etc/init.d/poemgr-realtek

Install dependencies:

opkg install lua-rs232 libuci-lua libubus-lua

(If you're not on a clean install, "opkg remove" anything else that uses /dev/ttyS1.)

Start the service:

~# chmod +x /usr/bin/poemgr-realtek
~# chmod +x /etc/init.d/poemgr-realtek
~# service poemgr-realtek start

Run a test:

~# ubus call poe info
{
        "budget": "77.0",
        "mcu": "Realtek RTL8238",
        "poe_ports": 8,
        "ports": [
                {
                        "name": "lan1",
                        "enabled": "yes",
                        "consumption": "4.9"
                },
                [... snip ...]
}
1 Like

Hello @appelsin,

thanks for your work on this.

I'm running into an issue in line 136. I added some debug information:

# poemgr-realtek 
## Error: "pseId" not defined
Model: ZyXEL_GS1900_10HPv2
Expected Model (MATCH_BOARDNAME): ZyXEL_GS1900_10HPv2
PSE ID: 
Expected PSE ID (MATCH_PSEID): 4
ERROR: unknown hardware model or revision/usr/bin/lua: /usr/bin/poemgr-realtek:141: assertion failed!

My pseid is empty. Do you have any idea what could go wrong here?

ubus doesn't work, probably because the poemgr fails at that assertion.

# ubus call poe info
Command failed: Not found

Best,
Matthias

thanks for your work on this.

Thanks for trying it out!

My pseid is empty. Do you have any idea what could go wrong here?

It's a check to see if the mainboard revision is correct, merely for the sake of being overly cautious.

The pseId comes directly from fw_printenv:

root@switch:~# fw_printenv | egrep 'pse|board'
boardmodel=ZyXEL_GS1900_10HPv2
pseId=4

But obviously pseId is not present on all rev B1 switches.

My best guess would be that the pseId exists only in switches with slightly newer stock firmware than yours. Likely added in an effort from the manufacturer to make the PoE manager in the stock firmware work on all hardware revisions of the switch.

A quick fix is to set it yourself:

root@switch:~# fw_setenv pseId 4

(I suspect a stock firmware upgrade might do the same.)

In any case, poemgr-realtek should handle that particular edge case more gracefully. Below is a software fix.

Could you try replacing line 134 in /usr/bin/poemgr-realtek:

134:	if model ~= MATCH_BOARDNAME or pse_id ~= MATCH_PSEID then

With this:

134:	if model ~= MATCH_BOARDNAME or (pse_id ~= MATCH_PSEID and pse_id ~= '') then

And restart the service (service poemgr-realtek restart)?

Check the logs with eg. logread | grep poe to see if the service starts working!

ubus doesn't work, probably because the poemgr fails at that assertion.

Yes, correct. The service refused to run and now there's noone to reply to that ubus call.

Here's the entire new version of the service (/usr/bin/poemgr-realtek):

#!/usr/bin/lua

-------------------------------------------------------------
----  For testing, start in foreground with:
----  $ poemgr-realtek -d -v
----
----  ("-d" is debug, "-v" is verbose)
----
----  While the daemon is running, PoE ports can be manually
----  enabled via ubus:
----  $ ubus call poe manage "{'port':'lan1', 'enable':true}"
----
----  Inspect the per-port consumption to see if power is delivered:
----  $ ubus call poe info
----
----  If everything works, install the PoE manager:
----    1. poe manager (this file)   --> /usr/bin/poemgr-realtek
----    2. service definition file   --> /etc/init.d/poemgr-realtek
----    3. configuration             --> /etc/config/poe
----
----  And install required packages / dependencies:
----  $ opkg install lua-rs232 libuci-lua libubus-lua
----
----  Start the service via GUI (System -> Startup) or CLI:
----  $ service poemgr-realtek start
----
----  In the configuration file, PoE ports can be enabled or
----  disabled with the "enable" property.
----
----  Priority groups can be configured with "priority".
----  Priorities range from 0 (low) to 3 (critical), with
----  1 (normal) and 2 (high) in between.
----
----  Mapping between the PSE power lines and Ethernet ports
----  can be adjusted with the "id" and "name" properties.
----
----  After modifying the configuration file or updating
----  configuration via UCI, perform a commit to push
----  changes to the PoE controller:
----  $ ubus call uci commit '{"config":"poe"}'
----
----  For reading out internal statistics, the following
----  command is available.
----  $ ubus call poe stats
----
----  For experimenting with new or updated opcodes in the
----  controller firmware, the following command is available.
----  $ ubus call poe sendframe '{"frame":"40"}'
-------------------------------------------------------------

require "uci"
require "ubus"
require "uloop"

local rs = require "luars232"
local stderr = io.stderr

-- PSE known power limits for this board (zyxel,gs1900-10hp-v2)
local PSE_WATTS = 77.0
local PSE_GUARD = 11.0

-- SoC serial port for talking to MCU
local SOC_UART = "/dev/ttyS1"

-- MCU baud rate (Realtek RTL8238)
local MCU_RATE = rs.RS232_BAUD_115200

-- How many times to try again in case of timeouts, checksum errors etc.
local MAX_RETRIES = 3

-- MCU error codes (Realtek RTL8238)
local MCU_ERR_CHECKSUM = 0x62
local MCU_ERR_INCOMPLETE = 0xfd
local MCU_ERR_MALFORMED = 0xfe
local MCU_ERR_BUSY = 0xff

-- MCU commands (Realtek RTL8238)
local MCU_CMD_GET_CONTROLLER_STATUS = 0x40
local MCU_CMD_SET_GLOBAL_POWER_LIMITS = 0x04
local MCU_CMD_GET_GLOBAL_POWER_USAGE = 0x41
local MCU_CMD_SET_PORT_PRIORITY = 0x15
local MCU_CMD_SET_PORT_POWER_DELIVERY = 0x01
local MCU_CMD_GET_PORT_POWER_USAGE = 0x44

-- Hardware the above is tested on
local MATCH_BOARDNAME = "ZyXEL_GS1900_10HPv2"
local MATCH_PSEID = "4"

-- Text markings on packages - guesswork, need photos
local CHIPNAME_PSE = "Nuvoton NUC029ZAN"
local CHIPNAME_MCU = "Realtek RTL8238"

-- Filled with defaults (1=lan1, 2=lan2 etc.) during init
local ifname
local portid, portprio, portenable
local budget, guard
local poe_ports

-- Internal statistics counters
local stats_msg = 0
local stats_timeout = 0
local stats_checksum = 0
local stats_error = 0
local stats_notify = 0
local stats_reload = 0

-- Command-line options
local log_verbose = false
local log_debug = false

local seq = 0
local queue = {}

function verbose(s)
	if log_verbose then io.write(s) end
end

function debug(s)
	if log_debug then io.write(s) end
end

function read_fw_env(key)
	local handle = io.popen(string.format("fw_printenv -n %s", key))
	local output = handle:read("*a")
	local res = output:gsub('[\n\r]', '')
	handle:close()
	return res
end

function check_compatible()
	-- mainboard revision "B1" / router model "v2" has the relevant MCU
	local model = read_fw_env("boardmodel")
	local pse_id = read_fw_env("pseId")
	if model ~= MATCH_BOARDNAME or (pse_id ~= MATCH_PSEID and pse_id ~= '') then
		stderr:write(string.format("ERROR: unknown hardware model or revision\n"))
		assert(false)
	end
end

function configure_uart()
	local err, handle = rs.open(SOC_UART)
	if err ~= rs.RS232_ERR_NOERROR then
		stderr:write(string.format("ERROR: failed to open('%s'), cause: '%s'\n", SOC_UART, rs.error_tostring(err)))
		assert(false)
	end

	assert(handle:set_baud_rate(MCU_RATE) == rs.RS232_ERR_NOERROR)
	assert(handle:set_data_bits(rs.RS232_DATA_8) == rs.RS232_ERR_NOERROR)
	assert(handle:set_parity(rs.RS232_PARITY_NONE) == rs.RS232_ERR_NOERROR)
	assert(handle:set_stop_bits(rs.RS232_STOP_1) == rs.RS232_ERR_NOERROR)
	assert(handle:set_flow_control(rs.RS232_FLOW_OFF)  == rs.RS232_ERR_NOERROR)
	io.write(string.format("configured %s\n", tostring(handle)))
	synchronize(handle)
	return handle
end

function synchronize(handle)
	local interval = 1000
	local buffer = {}
	local size = 1
	local err, data, i

	io.write(string.format("wait for quiescent %s\n", SOC_UART))
	while size > 0 do
		err, data, size = handle:read(4096, interval)
		if err == rs.RS232_ERR_NOERROR then
			for i = 1, string.len(data) do
				table.insert(buffer, string.byte(string.sub(data, i, i)))
			end
		elseif err == rs.RS232_ERR_TIMEOUT then
			size = 0
		else
			stderr:write(string.format("ERROR: failed to read(), cause: '%s'\n", rs.error_tostring(err)))
			assert(false)
		end
	end

	if #buffer > 0 then
		debug("RX")
		for i = 1, #buffer do
			debug(string.format(" %02x", buffer[i]))
		end
		debug("\n")
	end
end

function try_process_unsolicited()
	-- handle any notifications received asynchronously
	stats_notify = stats_notify + #queue
	if #queue == 0 then return nil end
	verbose(string.format("ignoring %d unknown notifications from controller\n", #queue))
	queue = {}
end

function try_exchange_message(h, xmit)
	local interval = 1000
	local retries = 10
	local recv = {}
	local round = 0
	local sum = 0
	local data = ""
	local err, size, left, i

	if #xmit > 12 then
		verbose(string.format("output frame too large, discard\n", xmit))
		return nil
	end

	if #xmit < 1 then
		verbose("output frame too small, discard\n")
		return nil
	end

	-- reserve all-zeroes and bit 2^7 for controller
	seq = (seq % 127) + 1
	xmit[2] = seq

	-- pad to full frame length
	while #xmit < 12 do
		table.insert(xmit, 0xff)
	end

	-- coalesce to zero to avoid crash on invalid input
	for i = 1, 12 do
		if type(xmit[i]) ~= "number" then xmit[i] = 0 end
		xmit[i] = math.floor(math.abs(xmit[i])) % 256
	end

	-- calculate checksum
	for i = 1, 11 do
		sum = sum + xmit[i]
	end
	xmit[12] = sum % 256

	debug("TX ->")
	for i = 1, 12 do
		debug(string.format(" %02x", xmit[i]))
	end
	debug("\n")

	for i = 1, 12 do
		data = data .. string.char(xmit[i])
	end

	err, size = h:write(data)
	assert(err == rs.RS232_ERR_NOERROR)

	while round < retries do
		while round < retries and #recv < 12 do
			err, data, size = h:read(12 - #recv, interval)
			assert(err == rs.RS232_ERR_NOERROR or err == rs.RS232_ERR_TIMEOUT)
			if err == rs.RS232_ERR_NOERROR then
				assert(#recv + size < 13)
				for i = 1, string.len(data) do
					table.insert(recv, string.byte(string.sub(data, i, i)))
				end
			end
			round = round + 1
		end

		if #recv > 0 then
			stats_msg = stats_msg + 1
			debug("RX <-")
			for i = 1, #recv do
				debug(string.format(" %02x", recv[i]))
			end
			debug("\n")
		end

		if #recv < 12 then
			stats_timeout = stats_timeout + 1
			stderr:write("timeout while waiting for response\n")
			return nil
		end

		sum = 0
		for i = 1, 11 do
			sum = sum + recv[i]
		end

		if sum % 256 ~= recv[12] then
			stats_checksum = stats_checksum + 1
			stderr:write("frame checksum invalid in response\n")
			return nil

		elseif recv[1] ~= xmit[1] and recv[2] ~= xmit[2] then
			verbose("unsolicited frame, add to queue\n");
			table.insert(queue, recv)
			recv = {}
			round = 0

		elseif recv[2] ~= xmit[2] then
			recv = {}
			verbose("wrong sequence number from controller, discard\n")

		elseif recv[1] == MCU_ERR_CHECKSUM then
			stats_error = stats_error + 1
			verbose("controller report: invalid checksum\n")

		elseif recv[1] == MCU_ERR_INCOMPLETE then
			stats_error = stats_error + 1
			verbose("controller report: incomplete request\n")

		elseif recv[1] == MCU_ERR_MALFORMED then
			stats_error = stats_error + 1
			verbose("controller report: malformed request\n")

		elseif recv[1] == MCU_ERR_BUSY then
			stats_error = stats_error + 1
			verbose("controller report: busy\n")

		elseif recv[1] ~= xmit[1] then
			stats_error = stats_error + 1
			verbose(string.format("controller report: (unknown error reply %s)\n", recv[1]));

		else
			return recv
		end
	end
end

function exchange_message_once(h, xmit)
	local recv = try_exchange_message(h, xmit)
	try_process_unsolicited()
	return recv
end

function exchange_message(h, xmit)
	local round
	for round = 1, MAX_RETRIES do
		if round > 1 then verbose("Retrying...\n") end
		local recv = exchange_message_once(h, xmit)
		if recv ~= nil then return recv end
	end
	return recv
end

function uintbe16(nr)
	-- coalesce to zero to avoid crash on invalid input
	if type(nr) ~= "number" then nr = 0 end
	local l = math.floor(nr * 10 / 256)
	local r = math.floor(nr * 10) % 256
	return l, r
end

function get_controller_status(h)
	verbose("request controller status\n")
	local cmd = {MCU_CMD_GET_CONTROLLER_STATUS}
	local reply = exchange_message(h, cmd)
	if type(reply) == "nil" then return nil end
	table.remove(reply, #reply)
	table.remove(reply, 1)
	table.remove(reply, 1)
	return unpack(reply)
end

function set_power_limits(h, total, guard)
	verbose(string.format("set power limits, global budget: %s, port guard: %s\n", total, guard))
	local cmd = {MCU_CMD_SET_GLOBAL_POWER_LIMITS, 0, 0x00}
	cmd[4], cmd[5] = uintbe16(total)
	cmd[6], cmd[7] = uintbe16(guard)
	exchange_message(h, cmd)
end

function get_global_power_usage(h)
	verbose("get global power usage\n")
	local cmd = {MCU_CMD_GET_GLOBAL_POWER_USAGE, 0}
	local reply = exchange_message(h, cmd)
	if type(reply) == "nil" then return nil end
	local watts = (reply[3] * 256 + reply[4]) / 10.0
	verbose(string.format("global watts: %s\n", watts))
	return watts
end

function set_port_priority(h, port, prio)
	verbose(string.format("set priority %s for port %s\n", prio, port))
	local cmd = {MCU_CMD_SET_PORT_PRIORITY, 0, port, prio}
	exchange_message(h, cmd)
end

function disable_port_power(h, port)
	verbose(string.format("disable power on port %s\n", port))
	local cmd = {MCU_CMD_SET_PORT_POWER_DELIVERY, 0, port, 0x00}
	exchange_message(h, cmd)
end

function enable_port_power(h, port)
	verbose(string.format("enable power on port %s\n", port))
	local cmd = {MCU_CMD_SET_PORT_POWER_DELIVERY, 0, port, 0x01}
	exchange_message(h, cmd)
end

function get_port_power_usage(h, port)
	verbose(string.format("get power usage on port %s\n", port))
	local cmd = {MCU_CMD_GET_PORT_POWER_USAGE, 0, port}
	local reply = exchange_message(h, cmd)
	if type(reply) == "nil" then return nil end
	local watts = (reply[10] * 256 + reply[11]) / 10.0
	local milliamps = reply[6] * 256 + reply[7]
	verbose(string.format("port %s watts: %s milliAmps: %s\n", port, watts, milliamps))
	return watts, milliamps
end

function count_ports(h)
	local ports = ({get_controller_status(h)})[2]
	if type(ports) == "nil" then
		stderr:write(string.format("ERROR: failed to get controller global state\n"))
		assert(false)
	end
	io.write(string.format("controller status request ok, %s ports\n", tostring(ports)))
	poe_ports = ports
end

function ensure_defaults()
	-- make sure port count is complete
	assert(type(poe_ports) ~= "nil")
	-- default available pse port ids
	portid = {}
	local i
	for i = 1, poe_ports do
		table.insert(portid, i)
	end
	-- default global policy
	budget = PSE_WATTS
	guard = PSE_GUARD
	-- default pse port id to interface name mapping is lan1->pse1, lan2->pse2 etc.
	ifname = {}
	for i = 1, poe_ports do
		ifname[i] = "lan" .. tostring(i)
	end
	-- default is empty, so to do nothing
	portprio = {}
	portenable = {}
end

function load_config()
	-- preferred if each poe controller had its own
	-- top-level section (fx. "poe.poectrl0") but alas
	local cfg = uci.cursor()
	local k, v

	local global_budget
	local global_guard
	cfg:foreach("poe", "global", function(s)
		for k, v in pairs(s) do
			if k == "budget" then global_budget = tonumber(v) end
			if k == "guard" then global_guard = tonumber(v) end
		end
	end)
	-- if user supplied budget and/or guard values exist, apply
	if type(global_budget) == "number" then budget = global_budget end
	if type(global_guard) == "number" then guard = global_guard end

	verbose("loaded global policy")
	if type(global_budget) == "number" then verbose(" [budget]") end
	if type(global_guard) == "number" then verbose(" [guard]") end
	verbose("\n")

	local custom_ifname = {}
	local custom_enable = {}
	local custom_prio = {}
	local custom_portid = {}
	cfg:foreach("poe", "port", function(s)
		local p_id
		local p_prio
		local p_enable
		local p_name
		for k, v in pairs(s) do
			if k == "id" then p_id = tonumber(v) end
			if k == "priority" then p_prio = tonumber(v) end
			if k == "enable" then p_enable = tostring(v) end
			if k == "name" then p_name = tostring(v) end
		end
		if type(p_id) == "number" then
			table.insert(custom_portid, p_id)
			if type(p_prio) == "number" then custom_prio[p_id] = p_prio end
			if type(p_enable) == "string" then
				custom_enable[p_id] = (p_enable ~= "0" and p_enable ~= "false" and p_enable ~= "off" and p_enable ~= "no")
			end
			if type(p_name) == "string" then custom_ifname[p_id] = p_name end
		end
	end)
	-- user supplied pse port id values exist, apply
	if #custom_portid > 0 then portid = custom_portid end
	-- user supplied pse port priorities exist, apply
	if #custom_prio > 0 then portprio = custom_prio end
	-- user supplied pse port enable exist, apply
	if #custom_enable > 0 then portenable = custom_enable end
	-- user supplied eth<->pse interface name/id mapping exist, apply
	if #custom_ifname > 0 then ifname = custom_ifname end

	verbose("loaded per-port policy")
	if #custom_portid > 0 then verbose(" [port-ids: " .. tostring(#custom_portid) .. "]") end
	if #custom_prio > 0 then verbose(" [port-prios: " .. tostring(#custom_prio) .. "]") end
	if #custom_enable > 0 then verbose(" [port-enables: " .. tostring(#custom_enable) .. "]") end
	if #custom_ifname > 0 then verbose(" [port-idnames: " .. tostring(#custom_ifname) .. "]") end
	verbose("\n")

	stats_reload = stats_reload + 1
end

function apply_config(h)
	-- set global policy (budget etc.)
	set_power_limits(h, budget, guard)
	-- set policy for each port (priority etc.)
	local port, prio
	for port, prio in pairs(portprio) do
		set_port_priority(h, port - 1, prio)
	end
end

function run_administrative_actions(h)
	-- enable and disable individual ports
	local port, enable
	for port, enable in pairs(portenable) do
		local bit = enable ~= 0
		if bit then
			enable_port_power(h, port - 1)
		else
			disable_port_power(h, port - 1)
		end
	end
end

function parse_cmdline()
	local i
	for i = 1, #arg do
		if arg[i] == "-v" then log_verbose = true end
		if arg[i] == "-d" then log_debug = true end
	end
end

-- adjust verbosity
parse_cmdline()
-- quit early if wrong hardware
check_compatible()
-- make sure we can talk to the MCU
local h = configure_uart()
-- grab a port count used to build defaults
count_ports(h)
-- configure final bits and pieces of configurable settings
ensure_defaults()
-- load user config
load_config()
-- apply loaded configuration
apply_config(h)
-- finally, after applying policies, enable and disable power on the ports
run_administrative_actions(h)

uloop.init()

local conn = ubus.connect()
if not conn then
	stderr:write("ERROR: could not connect to ubus\n")
	assert(false)
end

local methods = {
	-- could register as eg. poe[0] in case of multiple controllers
	poe = {
		reload = {
			function(req, msg)
				ensure_defaults()
				load_config()
				apply_config(h)
				run_administrative_actions(h)
			end, {}
		},
		stats = {
			function(req, msg)
				local reply = {}
				reply['configuration reloads'] = stats_reload
				reply['messages exchanged'] = stats_msg
				reply['communication timeouts'] = stats_timeout
				reply['frame checksum errors'] = stats_checksum
				reply['notifications received'] = stats_notify
				reply['controller errors'] = stats_error
				conn:reply(req, reply)
			end, {}
		},
		sendframe = {
			function(req, msg)
				local reply = {}
				local hex = tostring(msg.frame)
				if type(hex) ~= "string" then hex = "" end
				hex = string.gsub(hex, '[^%x]', '')
				local xmit = {}
				for k in string.gmatch(hex, "(%x%x)") do table.insert(xmit, tonumber(k, 16)) end
				local recv = exchange_message_once(h, xmit)
				if type(recv) ~= "nil" then reply.frame = string.gsub(table.concat(recv, ' '), '(%d+)', function(k) return string.format('%02x', k) end) end
				conn:reply(req, reply)
			end, {frame = ubus.STRING}
		},
		info = {
			function(req, msg)
				local reply = {}
				local mcu_usage = get_global_power_usage(h)

				-- static values
				reply.mcu = CHIPNAME_MCU
				reply.pse = CHIPNAME_PSE

				-- values applied during last config reload
				reply.budget = string.format("%.1f", budget)
				reply.guard = string.format("%.1f", guard)

				-- values from controller
				reply.poe_ports = poe_ports
				-- seem to report about 4x too high ?
				reply.consumption = string.format("%.1f", mcu_usage)

				reply.ports = {}
				local i
				for i = 1, #portid do
					local item = {}
					local id = portid[i]
					local name = ifname[id]
					local prio = portprio[id]
					local enable = portenable[id]
					local w, ma = get_port_power_usage(h, id - 1)

					-- values applied during last config reload
					if type(name) ~= "nil" then item.name = name end
					if type(prio) ~= "nil" then item.priority = prio end
					if type(enable) ~= "nil" then
						if enable then item.enabled = "yes" else item.enabled = "no" end
					end

					-- values from controller
					item.consumption = string.format("%.1f", w)
					-- figure out command to fetch these
					--item.status = "disable" or "search" or "deliver" or "unknown"
					--item.mode = "poe++" or "poe+" or "poe", alt. "802.3bt" or "802.3at" or "802.3af"

					table.insert(reply.ports, item)
				end
				conn:reply(req, reply)
			end, {}
		},
		manage = {
			function(req, msg)
				local enable = msg.enable
				local portname = msg.port
				if type(enable) == "boolean" and type(portname) == "string" then
					local i
					for i = 1, #portid do
						local id = portid[i]
						local name = ifname[id]
						if name == portname then
							portenable[id] = enable
							if enable then
								enable_port_power(h, id - 1)
							else
								disable_port_power(h, id - 1)
							end
						end
					end
				end
			end, {port = ubus.STRING, enable = ubus.BOOLEAN}
		},
	},
}

conn:add(methods)

uloop.run()

And the init script (/etc/init.d/poemgr-realtek):

#!/bin/sh /etc/rc.common

START=80
USE_PROCD=1
PROG=/usr/bin/poemgr-realtek

reload_service() {
	ubus call poe reload
}

service_triggers() {
	procd_add_config_trigger "config.change" "poe" ubus call poe reload
}

start_service() {
	procd_open_instance
	procd_set_param command "$PROG" -d
	procd_set_param respawn
	procd_set_param stdout 1
	procd_set_param stderr 1
	procd_close_instance
}
2 Likes

Now that I think of it, poemgr-realtek should work on the Zyxel GS1900-24EP model switches too.

We need someone who owns that model to test it (with another modification to the boardmodel + pseid check). :thinking:

This works, awesome!

root@Switch-Garage:~# ubus call poe info
{
	"ports": [
		{
			"enabled": "yes",
			"priority": 2,
			"name": "lan1",
			"consumption": "0.0"
		},
		{
			"enabled": "yes",
			"priority": 2,
			"name": "lan2",
			"consumption": "0.0"
		},
		{
			"enabled": "yes",
			"priority": 2,
			"name": "lan3",
			"consumption": "0.0"
		},
		{
			"enabled": "yes",
			"priority": 2,
			"name": "lan4",
			"consumption": "0.0"
		},
		{
			"enabled": "yes",
			"priority": 2,
			"name": "lan5",
			"consumption": "0.0"
		},
		{
			"enabled": "yes",
			"priority": 2,
			"name": "lan6",
			"consumption": "0.0"
		},
		{
			"enabled": "yes",
			"priority": 2,
			"name": "lan7",
			"consumption": "0.0"
		},
		{
			"enabled": "yes",
			"priority": 2,
			"name": "lan8",
			"consumption": "3.1"
		}
	],
	"pse": "Nuvoton NUC029ZAN",
	"consumption": "30.0",
	"budget": "77.0",
	"poe_ports": 8,
	"mcu": "Realtek RTL8238",
	"guard": "11.0"
}

The output is correct, only port 8 provides power in my setup.

Edit:

Do you plan to implement an easier syntax to manage the power delivery?

root@Switch-Dachboden:~# ubus call poe manage "{'port':'lan3', 'enable':false}"
root@Switch-Dachboden:~# ubus call poe manage "{'port':'lan3', 'enable':true}"

That's functional but not very elegant nor easy to remember.

Something like:

ubus call poe manage lan3 disable|enable

would be nice

1 Like

This works, awesome!

Great stuff. Thanks for coming back with an update!

...
The output is correct, only port 8 provides power in my setup.

Looks good. The per-port power limit of 11W is the default setting, so likely there is no global "guard" setting in /etc/config/poe. You can set it with uci, and probably up it to whatever the stock firmware uses if necessary.

Do you plan to implement an easier syntax to manage the power delivery?

The possibility for improvement here is somewhat limited, since ubus expects JSON for the parameters.

Note also that changes performed with the 'manage' command are immediately sent to the controller and not saved to configuration. If one does this instead:

root@switch:~# uci set poe.@port[7].enable=1
root@switch:~# ubus call uci commit '{"config":"poe"}'

Then the changes will persist in the configuration (/etc/config/poe) across reboots etc. Also happens to more or less emulate what a GUI for PoE implemented in LuCI would do.

I can see how both syntaxes are slightly unwieldy though. Perhaps a shell script to make it more user friendly could do in a pinch?

#!/bin/sh

PORT=$(uci show poe | egrep '^poe.@port' | grep ".name='${1}'" | awk -F. '{print $2}' | egrep -o '\d+')
ENABLE=$(echo "${2}" | sed 's/^$/-1/g' | sed 's/enable/1/g' | sed 's/disable/0/g')

if [ "${PORT}" != "" ] && [ "${ENABLE}" -ge 0 ] && [ "${ENABLE}" -le 1 ]; then
	uci set poe.@port["${PORT}"].enable="${ENABLE}"
	ubus call uci commit '{"config":"poe"}'
fi

Save to /usr/bin/poe and run with:

root@switch:~# poe lan8 enable
3 Likes