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
}