Beg to differ.
Pretty sure that PoE works perfectly fine, if you use this:
/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 smallest supported device (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_1 = "ZyXEL_GS1900_10HPv2"
local MATCH_BOARDNAME_2 = "ZyXEL_GS1900_24EP"
local MATCH_PSEID_1 = "4"
local MATCH_PSEID_2 = ""
-- Text markings on packages - guesswork, need photos
local CHIPNAME_PSE = "Nuvoton NUC029ZAN"
local CHIPNAME_MCU = "Realtek RTL8238"
-- 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 skip_hwcheck = false
local log_verbose = false
local log_debug = false
-- Filled with defaults (1=lan1, 2=lan2 etc.) during init
local ifname
local portid, portprio, portenable
local budget, guard
local poe_ports
-- Globals
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 check(cond, msg)
if not cond then
if type(msg) == "function" then msg = msg() end
stderr:write(msg)
assert(false)
end
end
function read_fw(tool, key)
local handle = io.popen(string.format("%s -n %s", tool, key))
local output = handle:read("*a")
local res = output:gsub('[\n\r]', '')
handle:close()
return res
end
function read_fw_env(key)
return read_fw("fw_printenv", key)
end
function read_fw_sys(key)
return read_fw("fw_printsys", key)
end
function check_compatible()
if skip_hwcheck then
verbose("skip board revision check\n")
else
local model = read_fw_env("boardmodel")
local pseid = read_fw_sys("pseId")
check(
(model == MATCH_BOARDNAME_1 or model == MATCH_BOARDNAME_2) and (pseid == MATCH_PSEID_1 or pseid == MATCH_PSEID_2),
"ERROR: unknown hardware model or revision\n"
)
end
end
function configure_uart()
local err, handle = rs.open(SOC_UART)
check(err == rs.RS232_ERR_NOERROR, function() return string.format("ERROR: failed to open('%s'): %s\n", SOC_UART, rs.error_tostring(err)) end)
check(handle:set_baud_rate(MCU_RATE) == rs.RS232_ERR_NOERROR, "ERROR: failed to set baud rate")
check(handle:set_data_bits(rs.RS232_DATA_8) == rs.RS232_ERR_NOERROR, "ERROR: failed to set data bits")
check(handle:set_parity(rs.RS232_PARITY_NONE) == rs.RS232_ERR_NOERROR, "ERROR: failed to set parity")
check(handle:set_stop_bits(rs.RS232_STOP_1) == rs.RS232_ERR_NOERROR, "ERROR: failed to set stop bits")
check(handle:set_flow_control(rs.RS232_FLOW_OFF) == rs.RS232_ERR_NOERROR, "ERROR: failed to disable flow control")
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
check(false, string.format("ERROR: failed to read(): %s\n", rs.error_tostring(err)))
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)
check(err == rs.RS232_ERR_NOERROR, function() return string.format("ERROR: failed to write(): %s\n", rs.error_tostring(err)) end)
while round < retries do
while round < retries and #recv < 12 do
err, data, size = h:read(12 - #recv, interval)
if err == rs.RS232_ERR_NOERROR then
check(#recv + size < 13, "ERROR: read() returned more octets than asked")
check(size > 0, "ERROR: RX buffer emptied between select() and read()")
for i = 1, string.len(data) do
table.insert(recv, string.byte(string.sub(data, i, i)))
end
elseif err ~= rs.RS232_ERR_TIMEOUT then
check(false, string.format("ERROR: failed to read(): %s\n", rs.error_tostring(err)))
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 recv, round
for round = 1, MAX_RETRIES do
if round > 1 then verbose("Retrying...\n") end
recv = exchange_message_once(h, xmit)
if recv ~= nil then break end
end
return recv
end
function uintbe16(nr)
-- coalesce to zero to avoid crash on malformed 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]
check(type(ports) ~= "nil", "ERROR: failed to get controller global state\n")
io.write(string.format("controller status request ok, %s ports\n", tostring(ports)))
poe_ports = ports
end
function ensure_defaults()
check(type(poe_ports) ~= "nil", "ERROR: port count not retrieved yet\n")
-- 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
if arg[i] == "-f" then skip_hwcheck = true end
end
end
-- adjust verbosity etc.
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()
check(conn, "ERROR: could not connect to ubus\n")
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()
/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"
procd_set_param respawn
procd_set_param stdout 1
procd_set_param stderr 1
procd_close_instance
}
Since it was specifically designed for that new PSE controller...
I'm using it myself on another switch (Zyxel GS1900-10HPv2) having that same PSE controller with great success.
Still waiting for someone to actually test it on the Zyxel GS1900-24EP though (without running realtek-poe
at the same time), please feel free to apply if you have this switch.