Compare commits

..

1 Commits

Author SHA1 Message Date
Mikayla
e21c7d92fe Merge pull request #60 from MikaylaFischler/devel
Alpha PLC, RTU, and Supervisor Release
2022-05-27 18:43:01 -04:00
115 changed files with 1120 additions and 11849 deletions

1
.gitignore vendored
View File

@@ -1 +0,0 @@
_notes/

View File

@@ -8,10 +8,6 @@
"parallel",
"colors",
"textutils",
"shell",
"settings",
"window",
"read",
"periphemu"
"shell"
]
}

View File

@@ -1,16 +0,0 @@
local apisessions = {}
---@param packet capi_frame
function apisessions.handle_packet(packet)
end
function apisessions.check_all_watchdogs()
end
function apisessions.close_all()
end
function apisessions.free_all_closed()
end
return apisessions

View File

@@ -1,24 +0,0 @@
local config = {}
-- port of the SCADA supervisor
config.SCADA_SV_PORT = 16100
-- port to listen to incoming packets from supervisor
config.SCADA_SV_LISTEN = 16101
-- listen port for SCADA coordinator API access
config.SCADA_API_LISTEN = 16200
-- expected number of reactor units, used only to require that number of unit monitors
config.NUM_UNITS = 4
-- graphics color
config.RECOLOR = true
-- log path
config.LOG_PATH = "/log.txt"
-- log mode
-- 0 = APPEND (adds to existing file on start)
-- 1 = NEW (replaces existing file on start)
config.LOG_MODE = 0
-- crypto config
config.SECURE = true
-- must be common between all devices
config.PASSWORD = "testpassword!"
return config

View File

@@ -1,459 +1,12 @@
local comms = require("scada-common.comms")
local log = require("scada-common.log")
local ppm = require("scada-common.ppm")
local util = require("scada-common.util")
local apisessions = require("coordinator.apisessions")
local iocontrol = require("coordinator.iocontrol")
local dialog = require("coordinator.ui.dialog")
local coordinator = {}
local print = util.print
local println = util.println
local print_ts = util.print_ts
local println_ts = util.println_ts
local PROTOCOLS = comms.PROTOCOLS
local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES
local SCADA_CRDN_TYPES = comms.SCADA_CRDN_TYPES
-- request the user to select a monitor
---@param names table available monitors
local function ask_monitor(names)
println("available monitors:")
for i = 1, #names do
print(" " .. names[i])
end
println("")
println("select a monitor or type c to cancel")
local iface = dialog.ask_options(names, "c")
if iface ~= false and iface ~= nil then
util.filter_table(names, function (x) return x ~= iface end)
end
return iface
end
-- configure monitor layout
---@param num_units integer number of units expected
function coordinator.configure_monitors(num_units)
---@class monitors_struct
local monitors = {
primary = nil,
primary_name = "",
unit_displays = {},
unit_name_map = {}
}
local monitors_avail = ppm.get_monitor_list()
local names = {}
-- get all interface names
for iface, _ in pairs(monitors_avail) do
table.insert(names, iface)
end
-- we need a certain number of monitors (1 per unit + 1 primary display)
if #names < num_units + 1 then
println("not enough monitors connected (need " .. num_units + 1 .. ")")
log.warning("insufficient monitors present (need " .. num_units + 1 .. ")")
return false
end
-- attempt to load settings
settings.load("/coord.settings")
---------------------
-- PRIMARY DISPLAY --
---------------------
local iface_primary_display = settings.get("PRIMARY_DISPLAY")
if not util.table_contains(names, iface_primary_display) then
println("primary display is not connected")
local response = dialog.ask_y_n("would you like to change it", true)
if response == false then return false end
iface_primary_display = nil
end
while iface_primary_display == nil and #names > 0 do
-- lets get a monitor
iface_primary_display = ask_monitor(names)
end
if iface_primary_display == false then return false end
settings.set("PRIMARY_DISPLAY", iface_primary_display)
util.filter_table(names, function (x) return x ~= iface_primary_display end)
monitors.primary = ppm.get_periph(iface_primary_display)
monitors.primary_name = iface_primary_display
-------------------
-- UNIT DISPLAYS --
-------------------
local unit_displays = settings.get("UNIT_DISPLAYS")
if unit_displays == nil then
unit_displays = {}
for i = 1, num_units do
local display = nil
while display == nil and #names > 0 do
-- lets get a monitor
println("please select monitor for unit " .. i)
display = ask_monitor(names)
end
if display == false then return false end
unit_displays[i] = display
end
else
-- make sure all displays are connected
for i = 1, num_units do
---@diagnostic disable-next-line: need-check-nil
local display = unit_displays[i]
if not util.table_contains(names, display) then
local response = dialog.ask_y_n("unit display " .. i .. " is not connected, would you like to change it?", true)
if response == false then return false end
display = nil
end
while display == nil and #names > 0 do
-- lets get a monitor
display = ask_monitor(names)
end
if display == false then return false end
unit_displays[i] = display
end
end
settings.set("UNIT_DISPLAYS", unit_displays)
settings.save("/coord.settings")
for i = 1, #unit_displays do
monitors.unit_displays[i] = ppm.get_periph(unit_displays[i])
monitors.unit_name_map[i] = unit_displays[i]
end
return true, monitors
end
-- dmesg print wrapper
---@param message string message
---@param dmesg_tag string tag
---@param working? boolean to use dmesg_working
---@return function? update, function? done
local function log_dmesg(message, dmesg_tag, working)
local colors = {
GRAPHICS = colors.green,
SYSTEM = colors.cyan,
BOOT = colors.blue,
COMMS = colors.purple
}
if working then
return log.dmesg_working(message, dmesg_tag, colors[dmesg_tag])
else
log.dmesg(message, dmesg_tag, colors[dmesg_tag])
end
end
function coordinator.log_graphics(message) log_dmesg(message, "GRAPHICS") end
function coordinator.log_sys(message) log_dmesg(message, "SYSTEM") end
function coordinator.log_boot(message) log_dmesg(message, "BOOT") end
function coordinator.log_comms(message) log_dmesg(message, "COMMS") end
---@param message string
---@return function update, function done
function coordinator.log_comms_connecting(message) return log_dmesg(message, "COMMS", true) end
-- coordinator communications
---@param version string
---@param modem table
---@param sv_port integer
---@param sv_listen integer
---@param api_listen integer
---@param sv_watchdog watchdog
function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, sv_watchdog)
coordinator.coord_comms = function ()
local self = {
sv_linked = false,
sv_seq_num = 0,
sv_r_seq_num = nil,
modem = modem,
connected = false
reactor_struct_cache = nil
}
---@class coord_comms
local public = {}
-- PRIVATE FUNCTIONS --
-- open all channels
local function _open_channels()
if not self.modem.isOpen(sv_listen) then
self.modem.open(sv_listen)
end
if not self.modem.isOpen(api_listen) then
self.modem.open(api_listen)
end
end
-- open at construct time
_open_channels()
-- send a packet to the supervisor
---@param msg_type SCADA_MGMT_TYPES|SCADA_CRDN_TYPES
---@param msg table
local function _send_sv(protocol, msg_type, msg)
local s_pkt = comms.scada_packet()
local pkt = nil ---@type mgmt_packet|crdn_packet
if protocol == PROTOCOLS.SCADA_MGMT then
pkt = comms.mgmt_packet()
elseif protocol == PROTOCOLS.SCADA_CRDN then
pkt = comms.crdn_packet()
else
return
end
pkt.make(msg_type, msg)
s_pkt.make(self.sv_seq_num, protocol, pkt.raw_sendable())
self.modem.transmit(sv_port, sv_listen, s_pkt.raw_sendable())
self.sv_seq_num = self.sv_seq_num + 1
end
-- attempt connection establishment
local function _send_establish()
_send_sv(PROTOCOLS.SCADA_CRDN, SCADA_CRDN_TYPES.ESTABLISH, { version })
end
-- keep alive ack
---@param srv_time integer
local function _send_keep_alive_ack(srv_time)
_send_sv(PROTOCOLS.SCADA_MGMT, SCADA_MGMT_TYPES.KEEP_ALIVE, { srv_time, util.time() })
end
-- PUBLIC FUNCTIONS --
-- reconnect a newly connected modem
---@param modem table
---@diagnostic disable-next-line: redefined-local
function public.reconnect_modem(modem)
self.modem = modem
_open_channels()
end
-- close the connection to the server
function public.close()
sv_watchdog.cancel()
self.sv_linked = false
_send_sv(PROTOCOLS.SCADA_MGMT, SCADA_MGMT_TYPES.CLOSE, {})
end
-- attempt to connect to the subervisor
---@param timeout_s number timeout in seconds
---@param tick_dmesg_waiting function callback to tick dmesg waiting
---@param task_done function callback to show done on dmesg
---@return boolean sv_linked true if connected, false otherwise
--- EVENT_CONSUMER: this function consumes events
function public.sv_connect(timeout_s, tick_dmesg_waiting, task_done)
local clock = util.new_clock(1)
local start = util.time_s()
local terminated = false
_send_establish()
clock.start()
while (util.time_s() - start) < timeout_s and not self.sv_linked do
local event, p1, p2, p3, p4, p5 = util.pull_event()
if event == "timer" and clock.is_clock(p1) then
-- timed out attempt, try again
tick_dmesg_waiting(math.max(0, timeout_s - (util.time_s() - start)))
_send_establish()
clock.start()
elseif event == "modem_message" then
-- handle message
local packet = public.parse_packet(p1, p2, p3, p4, p5)
if packet ~= nil and packet.type == SCADA_CRDN_TYPES.ESTABLISH then
public.handle_packet(packet)
end
elseif event == "terminate" then
terminated = true
break
end
end
task_done(self.sv_linked)
if terminated then
coordinator.log_comms("supervisor connection attempt cancelled by user")
end
return self.sv_linked
end
-- parse a packet
---@param side string
---@param sender integer
---@param reply_to integer
---@param message any
---@param distance integer
---@return mgmt_frame|crdn_frame|capi_frame|nil packet
function public.parse_packet(side, sender, reply_to, message, distance)
local pkt = nil
local s_pkt = comms.scada_packet()
-- parse packet as generic SCADA packet
s_pkt.receive(side, sender, reply_to, message, distance)
if s_pkt.is_valid() then
-- get as SCADA management packet
if s_pkt.protocol() == PROTOCOLS.SCADA_MGMT then
local mgmt_pkt = comms.mgmt_packet()
if mgmt_pkt.decode(s_pkt) then
pkt = mgmt_pkt.get()
end
-- get as coordinator packet
elseif s_pkt.protocol() == PROTOCOLS.SCADA_CRDN then
local crdn_pkt = comms.crdn_packet()
if crdn_pkt.decode(s_pkt) then
pkt = crdn_pkt.get()
end
-- get as coordinator API packet
elseif s_pkt.protocol() == PROTOCOLS.COORD_API then
local capi_pkt = comms.capi_packet()
if capi_pkt.decode(s_pkt) then
pkt = capi_pkt.get()
end
else
log.debug("attempted parse of illegal packet type " .. s_pkt.protocol(), true)
end
end
return pkt
end
-- handle a packet
---@param packet mgmt_frame|crdn_frame|capi_frame
function public.handle_packet(packet)
if packet ~= nil then
local protocol = packet.scada_frame.protocol()
if protocol == PROTOCOLS.COORD_API then
apisessions.handle_packet(packet)
else
-- check sequence number
if self.sv_r_seq_num == nil then
self.sv_r_seq_num = packet.scada_frame.seq_num()
elseif self.connected and self.sv_r_seq_num >= packet.scada_frame.seq_num() then
log.warning("sequence out-of-order: last = " .. self.sv_r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
return
else
self.sv_r_seq_num = packet.scada_frame.seq_num()
end
-- feed watchdog on valid sequence number
sv_watchdog.feed()
-- handle packet
if protocol == PROTOCOLS.SCADA_CRDN then
if packet.type == SCADA_CRDN_TYPES.ESTABLISH then
-- connection with supervisor established
if packet.length > 1 then
-- get configuration
---@class facility_conf
local conf = {
num_units = packet.data[1],
defs = {} -- boilers and turbines
}
if (packet.length - 1) == (conf.num_units * 2) then
-- record sequence of pairs of [#boilers, #turbines] per unit
for i = 2, packet.length do
table.insert(conf.defs, packet.data[i])
end
-- init io controller
iocontrol.init(conf)
self.sv_linked = true
else
log.debug("supervisor conn establish packet length mismatch")
end
else
log.debug("supervisor conn establish packet length mismatch")
end
elseif packet.type == SCADA_CRDN_TYPES.STRUCT_BUILDS then
-- record builds
if iocontrol.record_builds(packet.data) then
-- acknowledge receipt of builds
_send_sv(PROTOCOLS.SCADA_CRDN, SCADA_CRDN_TYPES.STRUCT_BUILDS, {})
else
log.error("received invalid build packet")
end
elseif packet.type == SCADA_CRDN_TYPES.UNIT_STATUSES then
-- update statuses
if not iocontrol.update_statuses(packet.data) then
log.error("received invalid unit statuses packet")
end
elseif packet.type == SCADA_CRDN_TYPES.COMMAND_UNIT then
elseif packet.type == SCADA_CRDN_TYPES.ALARM then
else
log.warning("received unknown SCADA_CRDN packet type " .. packet.type)
end
elseif protocol == PROTOCOLS.SCADA_MGMT then
if packet.type == SCADA_MGMT_TYPES.KEEP_ALIVE then
-- keep alive request received, echo back
if packet.length == 1 then
local timestamp = packet.data[1]
local trip_time = util.time() - timestamp
if trip_time > 500 then
log.warning("coord KEEP_ALIVE trip time > 500ms (" .. trip_time .. "ms)")
end
-- log.debug("coord RTT = " .. trip_time .. "ms")
_send_keep_alive_ack(timestamp)
else
log.debug("SCADA keep alive packet length mismatch")
end
elseif packet.type == SCADA_MGMT_TYPES.CLOSE then
-- handle session close
sv_watchdog.cancel()
self.sv_linked = false
println_ts("server connection closed by remote host")
log.warning("server connection closed by remote host")
else
log.warning("received unknown SCADA_MGMT packet type " .. packet.type)
end
else
-- should be unreachable assuming packet is from parse_packet()
log.error("illegal packet type " .. protocol, true)
end
end
end
end
-- check if the coordinator is still linked to the supervisor
function public.is_linked() return self.sv_linked end
return public
end
return coordinator

View File

@@ -1,285 +0,0 @@
local psil = require("scada-common.psil")
local log = require("scada-common.log")
local iocontrol = {}
---@class ioctl
local io = {}
-- initialize the coordinator IO controller
---@param conf facility_conf configuration
function iocontrol.init(conf)
io.facility = {
scram = false,
num_units = conf.num_units,
ps = psil.create()
}
io.units = {}
for i = 1, conf.num_units do
---@class ioctl_entry
local entry = {
unit_id = i, ---@type integer
initialized = false,
num_boilers = 0,
num_turbines = 0,
control_state = false,
burn_rate_cmd = 0.0,
waste_control = 0,
---@fixme debug stubs to be linked into comms later?
start = function () print("UNIT " .. i .. ": start") end,
scram = function () print("UNIT " .. i .. ": SCRAM") end,
set_burn = function (rate) print("UNIT " .. i .. ": set burn rate to " .. rate) end,
reactor_ps = psil.create(),
reactor_data = {}, ---@type reactor_db
boiler_ps_tbl = {},
boiler_data_tbl = {},
turbine_ps_tbl = {},
turbine_data_tbl = {}
}
for _ = 1, conf.defs[(i * 2) - 1] do
local data = {} ---@type boiler_session_db|boilerv_session_db
table.insert(entry.boiler_ps_tbl, psil.create())
table.insert(entry.boiler_data_tbl, data)
end
for _ = 1, conf.defs[i * 2] do
local data = {} ---@type turbine_session_db|turbinev_session_db
table.insert(entry.turbine_ps_tbl, psil.create())
table.insert(entry.turbine_data_tbl, data)
end
entry.num_boilers = #entry.boiler_data_tbl
entry.num_turbines = #entry.turbine_data_tbl
table.insert(io.units, entry)
end
end
-- populate structure builds
---@param builds table
---@return boolean valid
function iocontrol.record_builds(builds)
if #builds ~= #io.units then
log.error("number of provided unit builds does not match expected number of units")
return false
else
for i = 1, #builds do
local unit = io.units[i] ---@type ioctl_entry
local build = builds[i]
-- reactor build
unit.reactor_data.mek_struct = build.reactor
for key, val in pairs(unit.reactor_data.mek_struct) do
unit.reactor_ps.publish(key, val)
end
-- boiler builds
for id, boiler in pairs(build.boilers) do
unit.boiler_data_tbl[id] = {
formed = boiler[2], ---@type boolean|nil
build = boiler[1] ---@type table
}
unit.boiler_ps_tbl[id].publish("formed", boiler[2])
for key, val in pairs(unit.boiler_data_tbl[id].build) do
unit.boiler_ps_tbl[id].publish(key, val)
end
end
-- turbine builds
for id, turbine in pairs(build.turbines) do
unit.turbine_data_tbl[id] = {
formed = turbine[2], ---@type boolean|nil
build = turbine[1] ---@type table
}
unit.turbine_ps_tbl[id].publish("formed", turbine[2])
for key, val in pairs(unit.turbine_data_tbl[id].build) do
unit.turbine_ps_tbl[id].publish(key, val)
end
end
end
end
return true
end
-- update unit statuses
---@param statuses table
---@return boolean valid
function iocontrol.update_statuses(statuses)
if #statuses ~= #io.units then
log.error("number of provided unit statuses does not match expected number of units")
return false
else
for i = 1, #statuses do
local unit = io.units[i] ---@type ioctl_entry
local status = statuses[i]
-- reactor PLC status
local reactor_status = status[1]
if #reactor_status == 0 then
unit.reactor_ps.publish("computed_status", 1) -- disconnected
else
local mek_status = reactor_status[1]
local rps_status = reactor_status[2]
local gen_status = reactor_status[3]
unit.reactor_data.last_status_update = gen_status[1]
unit.reactor_data.control_state = gen_status[2]
unit.reactor_data.rps_tripped = gen_status[3]
unit.reactor_data.rps_trip_cause = gen_status[4]
unit.reactor_data.degraded = gen_status[5]
unit.reactor_data.rps_status = rps_status ---@type rps_status
unit.reactor_data.mek_status = mek_status ---@type mek_status
if unit.reactor_data.mek_status.status then
unit.reactor_ps.publish("computed_status", 3) -- running
else
if unit.reactor_data.degraded then
unit.reactor_ps.publish("computed_status", 5) -- faulted
elseif unit.reactor_data.rps_tripped and unit.reactor_data.rps_trip_cause ~= "manual" then
unit.reactor_ps.publish("computed_status", 4) -- SCRAM
else
unit.reactor_ps.publish("computed_status", 2) -- disabled
end
end
for key, val in pairs(unit.reactor_data) do
if key ~= "rps_status" and key ~= "mek_struct" and key ~= "mek_status" then
unit.reactor_ps.publish(key, val)
end
end
for key, val in pairs(unit.reactor_data.rps_status) do
unit.reactor_ps.publish(key, val)
end
for key, val in pairs(unit.reactor_data.mek_status) do
unit.reactor_ps.publish(key, val)
end
end
-- annunciator
local annunciator = status[2] ---@type annunciator
for key, val in pairs(annunciator) do
if key == "TurbineTrip" then
-- split up turbine trip table for all turbines and a general OR combination
local trips = val
local any = false
for id = 1, #trips do
any = any or trips[id]
unit.turbine_ps_tbl[id].publish(key, trips[id])
end
unit.reactor_ps.publish("TurbineTrip", any)
elseif key == "BoilerOnline" or key == "HeatingRateLow" then
-- split up array for all boilers
for id = 1, #val do
unit.boiler_ps_tbl[id].publish(key, val[id])
end
elseif key == "TurbineOnline" or key == "SteamDumpOpen" or key == "TurbineOverSpeed" then
-- split up array for all turbines
for id = 1, #val do
unit.turbine_ps_tbl[id].publish(key, val[id])
end
elseif type(val) == "table" then
-- we missed one of the tables?
log.error("unrecognized table found in annunciator list, this is a bug", true)
else
-- non-table fields
unit.reactor_ps.publish(key, val)
end
end
-- RTU statuses
local rtu_statuses = status[3]
-- boiler statuses
for id = 1, #unit.boiler_data_tbl do
if rtu_statuses.boilers[i] == nil then
-- disconnected
unit.boiler_ps_tbl[id].publish("computed_status", 1)
end
end
for id, boiler in pairs(rtu_statuses.boilers) do
unit.boiler_data_tbl[id].state = boiler[1] ---@type table
unit.boiler_data_tbl[id].tanks = boiler[2] ---@type table
local data = unit.boiler_data_tbl[id] ---@type boiler_session_db|boilerv_session_db
if data.state.boil_rate > 0 then
unit.boiler_ps_tbl[id].publish("computed_status", 3) -- active
else
unit.boiler_ps_tbl[id].publish("computed_status", 2) -- idle
end
for key, val in pairs(unit.boiler_data_tbl[id].state) do
unit.boiler_ps_tbl[id].publish(key, val)
end
for key, val in pairs(unit.boiler_data_tbl[id].tanks) do
unit.boiler_ps_tbl[id].publish(key, val)
end
end
-- turbine statuses
for id = 1, #unit.turbine_ps_tbl do
if rtu_statuses.turbines[i] == nil then
-- disconnected
unit.turbine_ps_tbl[id].publish("computed_status", 1)
end
end
for id, turbine in pairs(rtu_statuses.turbines) do
unit.turbine_data_tbl[id].state = turbine[1] ---@type table
unit.turbine_data_tbl[id].tanks = turbine[2] ---@type table
local data = unit.turbine_data_tbl[id] ---@type turbine_session_db|turbinev_session_db
if data.tanks.steam_fill >= 0.99 then
unit.turbine_ps_tbl[id].publish("computed_status", 4) -- trip
elseif data.state.flow_rate < 100 then
unit.turbine_ps_tbl[id].publish("computed_status", 2) -- idle
else
unit.turbine_ps_tbl[id].publish("computed_status", 3) -- active
end
for key, val in pairs(unit.turbine_data_tbl[id].state) do
unit.turbine_ps_tbl[id].publish(key, val)
end
for key, val in pairs(unit.turbine_data_tbl[id].tanks) do
unit.turbine_ps_tbl[id].publish(key, val)
end
end
end
end
return true
end
-- get the IO controller database
function iocontrol.get_db() return io end
return iocontrol

View File

@@ -1,157 +0,0 @@
local log = require("scada-common.log")
local iocontrol = require("coordinator.iocontrol")
local style = require("coordinator.ui.style")
local main_view = require("coordinator.ui.layout.main_view")
local unit_view = require("coordinator.ui.layout.unit_view")
local renderer = {}
-- render engine
local engine = {
monitors = nil,
dmesg_window = nil,
ui_ready = false
}
-- UI layouts
local ui = {
main_layout = nil,
unit_layouts = {}
}
-- reset a display to the "default", but set text scale to 0.5
---@param monitor table monitor
---@param recolor? boolean override default color palette
local function _reset_display(monitor, recolor)
monitor.setTextScale(0.5)
monitor.setTextColor(colors.white)
monitor.setBackgroundColor(colors.black)
monitor.clear()
monitor.setCursorPos(1, 1)
if recolor then
-- set overridden colors
for i = 1, #style.colors do
monitor.setPaletteColor(style.colors[i].c, style.colors[i].hex)
end
else
-- reset all colors
for _, val in pairs(colors) do
-- colors api has constants and functions, just get color constants
if type(val) == "number" then
monitor.setPaletteColor(val, term.nativePaletteColor(val))
end
end
end
end
-- link to the monitor peripherals
---@param monitors monitors_struct
function renderer.set_displays(monitors)
engine.monitors = monitors
end
-- check if the renderer is configured to use a given monitor peripheral
---@param periph table peripheral
---@return boolean is_used
function renderer.is_monitor_used(periph)
if engine.monitors ~= nil then
if engine.monitors.primary == periph then
return true
else
for i = 1, #engine.monitors.unit_displays do
if engine.monitors.unit_displays[i] == periph then
return true
end
end
end
end
return false
end
-- reset all displays in use by the renderer
---@param recolor? boolean true to use color palette from style
function renderer.reset(recolor)
-- reset primary monitor
_reset_display(engine.monitors.primary, recolor)
-- reset unit displays
for _, monitor in pairs(engine.monitors.unit_displays) do
_reset_display(monitor, recolor)
end
end
-- initialize the dmesg output window
function renderer.init_dmesg()
local disp_x, disp_y = engine.monitors.primary.getSize()
engine.dmesg_window = window.create(engine.monitors.primary, 1, 1, disp_x, disp_y)
log.direct_dmesg(engine.dmesg_window)
end
-- start the coordinator GUI
function renderer.start_ui()
if not engine.ui_ready then
-- hide dmesg
engine.dmesg_window.setVisible(false)
-- show main view on main monitor
ui.main_layout = main_view(engine.monitors.primary)
-- show unit views on unit displays
for id, monitor in pairs(engine.monitors.unit_displays) do
table.insert(ui.unit_layouts, unit_view(monitor, id))
end
-- report ui as ready
engine.ui_ready = true
end
end
-- close out the UI
function renderer.close_ui()
if engine.ui_ready then
-- report ui as not ready
engine.ui_ready = false
-- hide to stop animation callbacks
ui.main_layout.hide()
for i = 1, #ui.unit_layouts do
ui.unit_layouts[i].hide()
engine.monitors.unit_displays[i].clear()
end
-- clear root UI elements
ui.main_layout = nil
ui.unit_layouts = {}
-- re-draw dmesg
engine.dmesg_window.setVisible(true)
engine.dmesg_window.redraw()
end
end
-- is the UI ready?
---@return boolean ready
function renderer.ui_ready() return engine.ui_ready end
-- handle a touch event
---@param event monitor_touch
function renderer.handle_touch(event)
if event.monitor == engine.monitors.primary_name then
ui.main_layout.handle_touch(event)
else
for id, monitor in pairs(engine.monitors.unit_name_map) do
if event.monitor == monitor then
local layout = ui.unit_layouts[id] ---@type graphics_element
layout.handle_touch(event)
end
end
end
end
return renderer

View File

@@ -4,315 +4,34 @@
require("/initenv").init_env()
local log = require("scada-common.log")
local ppm = require("scada-common.ppm")
local tcallbackdsp = require("scada-common.tcallbackdsp")
local util = require("scada-common.util")
local log = require("scada-common.log")
local ppm = require("scada-common.ppm")
local util = require("scada-common.util")
local core = require("graphics.core")
local config = require("coordinator.config")
local coordinator = require("coordinator.coordinator")
local apisessions = require("coordinator.apisessions")
local config = require("coordinator.config")
local coordinator = require("coordinator.coordinator")
local renderer = require("coordinator.renderer")
local COORDINATOR_VERSION = "alpha-v0.4.12"
local COORDINATOR_VERSION = "alpha-v0.1.2"
local print = util.print
local println = util.println
local print_ts = util.print_ts
local println_ts = util.println_ts
local log_graphics = coordinator.log_graphics
local log_sys = coordinator.log_sys
local log_boot = coordinator.log_boot
local log_comms = coordinator.log_comms
local log_comms_connecting = coordinator.log_comms_connecting
----------------------------------------
-- config validation
----------------------------------------
local cfv = util.new_validator()
cfv.assert_port(config.SCADA_SV_PORT)
cfv.assert_port(config.SCADA_SV_LISTEN)
cfv.assert_port(config.SCADA_API_LISTEN)
cfv.assert_type_int(config.NUM_UNITS)
cfv.assert_type_bool(config.RECOLOR)
cfv.assert_type_str(config.LOG_PATH)
cfv.assert_type_int(config.LOG_MODE)
cfv.assert_type_bool(config.SECURE)
cfv.assert_type_str(config.PASSWORD)
assert(cfv.valid(), "bad config file: missing/invalid fields")
----------------------------------------
-- log init
----------------------------------------
log.init(config.LOG_PATH, config.LOG_MODE)
log.init("/log.txt", log.MODE.APPEND)
log.info("========================================")
log.info("BOOTING coordinator.startup " .. COORDINATOR_VERSION)
log.info("========================================")
println(">> SCADA Coordinator " .. COORDINATOR_VERSION .. " <<")
----------------------------------------
-- system startup
----------------------------------------
-- mount connected devices
ppm.mount_all()
-- setup monitors
local configured, monitors = coordinator.configure_monitors(config.NUM_UNITS)
if not configured then
println("boot> monitor setup failed")
log.fatal("monitor configuration failed")
return
end
log.info("monitors ready, dmesg output incoming...")
-- init renderer
renderer.set_displays(monitors)
renderer.reset(config.RECOLOR)
renderer.init_dmesg()
log_graphics("displays connected and reset")
log_sys("system start on " .. os.date("%c"))
log_boot("starting " .. COORDINATOR_VERSION)
----------------------------------------
-- setup communications
----------------------------------------
-- get the communications modem
local modem = ppm.get_wireless_modem()
-- we need a modem
if modem == nil then
log_comms("wireless modem not found")
println("boot> wireless modem not found")
log.fatal("no wireless modem on startup")
println("please connect a wireless modem")
return
else
log_comms("wireless modem connected")
end
-- create connection watchdog
local conn_watchdog = util.new_watchdog(5)
conn_watchdog.cancel()
log.debug("boot> conn watchdog created")
-- start comms, open all channels
local coord_comms = coordinator.comms(COORDINATOR_VERSION, modem, config.SCADA_SV_PORT, config.SCADA_SV_LISTEN, config.SCADA_API_LISTEN, conn_watchdog)
log.debug("boot> comms init")
log_comms("comms initialized")
-- base loop clock (2Hz, 10 ticks)
local MAIN_CLOCK = 0.5
local loop_clock = util.new_clock(MAIN_CLOCK)
----------------------------------------
-- connect to the supervisor
----------------------------------------
-- attempt to connect to the supervisor or exit
local function init_connect_sv()
local tick_waiting, task_done = log_comms_connecting("attempting to connect to configured supervisor on channel " .. config.SCADA_SV_PORT)
-- attempt to establish a connection with the supervisory computer
if not coord_comms.sv_connect(60, tick_waiting, task_done) then
log_comms("supervisor connection failed")
log.fatal("failed to connect to supervisor")
return false
end
return true
end
if not init_connect_sv() then
println("boot> failed to connect to supervisor")
log_sys("system shutdown")
return
else
log_sys("supervisor connected, proceeding to UI start")
end
----------------------------------------
-- start the UI
----------------------------------------
-- start up the UI
---@return boolean ui_ok started ok
local function init_start_ui()
log_graphics("starting UI...")
-- util.psleep(3)
local draw_start = util.time_ms()
local ui_ok, message = pcall(renderer.start_ui)
if not ui_ok then
renderer.close_ui()
log_graphics(util.c("UI crashed: ", message))
println_ts("UI crashed")
log.fatal(util.c("ui crashed with error ", message))
else
log_graphics("first UI draw took " .. (util.time_ms() - draw_start) .. "ms")
-- start clock
loop_clock.start()
end
return ui_ok
end
local ui_ok = init_start_ui()
----------------------------------------
-- main event loop
----------------------------------------
local no_modem = false
-- start connection watchdog
conn_watchdog.feed()
log.debug("boot> conn watchdog started")
log_sys("system started successfully")
-- event loop
-- ui_ok will never change in this loop, same as while true or exit if UI start failed
while ui_ok do
local event, param1, param2, param3, param4, param5 = util.pull_event()
-- handle event
if event == "peripheral_detach" then
local type, device = ppm.handle_unmount(param1)
if type ~= nil and device ~= nil then
if type == "modem" then
-- we only really care if this is our wireless modem
if device == modem then
no_modem = true
log_sys("comms modem disconnected")
println_ts("wireless modem disconnected!")
log.error("comms modem disconnected!")
-- close out UI
renderer.close_ui()
-- alert user to status
log_sys("awaiting comms modem reconnect...")
else
log_sys("non-comms modem disconnected")
log.warning("non-comms modem disconnected")
end
elseif type == "monitor" then
if renderer.is_monitor_used(device) then
-- "halt and catch fire" style handling
log_sys("lost a configured monitor, system will now exit")
break
else
log_sys("lost unused monitor, ignoring")
end
end
end
elseif event == "peripheral" then
local type, device = ppm.mount(param1)
if type ~= nil and device ~= nil then
if type == "modem" then
if device.isWireless() then
-- reconnected modem
no_modem = false
modem = device
coord_comms.reconnect_modem(modem)
log_sys("comms modem reconnected")
println_ts("wireless modem reconnected.")
-- re-init system
if not init_connect_sv() then break end
ui_ok = init_start_ui()
else
log_sys("wired modem reconnected")
end
elseif type == "monitor" then
-- not supported, system will exit on loss of in-use monitors
end
end
elseif event == "timer" then
if loop_clock.is_clock(param1) then
-- main loop tick
-- free any closed sessions
--apisessions.free_all_closed()
loop_clock.start()
elseif conn_watchdog.is_timer(param1) then
-- supervisor watchdog timeout
local msg = "supervisor server timeout"
log_comms(msg)
println_ts(msg)
log.warning(msg)
-- close connection and UI
coord_comms.close()
renderer.close_ui()
if not no_modem then
-- try to re-connect to the supervisor
if not init_connect_sv() then break end
ui_ok = init_start_ui()
end
else
-- a non-clock/main watchdog timer event
--check API watchdogs
--apisessions.check_all_watchdogs(param1)
-- notify timer callback dispatcher
tcallbackdsp.handle(param1)
end
elseif event == "modem_message" then
-- got a packet
local packet = coord_comms.parse_packet(param1, param2, param3, param4, param5)
coord_comms.handle_packet(packet)
-- check if it was a disconnect
if not coord_comms.is_linked() then
log_comms("supervisor closed connection")
-- close connection and UI
coord_comms.close()
renderer.close_ui()
if not no_modem then
-- try to re-connect to the supervisor
if not init_connect_sv() then break end
ui_ok = init_start_ui()
end
end
elseif event == "monitor_touch" then
-- handle a monitor touch event
renderer.handle_touch(core.events.touch(param1, param2, param3))
end
-- check for termination request
if event == "terminate" or ppm.should_terminate() then
println_ts("terminate requested, closing connections...")
log_comms("terminate requested, closing supervisor connection...")
coord_comms.close()
log_comms("supervisor connection closed")
log_comms("closing api sessions...")
apisessions.close_all()
log_comms("api sessions closed")
break
end
end
renderer.close_ui()
log_sys("system shutdown")
println_ts("exited")
log.info("exited")

View File

@@ -1,49 +0,0 @@
local core = require("graphics.core")
local style = require("coordinator.ui.style")
local DataIndicator = require("graphics.elements.indicators.data")
local StateIndicator = require("graphics.elements.indicators.state")
local Rectangle = require("graphics.elements.rectangle")
local TextBox = require("graphics.elements.textbox")
local VerticalBar = require("graphics.elements.indicators.vbar")
local cpair = core.graphics.cpair
local border = core.graphics.border
-- new boiler view
---@param root graphics_element parent
---@param x integer top left x
---@param y integer top left y
---@param ps psil ps interface
local function new_view(root, x, y, ps)
local boiler = Rectangle{parent=root,border=border(1, colors.gray, true),width=31,height=7,x=x,y=y}
local text_fg_bg = cpair(colors.black, colors.lightGray)
local lu_col = cpair(colors.gray, colors.gray)
local status = StateIndicator{parent=boiler,x=10,y=1,states=style.boiler.states,value=1,min_width=10}
local temp = DataIndicator{parent=boiler,x=5,y=3,lu_colors=lu_col,label="Temp:",unit="K",format="%10.2f",value=0,width=22,fg_bg=text_fg_bg}
local boil_r = DataIndicator{parent=boiler,x=5,y=4,lu_colors=lu_col,label="Boil:",unit="mB/t",format="%10.0f",value=0,commas=true,width=22,fg_bg=text_fg_bg}
ps.subscribe("computed_status", status.update)
ps.subscribe("temperature", temp.update)
ps.subscribe("boil_rate", boil_r.update)
TextBox{parent=boiler,text="H",x=2,y=5,height=1,width=1,fg_bg=text_fg_bg}
TextBox{parent=boiler,text="W",x=3,y=5,height=1,width=1,fg_bg=text_fg_bg}
TextBox{parent=boiler,text="S",x=27,y=5,height=1,width=1,fg_bg=text_fg_bg}
TextBox{parent=boiler,text="C",x=28,y=5,height=1,width=1,fg_bg=text_fg_bg}
local hcool = VerticalBar{parent=boiler,x=2,y=1,fg_bg=cpair(colors.orange,colors.gray),height=4,width=1}
local water = VerticalBar{parent=boiler,x=3,y=1,fg_bg=cpair(colors.blue,colors.gray),height=4,width=1}
local steam = VerticalBar{parent=boiler,x=27,y=1,fg_bg=cpair(colors.white,colors.gray),height=4,width=1}
local ccool = VerticalBar{parent=boiler,x=28,y=1,fg_bg=cpair(colors.lightBlue,colors.gray),height=4,width=1}
ps.subscribe("hcool_fill", hcool.update)
ps.subscribe("water_fill", water.update)
ps.subscribe("steam_fill", steam.update)
ps.subscribe("ccool_fill", ccool.update)
end
return new_view

View File

@@ -1,63 +0,0 @@
local util = require("scada-common.util")
local core = require("graphics.core")
local style = require("coordinator.ui.style")
local HorizontalBar = require("graphics.elements.indicators.hbar")
local DataIndicator = require("graphics.elements.indicators.data")
local StateIndicator = require("graphics.elements.indicators.state")
local Rectangle = require("graphics.elements.rectangle")
local TextBox = require("graphics.elements.textbox")
local TEXT_ALIGN = core.graphics.TEXT_ALIGN
local cpair = core.graphics.cpair
local border = core.graphics.border
-- create new reactor view
---@param root graphics_element parent
---@param x integer top left x
---@param y integer top left y
---@param data reactor_db reactor data
---@param ps psil ps interface
local function new_view(root, x, y, data, ps)
local reactor = Rectangle{parent=root,border=border(1, colors.gray, true),width=30,height=7,x=x,y=y}
local text_fg_bg = cpair(colors.black, colors.lightGray)
local lu_col = cpair(colors.gray, colors.gray)
local status = StateIndicator{parent=reactor,x=8,y=1,states=style.reactor.states,value=1,min_width=14}
local core_temp = DataIndicator{parent=reactor,x=2,y=3,lu_colors=lu_col,label="Core Temp:",unit="K",format="%10.2f",value=0,width=26,fg_bg=text_fg_bg}
local burn_r = DataIndicator{parent=reactor,x=2,y=4,lu_colors=lu_col,label="Burn Rate:",unit="mB/t",format="%10.1f",value=0,width=26,fg_bg=text_fg_bg}
local heating_r = DataIndicator{parent=reactor,x=2,y=5,lu_colors=lu_col,label="Heating:",unit="mB/t",format="%12.0f",value=0,commas=true,width=26,fg_bg=text_fg_bg}
ps.subscribe("computed_status", status.update)
ps.subscribe("temp", core_temp.update)
ps.subscribe("act_burn_rate", burn_r.update)
ps.subscribe("heating_rate", heating_r.update)
local reactor_fills = Rectangle{parent=root,border=border(1, colors.gray, true),width=24,height=7,x=(x + 29),y=y}
TextBox{parent=reactor_fills,text="FUEL",x=2,y=1,height=1,fg_bg=text_fg_bg}
TextBox{parent=reactor_fills,text="COOL",x=2,y=2,height=1,fg_bg=text_fg_bg}
TextBox{parent=reactor_fills,text="HCOOL",x=2,y=4,height=1,fg_bg=text_fg_bg}
TextBox{parent=reactor_fills,text="WASTE",x=2,y=5,height=1,fg_bg=text_fg_bg}
-- local ccool_color = util.trinary(data.mek_status.ccool_type == "sodium", cpair(colors.lightBlue,colors.gray), cpair(colors.blue,colors.gray))
-- local hcool_color = util.trinary(data.mek_status.hcool_type == "superheated_sodium", cpair(colors.orange,colors.gray), cpair(colors.white,colors.gray))
local ccool_color = util.trinary(true, cpair(colors.lightBlue,colors.gray), cpair(colors.blue,colors.gray))
local hcool_color = util.trinary(true, cpair(colors.orange,colors.gray), cpair(colors.white,colors.gray))
local fuel = HorizontalBar{parent=reactor_fills,x=8,y=1,show_percent=true,bar_fg_bg=cpair(colors.black,colors.gray),height=1,width=14}
local ccool = HorizontalBar{parent=reactor_fills,x=8,y=2,show_percent=true,bar_fg_bg=ccool_color,height=1,width=14}
local hcool = HorizontalBar{parent=reactor_fills,x=8,y=4,show_percent=true,bar_fg_bg=hcool_color,height=1,width=14}
local waste = HorizontalBar{parent=reactor_fills,x=8,y=5,show_percent=true,bar_fg_bg=cpair(colors.brown,colors.gray),height=1,width=14}
ps.subscribe("fuel_fill", fuel.update)
ps.subscribe("ccool_fill", ccool.update)
ps.subscribe("hcool_fill", hcool.update)
ps.subscribe("waste_fill", waste.update)
end
return new_view

View File

@@ -1,37 +0,0 @@
local core = require("graphics.core")
local style = require("coordinator.ui.style")
local DataIndicator = require("graphics.elements.indicators.data")
local StateIndicator = require("graphics.elements.indicators.state")
local Rectangle = require("graphics.elements.rectangle")
local VerticalBar = require("graphics.elements.indicators.vbar")
local cpair = core.graphics.cpair
local border = core.graphics.border
-- new turbine view
---@param root graphics_element parent
---@param x integer top left x
---@param y integer top left y
---@param ps psil ps interface
local function new_view(root, x, y, ps)
local turbine = Rectangle{parent=root,border=border(1, colors.gray, true),width=23,height=7,x=x,y=y}
local text_fg_bg = cpair(colors.black, colors.lightGray)
local lu_col = cpair(colors.gray, colors.gray)
local status = StateIndicator{parent=turbine,x=8,y=1,states=style.turbine.states,value=1,min_width=10}
local prod_rate = DataIndicator{parent=turbine,x=5,y=3,lu_colors=lu_col,label="",unit="MFE",format="%10.2f",value=0,width=16,fg_bg=text_fg_bg}
local flow_rate = DataIndicator{parent=turbine,x=5,y=4,lu_colors=lu_col,label="",unit="mB/t",format="%10.0f",value=0,commas=true,width=16,fg_bg=text_fg_bg}
ps.subscribe("computed_status", status.update)
ps.subscribe("prod_rate", prod_rate.update)
ps.subscribe("flow_rate", flow_rate.update)
local steam = VerticalBar{parent=turbine,x=2,y=1,fg_bg=cpair(colors.white,colors.gray),height=5,width=2}
ps.subscribe("steam_fill", steam.update)
end
return new_view

View File

@@ -1,289 +0,0 @@
--
-- Reactor Unit SCADA Coordinator GUI
--
local tcallbackdsp = require("scada-common.tcallbackdsp")
local iocontrol = require("coordinator.iocontrol")
local style = require("coordinator.ui.style")
local core = require("graphics.core")
local Div = require("graphics.elements.div")
local TextBox = require("graphics.elements.textbox")
local ColorMap = require("graphics.elements.colormap")
local CoreMap = require("graphics.elements.indicators.coremap")
local DataIndicator = require("graphics.elements.indicators.data")
local IndicatorLight = require("graphics.elements.indicators.light")
local TriIndicatorLight = require("graphics.elements.indicators.trilight")
local MultiButton = require("graphics.elements.controls.multi_button")
local PushButton = require("graphics.elements.controls.push_button")
local SCRAMButton = require("graphics.elements.controls.scram_button")
local StartButton = require("graphics.elements.controls.start_button")
local SpinboxNumeric = require("graphics.elements.controls.spinbox_numeric")
local TEXT_ALIGN = core.graphics.TEXT_ALIGN
local cpair = core.graphics.cpair
-- create a unit view
---@param parent graphics_element parent
---@param id integer
local function init(parent, id)
local unit = iocontrol.get_db().units[id] ---@type ioctl_entry
local r_ps = unit.reactor_ps
local b_ps = unit.boiler_ps_tbl
local t_ps = unit.turbine_ps_tbl
local main = Div{parent=parent,x=1,y=1}
TextBox{parent=main,text="Reactor Unit #" .. id,alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header}
local scram_fg_bg = cpair(colors.white, colors.gray)
local lu_cpair = cpair(colors.gray, colors.gray)
-- main stats and core map --
---@todo need to be checking actual reactor dimensions somehow
local core_map = CoreMap{parent=main,x=2,y=3,reactor_l=18,reactor_w=18}
r_ps.subscribe("temp", core_map.update)
local stat_fg_bg = cpair(colors.black,colors.white)
TextBox{parent=main,x=21,y=3,text="Core Temp",height=1,fg_bg=style.label}
local core_temp = DataIndicator{parent=main,x=21,label="",format="%9.2f",value=0,unit="K",lu_colors=lu_cpair,width=12,fg_bg=stat_fg_bg}
r_ps.subscribe("temp", core_temp.update)
main.line_break()
TextBox{parent=main,x=21,text="Burn Rate",height=1,width=12,fg_bg=style.label}
local act_burn_r = DataIndicator{parent=main,x=21,label="",format="%6.1f",value=0,unit="mB/t",lu_colors=lu_cpair,width=12,fg_bg=stat_fg_bg}
r_ps.subscribe("act_burn_rate", act_burn_r.update)
main.line_break()
TextBox{parent=main,x=21,text="Commanded Burn Rate",height=2,width=12,fg_bg=style.label}
local burn_r = DataIndicator{parent=main,x=21,label="",format="%6.1f",value=0,unit="mB/t",lu_colors=lu_cpair,width=12,fg_bg=stat_fg_bg}
r_ps.subscribe("burn_rate", burn_r.update)
main.line_break()
TextBox{parent=main,x=21,text="Heating Rate",height=1,width=12,fg_bg=style.label}
local heating_r = DataIndicator{parent=main,x=21,label="",format="%11.0f",value=0,unit="",lu_colors=lu_cpair,width=12,fg_bg=stat_fg_bg}
r_ps.subscribe("heating_rate", heating_r.update)
main.line_break()
TextBox{parent=main,x=21,text="Containment Integrity",height=2,width=12,fg_bg=style.label}
local integ = DataIndicator{parent=main,x=21,label="",format="%9.0f",value=100,unit="%",lu_colors=lu_cpair,width=12,fg_bg=stat_fg_bg}
r_ps.subscribe("damage", function (x) integ.update(100.0 - x) end)
main.line_break()
-- TextBox{parent=main,text="FL",x=21,y=19,height=1,width=2,fg_bg=style.label}
-- TextBox{parent=main,text="WS",x=24,y=19,height=1,width=2,fg_bg=style.label}
-- TextBox{parent=main,text="CL",x=28,y=19,height=1,width=2,fg_bg=style.label}
-- TextBox{parent=main,text="HC",x=31,y=19,height=1,width=2,fg_bg=style.label}
-- local fuel = VerticalBar{parent=main,x=21,y=12,fg_bg=cpair(colors.black,colors.gray),height=6,width=2}
-- local waste = VerticalBar{parent=main,x=24,y=12,fg_bg=cpair(colors.brown,colors.gray),height=6,width=2}
-- local ccool = VerticalBar{parent=main,x=28,y=12,fg_bg=cpair(colors.lightBlue,colors.gray),height=6,width=2}
-- local hcool = VerticalBar{parent=main,x=31,y=12,fg_bg=cpair(colors.orange,colors.gray),height=6,width=2}
-- annunciator --
local annunciator = Div{parent=main,x=34,y=3}
-- annunciator colors per IAEA-TECDOC-812 recommendations
-- connectivity/basic state
local plc_online = IndicatorLight{parent=annunciator,label="PLC Online",colors=cpair(colors.green,colors.red)}
local plc_hbeat = IndicatorLight{parent=annunciator,label="PLC Heartbeat",colors=cpair(colors.white,colors.gray)}
local r_active = IndicatorLight{parent=annunciator,label="Active",colors=cpair(colors.green,colors.gray)}
---@todo auto control as info sent here
local r_auto = IndicatorLight{parent=annunciator,label="Auto Control",colors=cpair(colors.blue,colors.gray)}
r_ps.subscribe("PLCOnline", plc_online.update)
r_ps.subscribe("PLCHeartbeat", plc_hbeat.update)
r_ps.subscribe("status", r_active.update)
annunciator.line_break()
-- annunciator fields
local r_scram = IndicatorLight{parent=annunciator,label="Reactor SCRAM",colors=cpair(colors.red,colors.gray)}
local r_mscrm = IndicatorLight{parent=annunciator,label="Manual Reactor SCRAM",colors=cpair(colors.red,colors.gray)}
local r_rtrip = IndicatorLight{parent=annunciator,label="RCP Trip",colors=cpair(colors.red,colors.gray)}
local r_cflow = IndicatorLight{parent=annunciator,label="RCS Flow Low",colors=cpair(colors.yellow,colors.gray)}
local r_temp = IndicatorLight{parent=annunciator,label="Reactor Temp. High",colors=cpair(colors.red,colors.gray)}
local r_rhdt = IndicatorLight{parent=annunciator,label="Reactor High Delta T",colors=cpair(colors.yellow,colors.gray)}
local r_firl = IndicatorLight{parent=annunciator,label="Fuel Input Rate Low",colors=cpair(colors.yellow,colors.gray)}
local r_wloc = IndicatorLight{parent=annunciator,label="Waste Line Occlusion",colors=cpair(colors.yellow,colors.gray)}
local r_hsrt = IndicatorLight{parent=annunciator,label="High Startup Rate",colors=cpair(colors.yellow,colors.gray)}
r_ps.subscribe("ReactorSCRAM", r_scram.update)
r_ps.subscribe("ManualReactorSCRAM", r_mscrm.update)
r_ps.subscribe("RCPTrip", r_rtrip.update)
r_ps.subscribe("RCSFlowLow", r_cflow.update)
r_ps.subscribe("ReactorTempHigh", r_temp.update)
r_ps.subscribe("ReactorHighDeltaT", r_rhdt.update)
r_ps.subscribe("FuelInputRateLow", r_firl.update)
r_ps.subscribe("WasteLineOcclusion", r_wloc.update)
r_ps.subscribe("HighStartupRate", r_hsrt.update)
annunciator.line_break()
-- RPS
local rps_trp = IndicatorLight{parent=annunciator,label="RPS Trip",colors=cpair(colors.red,colors.gray)}
local rps_dmg = IndicatorLight{parent=annunciator,label="Damage Critical",colors=cpair(colors.yellow,colors.gray)}
local rps_exh = IndicatorLight{parent=annunciator,label="Excess Heated Coolant",colors=cpair(colors.yellow,colors.gray)}
local rps_exw = IndicatorLight{parent=annunciator,label="Excess Waste",colors=cpair(colors.yellow,colors.gray)}
local rps_tmp = IndicatorLight{parent=annunciator,label="High Core Temp",colors=cpair(colors.yellow,colors.gray)}
local rps_nof = IndicatorLight{parent=annunciator,label="No Fuel",colors=cpair(colors.yellow,colors.gray)}
local rps_noc = IndicatorLight{parent=annunciator,label="No Coolant",colors=cpair(colors.yellow,colors.gray)}
local rps_flt = IndicatorLight{parent=annunciator,label="PPM Fault",colors=cpair(colors.yellow,colors.gray)}
local rps_tmo = IndicatorLight{parent=annunciator,label="Timeout",colors=cpair(colors.yellow,colors.gray)}
r_ps.subscribe("rps_tripped", rps_trp.update)
r_ps.subscribe("dmg_crit", rps_dmg.update)
r_ps.subscribe("ex_hcool", rps_exh.update)
r_ps.subscribe("ex_waste", rps_exw.update)
r_ps.subscribe("high_temp", rps_tmp.update)
r_ps.subscribe("no_fuel", rps_nof.update)
r_ps.subscribe("no_cool", rps_noc.update)
r_ps.subscribe("fault", rps_flt.update)
r_ps.subscribe("timeout", rps_tmo.update)
annunciator.line_break()
-- cooling
local c_brm = IndicatorLight{parent=annunciator,label="Boil Rate Mismatch",colors=cpair(colors.yellow,colors.gray)}
local c_cfm = IndicatorLight{parent=annunciator,label="Coolant Feed Mismatch",colors=cpair(colors.yellow,colors.gray)}
local c_sfm = IndicatorLight{parent=annunciator,label="Steam Feed Mismatch",colors=cpair(colors.yellow,colors.gray)}
local c_mwrf = IndicatorLight{parent=annunciator,label="Max Water Return Feed",colors=cpair(colors.yellow,colors.gray)}
local c_tbnt = IndicatorLight{parent=annunciator,label="Turbine Trip",colors=cpair(colors.red,colors.gray)}
r_ps.subscribe("BoilRateMismatch", c_brm.update)
r_ps.subscribe("CoolantFeedMismatch", c_cfm.update)
r_ps.subscribe("SteamFeedMismatch", c_sfm.update)
r_ps.subscribe("MaxWaterReturnFeed", c_mwrf.update)
r_ps.subscribe("TurbineTrip", c_tbnt.update)
annunciator.line_break()
-- machine-specific indicators
if unit.num_boilers > 0 then
TextBox{parent=main,x=32,y=34,text="B1",width=2,height=1,fg_bg=cpair(colors.black, colors.white)}
local b1_hr = IndicatorLight{parent=annunciator,label="Heating Rate Low",colors=cpair(colors.yellow,colors.gray)}
b_ps[1].subscribe("HeatingRateLow", b1_hr.update)
end
if unit.num_boilers > 1 then
TextBox{parent=main,x=32,text="B2",width=2,height=1,fg_bg=cpair(colors.black, colors.white)}
local b2_hr = IndicatorLight{parent=annunciator,label="Heating Rate Low",colors=cpair(colors.yellow,colors.gray)}
b_ps[2].subscribe("HeatingRateLow", b2_hr.update)
end
if unit.num_boilers > 0 then
main.line_break()
annunciator.line_break()
end
TextBox{parent=main,x=32,text="T1",width=2,height=1,fg_bg=cpair(colors.black, colors.white)}
local t1_sdo = TriIndicatorLight{parent=annunciator,label="Steam Dump Open",c1=colors.gray,c2=colors.yellow,c3=colors.red}
t_ps[1].subscribe("SteamDumpOpen", t1_sdo.update)
TextBox{parent=main,x=32,text="T1",width=2,height=1,fg_bg=cpair(colors.black, colors.white)}
local t1_tos = IndicatorLight{parent=annunciator,label="Turbine Over Speed",colors=cpair(colors.red,colors.gray)}
t_ps[1].subscribe("TurbineOverSpeed", t1_tos.update)
TextBox{parent=main,x=32,text="T1",width=2,height=1,fg_bg=cpair(colors.black, colors.white)}
local t1_trp = IndicatorLight{parent=annunciator,label="Turbine Trip",colors=cpair(colors.red,colors.gray)}
t_ps[1].subscribe("TurbineTrip", t1_trp.update)
main.line_break()
annunciator.line_break()
if unit.num_turbines > 1 then
TextBox{parent=main,x=32,text="T2",width=2,height=1,fg_bg=cpair(colors.black, colors.white)}
local t2_sdo = TriIndicatorLight{parent=annunciator,label="Steam Dump Open",c1=colors.gray,c2=colors.yellow,c3=colors.red}
t_ps[2].subscribe("SteamDumpOpen", t2_sdo.update)
TextBox{parent=main,x=32,text="T2",width=2,height=1,fg_bg=cpair(colors.black, colors.white)}
local t2_tos = IndicatorLight{parent=annunciator,label="Turbine Over Speed",colors=cpair(colors.red,colors.gray)}
t_ps[2].subscribe("TurbineOverSpeed", t2_tos.update)
TextBox{parent=main,x=32,text="T2",width=2,height=1,fg_bg=cpair(colors.black, colors.white)}
local t2_trp = IndicatorLight{parent=annunciator,label="Turbine Trip",colors=cpair(colors.red,colors.gray)}
t_ps[2].subscribe("TurbineTrip", t2_trp.update)
main.line_break()
annunciator.line_break()
end
if unit.num_turbines > 2 then
TextBox{parent=main,x=32,text="T3",width=2,height=1,fg_bg=cpair(colors.black, colors.white)}
local t3_sdo = TriIndicatorLight{parent=annunciator,label="Steam Dump Open",c1=colors.gray,c2=colors.yellow,c3=colors.red}
t_ps[3].subscribe("SteamDumpOpen", t3_sdo.update)
TextBox{parent=main,x=32,text="T3",width=2,height=1,fg_bg=cpair(colors.black, colors.white)}
local t3_tos = IndicatorLight{parent=annunciator,label="Turbine Over Speed",colors=cpair(colors.red,colors.gray)}
t_ps[3].subscribe("TurbineOverSpeed", t3_tos.update)
TextBox{parent=main,x=32,text="T3",width=2,height=1,fg_bg=cpair(colors.black, colors.white)}
local t3_trp = IndicatorLight{parent=annunciator,label="Turbine Trip",colors=cpair(colors.red,colors.gray)}
t_ps[3].subscribe("TurbineTrip", t3_trp.update)
annunciator.line_break()
end
---@todo radiation monitor
IndicatorLight{parent=annunciator,label="Radiation Monitor",colors=cpair(colors.green,colors.gray)}
IndicatorLight{parent=annunciator,label="Radiation Alarm",colors=cpair(colors.red,colors.gray)}
DataIndicator{parent=main,x=34,y=51,label="",format="%10.1f",value=0,unit="mSv/h",lu_colors=lu_cpair,width=18,fg_bg=stat_fg_bg}
-- reactor controls --
StartButton{parent=main,x=12,y=44,callback=unit.start,fg_bg=scram_fg_bg}
SCRAMButton{parent=main,x=22,y=44,callback=unit.scram,fg_bg=scram_fg_bg}
local burn_control = Div{parent=main,x=12,y=40,width=19,height=3,fg_bg=cpair(colors.gray,colors.white)}
local burn_rate = SpinboxNumeric{parent=burn_control,x=2,y=1,whole_num_precision=4,fractional_precision=1,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=cpair(colors.black,colors.white)}
TextBox{parent=burn_control,x=9,y=2,text="mB/t"}
local set_burn = function () unit.set_burn(burn_rate.get_value()) end
PushButton{parent=burn_control,x=14,y=2,text="SET",min_width=5,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.white,colors.gray),callback=set_burn}
local opts = {
{
text = "Auto",
fg_bg = cpair(colors.black, colors.lightGray),
active_fg_bg = cpair(colors.white, colors.gray)
},
{
text = "Pu",
fg_bg = cpair(colors.black, colors.lightGray),
active_fg_bg = cpair(colors.black, colors.lime)
},
{
text = "Po",
fg_bg = cpair(colors.black, colors.lightGray),
active_fg_bg = cpair(colors.black, colors.cyan)
},
{
text = "AM",
fg_bg = cpair(colors.black, colors.lightGray),
active_fg_bg = cpair(colors.black, colors.purple)
}
}
---@todo waste selection
local waste_sel_f = function (s) print("waste: " .. s) end
local waste_sel = Div{parent=main,x=2,y=48,width=29,height=2,fg_bg=cpair(colors.black, colors.white)}
MultiButton{parent=waste_sel,x=1,y=1,options=opts,callback=waste_sel_f,min_width=6,fg_bg=cpair(colors.black, colors.white)}
TextBox{parent=waste_sel,text="Waste Processing",alignment=TEXT_ALIGN.CENTER,x=1,y=1,height=1}
---@fixme test code
main.line_break()
ColorMap{parent=main,x=2,y=51}
return main
end
return init

View File

@@ -1,176 +0,0 @@
--
-- Basic Unit Overview
--
local core = require("graphics.core")
local style = require("coordinator.ui.style")
local reactor_view = require("coordinator.ui.components.reactor")
local boiler_view = require("coordinator.ui.components.boiler")
local turbine_view = require("coordinator.ui.components.turbine")
local Div = require("graphics.elements.div")
local PipeNetwork = require("graphics.elements.pipenet")
local TextBox = require("graphics.elements.textbox")
local TEXT_ALIGN = core.graphics.TEXT_ALIGN
local cpair = core.graphics.cpair
local border = core.graphics.border
local pipe = core.graphics.pipe
-- make a new unit overview window
---@param parent graphics_element parent
---@param x integer top left x
---@param y integer top left y
---@param unit ioctl_entry unit database entry
local function make(parent, x, y, unit)
local height = 0
local num_boilers = #unit.boiler_data_tbl
local num_turbines = #unit.turbine_data_tbl
assert(num_boilers >= 0 and num_boilers <= 2, "minimum 0 boilers, maximum 2 boilers")
assert(num_turbines >= 1 and num_turbines <= 3, "minimum 1 turbine, maximum 3 turbines")
if num_boilers == 0 and num_turbines == 1 then
height = 9
elseif num_boilers == 1 and num_turbines <= 2 then
height = 17
else
height = 25
end
-- bounding box div
local root = Div{parent=parent,x=x,y=y,width=80,height=height}
-- unit header message
TextBox{parent=root,text="Unit #" .. unit.unit_id,alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header}
-------------
-- REACTOR --
-------------
reactor_view(root, 1, 3, unit.reactor_data, unit.reactor_ps)
if num_boilers > 0 then
local coolant_pipes = {}
if num_boilers >= 2 then
table.insert(coolant_pipes, pipe(0, 0, 11, 12, colors.lightBlue))
end
table.insert(coolant_pipes, pipe(0, 0, 11, 3, colors.lightBlue))
table.insert(coolant_pipes, pipe(2, 0, 11, 2, colors.orange))
if num_boilers >= 2 then
table.insert(coolant_pipes, pipe(2, 0, 11, 11, colors.orange))
end
PipeNetwork{parent=root,x=4,y=10,pipes=coolant_pipes,bg=colors.lightGray}
end
-------------
-- BOILERS --
-------------
if num_boilers >= 1 then boiler_view(root, 16, 11, unit.boiler_ps_tbl[1]) end
if num_boilers >= 2 then boiler_view(root, 16, 19, unit.boiler_ps_tbl[2]) end
--------------
-- TURBINES --
--------------
local t_idx = 1
local no_boilers = num_boilers == 0
if (num_turbines >= 3) or no_boilers or (num_boilers == 1 and num_turbines >= 2) then
turbine_view(root, 58, 3, unit.turbine_ps_tbl[t_idx])
t_idx = t_idx + 1
end
if (num_turbines >= 1 and not no_boilers) or num_turbines >= 2 then
turbine_view(root, 58, 11, unit.turbine_ps_tbl[t_idx])
t_idx = t_idx + 1
end
if (num_turbines >= 2 and num_boilers >= 2) or num_turbines >= 3 then
turbine_view(root, 58, 19, unit.turbine_ps_tbl[t_idx])
end
local steam_pipes_b = {}
if no_boilers then
table.insert(steam_pipes_b, pipe(0, 1, 3, 1, colors.white)) -- steam to turbine 1
table.insert(steam_pipes_b, pipe(0, 2, 3, 2, colors.blue)) -- water to turbine 1
if num_turbines >= 2 then
table.insert(steam_pipes_b, pipe(1, 2, 3, 9, colors.white)) -- steam to turbine 2
table.insert(steam_pipes_b, pipe(2, 3, 3, 10, colors.blue)) -- water to turbine 2
end
if num_turbines >= 3 then
table.insert(steam_pipes_b, pipe(1, 9, 3, 17, colors.white)) -- steam boiler 1 to turbine 1 junction end
table.insert(steam_pipes_b, pipe(2, 10, 3, 18, colors.blue)) -- water boiler 1 to turbine 1 junction start
end
else
-- boiler side pipes
local steam_pipes_a = {
-- boiler 1 steam/water pipes
pipe(0, 1, 6, 1, colors.white, false, true), -- steam boiler 1 to turbine junction
pipe(0, 2, 6, 2, colors.blue, false, true) -- water boiler 1 to turbine junction
}
if num_boilers >= 2 then
-- boiler 2 steam/water pipes
table.insert(steam_pipes_a, pipe(0, 9, 6, 9, colors.white, false, true)) -- steam boiler 2 to turbine junction
table.insert(steam_pipes_a, pipe(0, 10, 6, 10, colors.blue, false, true)) -- water boiler 2 to turbine junction
end
-- turbine side pipes
if num_turbines >= 3 or (num_boilers == 1 and num_turbines == 2) then
table.insert(steam_pipes_b, pipe(0, 9, 1, 2, colors.white, false, true)) -- steam boiler 1 to turbine 1 junction start
table.insert(steam_pipes_b, pipe(1, 1, 3, 1, colors.white, false, false)) -- steam boiler 1 to turbine 1 junction end
end
table.insert(steam_pipes_b, pipe(0, 9, 3, 9, colors.white, false, true)) -- steam boiler 1 to turbine 2
if num_turbines >= 3 or (num_boilers == 1 and num_turbines == 2) then
table.insert(steam_pipes_b, pipe(0, 10, 2, 3, colors.blue, false, true)) -- water boiler 1 to turbine 1 junction start
table.insert(steam_pipes_b, pipe(2, 2, 3, 2, colors.blue, false, false)) -- water boiler 1 to turbine 1 junction end
end
table.insert(steam_pipes_b, pipe(0, 10, 3, 10, colors.blue, false, true)) -- water boiler 1 to turbine 2
if num_turbines >= 3 or (num_turbines >= 2 and num_boilers >= 2) then
if num_boilers >= 2 then
table.insert(steam_pipes_b, pipe(0, 17, 1, 9, colors.white, false, true)) -- steam boiler 2 to turbine 2 junction
table.insert(steam_pipes_b, pipe(0, 17, 3, 17, colors.white, false, true)) -- steam boiler 2 to turbine 3
table.insert(steam_pipes_b, pipe(0, 18, 2, 10, colors.blue, false, true)) -- water boiler 2 to turbine 3
table.insert(steam_pipes_b, pipe(0, 18, 3, 18, colors.blue, false, true)) -- water boiler 2 to turbine 2 junction
else
table.insert(steam_pipes_b, pipe(1, 17, 1, 9, colors.white, false, true)) -- steam boiler 2 to turbine 2 junction
table.insert(steam_pipes_b, pipe(1, 17, 3, 17, colors.white, false, true)) -- steam boiler 2 to turbine 3
table.insert(steam_pipes_b, pipe(2, 18, 2, 10, colors.blue, false, true)) -- water boiler 2 to turbine 3
table.insert(steam_pipes_b, pipe(2, 18, 3, 18, colors.blue, false, true)) -- water boiler 2 to turbine 2 junction
end
elseif num_turbines == 1 and num_boilers >= 2 then
table.insert(steam_pipes_b, pipe(0, 17, 1, 9, colors.white, false, true)) -- steam boiler 2 to turbine 2 junction
table.insert(steam_pipes_b, pipe(0, 17, 1, 17, colors.white, false, true)) -- steam boiler 2 to turbine 3
table.insert(steam_pipes_b, pipe(0, 18, 2, 10, colors.blue, false, true)) -- water boiler 2 to turbine 3
table.insert(steam_pipes_b, pipe(0, 18, 2, 18, colors.blue, false, true)) -- water boiler 2 to turbine 2 junction
end
PipeNetwork{parent=root,x=47,y=11,pipes=steam_pipes_a,bg=colors.lightGray}
end
PipeNetwork{parent=root,x=54,y=3,pipes=steam_pipes_b,bg=colors.lightGray}
return root
end
return make

View File

@@ -1,33 +0,0 @@
--
-- Reactor Unit SCADA Coordinator GUI
--
local style = require("coordinator.ui.style")
local core = require("graphics.core")
local Div = require("graphics.elements.div")
local TextBox = require("graphics.elements.textbox")
local WaitingAnim = require("graphics.elements.animations.waiting")
local TEXT_ALIGN = core.graphics.TEXT_ALIGN
local cpair = core.graphics.cpair
-- create a unit waiting view
---@param parent graphics_element parent
---@param y integer y offset
local function init(parent, y)
-- bounding box div
local root = Div{parent=parent,x=1,y=y,height=5}
local waiting_x = math.floor(parent.width() / 2) - 2
TextBox{parent=root,text="Waiting for status...",alignment=TEXT_ALIGN.CENTER,y=1,height=1,fg_bg=cpair(colors.black,style.root.bkg)}
WaitingAnim{parent=root,x=waiting_x,y=3,fg_bg=cpair(colors.blue,style.root.bkg)}
return root
end
return init

View File

@@ -1,45 +0,0 @@
local completion = require("cc.completion")
local util = require("scada-common.util")
local print = util.print
local println = util.println
local print_ts = util.print_ts
local println_ts = util.println_ts
local dialog = {}
function dialog.ask_y_n(question, default)
print(question)
if default == true then
print(" (Y/n)? ")
else
print(" (y/N)? ")
end
local response = read(nil, nil)
if response == "" then
return default
elseif response == "Y" or response == "y" then
return true
elseif response == "N" or response == "n" then
return false
else
return nil
end
end
function dialog.ask_options(options, cancel)
print("> ")
local response = read(nil, nil, function(text) return completion.choice(text, options) end)
if response == cancel then return false end
if util.table_contains(options, response) then
return response
else return nil end
end
return dialog

View File

@@ -1,47 +0,0 @@
--
-- Main SCADA Coordinator GUI
--
local iocontrol = require("coordinator.iocontrol")
local style = require("coordinator.ui.style")
local unit_overview = require("coordinator.ui.components.unit_overview")
local core = require("graphics.core")
local DisplayBox = require("graphics.elements.displaybox")
local TextBox = require("graphics.elements.textbox")
local TEXT_ALIGN = core.graphics.TEXT_ALIGN
-- create new main view
---@param monitor table main viewscreen
local function init(monitor)
local main = DisplayBox{window=monitor,fg_bg=style.root}
-- window header message
TextBox{parent=main,text="Nuclear Generation Facility SCADA Coordinator",alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header}
local db = iocontrol.get_db()
local uo_1, uo_2, uo_3, uo_4 ---@type graphics_element
-- unit overviews
if db.facility.num_units >= 1 then uo_1 = unit_overview(main, 2, 3, db.units[1]) end
if db.facility.num_units >= 2 then uo_2 = unit_overview(main, 84, 3, db.units[2]) end
if db.facility.num_units >= 3 then
-- base offset 3, spacing 1, max height of units 1 and 2
local row_2_offset = 3 + 1 + math.max(uo_1.height(), uo_2.height())
uo_3 = unit_overview(main, 2, row_2_offset, db.units[3])
if db.facility.num_units == 4 then uo_4 = unit_overview(main, 84, row_2_offset, db.units[4]) end
end
-- command & control
return main
end
return init

View File

@@ -1,26 +0,0 @@
--
-- Reactor Unit SCADA Coordinator GUI
--
local tcallbackdsp = require("scada-common.tcallbackdsp")
local iocontrol = require("coordinator.iocontrol")
local style = require("coordinator.ui.style")
local unit_detail = require("coordinator.ui.components.unit_detail")
local DisplayBox = require("graphics.elements.displaybox")
-- create a unit view
---@param monitor table
---@param id integer
local function init(monitor, id)
local main = DisplayBox{window=monitor,fg_bg=style.root}
unit_detail(main, id)
return main
end
return init

View File

@@ -1,101 +0,0 @@
local core = require("graphics.core")
local style = {}
local cpair = core.graphics.cpair
-- GLOBAL --
style.root = cpair(colors.black, colors.lightGray)
style.header = cpair(colors.white, colors.gray)
style.label = cpair(colors.gray, colors.lightGray)
style.colors = {
{ c = colors.red, hex = 0xdf4949 },
{ c = colors.orange, hex = 0xffb659 },
{ c = colors.yellow, hex = 0xfffc79 },
{ c = colors.lime, hex = 0x64dd20 },
{ c = colors.green, hex = 0x4aee8a },
{ c = colors.cyan, hex = 0x34bac8 },
{ c = colors.lightBlue, hex = 0x6cc0f2 },
{ c = colors.blue, hex = 0x0096ff },
{ c = colors.purple, hex = 0xb156ee },
{ c = colors.pink, hex = 0xf26ba2 },
{ c = colors.magenta, hex = 0xf9488a },
-- { c = colors.white, hex = 0xf0f0f0 },
{ c = colors.lightGray, hex = 0xcacaca },
{ c = colors.gray, hex = 0x575757 },
-- { c = colors.black, hex = 0x191919 },
-- { c = colors.brown, hex = 0x7f664c }
}
-- MAIN LAYOUT --
style.reactor = {
-- reactor states
states = {
{
color = cpair(colors.black, colors.yellow),
text = "PLC OFF-LINE"
},
{
color = cpair(colors.white, colors.gray),
text = "DISABLED"
},
{
color = cpair(colors.black, colors.green),
text = "ACTIVE"
},
{
color = cpair(colors.black, colors.red),
text = "SCRAMMED"
},
{
color = cpair(colors.black, colors.orange),
text = "PLC FAULT"
}
}
}
style.boiler = {
-- boiler states
states = {
{
color = cpair(colors.black, colors.yellow),
text = "OFF-LINE"
},
{
color = cpair(colors.white, colors.gray),
text = "IDLE"
},
{
color = cpair(colors.black, colors.green),
text = "ACTIVE"
}
}
}
style.turbine = {
-- turbine states
states = {
{
color = cpair(colors.black, colors.yellow),
text = "OFF-LINE"
},
{
color = cpair(colors.white, colors.gray),
text = "IDLE"
},
{
color = cpair(colors.black, colors.green),
text = "ACTIVE"
},
{
color = cpair(colors.black, colors.red),
text = "TRIP"
}
}
}
return style

View File

@@ -1,146 +0,0 @@
--
-- Graphics Core Functions and Objects
--
local core = {}
local events = {}
---@class monitor_touch
---@field monitor string
---@field x integer
---@field y integer
-- create a new touch event definition
---@param monitor string
---@param x integer
---@param y integer
---@return monitor_touch
function events.touch(monitor, x, y)
return {
monitor = monitor,
x = x,
y = y
}
end
core.events = events
local graphics = {}
---@alias TEXT_ALIGN integer
graphics.TEXT_ALIGN = {
LEFT = 1,
CENTER = 2,
RIGHT = 3
}
---@class graphics_border
---@field width integer
---@field color color
---@field even boolean
---@alias element_id string|integer
-- create a new border definition
---@param width integer border width
---@param color color border color
---@param even? boolean whether to pad width extra to account for rectangular pixels, defaults to false
---@return graphics_border
function graphics.border(width, color, even)
return {
width = width,
color = color,
even = even or false -- convert nil to false
}
end
---@class graphics_frame
---@field x integer
---@field y integer
---@field w integer
---@field h integer
-- create a new graphics frame definition
---@param x integer
---@param y integer
---@param w integer
---@param h integer
---@return graphics_frame
function graphics.gframe(x, y, w, h)
return {
x = x,
y = y,
w = w,
h = h
}
end
---@class cpair
---@field color_a color
---@field color_b color
---@field blit_a string
---@field blit_b string
---@field fgd color
---@field bkg color
---@field blit_fgd string
---@field blit_bkg string
-- create a new color pair definition
---@param a color
---@param b color
---@return cpair
function graphics.cpair(a, b)
return {
-- color pairs
color_a = a,
color_b = b,
blit_a = colors.toBlit(a),
blit_b = colors.toBlit(b),
-- aliases
fgd = a,
bkg = b,
blit_fgd = colors.toBlit(a),
blit_bkg = colors.toBlit(b)
}
end
---@class pipe
---@field x1 integer starting x, origin is 0
---@field y1 integer starting y, origin is 0
---@field x2 integer ending x, origin is 0
---@field y2 integer ending y, origin is 0
---@field w integer width
---@field h integer height
---@field color color pipe color
---@field thin boolean true for 1 subpixel, false (default) for 2
---@field align_tr boolean false to align bottom left (default), true to align top right
-- create a new pipe
--
-- note: pipe coordinate origin is (0, 0)
---@param x1 integer starting x, origin is 0
---@param y1 integer starting y, origin is 0
---@param x2 integer ending x, origin is 0
---@param y2 integer ending y, origin is 0
---@param color color pipe color
---@param thin? boolean true for 1 subpixel, false (default) for 2
---@param align_tr? boolean false to align bottom left (default), true to align top right
---@return pipe
function graphics.pipe(x1, y1, x2, y2, color, thin, align_tr)
return {
x1 = x1,
y1 = y1,
x2 = x2,
y2 = y2,
w = math.abs(x2 - x1) + 1,
h = math.abs(y2 - y1) + 1,
color = color,
thin = thin or false,
align_tr = align_tr or false
}
end
core.graphics = graphics
return core

View File

@@ -1,342 +0,0 @@
--
-- Generic Graphics Element
--
local core = require("graphics.core")
local element = {}
---@class graphics_args_generic
---@field window? table
---@field parent? graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer next line if omitted
---@field offset_x? integer 0 if omitted
---@field offset_y? integer 0 if omitted
---@field width? integer parent width if omitted
---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors
-- a base graphics element, should not be created on its own
---@param args graphics_args_generic arguments
function element.new(args)
local self = {
id = -1,
elem_type = debug.getinfo(2).name,
define_completed = false,
p_window = nil, ---@type table
position = { x = 1, y = 1 },
child_offset = { x = 0, y = 0 },
bounds = { x1 = 1, y1 = 1, x2 = 1, y2 = 1},
next_y = 1,
children = {},
mt = {}
}
---@class graphics_template
local protected = {
value = nil, ---@type any
window = nil, ---@type table
fg_bg = core.graphics.cpair(colors.white, colors.black),
frame = core.graphics.gframe(1, 1, 1, 1)
}
-- element as string
function self.mt.__tostring()
return "graphics.element{" .. self.elem_type .. "} @ " .. tostring(self)
end
---@class graphics_element
local public = {}
setmetatable(public, self.mt)
-------------------------
-- PROTECTED FUNCTIONS --
-------------------------
-- prepare the template
---@param offset_x integer x offset
---@param offset_y integer y offset
---@param next_y integer next line if no y was provided
function protected.prepare_template(offset_x, offset_y, next_y)
-- get frame coordinates/size
if args.gframe ~= nil then
protected.frame.x = args.gframe.x
protected.frame.y = args.gframe.y
protected.frame.w = args.gframe.w
protected.frame.h = args.gframe.h
else
local w, h = self.p_window.getSize()
protected.frame.x = args.x or 1
protected.frame.y = args.y or next_y
protected.frame.w = args.width or w
protected.frame.h = args.height or h
end
-- inner offsets
if args.offset_x ~= nil then self.child_offset.x = args.offset_x end
if args.offset_y ~= nil then self.child_offset.y = args.offset_y end
-- adjust window frame if applicable
local f = protected.frame
local x = f.x
local y = f.y
-- apply offsets
if args.parent ~= nil then
-- constrain to parent inner width/height
local w, h = self.p_window.getSize()
f.w = math.min(f.w, w - ((2 * offset_x) + (f.x - 1)))
f.h = math.min(f.h, h - ((2 * offset_y) + (f.y - 1)))
-- offset x/y
f.x = x + offset_x
f.y = y + offset_y
end
-- check frame
assert(f.x >= 1, "graphics.element{" .. self.elem_type .. "}: frame x not >= 1")
assert(f.y >= 1, "graphics.element{" .. self.elem_type .. "}: frame y not >= 1")
assert(f.w >= 1, "graphics.element{" .. self.elem_type .. "}: frame width not >= 1")
assert(f.h >= 1, "graphics.element{" .. self.elem_type .. "}: frame height not >= 1")
-- create window
protected.window = window.create(self.p_window, f.x, f.y, f.w, f.h, true)
-- init colors
if args.fg_bg ~= nil then
protected.fg_bg = args.fg_bg
elseif args.parent ~= nil then
protected.fg_bg = args.parent.get_fg_bg()
end
-- set colors
protected.window.setBackgroundColor(protected.fg_bg.bkg)
protected.window.setTextColor(protected.fg_bg.fgd)
protected.window.clear()
-- record position
self.position.x, self.position.y = protected.window.getPosition()
-- calculate bounds
self.bounds.x1 = self.position.x
self.bounds.x2 = self.position.x + f.w - 1
self.bounds.y1 = self.position.y
self.bounds.y2 = self.position.y + f.h - 1
end
-- handle a touch event
---@param event table monitor_touch event
function protected.handle_touch(event)
end
-- handle data value changes
function protected.on_update(...)
end
-- get value
function protected.get_value()
return protected.value
end
-- set value
---@param value any value to set
function protected.set_value(value)
return nil
end
-- custom recolor command, varies by element if implemented
---@vararg cpair|color color(s)
function protected.recolor(...)
end
-- custom resize command, varies by element if implemented
---@vararg integer sizing
function protected.resize(...)
end
-- start animations
function protected.start_anim()
end
-- stop animations
function protected.stop_anim()
end
-- get public interface
---@return graphics_element element, element_id id
function protected.get() return public, self.id end
-----------
-- SETUP --
-----------
-- get the parent window
self.p_window = args.window
if self.p_window == nil and args.parent ~= nil then
self.p_window = args.parent.window()
end
-- check window
assert(self.p_window, "graphics.element{" .. self.elem_type .. "}: no parent window provided")
-- prepare the template
if args.parent == nil then
protected.prepare_template(0, 0, 1)
else
self.id = args.parent.__add_child(args.id, protected)
end
----------------------
-- PUBLIC FUNCTIONS --
----------------------
-- get the window object
function public.window() return protected.window end
-- CHILD ELEMENTS --
-- add a child element
---@param key string|nil id
---@param child graphics_template
---@return integer|string key
function public.__add_child(key, child)
child.prepare_template(self.child_offset.x, self.child_offset.y, self.next_y)
self.next_y = child.frame.y + child.frame.h
local child_element = child.get()
if key == nil then
table.insert(self.children, child_element)
return #self.children
else
self.children[key] = child_element
return key
end
end
-- get a child element
---@return graphics_element
function public.get_child(key) return self.children[key] end
-- remove child
---@param key string|integer
function public.remove(key) self.children[key] = nil end
-- attempt to get a child element by ID (does not include this element itself)
---@param id element_id
---@return graphics_element|nil element
function public.get_element_by_id(id)
if self.children[id] == nil then
for _, child in pairs(self.children) do
local elem = child.get_element_by_id(id)
if elem ~= nil then return elem end
end
else
return self.children[id]
end
return nil
end
-- AUTO-PLACEMENT --
-- skip a line for automatically placed elements
function public.line_break() self.next_y = self.next_y + 1 end
-- PROPERTIES --
-- get the foreground/background colors
---@return cpair fg_bg
function public.get_fg_bg() return protected.fg_bg end
-- get element width
---@return integer width
function public.width()
return protected.frame.w
end
-- get element height
---@return integer height
function public.height()
return protected.frame.h
end
-- get the element value
---@return any value
function public.get_value()
return protected.get_value()
end
-- set the element value
---@param value any new value
function public.set_value(value)
protected.set_value(value)
end
-- resize attributes of the element value if supported
---@vararg number dimensions (element specific)
function public.resize(...)
protected.resize(...)
end
-- FUNCTION CALLBACKS --
-- handle a monitor touch
---@param event monitor_touch monitor touch event
function public.handle_touch(event)
local in_x = event.x >= self.bounds.x1 and event.x <= self.bounds.x2
local in_y = event.y >= self.bounds.y1 and event.y <= self.bounds.y2
if in_x and in_y then
local event_T = core.events.touch(event.monitor, (event.x - self.position.x) + 1, (event.y - self.position.y) + 1)
-- handle the touch event, transformed into the window frame
protected.handle_touch(event_T)
-- pass on touch event to children
for _, val in pairs(self.children) do val.handle_touch(event_T) end
end
end
-- draw the element given new data
---@vararg any new data
function public.update(...)
protected.on_update(...)
end
-- VISIBILITY --
-- show the element
function public.show()
protected.window.setVisible(true)
protected.start_anim()
for i = 1, #self.children do
self.children[i].show()
end
end
-- hide the element
function public.hide()
protected.stop_anim()
for i = 1, #self.children do
self.children[i].hide()
end
protected.window.setVisible(false)
end
-- re-draw the element
function public.redraw()
protected.window.redraw()
end
return protected
end
return element

View File

@@ -1,108 +0,0 @@
-- Loading/Waiting Animation Graphics Element
local tcd = require("scada-common.tcallbackdsp")
local element = require("graphics.element")
---@class waiting_args
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field fg_bg? cpair foreground/background colors
-- new waiting animation element
---@param args waiting_args
---@return graphics_element element, element_id id
local function waiting(args)
local state = 0
local run_animation = false
args.width = 4
args.height = 3
-- create new graphics element base object
local e = element.new(args)
local blit_fg = e.fg_bg.blit_fgd
local blit_bg = e.fg_bg.blit_bkg
local blit_fg_2x = e.fg_bg.blit_fgd .. e.fg_bg.blit_fgd
local blit_bg_2x = e.fg_bg.blit_bkg .. e.fg_bg.blit_bkg
-- tick the animation
local function animate()
e.window.clear()
if state >= 0 and state < 7 then
-- top
e.window.setCursorPos(1 + math.floor(state / 2), 1)
if state % 2 == 0 then
e.window.blit("\x8f", blit_fg, blit_bg)
else
e.window.blit("\x8a\x85", blit_fg_2x, blit_bg_2x)
end
-- bottom
e.window.setCursorPos(4 - math.ceil(state / 2), 3)
if state % 2 == 0 then
e.window.blit("\x8f", blit_fg, blit_bg)
else
e.window.blit("\x8a\x85", blit_fg_2x, blit_bg_2x)
end
else
local st = state - 7
-- right
if st % 3 == 0 then
e.window.setCursorPos(4, 1 + math.floor(st / 3))
e.window.blit("\x83", blit_bg, blit_fg)
elseif st % 3 == 1 then
e.window.setCursorPos(4, 1 + math.floor(st / 3))
e.window.blit("\x8f", blit_bg, blit_fg)
e.window.setCursorPos(4, 2 + math.floor(st / 3))
e.window.blit("\x83", blit_fg, blit_bg)
else
e.window.setCursorPos(4, 2 + math.floor(st / 3))
e.window.blit("\x8f", blit_fg, blit_bg)
end
-- left
if st % 3 == 0 then
e.window.setCursorPos(1, 3 - math.floor(st / 3))
e.window.blit("\x83", blit_fg, blit_bg)
e.window.setCursorPos(1, 2 - math.floor(st / 3))
e.window.blit("\x8f", blit_bg, blit_fg)
elseif st % 3 == 1 then
e.window.setCursorPos(1, 2 - math.floor(st / 3))
e.window.blit("\x83", blit_bg, blit_fg)
else
e.window.setCursorPos(1, 2 - math.floor(st / 3))
e.window.blit("\x8f", blit_fg, blit_bg)
end
end
state = state + 1
if state >= 12 then state = 0 end
if run_animation then
tcd.dispatch(0.5, animate)
end
end
-- start the animation
function e.start_anim()
run_animation = true
animate()
end
-- stop the animation
function e.stop_anim()
run_animation = false
end
e.start_anim()
return e.get()
end
return waiting

View File

@@ -1,33 +0,0 @@
-- Color Map Graphics Element
local util = require("scada-common.util")
local element = require("graphics.element")
---@class colormap_args
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
-- new color map
---@param args colormap_args
---@return graphics_element element, element_id id
local function colormap(args)
local bkg = "008877FFCCEE114455DD9933BBAA2266"
local spaces = util.spaces(32)
args.width = 32
args.height = 1
-- create new graphics element base object
local e = element.new(args)
-- draw color map
e.window.setCursorPos(1, 1)
e.window.blit(spaces, bkg, bkg)
return e.get()
end
return colormap

View File

@@ -1,121 +0,0 @@
-- Button Graphics Element
local element = require("graphics.element")
local util = require("scada-common.util")
---@class button_option
---@field text string
---@field fg_bg cpair
---@field active_fg_bg cpair
---@field _lpad integer automatically calculated left pad
---@field _start_x integer starting touch x range (inclusive)
---@field _end_x integer ending touch x range (inclusive)
---@class multi_button_args
---@field options table button options
---@field callback function function to call on touch
---@field default? boolean default state, defaults to options[1]
---@field min_width? integer text length + 2 if omitted
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field height? integer parent height if omitted
---@field fg_bg? cpair foreground/background colors
-- new multi button (latch selection, exclusively one button at a time)
---@param args multi_button_args
---@return graphics_element element, element_id id
local function multi_button(args)
assert(type(args.options) == "table", "graphics.elements.controls.multi_button: options is a required field")
assert(type(args.callback) == "function", "graphics.elements.controls.multi_button: callback is a required field")
-- single line
args.height = 3
-- determine widths
local max_width = 1
for i = 1, #args.options do
local opt = args.options[i] ---@type button_option
if string.len(opt.text) > max_width then
max_width = string.len(opt.text)
end
end
local button_width = math.max(max_width, args.min_width or 1)
args.width = (button_width * #args.options) + #args.options + 1
-- create new graphics element base object
local e = element.new(args)
-- button state (convert nil to 1 if missing)
e.value = args.default or 1
-- calculate required button information
local next_x = 2
for i = 1, #args.options do
local opt = args.options[i] ---@type button_option
local w = string.len(opt.text)
opt._lpad = math.floor((e.frame.w - w) / 2)
opt._start_x = next_x
opt._end_x = next_x + button_width - 1
next_x = next_x + (button_width + 1)
end
-- show the button state
local function draw()
for i = 1, #args.options do
local opt = args.options[i] ---@type button_option
e.window.setCursorPos(opt._start_x, 2)
if e.value == i then
-- show as pressed
e.window.setTextColor(opt.active_fg_bg.fgd)
e.window.setBackgroundColor(opt.active_fg_bg.bkg)
else
-- show as unpressed
e.window.setTextColor(opt.fg_bg.fgd)
e.window.setBackgroundColor(opt.fg_bg.bkg)
end
e.window.write(util.pad(opt.text, button_width))
end
end
-- handle touch
---@param event monitor_touch monitor touch event
function e.handle_touch(event)
-- determine what was pressed
if event.y == 2 then
for i = 1, #args.options do
local opt = args.options[i] ---@type button_option
if event.x >= opt._start_x and event.x <= opt._end_x then
e.value = i
draw()
args.callback(e.value)
end
end
end
end
-- set the value
---@param val integer new value
function e.set_value(val)
e.value = val
draw()
args.callback(e.value)
end
-- initial draw
draw()
return e.get()
end
return multi_button

View File

@@ -1,86 +0,0 @@
-- Button Graphics Element
local tcd = require("scada-common.tcallbackdsp")
local core = require("graphics.core")
local element = require("graphics.element")
---@class push_button_args
---@field text string button text
---@field callback function function to call on touch
---@field min_width? integer text length + 2 if omitted
---@field active_fg_bg? cpair foreground/background colors when pressed
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field height? integer parent height if omitted
---@field fg_bg? cpair foreground/background colors
-- new push button
---@param args push_button_args
---@return graphics_element element, element_id id
local function push_button(args)
assert(type(args.text) == "string", "graphics.elements.controls.push_button: text is a required field")
assert(type(args.callback) == "function", "graphics.elements.controls.push_button: callback is a required field")
-- single line
args.height = 1
args.min_width = args.min_width or 0
local text_width = string.len(args.text)
args.width = math.max(text_width + 2, args.min_width)
-- create new graphics element base object
local e = element.new(args)
local h_pad = math.floor((e.frame.w - text_width) / 2) + 1
local v_pad = math.floor(e.frame.h / 2) + 1
-- draw the button
local function draw()
e.window.clear()
-- write the button text
e.window.setCursorPos(h_pad, v_pad)
e.window.write(args.text)
end
-- handle touch
---@param event monitor_touch monitor touch event
---@diagnostic disable-next-line: unused-local
function e.handle_touch(event)
if args.active_fg_bg ~= nil then
-- show as pressed
e.value = true
e.window.setTextColor(args.active_fg_bg.fgd)
e.window.setBackgroundColor(args.active_fg_bg.bkg)
draw()
-- show as unpressed in 0.25 seconds
tcd.dispatch(0.25, function ()
e.value = false
e.window.setTextColor(e.fg_bg.fgd)
e.window.setBackgroundColor(e.fg_bg.bkg)
draw()
end)
end
-- call the touch callback
args.callback()
end
-- set the value
---@param val boolean new value
function e.set_value(val)
if val then e.handle_touch(core.events.touch("", 1, 1)) end
end
-- initial draw
draw()
return e.get()
end
return push_button

View File

@@ -1,76 +0,0 @@
-- SCRAM Button Graphics Element
local tcd = require("scada-common.tcallbackdsp")
local core = require("graphics.core")
local element = require("graphics.element")
---@class scram_button_args
---@field callback function function to call on touch
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field fg_bg? cpair foreground/background colors
-- new scram button
---@param args scram_button_args
---@return graphics_element element, element_id id
local function scram_button(args)
assert(type(args.callback) == "function", "graphics.elements.controls.scram_button: callback is a required field")
-- static dimensions
args.height = 3
args.width = 9
-- create new graphics element base object
local e = element.new(args)
-- write the button text
e.window.setCursorPos(3, 2)
e.window.write("SCRAM")
-- draw border
-- top
e.window.setTextColor(colors.yellow)
e.window.setBackgroundColor(args.fg_bg.bkg)
e.window.setCursorPos(1, 1)
e.window.write("\x99\x89\x89\x89\x89\x89\x89\x89\x99")
-- center left
e.window.setCursorPos(1, 2)
e.window.setTextColor(args.fg_bg.bkg)
e.window.setBackgroundColor(colors.yellow)
e.window.write("\x99")
-- center right
e.window.setTextColor(args.fg_bg.bkg)
e.window.setBackgroundColor(colors.yellow)
e.window.setCursorPos(9, 2)
e.window.write("\x99")
-- bottom
e.window.setTextColor(colors.yellow)
e.window.setBackgroundColor(args.fg_bg.bkg)
e.window.setCursorPos(1, 3)
e.window.write("\x99\x98\x98\x98\x98\x98\x98\x98\x99")
-- handle touch
---@param event monitor_touch monitor touch event
---@diagnostic disable-next-line: unused-local
function e.handle_touch(event)
-- call the touch callback
args.callback()
end
-- set the value
---@param val boolean new value
function e.set_value(val)
if val then e.handle_touch(core.events.touch("", 1, 1)) end
end
return e.get()
end
return scram_button

View File

@@ -1,145 +0,0 @@
-- Spinbox Numeric Graphics Element
local element = require("graphics.element")
local util = require("scada-common.util")
---@class spinbox_args
---@field default? number default value, defaults to 0.0
---@field whole_num_precision integer number of whole number digits
---@field fractional_precision integer number of fractional digits
---@field arrow_fg_bg cpair arrow foreground/background colors
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field fg_bg? cpair foreground/background colors
-- new spinbox control (minimum value is 0)
---@param args spinbox_args
---@return graphics_element element, element_id id
local function spinbox(args)
-- properties
local digits = {}
local wn_prec = args.whole_num_precision
local fr_prec = args.fractional_precision
assert(util.is_int(wn_prec), "graphics.element.controls.spinbox_numeric: whole number precision must be an integer")
assert(util.is_int(fr_prec), "graphics.element.controls.spinbox_numeric: fractional precision must be an integer")
local fmt = "%" .. (wn_prec + fr_prec + 1) .. "." .. fr_prec .. "f"
local fmt_init = "%0" .. (wn_prec + fr_prec + 1) .. "." .. fr_prec .. "f"
local dec_point_x = args.whole_num_precision + 1
assert(type(args.arrow_fg_bg) == "table", "graphics.element.spinbox_numeric: arrow_fg_bg is a required field")
-- determine widths
args.width = wn_prec + fr_prec + util.trinary(fr_prec > 0, 1, 0)
args.height = 3
-- create new graphics element base object
local e = element.new(args)
-- set initial value
e.value = args.default or 0.0
local initial_str = util.sprintf(fmt_init, e.value)
---@diagnostic disable-next-line: discard-returns
initial_str:gsub("%d", function (char) table.insert(digits, char) end)
-- draw the arrows
e.window.setBackgroundColor(args.arrow_fg_bg.bkg)
e.window.setTextColor(args.arrow_fg_bg.fgd)
e.window.setCursorPos(1, 1)
e.window.write(util.strrep("\x1e", wn_prec))
e.window.setCursorPos(1, 3)
e.window.write(util.strrep("\x1f", wn_prec))
if fr_prec > 0 then
e.window.setCursorPos(1 + wn_prec, 1)
e.window.write(" " .. util.strrep("\x1e", fr_prec))
e.window.setCursorPos(1 + wn_prec, 3)
e.window.write(" " .. util.strrep("\x1f", fr_prec))
end
-- zero the value
local function zero()
for i = 1, #digits do digits[i] = 0 end
e.value = 0
end
-- print out the current value
local function show_num()
e.window.setBackgroundColor(e.fg_bg.bkg)
e.window.setTextColor(e.fg_bg.fgd)
e.window.setCursorPos(1, 2)
e.window.write(util.sprintf(fmt, e.value))
end
-- update the value per digits table
local function update_value()
e.value = 0
for i = 1, #digits do
local pow = math.abs(wn_prec - i)
if i <= wn_prec then
e.value = e.value + (digits[i] * (10 ^ pow))
else
e.value = e.value + (digits[i] * (10 ^ -pow))
end
end
end
-- enforce numeric limits
local function enforce_limits()
-- min 0
if e.value < 0 then
zero()
-- max printable
elseif string.len(util.sprintf(fmt, e.value)) > args.width then
-- max out
for i = 1, #digits do digits[i] = 9 end
-- re-update value
update_value()
end
end
-- update value and show
local function parse_and_show()
update_value()
enforce_limits()
show_num()
end
-- init with the default value
show_num()
-- handle touch
---@param event monitor_touch monitor touch event
function e.handle_touch(event)
-- only handle if on an increment or decrement arrow
if event.x ~= dec_point_x then
local idx = util.trinary(event.x > dec_point_x, event.x - 1, event.x)
if event.y == 1 then
-- increment
digits[idx] = digits[idx] + 1
elseif event.y == 3 then
-- decrement
digits[idx] = digits[idx] - 1
end
parse_and_show()
end
end
-- set the value
---@param val number number to show
function e.set_value(val)
e.value = val
parse_and_show()
end
return e.get()
end
return spinbox

View File

@@ -1,76 +0,0 @@
-- SCRAM Button Graphics Element
local tcd = require("scada-common.tcallbackdsp")
local core = require("graphics.core")
local element = require("graphics.element")
---@class start_button_args
---@field callback function function to call on touch
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field fg_bg? cpair foreground/background colors
-- new start button
---@param args start_button_args
---@return graphics_element element, element_id id
local function start_button(args)
assert(type(args.callback) == "function", "graphics.elements.controls.start_button: callback is a required field")
-- static dimensions
args.height = 3
args.width = 9
-- create new graphics element base object
local e = element.new(args)
-- write the button text
e.window.setCursorPos(3, 2)
e.window.write("START")
-- draw border
-- top
e.window.setTextColor(colors.orange)
e.window.setBackgroundColor(args.fg_bg.bkg)
e.window.setCursorPos(1, 1)
e.window.write("\x99\x89\x89\x89\x89\x89\x89\x89\x99")
-- center left
e.window.setCursorPos(1, 2)
e.window.setTextColor(args.fg_bg.bkg)
e.window.setBackgroundColor(colors.orange)
e.window.write("\x99")
-- center right
e.window.setTextColor(args.fg_bg.bkg)
e.window.setBackgroundColor(colors.orange)
e.window.setCursorPos(9, 2)
e.window.write("\x99")
-- bottom
e.window.setTextColor(colors.orange)
e.window.setBackgroundColor(args.fg_bg.bkg)
e.window.setCursorPos(1, 3)
e.window.write("\x99\x98\x98\x98\x98\x98\x98\x98\x99")
-- handle touch
---@param event monitor_touch monitor touch event
---@diagnostic disable-next-line: unused-local
function e.handle_touch(event)
-- call the touch callback
args.callback()
end
-- set the value
---@param val boolean new value
function e.set_value(val)
if val then e.handle_touch(core.events.touch("", 1, 1)) end
end
return e.get()
end
return start_button

View File

@@ -1,88 +0,0 @@
-- Button Graphics Element
local element = require("graphics.element")
---@class switch_button_args
---@field text string button text
---@field callback function function to call on touch
---@field default? boolean default state, defaults to off (false)
---@field min_width? integer text length + 2 if omitted
---@field active_fg_bg cpair foreground/background colors when pressed
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field height? integer parent height if omitted
---@field fg_bg? cpair foreground/background colors
-- new switch button (latch high/low)
---@param args switch_button_args
---@return graphics_element element, element_id id
local function switch_button(args)
assert(type(args.text) == "string", "graphics.elements.controls.switch_button: text is a required field")
assert(type(args.callback) == "function", "graphics.elements.controls.switch_button: callback is a required field")
assert(type(args.active_fg_bg) == "table", "graphics.elements.controls.switch_button: active_fg_bg is a required field")
-- single line
args.height = 1
-- determine widths
local text_width = string.len(args.text)
args.width = math.max(text_width + 2, args.min_width)
-- create new graphics element base object
local e = element.new(args)
-- button state (convert nil to false if missing)
e.value = args.default or false
local h_pad = math.floor((e.frame.w - text_width) / 2)
local v_pad = math.floor(e.frame.h / 2) + 1
-- show the button state
local function draw_state()
if e.value then
-- show as pressed
e.window.setTextColor(args.active_fg_bg.fgd)
e.window.setBackgroundColor(args.active_fg_bg.bkg)
else
-- show as unpressed
e.window.setTextColor(e.fg_bg.fgd)
e.window.setBackgroundColor(e.fg_bg.bkg)
end
-- write the button text
e.window.setCursorPos(h_pad, v_pad)
e.window.write(args.text)
end
-- initial draw
draw_state()
-- handle touch
---@param event monitor_touch monitor touch event
---@diagnostic disable-next-line: unused-local
function e.handle_touch(event)
-- toggle state
e.value = not e.value
draw_state()
-- call the touch callback with state
args.callback(e.value)
end
-- set the value
---@param val boolean new value
function e.set_value(val)
-- set state
e.value = val
draw_state()
-- call the touch callback with state
args.callback(e.value)
end
return e.get()
end
return switch_button

View File

@@ -1,21 +0,0 @@
-- Root Display Box Graphics Element
local element = require("graphics.element")
---@class displaybox_args
---@field window table
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field width? integer parent width if omitted
---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors
-- new root display box
---@param args displaybox_args
local function displaybox(args)
-- create new graphics element base object
return element.new(args).get()
end
return displaybox

View File

@@ -1,23 +0,0 @@
-- Div (Division, like in HTML) Graphics Element
local element = require("graphics.element")
---@class div_args
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field width? integer parent width if omitted
---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors
-- new div element
---@param args div_args
---@return graphics_element element, element_id id
local function div(args)
-- create new graphics element base object
return element.new(args).get()
end
return div

View File

@@ -1,165 +0,0 @@
-- Reactor Core View Graphics Element
local util = require("scada-common.util")
local core = require("graphics.core")
local element = require("graphics.element")
---@class core_map_args
---@field reactor_l integer reactor length
---@field reactor_w integer reactor width
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
-- new core map box
---@param args core_map_args
---@return graphics_element element, element_id id
local function core_map(args)
assert(util.is_int(args.reactor_l), "graphics.elements.indicators.coremap: reactor_l is a required field")
assert(util.is_int(args.reactor_w), "graphics.elements.indicators.coremap: reactor_w is a required field")
-- require max dimensions
args.width = 18
args.height = 18
-- inherit only foreground color
args.fg_bg = core.graphics.cpair(args.parent.get_fg_bg().fgd, colors.gray)
-- create new graphics element base object
local e = element.new(args)
local alternator = true
local core_l = args.reactor_l - 2
local core_w = args.reactor_w - 2
local shift_x = 8 - math.floor(core_l / 2)
local shift_y = 8 - math.floor(core_w / 2)
local start_x = 2 + shift_x
local start_y = 2 + shift_y
local inner_width = core_l
local inner_height = core_w
-- create coordinate grid and frame
local function draw_frame()
e.window.setTextColor(colors.white)
for x = 0, (inner_width - 1) do
e.window.setCursorPos(x + start_x, 1)
e.window.write(util.sprintf("%X", x))
end
for y = 0, (inner_height - 1) do
e.window.setCursorPos(1, y + start_y)
e.window.write(util.sprintf("%X", y))
end
-- even out bottom edge
e.window.setTextColor(e.fg_bg.bkg)
e.window.setBackgroundColor(args.parent.get_fg_bg().bkg)
e.window.setCursorPos(1, e.frame.h)
e.window.write(util.strrep("\x8f", e.frame.w))
e.window.setTextColor(e.fg_bg.fgd)
e.window.setBackgroundColor(e.fg_bg.bkg)
end
-- draw the core
---@param t number temperature in K
local function draw_core(t)
local i = 1
local back_c = "F"
local text_c = "8"
-- determine fuel assembly coloring
if t <= 300 then
-- gray
text_c = "8"
elseif t <= 350 then
-- blue
text_c = "3"
elseif t < 600 then
-- green
text_c = "D"
elseif t < 1000 then
-- yellow
text_c = "4"
-- back_c = "8"
elseif t < 1200 then
-- orange
text_c = "1"
elseif t < 1300 then
-- red
text_c = "E"
else
-- pink
text_c = "2"
end
-- draw pattern
for y = start_y, inner_height + (start_y - 1) do
e.window.setCursorPos(start_x, y)
for _ = 1, inner_width do
if alternator then
i = i + 1
e.window.blit("\x07", text_c, back_c)
else
e.window.blit("\x07", "7", "8")
end
alternator = not alternator
end
if inner_width % 2 == 0 then alternator = not alternator end
end
end
-- on state change
---@param temperature number temperature in Kelvin
function e.on_update(temperature)
e.value = temperature
draw_core(e.value)
end
-- set temperature to display
---@param val number degrees K
function e.set_value(val) e.on_update(val) end
-- resize reactor dimensions
---@param reactor_l integer reactor length (rendered in 2D top-down as width)
---@param reactor_w integer reactor width (rendered in 2D top-down as height)
function e.resize(reactor_l, reactor_w)
-- enforce possible dimensions
if reactor_l > 18 then reactor_l = 18 elseif reactor_l < 3 then reactor_l = 3 end
if reactor_w > 18 then reactor_w = 18 elseif reactor_w < 3 then reactor_w = 3 end
-- update dimensions
core_l = reactor_l - 2
core_w = reactor_w - 2
shift_x = 8 - math.floor(core_l / 2)
shift_y = 8 - math.floor(core_w / 2)
start_x = 2 + shift_x
start_y = 2 + shift_y
inner_width = core_l
inner_height = core_w
e.window.clear()
-- re-draw
draw_frame()
e.on_update(e.value)
end
-- initial (one-time except for resize()) frame draw
draw_frame()
-- initial draw
e.on_update(0)
return e.get()
end
return core_map

View File

@@ -1,105 +0,0 @@
-- Data Indicator Graphics Element
local util = require("scada-common.util")
local element = require("graphics.element")
-- format a number string with commas as the thousands separator
--
-- subtracts from spaces at the start if present for each comma used
---@param num string number string
---@return string
local function comma_format(num)
local formatted = num
local commas = 0
local i = 1
while i > 0 do
formatted, i = formatted:gsub("^(%s-%d+)(%d%d%d)", '%1,%2')
if i > 0 then commas = commas + 1 end
end
local _, num_spaces = formatted:gsub(" %s-", "")
local remove = math.min(num_spaces, commas)
formatted = string.sub(formatted, remove + 1)
return formatted
end
---@class data_indicator_args
---@field label string indicator label
---@field unit? string indicator unit
---@field format string data format (lua string format)
---@field commas boolean whether to use commas if a number is given (default to false)
---@field lu_colors? cpair label foreground color (a), unit foreground color (b)
---@field value any default value
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field width integer length
---@field fg_bg? cpair foreground/background colors
-- new data indicator
---@param args data_indicator_args
---@return graphics_element element, element_id id
local function data(args)
assert(type(args.label) == "string", "graphics.elements.indicators.data: label is a required field")
assert(type(args.format) == "string", "graphics.elements.indicators.data: format is a required field")
assert(args.value ~= nil, "graphics.elements.indicators.data: value is a required field")
assert(util.is_int(args.width), "graphics.elements.indicators.data: width is a required field")
-- single line
args.height = 1
-- create new graphics element base object
local e = element.new(args)
-- label color
if args.lu_colors ~= nil then
e.window.setTextColor(args.lu_colors.color_a)
end
-- write label
e.window.setCursorPos(1, 1)
e.window.write(args.label)
local data_start = string.len(args.label) + 2
-- on state change
---@param value any new value
function e.on_update(value)
e.value = value
local data_str = util.sprintf(args.format, value)
-- write data
e.window.setCursorPos(data_start, 1)
e.window.setTextColor(e.fg_bg.fgd)
if args.commas then
e.window.write(comma_format(data_str))
else
e.window.write(data_str)
end
-- write label
if args.unit ~= nil then
if args.lu_colors ~= nil then
e.window.setTextColor(args.lu_colors.color_b)
end
e.window.write(" " .. args.unit)
end
end
-- set the value
---@param val any new value
function e.set_value(val) e.on_update(val) end
-- initial value draw
e.on_update(args.value)
return e.get()
end
return data

View File

@@ -1,123 +0,0 @@
-- Horizontal Bar Graphics Element
local util = require("scada-common.util")
local element = require("graphics.element")
---@class hbar_args
---@field show_percent? boolean whether or not to show the percent
---@field bar_fg_bg? cpair bar foreground/background colors if showing percent
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field width? integer parent width if omitted
---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors
-- new horizontal bar
---@param args hbar_args
---@return graphics_element element, element_id id
---@return graphics_element element, element_id id
local function hbar(args)
-- properties/state
local last_num_bars = -1
-- create new graphics element base object
local e = element.new(args)
-- bar width is width - 5 characters for " 100%" if showing percent
local bar_width = util.trinary(args.show_percent, e.frame.w - 5, e.frame.w)
assert(bar_width > 0, "graphics.elements.indicators.hbar: too small for bar")
-- determine bar colors
local bar_bkg = e.fg_bg.blit_bkg
local bar_fgd = e.fg_bg.blit_fgd
if args.bar_fg_bg ~= nil then
bar_bkg = args.bar_fg_bg.blit_bkg
bar_fgd = args.bar_fg_bg.blit_fgd
end
-- handle data changes
---@param fraction number 0.0 to 1.0
function e.on_update(fraction)
e.value = fraction
-- enforce minimum and maximum
if fraction < 0 then
fraction = 0.0
elseif fraction > 1 then
fraction = 1.0
end
-- compute number of bars
local num_bars = util.round(fraction * (bar_width * 2))
-- redraw bar if changed
if num_bars ~= last_num_bars then
last_num_bars = num_bars
local fgd = ""
local bkg = ""
local spaces = ""
-- fill percentage
for _ = 1, num_bars / 2 do
spaces = spaces .. " "
fgd = fgd .. bar_fgd
bkg = bkg .. bar_bkg
end
-- add fractional bar if needed
if num_bars % 2 == 1 then
spaces = spaces .. "\x95"
fgd = fgd .. bar_bkg
bkg = bkg .. bar_fgd
end
-- pad background
for _ = 1, ((bar_width * 2) - num_bars) / 2 do
spaces = spaces .. " "
fgd = fgd .. bar_bkg
bkg = bkg .. bar_bkg
end
-- draw bar
for y = 1, e.frame.h do
e.window.setCursorPos(1, y)
-- intentionally swapped fgd/bkg since we use spaces as fill, but they are the opposite
e.window.blit(spaces, bkg, fgd)
end
end
-- update percentage
if args.show_percent then
e.window.setCursorPos(bar_width + 2, math.max(1, math.ceil(e.frame.h / 2)))
e.window.write(util.sprintf("%3.0f%%", fraction * 100))
end
end
-- change bar color
---@param bar_fg_bg cpair new bar colors
function e.recolor(bar_fg_bg)
bar_bkg = bar_fg_bg.blit_bkg
bar_fgd = bar_fg_bg.blit_fgd
-- re-draw
last_num_bars = 0
e.on_update(e.value)
end
-- set the percentage value
---@param val number 0.0 to 1.0
function e.set_value(val) e.on_update(val) end
-- initialize to 0
e.on_update(0)
return e.get()
end
return hbar

View File

@@ -1,73 +0,0 @@
-- Icon Indicator Graphics Element
local util = require("scada-common.util")
local element = require("graphics.element")
---@class icon_sym_color
---@field color cpair
---@field symbol string
---@class icon_indicator_args
---@field label string indicator label
---@field states table state color and symbol table
---@field value? integer default state, defaults to 1
---@field min_label_width? integer label length if omitted
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field fg_bg? cpair foreground/background colors
-- new icon indicator
---@param args icon_indicator_args
---@return graphics_element element, element_id id
local function icon(args)
assert(type(args.label) == "string", "graphics.elements.indicators.icon: label is a required field")
assert(type(args.states) == "table", "graphics.elements.indicators.icon: states is a required field")
-- single line
args.height = 1
-- determine width
args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 4
-- create new graphics element base object
local e = element.new(args)
-- state blit strings
local state_blit_cmds = {}
for i = 1, #args.states do
local sym_color = args.states[i] ---@type icon_sym_color
table.insert(state_blit_cmds, {
text = " " .. sym_color.symbol .. " ",
fgd = util.strrep(sym_color.color.blit_fgd, 3),
bkg = util.strrep(sym_color.color.blit_bkg, 3)
})
end
-- write label and initial indicator light
e.window.setCursorPos(5, 1)
e.window.write(args.label)
-- on state change
---@param new_state integer indicator state
function e.on_update(new_state)
local blit_cmd = state_blit_cmds[new_state]
e.value = new_state
e.window.setCursorPos(1, 1)
e.window.blit(blit_cmd.text, blit_cmd.fgd, blit_cmd.bkg)
end
-- set indicator state
---@param val integer indicator state
function e.set_value(val) e.on_update(val) end
-- initial icon draw
e.on_update(args.value or 1)
return e.get()
end
return icon

View File

@@ -1,54 +0,0 @@
-- Indicator Light Graphics Element
local element = require("graphics.element")
---@class indicator_light_args
---@field label string indicator label
---@field colors cpair on/off colors (a/b respectively)
---@field min_label_width? integer label length if omitted
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field fg_bg? cpair foreground/background colors
-- new indicator light
---@param args indicator_light_args
---@return graphics_element element, element_id id
local function indicator_light(args)
assert(type(args.label) == "string", "graphics.elements.indicators.light: label is a required field")
assert(type(args.colors) == "table", "graphics.elements.indicators.light: colors is a required field")
-- single line
args.height = 1
-- determine width
args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2
-- create new graphics element base object
local e = element.new(args)
-- on state change
---@param new_state boolean indicator state
function e.on_update(new_state)
e.value = new_state
e.window.setCursorPos(1, 1)
if new_state then
e.window.blit(" \x95", "0" .. args.colors.blit_a, args.colors.blit_a .. e.fg_bg.blit_bkg)
else
e.window.blit(" \x95", "0" .. args.colors.blit_b, args.colors.blit_b .. e.fg_bg.blit_bkg)
end
end
-- set indicator state
---@param val boolean indicator state
function e.set_value(val) e.on_update(val) end
-- write label and initial indicator light
e.on_update(false)
e.window.write(args.label)
return e.get()
end
return indicator_light

View File

@@ -1,79 +0,0 @@
-- State (Text) Indicator Graphics Element
local util = require("scada-common.util")
local element = require("graphics.element")
---@class state_text_color
---@field color cpair
---@field text string
---@class state_indicator_args
---@field states table state color and text table
---@field value? integer default state, defaults to 1
---@field min_width? integer max state text length if omitted
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field height? integer 1 if omitted, must be an odd number
---@field fg_bg? cpair foreground/background colors
-- new state indicator
---@param args state_indicator_args
---@return graphics_element element, element_id id
local function state_indicator(args)
assert(type(args.states) == "table", "graphics.elements.indicators.state: states is a required field")
-- determine height
if util.is_int(args.height) then
assert(args.height % 2 == 1, "graphics.elements.indicators.state: height should be an odd number")
else
args.height = 1
end
-- initial guess at width
args.width = args.min_width or 1
-- state blit strings
local state_blit_cmds = {}
for i = 1, #args.states do
local state_def = args.states[i] ---@type state_text_color
-- re-determine width
if string.len(state_def.text) > args.width then
args.width = string.len(state_def.text)
end
local text = util.pad(state_def.text, args.width)
table.insert(state_blit_cmds, {
text = text,
fgd = util.strrep(state_def.color.blit_fgd, string.len(text)),
bkg = util.strrep(state_def.color.blit_bkg, string.len(text))
})
end
-- create new graphics element base object
local e = element.new(args)
-- on state change
---@param new_state integer indicator state
function e.on_update(new_state)
local blit_cmd = state_blit_cmds[new_state]
e.value = new_state
e.window.setCursorPos(1, 1)
e.window.blit(blit_cmd.text, blit_cmd.fgd, blit_cmd.bkg)
end
-- set indicator state
---@param val integer indicator state
function e.set_value(val) e.on_update(val) end
-- initial draw
e.on_update(args.value or 1)
return e.get()
end
return state_indicator

View File

@@ -1,65 +0,0 @@
-- Tri-State Indicator Light Graphics Element
local element = require("graphics.element")
---@class tristate_indicator_light_args
---@field label string indicator label
---@field c1 color color for state 1
---@field c2 color color for state 2
---@field c3 color color for state 3
---@field min_label_width? integer label length if omitted
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field fg_bg? cpair foreground/background colors
-- new indicator light
---@param args tristate_indicator_light_args
---@return graphics_element element, element_id id
local function tristate_indicator_light(args)
assert(type(args.label) == "string", "graphics.elements.indicators.trilight: label is a required field")
assert(type(args.c1) == "number", "graphics.elements.indicators.trilight: c1 is a required field")
assert(type(args.c2) == "number", "graphics.elements.indicators.trilight: c2 is a required field")
assert(type(args.c3) == "number", "graphics.elements.indicators.trilight: c3 is a required field")
-- single line
args.height = 1
-- determine width
args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2
-- blit translations
local c1 = colors.toBlit(args.c1)
local c2 = colors.toBlit(args.c2)
local c3 = colors.toBlit(args.c3)
-- create new graphics element base object
local e = element.new(args)
-- on state change
---@param new_state integer indicator state
function e.on_update(new_state)
e.value = new_state
e.window.setCursorPos(1, 1)
if new_state == 2 then
e.window.blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg)
elseif new_state == 3 then
e.window.blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
else
e.window.blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
end
end
-- set indicator state
---@param val integer indicator state
function e.set_value(val) e.on_update(val) end
-- write label and initial indicator light
e.on_update(0)
e.window.write(args.label)
return e.get()
end
return tristate_indicator_light

View File

@@ -1,102 +0,0 @@
-- Vertical Bar Graphics Element
local util = require("scada-common.util")
local element = require("graphics.element")
---@class vbar_args
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field width? integer parent width if omitted
---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors
-- new vertical bar
---@param args vbar_args
---@return graphics_element element, element_id id
local function vbar(args)
-- properties/state
local last_num_bars = -1
-- create new graphics element base object
local e = element.new(args)
-- blit strings
local fgd = util.strrep(e.fg_bg.blit_fgd, e.frame.w)
local bkg = util.strrep(e.fg_bg.blit_bkg, e.frame.w)
local spaces = util.spaces(e.frame.w)
local one_third = util.strrep("\x8f", e.frame.w)
local two_thirds = util.strrep("\x83", e.frame.w)
-- handle data changes
---@param fraction number 0.0 to 1.0
function e.on_update(fraction)
e.value = fraction
-- enforce minimum and maximum
if fraction < 0 then
fraction = 0.0
elseif fraction > 1 then
fraction = 1.0
end
-- compute number of bars
local num_bars = util.round(fraction * (e.frame.h * 3))
-- redraw only if number of bars has changed
if num_bars ~= last_num_bars then
last_num_bars = num_bars
-- start bottom up
local y = e.frame.h
-- start at base of vertical bar
e.window.setCursorPos(1, y)
-- fill percentage
for _ = 1, num_bars / 3 do
e.window.blit(spaces, bkg, fgd)
y = y - 1
e.window.setCursorPos(1, y)
end
-- add fractional bar if needed
if num_bars % 3 == 1 then
e.window.blit(one_third, bkg, fgd)
y = y - 1
elseif num_bars % 3 == 2 then
e.window.blit(two_thirds, bkg, fgd)
y = y - 1
end
-- fill the rest blank
while y > 0 do
e.window.setCursorPos(1, y)
e.window.blit(spaces, fgd, bkg)
y = y - 1
end
end
end
-- change bar color
---@param fg_bg cpair new bar colors
function e.recolor(fg_bg)
fgd = util.strrep(fg_bg.blit_fgd, e.frame.w)
bkg = util.strrep(fg_bg.blit_bkg, e.frame.w)
-- re-draw
last_num_bars = 0
e.on_update(e.value)
end
-- set the percentage value
---@param val number 0.0 to 1.0
function e.set_value(val) e.on_update(val) end
return e.get()
end
return vbar

View File

@@ -1,147 +0,0 @@
-- Pipe Graphics Element
local util = require("scada-common.util")
local core = require("graphics.core")
local element = require("graphics.element")
---@class pipenet_args
---@field pipes table pipe list
---@field bg? color background color
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
-- new pipe network
---@param args pipenet_args
---@return graphics_element element, element_id id
local function pipenet(args)
assert(type(args.pipes) == "table", "graphics.elements.indicators.pipenet: pipes is a required field")
args.width = 0
args.height = 0
-- determine width/height
for i = 1, #args.pipes do
local pipe = args.pipes[i] ---@type pipe
local true_w = pipe.w + math.min(pipe.x1, pipe.x2)
local true_h = pipe.h + math.min(pipe.y1, pipe.y2)
if true_w > args.width then args.width = true_w end
if true_h > args.height then args.height = true_h end
end
args.x = args.x or 1
args.y = args.y or 1
if args.bg ~= nil then
args.fg_bg = core.graphics.cpair(args.bg, args.bg)
end
-- create new graphics element base object
local e = element.new(args)
-- draw all pipes
for p = 1, #args.pipes do
local pipe = args.pipes[p] ---@type pipe
local x = 1 + pipe.x1
local y = 1 + pipe.y1
local x_step = util.trinary(pipe.x1 >= pipe.x2, -1, 1)
local y_step = util.trinary(pipe.y1 >= pipe.y2, -1, 1)
e.window.setCursorPos(x, y)
local c = core.graphics.cpair(pipe.color, e.fg_bg.bkg)
if pipe.align_tr then
-- cross width then height
for i = 1, pipe.w do
if pipe.thin then
if i == pipe.w then
-- corner
if y_step > 0 then
e.window.blit("\x93", c.blit_bkg, c.blit_fgd)
else
e.window.blit("\x8e", c.blit_fgd, c.blit_bkg)
end
else
e.window.blit("\x8c", c.blit_fgd, c.blit_bkg)
end
else
if i == pipe.w and y_step > 0 then
-- corner
e.window.blit(" ", c.blit_bkg, c.blit_fgd)
else
e.window.blit("\x8f", c.blit_fgd, c.blit_bkg)
end
end
x = x + x_step
e.window.setCursorPos(x, y)
end
-- back up one
x = x - x_step
for _ = 1, pipe.h - 1 do
y = y + y_step
e.window.setCursorPos(x, y)
if pipe.thin then
e.window.blit("\x95", c.blit_bkg, c.blit_fgd)
else
e.window.blit(" ", c.blit_bkg, c.blit_fgd)
end
end
else
-- cross height then width
for i = 1, pipe.h do
if pipe.thin then
if i == pipe.h then
-- corner
if y_step < 0 then
e.window.blit("\x97", c.blit_bkg, c.blit_fgd)
else
e.window.blit("\x8d", c.blit_fgd, c.blit_bkg)
end
else
e.window.blit("\x95", c.blit_fgd, c.blit_bkg)
end
else
if i == pipe.h and y_step < 0 then
-- corner
e.window.blit("\x83", c.blit_bkg, c.blit_fgd)
else
e.window.blit(" ", c.blit_bkg, c.blit_fgd)
end
end
y = y + y_step
e.window.setCursorPos(x, y)
end
-- back up one
y = y - y_step
for _ = 1, pipe.w - 1 do
x = x + x_step
e.window.setCursorPos(x, y)
if pipe.thin then
e.window.blit("\x8c", c.blit_fgd, c.blit_bkg)
else
e.window.blit("\x83", c.blit_bkg, c.blit_fgd)
end
end
end
end
return e.get()
end
return pipenet

View File

@@ -1,116 +0,0 @@
-- Rectangle Graphics Element
local util = require("scada-common.util")
local element = require("graphics.element")
---@class rectangle_args
---@field border? graphics_border
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field width? integer parent width if omitted
---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors
-- new rectangle
---@param args rectangle_args
---@return graphics_element element, element_id id
local function rectangle(args)
-- offset children
if args.border ~= nil then
args.offset_x = args.border.width
args.offset_y = args.border.width
-- slightly different y offset if the border is set to even
if args.border.even then
local width_x2 = (2 * args.border.width)
args.offset_y = math.floor(width_x2 / 3) + util.trinary(width_x2 % 3 > 0, 1, 0)
end
end
-- create new graphics element base object
local e = element.new(args)
-- draw bordered box if requested
-- element constructor will have drawn basic colored rectangle regardless
if args.border ~= nil then
e.window.setCursorPos(1, 1)
local border_width = args.offset_x
local border_height = args.offset_y
local border_blit = colors.toBlit(args.border.color)
local width_x2 = border_width * 2
local inner_width = e.frame.w - width_x2
-- check dimensions
assert(width_x2 <= e.frame.w, "graphics.elements.rectangle: border too thick for width")
assert(width_x2 <= e.frame.h, "graphics.elements.rectangle: border too thick for height")
-- form the basic line strings and top/bottom blit strings
local spaces = util.spaces(e.frame.w)
local blit_fg = util.strrep(e.fg_bg.blit_fgd, e.frame.w)
local blit_bg_sides = ""
local blit_bg_top_bot = util.strrep(border_blit, e.frame.w)
-- partial bars
local p_a = util.spaces(border_width) .. util.strrep("\x8f", inner_width) .. util.spaces(border_width)
local p_b = util.spaces(border_width) .. util.strrep("\x83", inner_width) .. util.spaces(border_width)
local p_inv_fg = util.strrep(border_blit, border_width) .. util.strrep(e.fg_bg.blit_bkg, inner_width) ..
util.strrep(border_blit, border_width)
local p_inv_bg = util.strrep(e.fg_bg.blit_bkg, border_width) .. util.strrep(border_blit, inner_width) ..
util.strrep(e.fg_bg.blit_bkg, border_width)
-- form the body blit strings (sides are border, inside is normal)
for x = 1, e.frame.w do
-- edges get border color, center gets normal
if x <= border_width or x > (e.frame.w - border_width) then
blit_bg_sides = blit_bg_sides .. border_blit
else
blit_bg_sides = blit_bg_sides .. e.fg_bg.blit_bkg
end
end
-- draw rectangle with borders
for y = 1, e.frame.h do
e.window.setCursorPos(1, y)
if y <= border_height then
-- partial pixel fill
if args.border.even and y == border_height then
if width_x2 % 3 == 1 then
e.window.blit(p_b, p_inv_bg, p_inv_fg)
elseif width_x2 % 3 == 2 then
e.window.blit(p_a, p_inv_bg, p_inv_fg)
else
-- skip line
e.window.blit(spaces, blit_fg, blit_bg_sides)
end
else
e.window.blit(spaces, blit_fg, blit_bg_top_bot)
end
elseif y > (e.frame.h - border_width) then
-- partial pixel fill
if args.border.even and y == ((e.frame.h - border_width) + 1) then
if width_x2 % 3 == 1 then
e.window.blit(p_a, p_inv_fg, blit_bg_top_bot)
elseif width_x2 % 3 == 2 then
e.window.blit(p_b, p_inv_fg, blit_bg_top_bot)
else
-- skip line
e.window.blit(spaces, blit_fg, blit_bg_sides)
end
else
e.window.blit(spaces, blit_fg, blit_bg_top_bot)
end
else
e.window.blit(spaces, blit_fg, blit_bg_sides)
end
end
end
return e.get()
end
return rectangle

View File

@@ -1,70 +0,0 @@
-- Text Box Graphics Element
local util = require("scada-common.util")
local core = require("graphics.core")
local element = require("graphics.element")
local TEXT_ALIGN = core.graphics.TEXT_ALIGN
---@class textbox_args
---@field text string text to show
---@field alignment? TEXT_ALIGN text alignment, left by default
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field width? integer parent width if omitted
---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors
-- new text box
---@param args textbox_args
---@return graphics_element element, element_id id
local function textbox(args)
assert(type(args.text) == "string", "graphics.elements.textbox: text is a required field")
-- create new graphics element base object
local e = element.new(args)
local alignment = args.alignment or TEXT_ALIGN.LEFT
-- draw textbox
local function display_text(text)
e.value = text
local lines = util.strwrap(text, e.frame.w)
for i = 1, #lines do
if i > e.frame.h then break end
local len = string.len(lines[i])
-- use cursor position to align this line
if alignment == TEXT_ALIGN.CENTER then
e.window.setCursorPos(math.floor((e.frame.w - len) / 2) + 1, i)
elseif alignment == TEXT_ALIGN.RIGHT then
e.window.setCursorPos((e.frame.w - len) + 1, i)
else
e.window.setCursorPos(1, i)
end
e.window.write(lines[i])
end
end
display_text(args.text)
-- set the string value and re-draw the text
---@param val string value
function e.set_value(val)
e.window.clear()
display_text(val)
end
return e.get()
end
return textbox

View File

@@ -1,87 +0,0 @@
-- "Basketweave" Tiling Graphics Element
local util = require("scada-common.util")
local element = require("graphics.element")
---@class tiling_args
---@field fill_c cpair colors to fill with
---@field even? boolean whether to account for rectangular pixels
---@field border_c? color optional frame color
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field width? integer parent width if omitted
---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors
-- new tiling box
---@param args tiling_args
---@return graphics_element element, element_id id
local function tiling(args)
assert(type(args.fill_c) == "table", "graphics.elements.tiling: fill_c is a required field")
-- create new graphics element base object
local e = element.new(args)
-- draw tiling box
local fill_a = args.fill_c.blit_a
local fill_b = args.fill_c.blit_b
local even = args.even == true
local start_x = 1
local start_y = 1
local inner_width = math.floor(e.frame.w / util.trinary(even, 2, 1))
local inner_height = e.frame.h
local alternator = true
-- border
if args.border_c ~= nil then
e.window.setBackgroundColor(args.border_c)
e.window.clear()
start_x = 1 + util.trinary(even, 2, 1)
start_y = 2
inner_width = math.floor((e.frame.w - 2 * util.trinary(even, 2, 1)) / util.trinary(even, 2, 1))
inner_height = e.frame.h - 2
end
-- check dimensions
assert(inner_width > 0, "graphics.elements.tiling: inner_width <= 0")
assert(inner_height > 0, "graphics.elements.tiling: inner_height <= 0")
assert(start_x <= inner_width, "graphics.elements.tiling: start_x > inner_width")
assert(start_y <= inner_height, "graphics.elements.tiling: start_y > inner_height")
-- create pattern
for y = start_y, inner_height + (start_y - 1) do
e.window.setCursorPos(start_x, y)
for x = 1, inner_width do
if alternator then
if even then
e.window.blit(" ", "00", fill_a .. fill_a)
else
e.window.blit(" ", "0", fill_a)
end
else
if even then
e.window.blit(" ", "00", fill_b .. fill_b)
else
e.window.blit(" ", "0", fill_b)
end
end
alternator = not alternator
end
if inner_width % 2 == 0 then alternator = not alternator end
end
return e.get()
end
return tiling

View File

@@ -2,17 +2,17 @@
-- Initialize the Post-Boot Module Environment
--
return {
-- initialize booted environment
init_env = function ()
local _require = require("cc.require")
local _env = setmetatable({}, { __index = _ENV })
-- initialize booted environment
local init_env = function ()
local _require = require("cc.require")
local _env = setmetatable({}, { __index = _ENV })
-- overwrite require/package globals
require, package = _require.make(_env, "/")
-- overwrite require/package globals
require, package = _require.make(_env, "/")
-- reset terminal
term.clear()
term.setCursorPos(1, 1)
end
}
-- reset terminal
term.clear()
term.setCursorPos(1, 1)
end
return { init_env = init_env }

View File

@@ -1,22 +0,0 @@
The MIT License (MIT)
Copyright (c) 2015 James L.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,415 +0,0 @@
local Array = require("lockbox.util.array");
local Bit = require("lockbox.util.bit");
local XOR = Bit.bxor;
local SBOX = {
[0] = 0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75,
0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8,
0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2,
0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79,
0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A,
0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E,
0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16};
local ISBOX = {
[0] = 0x52, 0x09, 0x6A, 0xD5, 0x30, 0x36, 0xA5, 0x38, 0xBF, 0x40, 0xA3, 0x9E, 0x81, 0xF3, 0xD7, 0xFB,
0x7C, 0xE3, 0x39, 0x82, 0x9B, 0x2F, 0xFF, 0x87, 0x34, 0x8E, 0x43, 0x44, 0xC4, 0xDE, 0xE9, 0xCB,
0x54, 0x7B, 0x94, 0x32, 0xA6, 0xC2, 0x23, 0x3D, 0xEE, 0x4C, 0x95, 0x0B, 0x42, 0xFA, 0xC3, 0x4E,
0x08, 0x2E, 0xA1, 0x66, 0x28, 0xD9, 0x24, 0xB2, 0x76, 0x5B, 0xA2, 0x49, 0x6D, 0x8B, 0xD1, 0x25,
0x72, 0xF8, 0xF6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xD4, 0xA4, 0x5C, 0xCC, 0x5D, 0x65, 0xB6, 0x92,
0x6C, 0x70, 0x48, 0x50, 0xFD, 0xED, 0xB9, 0xDA, 0x5E, 0x15, 0x46, 0x57, 0xA7, 0x8D, 0x9D, 0x84,
0x90, 0xD8, 0xAB, 0x00, 0x8C, 0xBC, 0xD3, 0x0A, 0xF7, 0xE4, 0x58, 0x05, 0xB8, 0xB3, 0x45, 0x06,
0xD0, 0x2C, 0x1E, 0x8F, 0xCA, 0x3F, 0x0F, 0x02, 0xC1, 0xAF, 0xBD, 0x03, 0x01, 0x13, 0x8A, 0x6B,
0x3A, 0x91, 0x11, 0x41, 0x4F, 0x67, 0xDC, 0xEA, 0x97, 0xF2, 0xCF, 0xCE, 0xF0, 0xB4, 0xE6, 0x73,
0x96, 0xAC, 0x74, 0x22, 0xE7, 0xAD, 0x35, 0x85, 0xE2, 0xF9, 0x37, 0xE8, 0x1C, 0x75, 0xDF, 0x6E,
0x47, 0xF1, 0x1A, 0x71, 0x1D, 0x29, 0xC5, 0x89, 0x6F, 0xB7, 0x62, 0x0E, 0xAA, 0x18, 0xBE, 0x1B,
0xFC, 0x56, 0x3E, 0x4B, 0xC6, 0xD2, 0x79, 0x20, 0x9A, 0xDB, 0xC0, 0xFE, 0x78, 0xCD, 0x5A, 0xF4,
0x1F, 0xDD, 0xA8, 0x33, 0x88, 0x07, 0xC7, 0x31, 0xB1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xEC, 0x5F,
0x60, 0x51, 0x7F, 0xA9, 0x19, 0xB5, 0x4A, 0x0D, 0x2D, 0xE5, 0x7A, 0x9F, 0x93, 0xC9, 0x9C, 0xEF,
0xA0, 0xE0, 0x3B, 0x4D, 0xAE, 0x2A, 0xF5, 0xB0, 0xC8, 0xEB, 0xBB, 0x3C, 0x83, 0x53, 0x99, 0x61,
0x17, 0x2B, 0x04, 0x7E, 0xBA, 0x77, 0xD6, 0x26, 0xE1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0C, 0x7D};
local ROW_SHIFT = { 1, 6, 11, 16, 5, 10, 15, 4, 9, 14, 3, 8, 13, 2, 7, 12, };
local IROW_SHIFT = { 1, 14, 11, 8, 5, 2, 15, 12, 9, 6, 3, 16, 13, 10, 7, 4, };
local ETABLE = {
[0] = 0x01, 0x03, 0x05, 0x0F, 0x11, 0x33, 0x55, 0xFF, 0x1A, 0x2E, 0x72, 0x96, 0xA1, 0xF8, 0x13, 0x35,
0x5F, 0xE1, 0x38, 0x48, 0xD8, 0x73, 0x95, 0xA4, 0xF7, 0x02, 0x06, 0x0A, 0x1E, 0x22, 0x66, 0xAA,
0xE5, 0x34, 0x5C, 0xE4, 0x37, 0x59, 0xEB, 0x26, 0x6A, 0xBE, 0xD9, 0x70, 0x90, 0xAB, 0xE6, 0x31,
0x53, 0xF5, 0x04, 0x0C, 0x14, 0x3C, 0x44, 0xCC, 0x4F, 0xD1, 0x68, 0xB8, 0xD3, 0x6E, 0xB2, 0xCD,
0x4C, 0xD4, 0x67, 0xA9, 0xE0, 0x3B, 0x4D, 0xD7, 0x62, 0xA6, 0xF1, 0x08, 0x18, 0x28, 0x78, 0x88,
0x83, 0x9E, 0xB9, 0xD0, 0x6B, 0xBD, 0xDC, 0x7F, 0x81, 0x98, 0xB3, 0xCE, 0x49, 0xDB, 0x76, 0x9A,
0xB5, 0xC4, 0x57, 0xF9, 0x10, 0x30, 0x50, 0xF0, 0x0B, 0x1D, 0x27, 0x69, 0xBB, 0xD6, 0x61, 0xA3,
0xFE, 0x19, 0x2B, 0x7D, 0x87, 0x92, 0xAD, 0xEC, 0x2F, 0x71, 0x93, 0xAE, 0xE9, 0x20, 0x60, 0xA0,
0xFB, 0x16, 0x3A, 0x4E, 0xD2, 0x6D, 0xB7, 0xC2, 0x5D, 0xE7, 0x32, 0x56, 0xFA, 0x15, 0x3F, 0x41,
0xC3, 0x5E, 0xE2, 0x3D, 0x47, 0xC9, 0x40, 0xC0, 0x5B, 0xED, 0x2C, 0x74, 0x9C, 0xBF, 0xDA, 0x75,
0x9F, 0xBA, 0xD5, 0x64, 0xAC, 0xEF, 0x2A, 0x7E, 0x82, 0x9D, 0xBC, 0xDF, 0x7A, 0x8E, 0x89, 0x80,
0x9B, 0xB6, 0xC1, 0x58, 0xE8, 0x23, 0x65, 0xAF, 0xEA, 0x25, 0x6F, 0xB1, 0xC8, 0x43, 0xC5, 0x54,
0xFC, 0x1F, 0x21, 0x63, 0xA5, 0xF4, 0x07, 0x09, 0x1B, 0x2D, 0x77, 0x99, 0xB0, 0xCB, 0x46, 0xCA,
0x45, 0xCF, 0x4A, 0xDE, 0x79, 0x8B, 0x86, 0x91, 0xA8, 0xE3, 0x3E, 0x42, 0xC6, 0x51, 0xF3, 0x0E,
0x12, 0x36, 0x5A, 0xEE, 0x29, 0x7B, 0x8D, 0x8C, 0x8F, 0x8A, 0x85, 0x94, 0xA7, 0xF2, 0x0D, 0x17,
0x39, 0x4B, 0xDD, 0x7C, 0x84, 0x97, 0xA2, 0xFD, 0x1C, 0x24, 0x6C, 0xB4, 0xC7, 0x52, 0xF6, 0x01};
local LTABLE = {
[0] = 0x00, 0x00, 0x19, 0x01, 0x32, 0x02, 0x1A, 0xC6, 0x4B, 0xC7, 0x1B, 0x68, 0x33, 0xEE, 0xDF, 0x03,
0x64, 0x04, 0xE0, 0x0E, 0x34, 0x8D, 0x81, 0xEF, 0x4C, 0x71, 0x08, 0xC8, 0xF8, 0x69, 0x1C, 0xC1,
0x7D, 0xC2, 0x1D, 0xB5, 0xF9, 0xB9, 0x27, 0x6A, 0x4D, 0xE4, 0xA6, 0x72, 0x9A, 0xC9, 0x09, 0x78,
0x65, 0x2F, 0x8A, 0x05, 0x21, 0x0F, 0xE1, 0x24, 0x12, 0xF0, 0x82, 0x45, 0x35, 0x93, 0xDA, 0x8E,
0x96, 0x8F, 0xDB, 0xBD, 0x36, 0xD0, 0xCE, 0x94, 0x13, 0x5C, 0xD2, 0xF1, 0x40, 0x46, 0x83, 0x38,
0x66, 0xDD, 0xFD, 0x30, 0xBF, 0x06, 0x8B, 0x62, 0xB3, 0x25, 0xE2, 0x98, 0x22, 0x88, 0x91, 0x10,
0x7E, 0x6E, 0x48, 0xC3, 0xA3, 0xB6, 0x1E, 0x42, 0x3A, 0x6B, 0x28, 0x54, 0xFA, 0x85, 0x3D, 0xBA,
0x2B, 0x79, 0x0A, 0x15, 0x9B, 0x9F, 0x5E, 0xCA, 0x4E, 0xD4, 0xAC, 0xE5, 0xF3, 0x73, 0xA7, 0x57,
0xAF, 0x58, 0xA8, 0x50, 0xF4, 0xEA, 0xD6, 0x74, 0x4F, 0xAE, 0xE9, 0xD5, 0xE7, 0xE6, 0xAD, 0xE8,
0x2C, 0xD7, 0x75, 0x7A, 0xEB, 0x16, 0x0B, 0xF5, 0x59, 0xCB, 0x5F, 0xB0, 0x9C, 0xA9, 0x51, 0xA0,
0x7F, 0x0C, 0xF6, 0x6F, 0x17, 0xC4, 0x49, 0xEC, 0xD8, 0x43, 0x1F, 0x2D, 0xA4, 0x76, 0x7B, 0xB7,
0xCC, 0xBB, 0x3E, 0x5A, 0xFB, 0x60, 0xB1, 0x86, 0x3B, 0x52, 0xA1, 0x6C, 0xAA, 0x55, 0x29, 0x9D,
0x97, 0xB2, 0x87, 0x90, 0x61, 0xBE, 0xDC, 0xFC, 0xBC, 0x95, 0xCF, 0xCD, 0x37, 0x3F, 0x5B, 0xD1,
0x53, 0x39, 0x84, 0x3C, 0x41, 0xA2, 0x6D, 0x47, 0x14, 0x2A, 0x9E, 0x5D, 0x56, 0xF2, 0xD3, 0xAB,
0x44, 0x11, 0x92, 0xD9, 0x23, 0x20, 0x2E, 0x89, 0xB4, 0x7C, 0xB8, 0x26, 0x77, 0x99, 0xE3, 0xA5,
0x67, 0x4A, 0xED, 0xDE, 0xC5, 0x31, 0xFE, 0x18, 0x0D, 0x63, 0x8C, 0x80, 0xC0, 0xF7, 0x70, 0x07};
local MIXTABLE = {
0x02, 0x03, 0x01, 0x01,
0x01, 0x02, 0x03, 0x01,
0x01, 0x01, 0x02, 0x03,
0x03, 0x01, 0x01, 0x02};
local IMIXTABLE = {
0x0E, 0x0B, 0x0D, 0x09,
0x09, 0x0E, 0x0B, 0x0D,
0x0D, 0x09, 0x0E, 0x0B,
0x0B, 0x0D, 0x09, 0x0E};
local RCON = {
[0] = 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a,
0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39,
0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a,
0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8,
0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef,
0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc,
0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b,
0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3,
0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94,
0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20,
0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35,
0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f,
0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04,
0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63,
0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd,
0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d};
local GMUL = function(A, B)
if(A == 0x01) then return B; end
if(B == 0x01) then return A; end
if(A == 0x00) then return 0; end
if(B == 0x00) then return 0; end
local LA = LTABLE[A];
local LB = LTABLE[B];
local sum = LA + LB;
if (sum > 0xFF) then sum = sum - 0xFF; end
return ETABLE[sum];
end
local byteSub = Array.substitute;
local shiftRow = Array.permute;
local mixCol = function(i, mix)
local out = {};
local a, b, c, d;
a = GMUL(i[ 1], mix[ 1]);
b = GMUL(i[ 2], mix[ 2]);
c = GMUL(i[ 3], mix[ 3]);
d = GMUL(i[ 4], mix[ 4]);
out[ 1] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 1], mix[ 5]);
b = GMUL(i[ 2], mix[ 6]);
c = GMUL(i[ 3], mix[ 7]);
d = GMUL(i[ 4], mix[ 8]);
out[ 2] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 1], mix[ 9]);
b = GMUL(i[ 2], mix[10]);
c = GMUL(i[ 3], mix[11]);
d = GMUL(i[ 4], mix[12]);
out[ 3] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 1], mix[13]);
b = GMUL(i[ 2], mix[14]);
c = GMUL(i[ 3], mix[15]);
d = GMUL(i[ 4], mix[16]);
out[ 4] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 5], mix[ 1]);
b = GMUL(i[ 6], mix[ 2]);
c = GMUL(i[ 7], mix[ 3]);
d = GMUL(i[ 8], mix[ 4]);
out[ 5] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 5], mix[ 5]);
b = GMUL(i[ 6], mix[ 6]);
c = GMUL(i[ 7], mix[ 7]);
d = GMUL(i[ 8], mix[ 8]);
out[ 6] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 5], mix[ 9]);
b = GMUL(i[ 6], mix[10]);
c = GMUL(i[ 7], mix[11]);
d = GMUL(i[ 8], mix[12]);
out[ 7] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 5], mix[13]);
b = GMUL(i[ 6], mix[14]);
c = GMUL(i[ 7], mix[15]);
d = GMUL(i[ 8], mix[16]);
out[ 8] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 9], mix[ 1]);
b = GMUL(i[10], mix[ 2]);
c = GMUL(i[11], mix[ 3]);
d = GMUL(i[12], mix[ 4]);
out[ 9] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 9], mix[ 5]);
b = GMUL(i[10], mix[ 6]);
c = GMUL(i[11], mix[ 7]);
d = GMUL(i[12], mix[ 8]);
out[10] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 9], mix[ 9]);
b = GMUL(i[10], mix[10]);
c = GMUL(i[11], mix[11]);
d = GMUL(i[12], mix[12]);
out[11] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 9], mix[13]);
b = GMUL(i[10], mix[14]);
c = GMUL(i[11], mix[15]);
d = GMUL(i[12], mix[16]);
out[12] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[13], mix[ 1]);
b = GMUL(i[14], mix[ 2]);
c = GMUL(i[15], mix[ 3]);
d = GMUL(i[16], mix[ 4]);
out[13] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[13], mix[ 5]);
b = GMUL(i[14], mix[ 6]);
c = GMUL(i[15], mix[ 7]);
d = GMUL(i[16], mix[ 8]);
out[14] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[13], mix[ 9]);
b = GMUL(i[14], mix[10]);
c = GMUL(i[15], mix[11]);
d = GMUL(i[16], mix[12]);
out[15] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[13], mix[13]);
b = GMUL(i[14], mix[14]);
c = GMUL(i[15], mix[15]);
d = GMUL(i[16], mix[16]);
out[16] = XOR(XOR(a, b), XOR(c, d));
return out;
end
local keyRound = function(key, round)
local out = {};
out[ 1] = XOR(key[ 1], XOR(SBOX[key[14]], RCON[round]));
out[ 2] = XOR(key[ 2], SBOX[key[15]]);
out[ 3] = XOR(key[ 3], SBOX[key[16]]);
out[ 4] = XOR(key[ 4], SBOX[key[13]]);
out[ 5] = XOR(out[ 1], key[ 5]);
out[ 6] = XOR(out[ 2], key[ 6]);
out[ 7] = XOR(out[ 3], key[ 7]);
out[ 8] = XOR(out[ 4], key[ 8]);
out[ 9] = XOR(out[ 5], key[ 9]);
out[10] = XOR(out[ 6], key[10]);
out[11] = XOR(out[ 7], key[11]);
out[12] = XOR(out[ 8], key[12]);
out[13] = XOR(out[ 9], key[13]);
out[14] = XOR(out[10], key[14]);
out[15] = XOR(out[11], key[15]);
out[16] = XOR(out[12], key[16]);
return out;
end
local keyExpand = function(key)
local keys = {};
local temp = key;
keys[1] = temp;
for i = 1, 10 do
temp = keyRound(temp, i);
keys[i + 1] = temp;
end
return keys;
end
local addKey = Array.XOR;
local AES = {};
AES.blockSize = 16;
AES.encrypt = function(_key, block)
local key = keyExpand(_key);
--round 0
block = addKey(block, key[1]);
--round 1
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[2]);
--round 2
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[3]);
--round 3
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[4]);
--round 4
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[5]);
--round 5
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[6]);
--round 6
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[7]);
--round 7
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[8]);
--round 8
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[9]);
--round 9
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[10]);
--round 10
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = addKey(block, key[11]);
return block;
end
AES.decrypt = function(_key, block)
local key = keyExpand(_key);
--round 0
block = addKey(block, key[11]);
--round 1
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[10]);
block = mixCol(block, IMIXTABLE);
--round 2
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[9]);
block = mixCol(block, IMIXTABLE);
--round 3
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[8]);
block = mixCol(block, IMIXTABLE);
--round 4
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[7]);
block = mixCol(block, IMIXTABLE);
--round 5
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[6]);
block = mixCol(block, IMIXTABLE);
--round 6
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[5]);
block = mixCol(block, IMIXTABLE);
--round 7
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[4]);
block = mixCol(block, IMIXTABLE);
--round 8
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[3]);
block = mixCol(block, IMIXTABLE);
--round 9
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[2]);
block = mixCol(block, IMIXTABLE);
--round 10
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[1]);
return block;
end
return AES;

View File

@@ -1,462 +0,0 @@
local Array = require("lockbox.util.array");
local Bit = require("lockbox.util.bit");
local XOR = Bit.bxor;
local SBOX = {
[0] = 0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75,
0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8,
0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2,
0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79,
0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A,
0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E,
0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16};
local ISBOX = {
[0] = 0x52, 0x09, 0x6A, 0xD5, 0x30, 0x36, 0xA5, 0x38, 0xBF, 0x40, 0xA3, 0x9E, 0x81, 0xF3, 0xD7, 0xFB,
0x7C, 0xE3, 0x39, 0x82, 0x9B, 0x2F, 0xFF, 0x87, 0x34, 0x8E, 0x43, 0x44, 0xC4, 0xDE, 0xE9, 0xCB,
0x54, 0x7B, 0x94, 0x32, 0xA6, 0xC2, 0x23, 0x3D, 0xEE, 0x4C, 0x95, 0x0B, 0x42, 0xFA, 0xC3, 0x4E,
0x08, 0x2E, 0xA1, 0x66, 0x28, 0xD9, 0x24, 0xB2, 0x76, 0x5B, 0xA2, 0x49, 0x6D, 0x8B, 0xD1, 0x25,
0x72, 0xF8, 0xF6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xD4, 0xA4, 0x5C, 0xCC, 0x5D, 0x65, 0xB6, 0x92,
0x6C, 0x70, 0x48, 0x50, 0xFD, 0xED, 0xB9, 0xDA, 0x5E, 0x15, 0x46, 0x57, 0xA7, 0x8D, 0x9D, 0x84,
0x90, 0xD8, 0xAB, 0x00, 0x8C, 0xBC, 0xD3, 0x0A, 0xF7, 0xE4, 0x58, 0x05, 0xB8, 0xB3, 0x45, 0x06,
0xD0, 0x2C, 0x1E, 0x8F, 0xCA, 0x3F, 0x0F, 0x02, 0xC1, 0xAF, 0xBD, 0x03, 0x01, 0x13, 0x8A, 0x6B,
0x3A, 0x91, 0x11, 0x41, 0x4F, 0x67, 0xDC, 0xEA, 0x97, 0xF2, 0xCF, 0xCE, 0xF0, 0xB4, 0xE6, 0x73,
0x96, 0xAC, 0x74, 0x22, 0xE7, 0xAD, 0x35, 0x85, 0xE2, 0xF9, 0x37, 0xE8, 0x1C, 0x75, 0xDF, 0x6E,
0x47, 0xF1, 0x1A, 0x71, 0x1D, 0x29, 0xC5, 0x89, 0x6F, 0xB7, 0x62, 0x0E, 0xAA, 0x18, 0xBE, 0x1B,
0xFC, 0x56, 0x3E, 0x4B, 0xC6, 0xD2, 0x79, 0x20, 0x9A, 0xDB, 0xC0, 0xFE, 0x78, 0xCD, 0x5A, 0xF4,
0x1F, 0xDD, 0xA8, 0x33, 0x88, 0x07, 0xC7, 0x31, 0xB1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xEC, 0x5F,
0x60, 0x51, 0x7F, 0xA9, 0x19, 0xB5, 0x4A, 0x0D, 0x2D, 0xE5, 0x7A, 0x9F, 0x93, 0xC9, 0x9C, 0xEF,
0xA0, 0xE0, 0x3B, 0x4D, 0xAE, 0x2A, 0xF5, 0xB0, 0xC8, 0xEB, 0xBB, 0x3C, 0x83, 0x53, 0x99, 0x61,
0x17, 0x2B, 0x04, 0x7E, 0xBA, 0x77, 0xD6, 0x26, 0xE1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0C, 0x7D};
local ROW_SHIFT = { 1, 6, 11, 16, 5, 10, 15, 4, 9, 14, 3, 8, 13, 2, 7, 12, };
local IROW_SHIFT = { 1, 14, 11, 8, 5, 2, 15, 12, 9, 6, 3, 16, 13, 10, 7, 4, };
local ETABLE = {
[0] = 0x01, 0x03, 0x05, 0x0F, 0x11, 0x33, 0x55, 0xFF, 0x1A, 0x2E, 0x72, 0x96, 0xA1, 0xF8, 0x13, 0x35,
0x5F, 0xE1, 0x38, 0x48, 0xD8, 0x73, 0x95, 0xA4, 0xF7, 0x02, 0x06, 0x0A, 0x1E, 0x22, 0x66, 0xAA,
0xE5, 0x34, 0x5C, 0xE4, 0x37, 0x59, 0xEB, 0x26, 0x6A, 0xBE, 0xD9, 0x70, 0x90, 0xAB, 0xE6, 0x31,
0x53, 0xF5, 0x04, 0x0C, 0x14, 0x3C, 0x44, 0xCC, 0x4F, 0xD1, 0x68, 0xB8, 0xD3, 0x6E, 0xB2, 0xCD,
0x4C, 0xD4, 0x67, 0xA9, 0xE0, 0x3B, 0x4D, 0xD7, 0x62, 0xA6, 0xF1, 0x08, 0x18, 0x28, 0x78, 0x88,
0x83, 0x9E, 0xB9, 0xD0, 0x6B, 0xBD, 0xDC, 0x7F, 0x81, 0x98, 0xB3, 0xCE, 0x49, 0xDB, 0x76, 0x9A,
0xB5, 0xC4, 0x57, 0xF9, 0x10, 0x30, 0x50, 0xF0, 0x0B, 0x1D, 0x27, 0x69, 0xBB, 0xD6, 0x61, 0xA3,
0xFE, 0x19, 0x2B, 0x7D, 0x87, 0x92, 0xAD, 0xEC, 0x2F, 0x71, 0x93, 0xAE, 0xE9, 0x20, 0x60, 0xA0,
0xFB, 0x16, 0x3A, 0x4E, 0xD2, 0x6D, 0xB7, 0xC2, 0x5D, 0xE7, 0x32, 0x56, 0xFA, 0x15, 0x3F, 0x41,
0xC3, 0x5E, 0xE2, 0x3D, 0x47, 0xC9, 0x40, 0xC0, 0x5B, 0xED, 0x2C, 0x74, 0x9C, 0xBF, 0xDA, 0x75,
0x9F, 0xBA, 0xD5, 0x64, 0xAC, 0xEF, 0x2A, 0x7E, 0x82, 0x9D, 0xBC, 0xDF, 0x7A, 0x8E, 0x89, 0x80,
0x9B, 0xB6, 0xC1, 0x58, 0xE8, 0x23, 0x65, 0xAF, 0xEA, 0x25, 0x6F, 0xB1, 0xC8, 0x43, 0xC5, 0x54,
0xFC, 0x1F, 0x21, 0x63, 0xA5, 0xF4, 0x07, 0x09, 0x1B, 0x2D, 0x77, 0x99, 0xB0, 0xCB, 0x46, 0xCA,
0x45, 0xCF, 0x4A, 0xDE, 0x79, 0x8B, 0x86, 0x91, 0xA8, 0xE3, 0x3E, 0x42, 0xC6, 0x51, 0xF3, 0x0E,
0x12, 0x36, 0x5A, 0xEE, 0x29, 0x7B, 0x8D, 0x8C, 0x8F, 0x8A, 0x85, 0x94, 0xA7, 0xF2, 0x0D, 0x17,
0x39, 0x4B, 0xDD, 0x7C, 0x84, 0x97, 0xA2, 0xFD, 0x1C, 0x24, 0x6C, 0xB4, 0xC7, 0x52, 0xF6, 0x01};
local LTABLE = {
[0] = 0x00, 0x00, 0x19, 0x01, 0x32, 0x02, 0x1A, 0xC6, 0x4B, 0xC7, 0x1B, 0x68, 0x33, 0xEE, 0xDF, 0x03,
0x64, 0x04, 0xE0, 0x0E, 0x34, 0x8D, 0x81, 0xEF, 0x4C, 0x71, 0x08, 0xC8, 0xF8, 0x69, 0x1C, 0xC1,
0x7D, 0xC2, 0x1D, 0xB5, 0xF9, 0xB9, 0x27, 0x6A, 0x4D, 0xE4, 0xA6, 0x72, 0x9A, 0xC9, 0x09, 0x78,
0x65, 0x2F, 0x8A, 0x05, 0x21, 0x0F, 0xE1, 0x24, 0x12, 0xF0, 0x82, 0x45, 0x35, 0x93, 0xDA, 0x8E,
0x96, 0x8F, 0xDB, 0xBD, 0x36, 0xD0, 0xCE, 0x94, 0x13, 0x5C, 0xD2, 0xF1, 0x40, 0x46, 0x83, 0x38,
0x66, 0xDD, 0xFD, 0x30, 0xBF, 0x06, 0x8B, 0x62, 0xB3, 0x25, 0xE2, 0x98, 0x22, 0x88, 0x91, 0x10,
0x7E, 0x6E, 0x48, 0xC3, 0xA3, 0xB6, 0x1E, 0x42, 0x3A, 0x6B, 0x28, 0x54, 0xFA, 0x85, 0x3D, 0xBA,
0x2B, 0x79, 0x0A, 0x15, 0x9B, 0x9F, 0x5E, 0xCA, 0x4E, 0xD4, 0xAC, 0xE5, 0xF3, 0x73, 0xA7, 0x57,
0xAF, 0x58, 0xA8, 0x50, 0xF4, 0xEA, 0xD6, 0x74, 0x4F, 0xAE, 0xE9, 0xD5, 0xE7, 0xE6, 0xAD, 0xE8,
0x2C, 0xD7, 0x75, 0x7A, 0xEB, 0x16, 0x0B, 0xF5, 0x59, 0xCB, 0x5F, 0xB0, 0x9C, 0xA9, 0x51, 0xA0,
0x7F, 0x0C, 0xF6, 0x6F, 0x17, 0xC4, 0x49, 0xEC, 0xD8, 0x43, 0x1F, 0x2D, 0xA4, 0x76, 0x7B, 0xB7,
0xCC, 0xBB, 0x3E, 0x5A, 0xFB, 0x60, 0xB1, 0x86, 0x3B, 0x52, 0xA1, 0x6C, 0xAA, 0x55, 0x29, 0x9D,
0x97, 0xB2, 0x87, 0x90, 0x61, 0xBE, 0xDC, 0xFC, 0xBC, 0x95, 0xCF, 0xCD, 0x37, 0x3F, 0x5B, 0xD1,
0x53, 0x39, 0x84, 0x3C, 0x41, 0xA2, 0x6D, 0x47, 0x14, 0x2A, 0x9E, 0x5D, 0x56, 0xF2, 0xD3, 0xAB,
0x44, 0x11, 0x92, 0xD9, 0x23, 0x20, 0x2E, 0x89, 0xB4, 0x7C, 0xB8, 0x26, 0x77, 0x99, 0xE3, 0xA5,
0x67, 0x4A, 0xED, 0xDE, 0xC5, 0x31, 0xFE, 0x18, 0x0D, 0x63, 0x8C, 0x80, 0xC0, 0xF7, 0x70, 0x07};
local MIXTABLE = {
0x02, 0x03, 0x01, 0x01,
0x01, 0x02, 0x03, 0x01,
0x01, 0x01, 0x02, 0x03,
0x03, 0x01, 0x01, 0x02};
local IMIXTABLE = {
0x0E, 0x0B, 0x0D, 0x09,
0x09, 0x0E, 0x0B, 0x0D,
0x0D, 0x09, 0x0E, 0x0B,
0x0B, 0x0D, 0x09, 0x0E};
local RCON = {
[0] = 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a,
0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39,
0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a,
0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8,
0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef,
0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc,
0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b,
0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3,
0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94,
0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20,
0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35,
0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f,
0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04,
0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63,
0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd,
0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d};
local GMUL = function(A, B)
if(A == 0x01) then return B; end
if(B == 0x01) then return A; end
if(A == 0x00) then return 0; end
if(B == 0x00) then return 0; end
local LA = LTABLE[A];
local LB = LTABLE[B];
local sum = LA + LB;
if (sum > 0xFF) then sum = sum - 0xFF; end
return ETABLE[sum];
end
local byteSub = Array.substitute;
local shiftRow = Array.permute;
local mixCol = function(i, mix)
local out = {};
local a, b, c, d;
a = GMUL(i[ 1], mix[ 1]);
b = GMUL(i[ 2], mix[ 2]);
c = GMUL(i[ 3], mix[ 3]);
d = GMUL(i[ 4], mix[ 4]);
out[ 1] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 1], mix[ 5]);
b = GMUL(i[ 2], mix[ 6]);
c = GMUL(i[ 3], mix[ 7]);
d = GMUL(i[ 4], mix[ 8]);
out[ 2] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 1], mix[ 9]);
b = GMUL(i[ 2], mix[10]);
c = GMUL(i[ 3], mix[11]);
d = GMUL(i[ 4], mix[12]);
out[ 3] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 1], mix[13]);
b = GMUL(i[ 2], mix[14]);
c = GMUL(i[ 3], mix[15]);
d = GMUL(i[ 4], mix[16]);
out[ 4] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 5], mix[ 1]);
b = GMUL(i[ 6], mix[ 2]);
c = GMUL(i[ 7], mix[ 3]);
d = GMUL(i[ 8], mix[ 4]);
out[ 5] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 5], mix[ 5]);
b = GMUL(i[ 6], mix[ 6]);
c = GMUL(i[ 7], mix[ 7]);
d = GMUL(i[ 8], mix[ 8]);
out[ 6] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 5], mix[ 9]);
b = GMUL(i[ 6], mix[10]);
c = GMUL(i[ 7], mix[11]);
d = GMUL(i[ 8], mix[12]);
out[ 7] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 5], mix[13]);
b = GMUL(i[ 6], mix[14]);
c = GMUL(i[ 7], mix[15]);
d = GMUL(i[ 8], mix[16]);
out[ 8] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 9], mix[ 1]);
b = GMUL(i[10], mix[ 2]);
c = GMUL(i[11], mix[ 3]);
d = GMUL(i[12], mix[ 4]);
out[ 9] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 9], mix[ 5]);
b = GMUL(i[10], mix[ 6]);
c = GMUL(i[11], mix[ 7]);
d = GMUL(i[12], mix[ 8]);
out[10] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 9], mix[ 9]);
b = GMUL(i[10], mix[10]);
c = GMUL(i[11], mix[11]);
d = GMUL(i[12], mix[12]);
out[11] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 9], mix[13]);
b = GMUL(i[10], mix[14]);
c = GMUL(i[11], mix[15]);
d = GMUL(i[12], mix[16]);
out[12] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[13], mix[ 1]);
b = GMUL(i[14], mix[ 2]);
c = GMUL(i[15], mix[ 3]);
d = GMUL(i[16], mix[ 4]);
out[13] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[13], mix[ 5]);
b = GMUL(i[14], mix[ 6]);
c = GMUL(i[15], mix[ 7]);
d = GMUL(i[16], mix[ 8]);
out[14] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[13], mix[ 9]);
b = GMUL(i[14], mix[10]);
c = GMUL(i[15], mix[11]);
d = GMUL(i[16], mix[12]);
out[15] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[13], mix[13]);
b = GMUL(i[14], mix[14]);
c = GMUL(i[15], mix[15]);
d = GMUL(i[16], mix[16]);
out[16] = XOR(XOR(a, b), XOR(c, d));
return out;
end
local keyRound = function(key, round)
local i = (round - 1) * 24;
local out = key;
out[25 + i] = XOR(key[ 1 + i], XOR(SBOX[key[22 + i]], RCON[round]));
out[26 + i] = XOR(key[ 2 + i], SBOX[key[23 + i]]);
out[27 + i] = XOR(key[ 3 + i], SBOX[key[24 + i]]);
out[28 + i] = XOR(key[ 4 + i], SBOX[key[21 + i]]);
out[29 + i] = XOR(out[25 + i], key[ 5 + i]);
out[30 + i] = XOR(out[26 + i], key[ 6 + i]);
out[31 + i] = XOR(out[27 + i], key[ 7 + i]);
out[32 + i] = XOR(out[28 + i], key[ 8 + i]);
out[33 + i] = XOR(out[29 + i], key[ 9 + i]);
out[34 + i] = XOR(out[30 + i], key[10 + i]);
out[35 + i] = XOR(out[31 + i], key[11 + i]);
out[36 + i] = XOR(out[32 + i], key[12 + i]);
out[37 + i] = XOR(out[33 + i], key[13 + i]);
out[38 + i] = XOR(out[34 + i], key[14 + i]);
out[39 + i] = XOR(out[35 + i], key[15 + i]);
out[40 + i] = XOR(out[36 + i], key[16 + i]);
out[41 + i] = XOR(out[37 + i], key[17 + i]);
out[42 + i] = XOR(out[38 + i], key[18 + i]);
out[43 + i] = XOR(out[39 + i], key[19 + i]);
out[44 + i] = XOR(out[40 + i], key[20 + i]);
out[45 + i] = XOR(out[41 + i], key[21 + i]);
out[46 + i] = XOR(out[42 + i], key[22 + i]);
out[47 + i] = XOR(out[43 + i], key[23 + i]);
out[48 + i] = XOR(out[44 + i], key[24 + i]);
return out;
end
local keyExpand = function(key)
local bytes = Array.copy(key);
for i = 1, 8 do
keyRound(bytes, i);
end
local keys = {};
keys[ 1] = Array.slice(bytes, 1, 16);
keys[ 2] = Array.slice(bytes, 17, 32);
keys[ 3] = Array.slice(bytes, 33, 48);
keys[ 4] = Array.slice(bytes, 49, 64);
keys[ 5] = Array.slice(bytes, 65, 80);
keys[ 6] = Array.slice(bytes, 81, 96);
keys[ 7] = Array.slice(bytes, 97, 112);
keys[ 8] = Array.slice(bytes, 113, 128);
keys[ 9] = Array.slice(bytes, 129, 144);
keys[10] = Array.slice(bytes, 145, 160);
keys[11] = Array.slice(bytes, 161, 176);
keys[12] = Array.slice(bytes, 177, 192);
keys[13] = Array.slice(bytes, 193, 208);
return keys;
end
local addKey = Array.XOR;
local AES = {};
AES.blockSize = 16;
AES.encrypt = function(_key, block)
local key = keyExpand(_key);
--round 0
block = addKey(block, key[1]);
--round 1
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[2]);
--round 2
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[3]);
--round 3
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[4]);
--round 4
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[5]);
--round 5
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[6]);
--round 6
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[7]);
--round 7
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[8]);
--round 8
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[9]);
--round 9
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[10]);
--round 10
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[11]);
--round 11
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[12]);
--round 12
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = addKey(block, key[13]);
return block;
end
AES.decrypt = function(_key, block)
local key = keyExpand(_key);
--round 0
block = addKey(block, key[13]);
--round 1
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[12]);
block = mixCol(block, IMIXTABLE);
--round 2
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[11]);
block = mixCol(block, IMIXTABLE);
--round 3
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[10]);
block = mixCol(block, IMIXTABLE);
--round 4
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[9]);
block = mixCol(block, IMIXTABLE);
--round 5
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[8]);
block = mixCol(block, IMIXTABLE);
--round 6
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[7]);
block = mixCol(block, IMIXTABLE);
--round 7
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[6]);
block = mixCol(block, IMIXTABLE);
--round 8
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[5]);
block = mixCol(block, IMIXTABLE);
--round 9
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[4]);
block = mixCol(block, IMIXTABLE);
--round 10
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[3]);
block = mixCol(block, IMIXTABLE);
--round 11
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[2]);
block = mixCol(block, IMIXTABLE);
--round 12
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[1]);
return block;
end
return AES;

View File

@@ -1,498 +0,0 @@
local Array = require("lockbox.util.array");
local Bit = require("lockbox.util.bit");
local XOR = Bit.bxor;
local SBOX = {
[0] = 0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75,
0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8,
0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2,
0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79,
0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A,
0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E,
0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16};
local ISBOX = {
[0] = 0x52, 0x09, 0x6A, 0xD5, 0x30, 0x36, 0xA5, 0x38, 0xBF, 0x40, 0xA3, 0x9E, 0x81, 0xF3, 0xD7, 0xFB,
0x7C, 0xE3, 0x39, 0x82, 0x9B, 0x2F, 0xFF, 0x87, 0x34, 0x8E, 0x43, 0x44, 0xC4, 0xDE, 0xE9, 0xCB,
0x54, 0x7B, 0x94, 0x32, 0xA6, 0xC2, 0x23, 0x3D, 0xEE, 0x4C, 0x95, 0x0B, 0x42, 0xFA, 0xC3, 0x4E,
0x08, 0x2E, 0xA1, 0x66, 0x28, 0xD9, 0x24, 0xB2, 0x76, 0x5B, 0xA2, 0x49, 0x6D, 0x8B, 0xD1, 0x25,
0x72, 0xF8, 0xF6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xD4, 0xA4, 0x5C, 0xCC, 0x5D, 0x65, 0xB6, 0x92,
0x6C, 0x70, 0x48, 0x50, 0xFD, 0xED, 0xB9, 0xDA, 0x5E, 0x15, 0x46, 0x57, 0xA7, 0x8D, 0x9D, 0x84,
0x90, 0xD8, 0xAB, 0x00, 0x8C, 0xBC, 0xD3, 0x0A, 0xF7, 0xE4, 0x58, 0x05, 0xB8, 0xB3, 0x45, 0x06,
0xD0, 0x2C, 0x1E, 0x8F, 0xCA, 0x3F, 0x0F, 0x02, 0xC1, 0xAF, 0xBD, 0x03, 0x01, 0x13, 0x8A, 0x6B,
0x3A, 0x91, 0x11, 0x41, 0x4F, 0x67, 0xDC, 0xEA, 0x97, 0xF2, 0xCF, 0xCE, 0xF0, 0xB4, 0xE6, 0x73,
0x96, 0xAC, 0x74, 0x22, 0xE7, 0xAD, 0x35, 0x85, 0xE2, 0xF9, 0x37, 0xE8, 0x1C, 0x75, 0xDF, 0x6E,
0x47, 0xF1, 0x1A, 0x71, 0x1D, 0x29, 0xC5, 0x89, 0x6F, 0xB7, 0x62, 0x0E, 0xAA, 0x18, 0xBE, 0x1B,
0xFC, 0x56, 0x3E, 0x4B, 0xC6, 0xD2, 0x79, 0x20, 0x9A, 0xDB, 0xC0, 0xFE, 0x78, 0xCD, 0x5A, 0xF4,
0x1F, 0xDD, 0xA8, 0x33, 0x88, 0x07, 0xC7, 0x31, 0xB1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xEC, 0x5F,
0x60, 0x51, 0x7F, 0xA9, 0x19, 0xB5, 0x4A, 0x0D, 0x2D, 0xE5, 0x7A, 0x9F, 0x93, 0xC9, 0x9C, 0xEF,
0xA0, 0xE0, 0x3B, 0x4D, 0xAE, 0x2A, 0xF5, 0xB0, 0xC8, 0xEB, 0xBB, 0x3C, 0x83, 0x53, 0x99, 0x61,
0x17, 0x2B, 0x04, 0x7E, 0xBA, 0x77, 0xD6, 0x26, 0xE1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0C, 0x7D};
local ROW_SHIFT = { 1, 6, 11, 16, 5, 10, 15, 4, 9, 14, 3, 8, 13, 2, 7, 12, };
local IROW_SHIFT = { 1, 14, 11, 8, 5, 2, 15, 12, 9, 6, 3, 16, 13, 10, 7, 4, };
local ETABLE = {
[0] = 0x01, 0x03, 0x05, 0x0F, 0x11, 0x33, 0x55, 0xFF, 0x1A, 0x2E, 0x72, 0x96, 0xA1, 0xF8, 0x13, 0x35,
0x5F, 0xE1, 0x38, 0x48, 0xD8, 0x73, 0x95, 0xA4, 0xF7, 0x02, 0x06, 0x0A, 0x1E, 0x22, 0x66, 0xAA,
0xE5, 0x34, 0x5C, 0xE4, 0x37, 0x59, 0xEB, 0x26, 0x6A, 0xBE, 0xD9, 0x70, 0x90, 0xAB, 0xE6, 0x31,
0x53, 0xF5, 0x04, 0x0C, 0x14, 0x3C, 0x44, 0xCC, 0x4F, 0xD1, 0x68, 0xB8, 0xD3, 0x6E, 0xB2, 0xCD,
0x4C, 0xD4, 0x67, 0xA9, 0xE0, 0x3B, 0x4D, 0xD7, 0x62, 0xA6, 0xF1, 0x08, 0x18, 0x28, 0x78, 0x88,
0x83, 0x9E, 0xB9, 0xD0, 0x6B, 0xBD, 0xDC, 0x7F, 0x81, 0x98, 0xB3, 0xCE, 0x49, 0xDB, 0x76, 0x9A,
0xB5, 0xC4, 0x57, 0xF9, 0x10, 0x30, 0x50, 0xF0, 0x0B, 0x1D, 0x27, 0x69, 0xBB, 0xD6, 0x61, 0xA3,
0xFE, 0x19, 0x2B, 0x7D, 0x87, 0x92, 0xAD, 0xEC, 0x2F, 0x71, 0x93, 0xAE, 0xE9, 0x20, 0x60, 0xA0,
0xFB, 0x16, 0x3A, 0x4E, 0xD2, 0x6D, 0xB7, 0xC2, 0x5D, 0xE7, 0x32, 0x56, 0xFA, 0x15, 0x3F, 0x41,
0xC3, 0x5E, 0xE2, 0x3D, 0x47, 0xC9, 0x40, 0xC0, 0x5B, 0xED, 0x2C, 0x74, 0x9C, 0xBF, 0xDA, 0x75,
0x9F, 0xBA, 0xD5, 0x64, 0xAC, 0xEF, 0x2A, 0x7E, 0x82, 0x9D, 0xBC, 0xDF, 0x7A, 0x8E, 0x89, 0x80,
0x9B, 0xB6, 0xC1, 0x58, 0xE8, 0x23, 0x65, 0xAF, 0xEA, 0x25, 0x6F, 0xB1, 0xC8, 0x43, 0xC5, 0x54,
0xFC, 0x1F, 0x21, 0x63, 0xA5, 0xF4, 0x07, 0x09, 0x1B, 0x2D, 0x77, 0x99, 0xB0, 0xCB, 0x46, 0xCA,
0x45, 0xCF, 0x4A, 0xDE, 0x79, 0x8B, 0x86, 0x91, 0xA8, 0xE3, 0x3E, 0x42, 0xC6, 0x51, 0xF3, 0x0E,
0x12, 0x36, 0x5A, 0xEE, 0x29, 0x7B, 0x8D, 0x8C, 0x8F, 0x8A, 0x85, 0x94, 0xA7, 0xF2, 0x0D, 0x17,
0x39, 0x4B, 0xDD, 0x7C, 0x84, 0x97, 0xA2, 0xFD, 0x1C, 0x24, 0x6C, 0xB4, 0xC7, 0x52, 0xF6, 0x01};
local LTABLE = {
[0] = 0x00, 0x00, 0x19, 0x01, 0x32, 0x02, 0x1A, 0xC6, 0x4B, 0xC7, 0x1B, 0x68, 0x33, 0xEE, 0xDF, 0x03,
0x64, 0x04, 0xE0, 0x0E, 0x34, 0x8D, 0x81, 0xEF, 0x4C, 0x71, 0x08, 0xC8, 0xF8, 0x69, 0x1C, 0xC1,
0x7D, 0xC2, 0x1D, 0xB5, 0xF9, 0xB9, 0x27, 0x6A, 0x4D, 0xE4, 0xA6, 0x72, 0x9A, 0xC9, 0x09, 0x78,
0x65, 0x2F, 0x8A, 0x05, 0x21, 0x0F, 0xE1, 0x24, 0x12, 0xF0, 0x82, 0x45, 0x35, 0x93, 0xDA, 0x8E,
0x96, 0x8F, 0xDB, 0xBD, 0x36, 0xD0, 0xCE, 0x94, 0x13, 0x5C, 0xD2, 0xF1, 0x40, 0x46, 0x83, 0x38,
0x66, 0xDD, 0xFD, 0x30, 0xBF, 0x06, 0x8B, 0x62, 0xB3, 0x25, 0xE2, 0x98, 0x22, 0x88, 0x91, 0x10,
0x7E, 0x6E, 0x48, 0xC3, 0xA3, 0xB6, 0x1E, 0x42, 0x3A, 0x6B, 0x28, 0x54, 0xFA, 0x85, 0x3D, 0xBA,
0x2B, 0x79, 0x0A, 0x15, 0x9B, 0x9F, 0x5E, 0xCA, 0x4E, 0xD4, 0xAC, 0xE5, 0xF3, 0x73, 0xA7, 0x57,
0xAF, 0x58, 0xA8, 0x50, 0xF4, 0xEA, 0xD6, 0x74, 0x4F, 0xAE, 0xE9, 0xD5, 0xE7, 0xE6, 0xAD, 0xE8,
0x2C, 0xD7, 0x75, 0x7A, 0xEB, 0x16, 0x0B, 0xF5, 0x59, 0xCB, 0x5F, 0xB0, 0x9C, 0xA9, 0x51, 0xA0,
0x7F, 0x0C, 0xF6, 0x6F, 0x17, 0xC4, 0x49, 0xEC, 0xD8, 0x43, 0x1F, 0x2D, 0xA4, 0x76, 0x7B, 0xB7,
0xCC, 0xBB, 0x3E, 0x5A, 0xFB, 0x60, 0xB1, 0x86, 0x3B, 0x52, 0xA1, 0x6C, 0xAA, 0x55, 0x29, 0x9D,
0x97, 0xB2, 0x87, 0x90, 0x61, 0xBE, 0xDC, 0xFC, 0xBC, 0x95, 0xCF, 0xCD, 0x37, 0x3F, 0x5B, 0xD1,
0x53, 0x39, 0x84, 0x3C, 0x41, 0xA2, 0x6D, 0x47, 0x14, 0x2A, 0x9E, 0x5D, 0x56, 0xF2, 0xD3, 0xAB,
0x44, 0x11, 0x92, 0xD9, 0x23, 0x20, 0x2E, 0x89, 0xB4, 0x7C, 0xB8, 0x26, 0x77, 0x99, 0xE3, 0xA5,
0x67, 0x4A, 0xED, 0xDE, 0xC5, 0x31, 0xFE, 0x18, 0x0D, 0x63, 0x8C, 0x80, 0xC0, 0xF7, 0x70, 0x07};
local MIXTABLE = {
0x02, 0x03, 0x01, 0x01,
0x01, 0x02, 0x03, 0x01,
0x01, 0x01, 0x02, 0x03,
0x03, 0x01, 0x01, 0x02};
local IMIXTABLE = {
0x0E, 0x0B, 0x0D, 0x09,
0x09, 0x0E, 0x0B, 0x0D,
0x0D, 0x09, 0x0E, 0x0B,
0x0B, 0x0D, 0x09, 0x0E};
local RCON = {
[0] = 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a,
0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39,
0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a,
0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8,
0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef,
0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc,
0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b,
0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3,
0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94,
0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20,
0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35,
0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f,
0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04,
0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63,
0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd,
0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d};
local GMUL = function(A, B)
if(A == 0x01) then return B; end
if(B == 0x01) then return A; end
if(A == 0x00) then return 0; end
if(B == 0x00) then return 0; end
local LA = LTABLE[A];
local LB = LTABLE[B];
local sum = LA + LB;
if (sum > 0xFF) then sum = sum - 0xFF; end
return ETABLE[sum];
end
local byteSub = Array.substitute;
local shiftRow = Array.permute;
local mixCol = function(i, mix)
local out = {};
local a, b, c, d;
a = GMUL(i[ 1], mix[ 1]);
b = GMUL(i[ 2], mix[ 2]);
c = GMUL(i[ 3], mix[ 3]);
d = GMUL(i[ 4], mix[ 4]);
out[ 1] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 1], mix[ 5]);
b = GMUL(i[ 2], mix[ 6]);
c = GMUL(i[ 3], mix[ 7]);
d = GMUL(i[ 4], mix[ 8]);
out[ 2] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 1], mix[ 9]);
b = GMUL(i[ 2], mix[10]);
c = GMUL(i[ 3], mix[11]);
d = GMUL(i[ 4], mix[12]);
out[ 3] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 1], mix[13]);
b = GMUL(i[ 2], mix[14]);
c = GMUL(i[ 3], mix[15]);
d = GMUL(i[ 4], mix[16]);
out[ 4] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 5], mix[ 1]);
b = GMUL(i[ 6], mix[ 2]);
c = GMUL(i[ 7], mix[ 3]);
d = GMUL(i[ 8], mix[ 4]);
out[ 5] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 5], mix[ 5]);
b = GMUL(i[ 6], mix[ 6]);
c = GMUL(i[ 7], mix[ 7]);
d = GMUL(i[ 8], mix[ 8]);
out[ 6] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 5], mix[ 9]);
b = GMUL(i[ 6], mix[10]);
c = GMUL(i[ 7], mix[11]);
d = GMUL(i[ 8], mix[12]);
out[ 7] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 5], mix[13]);
b = GMUL(i[ 6], mix[14]);
c = GMUL(i[ 7], mix[15]);
d = GMUL(i[ 8], mix[16]);
out[ 8] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 9], mix[ 1]);
b = GMUL(i[10], mix[ 2]);
c = GMUL(i[11], mix[ 3]);
d = GMUL(i[12], mix[ 4]);
out[ 9] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 9], mix[ 5]);
b = GMUL(i[10], mix[ 6]);
c = GMUL(i[11], mix[ 7]);
d = GMUL(i[12], mix[ 8]);
out[10] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 9], mix[ 9]);
b = GMUL(i[10], mix[10]);
c = GMUL(i[11], mix[11]);
d = GMUL(i[12], mix[12]);
out[11] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[ 9], mix[13]);
b = GMUL(i[10], mix[14]);
c = GMUL(i[11], mix[15]);
d = GMUL(i[12], mix[16]);
out[12] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[13], mix[ 1]);
b = GMUL(i[14], mix[ 2]);
c = GMUL(i[15], mix[ 3]);
d = GMUL(i[16], mix[ 4]);
out[13] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[13], mix[ 5]);
b = GMUL(i[14], mix[ 6]);
c = GMUL(i[15], mix[ 7]);
d = GMUL(i[16], mix[ 8]);
out[14] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[13], mix[ 9]);
b = GMUL(i[14], mix[10]);
c = GMUL(i[15], mix[11]);
d = GMUL(i[16], mix[12]);
out[15] = XOR(XOR(a, b), XOR(c, d));
a = GMUL(i[13], mix[13]);
b = GMUL(i[14], mix[14]);
c = GMUL(i[15], mix[15]);
d = GMUL(i[16], mix[16]);
out[16] = XOR(XOR(a, b), XOR(c, d));
return out;
end
local keyRound = function(key, round)
local i = (round - 1) * 32;
local out = key;
out[33 + i] = XOR(key[ 1 + i], XOR(SBOX[key[30 + i]], RCON[round]));
out[34 + i] = XOR(key[ 2 + i], SBOX[key[31 + i]]);
out[35 + i] = XOR(key[ 3 + i], SBOX[key[32 + i]]);
out[36 + i] = XOR(key[ 4 + i], SBOX[key[29 + i]]);
out[37 + i] = XOR(out[33 + i], key[ 5 + i]);
out[38 + i] = XOR(out[34 + i], key[ 6 + i]);
out[39 + i] = XOR(out[35 + i], key[ 7 + i]);
out[40 + i] = XOR(out[36 + i], key[ 8 + i]);
out[41 + i] = XOR(out[37 + i], key[ 9 + i]);
out[42 + i] = XOR(out[38 + i], key[10 + i]);
out[43 + i] = XOR(out[39 + i], key[11 + i]);
out[44 + i] = XOR(out[40 + i], key[12 + i]);
out[45 + i] = XOR(out[41 + i], key[13 + i]);
out[46 + i] = XOR(out[42 + i], key[14 + i]);
out[47 + i] = XOR(out[43 + i], key[15 + i]);
out[48 + i] = XOR(out[44 + i], key[16 + i]);
out[49 + i] = XOR(SBOX[out[45 + i]], key[17 + i]);
out[50 + i] = XOR(SBOX[out[46 + i]], key[18 + i]);
out[51 + i] = XOR(SBOX[out[47 + i]], key[19 + i]);
out[52 + i] = XOR(SBOX[out[48 + i]], key[20 + i]);
out[53 + i] = XOR(out[49 + i], key[21 + i]);
out[54 + i] = XOR(out[50 + i], key[22 + i]);
out[55 + i] = XOR(out[51 + i], key[23 + i]);
out[56 + i] = XOR(out[52 + i], key[24 + i]);
out[57 + i] = XOR(out[53 + i], key[25 + i]);
out[58 + i] = XOR(out[54 + i], key[26 + i]);
out[59 + i] = XOR(out[55 + i], key[27 + i]);
out[60 + i] = XOR(out[56 + i], key[28 + i]);
out[61 + i] = XOR(out[57 + i], key[29 + i]);
out[62 + i] = XOR(out[58 + i], key[30 + i]);
out[63 + i] = XOR(out[59 + i], key[31 + i]);
out[64 + i] = XOR(out[60 + i], key[32 + i]);
return out;
end
local keyExpand = function(key)
local bytes = Array.copy(key);
for i = 1, 7 do
keyRound(bytes, i);
end
local keys = {};
keys[ 1] = Array.slice(bytes, 1, 16);
keys[ 2] = Array.slice(bytes, 17, 32);
keys[ 3] = Array.slice(bytes, 33, 48);
keys[ 4] = Array.slice(bytes, 49, 64);
keys[ 5] = Array.slice(bytes, 65, 80);
keys[ 6] = Array.slice(bytes, 81, 96);
keys[ 7] = Array.slice(bytes, 97, 112);
keys[ 8] = Array.slice(bytes, 113, 128);
keys[ 9] = Array.slice(bytes, 129, 144);
keys[10] = Array.slice(bytes, 145, 160);
keys[11] = Array.slice(bytes, 161, 176);
keys[12] = Array.slice(bytes, 177, 192);
keys[13] = Array.slice(bytes, 193, 208);
keys[14] = Array.slice(bytes, 209, 224);
keys[15] = Array.slice(bytes, 225, 240);
return keys;
end
local addKey = Array.XOR;
local AES = {};
AES.blockSize = 16;
AES.encrypt = function(_key, block)
local key = keyExpand(_key);
--round 0
block = addKey(block, key[1]);
--round 1
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[2]);
--round 2
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[3]);
--round 3
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[4]);
--round 4
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[5]);
--round 5
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[6]);
--round 6
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[7]);
--round 7
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[8]);
--round 8
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[9]);
--round 9
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[10]);
--round 10
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[11]);
--round 11
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[12]);
--round 12
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[13]);
--round 13
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = mixCol(block, MIXTABLE);
block = addKey(block, key[14]);
--round 14
block = byteSub(block, SBOX);
block = shiftRow(block, ROW_SHIFT);
block = addKey(block, key[15]);
return block;
end
AES.decrypt = function(_key, block)
local key = keyExpand(_key);
--round 0
block = addKey(block, key[15]);
--round 1
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[14]);
block = mixCol(block, IMIXTABLE);
--round 2
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[13]);
block = mixCol(block, IMIXTABLE);
--round 3
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[12]);
block = mixCol(block, IMIXTABLE);
--round 4
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[11]);
block = mixCol(block, IMIXTABLE);
--round 5
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[10]);
block = mixCol(block, IMIXTABLE);
--round 6
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[9]);
block = mixCol(block, IMIXTABLE);
--round 7
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[8]);
block = mixCol(block, IMIXTABLE);
--round 8
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[7]);
block = mixCol(block, IMIXTABLE);
--round 9
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[6]);
block = mixCol(block, IMIXTABLE);
--round 10
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[5]);
block = mixCol(block, IMIXTABLE);
--round 11
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[4]);
block = mixCol(block, IMIXTABLE);
--round 12
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[3]);
block = mixCol(block, IMIXTABLE);
--round 13
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[2]);
block = mixCol(block, IMIXTABLE);
--round 14
block = shiftRow(block, IROW_SHIFT);
block = byteSub(block, ISBOX);
block = addKey(block, key[1]);
return block;
end
return AES;

View File

@@ -1,164 +0,0 @@
local Array = require("lockbox.util.array");
local Stream = require("lockbox.util.stream");
local Queue = require("lockbox.util.queue");
local CBC = {};
CBC.Cipher = function()
local public = {};
local key;
local blockCipher;
local padding;
local inputQueue;
local outputQueue;
local iv;
public.setKey = function(keyBytes)
key = keyBytes;
return public;
end
public.setBlockCipher = function(cipher)
blockCipher = cipher;
return public;
end
public.setPadding = function(paddingMode)
padding = paddingMode;
return public;
end
public.init = function()
inputQueue = Queue();
outputQueue = Queue();
iv = nil;
return public;
end
public.update = function(messageStream)
local byte = messageStream();
while (byte ~= nil) do
inputQueue.push(byte);
if(inputQueue.size() >= blockCipher.blockSize) then
local block = Array.readFromQueue(inputQueue, blockCipher.blockSize);
if(iv == nil) then
iv = block;
else
local out = Array.XOR(iv, block);
out = blockCipher.encrypt(key, out);
Array.writeToQueue(outputQueue, out);
iv = out;
end
end
byte = messageStream();
end
return public;
end
public.finish = function()
local paddingStream = padding(blockCipher.blockSize, inputQueue.getHead());
public.update(paddingStream);
return public;
end
public.getOutputQueue = function()
return outputQueue;
end
public.asHex = function()
return Stream.toHex(outputQueue.pop);
end
public.asBytes = function()
return Stream.toArray(outputQueue.pop);
end
return public;
end
CBC.Decipher = function()
local public = {};
local key;
local blockCipher;
local padding;
local inputQueue;
local outputQueue;
local iv;
public.setKey = function(keyBytes)
key = keyBytes;
return public;
end
public.setBlockCipher = function(cipher)
blockCipher = cipher;
return public;
end
public.setPadding = function(paddingMode)
padding = paddingMode;
return public;
end
public.init = function()
inputQueue = Queue();
outputQueue = Queue();
iv = nil;
return public;
end
public.update = function(messageStream)
local byte = messageStream();
while (byte ~= nil) do
inputQueue.push(byte);
if(inputQueue.size() >= blockCipher.blockSize) then
local block = Array.readFromQueue(inputQueue, blockCipher.blockSize);
if(iv == nil) then
iv = block;
else
local out = block;
out = blockCipher.decrypt(key, out);
out = Array.XOR(iv, out);
Array.writeToQueue(outputQueue, out);
iv = block;
end
end
byte = messageStream();
end
return public;
end
public.finish = function()
local paddingStream = padding(blockCipher.blockSize, inputQueue.getHead());
public.update(paddingStream);
return public;
end
public.getOutputQueue = function()
return outputQueue;
end
public.asHex = function()
return Stream.toHex(outputQueue.pop);
end
public.asBytes = function()
return Stream.toArray(outputQueue.pop);
end
return public;
end
return CBC;

View File

@@ -1,163 +0,0 @@
local Array = require("lockbox.util.array");
local Stream = require("lockbox.util.stream");
local Queue = require("lockbox.util.queue");
local CFB = {};
CFB.Cipher = function()
local public = {};
local key;
local blockCipher;
local padding;
local inputQueue;
local outputQueue;
local iv;
public.setKey = function(keyBytes)
key = keyBytes;
return public;
end
public.setBlockCipher = function(cipher)
blockCipher = cipher;
return public;
end
public.setPadding = function(paddingMode)
padding = paddingMode;
return public;
end
public.init = function()
inputQueue = Queue();
outputQueue = Queue();
iv = nil;
return public;
end
public.update = function(messageStream)
local byte = messageStream();
while (byte ~= nil) do
inputQueue.push(byte);
if(inputQueue.size() >= blockCipher.blockSize) then
local block = Array.readFromQueue(inputQueue, blockCipher.blockSize);
if(iv == nil) then
iv = block;
else
local out = iv;
out = blockCipher.encrypt(key, out);
out = Array.XOR(out, block);
Array.writeToQueue(outputQueue, out);
iv = out;
end
end
byte = messageStream();
end
return public;
end
public.finish = function()
local paddingStream = padding(blockCipher.blockSize, inputQueue.getHead());
public.update(paddingStream);
return public;
end
public.getOutputQueue = function()
return outputQueue;
end
public.asHex = function()
return Stream.toHex(outputQueue.pop);
end
public.asBytes = function()
return Stream.toArray(outputQueue.pop);
end
return public;
end
CFB.Decipher = function()
local public = {};
local key;
local blockCipher;
local padding;
local inputQueue;
local outputQueue;
local iv;
public.setKey = function(keyBytes)
key = keyBytes;
return public;
end
public.setBlockCipher = function(cipher)
blockCipher = cipher;
return public;
end
public.setPadding = function(paddingMode)
padding = paddingMode;
return public;
end
public.init = function()
inputQueue = Queue();
outputQueue = Queue();
iv = nil;
return public;
end
public.update = function(messageStream)
local byte = messageStream();
while (byte ~= nil) do
inputQueue.push(byte);
if(inputQueue.size() >= blockCipher.blockSize) then
local block = Array.readFromQueue(inputQueue, blockCipher.blockSize);
if(iv == nil) then
iv = block;
else
local out = iv;
out = blockCipher.encrypt(key, out);
out = Array.XOR(out, block);
Array.writeToQueue(outputQueue, out);
iv = block;
end
end
byte = messageStream();
end
return public;
end
public.finish = function()
local paddingStream = padding(blockCipher.blockSize, inputQueue.getHead());
public.update(paddingStream);
return public;
end
public.getOutputQueue = function()
return outputQueue;
end
public.asHex = function()
return Stream.toHex(outputQueue.pop);
end
public.asBytes = function()
return Stream.toArray(outputQueue.pop);
end
return public;
end
return CFB;

View File

@@ -1,248 +0,0 @@
local Array = require("lockbox.util.array");
local Stream = require("lockbox.util.stream");
local Queue = require("lockbox.util.queue");
local Bit = require("lockbox.util.bit");
local AND = Bit.band;
local CTR = {};
CTR.Cipher = function()
local public = {};
local key;
local blockCipher;
local padding;
local inputQueue;
local outputQueue;
local iv;
public.setKey = function(keyBytes)
key = keyBytes;
return public;
end
public.setBlockCipher = function(cipher)
blockCipher = cipher;
return public;
end
public.setPadding = function(paddingMode)
padding = paddingMode;
return public;
end
public.init = function()
inputQueue = Queue();
outputQueue = Queue();
iv = nil;
return public;
end
local updateIV = function()
iv[16] = iv[16] + 1;
if iv[16] <= 0xFF then return; end
iv[16] = AND(iv[16], 0xFF);
iv[15] = iv[15] + 1;
if iv[15] <= 0xFF then return; end
iv[15] = AND(iv[15], 0xFF);
iv[14] = iv[14] + 1;
if iv[14] <= 0xFF then return; end
iv[14] = AND(iv[14], 0xFF);
iv[13] = iv[13] + 1;
if iv[13] <= 0xFF then return; end
iv[13] = AND(iv[13], 0xFF);
iv[12] = iv[12] + 1;
if iv[12] <= 0xFF then return; end
iv[12] = AND(iv[12], 0xFF);
iv[11] = iv[11] + 1;
if iv[11] <= 0xFF then return; end
iv[11] = AND(iv[11], 0xFF);
iv[10] = iv[10] + 1;
if iv[10] <= 0xFF then return; end
iv[10] = AND(iv[10], 0xFF);
iv[9] = iv[9] + 1;
if iv[9] <= 0xFF then return; end
iv[9] = AND(iv[9], 0xFF);
return;
end
public.update = function(messageStream)
local byte = messageStream();
while (byte ~= nil) do
inputQueue.push(byte);
if(inputQueue.size() >= blockCipher.blockSize) then
local block = Array.readFromQueue(inputQueue, blockCipher.blockSize);
if(iv == nil) then
iv = block;
else
local out = iv;
out = blockCipher.encrypt(key, out);
out = Array.XOR(out, block);
Array.writeToQueue(outputQueue, out);
updateIV();
end
end
byte = messageStream();
end
return public;
end
public.finish = function()
local paddingStream = padding(blockCipher.blockSize, inputQueue.getHead());
public.update(paddingStream);
return public;
end
public.getOutputQueue = function()
return outputQueue;
end
public.asHex = function()
return Stream.toHex(outputQueue.pop);
end
public.asBytes = function()
return Stream.toArray(outputQueue.pop);
end
return public;
end
CTR.Decipher = function()
local public = {};
local key;
local blockCipher;
local padding;
local inputQueue;
local outputQueue;
local iv;
public.setKey = function(keyBytes)
key = keyBytes;
return public;
end
public.setBlockCipher = function(cipher)
blockCipher = cipher;
return public;
end
public.setPadding = function(paddingMode)
padding = paddingMode;
return public;
end
public.init = function()
inputQueue = Queue();
outputQueue = Queue();
iv = nil;
return public;
end
local updateIV = function()
iv[16] = iv[16] + 1;
if iv[16] <= 0xFF then return; end
iv[16] = AND(iv[16], 0xFF);
iv[15] = iv[15] + 1;
if iv[15] <= 0xFF then return; end
iv[15] = AND(iv[15], 0xFF);
iv[14] = iv[14] + 1;
if iv[14] <= 0xFF then return; end
iv[14] = AND(iv[14], 0xFF);
iv[13] = iv[13] + 1;
if iv[13] <= 0xFF then return; end
iv[13] = AND(iv[13], 0xFF);
iv[12] = iv[12] + 1;
if iv[12] <= 0xFF then return; end
iv[12] = AND(iv[12], 0xFF);
iv[11] = iv[11] + 1;
if iv[11] <= 0xFF then return; end
iv[11] = AND(iv[11], 0xFF);
iv[10] = iv[10] + 1;
if iv[10] <= 0xFF then return; end
iv[10] = AND(iv[10], 0xFF);
iv[9] = iv[9] + 1;
if iv[9] <= 0xFF then return; end
iv[9] = AND(iv[9], 0xFF);
return;
end
public.update = function(messageStream)
local byte = messageStream();
while (byte ~= nil) do
inputQueue.push(byte);
if(inputQueue.size() >= blockCipher.blockSize) then
local block = Array.readFromQueue(inputQueue, blockCipher.blockSize);
if(iv == nil) then
iv = block;
else
local out = iv;
out = blockCipher.encrypt(key, out);
out = Array.XOR(out, block);
Array.writeToQueue(outputQueue, out);
updateIV();
end
end
byte = messageStream();
end
return public;
end
public.finish = function()
local paddingStream = padding(blockCipher.blockSize, inputQueue.getHead());
public.update(paddingStream);
return public;
end
public.getOutputQueue = function()
return outputQueue;
end
public.asHex = function()
return Stream.toHex(outputQueue.pop);
end
public.asBytes = function()
return Stream.toArray(outputQueue.pop);
end
return public;
end
return CTR;

View File

@@ -1,164 +0,0 @@
local Array = require("lockbox.util.array");
local Stream = require("lockbox.util.stream");
local Queue = require("lockbox.util.queue");
local OFB = {};
OFB.Cipher = function()
local public = {};
local key;
local blockCipher;
local padding;
local inputQueue;
local outputQueue;
local iv;
public.setKey = function(keyBytes)
key = keyBytes;
return public;
end
public.setBlockCipher = function(cipher)
blockCipher = cipher;
return public;
end
public.setPadding = function(paddingMode)
padding = paddingMode;
return public;
end
public.init = function()
inputQueue = Queue();
outputQueue = Queue();
iv = nil;
return public;
end
public.update = function(messageStream)
local byte = messageStream();
while (byte ~= nil) do
inputQueue.push(byte);
if(inputQueue.size() >= blockCipher.blockSize) then
local block = Array.readFromQueue(inputQueue, blockCipher.blockSize);
if(iv == nil) then
iv = block;
else
local out = iv;
out = blockCipher.encrypt(key, out);
iv = out;
out = Array.XOR(out, block);
Array.writeToQueue(outputQueue, out);
end
end
byte = messageStream();
end
return public;
end
public.finish = function()
local paddingStream = padding(blockCipher.blockSize, inputQueue.getHead());
public.update(paddingStream);
return public;
end
public.getOutputQueue = function()
return outputQueue;
end
public.asHex = function()
return Stream.toHex(outputQueue.pop);
end
public.asBytes = function()
return Stream.toArray(outputQueue.pop);
end
return public;
end
OFB.Decipher = function()
local public = {};
local key;
local blockCipher;
local padding;
local inputQueue;
local outputQueue;
local iv;
public.setKey = function(keyBytes)
key = keyBytes;
return public;
end
public.setBlockCipher = function(cipher)
blockCipher = cipher;
return public;
end
public.setPadding = function(paddingMode)
padding = paddingMode;
return public;
end
public.init = function()
inputQueue = Queue();
outputQueue = Queue();
iv = nil;
return public;
end
public.update = function(messageStream)
local byte = messageStream();
while (byte ~= nil) do
inputQueue.push(byte);
if(inputQueue.size() >= blockCipher.blockSize) then
local block = Array.readFromQueue(inputQueue, blockCipher.blockSize);
if(iv == nil) then
iv = block;
else
local out = iv;
out = blockCipher.encrypt(key, out);
iv = out;
out = Array.XOR(out, block);
Array.writeToQueue(outputQueue, out);
end
end
byte = messageStream();
end
return public;
end
public.finish = function()
local paddingStream = padding(blockCipher.blockSize, inputQueue.getHead());
public.update(paddingStream);
return public;
end
public.getOutputQueue = function()
return outputQueue;
end
public.asHex = function()
return Stream.toHex(outputQueue.pop);
end
public.asBytes = function()
return Stream.toArray(outputQueue.pop);
end
return public;
end
return OFB;

View File

@@ -1,173 +0,0 @@
require("lockbox").insecure();
local Bit = require("lockbox.util.bit");
local String = require("string");
local Math = require("math");
local Queue = require("lockbox.util.queue");
local AND = Bit.band;
local OR = Bit.bor;
local XOR = Bit.bxor;
local LROT = Bit.lrotate;
local LSHIFT = Bit.lshift;
local RSHIFT = Bit.rshift;
--SHA1 is big-endian
local bytes2word = function(b0, b1, b2, b3)
local i = b0; i = LSHIFT(i, 8);
i = OR(i, b1); i = LSHIFT(i, 8);
i = OR(i, b2); i = LSHIFT(i, 8);
i = OR(i, b3);
return i;
end
local word2bytes = function(word)
local b0, b1, b2, b3;
b3 = AND(word, 0xFF); word = RSHIFT(word, 8);
b2 = AND(word, 0xFF); word = RSHIFT(word, 8);
b1 = AND(word, 0xFF); word = RSHIFT(word, 8);
b0 = AND(word, 0xFF);
return b0, b1, b2, b3;
end
local dword2bytes = function(i)
local b4, b5, b6, b7 = word2bytes(i);
local b0, b1, b2, b3 = word2bytes(Math.floor(i / 0x100000000));
return b0, b1, b2, b3, b4, b5, b6, b7;
end
local F = function(x, y, z) return XOR(z, AND(x, XOR(y, z))); end
local G = function(x, y, z) return XOR(x, XOR(y, z)); end
local H = function(x, y, z) return OR(AND(x, OR(y, z)), AND(y, z)); end
local I = function(x, y, z) return XOR(x, XOR(y, z)); end
local SHA1 = function()
local queue = Queue();
local h0 = 0x67452301;
local h1 = 0xEFCDAB89;
local h2 = 0x98BADCFE;
local h3 = 0x10325476;
local h4 = 0xC3D2E1F0;
local public = {};
local processBlock = function()
local a = h0;
local b = h1;
local c = h2;
local d = h3;
local e = h4;
local temp;
local k;
local w = {};
for i = 0, 15 do
w[i] = bytes2word(queue.pop(), queue.pop(), queue.pop(), queue.pop());
end
for i = 16, 79 do
w[i] = LROT((XOR(XOR(w[i - 3], w[i - 8]), XOR(w[i - 14], w[i - 16]))), 1);
end
for i = 0, 79 do
if (i <= 19) then
temp = F(b, c, d);
k = 0x5A827999;
elseif (i <= 39) then
temp = G(b, c, d);
k = 0x6ED9EBA1;
elseif (i <= 59) then
temp = H(b, c, d);
k = 0x8F1BBCDC;
else
temp = I(b, c, d);
k = 0xCA62C1D6;
end
temp = LROT(a, 5) + temp + e + k + w[i];
e = d;
d = c;
c = LROT(b, 30);
b = a;
a = temp;
end
h0 = AND(h0 + a, 0xFFFFFFFF);
h1 = AND(h1 + b, 0xFFFFFFFF);
h2 = AND(h2 + c, 0xFFFFFFFF);
h3 = AND(h3 + d, 0xFFFFFFFF);
h4 = AND(h4 + e, 0xFFFFFFFF);
end
public.init = function()
queue.reset();
h0 = 0x67452301;
h1 = 0xEFCDAB89;
h2 = 0x98BADCFE;
h3 = 0x10325476;
h4 = 0xC3D2E1F0;
return public;
end
public.update = function(bytes)
for b in bytes do
queue.push(b);
if queue.size() >= 64 then processBlock(); end
end
return public;
end
public.finish = function()
local bits = queue.getHead() * 8;
queue.push(0x80);
while ((queue.size() + 7) % 64) < 63 do
queue.push(0x00);
end
local b0, b1, b2, b3, b4, b5, b6, b7 = dword2bytes(bits);
queue.push(b0);
queue.push(b1);
queue.push(b2);
queue.push(b3);
queue.push(b4);
queue.push(b5);
queue.push(b6);
queue.push(b7);
while queue.size() > 0 do
processBlock();
end
return public;
end
public.asBytes = function()
local b0, b1, b2, b3 = word2bytes(h0);
local b4, b5, b6, b7 = word2bytes(h1);
local b8, b9, b10, b11 = word2bytes(h2);
local b12, b13, b14, b15 = word2bytes(h3);
local b16, b17, b18, b19 = word2bytes(h4);
return {b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15, b16, b17, b18, b19};
end
public.asHex = function()
local b0, b1, b2, b3 = word2bytes(h0);
local b4, b5, b6, b7 = word2bytes(h1);
local b8, b9, b10, b11 = word2bytes(h2);
local b12, b13, b14, b15 = word2bytes(h3);
local b16, b17, b18, b19 = word2bytes(h4);
return String.format("%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15, b16, b17, b18, b19);
end
return public;
end
return SHA1;

View File

@@ -1,200 +0,0 @@
local Bit = require("lockbox.util.bit");
local String = require("string");
local Math = require("math");
local Queue = require("lockbox.util.queue");
local CONSTANTS = {
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 };
local fmt = "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x" ..
"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x"
local AND = Bit.band;
local OR = Bit.bor;
local NOT = Bit.bnot;
local XOR = Bit.bxor;
local RROT = Bit.rrotate;
local LSHIFT = Bit.lshift;
local RSHIFT = Bit.rshift;
--SHA2 is big-endian
local bytes2word = function(b0, b1, b2, b3)
local i = b0; i = LSHIFT(i, 8);
i = OR(i, b1); i = LSHIFT(i, 8);
i = OR(i, b2); i = LSHIFT(i, 8);
i = OR(i, b3);
return i;
end
local word2bytes = function(word)
local b0, b1, b2, b3;
b3 = AND(word, 0xFF); word = RSHIFT(word, 8);
b2 = AND(word, 0xFF); word = RSHIFT(word, 8);
b1 = AND(word, 0xFF); word = RSHIFT(word, 8);
b0 = AND(word, 0xFF);
return b0, b1, b2, b3;
end
local dword2bytes = function(i)
local b4, b5, b6, b7 = word2bytes(i);
local b0, b1, b2, b3 = word2bytes(Math.floor(i / 0x100000000));
return b0, b1, b2, b3, b4, b5, b6, b7;
end
local SHA2_224 = function()
local queue = Queue();
local h0 = 0xc1059ed8;
local h1 = 0x367cd507;
local h2 = 0x3070dd17;
local h3 = 0xf70e5939;
local h4 = 0xffc00b31;
local h5 = 0x68581511;
local h6 = 0x64f98fa7;
local h7 = 0xbefa4fa4;
local public = {};
local processBlock = function()
local a = h0;
local b = h1;
local c = h2;
local d = h3;
local e = h4;
local f = h5;
local g = h6;
local h = h7;
local w = {};
for i = 0, 15 do
w[i] = bytes2word(queue.pop(), queue.pop(), queue.pop(), queue.pop());
end
for i = 16, 63 do
local s0 = XOR(RROT(w[i - 15], 7), XOR(RROT(w[i - 15], 18), RSHIFT(w[i - 15], 3)));
local s1 = XOR(RROT(w[i - 2], 17), XOR(RROT(w[i - 2], 19), RSHIFT(w[i - 2], 10)));
w[i] = AND(w[i - 16] + s0 + w[i - 7] + s1, 0xFFFFFFFF);
end
for i = 0, 63 do
local s1 = XOR(RROT(e, 6), XOR(RROT(e, 11), RROT(e, 25)));
local ch = XOR(AND(e, f), AND(NOT(e), g));
local temp1 = h + s1 + ch + CONSTANTS[i + 1] + w[i];
local s0 = XOR(RROT(a, 2), XOR(RROT(a, 13), RROT(a, 22)));
local maj = XOR(AND(a, b), XOR(AND(a, c), AND(b, c)));
local temp2 = s0 + maj;
h = g;
g = f;
f = e;
e = d + temp1;
d = c;
c = b;
b = a;
a = temp1 + temp2;
end
h0 = AND(h0 + a, 0xFFFFFFFF);
h1 = AND(h1 + b, 0xFFFFFFFF);
h2 = AND(h2 + c, 0xFFFFFFFF);
h3 = AND(h3 + d, 0xFFFFFFFF);
h4 = AND(h4 + e, 0xFFFFFFFF);
h5 = AND(h5 + f, 0xFFFFFFFF);
h6 = AND(h6 + g, 0xFFFFFFFF);
h7 = AND(h7 + h, 0xFFFFFFFF);
end
public.init = function()
queue.reset();
h0 = 0xc1059ed8;
h1 = 0x367cd507;
h2 = 0x3070dd17;
h3 = 0xf70e5939;
h4 = 0xffc00b31;
h5 = 0x68581511;
h6 = 0x64f98fa7;
h7 = 0xbefa4fa4;
return public;
end
public.update = function(bytes)
for b in bytes do
queue.push(b);
if queue.size() >= 64 then processBlock(); end
end
return public;
end
public.finish = function()
local bits = queue.getHead() * 8;
queue.push(0x80);
while ((queue.size() + 7) % 64) < 63 do
queue.push(0x00);
end
local b0, b1, b2, b3, b4, b5, b6, b7 = dword2bytes(bits);
queue.push(b0);
queue.push(b1);
queue.push(b2);
queue.push(b3);
queue.push(b4);
queue.push(b5);
queue.push(b6);
queue.push(b7);
while queue.size() > 0 do
processBlock();
end
return public;
end
public.asBytes = function()
local b0, b1, b2, b3 = word2bytes(h0);
local b4, b5, b6, b7 = word2bytes(h1);
local b8, b9, b10, b11 = word2bytes(h2);
local b12, b13, b14, b15 = word2bytes(h3);
local b16, b17, b18, b19 = word2bytes(h4);
local b20, b21, b22, b23 = word2bytes(h5);
local b24, b25, b26, b27 = word2bytes(h6);
return { b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15
, b16, b17, b18, b19, b20, b21, b22, b23, b24, b25, b26, b27};
end
public.asHex = function()
local b0, b1, b2, b3 = word2bytes(h0);
local b4, b5, b6, b7 = word2bytes(h1);
local b8, b9, b10, b11 = word2bytes(h2);
local b12, b13, b14, b15 = word2bytes(h3);
local b16, b17, b18, b19 = word2bytes(h4);
local b20, b21, b22, b23 = word2bytes(h5);
local b24, b25, b26, b27 = word2bytes(h6);
return String.format(fmt, b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15
, b16, b17, b18, b19, b20, b21, b22, b23, b24, b25, b26, b27);
end
return public;
end
return SHA2_224;

View File

@@ -1,203 +0,0 @@
local Bit = require("lockbox.util.bit");
local String = require("string");
local Math = require("math");
local Queue = require("lockbox.util.queue");
local CONSTANTS = {
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 };
local fmt = "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x" ..
"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x"
local AND = Bit.band;
local OR = Bit.bor;
local NOT = Bit.bnot;
local XOR = Bit.bxor;
local RROT = Bit.rrotate;
local LSHIFT = Bit.lshift;
local RSHIFT = Bit.rshift;
--SHA2 is big-endian
local bytes2word = function(b0, b1, b2, b3)
local i = b0; i = LSHIFT(i, 8);
i = OR(i, b1); i = LSHIFT(i, 8);
i = OR(i, b2); i = LSHIFT(i, 8);
i = OR(i, b3);
return i;
end
local word2bytes = function(word)
local b0, b1, b2, b3;
b3 = AND(word, 0xFF); word = RSHIFT(word, 8);
b2 = AND(word, 0xFF); word = RSHIFT(word, 8);
b1 = AND(word, 0xFF); word = RSHIFT(word, 8);
b0 = AND(word, 0xFF);
return b0, b1, b2, b3;
end
local dword2bytes = function(i)
local b4, b5, b6, b7 = word2bytes(i);
local b0, b1, b2, b3 = word2bytes(Math.floor(i / 0x100000000));
return b0, b1, b2, b3, b4, b5, b6, b7;
end
local SHA2_256 = function()
local queue = Queue();
local h0 = 0x6a09e667;
local h1 = 0xbb67ae85;
local h2 = 0x3c6ef372;
local h3 = 0xa54ff53a;
local h4 = 0x510e527f;
local h5 = 0x9b05688c;
local h6 = 0x1f83d9ab;
local h7 = 0x5be0cd19;
local public = {};
local processBlock = function()
local a = h0;
local b = h1;
local c = h2;
local d = h3;
local e = h4;
local f = h5;
local g = h6;
local h = h7;
local w = {};
for i = 0, 15 do
w[i] = bytes2word(queue.pop(), queue.pop(), queue.pop(), queue.pop());
end
for i = 16, 63 do
local s0 = XOR(RROT(w[i - 15], 7), XOR(RROT(w[i - 15], 18), RSHIFT(w[i - 15], 3)));
local s1 = XOR(RROT(w[i - 2], 17), XOR(RROT(w[i - 2], 19), RSHIFT(w[i - 2], 10)));
w[i] = AND(w[i - 16] + s0 + w[i - 7] + s1, 0xFFFFFFFF);
end
for i = 0, 63 do
local s1 = XOR(RROT(e, 6), XOR(RROT(e, 11), RROT(e, 25)));
local ch = XOR(AND(e, f), AND(NOT(e), g));
local temp1 = h + s1 + ch + CONSTANTS[i + 1] + w[i];
local s0 = XOR(RROT(a, 2), XOR(RROT(a, 13), RROT(a, 22)));
local maj = XOR(AND(a, b), XOR(AND(a, c), AND(b, c)));
local temp2 = s0 + maj;
h = g;
g = f;
f = e;
e = d + temp1;
d = c;
c = b;
b = a;
a = temp1 + temp2;
end
h0 = AND(h0 + a, 0xFFFFFFFF);
h1 = AND(h1 + b, 0xFFFFFFFF);
h2 = AND(h2 + c, 0xFFFFFFFF);
h3 = AND(h3 + d, 0xFFFFFFFF);
h4 = AND(h4 + e, 0xFFFFFFFF);
h5 = AND(h5 + f, 0xFFFFFFFF);
h6 = AND(h6 + g, 0xFFFFFFFF);
h7 = AND(h7 + h, 0xFFFFFFFF);
end
public.init = function()
queue.reset();
h0 = 0x6a09e667;
h1 = 0xbb67ae85;
h2 = 0x3c6ef372;
h3 = 0xa54ff53a;
h4 = 0x510e527f;
h5 = 0x9b05688c;
h6 = 0x1f83d9ab;
h7 = 0x5be0cd19;
return public;
end
public.update = function(bytes)
for b in bytes do
queue.push(b);
if queue.size() >= 64 then processBlock(); end
end
return public;
end
public.finish = function()
local bits = queue.getHead() * 8;
queue.push(0x80);
while ((queue.size() + 7) % 64) < 63 do
queue.push(0x00);
end
local b0, b1, b2, b3, b4, b5, b6, b7 = dword2bytes(bits);
queue.push(b0);
queue.push(b1);
queue.push(b2);
queue.push(b3);
queue.push(b4);
queue.push(b5);
queue.push(b6);
queue.push(b7);
while queue.size() > 0 do
processBlock();
end
return public;
end
public.asBytes = function()
local b0, b1, b2, b3 = word2bytes(h0);
local b4, b5, b6, b7 = word2bytes(h1);
local b8, b9, b10, b11 = word2bytes(h2);
local b12, b13, b14, b15 = word2bytes(h3);
local b16, b17, b18, b19 = word2bytes(h4);
local b20, b21, b22, b23 = word2bytes(h5);
local b24, b25, b26, b27 = word2bytes(h6);
local b28, b29, b30, b31 = word2bytes(h7);
return { b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15
, b16, b17, b18, b19, b20, b21, b22, b23, b24, b25, b26, b27, b28, b29, b30, b31};
end
public.asHex = function()
local b0, b1, b2, b3 = word2bytes(h0);
local b4, b5, b6, b7 = word2bytes(h1);
local b8, b9, b10, b11 = word2bytes(h2);
local b12, b13, b14, b15 = word2bytes(h3);
local b16, b17, b18, b19 = word2bytes(h4);
local b20, b21, b22, b23 = word2bytes(h5);
local b24, b25, b26, b27 = word2bytes(h6);
local b28, b29, b30, b31 = word2bytes(h7);
return String.format(fmt, b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15
, b16, b17, b18, b19, b20, b21, b22, b23, b24, b25, b26, b27, b28, b29, b30, b31);
end
return public;
end
return SHA2_256;

View File

@@ -1,22 +0,0 @@
local Lockbox = {};
--[[
package.path = "./?.lua;"
.. "./cipher/?.lua;"
.. "./digest/?.lua;"
.. "./kdf/?.lua;"
.. "./mac/?.lua;"
.. "./padding/?.lua;"
.. "./test/?.lua;"
.. "./util/?.lua;"
.. package.path;
--]]
Lockbox.ALLOW_INSECURE = true;
Lockbox.insecure = function()
assert(Lockbox.ALLOW_INSECURE,
"This module is insecure! It should not be used in production." ..
"If you really want to use it, set Lockbox.ALLOW_INSECURE to true before importing it");
end
return Lockbox;

View File

@@ -1,114 +0,0 @@
local Bit = require("lockbox.util.bit");
local Array = require("lockbox.util.array");
local Stream = require("lockbox.util.stream");
local Math = require("math");
local AND = Bit.band;
local RSHIFT = Bit.rshift;
local word2bytes = function(word)
local b0, b1, b2, b3;
b3 = AND(word, 0xFF); word = RSHIFT(word, 8);
b2 = AND(word, 0xFF); word = RSHIFT(word, 8);
b1 = AND(word, 0xFF); word = RSHIFT(word, 8);
b0 = AND(word, 0xFF);
return b0, b1, b2, b3;
end
local PBKDF2 = function()
local public = {};
local blockLen = 16;
local dKeyLen = 256;
local iterations = 4096;
local salt;
local password;
local PRF;
local dKey;
public.setBlockLen = function(len)
blockLen = len;
return public;
end
public.setDKeyLen = function(len)
dKeyLen = len
return public;
end
public.setIterations = function(iter)
iterations = iter;
return public;
end
public.setSalt = function(saltBytes)
salt = saltBytes;
return public;
end
public.setPassword = function(passwordBytes)
password = passwordBytes;
return public;
end
public.setPRF = function(prf)
PRF = prf;
return public;
end
local buildBlock = function(i)
local b0, b1, b2, b3 = word2bytes(i);
local ii = {b0, b1, b2, b3};
local s = Array.concat(salt, ii);
local out = {};
PRF.setKey(password);
for c = 1, iterations do
PRF.init()
.update(Stream.fromArray(s));
s = PRF.finish().asBytes();
if(c > 1) then
out = Array.XOR(out, s);
else
out = s;
end
end
return out;
end
public.finish = function()
local blocks = Math.ceil(dKeyLen / blockLen);
dKey = {};
for b = 1, blocks do
local block = buildBlock(b);
dKey = Array.concat(dKey, block);
end
if(Array.size(dKey) > dKeyLen) then dKey = Array.truncate(dKey, dKeyLen); end
return public;
end
public.asBytes = function()
return dKey;
end
public.asHex = function()
return Array.toHex(dKey);
end
return public;
end
return PBKDF2;

View File

@@ -1,85 +0,0 @@
local Bit = require("lockbox.util.bit");
local Stream = require("lockbox.util.stream");
local Array = require("lockbox.util.array");
local XOR = Bit.bxor;
local HMAC = function()
local public = {};
local blockSize = 64;
local Digest = nil;
local outerPadding = {};
local innerPadding = {}
local digest;
public.setBlockSize = function(bytes)
blockSize = bytes;
return public;
end
public.setDigest = function(digestModule)
Digest = digestModule;
digest = Digest();
return public;
end
public.setKey = function(key)
local keyStream;
if(Array.size(key) > blockSize) then
keyStream = Stream.fromArray(Digest()
.update(Stream.fromArray(key))
.finish()
.asBytes());
else
keyStream = Stream.fromArray(key);
end
outerPadding = {};
innerPadding = {};
for i = 1, blockSize do
local byte = keyStream();
if byte == nil then byte = 0x00; end
outerPadding[i] = XOR(0x5C, byte);
innerPadding[i] = XOR(0x36, byte);
end
return public;
end
public.init = function()
digest.init()
.update(Stream.fromArray(innerPadding));
return public;
end
public.update = function(messageStream)
digest.update(messageStream);
return public;
end
public.finish = function()
local inner = digest.finish().asBytes();
digest.init()
.update(Stream.fromArray(outerPadding))
.update(Stream.fromArray(inner))
.finish();
return public;
end
public.asBytes = function()
return digest.asBytes();
end
public.asHex = function()
return digest.asHex();
end
return public;
end
return HMAC;

View File

@@ -1,22 +0,0 @@
local ANSIX923Padding = function(blockSize, byteCount)
local paddingCount = blockSize - (byteCount % blockSize);
local bytesLeft = paddingCount;
local stream = function()
if bytesLeft > 1 then
bytesLeft = bytesLeft - 1;
return 0x00;
elseif bytesLeft > 0 then
bytesLeft = bytesLeft - 1;
return paddingCount;
else
return nil;
end
end
return stream;
end
return ANSIX923Padding;

View File

@@ -1,22 +0,0 @@
local ISOIEC7816Padding = function(blockSize, byteCount)
local paddingCount = blockSize - (byteCount % blockSize);
local bytesLeft = paddingCount;
local stream = function()
if bytesLeft == paddingCount then
bytesLeft = bytesLeft - 1;
return 0x80;
elseif bytesLeft > 0 then
bytesLeft = bytesLeft - 1;
return 0x00;
else
return nil;
end
end
return stream;
end
return ISOIEC7816Padding;

View File

@@ -1,18 +0,0 @@
local PKCS7Padding = function(blockSize, byteCount)
local paddingCount = blockSize - ((byteCount -1) % blockSize) + 1;
local bytesLeft = paddingCount;
local stream = function()
if bytesLeft > 0 then
bytesLeft = bytesLeft - 1;
return paddingCount;
else
return nil;
end
end
return stream;
end
return PKCS7Padding;

View File

@@ -1,19 +0,0 @@
local ZeroPadding = function(blockSize, byteCount)
local paddingCount = blockSize - ((byteCount -1) % blockSize) + 1;
local bytesLeft = paddingCount;
local stream = function()
if bytesLeft > 0 then
bytesLeft = bytesLeft - 1;
return 0x00;
else
return nil;
end
end
return stream;
end
return ZeroPadding;

View File

@@ -1,211 +0,0 @@
local String = require("string");
local Bit = require("lockbox.util.bit");
local Queue = require("lockbox.util.queue");
local XOR = Bit.bxor;
local Array = {};
Array.size = function(array)
return #array;
end
Array.fromString = function(string)
local bytes = {};
local i = 1;
local byte = String.byte(string, i);
while byte ~= nil do
bytes[i] = byte;
i = i + 1;
byte = String.byte(string, i);
end
return bytes;
end
Array.toString = function(bytes)
local chars = {};
local i = 1;
local byte = bytes[i];
while byte ~= nil do
chars[i] = String.char(byte);
i = i + 1;
byte = bytes[i];
end
return table.concat(chars, "");
end
Array.fromStream = function(stream)
local array = {};
local i = 1;
local byte = stream();
while byte ~= nil do
array[i] = byte;
i = i + 1;
byte = stream();
end
return array;
end
Array.readFromQueue = function(queue, size)
local array = {};
for i = 1, size do
array[i] = queue.pop();
end
return array;
end
Array.writeToQueue = function(queue, array)
local size = Array.size(array);
for i = 1, size do
queue.push(array[i]);
end
end
Array.toStream = function(array)
local queue = Queue();
local i = 1;
local byte = array[i];
while byte ~= nil do
queue.push(byte);
i = i + 1;
byte = array[i];
end
return queue.pop;
end
local fromHexTable = {};
for i = 0, 255 do
fromHexTable[String.format("%02X", i)] = i;
fromHexTable[String.format("%02x", i)] = i;
end
Array.fromHex = function(hex)
local array = {};
for i = 1, String.len(hex) / 2 do
local h = String.sub(hex, i * 2 - 1, i * 2);
array[i] = fromHexTable[h];
end
return array;
end
local toHexTable = {};
for i = 0, 255 do
toHexTable[i] = String.format("%02X", i);
end
Array.toHex = function(array)
local hex = {};
local i = 1;
local byte = array[i];
while byte ~= nil do
hex[i] = toHexTable[byte];
i = i + 1;
byte = array[i];
end
return table.concat(hex, "");
end
Array.concat = function(a, b)
local concat = {};
local out = 1;
local i = 1;
local byte = a[i];
while byte ~= nil do
concat[out] = byte;
i = i + 1;
out = out + 1;
byte = a[i];
end
i = 1;
byte = b[i];
while byte ~= nil do
concat[out] = byte;
i = i + 1;
out = out + 1;
byte = b[i];
end
return concat;
end
Array.truncate = function(a, newSize)
local x = {};
for i = 1, newSize do
x[i] = a[i];
end
return x;
end
Array.XOR = function(a, b)
local x = {};
for k, v in pairs(a) do
x[k] = XOR(v, b[k]);
end
return x;
end
Array.substitute = function(input, sbox)
local out = {};
for k, v in pairs(input) do
out[k] = sbox[v];
end
return out;
end
Array.permute = function(input, pbox)
local out = {};
for k, v in pairs(pbox) do
out[k] = input[v];
end
return out;
end
Array.copy = function(input)
local out = {};
for k, v in pairs(input) do
out[k] = v;
end
return out;
end
Array.slice = function(input, start, stop)
local out = {};
for i = start, stop do
out[i - start + 1] = input[i];
end
return out;
end
return Array;

View File

@@ -1,25 +0,0 @@
local ok, e
ok = nil
if not ok then
ok, e = pcall(require, "bit") -- the LuaJIT one ?
end
if not ok then
ok, e = pcall(require, "bit32") -- Lua 5.2
end
if not ok then
ok, e = pcall(require, "bit.numberlua") -- for Lua 5.1, https://github.com/tst2005/lua-bit-numberlua/
end
if not ok then
error("no bitwise support found", 2)
end
assert(type(e) == "table", "invalid bit module")
-- Workaround to support Lua 5.2 bit32 API with the LuaJIT bit one
if e.rol and not e.lrotate then
e.lrotate = e.rol
end
if e.ror and not e.rrotate then
e.rrotate = e.ror
end
return e

View File

@@ -1,47 +0,0 @@
local Queue = function()
local queue = {};
local tail = 0;
local head = 0;
local public = {};
public.push = function(obj)
queue[head] = obj;
head = head + 1;
return;
end
public.pop = function()
if tail < head
then
local obj = queue[tail];
queue[tail] = nil;
tail = tail + 1;
return obj;
else
return nil;
end
end
public.size = function()
return head - tail;
end
public.getHead = function()
return head;
end
public.getTail = function()
return tail;
end
public.reset = function()
queue = {};
head = 0;
tail = 0;
end
return public;
end
return Queue;

View File

@@ -1,99 +0,0 @@
local Queue = require("lockbox.util.queue");
local String = require("string");
local Stream = {};
Stream.fromString = function(string)
local i = 0;
return function()
i = i + 1;
return String.byte(string, i);
end
end
Stream.toString = function(stream)
local array = {};
local i = 1;
local byte = stream();
while byte ~= nil do
array[i] = String.char(byte);
i = i + 1;
byte = stream();
end
return table.concat(array);
end
Stream.fromArray = function(array)
local queue = Queue();
local i = 1;
local byte = array[i];
while byte ~= nil do
queue.push(byte);
i = i + 1;
byte = array[i];
end
return queue.pop;
end
Stream.toArray = function(stream)
local array = {};
local i = 1;
local byte = stream();
while byte ~= nil do
array[i] = byte;
i = i + 1;
byte = stream();
end
return array;
end
local fromHexTable = {};
for i = 0, 255 do
fromHexTable[String.format("%02X", i)] = i;
fromHexTable[String.format("%02x", i)] = i;
end
Stream.fromHex = function(hex)
local queue = Queue();
for i = 1, String.len(hex) / 2 do
local h = String.sub(hex, i * 2 - 1, i * 2);
queue.push(fromHexTable[h]);
end
return queue.pop;
end
local toHexTable = {};
for i = 0, 255 do
toHexTable[i] = String.format("%02X", i);
end
Stream.toHex = function(stream)
local hex = {};
local i = 1;
local byte = stream();
while byte ~= nil do
hex[i] = toHexTable[byte];
i = i + 1;
byte = stream();
end
return table.concat(hex);
end
return Stream;

View File

@@ -3,7 +3,7 @@ local config = {}
-- set to false to run in offline mode (safety regulation only)
config.NETWORKED = true
-- unique reactor ID
config.REACTOR_ID = 1
config.REACTOR_ID = 1
-- port to send packets TO server
config.SERVER_PORT = 16000
-- port to listen to incoming packets FROM server

View File

@@ -1,8 +1,8 @@
local comms = require("scada-common.comms")
local log = require("scada-common.log")
local ppm = require("scada-common.ppm")
local log = require("scada-common.log")
local ppm = require("scada-common.ppm")
local types = require("scada-common.types")
local util = require("scada-common.util")
local util = require("scada-common.util")
local plc = {}
@@ -23,7 +23,7 @@ local println_ts = util.println_ts
--- identifies dangerous states and SCRAMs reactor if warranted
---
--- autonomous from main SCADA supervisor/coordinator control
function plc.rps_init(reactor)
plc.rps_init = function (reactor)
local state_keys = {
dmg_crit = 1,
high_temp = 2,
@@ -41,7 +41,7 @@ function plc.rps_init(reactor)
state = { false, false, false, false, false, false, false, false, false },
reactor_enabled = false,
tripped = false,
trip_cause = "" ---@type rps_trip_cause
trip_cause = ""
}
---@class rps
@@ -50,19 +50,19 @@ function plc.rps_init(reactor)
-- PRIVATE FUNCTIONS --
-- set reactor access fault flag
local function _set_fault()
local _set_fault = function ()
if self.reactor.__p_last_fault() ~= "Terminated" then
self.state[state_keys.fault] = true
end
end
-- clear reactor access fault flag
local function _clear_fault()
local _clear_fault = function ()
self.state[state_keys.fault] = false
end
-- check for critical damage
local function _damage_critical()
local _damage_critical = function ()
local damage_percent = self.reactor.getDamagePercent()
if damage_percent == ppm.ACCESS_FAULT then
-- lost the peripheral or terminated, handled later
@@ -75,7 +75,7 @@ function plc.rps_init(reactor)
end
-- check if the reactor is at a critically high temperature
local function _high_temp()
local _high_temp = function ()
-- mekanism: MAX_DAMAGE_TEMPERATURE = 1_200
local temp = self.reactor.getTemperature()
if temp == ppm.ACCESS_FAULT then
@@ -89,7 +89,7 @@ function plc.rps_init(reactor)
end
-- check if there is no coolant (<2% filled)
local function _no_coolant()
local _no_coolant = function ()
local coolant_filled = self.reactor.getCoolantFilledPercentage()
if coolant_filled == ppm.ACCESS_FAULT then
-- lost the peripheral or terminated, handled later
@@ -102,7 +102,7 @@ function plc.rps_init(reactor)
end
-- check for excess waste (>80% filled)
local function _excess_waste()
local _excess_waste = function ()
local w_filled = self.reactor.getWasteFilledPercentage()
if w_filled == ppm.ACCESS_FAULT then
-- lost the peripheral or terminated, handled later
@@ -115,7 +115,7 @@ function plc.rps_init(reactor)
end
-- check for heated coolant backup (>95% filled)
local function _excess_heated_coolant()
local _excess_heated_coolant = function ()
local hc_filled = self.reactor.getHeatedCoolantFilledPercentage()
if hc_filled == ppm.ACCESS_FAULT then
-- lost the peripheral or terminated, handled later
@@ -128,7 +128,7 @@ function plc.rps_init(reactor)
end
-- check if there is no fuel
local function _insufficient_fuel()
local _insufficient_fuel = function ()
local fuel = self.reactor.getFuel()
if fuel == ppm.ACCESS_FAULT then
-- lost the peripheral or terminated, handled later
@@ -144,28 +144,28 @@ function plc.rps_init(reactor)
-- re-link a reactor after a peripheral re-connect
---@diagnostic disable-next-line: redefined-local
function public.reconnect_reactor(reactor)
public.reconnect_reactor = function (reactor)
self.reactor = reactor
end
-- trip for lost peripheral
function public.trip_fault()
public.trip_fault = function ()
_set_fault()
end
-- trip for a PLC comms timeout
function public.trip_timeout()
public.trip_timeout = function ()
self.state[state_keys.timeout] = true
end
-- manually SCRAM the reactor
function public.trip_manual()
public.trip_manual = function ()
self.state[state_keys.manual] = true
end
-- SCRAM the reactor now
---@return boolean success
function public.scram()
public.scram = function ()
log.info("RPS: reactor SCRAM")
self.reactor.scram()
@@ -180,7 +180,7 @@ function plc.rps_init(reactor)
-- start the reactor
---@return boolean success
function public.activate()
public.activate = function ()
if not self.tripped then
log.info("RPS: reactor start")
@@ -198,7 +198,7 @@ function plc.rps_init(reactor)
-- check all safety conditions
---@return boolean tripped, rps_status_t trip_status, boolean first_trip
function public.check()
public.check = function ()
local status = rps_status_t.ok
local was_tripped = self.tripped
local first_trip = false
@@ -259,12 +259,12 @@ function plc.rps_init(reactor)
return self.tripped, status, first_trip
end
function public.status() return self.state end
function public.is_tripped() return self.tripped end
function public.is_active() return self.reactor_enabled end
public.status = function () return self.state end
public.is_tripped = function () return self.tripped end
public.is_active = function () return self.reactor_enabled end
-- reset the RPS
function public.reset()
public.reset = function ()
self.tripped = false
self.trip_cause = rps_status_t.ok
@@ -285,7 +285,7 @@ end
---@param reactor table
---@param rps rps
---@param conn_watchdog watchdog
function plc.comms(id, version, modem, local_port, server_port, reactor, rps, conn_watchdog)
plc.comms = function (id, version, modem, local_port, server_port, reactor, rps, conn_watchdog)
local self = {
id = id,
version = version,
@@ -316,7 +316,7 @@ function plc.comms(id, version, modem, local_port, server_port, reactor, rps, co
-- send an RPLC packet
---@param msg_type RPLC_TYPES
---@param msg string
local function _send(msg_type, msg)
local _send = function (msg_type, msg)
local s_pkt = comms.scada_packet()
local r_pkt = comms.rplc_packet()
@@ -330,7 +330,7 @@ function plc.comms(id, version, modem, local_port, server_port, reactor, rps, co
-- send a SCADA management packet
---@param msg_type SCADA_MGMT_TYPES
---@param msg string
local function _send_mgmt(msg_type, msg)
local _send_mgmt = function (msg_type, msg)
local s_pkt = comms.scada_packet()
local m_pkt = comms.mgmt_packet()
@@ -343,9 +343,7 @@ function plc.comms(id, version, modem, local_port, server_port, reactor, rps, co
-- variable reactor status information, excluding heating rate
---@return table data_table, boolean faulted
local function _reactor_status()
local fuel = nil
local waste = nil
local _reactor_status = function ()
local coolant = nil
local hcoolant = nil
@@ -377,9 +375,9 @@ function plc.comms(id, version, modem, local_port, server_port, reactor, rps, co
function () data_table[5] = self.reactor.getDamagePercent() end,
function () data_table[6] = self.reactor.getBoilEfficiency() end,
function () data_table[7] = self.reactor.getEnvironmentalLoss() end,
function () fuel = self.reactor.getFuel() end,
function () data_table[8] = self.reactor.getFuel() end,
function () data_table[9] = self.reactor.getFuelFilledPercentage() end,
function () waste = self.reactor.getWaste() end,
function () data_table[10] = self.reactor.getWaste() end,
function () data_table[11] = self.reactor.getWasteFilledPercentage() end,
function () coolant = self.reactor.getCoolant() end,
function () data_table[14] = self.reactor.getCoolantFilledPercentage() end,
@@ -389,18 +387,6 @@ function plc.comms(id, version, modem, local_port, server_port, reactor, rps, co
parallel.waitForAll(table.unpack(tasks))
if type(fuel) == "table" then
data_table[8] = fuel.amount
elseif type(fuel) == "number" then
data_table[8] = fuel
end
if type(waste) == "table" then
data_table[10] = waste.amount
elseif type(waste) == "number" then
data_table[10] = waste
end
if coolant ~= nil then
data_table[12] = coolant.name
data_table[13] = coolant.amount
@@ -416,7 +402,7 @@ function plc.comms(id, version, modem, local_port, server_port, reactor, rps, co
-- update the status cache if changed
---@return boolean changed
local function _update_status_cache()
local _update_status_cache = function ()
local status, faulted = _reactor_status()
local changed = false
@@ -442,19 +428,19 @@ function plc.comms(id, version, modem, local_port, server_port, reactor, rps, co
-- keep alive ack
---@param srv_time integer
local function _send_keep_alive_ack(srv_time)
local _send_keep_alive_ack = function (srv_time)
_send_mgmt(SCADA_MGMT_TYPES.KEEP_ALIVE, { srv_time, util.time() })
end
-- general ack
---@param msg_type RPLC_TYPES
---@param succeeded boolean
local function _send_ack(msg_type, succeeded)
local _send_ack = function (msg_type, succeeded)
_send(msg_type, { succeeded })
end
-- send structure properties (these should not change, server will cache these)
local function _send_struct()
local _send_struct = function ()
local mek_data = { 0, 0, 0, 0, 0, 0, 0, 0 }
local tasks = {
@@ -482,7 +468,7 @@ function plc.comms(id, version, modem, local_port, server_port, reactor, rps, co
-- reconnect a newly connected modem
---@param modem table
---@diagnostic disable-next-line: redefined-local
function public.reconnect_modem(modem)
public.reconnect_modem = function (modem)
self.modem = modem
-- open modem
@@ -494,33 +480,33 @@ function plc.comms(id, version, modem, local_port, server_port, reactor, rps, co
-- reconnect a newly connected reactor
---@param reactor table
---@diagnostic disable-next-line: redefined-local
function public.reconnect_reactor(reactor)
public.reconnect_reactor = function (reactor)
self.reactor = reactor
self.status_cache = nil
end
-- unlink from the server
function public.unlink()
public.unlink = function ()
self.linked = false
self.r_seq_num = nil
self.status_cache = nil
end
-- close the connection to the server
function public.close()
public.close = function ()
self.conn_watchdog.cancel()
public.unlink()
_send_mgmt(SCADA_MGMT_TYPES.CLOSE, {})
end
-- attempt to establish link with supervisor
function public.send_link_req()
public.send_link_req = function ()
_send(RPLC_TYPES.LINK_REQ, { self.id, self.version })
end
-- send live status information
---@param degraded boolean
function public.send_status(degraded)
public.send_status = function (degraded)
if self.linked then
local mek_data = nil
@@ -531,7 +517,7 @@ function plc.comms(id, version, modem, local_port, server_port, reactor, rps, co
local sys_status = {
util.time(), -- timestamp
(not self.scrammed), -- requested control state
rps.is_tripped(), -- rps_tripped
rps.is_tripped(), -- overridden
degraded, -- degraded
self.reactor.getHeatingRate(), -- heating rate
mek_data -- mekanism status data
@@ -546,7 +532,7 @@ function plc.comms(id, version, modem, local_port, server_port, reactor, rps, co
end
-- send reactor protection system status
function public.send_rps_status()
public.send_rps_status = function ()
if self.linked then
_send(RPLC_TYPES.RPS_STATUS, rps.status())
end
@@ -554,7 +540,7 @@ function plc.comms(id, version, modem, local_port, server_port, reactor, rps, co
-- send reactor protection system alarm
---@param cause rps_status_t
function public.send_rps_alarm(cause)
public.send_rps_alarm = function (cause)
if self.linked then
local rps_alarm = {
cause,
@@ -572,7 +558,7 @@ function plc.comms(id, version, modem, local_port, server_port, reactor, rps, co
---@param message any
---@param distance integer
---@return rplc_frame|mgmt_frame|nil packet
function public.parse_packet(side, sender, reply_to, message, distance)
public.parse_packet = function(side, sender, reply_to, message, distance)
local pkt = nil
local s_pkt = comms.scada_packet()
@@ -604,7 +590,7 @@ function plc.comms(id, version, modem, local_port, server_port, reactor, rps, co
---@param packet rplc_frame|mgmt_frame
---@param plc_state plc_state
---@param setpoints setpoints
function public.handle_packet(packet, plc_state, setpoints)
public.handle_packet = function (packet, plc_state, setpoints)
if packet ~= nil then
-- check sequence number
if self.r_seq_num == nil then
@@ -752,7 +738,7 @@ function plc.comms(id, version, modem, local_port, server_port, reactor, rps, co
log.warning("PLC KEEP_ALIVE trip time > 500ms (" .. trip_time .. "ms)")
end
-- log.debug("RPLC RTT = " .. trip_time .. "ms")
-- log.debug("RPLC RTT = ".. trip_time .. "ms")
_send_keep_alive_ack(timestamp)
else
@@ -774,8 +760,8 @@ function plc.comms(id, version, modem, local_port, server_port, reactor, rps, co
end
end
function public.is_scrammed() return self.scrammed end
function public.is_linked() return self.linked end
public.is_scrammed = function () return self.scrammed end
public.is_linked = function () return self.linked end
return public
end

View File

@@ -4,40 +4,22 @@
require("/initenv").init_env()
local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue")
local ppm = require("scada-common.ppm")
local util = require("scada-common.util")
local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue")
local ppm = require("scada-common.ppm")
local util = require("scada-common.util")
local config = require("reactor-plc.config")
local plc = require("reactor-plc.plc")
local config = require("reactor-plc.config")
local plc = require("reactor-plc.plc")
local threads = require("reactor-plc.threads")
local R_PLC_VERSION = "beta-v0.8.2"
local R_PLC_VERSION = "alpha-v0.7.2"
local print = util.print
local println = util.println
local print_ts = util.print_ts
local println_ts = util.println_ts
----------------------------------------
-- config validation
----------------------------------------
local cfv = util.new_validator()
cfv.assert_type_bool(config.NETWORKED)
cfv.assert_type_int(config.REACTOR_ID)
cfv.assert_port(config.SERVER_PORT)
cfv.assert_port(config.LISTEN_PORT)
cfv.assert_type_str(config.LOG_PATH)
cfv.assert_type_int(config.LOG_MODE)
assert(cfv.valid(), "bad config file: missing/invalid fields")
----------------------------------------
-- log init
----------------------------------------
log.init(config.LOG_PATH, config.LOG_MODE)
log.info("========================================")
@@ -45,10 +27,6 @@ log.info("BOOTING reactor-plc.startup " .. R_PLC_VERSION)
log.info("========================================")
println(">> Reactor PLC " .. R_PLC_VERSION .. " <<")
----------------------------------------
-- startup
----------------------------------------
-- mount connected devices
ppm.mount_all()
@@ -124,7 +102,7 @@ if __shared_memory.networked and smem_dev.modem == nil then
end
-- PLC init
local function init()
local init = function ()
if plc_state.init_ok then
-- just booting up, no fission allowed (neutrons stay put thanks)
smem_dev.reactor.scram()

View File

@@ -1,7 +1,7 @@
local log = require("scada-common.log")
local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue")
local ppm = require("scada-common.ppm")
local util = require("scada-common.util")
local ppm = require("scada-common.ppm")
local util = require("scada-common.util")
local threads = {}
@@ -30,14 +30,14 @@ local MQ__COMM_CMD = {
-- main thread
---@param smem plc_shared_memory
---@param init function
function threads.thread__main(smem, init)
threads.thread__main = function (smem, init)
local public = {} ---@class thread
-- execute thread
function public.exec()
public.exec = function ()
log.debug("main thread init, clock inactive")
-- send status updates at 1Hz (every 20 server ticks) (every loop tick)
-- send status updates at 2Hz (every 10 server ticks) (every loop tick)
-- send link requests at 0.5Hz (every 40 server ticks) (every 4 loop ticks)
local LINK_TICKS = 4
local ticks_to_update = 0
@@ -55,7 +55,8 @@ function threads.thread__main(smem, init)
local plc_comms = smem.plc_sys.plc_comms
local conn_watchdog = smem.plc_sys.conn_watchdog
local event, param1, param2, param3, param4, param5 = util.pull_event()
---@diagnostic disable-next-line: undefined-field
local event, param1, param2, param3, param4, param5 = os.pullEventRaw()
-- handle event
if event == "timer" and loop_clock.is_clock(param1) then
@@ -188,7 +189,7 @@ function threads.thread__main(smem, init)
end
-- execute the thread in a protected mode, retrying it on return if not shutting down
function public.p_exec()
public.p_exec = function ()
local plc_state = smem.plc_state
while not plc_state.shutdown do
@@ -214,11 +215,11 @@ end
-- RPS operation thread
---@param smem plc_shared_memory
function threads.thread__rps(smem)
threads.thread__rps = function (smem)
local public = {} ---@class thread
-- execute thread
function public.exec()
public.exec = function ()
log.debug("rps thread start")
-- load in from shared memory
@@ -331,7 +332,7 @@ function threads.thread__rps(smem)
end
-- execute the thread in a protected mode, retrying it on return if not shutting down
function public.p_exec()
public.p_exec = function ()
local plc_state = smem.plc_state
while not plc_state.shutdown do
@@ -353,11 +354,11 @@ end
-- communications sender thread
---@param smem plc_shared_memory
function threads.thread__comms_tx(smem)
threads.thread__comms_tx = function (smem)
local public = {} ---@class thread
-- execute thread
function public.exec()
public.exec = function ()
log.debug("comms tx thread start")
-- load in from shared memory
@@ -406,7 +407,7 @@ function threads.thread__comms_tx(smem)
end
-- execute the thread in a protected mode, retrying it on return if not shutting down
function public.p_exec()
public.p_exec = function ()
local plc_state = smem.plc_state
while not plc_state.shutdown do
@@ -427,11 +428,11 @@ end
-- communications handler thread
---@param smem plc_shared_memory
function threads.thread__comms_rx(smem)
threads.thread__comms_rx = function (smem)
local public = {} ---@class thread
-- execute thread
function public.exec()
public.exec = function ()
log.debug("comms rx thread start")
-- load in from shared memory
@@ -480,7 +481,7 @@ function threads.thread__comms_rx(smem)
end
-- execute the thread in a protected mode, retrying it on return if not shutting down
function public.p_exec()
public.p_exec = function ()
local plc_state = smem.plc_state
while not plc_state.shutdown do
@@ -501,11 +502,11 @@ end
-- apply setpoints
---@param smem plc_shared_memory
function threads.thread__setpoint_control(smem)
threads.thread__setpoint_control = function (smem)
local public = {} ---@class thread
-- execute thread
function public.exec()
public.exec = function ()
log.debug("setpoint control thread start")
-- load in from shared memory
@@ -604,7 +605,7 @@ function threads.thread__setpoint_control(smem)
end
-- execute the thread in a protected mode, retrying it on return if not shutting down
function public.p_exec()
public.p_exec = function ()
local plc_state = smem.plc_state
while not plc_state.shutdown do

View File

@@ -15,12 +15,12 @@ config.LOG_MODE = 0
-- RTU peripheral devices (named: side/network device name)
config.RTU_DEVICES = {
{
name = "boilerValve_0",
name = "boiler_1",
index = 1,
for_reactor = 1
},
{
name = "turbineValve_0",
name = "turbine_1",
index = 1,
for_reactor = 1
}

View File

@@ -4,8 +4,11 @@ local boiler_rtu = {}
-- create new boiler (mek 10.0) device
---@param boiler table
function boiler_rtu.new(boiler)
local unit = rtu.init_unit()
boiler_rtu.new = function (boiler)
local self = {
rtu = rtu.init_unit(),
boiler = boiler
}
-- discrete inputs --
-- none
@@ -15,34 +18,34 @@ function boiler_rtu.new(boiler)
-- input registers --
-- build properties
unit.connect_input_reg(boiler.getBoilCapacity)
unit.connect_input_reg(boiler.getSteamCapacity)
unit.connect_input_reg(boiler.getWaterCapacity)
unit.connect_input_reg(boiler.getHeatedCoolantCapacity)
unit.connect_input_reg(boiler.getCooledCoolantCapacity)
unit.connect_input_reg(boiler.getSuperheaters)
unit.connect_input_reg(boiler.getMaxBoilRate)
self.rtu.connect_input_reg(self.boiler.getBoilCapacity)
self.rtu.connect_input_reg(self.boiler.getSteamCapacity)
self.rtu.connect_input_reg(self.boiler.getWaterCapacity)
self.rtu.connect_input_reg(self.boiler.getHeatedCoolantCapacity)
self.rtu.connect_input_reg(self.boiler.getCooledCoolantCapacity)
self.rtu.connect_input_reg(self.boiler.getSuperheaters)
self.rtu.connect_input_reg(self.boiler.getMaxBoilRate)
-- current state
unit.connect_input_reg(boiler.getTemperature)
unit.connect_input_reg(boiler.getBoilRate)
self.rtu.connect_input_reg(self.boiler.getTemperature)
self.rtu.connect_input_reg(self.boiler.getBoilRate)
-- tanks
unit.connect_input_reg(boiler.getSteam)
unit.connect_input_reg(boiler.getSteamNeeded)
unit.connect_input_reg(boiler.getSteamFilledPercentage)
unit.connect_input_reg(boiler.getWater)
unit.connect_input_reg(boiler.getWaterNeeded)
unit.connect_input_reg(boiler.getWaterFilledPercentage)
unit.connect_input_reg(boiler.getHeatedCoolant)
unit.connect_input_reg(boiler.getHeatedCoolantNeeded)
unit.connect_input_reg(boiler.getHeatedCoolantFilledPercentage)
unit.connect_input_reg(boiler.getCooledCoolant)
unit.connect_input_reg(boiler.getCooledCoolantNeeded)
unit.connect_input_reg(boiler.getCooledCoolantFilledPercentage)
self.rtu.connect_input_reg(self.boiler.getSteam)
self.rtu.connect_input_reg(self.boiler.getSteamNeeded)
self.rtu.connect_input_reg(self.boiler.getSteamFilledPercentage)
self.rtu.connect_input_reg(self.boiler.getWater)
self.rtu.connect_input_reg(self.boiler.getWaterNeeded)
self.rtu.connect_input_reg(self.boiler.getWaterFilledPercentage)
self.rtu.connect_input_reg(self.boiler.getHeatedCoolant)
self.rtu.connect_input_reg(self.boiler.getHeatedCoolantNeeded)
self.rtu.connect_input_reg(self.boiler.getHeatedCoolantFilledPercentage)
self.rtu.connect_input_reg(self.boiler.getCooledCoolant)
self.rtu.connect_input_reg(self.boiler.getCooledCoolantNeeded)
self.rtu.connect_input_reg(self.boiler.getCooledCoolantFilledPercentage)
-- holding registers --
-- none
return unit.interface()
return self.rtu.interface()
end
return boiler_rtu

View File

@@ -4,52 +4,55 @@ local boilerv_rtu = {}
-- create new boiler (mek 10.1+) device
---@param boiler table
function boilerv_rtu.new(boiler)
local unit = rtu.init_unit()
boilerv_rtu.new = function (boiler)
local self = {
rtu = rtu.init_unit(),
boiler = boiler
}
-- discrete inputs --
unit.connect_di(boiler.isFormed)
self.rtu.connect_di(self.boiler.isFormed)
-- coils --
-- none
-- input registers --
-- multiblock properties
unit.connect_input_reg(boiler.getLength)
unit.connect_input_reg(boiler.getWidth)
unit.connect_input_reg(boiler.getHeight)
unit.connect_input_reg(boiler.getMinPos)
unit.connect_input_reg(boiler.getMaxPos)
self.rtu.connect_input_reg(self.boiler.getLength)
self.rtu.connect_input_reg(self.boiler.getWidth)
self.rtu.connect_input_reg(self.boiler.getHeight)
self.rtu.connect_input_reg(self.boiler.getMinPos)
self.rtu.connect_input_reg(self.boiler.getMaxPos)
-- build properties
unit.connect_input_reg(boiler.getBoilCapacity)
unit.connect_input_reg(boiler.getSteamCapacity)
unit.connect_input_reg(boiler.getWaterCapacity)
unit.connect_input_reg(boiler.getHeatedCoolantCapacity)
unit.connect_input_reg(boiler.getCooledCoolantCapacity)
unit.connect_input_reg(boiler.getSuperheaters)
unit.connect_input_reg(boiler.getMaxBoilRate)
unit.connect_input_reg(boiler.getEnvironmentalLoss)
self.rtu.connect_input_reg(self.boiler.getBoilCapacity)
self.rtu.connect_input_reg(self.boiler.getSteamCapacity)
self.rtu.connect_input_reg(self.boiler.getWaterCapacity)
self.rtu.connect_input_reg(self.boiler.getHeatedCoolantCapacity)
self.rtu.connect_input_reg(self.boiler.getCooledCoolantCapacity)
self.rtu.connect_input_reg(self.boiler.getSuperheaters)
self.rtu.connect_input_reg(self.boiler.getMaxBoilRate)
self.rtu.connect_input_reg(self.boiler.getEnvironmentalLoss)
-- current state
unit.connect_input_reg(boiler.getTemperature)
unit.connect_input_reg(boiler.getBoilRate)
self.rtu.connect_input_reg(self.boiler.getTemperature)
self.rtu.connect_input_reg(self.boiler.getBoilRate)
-- tanks
unit.connect_input_reg(boiler.getSteam)
unit.connect_input_reg(boiler.getSteamNeeded)
unit.connect_input_reg(boiler.getSteamFilledPercentage)
unit.connect_input_reg(boiler.getWater)
unit.connect_input_reg(boiler.getWaterNeeded)
unit.connect_input_reg(boiler.getWaterFilledPercentage)
unit.connect_input_reg(boiler.getHeatedCoolant)
unit.connect_input_reg(boiler.getHeatedCoolantNeeded)
unit.connect_input_reg(boiler.getHeatedCoolantFilledPercentage)
unit.connect_input_reg(boiler.getCooledCoolant)
unit.connect_input_reg(boiler.getCooledCoolantNeeded)
unit.connect_input_reg(boiler.getCooledCoolantFilledPercentage)
self.rtu.connect_input_reg(self.boiler.getSteam)
self.rtu.connect_input_reg(self.boiler.getSteamNeeded)
self.rtu.connect_input_reg(self.boiler.getSteamFilledPercentage)
self.rtu.connect_input_reg(self.boiler.getWater)
self.rtu.connect_input_reg(self.boiler.getWaterNeeded)
self.rtu.connect_input_reg(self.boiler.getWaterFilledPercentage)
self.rtu.connect_input_reg(self.boiler.getHeatedCoolant)
self.rtu.connect_input_reg(self.boiler.getHeatedCoolantNeeded)
self.rtu.connect_input_reg(self.boiler.getHeatedCoolantFilledPercentage)
self.rtu.connect_input_reg(self.boiler.getCooledCoolant)
self.rtu.connect_input_reg(self.boiler.getCooledCoolantNeeded)
self.rtu.connect_input_reg(self.boiler.getCooledCoolantFilledPercentage)
-- holding registers --
-- none
return unit.interface()
return self.rtu.interface()
end
return boilerv_rtu

View File

@@ -4,8 +4,17 @@ local energymachine_rtu = {}
-- create new energy machine device
---@param machine table
function energymachine_rtu.new(machine)
local unit = rtu.init_unit()
energymachine_rtu.new = function (machine)
local self = {
rtu = rtu.init_unit(),
machine = machine
}
---@class rtu_device
local public = {}
-- get the RTU interface
public.rtu_interface = function () return self.rtu end
-- discrete inputs --
-- none
@@ -15,16 +24,16 @@ function energymachine_rtu.new(machine)
-- input registers --
-- build properties
unit.connect_input_reg(machine.getTotalMaxEnergy)
self.rtu.connect_input_reg(self.machine.getTotalMaxEnergy)
-- containers
unit.connect_input_reg(machine.getTotalEnergy)
unit.connect_input_reg(machine.getTotalEnergyNeeded)
unit.connect_input_reg(machine.getTotalEnergyFilledPercentage)
self.rtu.connect_input_reg(self.machine.getTotalEnergy)
self.rtu.connect_input_reg(self.machine.getTotalEnergyNeeded)
self.rtu.connect_input_reg(self.machine.getTotalEnergyFilledPercentage)
-- holding registers --
-- none
return unit.interface()
return public
end
return energymachine_rtu

View File

@@ -1,26 +0,0 @@
local rtu = require("rtu.rtu")
local envd_rtu = {}
-- create new environment detector device
---@param envd table
function envd_rtu.new(envd)
local unit = rtu.init_unit()
-- discrete inputs --
-- none
-- coils --
-- none
-- input registers --
unit.connect_input_reg(envd.getRadiation)
unit.connect_input_reg(envd.getRadiationRaw)
-- holding registers --
-- none
return unit.interface()
end
return envd_rtu

View File

@@ -4,39 +4,42 @@ local imatrix_rtu = {}
-- create new induction matrix (mek 10.1+) device
---@param imatrix table
function imatrix_rtu.new(imatrix)
local unit = rtu.init_unit()
imatrix_rtu.new = function (imatrix)
local self = {
rtu = rtu.init_unit(),
imatrix = imatrix
}
-- discrete inputs --
unit.connect_di(imatrix.isFormed)
self.rtu.connect_di(self.boiler.isFormed)
-- coils --
-- none
-- input registers --
-- multiblock properties
unit.connect_input_reg(imatrix.getLength)
unit.connect_input_reg(imatrix.getWidth)
unit.connect_input_reg(imatrix.getHeight)
unit.connect_input_reg(imatrix.getMinPos)
unit.connect_input_reg(imatrix.getMaxPos)
self.rtu.connect_input_reg(self.boiler.getLength)
self.rtu.connect_input_reg(self.boiler.getWidth)
self.rtu.connect_input_reg(self.boiler.getHeight)
self.rtu.connect_input_reg(self.boiler.getMinPos)
self.rtu.connect_input_reg(self.boiler.getMaxPos)
-- build properties
unit.connect_input_reg(imatrix.getMaxEnergy)
unit.connect_input_reg(imatrix.getTransferCap)
unit.connect_input_reg(imatrix.getInstalledCells)
unit.connect_input_reg(imatrix.getInstalledProviders)
self.rtu.connect_input_reg(self.imatrix.getMaxEnergy)
self.rtu.connect_input_reg(self.imatrix.getTransferCap)
self.rtu.connect_input_reg(self.imatrix.getInstalledCells)
self.rtu.connect_input_reg(self.imatrix.getInstalledProviders)
-- containers
self.rtu.connect_input_reg(self.imatrix.getEnergy)
self.rtu.connect_input_reg(self.imatrix.getEnergyNeeded)
self.rtu.connect_input_reg(self.imatrix.getEnergyFilledPercentage)
-- I/O rates
unit.connect_input_reg(imatrix.getLastInput)
unit.connect_input_reg(imatrix.getLastOutput)
-- tanks
unit.connect_input_reg(imatrix.getEnergy)
unit.connect_input_reg(imatrix.getEnergyNeeded)
unit.connect_input_reg(imatrix.getEnergyFilledPercentage)
self.rtu.connect_input_reg(self.imatrix.getLastInput)
self.rtu.connect_input_reg(self.imatrix.getLastOutput)
-- holding registers --
-- none
return unit.interface()
return self.rtu.interface()
end
return imatrix_rtu

View File

@@ -1,4 +1,4 @@
local rtu = require("rtu.rtu")
local rtu = require("rtu.rtu")
local rsio = require("scada-common.rsio")
local redstone_rtu = {}
@@ -8,11 +8,13 @@ local digital_write = rsio.digital_write
local digital_is_active = rsio.digital_is_active
-- create new redstone device
function redstone_rtu.new()
local unit = rtu.init_unit()
redstone_rtu.new = function ()
local self = {
rtu = rtu.init_unit()
}
-- get RTU interface
local interface = unit.interface()
local interface = self.rtu.interface()
---@class rtu_rs_device
--- extends rtu_device; fields added manually to please Lua diagnostics
@@ -29,7 +31,7 @@ function redstone_rtu.new()
-- link digital input
---@param side string
---@param color integer
function public.link_di(side, color)
public.link_di = function (side, color)
local f_read = nil
if color then
@@ -42,14 +44,14 @@ function redstone_rtu.new()
end
end
unit.connect_di(f_read)
self.rtu.connect_di(f_read)
end
-- link digital output
---@param channel RS_IO
---@param side string
---@param color integer
function public.link_do(channel, side, color)
public.link_do = function (channel, side, color)
local f_read = nil
local f_write = nil
@@ -79,13 +81,13 @@ function redstone_rtu.new()
end
end
unit.connect_coil(f_read, f_write)
self.rtu.connect_coil(f_read, f_write)
end
-- link analog input
---@param side string
function public.link_ai(side)
unit.connect_input_reg(
public.link_ai = function (side)
self.rtu.connect_input_reg(
function ()
return rs.getAnalogInput(side)
end
@@ -94,8 +96,8 @@ function redstone_rtu.new()
-- link analog output
---@param side string
function public.link_ao(side)
unit.connect_holding_reg(
public.link_ao = function (side)
self.rtu.connect_holding_reg(
function ()
return rs.getAnalogOutput(side)
end,

View File

@@ -1,37 +0,0 @@
local rtu = require("rtu.rtu")
local sna_rtu = {}
-- create new solar neutron activator (sna) device
---@param sna table
function sna_rtu.new(sna)
local unit = rtu.init_unit()
-- discrete inputs --
-- none
-- coils --
-- none
-- input registers --
-- build properties
unit.connect_input_reg(sna.getInputCapacity)
unit.connect_input_reg(sna.getOutputCapacity)
-- current state
unit.connect_input_reg(sna.getProductionRate)
unit.connect_input_reg(sna.getPeakProductionRate)
-- tanks
unit.connect_input_reg(sna.getInput)
unit.connect_input_reg(sna.getInputNeeded)
unit.connect_input_reg(sna.getInputFilledPercentage)
unit.connect_input_reg(sna.getOutput)
unit.connect_input_reg(sna.getOutputNeeded)
unit.connect_input_reg(sna.getOutputFilledPercentage)
-- holding registers --
-- none
return unit.interface()
end
return sna_rtu

View File

@@ -1,47 +0,0 @@
local rtu = require("rtu.rtu")
local sps_rtu = {}
-- create new super-critical phase shifter (sps) device
---@param sps table
function sps_rtu.new(sps)
local unit = rtu.init_unit()
-- discrete inputs --
unit.connect_di(sps.isFormed)
-- coils --
-- none
-- input registers --
-- multiblock properties
unit.connect_input_reg(sps.getLength)
unit.connect_input_reg(sps.getWidth)
unit.connect_input_reg(sps.getHeight)
unit.connect_input_reg(sps.getMinPos)
unit.connect_input_reg(sps.getMaxPos)
-- build properties
unit.connect_input_reg(sps.getCoils)
unit.connect_input_reg(sps.getInputCapacity)
unit.connect_input_reg(sps.getOutputCapacity)
unit.connect_input_reg(sps.getMaxEnergy)
-- current state
unit.connect_input_reg(sps.getProcessRate)
-- tanks
unit.connect_input_reg(sps.getInput)
unit.connect_input_reg(sps.getInputNeeded)
unit.connect_input_reg(sps.getInputFilledPercentage)
unit.connect_input_reg(sps.getOutput)
unit.connect_input_reg(sps.getOutputNeeded)
unit.connect_input_reg(sps.getOutputFilledPercentage)
unit.connect_input_reg(sps.getEnergy)
unit.connect_input_reg(sps.getEnergyNeeded)
unit.connect_input_reg(sps.getEnergyFilledPercentage)
-- holding registers --
-- none
return unit.interface()
end
return sps_rtu

View File

@@ -4,8 +4,11 @@ local turbine_rtu = {}
-- create new turbine (mek 10.0) device
---@param turbine table
function turbine_rtu.new(turbine)
local unit = rtu.init_unit()
turbine_rtu.new = function (turbine)
local self = {
rtu = rtu.init_unit(),
turbine = turbine
}
-- discrete inputs --
-- none
@@ -15,29 +18,29 @@ function turbine_rtu.new(turbine)
-- input registers --
-- build properties
unit.connect_input_reg(turbine.getBlades)
unit.connect_input_reg(turbine.getCoils)
unit.connect_input_reg(turbine.getVents)
unit.connect_input_reg(turbine.getDispersers)
unit.connect_input_reg(turbine.getCondensers)
unit.connect_input_reg(turbine.getSteamCapacity)
unit.connect_input_reg(turbine.getMaxFlowRate)
unit.connect_input_reg(turbine.getMaxProduction)
unit.connect_input_reg(turbine.getMaxWaterOutput)
self.rtu.connect_input_reg(self.turbine.getBlades)
self.rtu.connect_input_reg(self.turbine.getCoils)
self.rtu.connect_input_reg(self.turbine.getVents)
self.rtu.connect_input_reg(self.turbine.getDispersers)
self.rtu.connect_input_reg(self.turbine.getCondensers)
self.rtu.connect_input_reg(self.turbine.getSteamCapacity)
self.rtu.connect_input_reg(self.turbine.getMaxFlowRate)
self.rtu.connect_input_reg(self.turbine.getMaxProduction)
self.rtu.connect_input_reg(self.turbine.getMaxWaterOutput)
-- current state
unit.connect_input_reg(turbine.getFlowRate)
unit.connect_input_reg(turbine.getProductionRate)
unit.connect_input_reg(turbine.getLastSteamInputRate)
unit.connect_input_reg(turbine.getDumpingMode)
self.rtu.connect_input_reg(self.turbine.getFlowRate)
self.rtu.connect_input_reg(self.turbine.getProductionRate)
self.rtu.connect_input_reg(self.turbine.getLastSteamInputRate)
self.rtu.connect_input_reg(self.turbine.getDumpingMode)
-- tanks
unit.connect_input_reg(turbine.getSteam)
unit.connect_input_reg(turbine.getSteamNeeded)
unit.connect_input_reg(turbine.getSteamFilledPercentage)
self.rtu.connect_input_reg(self.turbine.getSteam)
self.rtu.connect_input_reg(self.turbine.getSteamNeeded)
self.rtu.connect_input_reg(self.turbine.getSteamFilledPercentage)
-- holding registers --
-- none
return unit.interface()
return self.rtu.interface()
end
return turbine_rtu

View File

@@ -4,51 +4,54 @@ local turbinev_rtu = {}
-- create new turbine (mek 10.1+) device
---@param turbine table
function turbinev_rtu.new(turbine)
local unit = rtu.init_unit()
turbinev_rtu.new = function (turbine)
local self = {
rtu = rtu.init_unit(),
turbine = turbine
}
-- discrete inputs --
unit.connect_di(turbine.isFormed)
self.rtu.connect_di(self.boiler.isFormed)
-- coils --
unit.connect_coil(function () turbine.incrementDumpingMode() end, function () end)
unit.connect_coil(function () turbine.decrementDumpingMode() end, function () end)
self.rtu.connect_coil(function () self.turbine.incrementDumpingMode() end, function () end)
self.rtu.connect_coil(function () self.turbine.decrementDumpingMode() end, function () end)
-- input registers --
-- multiblock properties
unit.connect_input_reg(turbine.getLength)
unit.connect_input_reg(turbine.getWidth)
unit.connect_input_reg(turbine.getHeight)
unit.connect_input_reg(turbine.getMinPos)
unit.connect_input_reg(turbine.getMaxPos)
self.rtu.connect_input_reg(self.boiler.getLength)
self.rtu.connect_input_reg(self.boiler.getWidth)
self.rtu.connect_input_reg(self.boiler.getHeight)
self.rtu.connect_input_reg(self.boiler.getMinPos)
self.rtu.connect_input_reg(self.boiler.getMaxPos)
-- build properties
unit.connect_input_reg(turbine.getBlades)
unit.connect_input_reg(turbine.getCoils)
unit.connect_input_reg(turbine.getVents)
unit.connect_input_reg(turbine.getDispersers)
unit.connect_input_reg(turbine.getCondensers)
unit.connect_input_reg(turbine.getSteamCapacity)
unit.connect_input_reg(turbine.getMaxEnergy)
unit.connect_input_reg(turbine.getMaxFlowRate)
unit.connect_input_reg(turbine.getMaxProduction)
unit.connect_input_reg(turbine.getMaxWaterOutput)
self.rtu.connect_input_reg(self.turbine.getBlades)
self.rtu.connect_input_reg(self.turbine.getCoils)
self.rtu.connect_input_reg(self.turbine.getVents)
self.rtu.connect_input_reg(self.turbine.getDispersers)
self.rtu.connect_input_reg(self.turbine.getCondensers)
self.rtu.connect_input_reg(self.turbine.getDumpingMode)
self.rtu.connect_input_reg(self.turbine.getSteamCapacity)
self.rtu.connect_input_reg(self.turbine.getMaxEnergy)
self.rtu.connect_input_reg(self.turbine.getMaxFlowRate)
self.rtu.connect_input_reg(self.turbine.getMaxWaterOutput)
self.rtu.connect_input_reg(self.turbine.getMaxProduction)
-- current state
unit.connect_input_reg(turbine.getFlowRate)
unit.connect_input_reg(turbine.getProductionRate)
unit.connect_input_reg(turbine.getLastSteamInputRate)
unit.connect_input_reg(turbine.getDumpingMode)
self.rtu.connect_input_reg(self.turbine.getFlowRate)
self.rtu.connect_input_reg(self.turbine.getProductionRate)
self.rtu.connect_input_reg(self.turbine.getLastSteamInputRate)
-- tanks/containers
unit.connect_input_reg(turbine.getSteam)
unit.connect_input_reg(turbine.getSteamNeeded)
unit.connect_input_reg(turbine.getSteamFilledPercentage)
unit.connect_input_reg(turbine.getEnergy)
unit.connect_input_reg(turbine.getEnergyNeeded)
unit.connect_input_reg(turbine.getEnergyFilledPercentage)
self.rtu.connect_input_reg(self.turbine.getSteam)
self.rtu.connect_input_reg(self.turbine.getSteamNeeded)
self.rtu.connect_input_reg(self.turbine.getSteamFilledPercentage)
self.rtu.connect_input_reg(self.turbine.getEnergy)
self.rtu.connect_input_reg(self.turbine.getEnergyNeeded)
self.rtu.connect_input_reg(self.turbine.getEnergyFilledPercentage)
-- holding registers --
unit.connect_holding_reg(turbine.setDumpingMode, turbine.getDumpingMode)
self.rtu.connect_holding_reg(self.turbine.setDumpingMode, self.turbine.getDumpingMode)
return unit.interface()
return self.rtu.interface()
end
return turbinev_rtu

View File

@@ -9,7 +9,7 @@ local MODBUS_EXCODE = types.MODBUS_EXCODE
-- new modbus comms handler object
---@param rtu_dev rtu_device|rtu_rs_device RTU device
---@param use_parallel_read boolean whether or not to use parallel calls when reading
function modbus.new(rtu_dev, use_parallel_read)
modbus.new = function (rtu_dev, use_parallel_read)
local self = {
rtu = rtu_dev,
use_parallel = use_parallel_read
@@ -23,7 +23,7 @@ function modbus.new(rtu_dev, use_parallel_read)
---@param c_addr_start integer
---@param count integer
---@return boolean ok, table readings
local function _1_read_coils(c_addr_start, count)
local _1_read_coils = function (c_addr_start, count)
local tasks = {}
local readings = {}
local access_fault = false
@@ -69,7 +69,7 @@ function modbus.new(rtu_dev, use_parallel_read)
---@param di_addr_start integer
---@param count integer
---@return boolean ok, table readings
local function _2_read_discrete_inputs(di_addr_start, count)
local _2_read_discrete_inputs = function (di_addr_start, count)
local tasks = {}
local readings = {}
local access_fault = false
@@ -115,7 +115,7 @@ function modbus.new(rtu_dev, use_parallel_read)
---@param hr_addr_start integer
---@param count integer
---@return boolean ok, table readings
local function _3_read_multiple_holding_registers(hr_addr_start, count)
local _3_read_multiple_holding_registers = function (hr_addr_start, count)
local tasks = {}
local readings = {}
local access_fault = false
@@ -161,7 +161,7 @@ function modbus.new(rtu_dev, use_parallel_read)
---@param ir_addr_start integer
---@param count integer
---@return boolean ok, table readings
local function _4_read_input_registers(ir_addr_start, count)
local _4_read_input_registers = function (ir_addr_start, count)
local tasks = {}
local readings = {}
local access_fault = false
@@ -207,7 +207,7 @@ function modbus.new(rtu_dev, use_parallel_read)
---@param c_addr integer
---@param value any
---@return boolean ok, MODBUS_EXCODE|nil
local function _5_write_single_coil(c_addr, value)
local _5_write_single_coil = function (c_addr, value)
local response = nil
local _, coils, _, _ = self.rtu.io_count()
local return_ok = c_addr <= coils
@@ -229,7 +229,7 @@ function modbus.new(rtu_dev, use_parallel_read)
---@param hr_addr integer
---@param value any
---@return boolean ok, MODBUS_EXCODE|nil
local function _6_write_single_holding_register(hr_addr, value)
local _6_write_single_holding_register = function (hr_addr, value)
local response = nil
local _, _, _, hold_regs = self.rtu.io_count()
local return_ok = hr_addr <= hold_regs
@@ -251,7 +251,7 @@ function modbus.new(rtu_dev, use_parallel_read)
---@param c_addr_start integer
---@param values any
---@return boolean ok, MODBUS_EXCODE|nil
local function _15_write_multiple_coils(c_addr_start, values)
local _15_write_multiple_coils = function (c_addr_start, values)
local response = nil
local _, coils, _, _ = self.rtu.io_count()
local count = #values
@@ -278,7 +278,7 @@ function modbus.new(rtu_dev, use_parallel_read)
---@param hr_addr_start integer
---@param values any
---@return boolean ok, MODBUS_EXCODE|nil
local function _16_write_multiple_holding_registers(hr_addr_start, values)
local _16_write_multiple_holding_registers = function (hr_addr_start, values)
local response = nil
local _, _, _, hold_regs = self.rtu.io_count()
local count = #values
@@ -305,7 +305,7 @@ function modbus.new(rtu_dev, use_parallel_read)
-- validate a request without actually executing it
---@param packet modbus_frame
---@return boolean return_code, modbus_packet reply
function public.check_request(packet)
public.check_request = function (packet)
local return_code = true
local response = { MODBUS_EXCODE.ACKNOWLEDGE }
@@ -332,9 +332,10 @@ function modbus.new(rtu_dev, use_parallel_read)
-- default is to echo back
local func_code = packet.func_code
-- echo back with error flag, on success the "error" will be acknowledgement
func_code = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG)
if not return_code then
-- echo back with error flag
func_code = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG)
end
-- create reply
local reply = comms.modbus_packet()
@@ -346,7 +347,7 @@ function modbus.new(rtu_dev, use_parallel_read)
-- handle a MODBUS TCP packet and generate a reply
---@param packet modbus_frame
---@return boolean return_code, modbus_packet reply
function public.handle_packet(packet)
public.handle_packet = function (packet)
local return_code = true
local response = nil
@@ -399,40 +400,40 @@ function modbus.new(rtu_dev, use_parallel_read)
return return_code, reply
end
-- return a SERVER_DEVICE_BUSY error reply
---@return modbus_packet reply
public.reply__srv_device_busy = function (packet)
-- reply back with error flag and exception code
local reply = comms.modbus_packet()
local fcode = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG)
local data = { MODBUS_EXCODE.SERVER_DEVICE_BUSY }
reply.make(packet.txn_id, packet.unit_id, fcode, data)
return reply
end
-- return a NEG_ACKNOWLEDGE error reply
---@return modbus_packet reply
public.reply__neg_ack = function (packet)
-- reply back with error flag and exception code
local reply = comms.modbus_packet()
local fcode = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG)
local data = { MODBUS_EXCODE.NEG_ACKNOWLEDGE }
reply.make(packet.txn_id, packet.unit_id, fcode, data)
return reply
end
-- return a GATEWAY_PATH_UNAVAILABLE error reply
---@return modbus_packet reply
public.reply__gw_unavailable = function (packet)
-- reply back with error flag and exception code
local reply = comms.modbus_packet()
local fcode = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG)
local data = { MODBUS_EXCODE.GATEWAY_PATH_UNAVAILABLE }
reply.make(packet.txn_id, packet.unit_id, fcode, data)
return reply
end
return public
end
-- return a SERVER_DEVICE_BUSY error reply
---@return modbus_packet reply
function modbus.reply__srv_device_busy(packet)
-- reply back with error flag and exception code
local reply = comms.modbus_packet()
local fcode = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG)
local data = { MODBUS_EXCODE.SERVER_DEVICE_BUSY }
reply.make(packet.txn_id, packet.unit_id, fcode, data)
return reply
end
-- return a NEG_ACKNOWLEDGE error reply
---@return modbus_packet reply
function modbus.reply__neg_ack(packet)
-- reply back with error flag and exception code
local reply = comms.modbus_packet()
local fcode = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG)
local data = { MODBUS_EXCODE.NEG_ACKNOWLEDGE }
reply.make(packet.txn_id, packet.unit_id, fcode, data)
return reply
end
-- return a GATEWAY_PATH_UNAVAILABLE error reply
---@return modbus_packet reply
function modbus.reply__gw_unavailable(packet)
-- reply back with error flag and exception code
local reply = comms.modbus_packet()
local fcode = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG)
local data = { MODBUS_EXCODE.GATEWAY_PATH_UNAVAILABLE }
reply.make(packet.txn_id, packet.unit_id, fcode, data)
return reply
end
return modbus

View File

@@ -20,7 +20,7 @@ local print_ts = util.print_ts
local println_ts = util.println_ts
-- create a new RTU
function rtu.init_unit()
rtu.init_unit = function ()
local self = {
discrete_inputs = {},
coils = {},
@@ -38,13 +38,13 @@ function rtu.init_unit()
local protected = {}
-- refresh IO count
local function _count_io()
local _count_io = function ()
self.io_count_cache = { #self.discrete_inputs, #self.coils, #self.input_regs, #self.holding_regs }
end
-- return IO count
---@return integer discrete_inputs, integer coils, integer input_regs, integer holding_regs
function public.io_count()
public.io_count = function ()
return self.io_count_cache[1], self.io_count_cache[2], self.io_count_cache[3], self.io_count_cache[4]
end
@@ -53,7 +53,7 @@ function rtu.init_unit()
-- connect discrete input
---@param f function
---@return integer count count of discrete inputs
function protected.connect_di(f)
protected.connect_di = function (f)
insert(self.discrete_inputs, { read = f })
_count_io()
return #self.discrete_inputs
@@ -62,7 +62,7 @@ function rtu.init_unit()
-- read discrete input
---@param di_addr integer
---@return any value, boolean access_fault
function public.read_di(di_addr)
public.read_di = function (di_addr)
ppm.clear_fault()
local value = self.discrete_inputs[di_addr].read()
return value, ppm.is_faulted()
@@ -74,7 +74,7 @@ function rtu.init_unit()
---@param f_read function
---@param f_write function
---@return integer count count of coils
function protected.connect_coil(f_read, f_write)
protected.connect_coil = function (f_read, f_write)
insert(self.coils, { read = f_read, write = f_write })
_count_io()
return #self.coils
@@ -83,7 +83,7 @@ function rtu.init_unit()
-- read coil
---@param coil_addr integer
---@return any value, boolean access_fault
function public.read_coil(coil_addr)
public.read_coil = function (coil_addr)
ppm.clear_fault()
local value = self.coils[coil_addr].read()
return value, ppm.is_faulted()
@@ -93,7 +93,7 @@ function rtu.init_unit()
---@param coil_addr integer
---@param value any
---@return boolean access_fault
function public.write_coil(coil_addr, value)
public.write_coil = function (coil_addr, value)
ppm.clear_fault()
self.coils[coil_addr].write(value)
return ppm.is_faulted()
@@ -104,7 +104,7 @@ function rtu.init_unit()
-- connect input register
---@param f function
---@return integer count count of input registers
function protected.connect_input_reg(f)
protected.connect_input_reg = function (f)
insert(self.input_regs, { read = f })
_count_io()
return #self.input_regs
@@ -113,7 +113,7 @@ function rtu.init_unit()
-- read input register
---@param reg_addr integer
---@return any value, boolean access_fault
function public.read_input_reg(reg_addr)
public.read_input_reg = function (reg_addr)
ppm.clear_fault()
local value = self.input_regs[reg_addr].read()
return value, ppm.is_faulted()
@@ -125,7 +125,7 @@ function rtu.init_unit()
---@param f_read function
---@param f_write function
---@return integer count count of holding registers
function protected.connect_holding_reg(f_read, f_write)
protected.connect_holding_reg = function (f_read, f_write)
insert(self.holding_regs, { read = f_read, write = f_write })
_count_io()
return #self.holding_regs
@@ -134,7 +134,7 @@ function rtu.init_unit()
-- read holding register
---@param reg_addr integer
---@return any value, boolean access_fault
function public.read_holding_reg(reg_addr)
public.read_holding_reg = function (reg_addr)
ppm.clear_fault()
local value = self.holding_regs[reg_addr].read()
return value, ppm.is_faulted()
@@ -144,7 +144,7 @@ function rtu.init_unit()
---@param reg_addr integer
---@param value any
---@return boolean access_fault
function public.write_holding_reg(reg_addr, value)
public.write_holding_reg = function (reg_addr, value)
ppm.clear_fault()
self.holding_regs[reg_addr].write(value)
return ppm.is_faulted()
@@ -153,7 +153,7 @@ function rtu.init_unit()
-- public RTU device access
-- get the public interface to this RTU
function protected.interface()
protected.interface = function ()
return public
end
@@ -166,7 +166,7 @@ end
---@param local_port integer
---@param server_port integer
---@param conn_watchdog watchdog
function rtu.comms(version, modem, local_port, server_port, conn_watchdog)
rtu.comms = function (version, modem, local_port, server_port, conn_watchdog)
local self = {
version = version,
seq_num = 0,
@@ -193,7 +193,7 @@ function rtu.comms(version, modem, local_port, server_port, conn_watchdog)
-- send a scada management packet
---@param msg_type SCADA_MGMT_TYPES
---@param msg table
local function _send(msg_type, msg)
local _send = function (msg_type, msg)
local s_pkt = comms.scada_packet()
local m_pkt = comms.mgmt_packet()
@@ -206,7 +206,7 @@ function rtu.comms(version, modem, local_port, server_port, conn_watchdog)
-- keep alive ack
---@param srv_time integer
local function _send_keep_alive_ack(srv_time)
local _send_keep_alive_ack = function (srv_time)
_send(SCADA_MGMT_TYPES.KEEP_ALIVE, { srv_time, util.time() })
end
@@ -214,7 +214,7 @@ function rtu.comms(version, modem, local_port, server_port, conn_watchdog)
-- send a MODBUS TCP packet
---@param m_pkt modbus_packet
function public.send_modbus(m_pkt)
public.send_modbus = function (m_pkt)
local s_pkt = comms.scada_packet()
s_pkt.make(self.seq_num, PROTOCOLS.MODBUS_TCP, m_pkt.raw_sendable())
self.modem.transmit(self.s_port, self.l_port, s_pkt.raw_sendable())
@@ -224,7 +224,7 @@ function rtu.comms(version, modem, local_port, server_port, conn_watchdog)
-- reconnect a newly connected modem
---@param modem table
---@diagnostic disable-next-line: redefined-local
function public.reconnect_modem(modem)
public.reconnect_modem = function (modem)
self.modem = modem
-- open modem
@@ -235,14 +235,14 @@ function rtu.comms(version, modem, local_port, server_port, conn_watchdog)
-- unlink from the server
---@param rtu_state rtu_state
function public.unlink(rtu_state)
public.unlink = function (rtu_state)
rtu_state.linked = false
self.r_seq_num = nil
end
-- close the connection to the server
---@param rtu_state rtu_state
function public.close(rtu_state)
public.close = function (rtu_state)
self.conn_watchdog.cancel()
public.unlink(rtu_state)
_send(SCADA_MGMT_TYPES.CLOSE, {})
@@ -250,7 +250,7 @@ function rtu.comms(version, modem, local_port, server_port, conn_watchdog)
-- send capability advertisement
---@param units table
function public.send_advertisement(units)
public.send_advertisement = function (units)
local advertisement = { self.version }
for i = 1, #units do
@@ -282,7 +282,7 @@ function rtu.comms(version, modem, local_port, server_port, conn_watchdog)
---@param message any
---@param distance integer
---@return modbus_frame|mgmt_frame|nil packet
function public.parse_packet(side, sender, reply_to, message, distance)
public.parse_packet = function(side, sender, reply_to, message, distance)
local pkt = nil
local s_pkt = comms.scada_packet()
@@ -314,7 +314,7 @@ function rtu.comms(version, modem, local_port, server_port, conn_watchdog)
---@param packet modbus_frame|mgmt_frame
---@param units table
---@param rtu_state rtu_state
function public.handle_packet(packet, units, rtu_state)
public.handle_packet = function(packet, units, rtu_state)
if packet ~= nil then
-- check sequence number
if self.r_seq_num == nil then
@@ -353,7 +353,7 @@ function rtu.comms(version, modem, local_port, server_port, conn_watchdog)
-- check if there are more than 3 active transactions
-- still queue the packet, but this may indicate a problem
if unit.pkt_queue.length() > 3 then
reply = modbus.reply__srv_device_busy(packet)
reply = unit.modbus_io.reply__srv_device_busy(packet)
log.debug("queueing new request with " .. unit.pkt_queue.length() ..
" transactions already in the queue" .. unit_dbg_tag)
end
@@ -383,7 +383,7 @@ function rtu.comms(version, modem, local_port, server_port, conn_watchdog)
log.warning("RTU KEEP_ALIVE trip time > 500ms (" .. trip_time .. "ms)")
end
-- log.debug("RTU RTT = " .. trip_time .. "ms")
-- log.debug("RTU RTT = ".. trip_time .. "ms")
_send_keep_alive_ack(timestamp)
else

View File

@@ -4,28 +4,27 @@
require("/initenv").init_env()
local log = require("scada-common.log")
local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue")
local ppm = require("scada-common.ppm")
local rsio = require("scada-common.rsio")
local types = require("scada-common.types")
local util = require("scada-common.util")
local ppm = require("scada-common.ppm")
local rsio = require("scada-common.rsio")
local types = require("scada-common.types")
local util = require("scada-common.util")
local config = require("rtu.config")
local modbus = require("rtu.modbus")
local rtu = require("rtu.rtu")
local config = require("rtu.config")
local modbus = require("rtu.modbus")
local rtu = require("rtu.rtu")
local threads = require("rtu.threads")
local redstone_rtu = require("rtu.dev.redstone_rtu")
local boiler_rtu = require("rtu.dev.boiler_rtu")
local boilerv_rtu = require("rtu.dev.boilerv_rtu")
local redstone_rtu = require("rtu.dev.redstone_rtu")
local boiler_rtu = require("rtu.dev.boiler_rtu")
local boilerv_rtu = require("rtu.dev.boilerv_rtu")
local energymachine_rtu = require("rtu.dev.energymachine_rtu")
local envd_rtu = require("rtu.dev.envd_rtu")
local imatrix_rtu = require("rtu.dev.imatrix_rtu")
local turbine_rtu = require("rtu.dev.turbine_rtu")
local turbinev_rtu = require("rtu.dev.turbinev_rtu")
local imatrix_rtu = require("rtu.dev.imatrix_rtu")
local turbine_rtu = require("rtu.dev.turbine_rtu")
local turbinev_rtu = require("rtu.dev.turbinev_rtu")
local RTU_VERSION = "beta-v0.7.12"
local RTU_VERSION = "alpha-v0.7.1"
local rtu_t = types.rtu_t
@@ -34,30 +33,12 @@ local println = util.println
local print_ts = util.print_ts
local println_ts = util.println_ts
----------------------------------------
-- config validation
----------------------------------------
local cfv = util.new_validator()
cfv.assert_port(config.SERVER_PORT)
cfv.assert_port(config.LISTEN_PORT)
cfv.assert_type_str(config.LOG_PATH)
cfv.assert_type_int(config.LOG_MODE)
cfv.assert_type_table(config.RTU_DEVICES)
cfv.assert_type_table(config.RTU_REDSTONE)
assert(cfv.valid(), "bad config file: missing/invalid fields")
----------------------------------------
-- log init
----------------------------------------
log.init(config.LOG_PATH, config.LOG_MODE)
log.info("========================================")
log.info("BOOTING rtu.startup " .. RTU_VERSION)
log.info("========================================")
println(">> RTU GATEWAY " .. RTU_VERSION .. " <<")
println(">> RTU " .. RTU_VERSION .. " <<")
----------------------------------------
-- startup
@@ -99,7 +80,7 @@ local smem_sys = __shared_memory.rtu_sys
-- get modem
if smem_dev.modem == nil then
println("boot> wireless modem not found")
log.fatal("no wireless modem on startup")
log.warning("no wireless modem on startup")
return
end
@@ -112,256 +93,194 @@ local units = __shared_memory.rtu_sys.units
local rtu_redstone = config.RTU_REDSTONE
local rtu_devices = config.RTU_DEVICES
-- configure RTU gateway based on config file definitions
local function configure()
-- redstone interfaces
for entry_idx = 1, #rtu_redstone do
local rs_rtu = redstone_rtu.new()
local io_table = rtu_redstone[entry_idx].io
local io_reactor = rtu_redstone[entry_idx].for_reactor
-- redstone interfaces
for entry_idx = 1, #rtu_redstone do
local rs_rtu = redstone_rtu.new()
local io_table = rtu_redstone[entry_idx].io
local io_reactor = rtu_redstone[entry_idx].for_reactor
-- CHECK: reactor ID must be >= to 1
if (not util.is_int(io_reactor)) or (io_reactor <= 0) then
println(util.c("configure> redstone entry #", entry_idx, " : ", io_reactor, " isn't an integer >= 1"))
return false
local capabilities = {}
log.debug("init> starting redstone RTU I/O linking for reactor " .. io_reactor .. "...")
local continue = true
for i = 1, #units do
local unit = units[i] ---@type rtu_unit_registry_entry
if unit.reactor == io_reactor and unit.type == rtu_t.redstone then
-- duplicate entry
log.warning("init> skipping definition block #" .. entry_idx .. " for reactor " .. io_reactor .. " with already defined redstone I/O")
continue = false
break
end
end
-- CHECK: io table exists
if type(io_table) ~= "table" then
println(util.c("configure> redstone entry #", entry_idx, " no IO table found"))
return false
end
if continue then
for i = 1, #io_table do
local valid = false
local conf = io_table[i]
local capabilities = {}
log.debug(util.c("configure> starting redstone RTU I/O linking for reactor ", io_reactor, "..."))
local continue = true
-- check for duplicate entries
for i = 1, #units do
local unit = units[i] ---@type rtu_unit_registry_entry
if unit.reactor == io_reactor and unit.type == rtu_t.redstone then
-- duplicate entry
local message = util.c("configure> skipping definition block #", entry_idx, " for reactor ", io_reactor,
" with already defined redstone I/O")
println(message)
log.warning(message)
continue = false
break
end
end
-- not a duplicate
if continue then
for i = 1, #io_table do
local valid = false
local conf = io_table[i]
-- verify configuration
if rsio.is_valid_channel(conf.channel) and rsio.is_valid_side(conf.side) then
if conf.bundled_color then
valid = rsio.is_color(conf.bundled_color)
else
valid = true
end
end
if not valid then
local message = util.c("configure> invalid redstone definition at index ", i, " in definition block #", entry_idx,
" (for reactor ", io_reactor, ")")
println(message)
log.error(message)
return false
-- verify configuration
if rsio.is_valid_channel(conf.channel) and rsio.is_valid_side(conf.side) then
if conf.bundled_color then
valid = rsio.is_color(conf.bundled_color)
else
-- link redstone in RTU
local mode = rsio.get_io_mode(conf.channel)
if mode == rsio.IO_MODE.DIGITAL_IN then
-- can't have duplicate inputs
if util.table_contains(capabilities, conf.channel) then
local message = util.c("configure> skipping duplicate input for channel ", rsio.to_string(conf.channel), " on side ", conf.side)
println(message)
log.warning(message)
else
rs_rtu.link_di(conf.side, conf.bundled_color)
end
elseif mode == rsio.IO_MODE.DIGITAL_OUT then
rs_rtu.link_do(conf.channel, conf.side, conf.bundled_color)
elseif mode == rsio.IO_MODE.ANALOG_IN then
-- can't have duplicate inputs
if util.table_contains(capabilities, conf.channel) then
local message = util.c("configure> skipping duplicate input for channel ", rsio.to_string(conf.channel), " on side ", conf.side)
println(message)
log.warning(message)
else
rs_rtu.link_ai(conf.side)
end
elseif mode == rsio.IO_MODE.ANALOG_OUT then
rs_rtu.link_ao(conf.side)
else
-- should be unreachable code, we already validated channels
log.error("configure> fell through if chain attempting to identify IO mode", true)
println("configure> encountered a software error, check logs")
return false
end
table.insert(capabilities, conf.channel)
log.debug(util.c("configure> linked redstone ", #capabilities, ": ", rsio.to_string(conf.channel),
" (", conf.side, ") for reactor ", io_reactor))
valid = true
end
end
if not valid then
local message = "init> invalid redstone definition at index " .. i .. " in definition block #" .. entry_idx ..
" (for reactor " .. io_reactor .. ")"
println_ts(message)
log.warning(message)
else
-- link redstone in RTU
local mode = rsio.get_io_mode(conf.channel)
if mode == rsio.IO_MODE.DIGITAL_IN then
-- can't have duplicate inputs
if util.table_contains(capabilities, conf.channel) then
log.warning("init> skipping duplicate input for channel " .. rsio.to_string(conf.channel) .. " on side " .. conf.side)
else
rs_rtu.link_di(conf.side, conf.bundled_color)
end
elseif mode == rsio.IO_MODE.DIGITAL_OUT then
rs_rtu.link_do(conf.channel, conf.side, conf.bundled_color)
elseif mode == rsio.IO_MODE.ANALOG_IN then
-- can't have duplicate inputs
if util.table_contains(capabilities, conf.channel) then
log.warning("init> skipping duplicate input for channel " .. rsio.to_string(conf.channel) .. " on side " .. conf.side)
else
rs_rtu.link_ai(conf.side)
end
elseif mode == rsio.IO_MODE.ANALOG_OUT then
rs_rtu.link_ao(conf.side)
else
-- should be unreachable code, we already validated channels
log.error("init> fell through if chain attempting to identify IO mode", true)
break
end
table.insert(capabilities, conf.channel)
log.debug("init> linked redstone " .. #capabilities .. ": " .. rsio.to_string(conf.channel) .. " (" .. conf.side ..
") for reactor " .. io_reactor)
end
end
---@class rtu_unit_registry_entry
local unit = {
name = "redstone_io",
type = rtu_t.redstone,
index = entry_idx,
reactor = io_reactor,
device = capabilities, -- use device field for redstone channels
rtu = rs_rtu,
modbus_io = modbus.new(rs_rtu, false),
pkt_queue = nil,
thread = nil
}
table.insert(units, unit)
log.debug("init> initialized RTU unit #" .. #units .. ": redstone_io (redstone) [1] for reactor " .. io_reactor)
end
end
-- mounted peripherals
for i = 1, #rtu_devices do
local device = ppm.get_periph(rtu_devices[i].name)
if device == nil then
local message = "init> '" .. rtu_devices[i].name .. "' not found"
println_ts(message)
log.warning(message)
else
local type = ppm.get_type(rtu_devices[i].name)
local rtu_iface = nil ---@type rtu_device
local rtu_type = ""
if type == "boiler" then
-- boiler multiblock
rtu_type = rtu_t.boiler
rtu_iface = boiler_rtu.new(device)
elseif type == "boilerValve" then
-- boiler multiblock (10.1+)
rtu_type = rtu_t.boiler_valve
rtu_iface = boilerv_rtu.new(device)
elseif type == "turbine" then
-- turbine multiblock
rtu_type = rtu_t.turbine
rtu_iface = turbine_rtu.new(device)
elseif type == "turbineValve" then
-- turbine multiblock (10.1+)
rtu_type = rtu_t.turbine_valve
rtu_iface = turbinev_rtu.new(device)
elseif type == "mekanismMachine" then
-- assumed to be an induction matrix multiblock, pre Mekanism 10.1
-- also works with energy cubes
rtu_type = rtu_t.energy_machine
rtu_iface = energymachine_rtu.new(device)
elseif type == "inductionPort" then
-- induction matrix multiblock (10.1+)
rtu_type = rtu_t.induction_matrix
rtu_iface = imatrix_rtu.new(device)
else
local message = "init> device '" .. rtu_devices[i].name .. "' is not a known type (" .. type .. ")"
println_ts(message)
log.warning(message)
end
if rtu_iface ~= nil then
---@class rtu_unit_registry_entry
local unit = {
name = "redstone_io",
type = rtu_t.redstone,
index = entry_idx,
reactor = io_reactor,
device = capabilities, -- use device field for redstone channels
rtu = rs_rtu,
modbus_io = modbus.new(rs_rtu, false),
pkt_queue = nil,
local rtu_unit = {
name = rtu_devices[i].name,
type = rtu_type,
index = rtu_devices[i].index,
reactor = rtu_devices[i].for_reactor,
device = device,
rtu = rtu_iface,
modbus_io = modbus.new(rtu_iface, true),
pkt_queue = mqueue.new(),
thread = nil
}
table.insert(units, unit)
rtu_unit.thread = threads.thread__unit_comms(__shared_memory, rtu_unit)
log.debug(util.c("init> initialized RTU unit #", #units, ": redstone_io (redstone) [1] for reactor ", io_reactor))
table.insert(units, rtu_unit)
log.debug("init> initialized RTU unit #" .. #units .. ": " .. rtu_devices[i].name .. " (" .. rtu_type .. ") [" ..
rtu_devices[i].index .. "] for reactor " .. rtu_devices[i].for_reactor)
end
end
-- mounted peripherals
for i = 1, #rtu_devices do
local name = rtu_devices[i].name
local index = rtu_devices[i].index
local for_reactor = rtu_devices[i].for_reactor
-- CHECK: name is a string
if type(name) ~= "string" then
println(util.c("configure> device entry #", i, ": device ", name, " isn't a string"))
return false
end
-- CHECK: index is an integer >= 1
if (not util.is_int(index)) or (index <= 0) then
println(util.c("configure> device entry #", i, ": index ", index, " isn't an integer >= 1"))
return false
end
-- CHECK: reactor is an integer >= 1
if (not util.is_int(for_reactor)) or (for_reactor <= 0) then
println(util.c("configure> device entry #", i, ": reactor ", for_reactor, " isn't an integer >= 1"))
return false
end
local device = ppm.get_periph(name)
if device == nil then
local message = util.c("configure> '", name, "' not found")
println(message)
log.fatal(message)
return false
else
local type = ppm.get_type(name)
local rtu_iface = nil ---@type rtu_device
local rtu_type = ""
if type == "boiler" then
-- boiler multiblock
rtu_type = rtu_t.boiler
rtu_iface = boiler_rtu.new(device)
elseif type == "boilerValve" then
-- boiler multiblock (10.1+)
rtu_type = rtu_t.boiler_valve
rtu_iface = boilerv_rtu.new(device)
elseif type == "turbine" then
-- turbine multiblock
rtu_type = rtu_t.turbine
rtu_iface = turbine_rtu.new(device)
elseif type == "turbineValve" then
-- turbine multiblock (10.1+)
rtu_type = rtu_t.turbine_valve
rtu_iface = turbinev_rtu.new(device)
elseif type == "mekanismMachine" then
-- assumed to be an induction matrix multiblock, pre Mekanism 10.1
-- also works with energy cubes
rtu_type = rtu_t.energy_machine
rtu_iface = energymachine_rtu.new(device)
elseif type == "inductionPort" then
-- induction matrix multiblock (10.1+)
rtu_type = rtu_t.induction_matrix
rtu_iface = imatrix_rtu.new(device)
elseif type == "environmentDetector" then
-- advanced peripherals environment detector
rtu_type = rtu_t.env_detector
rtu_iface = envd_rtu.new(device)
else
local message = util.c("configure> device '", name, "' is not a known type (", type, ")")
println_ts(message)
log.fatal(message)
return false
end
if rtu_iface ~= nil then
---@class rtu_unit_registry_entry
local rtu_unit = {
name = name,
type = rtu_type,
index = index,
reactor = for_reactor,
device = device,
rtu = rtu_iface,
modbus_io = modbus.new(rtu_iface, true),
pkt_queue = mqueue.new(),
thread = nil
}
rtu_unit.thread = threads.thread__unit_comms(__shared_memory, rtu_unit)
table.insert(units, rtu_unit)
log.debug(util.c("configure> initialized RTU unit #", #units, ": ", name, " (", rtu_type, ") [", index, "] for reactor ", for_reactor))
end
end
end
-- we made it through all that trusting-user-to-write-a-config-file chaos
return true
end
----------------------------------------
-- start system
----------------------------------------
log.debug("boot> running configure()")
-- start connection watchdog
smem_sys.conn_watchdog = util.new_watchdog(5)
log.debug("boot> conn watchdog started")
if configure() then
-- start connection watchdog
smem_sys.conn_watchdog = util.new_watchdog(5)
log.debug("boot> conn watchdog started")
-- setup comms
smem_sys.rtu_comms = rtu.comms(RTU_VERSION, smem_dev.modem, config.LISTEN_PORT, config.SERVER_PORT, smem_sys.conn_watchdog)
log.debug("boot> comms init")
-- setup comms
smem_sys.rtu_comms = rtu.comms(RTU_VERSION, smem_dev.modem, config.LISTEN_PORT, config.SERVER_PORT, smem_sys.conn_watchdog)
log.debug("boot> comms init")
-- init threads
local main_thread = threads.thread__main(__shared_memory)
local comms_thread = threads.thread__comms(__shared_memory)
-- init threads
local main_thread = threads.thread__main(__shared_memory)
local comms_thread = threads.thread__comms(__shared_memory)
-- assemble thread list
local _threads = { main_thread.p_exec, comms_thread.p_exec }
for i = 1, #units do
if units[i].thread ~= nil then
table.insert(_threads, units[i].thread.p_exec)
end
-- assemble thread list
local _threads = { main_thread.p_exec, comms_thread.p_exec }
for i = 1, #units do
if units[i].thread ~= nil then
table.insert(_threads, units[i].thread.p_exec)
end
-- run threads
parallel.waitForAll(table.unpack(_threads))
else
println("configuration failed, exiting...")
end
-- run threads
parallel.waitForAll(table.unpack(_threads))
println_ts("exited")
log.info("exited")

View File

@@ -27,11 +27,11 @@ local COMMS_SLEEP = 100 -- (100ms, 2 ticks)
-- main thread
---@param smem rtu_shared_memory
function threads.thread__main(smem)
threads.thread__main = function (smem)
local public = {} ---@class thread
-- execute thread
function public.exec()
public.exec = function ()
log.debug("main thread start")
-- main loop clock
@@ -44,15 +44,13 @@ function threads.thread__main(smem)
local conn_watchdog = smem.rtu_sys.conn_watchdog
local units = smem.rtu_sys.units
-- start unlinked (in case of restart)
rtu_comms.unlink(rtu_state)
-- start clock
loop_clock.start()
-- event loop
while true do
local event, param1, param2, param3, param4, param5 = util.pull_event()
---@diagnostic disable-next-line: undefined-field
local event, param1, param2, param3, param4, param5 = os.pullEventRaw()
if event == "timer" and loop_clock.is_clock(param1) then
-- start next clock timer
@@ -157,7 +155,7 @@ function threads.thread__main(smem)
end
-- execute the thread in a protected mode, retrying it on return if not shutting down
function public.p_exec()
public.p_exec = function ()
local rtu_state = smem.rtu_state
while not rtu_state.shutdown do
@@ -178,11 +176,11 @@ end
-- communications handler thread
---@param smem rtu_shared_memory
function threads.thread__comms(smem)
threads.thread__comms = function (smem)
local public = {} ---@class thread
-- execute thread
function public.exec()
public.exec = function ()
log.debug("comms thread start")
-- load in from shared memory
@@ -229,7 +227,7 @@ function threads.thread__comms(smem)
end
-- execute the thread in a protected mode, retrying it on return if not shutting down
function public.p_exec()
public.p_exec = function ()
local rtu_state = smem.rtu_state
while not rtu_state.shutdown do
@@ -251,12 +249,12 @@ end
-- per-unit communications handler thread
---@param smem rtu_shared_memory
---@param unit rtu_unit_registry_entry
function threads.thread__unit_comms(smem, unit)
threads.thread__unit_comms = function (smem, unit)
local public = {} ---@class thread
-- execute thread
function public.exec()
log.debug("rtu unit thread start -> " .. unit.type .. "(" .. unit.name .. ")")
public.exec = function ()
log.debug("rtu unit thread start -> " .. unit.name .. "(" .. unit.type .. ")")
-- load in from shared memory
local rtu_state = smem.rtu_state
@@ -289,7 +287,7 @@ function threads.thread__unit_comms(smem, unit)
-- check for termination request
if rtu_state.shutdown then
log.info("rtu unit thread exiting -> " .. unit.type .. "(" .. unit.name .. ")")
log.info("rtu unit thread exiting -> " .. unit.name .. "(" .. unit.type .. ")")
break
end
@@ -299,7 +297,7 @@ function threads.thread__unit_comms(smem, unit)
end
-- execute the thread in a protected mode, retrying it on return if not shutting down
function public.p_exec()
public.p_exec = function ()
local rtu_state = smem.rtu_state
while not rtu_state.shutdown do
@@ -309,7 +307,7 @@ function threads.thread__unit_comms(smem, unit)
end
if not rtu_state.shutdown then
log.info(util.c("rtu unit thread ", unit.type, "(", unit.name, ") restarting in 5 seconds..."))
log.info("rtu unit thread " .. unit.name .. "(" .. unit.type .. ") restarting in 5 seconds...")
util.psleep(5)
end
end

View File

@@ -17,7 +17,7 @@ alarm.SEVERITY = SEVERITY
-- severity integer to string
---@param severity SEVERITY
function alarm.severity_to_string(severity)
alarm.severity_to_string = function (severity)
if severity == SEVERITY.INFO then
return "INFO"
elseif severity == SEVERITY.WARNING then
@@ -39,7 +39,7 @@ end
---@param severity SEVERITY
---@param device string
---@param message string
function alarm.scada_alarm(severity, device, message)
alarm.scada_alarm = function (severity, device, message)
local self = {
time = util.time(),
ts_string = os.date("[%H:%M:%S]"),
@@ -53,12 +53,12 @@ function alarm.scada_alarm(severity, device, message)
-- format the alarm as a string
---@return string message
function public.format()
return self.ts_string .. " [" .. alarm.severity_to_string(self.severity) .. "] (" .. self.device .. ") >> " .. self.message
public.format = function ()
return self.ts_string .. " [" .. alarm.severity_to_string(self.severity) .. "] (" .. self.device ") >> " .. self.message
end
-- get alarm properties
function public.properties()
public.properties = function ()
return {
time = self.time,
severity = self.severity,

View File

@@ -2,7 +2,7 @@
-- Communications
--
local log = require("scada-common.log")
local log = require("scada-common.log")
local types = require("scada-common.types")
---@class comms
@@ -16,7 +16,7 @@ local PROTOCOLS = {
MODBUS_TCP = 0, -- our "MODBUS TCP"-esque protocol
RPLC = 1, -- reactor PLC protocol
SCADA_MGMT = 2, -- SCADA supervisor management, device advertisements, etc
SCADA_CRDN = 3, -- data/control packets for coordinators to/from supervisory controllers
COORD_DATA = 3, -- data/control packets for coordinators to/from supervisory controllers
COORD_API = 4 -- data/control packets for pocket computers to/from coordinators
}
@@ -48,20 +48,6 @@ local SCADA_MGMT_TYPES = {
REMOTE_LINKED = 3 -- remote device linked
}
---@alias SCADA_CRDN_TYPES integer
local SCADA_CRDN_TYPES = {
ESTABLISH = 0, -- initial greeting
STRUCT_BUILDS = 1, -- mekanism structure builds
UNIT_STATUSES = 2, -- state of reactor units
COMMAND_UNIT = 3, -- command a reactor unit
ALARM = 4 -- alarm signaling
}
---@alias CAPI_TYPES integer
local CAPI_TYPES = {
ESTABLISH = 0 -- initial greeting
}
---@alias RTU_UNIT_TYPES integer
local RTU_UNIT_TYPES = {
REDSTONE = 0, -- redstone I/O
@@ -70,21 +56,17 @@ local RTU_UNIT_TYPES = {
TURBINE = 3, -- turbine
TURBINE_VALVE = 4, -- turbine, mekanism 10.1+
EMACHINE = 5, -- energy machine
IMATRIX = 6, -- induction matrix
SPS = 7, -- SPS
SNA = 8, -- SNA
ENV_DETECTOR = 9 -- environment detector
IMATRIX = 6 -- induction matrix
}
comms.PROTOCOLS = PROTOCOLS
comms.RPLC_TYPES = RPLC_TYPES
comms.RPLC_LINKING = RPLC_LINKING
comms.SCADA_MGMT_TYPES = SCADA_MGMT_TYPES
comms.SCADA_CRDN_TYPES = SCADA_CRDN_TYPES
comms.RTU_UNIT_TYPES = RTU_UNIT_TYPES
-- generic SCADA packet object
function comms.scada_packet()
comms.scada_packet = function ()
local self = {
modem_msg_in = nil,
valid = false,
@@ -102,7 +84,7 @@ function comms.scada_packet()
---@param seq_num integer
---@param protocol PROTOCOLS
---@param payload table
function public.make(seq_num, protocol, payload)
public.make = function (seq_num, protocol, payload)
self.valid = true
self.seq_num = seq_num
self.protocol = protocol
@@ -117,7 +99,7 @@ function comms.scada_packet()
---@param reply_to integer
---@param message any
---@param distance integer
function public.receive(side, sender, reply_to, message, distance)
public.receive = function (side, sender, reply_to, message, distance)
self.modem_msg_in = {
iface = side,
s_port = sender,
@@ -130,15 +112,12 @@ function comms.scada_packet()
if type(self.raw) == "table" then
if #self.raw >= 3 then
self.valid = true
self.seq_num = self.raw[1]
self.protocol = self.raw[2]
self.length = #self.raw[3]
self.payload = self.raw[3]
end
self.valid = type(self.seq_num) == "number" and
type(self.protocol) == "number" and
type(self.payload) == "table"
end
return self.valid
@@ -146,25 +125,25 @@ function comms.scada_packet()
-- public accessors --
function public.modem_event() return self.modem_msg_in end
function public.raw_sendable() return self.raw end
public.modem_event = function () return self.modem_msg_in end
public.raw_sendable = function () return self.raw end
function public.local_port() return self.modem_msg_in.s_port end
function public.remote_port() return self.modem_msg_in.r_port end
public.local_port = function () return self.modem_msg_in.s_port end
public.remote_port = function () return self.modem_msg_in.r_port end
function public.is_valid() return self.valid end
public.is_valid = function () return self.valid end
function public.seq_num() return self.seq_num end
function public.protocol() return self.protocol end
function public.length() return self.length end
function public.data() return self.payload end
public.seq_num = function () return self.seq_num end
public.protocol = function () return self.protocol end
public.length = function () return self.length end
public.data = function () return self.payload end
return public
end
-- MODBUS packet
-- modeled after MODBUS TCP packet
function comms.modbus_packet()
comms.modbus_packet = function ()
local self = {
frame = nil,
raw = nil,
@@ -183,28 +162,24 @@ function comms.modbus_packet()
---@param unit_id integer
---@param func_code MODBUS_FCODE
---@param data table
function public.make(txn_id, unit_id, func_code, data)
if type(data) == "table" then
self.txn_id = txn_id
self.length = #data
self.unit_id = unit_id
self.func_code = func_code
self.data = data
public.make = function (txn_id, unit_id, func_code, data)
self.txn_id = txn_id
self.length = #data
self.unit_id = unit_id
self.func_code = func_code
self.data = data
-- populate raw array
self.raw = { self.txn_id, self.unit_id, self.func_code }
for i = 1, self.length do
insert(self.raw, data[i])
end
else
log.error("comms.modbus_packet.make(): data not table")
-- populate raw array
self.raw = { self.txn_id, self.unit_id, self.func_code }
for i = 1, self.length do
insert(self.raw, data[i])
end
end
-- decode a MODBUS packet from a SCADA frame
---@param frame scada_packet
---@return boolean success
function public.decode(frame)
public.decode = function (frame)
if frame then
self.frame = frame
@@ -216,11 +191,7 @@ function comms.modbus_packet()
public.make(data[1], data[2], data[3], { table.unpack(data, 4, #data) })
end
local valid = type(self.txn_id) == "number" and
type(self.unit_id) == "number" and
type(self.func_code) == "number"
return size_ok and valid
return size_ok
else
log.debug("attempted MODBUS_TCP parse of incorrect protocol " .. frame.protocol(), true)
return false
@@ -232,10 +203,10 @@ function comms.modbus_packet()
end
-- get raw to send
function public.raw_sendable() return self.raw end
public.raw_sendable = function () return self.raw end
-- get this packet as a frame with an immutable relation to this object
function public.get()
public.get = function ()
---@class modbus_frame
local frame = {
scada_frame = self.frame,
@@ -253,7 +224,7 @@ function comms.modbus_packet()
end
-- reactor PLC packet
function comms.rplc_packet()
comms.rplc_packet = function ()
local self = {
frame = nil,
raw = nil,
@@ -267,7 +238,7 @@ function comms.rplc_packet()
local public = {}
-- check that type is known
local function _rplc_type_valid()
local _rplc_type_valid = function ()
return self.type == RPLC_TYPES.LINK_REQ or
self.type == RPLC_TYPES.STATUS or
self.type == RPLC_TYPES.MEK_STRUCT or
@@ -283,28 +254,24 @@ function comms.rplc_packet()
---@param id integer
---@param packet_type RPLC_TYPES
---@param data table
function public.make(id, packet_type, data)
if type(data) == "table" then
-- packet accessor properties
self.id = id
self.type = packet_type
self.length = #data
self.data = data
public.make = function (id, packet_type, data)
-- packet accessor properties
self.id = id
self.type = packet_type
self.length = #data
self.data = data
-- populate raw array
self.raw = { self.id, self.type }
for i = 1, #data do
insert(self.raw, data[i])
end
else
log.error("comms.rplc_packet.make(): data not table")
-- populate raw array
self.raw = { self.id, self.type }
for i = 1, #data do
insert(self.raw, data[i])
end
end
-- decode an RPLC packet from a SCADA frame
---@param frame scada_packet
---@return boolean success
function public.decode(frame)
public.decode = function (frame)
if frame then
self.frame = frame
@@ -317,8 +284,6 @@ function comms.rplc_packet()
ok = _rplc_type_valid()
end
ok = ok and type(self.id) == "number"
return ok
else
log.debug("attempted RPLC parse of incorrect protocol " .. frame.protocol(), true)
@@ -331,10 +296,10 @@ function comms.rplc_packet()
end
-- get raw to send
function public.raw_sendable() return self.raw end
public.raw_sendable = function () return self.raw end
-- get this packet as a frame with an immutable relation to this object
function public.get()
public.get = function ()
---@class rplc_frame
local frame = {
scada_frame = self.frame,
@@ -351,7 +316,7 @@ function comms.rplc_packet()
end
-- SCADA management packet
function comms.mgmt_packet()
comms.mgmt_packet = function ()
local self = {
frame = nil,
raw = nil,
@@ -364,7 +329,7 @@ function comms.mgmt_packet()
local public = {}
-- check that type is known
local function _scada_type_valid()
local _scada_type_valid = function ()
return self.type == SCADA_MGMT_TYPES.KEEP_ALIVE or
self.type == SCADA_MGMT_TYPES.CLOSE or
self.type == SCADA_MGMT_TYPES.REMOTE_LINKED or
@@ -374,27 +339,23 @@ function comms.mgmt_packet()
-- make a SCADA management packet
---@param packet_type SCADA_MGMT_TYPES
---@param data table
function public.make(packet_type, data)
if type(data) == "table" then
-- packet accessor properties
self.type = packet_type
self.length = #data
self.data = data
public.make = function (packet_type, data)
-- packet accessor properties
self.type = packet_type
self.length = #data
self.data = data
-- populate raw array
self.raw = { self.type }
for i = 1, #data do
insert(self.raw, data[i])
end
else
log.error("comms.mgmt_packet.make(): data not table")
-- populate raw array
self.raw = { self.type }
for i = 1, #data do
insert(self.raw, data[i])
end
end
-- decode a SCADA management packet from a SCADA frame
---@param frame scada_packet
---@return boolean success
function public.decode(frame)
public.decode = function (frame)
if frame then
self.frame = frame
@@ -410,7 +371,7 @@ function comms.mgmt_packet()
return ok
else
log.debug("attempted SCADA_MGMT parse of incorrect protocol " .. frame.protocol(), true)
return false
return false
end
else
log.debug("nil frame encountered", true)
@@ -419,10 +380,10 @@ function comms.mgmt_packet()
end
-- get raw to send
function public.raw_sendable() return self.raw end
public.raw_sendable = function () return self.raw end
-- get this packet as a frame with an immutable relation to this object
function public.get()
public.get = function ()
---@class mgmt_frame
local frame = {
scada_frame = self.frame,
@@ -438,7 +399,8 @@ function comms.mgmt_packet()
end
-- SCADA coordinator packet
function comms.crdn_packet()
-- @todo
comms.coord_packet = function ()
local self = {
frame = nil,
raw = nil,
@@ -447,57 +409,49 @@ function comms.crdn_packet()
data = nil
}
---@class crdn_packet
---@class coord_packet
local public = {}
-- check that type is known
local function _crdn_type_valid()
return self.type == SCADA_CRDN_TYPES.ESTABLISH or
self.type == SCADA_CRDN_TYPES.STRUCT_BUILDS or
self.type == SCADA_CRDN_TYPES.UNIT_STATUSES or
self.type == SCADA_CRDN_TYPES.COMMAND_UNIT or
self.type == SCADA_CRDN_TYPES.ALARM
local _coord_type_valid = function ()
-- @todo
return false
end
-- make a coordinator packet
---@param packet_type SCADA_CRDN_TYPES
---@param packet_type any
---@param data table
function public.make(packet_type, data)
if type(data) == "table" then
-- packet accessor properties
self.type = packet_type
self.length = #data
self.data = data
public.make = function (packet_type, data)
-- packet accessor properties
self.type = packet_type
self.length = #data
self.data = data
-- populate raw array
self.raw = { self.type }
for i = 1, #data do
insert(self.raw, data[i])
end
else
log.error("comms.crdn_packet.make(): data not table")
-- populate raw array
self.raw = { self.type }
for i = 1, #data do
insert(self.raw, data[i])
end
end
-- decode a coordinator packet from a SCADA frame
---@param frame scada_packet
---@return boolean success
function public.decode(frame)
public.decode = function (frame)
if frame then
self.frame = frame
if frame.protocol() == PROTOCOLS.SCADA_CRDN then
if frame.protocol() == PROTOCOLS.COORD_DATA then
local ok = frame.length() >= 1
if ok then
local data = frame.data()
public.make(data[1], { table.unpack(data, 2, #data) })
ok = _crdn_type_valid()
ok = _coord_type_valid()
end
return ok
else
log.debug("attempted SCADA_CRDN parse of incorrect protocol " .. frame.protocol(), true)
log.debug("attempted COORD_DATA parse of incorrect protocol " .. frame.protocol(), true)
return false
end
else
@@ -507,11 +461,11 @@ function comms.crdn_packet()
end
-- get raw to send
function public.raw_sendable() return self.raw end
public.raw_sendable = function () return self.raw end
-- get this packet as a frame with an immutable relation to this object
function public.get()
---@class crdn_frame
public.get = function ()
---@class coord_frame
local frame = {
scada_frame = self.frame,
type = self.type,
@@ -527,7 +481,7 @@ end
-- coordinator API (CAPI) packet
-- @todo
function comms.capi_packet()
comms.capi_packet = function ()
local self = {
frame = nil,
raw = nil,
@@ -539,35 +493,31 @@ function comms.capi_packet()
---@class capi_packet
local public = {}
local function _capi_type_valid()
local _coord_type_valid = function ()
-- @todo
return false
end
-- make a coordinator API packet
---@param packet_type CAPI_TYPES
---@param packet_type any
---@param data table
function public.make(packet_type, data)
if type(data) == "table" then
-- packet accessor properties
self.type = packet_type
self.length = #data
self.data = data
public.make = function (packet_type, data)
-- packet accessor properties
self.type = packet_type
self.length = #data
self.data = data
-- populate raw array
self.raw = { self.type }
for i = 1, #data do
insert(self.raw, data[i])
end
else
log.error("comms.capi_packet.make(): data not table")
-- populate raw array
self.raw = { self.type }
for i = 1, #data do
insert(self.raw, data[i])
end
end
-- decode a coordinator API packet from a SCADA frame
---@param frame scada_packet
---@return boolean success
function public.decode(frame)
public.decode = function (frame)
if frame then
self.frame = frame
@@ -577,7 +527,7 @@ function comms.capi_packet()
if ok then
local data = frame.data()
public.make(data[1], { table.unpack(data, 2, #data) })
ok = _capi_type_valid()
ok = _coord_type_valid()
end
return ok
@@ -592,10 +542,10 @@ function comms.capi_packet()
end
-- get raw to send
function public.raw_sendable() return self.raw end
public.raw_sendable = function () return self.raw end
-- get this packet as a frame with an immutable relation to this object
function public.get()
public.get = function ()
---@class capi_frame
local frame = {
scada_frame = self.frame,
@@ -613,7 +563,7 @@ end
-- convert rtu_t to RTU unit type
---@param type rtu_t
---@return RTU_UNIT_TYPES|nil
function comms.rtu_t_to_unit_type(type)
comms.rtu_t_to_unit_type = function (type)
if type == rtu_t.redstone then
return RTU_UNIT_TYPES.REDSTONE
elseif type == rtu_t.boiler then
@@ -636,7 +586,7 @@ end
-- convert RTU unit type to rtu_t
---@param utype RTU_UNIT_TYPES
---@return rtu_t|nil
function comms.advert_type_to_rtu_t(utype)
comms.advert_type_to_rtu_t = function (utype)
if utype == RTU_UNIT_TYPES.REDSTONE then
return rtu_t.redstone
elseif utype == RTU_UNIT_TYPES.BOILER then

View File

@@ -1,245 +0,0 @@
--
-- Cryptographic Communications Engine
--
local aes128 = require("lockbox.cipher.aes128")
local ctr_mode = require("lockbox.cipher.mode.ctr");
local sha1 = require("lockbox.digest.sha1");
local sha2_224 = require("lockbox.digest.sha2_224");
local sha2_256 = require("lockbox.digest.sha2_256");
local pbkdf2 = require("lockbox.kdf.pbkdf2")
local hmac = require("lockbox.mac.hmac")
local zero_pad = require("lockbox.padding.zero");
local stream = require("lockbox.util.stream")
local array = require("lockbox.util.array")
local log = require("scada-common.log")
local util = require("scada-common.util")
local crypto = {}
local c_eng = {
key = nil,
cipher = nil,
decipher = nil,
hmac = nil
}
---@alias hex string
-- initialize cryptographic system
function crypto.init(password, server_port)
local key_deriv = pbkdf2()
-- setup PBKDF2
-- the primary goal is to just turn our password into a 16 byte key
key_deriv.setPassword(password)
key_deriv.setSalt("salty_salt_at_" .. server_port)
key_deriv.setIterations(32)
key_deriv.setBlockLen(8)
key_deriv.setDKeyLen(16)
local start = util.time()
key_deriv.setPRF(hmac().setBlockSize(64).setDigest(sha2_256))
key_deriv.finish()
log.dmesg("pbkdf2: key derivation took " .. (util.time() - start) .. "ms", "CRYPTO", colors.yellow)
c_eng.key = array.fromHex(key_deriv.asHex())
-- initialize cipher
c_eng.cipher = ctr_mode.Cipher()
c_eng.cipher.setKey(c_eng.key)
c_eng.cipher.setBlockCipher(aes128)
c_eng.cipher.setPadding(zero_pad);
-- initialize decipher
c_eng.decipher = ctr_mode.Decipher()
c_eng.decipher.setKey(c_eng.key)
c_eng.decipher.setBlockCipher(aes128)
c_eng.decipher.setPadding(zero_pad);
-- initialize HMAC
c_eng.hmac = hmac()
c_eng.hmac.setBlockSize(64)
c_eng.hmac.setDigest(sha1)
c_eng.hmac.setKey(c_eng.key)
log.dmesg("init: completed in " .. (util.time() - start) .. "ms", "CRYPTO", colors.yellow)
end
-- encrypt plaintext
---@param plaintext string
---@return string initial_value, string ciphertext
function crypto.encrypt(plaintext)
local start = util.time()
-- initial value
local iv = {
math.random(0, 255),
math.random(0, 255),
math.random(0, 255),
math.random(0, 255),
math.random(0, 255),
math.random(0, 255),
math.random(0, 255),
math.random(0, 255),
math.random(0, 255),
math.random(0, 255),
math.random(0, 255),
math.random(0, 255),
math.random(0, 255),
math.random(0, 255),
math.random(0, 255),
math.random(0, 255)
}
log.debug("crypto.random: iv random took " .. (util.time() - start) .. "ms")
start = util.time()
c_eng.cipher.init()
c_eng.cipher.update(stream.fromArray(iv))
c_eng.cipher.update(stream.fromString(plaintext))
c_eng.cipher.finish()
local ciphertext = c_eng.cipher.asHex() ---@type hex
log.debug("crypto.encrypt: aes128-ctr-mode took " .. (util.time() - start) .. "ms")
log.debug("ciphertext: " .. util.strval(ciphertext))
return iv, ciphertext
end
-- decrypt ciphertext
---@param iv string CTR initial value
---@param ciphertext string ciphertext hex
---@return string plaintext
function crypto.decrypt(iv, ciphertext)
local start = util.time()
c_eng.decipher.init()
c_eng.decipher.update(stream.fromArray(iv))
c_eng.decipher.update(stream.fromHex(ciphertext))
c_eng.decipher.finish()
local plaintext_hex = c_eng.decipher.asHex() ---@type hex
local plaintext = stream.toString(stream.fromHex(plaintext_hex))
log.debug("crypto.decrypt: aes128-ctr-mode took " .. (util.time() - start) .. "ms")
log.debug("plaintext: " .. util.strval(plaintext))
return plaintext
end
-- generate HMAC of message
---@param message_hex string initial value concatenated with ciphertext
function crypto.hmac(message_hex)
local start = util.time()
c_eng.hmac.init()
c_eng.hmac.update(stream.fromHex(message_hex))
c_eng.hmac.finish()
local hash = c_eng.hmac.asHex() ---@type hex
log.debug("crypto.hmac: hmac-sha1 took " .. (util.time() - start) .. "ms")
log.debug("hmac: " .. util.strval(hash))
return hash
end
-- wrap a modem as a secure modem to send encrypted traffic
---@param modem table modem to wrap
function crypto.secure_modem(modem)
local self = {
modem = modem
}
---@class secure_modem
---@field open function
---@field isOpen function
---@field close function
---@field closeAll function
---@field isWireless function
---@field getNamesRemote function
---@field isPresentRemote function
---@field getTypeRemote function
---@field hasTypeRemote function
---@field getMethodsRemote function
---@field callRemote function
---@field getNameLocal function
local public = {}
-- wrap a modem
---@param modem table
---@diagnostic disable-next-line: redefined-local
function public.wrap(modem)
self.modem = modem
for key, func in pairs(self.modem) do
public[key] = func
end
end
-- wrap modem functions, then we replace transmit
public.wrap(self.modem)
-- send a packet with encryption
---@param channel integer
---@param reply_channel integer
---@param payload table packet raw_sendable
function public.transmit(channel, reply_channel, payload)
local plaintext = textutils.serialize(payload, { allow_repetitions = true, compact = true })
local iv, ciphertext = crypto.encrypt(plaintext)
---@diagnostic disable-next-line: redefined-local
local hmac = crypto.hmac(iv .. ciphertext)
self.modem.transmit(channel, reply_channel, { hmac, iv, ciphertext })
end
-- parse in a modem message as a network packet
---@param side string
---@param sender integer
---@param reply_to integer
---@param message any encrypted packet sent with secure_modem.transmit
---@param distance integer
---@return string side, integer sender, integer reply_to, any plaintext_message, integer distance
function public.receive(side, sender, reply_to, message, distance)
local body = ""
if type(message) == "table" then
if #message == 3 then
---@diagnostic disable-next-line: redefined-local
local hmac = message[1]
local iv = message[2]
local ciphertext = message[3]
local computed_hmac = crypto.hmac(iv .. ciphertext)
if hmac == computed_hmac then
-- message intact
local plaintext = crypto.decrypt(iv, ciphertext)
body = textutils.deserialize(plaintext)
if body == nil then
-- failed decryption
log.debug("crypto.secure_modem: decryption failed")
body = ""
end
else
-- something went wrong
log.debug("crypto.secure_modem: hmac mismatch violation")
end
end
end
return side, sender, reply_to, body, distance
end
return public
end
return crypto

View File

@@ -1,9 +1,9 @@
local util = require("scada-common.util")
--
-- File System Logger
--
local util = require("scada-common.util")
---@class log
local log = {}
@@ -32,7 +32,7 @@ local free_space = fs.getFreeSpace
---@param path string file path
---@param write_mode MODE
---@param dmesg_redirect? table terminal/window to direct dmesg to
function log.init(path, write_mode, dmesg_redirect)
log.init = function (path, write_mode, dmesg_redirect)
_log_sys.path = path
_log_sys.mode = write_mode
@@ -49,15 +49,9 @@ function log.init(path, write_mode, dmesg_redirect)
end
end
-- direct dmesg output to a monitor/window
---@param window table window or terminal reference
function log.direct_dmesg(window)
_log_sys.dmesg_out = window
end
-- private log write function
---@param msg string
local function _log(msg)
local _log = function (msg)
local time_stamp = os.date("[%c] ")
local stamped = time_stamp .. util.strval(msg)
@@ -90,20 +84,9 @@ local function _log(msg)
end
end
-- dmesg style logging for boot because I like linux-y things
---@param msg string message
---@param tag? string log tag
---@param tag_color? integer log tag color
---@return dmesg_ts_coord coordinates line area to place working indicator
function log.dmesg(msg, tag, tag_color)
---@class dmesg_ts_coord
local ts_coord = { x1 = 2, x2 = 3, y = 1 }
msg = util.strval(msg)
tag = tag or ""
tag = util.strval(tag)
local t_stamp = string.format("%12.2f", os.clock())
-- write a message to the dmesg output
---@param msg string message to write
local _write = function (msg)
local out = _log_sys.dmesg_out
local out_w, out_h = out.getSize()
@@ -133,45 +116,11 @@ function log.dmesg(msg, tag, tag_color)
end
end
-- start output with tag and time, assuming we have enough width for this to be on one line
local cur_x, cur_y = out.getCursorPos()
if cur_x > 1 then
if cur_y == out_h then
out.scroll(1)
out.setCursorPos(1, cur_y)
else
out.setCursorPos(1, cur_y + 1)
end
end
-- colored time
local initial_color = out.getTextColor()
out.setTextColor(colors.white)
out.write("[")
out.setTextColor(colors.lightGray)
out.write(t_stamp)
ts_coord.x2, ts_coord.y = out.getCursorPos()
ts_coord.x2 = ts_coord.x2 - 1
out.setTextColor(colors.white)
out.write("] ")
-- print optionally colored tag
if tag ~= "" then
out.write("[")
if tag_color then out.setTextColor(tag_color) end
out.write(tag)
out.setTextColor(colors.white)
out.write("] ")
end
out.setTextColor(initial_color)
-- output message
for i = 1, #lines do
cur_x, cur_y = out.getCursorPos()
local cur_x, cur_y = out.getCursorPos()
if i > 1 and cur_x > 1 then
if cur_x > 1 then
if cur_y == out_h then
out.scroll(1)
out.setCursorPos(1, cur_y)
@@ -182,75 +131,21 @@ function log.dmesg(msg, tag, tag_color)
out.write(lines[i])
end
_log(util.c("[", t_stamp, "] [", tag, "] ", msg))
return ts_coord
end
-- print a dmesg message, but then show remaining seconds instead of timestamp
-- dmesg style logging for boot because I like linux-y things
---@param msg string message
---@param tag? string log tag
---@param tag_color? integer log tag color
---@return function update, function done
function log.dmesg_working(msg, tag, tag_color)
local ts_coord = log.dmesg(msg, tag, tag_color)
local out = _log_sys.dmesg_out
local width = (ts_coord.x2 - ts_coord.x1) + 1
local initial_color = out.getTextColor()
local counter = 0
local function update(sec_remaining)
local time = util.sprintf("%ds", sec_remaining)
local available = width - (string.len(time) + 2)
local progress = ""
out.setCursorPos(ts_coord.x1, ts_coord.y)
out.write(" ")
if counter % 4 == 0 then
progress = "|"
elseif counter % 4 == 1 then
progress = "/"
elseif counter % 4 == 2 then
progress = "-"
elseif counter % 4 == 3 then
progress = "\\"
end
out.setTextColor(colors.blue)
out.write(progress)
out.setTextColor(colors.lightGray)
out.write(util.spaces(available) .. time)
out.setTextColor(initial_color)
counter = counter + 1
end
local function done(ok)
out.setCursorPos(ts_coord.x1, ts_coord.y)
if ok or ok == nil then
out.setTextColor(colors.green)
out.write(util.pad("DONE", width))
else
out.setTextColor(colors.red)
out.write(util.pad("FAIL", width))
end
out.setTextColor(initial_color)
end
return update, done
---@param show_term? boolean whether or not to show on terminal output
log.dmesg = function (msg, show_term)
local message = string.format("[%10.3f] ", os.clock()) .. util.strval(msg)
if show_term then _write(message) end
_log(message)
end
-- log debug messages
---@param msg string message
---@param trace? boolean include file trace
function log.debug(msg, trace)
log.debug = function (msg, trace)
if LOG_DEBUG then
local dbg_info = ""
@@ -271,20 +166,20 @@ end
-- log info messages
---@param msg string message
function log.info(msg)
log.info = function (msg)
_log("[INF] " .. util.strval(msg))
end
-- log warning messages
---@param msg string message
function log.warning(msg)
log.warning = function (msg)
_log("[WRN] " .. util.strval(msg))
end
-- log error messages
---@param msg string message
---@param trace? boolean include file trace
function log.error(msg, trace)
log.error = function (msg, trace)
local dbg_info = ""
if trace then
@@ -303,7 +198,7 @@ end
-- log fatal errors
---@param msg string message
function log.fatal(msg)
log.fatal = function (msg)
_log("[FTL] " .. util.strval(msg))
end

View File

@@ -14,7 +14,7 @@ local TYPE = {
mqueue.TYPE = TYPE
-- create a new message queue
function mqueue.new()
mqueue.new = function ()
local queue = {}
local insert = table.insert
@@ -32,44 +32,44 @@ function mqueue.new()
local public = {}
-- get queue length
function public.length() return #queue end
public.length = function () return #queue end
-- check if queue is empty
---@return boolean is_empty
function public.empty() return #queue == 0 end
public.empty = function () return #queue == 0 end
-- check if queue has contents
function public.ready() return #queue ~= 0 end
public.ready = function () return #queue ~= 0 end
-- push a new item onto the queue
---@param qtype TYPE
---@param message string
local function _push(qtype, message)
local _push = function (qtype, message)
insert(queue, { qtype = qtype, message = message })
end
-- push a command onto the queue
---@param message any
function public.push_command(message)
public.push_command = function (message)
_push(TYPE.COMMAND, message)
end
-- push data onto the queue
---@param key any
---@param value any
function public.push_data(key, value)
public.push_data = function (key, value)
_push(TYPE.DATA, { key = key, val = value })
end
-- push a packet onto the queue
---@param packet scada_packet|modbus_packet|rplc_packet|crdn_packet|capi_packet
function public.push_packet(packet)
---@param packet scada_packet|modbus_packet|rplc_packet|coord_packet|capi_packet
public.push_packet = function (packet)
_push(TYPE.PACKET, packet)
end
-- get an item off the queue
---@return queue_item|nil
function public.pop()
public.pop = function ()
if #queue > 0 then
return remove(queue, 1)
else

View File

@@ -1,10 +1,9 @@
local log = require("scada-common.log")
--
-- Protected Peripheral Manager
--
local log = require("scada-common.log")
local util = require("scada-common.util")
---@class ppm
local ppm = {}
@@ -33,7 +32,7 @@ local _ppm_sys = {
---
---assumes iface is a valid peripheral
---@param iface string CC peripheral interface
local function peri_init(iface)
local peri_init = function (iface)
local self = {
faulted = false,
last_fault = "",
@@ -48,10 +47,7 @@ local function peri_init(iface)
for key, func in pairs(self.device) do
self.fault_counts[key] = 0
self.device[key] = function (...)
local return_table = table.pack(pcall(func, ...))
local status = return_table[1]
table.remove(return_table, 1)
local status, result = pcall(func, ...)
if status then
-- auto fault clear
@@ -60,10 +56,8 @@ local function peri_init(iface)
self.fault_counts[key] = 0
return table.unpack(return_table)
return result
else
local result = return_table[1]
-- function failed
self.faulted = true
self.last_fault = result
@@ -77,7 +71,7 @@ local function peri_init(iface)
count_str = " [" .. self.fault_counts[key] .. " total faults]"
end
log.error(util.c("PPM: protected ", key, "() -> ", result, count_str))
log.error("PPM: protected " .. key .. "() -> " .. result .. count_str)
end
self.fault_counts[key] = self.fault_counts[key] + 1
@@ -93,13 +87,13 @@ local function peri_init(iface)
-- fault management functions
local function clear_fault() self.faulted = false end
local function get_last_fault() return self.last_fault end
local function is_faulted() return self.faulted end
local function is_ok() return not self.faulted end
local clear_fault = function () self.faulted = false end
local get_last_fault = function () return self.last_fault end
local is_faulted = function () return self.faulted end
local is_ok = function () return not self.faulted end
local function enable_afc() self.auto_cf = true end
local function disable_afc() self.auto_cf = false end
local enable_afc = function () self.auto_cf = true end
local disable_afc = function () self.auto_cf = false end
-- append to device functions
@@ -123,53 +117,53 @@ end
-- REPORTING --
-- silence error prints
function ppm.disable_reporting()
ppm.disable_reporting = function ()
_ppm_sys.mute = true
end
-- allow error prints
function ppm.enable_reporting()
ppm.enable_reporting = function ()
_ppm_sys.mute = false
end
-- FAULT MEMORY --
-- enable automatically clearing fault flag
function ppm.enable_afc()
ppm.enable_afc = function ()
_ppm_sys.auto_cf = true
end
-- disable automatically clearing fault flag
function ppm.disable_afc()
ppm.disable_afc = function ()
_ppm_sys.auto_cf = false
end
-- clear fault flag
function ppm.clear_fault()
ppm.clear_fault = function ()
_ppm_sys.faulted = false
end
-- check fault flag
function ppm.is_faulted()
ppm.is_faulted = function ()
return _ppm_sys.faulted
end
-- get the last fault message
function ppm.get_last_fault()
ppm.get_last_fault = function ()
return _ppm_sys.last_fault
end
-- TERMINATION --
-- if a caught error was a termination request
function ppm.should_terminate()
ppm.should_terminate = function ()
return _ppm_sys.terminate
end
-- MOUNTING --
-- mount all available peripherals (clears mounts first)
function ppm.mount_all()
ppm.mount_all = function ()
local ifaces = peripheral.getNames()
_ppm_sys.mounts = {}
@@ -177,7 +171,7 @@ function ppm.mount_all()
for i = 1, #ifaces do
_ppm_sys.mounts[ifaces[i]] = peri_init(ifaces[i])
log.info(util.c("PPM: found a ", _ppm_sys.mounts[ifaces[i]].type, " (", ifaces[i], ")"))
log.info("PPM: found a " .. _ppm_sys.mounts[ifaces[i]].type .. " (" .. ifaces[i] .. ")")
end
if #ifaces == 0 then
@@ -188,7 +182,7 @@ end
-- mount a particular device
---@param iface string CC peripheral interface
---@return string|nil type, table|nil device
function ppm.mount(iface)
ppm.mount = function (iface)
local ifaces = peripheral.getNames()
local pm_dev = nil
local pm_type = nil
@@ -200,7 +194,7 @@ function ppm.mount(iface)
pm_type = _ppm_sys.mounts[iface].type
pm_dev = _ppm_sys.mounts[iface].dev
log.info(util.c("PPM: mount(", iface, ") -> found a ", pm_type))
log.info("PPM: mount(" .. iface .. ") -> found a " .. pm_type)
break
end
end
@@ -211,7 +205,7 @@ end
-- handle peripheral_detach event
---@param iface string CC peripheral interface
---@return string|nil type, table|nil device
function ppm.handle_unmount(iface)
ppm.handle_unmount = function (iface)
local pm_dev = nil
local pm_type = nil
@@ -222,9 +216,9 @@ function ppm.handle_unmount(iface)
pm_type = lost_dev.type
pm_dev = lost_dev.dev
log.warning(util.c("PPM: lost device ", pm_type, " mounted to ", iface))
log.warning("PPM: lost device " .. pm_type .. " mounted to " .. iface)
else
log.error(util.c("PPM: lost device unknown to the PPM mounted to ", iface))
log.error("PPM: lost device unknown to the PPM mounted to " .. iface)
end
return pm_type, pm_dev
@@ -234,20 +228,20 @@ end
-- list all available peripherals
---@return table names
function ppm.list_avail()
ppm.list_avail = function ()
return peripheral.getNames()
end
-- list mounted peripherals
---@return table mounts
function ppm.list_mounts()
ppm.list_mounts = function ()
return _ppm_sys.mounts
end
-- get a mounted peripheral by side/interface
---@param iface string CC peripheral interface
---@return table|nil device function table
function ppm.get_periph(iface)
ppm.get_periph = function (iface)
if _ppm_sys.mounts[iface] then
return _ppm_sys.mounts[iface].dev
else return nil end
@@ -256,7 +250,7 @@ end
-- get a mounted peripheral type by side/interface
---@param iface string CC peripheral interface
---@return string|nil type
function ppm.get_type(iface)
ppm.get_type = function (iface)
if _ppm_sys.mounts[iface] then
return _ppm_sys.mounts[iface].type
else return nil end
@@ -265,7 +259,7 @@ end
-- get all mounted peripherals by type
---@param name string type name
---@return table devices device function tables
function ppm.get_all_devices(name)
ppm.get_all_devices = function (name)
local devices = {}
for _, data in pairs(_ppm_sys.mounts) do
@@ -280,7 +274,7 @@ end
-- get a mounted peripheral by type (if multiple, returns the first)
---@param name string type name
---@return table|nil device function table
function ppm.get_device(name)
ppm.get_device = function (name)
local device = nil
for side, data in pairs(_ppm_sys.mounts) do
@@ -297,20 +291,17 @@ end
-- get the fission reactor (if multiple, returns the first)
---@return table|nil reactor function table
function ppm.get_fission_reactor()
return ppm.get_device("fissionReactor") or ppm.get_device("fissionReactorLogicAdapter")
ppm.get_fission_reactor = function ()
return ppm.get_device("fissionReactor")
end
-- get the wireless modem (if multiple, returns the first)
--
-- if this is in a CraftOS emulated environment, wired modems will be used instead
---@return table|nil modem function table
function ppm.get_wireless_modem()
ppm.get_wireless_modem = function ()
local w_modem = nil
local emulated_env = periphemu ~= nil
for _, device in pairs(_ppm_sys.mounts) do
if device.type == "modem" and (emulated_env or device.dev.isWireless()) then
if device.type == "modem" and device.dev.isWireless() then
w_modem = device.dev
break
end
@@ -321,16 +312,8 @@ end
-- list all connected monitors
---@return table monitors
function ppm.get_monitor_list()
local list = {}
for iface, device in pairs(_ppm_sys.mounts) do
if device.type == "monitor" then
list[iface] = device
end
end
return list
ppm.list_monitors = function ()
return ppm.get_all_devices("monitor")
end
return ppm

View File

@@ -1,57 +0,0 @@
--
-- Publisher-Subscriber Interconnect Layer
--
local psil = {}
-- instantiate a new PSI layer
function psil.create()
local self = {
ic = {}
}
-- allocate a new interconnect field
---@key string data key
local function alloc(key)
self.ic[key] = { subscribers = {}, value = nil }
end
---@class psil
local public = {}
-- subscribe to a data object in the interconnect
--
-- will call func() right away if a value is already avaliable
---@param key string data key
---@param func function function to call on change
function public.subscribe(key, func)
-- allocate new key if not found or notify if value is found
if self.ic[key] == nil then
alloc(key)
elseif self.ic[key].value ~= nil then
func(self.ic[key].value)
end
-- subscribe to key
table.insert(self.ic[key].subscribers, { notify = func })
end
-- publish data to a given key, passing it to all subscribers if it has changed
---@param key string data key
---@param value any data value
function public.publish(key, value)
if self.ic[key] == nil then alloc(key) end
if self.ic[key].value ~= value then
for i = 1, #self.ic[key].subscribers do
self.ic[key].subscribers[i].notify(value)
end
end
self.ic[key].value = value
end
return public
end
return psil

View File

@@ -2,8 +2,6 @@
-- Redstone I/O
--
local util = require("scada-common.util")
local rsio = {}
----------------------
@@ -79,7 +77,7 @@ rsio.IO = RS_IO
-- channel to string
---@param channel RS_IO
function rsio.to_string(channel)
rsio.to_string = function (channel)
local names = {
"F_SCRAM",
"R_SCRAM",
@@ -103,7 +101,7 @@ function rsio.to_string(channel)
"R_PLC_TIMEOUT"
}
if util.is_int(channel) and channel > 0 and channel <= #names then
if type(channel) == "number" and channel > 0 and channel <= #names then
return names[channel]
else
return ""
@@ -162,7 +160,7 @@ local RS_DIO_MAP = {
-- get the mode of a channel
---@param channel RS_IO
---@return IO_MODE
function rsio.get_io_mode(channel)
rsio.get_io_mode = function (channel)
local modes = {
IO_MODE.DIGITAL_IN, -- F_SCRAM
IO_MODE.DIGITAL_IN, -- R_SCRAM
@@ -186,7 +184,7 @@ function rsio.get_io_mode(channel)
IO_MODE.DIGITAL_OUT -- R_PLC_TIMEOUT
}
if util.is_int(channel) and channel > 0 and channel <= #modes then
if type(channel) == "number" and channel > 0 and channel <= #modes then
return modes[channel]
else
return IO_MODE.ANALOG_IN
@@ -202,14 +200,14 @@ local RS_SIDES = rs.getSides()
-- check if a channel is valid
---@param channel RS_IO
---@return boolean valid
function rsio.is_valid_channel(channel)
return util.is_int(channel) and (channel > 0) and (channel <= RS_IO.R_PLC_TIMEOUT)
rsio.is_valid_channel = function (channel)
return (type(channel) == "number") and (channel > 0) and (channel <= RS_IO.R_PLC_TIMEOUT)
end
-- check if a side is valid
---@param side string
---@return boolean valid
function rsio.is_valid_side(side)
rsio.is_valid_side = function (side)
if side ~= nil then
for i = 0, #RS_SIDES do
if RS_SIDES[i] == side then return true end
@@ -221,8 +219,8 @@ end
-- check if a color is a valid single color
---@param color integer
---@return boolean valid
function rsio.is_color(color)
return util.is_int(color) and (color > 0) and (_B_AND(color, (color - 1)) == 0);
rsio.is_color = function (color)
return (type(color) == "number") and (color > 0) and (_B_AND(color, (color - 1)) == 0);
end
-----------------
@@ -232,7 +230,7 @@ end
-- get digital IO level reading
---@param rs_value boolean
---@return IO_LVL
function rsio.digital_read(rs_value)
rsio.digital_read = function (rs_value)
if rs_value then
return IO_LVL.HIGH
else
@@ -244,8 +242,8 @@ end
---@param channel RS_IO
---@param level IO_LVL
---@return boolean
function rsio.digital_write(channel, level)
if (not util.is_int(channel)) or (channel < RS_IO.F_ALARM) or (channel > RS_IO.R_PLC_TIMEOUT) then
rsio.digital_write = function (channel, level)
if type(channel) ~= "number" or channel < RS_IO.F_ALARM or channel > RS_IO.R_PLC_TIMEOUT then
return false
else
return RS_DIO_MAP[channel]._f(level)
@@ -256,8 +254,8 @@ end
---@param channel RS_IO
---@param level IO_LVL
---@return boolean
function rsio.digital_is_active(channel, level)
if (not util.is_int(channel)) or (channel > RS_IO.R_ENABLE) then
rsio.digital_is_active = function (channel, level)
if type(channel) ~= "number" or channel > RS_IO.R_ENABLE then
return false
else
return RS_DIO_MAP[channel]._f(level)
@@ -273,7 +271,7 @@ end
---@param min number minimum of range
---@param max number maximum of range
---@return number value scaled reading (min to max)
function rsio.analog_read(rs_value, min, max)
rsio.analog_read = function (rs_value, min, max)
local value = rs_value / 15
return (value * (max - min)) + min
end
@@ -283,7 +281,7 @@ end
---@param min number minimum of range
---@param max number maximum of range
---@return number rs_value scaled redstone reading (0 to 15)
function rsio.analog_write(value, min, max)
rsio.analog_write = function (value, min, max)
local scaled_value = (value - min) / (max - min)
return scaled_value * 15
end

View File

@@ -1,26 +0,0 @@
--
-- Timer Callback Dispatcher
--
local tcallbackdsp = {}
local registry = {}
-- request a function to be called after the specified time
---@param time number seconds
---@param f function callback function
function tcallbackdsp.dispatch(time, f)
---@diagnostic disable-next-line: undefined-field
registry[os.startTimer(time)] = { callback = f }
end
-- lookup a timer event and execute the callback if found
---@param event integer timer event timer ID
function tcallbackdsp.handle(event)
if registry[event] ~= nil then
registry[event].callback()
registry[event] = nil
end
end
return tcallbackdsp

View File

@@ -22,10 +22,6 @@ local types = {}
---@field reactor integer
---@field rsio table|nil
-- ALIASES --
---@alias color integer
-- ENUMERATION TYPES --
---@alias TRI_FAIL integer
@@ -37,52 +33,6 @@ types.TRI_FAIL = {
-- STRING TYPES --
---@alias os_event
---| "alarm"
---| "char"
---| "computer_command"
---| "disk"
---| "disk_eject"
---| "http_check"
---| "http_failure"
---| "http_success"
---| "key"
---| "key_up"
---| "modem_message"
---| "monitor_resize"
---| "monitor_touch"
---| "mouse_click"
---| "mouse_drag"
---| "mouse_scroll"
---| "mouse_up"
---| "paste"
---| "peripheral"
---| "peripheral_detach"
---| "rednet_message"
---| "redstone"
---| "speaker_audio_empty"
---| "task_complete"
---| "term_resize"
---| "terminate"
---| "timer"
---| "turtle_inventory"
---| "websocket_closed"
---| "websocket_failure"
---| "websocket_message"
---| "websocket_success"
---@alias rps_trip_cause
---| "ok"
---| "dmg_crit"
---| "high_temp"
---| "no_coolant"
---| "full_waste"
---| "heated_coolant_backup"
---| "no_fuel"
---| "fault"
---| "timeout"
---| "manual"
---@alias rtu_t string
types.rtu_t = {
redstone = "redstone",
@@ -91,10 +41,7 @@ types.rtu_t = {
turbine = "turbine",
turbine_valve = "turbine_valve",
energy_machine = "emachine",
induction_matrix = "induction_matrix",
sps = "sps",
sna = "sna",
env_detector = "environment_detector"
induction_matrix = "induction_matrix"
}
---@alias rps_status_t string

View File

@@ -5,40 +5,29 @@
---@class util
local util = {}
-- OPERATORS --
-- trinary operator
---@param cond boolean condition
---@param a any return if true
---@param b any return if false
---@return any value
function util.trinary(cond, a, b)
if cond then return a else return b end
end
-- PRINT --
-- print
---@param message any
function util.print(message)
util.print = function (message)
term.write(tostring(message))
end
-- print line
---@param message any
function util.println(message)
util.println = function (message)
print(tostring(message))
end
-- timestamped print
---@param message any
function util.print_ts(message)
util.print_ts = function (message)
term.write(os.date("[%H:%M:%S] ") .. tostring(message))
end
-- timestamped print line
---@param message any
function util.println_ts(message)
util.println_ts = function (message)
print(os.date("[%H:%M:%S] ") .. tostring(message))
end
@@ -47,7 +36,7 @@ end
-- get a value as a string
---@param val any
---@return string
function util.strval(val)
util.strval = function (val)
local t = type(val)
if t == "table" or t == "function" then
return "[" .. tostring(val) .. "]"
@@ -56,88 +45,10 @@ function util.strval(val)
end
end
-- repeat a string n times
---@param str string
---@param n integer
---@return string
function util.strrep(str, n)
local repeated = ""
for _ = 1, n do
repeated = repeated .. str
end
return repeated
end
-- repeat a space n times
---@param n integer
---@return string
function util.spaces(n)
return util.strrep(" ", n)
end
-- pad text to a minimum width
---@param str string text
---@param n integer minimum width
---@return string
function util.pad(str, n)
local len = string.len(str)
local lpad = math.floor((n - len) / 2)
local rpad = (n - len) - lpad
return util.spaces(lpad) .. str .. util.spaces(rpad)
end
-- wrap a string into a table of lines, supporting single dash splits
---@param str string
---@param limit integer line limit
---@return table lines
function util.strwrap(str, limit)
local lines = {}
local ln_start = 1
local first_break = str:find("([%-%s]+)")
if first_break ~= nil then
lines[1] = string.sub(str, 1, first_break - 1)
else
lines[1] = str
end
---@diagnostic disable-next-line: discard-returns
str:gsub("(%s+)()(%S+)()",
function(space, start, word, stop)
-- support splitting SINGLE DASH words
word:gsub("(%S+)(%-)()(%S+)()",
function (pre, dash, d_start, post, d_stop)
if (stop + d_stop) - ln_start <= limit then
-- do nothing, it will entirely fit
elseif ((start + d_start) + 1) - ln_start <= limit then
-- we can fit including the dash
lines[#lines] = lines[#lines] .. space .. pre .. dash
-- drop the space and replace the word with the post
space = ""
word = post
-- force a wrap
stop = limit + 1 + ln_start
-- change start position for new line start
start = start + d_start - 1
end
end)
-- can we append this or do we have to start a new line?
if stop - ln_start > limit then
-- starting new line
ln_start = start
lines[#lines + 1] = word
else lines[#lines] = lines[#lines] .. space .. word end
end)
return lines
end
-- concatenation with built-in to string
---@vararg any
---@return string
function util.concat(...)
util.concat = function (...)
local str = ""
for _, v in ipairs(arg) do
str = str .. util.strval(v)
@@ -145,69 +56,41 @@ function util.concat(...)
return str
end
-- alias
util.c = util.concat
-- sprintf implementation
---@param format string
---@vararg any
function util.sprintf(format, ...)
util.sprintf = function (format, ...)
return string.format(format, table.unpack(arg))
end
-- MATH --
-- is a value an integer
---@param x any value
---@return boolean is_integer if the number is an integer
function util.is_int(x)
return type(x) == "number" and x == math.floor(x)
end
-- round a number to an integer
---@return integer rounded
function util.round(x)
return math.floor(x + 0.5)
end
-- TIME --
-- current time
---@return integer milliseconds
function util.time_ms()
util.time_ms = function ()
---@diagnostic disable-next-line: undefined-field
return os.epoch('local')
end
-- current time
---@return number seconds
function util.time_s()
util.time_s = function ()
---@diagnostic disable-next-line: undefined-field
return os.epoch('local') / 1000.0
end
-- current time
---@return integer milliseconds
function util.time()
util.time = function ()
return util.time_ms()
end
-- OS --
-- OS pull event raw wrapper with types
---@param target_event? string event to wait for
---@return os_event event, any param1, any param2, any param3, any param4, any param5
function util.pull_event(target_event)
---@diagnostic disable-next-line: undefined-field
return os.pullEventRaw(target_event)
end
-- PARALLELIZATION --
-- protected sleep call so we still are in charge of catching termination
---@param t integer seconds
--- EVENT_CONSUMER: this function consumes events
function util.psleep(t)
util.psleep = function (t)
---@diagnostic disable-next-line: undefined-field
pcall(os.sleep, t)
end
@@ -215,7 +98,7 @@ end
-- no-op to provide a brief pause (1 tick) to yield
---
--- EVENT_CONSUMER: this function consumes events
function util.nop()
util.nop = function ()
util.psleep(0.05)
end
@@ -223,8 +106,8 @@ end
---@param target_timing integer minimum amount of milliseconds to wait for
---@param last_update integer millisecond time of last update
---@return integer time_now
--- EVENT_CONSUMER: this function consumes events
function util.adaptive_delay(target_timing, last_update)
-- EVENT_CONSUMER: this function consumes events
util.adaptive_delay = function (target_timing, last_update)
local sleep_for = target_timing - (util.time() - last_update)
-- only if >50ms since worker loops already yield 0.05s
if sleep_for >= 50 then
@@ -241,7 +124,7 @@ end
---@param t table table to remove elements from
---@param f function should return false to delete an element when passed the element: f(elem) = true|false
---@param on_delete? function optional function to execute on deletion, passed the table element to be deleted as the parameter
function util.filter_table(t, f, on_delete)
util.filter_table = function (t, f, on_delete)
local move_to = 1
for i = 1, #t do
local element = t[i]
@@ -263,7 +146,7 @@ end
-- check if a table contains the provided element
---@param t table table to check
---@param element any element to check for
function util.table_contains(t, element)
util.table_contains = function (t, element)
for i = 1, #t do
if t[i] == element then return true end
end
@@ -308,7 +191,7 @@ end
---@param timeout number timeout duration
---
--- triggers a timer event if not fed within 'timeout' seconds
function util.new_watchdog(timeout)
util.new_watchdog = function (timeout)
---@diagnostic disable-next-line: undefined-field
local start_timer = os.startTimer
---@diagnostic disable-next-line: undefined-field
@@ -323,12 +206,12 @@ function util.new_watchdog(timeout)
local public = {}
---@param timer number timer event timer ID
function public.is_timer(timer)
public.is_timer = function (timer)
return self.wd_timer == timer
end
-- satiate the beast
function public.feed()
public.feed = function ()
if self.wd_timer ~= nil then
cancel_timer(self.wd_timer)
end
@@ -336,7 +219,7 @@ function util.new_watchdog(timeout)
end
-- cancel the watchdog
function public.cancel()
public.cancel = function ()
if self.wd_timer ~= nil then
cancel_timer(self.wd_timer)
end
@@ -351,7 +234,7 @@ end
---@param period number clock period
---
--- fires a timer event at the specified period, does not start at construct time
function util.new_clock(period)
util.new_clock = function (period)
---@diagnostic disable-next-line: undefined-field
local start_timer = os.startTimer
@@ -364,46 +247,16 @@ function util.new_clock(period)
local public = {}
---@param timer number timer event timer ID
function public.is_clock(timer)
public.is_clock = function (timer)
return self.timer == timer
end
-- start the clock
function public.start()
public.start = function ()
self.timer = start_timer(self.period)
end
return public
end
-- create a new type validator
--
-- can execute sequential checks and check valid() to see if it is still valid
function util.new_validator()
local valid = true
---@class validator
local public = {}
function public.assert_type_bool(value) valid = valid and type(value) == "boolean" end
function public.assert_type_num(value) valid = valid and type(value) == "number" end
function public.assert_type_int(value) valid = valid and util.is_int(value) end
function public.assert_type_str(value) valid = valid and type(value) == "string" end
function public.assert_type_table(value) valid = valid and type(value) == "table" end
function public.assert_eq(check, expect) valid = valid and check == expect end
function public.assert_min(check, min) valid = valid and check >= min end
function public.assert_min_ex(check, min) valid = valid and check > min end
function public.assert_max(check, max) valid = valid and check <= max end
function public.assert_max_ex(check, max) valid = valid and check < max end
function public.assert_range(check, min, max) valid = valid and check >= min and check <= max end
function public.assert_range_ex(check, min, max) valid = valid and check > min and check < max end
function public.assert_port(port) valid = valid and type(port) == "number" and port >= 0 and port <= 65535 end
function public.valid() return valid end
return public
end
return util

View File

@@ -1,278 +1,3 @@
local comms = require("scada-common.comms")
local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue")
local util = require("scada-common.util")
local coordinator = {}
local PROTOCOLS = comms.PROTOCOLS
local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES
local SCADA_CRDN_TYPES = comms.SCADA_CRDN_TYPES
local print = util.print
local println = util.println
local print_ts = util.print_ts
local println_ts = util.println_ts
-- retry time constants in ms
local INITIAL_WAIT = 1500
local RETRY_PERIOD = 1000
local PERIODICS = {
KEEP_ALIVE = 2000,
STATUS = 500
}
-- coordinator supervisor session
---@param id integer
---@param in_queue mqueue
---@param out_queue mqueue
---@param facility_units table
function coordinator.new_session(id, in_queue, out_queue, facility_units)
local log_header = "crdn_session(" .. id .. "): "
local self = {
id = id,
in_q = in_queue,
out_q = out_queue,
units = facility_units,
-- connection properties
seq_num = 0,
r_seq_num = nil,
connected = true,
conn_watchdog = util.new_watchdog(3),
last_rtt = 0,
-- periodic messages
periodics = {
last_update = 0,
keep_alive = 0,
status_packet = 0
},
-- when to next retry one of these messages
retry_times = {
builds_packet = (util.time() + 500)
},
-- message acknowledgements
acks = {
builds = true
}
}
-- mark this coordinator session as closed, stop watchdog
local function _close()
self.conn_watchdog.cancel()
self.connected = false
end
-- send a CRDN packet
---@param msg_type SCADA_CRDN_TYPES
---@param msg table
local function _send(msg_type, msg)
local s_pkt = comms.scada_packet()
local c_pkt = comms.crdn_packet()
c_pkt.make(msg_type, msg)
s_pkt.make(self.seq_num, PROTOCOLS.SCADA_CRDN, c_pkt.raw_sendable())
self.out_q.push_packet(s_pkt)
self.seq_num = self.seq_num + 1
end
-- send a SCADA management packet
---@param msg_type SCADA_MGMT_TYPES
---@param msg table
local function _send_mgmt(msg_type, msg)
local s_pkt = comms.scada_packet()
local m_pkt = comms.mgmt_packet()
m_pkt.make(msg_type, msg)
s_pkt.make(self.seq_num, PROTOCOLS.SCADA_MGMT, m_pkt.raw_sendable())
self.out_q.push_packet(s_pkt)
self.seq_num = self.seq_num + 1
end
-- send unit builds
local function _send_builds()
self.acks.builds = false
local builds = {}
for i = 1, #self.units do
local unit = self.units[i] ---@type reactor_unit
builds[unit.get_id()] = unit.get_build()
end
_send(SCADA_CRDN_TYPES.STRUCT_BUILDS, builds)
end
-- send unit statuses
local function _send_status()
local status = {}
for i = 1, #self.units do
local unit = self.units[i] ---@type reactor_unit
status[unit.get_id()] = { unit.get_reactor_status(), unit.get_annunciator(), unit.get_rtu_statuses() }
end
_send(SCADA_CRDN_TYPES.UNIT_STATUSES, status)
end
-- handle a packet
---@param pkt crdn_frame
local function _handle_packet(pkt)
-- check sequence number
if self.r_seq_num == nil then
self.r_seq_num = pkt.scada_frame.seq_num()
elseif self.r_seq_num >= pkt.scada_frame.seq_num() then
log.warning(log_header .. "sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num())
return
else
self.r_seq_num = pkt.scada_frame.seq_num()
end
-- feed watchdog
self.conn_watchdog.feed()
-- process packet
if pkt.scada_frame.protocol() == PROTOCOLS.SCADA_MGMT then
if pkt.type == SCADA_MGMT_TYPES.KEEP_ALIVE then
-- keep alive reply
if pkt.length == 2 then
local srv_start = pkt.data[1]
local coord_send = pkt.data[2]
local srv_now = util.time()
self.last_rtt = srv_now - srv_start
if self.last_rtt > 500 then
log.warning(log_header .. "COORD KEEP_ALIVE round trip time > 500ms (" .. self.last_rtt .. "ms)")
end
-- log.debug(log_header .. "COORD RTT = " .. self.last_rtt .. "ms")
-- log.debug(log_header .. "COORD TT = " .. (srv_now - coord_send) .. "ms")
else
log.debug(log_header .. "SCADA keep alive packet length mismatch")
end
elseif pkt.type == SCADA_MGMT_TYPES.CLOSE then
-- close the session
_close()
else
log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type)
end
elseif pkt.scada_frame.protocol() == PROTOCOLS.SCADA_CRDN then
if pkt.type == SCADA_CRDN_TYPES.STRUCT_BUILDS then
-- acknowledgement to coordinator receiving builds
self.acks.builds = true
else
log.debug(log_header .. "handler received unexpected SCADA_CRDN packet type " .. pkt.type)
end
end
end
---@class coord_session
local public = {}
-- get the session ID
function public.get_id() return self.id end
-- check if a timer matches this session's watchdog
function public.check_wd(timer)
return self.conn_watchdog.is_timer(timer) and self.connected
end
-- close the connection
function public.close()
_close()
_send_mgmt(SCADA_MGMT_TYPES.CLOSE, {})
println("connection to coordinator " .. self.id .. " closed by server")
log.info(log_header .. "session closed by server")
end
-- iterate the session
---@return boolean connected
function public.iterate()
if self.connected then
------------------
-- handle queue --
------------------
local handle_start = util.time()
while self.in_q.ready() and self.connected do
-- get a new message to process
local message = self.in_q.pop()
if message ~= nil then
if message.qtype == mqueue.TYPE.PACKET then
-- handle a packet
_handle_packet(message.message)
elseif message.qtype == mqueue.TYPE.COMMAND then
-- handle instruction
elseif message.qtype == mqueue.TYPE.DATA then
-- instruction with body
end
end
-- max 100ms spent processing queue
if util.time() - handle_start > 100 then
log.warning(log_header .. "exceeded 100ms queue process limit")
break
end
end
-- exit if connection was closed
if not self.connected then
println("connection to coordinator " .. self.id .. " closed by remote host")
log.info(log_header .. "session closed by remote host")
return self.connected
end
----------------------
-- update periodics --
----------------------
local elapsed = util.time() - self.periodics.last_update
local periodics = self.periodics
-- keep alive
periodics.keep_alive = periodics.keep_alive + elapsed
if periodics.keep_alive >= PERIODICS.KEEP_ALIVE then
_send_mgmt(SCADA_MGMT_TYPES.KEEP_ALIVE, { util.time() })
periodics.keep_alive = 0
end
-- unit statuses to coordinator
periodics.status_packet = periodics.status_packet + elapsed
if periodics.status_packet >= PERIODICS.STATUS then
_send_status()
periodics.status_packet = 0
end
self.periodics.last_update = util.time()
---------------------
-- attempt retries --
---------------------
local rtimes = self.retry_times
-- builds packet retry
if not self.acks.builds then
if rtimes.builds_packet - util.time() <= 0 then
_send_builds()
rtimes.builds_packet = util.time() + RETRY_PERIOD
end
end
end
return self.connected
end
return public
end
return coordinator

View File

@@ -1,7 +1,7 @@
local comms = require("scada-common.comms")
local log = require("scada-common.log")
local comms = require("scada-common.comms")
local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue")
local util = require("scada-common.util")
local util = require("scada-common.util")
local plc = {}
@@ -33,7 +33,7 @@ plc.PLC_S_CMDS = PLC_S_CMDS
plc.PLC_S_DATA = PLC_S_DATA
local PERIODICS = {
KEEP_ALIVE = 2000
KEEP_ALIVE = 2.0
}
-- PLC supervisor session
@@ -41,7 +41,7 @@ local PERIODICS = {
---@param for_reactor integer
---@param in_queue mqueue
---@param out_queue mqueue
function plc.new_session(id, for_reactor, in_queue, out_queue)
plc.new_session = function (id, for_reactor, in_queue, out_queue)
local log_header = "plc_session(" .. id .. "): "
local self = {
@@ -86,9 +86,10 @@ function plc.new_session(id, for_reactor, in_queue, out_queue)
sDB = {
last_status_update = 0,
control_state = false,
overridden = false,
degraded = false,
rps_tripped = false,
rps_trip_cause = "ok", ---@type rps_trip_cause
rps_trip_cause = "ok",
---@class rps_status
rps_status = {
dmg_crit = false,
@@ -97,9 +98,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue)
high_temp = false,
no_fuel = false,
no_cool = false,
fault = false,
timeout = false,
manual = false
timed_out = false
},
---@class mek_status
mek_status = {
@@ -147,21 +146,19 @@ function plc.new_session(id, for_reactor, in_queue, out_queue)
-- copy in the RPS status
---@param rps_status table
local function _copy_rps_status(rps_status)
local _copy_rps_status = function (rps_status)
self.sDB.rps_status.dmg_crit = rps_status[1]
self.sDB.rps_status.ex_hcool = rps_status[2]
self.sDB.rps_status.ex_waste = rps_status[3]
self.sDB.rps_status.high_temp = rps_status[4]
self.sDB.rps_status.no_fuel = rps_status[5]
self.sDB.rps_status.no_cool = rps_status[6]
self.sDB.rps_status.fault = rps_status[7]
self.sDB.rps_status.timeout = rps_status[8]
self.sDB.rps_status.manual = rps_status[9]
self.sDB.rps_status.timed_out = rps_status[7]
end
-- copy in the reactor status
---@param mek_data table
local function _copy_status(mek_data)
local _copy_status = function (mek_data)
-- copy status information
self.sDB.mek_status.status = mek_data[1]
self.sDB.mek_status.burn_rate = mek_data[2]
@@ -194,7 +191,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue)
-- copy in the reactor structure
---@param mek_data table
local function _copy_struct(mek_data)
local _copy_struct = function (mek_data)
self.sDB.mek_struct.heat_cap = mek_data[1]
self.sDB.mek_struct.fuel_asm = mek_data[2]
self.sDB.mek_struct.fuel_sa = mek_data[3]
@@ -206,7 +203,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue)
end
-- mark this PLC session as closed, stop watchdog
local function _close()
local _close = function ()
self.plc_conn_watchdog.cancel()
self.connected = false
end
@@ -214,7 +211,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue)
-- send an RPLC packet
---@param msg_type RPLC_TYPES
---@param msg table
local function _send(msg_type, msg)
local _send = function (msg_type, msg)
local s_pkt = comms.scada_packet()
local r_pkt = comms.rplc_packet()
@@ -228,7 +225,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue)
-- send a SCADA management packet
---@param msg_type SCADA_MGMT_TYPES
---@param msg table
local function _send_mgmt(msg_type, msg)
local _send_mgmt = function (msg_type, msg)
local s_pkt = comms.scada_packet()
local m_pkt = comms.mgmt_packet()
@@ -242,7 +239,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue)
-- get an ACK status
---@param pkt rplc_frame
---@return boolean|nil ack
local function _get_ack(pkt)
local _get_ack = function (pkt)
if pkt.length == 1 then
return pkt.data[1]
else
@@ -253,7 +250,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue)
-- handle a packet
---@param pkt rplc_frame
local function _handle_packet(pkt)
local _handle_packet = function (pkt)
-- check sequence number
if self.r_seq_num == nil then
self.r_seq_num = pkt.scada_frame.seq_num()
@@ -281,7 +278,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue)
if pkt.length >= 5 then
self.sDB.last_status_update = pkt.data[1]
self.sDB.control_state = pkt.data[2]
self.sDB.rps_tripped = pkt.data[3]
self.sDB.overridden = pkt.data[3]
self.sDB.degraded = pkt.data[4]
self.sDB.mek_status.heating_rate = pkt.data[5]
@@ -341,7 +338,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue)
end
elseif pkt.type == RPLC_TYPES.RPS_STATUS then
-- RPS status packet received, copy data
if pkt.length == 9 then
if pkt.length == 7 then
local status = pcall(_copy_rps_status, pkt.data)
if status then
-- copied in RPS status data OK
@@ -354,7 +351,8 @@ function plc.new_session(id, for_reactor, in_queue, out_queue)
end
elseif pkt.type == RPLC_TYPES.RPS_ALARM then
-- RPS alarm
if pkt.length == 10 then
self.sDB.overridden = true
if pkt.length == 8 then
self.sDB.rps_tripped = true
self.sDB.rps_trip_cause = pkt.data[1]
local status = pcall(_copy_rps_status, { table.unpack(pkt.data, 2, #pkt.length) })
@@ -393,8 +391,8 @@ function plc.new_session(id, for_reactor, in_queue, out_queue)
log.warning(log_header .. "PLC KEEP_ALIVE round trip time > 500ms (" .. self.last_rtt .. "ms)")
end
-- log.debug(log_header .. "PLC RTT = " .. self.last_rtt .. "ms")
-- log.debug(log_header .. "PLC TT = " .. (srv_now - plc_send) .. "ms")
-- log.debug(log_header .. "PLC RTT = ".. self.last_rtt .. "ms")
-- log.debug(log_header .. "PLC TT = ".. (srv_now - plc_send) .. "ms")
else
log.debug(log_header .. "SCADA keep alive packet length mismatch")
end
@@ -410,13 +408,13 @@ function plc.new_session(id, for_reactor, in_queue, out_queue)
-- PUBLIC FUNCTIONS --
-- get the session ID
function public.get_id() return self.id end
public.get_id = function () return self.id end
-- get the session database
function public.get_db() return self.sDB end
public.get_db = function () return self.sDB end
-- get the reactor structure
function public.get_struct()
public.get_struct = function ()
if self.received_struct then
return self.sDB.mek_struct
else
@@ -425,7 +423,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue)
end
-- get the reactor status
function public.get_status()
public.get_status = function ()
if self.received_status_cache then
return self.sDB.mek_status
else
@@ -434,28 +432,29 @@ function plc.new_session(id, for_reactor, in_queue, out_queue)
end
-- get the reactor RPS status
function public.get_rps()
public.get_rps = function ()
return self.sDB.rps_status
end
-- get the general status information
function public.get_general_status()
public.get_general_status = function ()
return {
self.sDB.last_status_update,
self.sDB.control_state,
self.sDB.rps_tripped,
self.sDB.rps_trip_cause,
self.sDB.degraded
last_status_update = self.sDB.last_status_update,
control_state = self.sDB.control_state,
overridden = self.sDB.overridden,
degraded = self.sDB.degraded,
rps_tripped = self.sDB.rps_tripped,
rps_trip_cause = self.sDB.rps_trip_cause
}
end
-- check if a timer matches this session's watchdog
function public.check_wd(timer)
public.check_wd = function (timer)
return self.plc_conn_watchdog.is_timer(timer) and self.connected
end
-- close the connection
function public.close()
public.close = function ()
_close()
_send_mgmt(SCADA_MGMT_TYPES.CLOSE, {})
println("connection to reactor " .. self.for_reactor .. " PLC closed by server")
@@ -464,7 +463,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue)
-- iterate the session
---@return boolean connected
function public.iterate()
public.iterate = function ()
if self.connected then
------------------
-- handle queue --

View File

@@ -1,20 +1,14 @@
local comms = require("scada-common.comms")
local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue")
local rsio = require("scada-common.rsio")
local util = require("scada-common.util")
local comms = require("scada-common.comms")
local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue")
local rsio = require("scada-common.rsio")
local util = require("scada-common.util")
-- supervisor rtu sessions (svrs)
local svrs_boiler = require("supervisor.session.rtu.boiler")
local svrs_boilerv = require("supervisor.session.rtu.boilerv")
local svrs_boiler = require("supervisor.session.rtu.boiler")
local svrs_emachine = require("supervisor.session.rtu.emachine")
local svrs_envd = require("supervisor.session.rtu.envd")
local svrs_imatrix = require("supervisor.session.rtu.imatrix")
local svrs_redstone = require("supervisor.session.rtu.redstone")
local svrs_sna = require("supervisor.session.rtu.sna")
local svrs_sps = require("supervisor.session.rtu.sps")
local svrs_turbine = require("supervisor.session.rtu.turbine")
local svrs_turbinev = require("supervisor.session.rtu.turbinev")
local svrs_turbine = require("supervisor.session.rtu.turbine")
local rtu = {}
@@ -39,7 +33,7 @@ rtu.RTU_S_CMDS = RTU_S_CMDS
rtu.RTU_S_DATA = RTU_S_DATA
local PERIODICS = {
KEEP_ALIVE = 2000
KEEP_ALIVE = 2.0
}
---@class rs_session_command
@@ -52,16 +46,13 @@ local PERIODICS = {
---@param in_queue mqueue
---@param out_queue mqueue
---@param advertisement table
---@param facility_units table
function rtu.new_session(id, in_queue, out_queue, advertisement, facility_units)
rtu.new_session = function (id, in_queue, out_queue, advertisement)
local log_header = "rtu_session(" .. id .. "): "
local self = {
id = id,
in_q = in_queue,
out_q = out_queue,
modbus_q = mqueue.new(),
f_units = facility_units,
advert = advertisement,
-- connection properties
seq_num = 0,
@@ -69,36 +60,21 @@ function rtu.new_session(id, in_queue, out_queue, advertisement, facility_units)
connected = true,
rtu_conn_watchdog = util.new_watchdog(3),
last_rtt = 0,
-- periodic messages
periodics = {
last_update = 0,
keep_alive = 0
},
rs_io_q = {},
turbine_cmd_q = {},
turbine_cmd_capable = false,
units = {}
}
---@class rtu_session
local public = {}
local function _reset_config()
self.units = {}
self.rs_io_q = {}
self.turbine_cmd_q = {}
self.turbine_cmd_capable = false
end
-- parse the recorded advertisement and create unit sub-sessions
local function _handle_advertisement()
local _handle_advertisement = function ()
self.units = {}
self.rs_io_q = {}
for i = 1, #self.advert do
local unit = nil ---@type unit_session|nil
local rs_in_q = nil ---@type mqueue|nil
local tbv_in_q = nil ---@type mqueue|nil
local unit = nil ---@type unit_session|nil
local rs_in_q = nil ---@type mqueue|nil
---@type rtu_advertisement
local unit_advert = {
@@ -108,67 +84,23 @@ function rtu.new_session(id, in_queue, out_queue, advertisement, facility_units)
rsio = self.advert[i][4]
}
local target_unit = self.f_units[unit_advert.reactor] ---@type reactor_unit
local u_type = unit_advert.type
-- validate unit advertisement
local advert_validator = util.new_validator()
advert_validator.assert_type_int(unit_advert.index)
advert_validator.assert_type_int(unit_advert.reactor)
if u_type == RTU_UNIT_TYPES.REDSTONE then
advert_validator.assert_type_table(unit_advert.rsio)
end
if advert_validator.valid() then
advert_validator.assert_min(unit_advert.index, 1)
advert_validator.assert_min(unit_advert.reactor, 1)
if not advert_validator.valid() then u_type = false end
else
u_type = false
end
-- create unit by type
if u_type == false then
-- validation fail
elseif u_type == RTU_UNIT_TYPES.REDSTONE then
-- redstone
unit, rs_in_q = svrs_redstone.new(self.id, i, unit_advert, self.modbus_q)
if u_type == RTU_UNIT_TYPES.REDSTONE then
unit, rs_in_q = svrs_redstone.new(self.id, i, unit_advert, self.out_q)
elseif u_type == RTU_UNIT_TYPES.BOILER then
-- boiler
unit = svrs_boiler.new(self.id, i, unit_advert, self.modbus_q)
target_unit.add_boiler(unit)
unit = svrs_boiler.new(self.id, i, unit_advert, self.out_q)
elseif u_type == RTU_UNIT_TYPES.BOILER_VALVE then
-- boiler (Mekanism 10.1+)
unit = svrs_boilerv.new(self.id, i, unit_advert, self.modbus_q)
target_unit.add_boiler(unit)
-- @todo Mekanism 10.1+
elseif u_type == RTU_UNIT_TYPES.TURBINE then
-- turbine
unit = svrs_turbine.new(self.id, i, unit_advert, self.modbus_q)
target_unit.add_turbine(unit)
unit = svrs_turbine.new(self.id, i, unit_advert, self.out_q)
elseif u_type == RTU_UNIT_TYPES.TURBINE_VALVE then
-- turbine (Mekanism 10.1+)
unit, tbv_in_q = svrs_turbinev.new(self.id, i, unit_advert, self.modbus_q)
target_unit.add_turbine(unit)
self.turbine_cmd_capable = true
-- @todo Mekanism 10.1+
elseif u_type == RTU_UNIT_TYPES.EMACHINE then
-- mekanism [energy] machine
unit = svrs_emachine.new(self.id, i, unit_advert, self.modbus_q)
unit = svrs_emachine.new(self.id, i, unit_advert, self.out_q)
elseif u_type == RTU_UNIT_TYPES.IMATRIX then
-- induction matrix
unit = svrs_imatrix.new(self.id, i, unit_advert, self.modbus_q)
elseif u_type == RTU_UNIT_TYPES.SPS then
-- super-critical phase shifter
unit = svrs_sps.new(self.id, i, unit_advert, self.modbus_q)
elseif u_type == RTU_UNIT_TYPES.SNA then
-- solar neutron activator
unit = svrs_sna.new(self.id, i, unit_advert, self.modbus_q)
elseif u_type == RTU_UNIT_TYPES.ENV_DETECTOR then
-- environment detector
unit = svrs_envd.new(self.id, i, unit_advert, self.modbus_q)
-- @todo Mekanism 10.1+
else
log.error(log_header .. "bad advertisement: encountered unsupported RTU type")
end
@@ -176,33 +108,21 @@ function rtu.new_session(id, in_queue, out_queue, advertisement, facility_units)
if unit ~= nil then
table.insert(self.units, unit)
if u_type == RTU_UNIT_TYPES.REDSTONE then
if self.rs_io_q[unit_advert.reactor] == nil then
self.rs_io_q[unit_advert.reactor] = rs_in_q
else
_reset_config()
log.error(log_header .. util.c("bad advertisement: duplicate redstone RTU for reactor " .. unit_advert.reactor))
break
end
elseif u_type == RTU_UNIT_TYPES.TURBINE_VALVE then
if self.turbine_cmd_q[unit_advert.reactor] == nil then
self.turbine_cmd_q[unit_advert.reactor] = {}
end
local queues = self.turbine_cmd_q[unit_advert.reactor]
if queues[unit_advert.index] == nil then
queues[unit_advert.index] = tbv_in_q
else
_reset_config()
log.error(log_header .. util.c("bad advertisement: duplicate turbine RTU (same index of ",
unit_advert.index, ") for reactor ", unit_advert.reactor))
break
end
if self.rs_io_q[unit_advert.reactor] == nil then
self.rs_io_q[unit_advert.reactor] = rs_in_q
else
self.units = {}
self.rs_io_q = {}
log.error(log_header .. "bad advertisement: duplicate redstone RTU for reactor " .. unit_advert.reactor)
break
end
else
_reset_config()
local type_string = util.strval(comms.advert_type_to_rtu_t(u_type))
self.units = {}
self.rs_io_q = {}
local type_string = comms.advert_type_to_rtu_t(u_type)
if type_string == nil then type_string = "unknown" end
log.error(log_header .. "bad advertisement: error occured while creating a unit (type is " .. type_string .. ")")
break
end
@@ -210,7 +130,7 @@ function rtu.new_session(id, in_queue, out_queue, advertisement, facility_units)
end
-- mark this RTU session as closed, stop watchdog
local function _close()
local _close = function ()
self.rtu_conn_watchdog.cancel()
self.connected = false
@@ -220,21 +140,10 @@ function rtu.new_session(id, in_queue, out_queue, advertisement, facility_units)
end
end
-- send a MODBUS packet
---@param m_pkt modbus_packet MODBUS packet
local function _send_modbus(m_pkt)
local s_pkt = comms.scada_packet()
s_pkt.make(self.seq_num, PROTOCOLS.MODBUS_TCP, m_pkt.raw_sendable())
self.out_q.push_packet(s_pkt)
self.seq_num = self.seq_num + 1
end
-- send a SCADA management packet
---@param msg_type SCADA_MGMT_TYPES
---@param msg table
local function _send_mgmt(msg_type, msg)
local _send_mgmt = function (msg_type, msg)
local s_pkt = comms.scada_packet()
local m_pkt = comms.mgmt_packet()
@@ -247,7 +156,7 @@ function rtu.new_session(id, in_queue, out_queue, advertisement, facility_units)
-- handle a packet
---@param pkt modbus_frame|mgmt_frame
local function _handle_packet(pkt)
local _handle_packet = function (pkt)
-- check sequence number
if self.r_seq_num == nil then
self.r_seq_num = pkt.scada_frame.seq_num()
@@ -281,8 +190,8 @@ function rtu.new_session(id, in_queue, out_queue, advertisement, facility_units)
log.warning(log_header .. "RTU KEEP_ALIVE round trip time > 500ms (" .. self.last_rtt .. "ms)")
end
-- log.debug(log_header .. "RTU RTT = " .. self.last_rtt .. "ms")
-- log.debug(log_header .. "RTU TT = " .. (srv_now - rtu_send) .. "ms")
-- log.debug(log_header .. "RTU RTT = ".. self.last_rtt .. "ms")
-- log.debug(log_header .. "RTU TT = ".. (srv_now - rtu_send) .. "ms")
else
log.debug(log_header .. "SCADA keep alive packet length mismatch")
end
@@ -303,16 +212,16 @@ function rtu.new_session(id, in_queue, out_queue, advertisement, facility_units)
-- PUBLIC FUNCTIONS --
-- get the session ID
function public.get_id() return self.id end
public.get_id = function () return self.id end
-- check if a timer matches this session's watchdog
---@param timer number
function public.check_wd(timer)
public.check_wd = function (timer)
return self.rtu_conn_watchdog.is_timer(timer) and self.connected
end
-- close the connection
function public.close()
public.close = function ()
_close()
_send_mgmt(SCADA_MGMT_TYPES.CLOSE, {})
println(log_header .. "connection to RTU closed by server")
@@ -321,7 +230,7 @@ function rtu.new_session(id, in_queue, out_queue, advertisement, facility_units)
-- iterate the session
---@return boolean connected
function public.iterate()
public.iterate = function ()
if self.connected then
------------------
-- handle queue --
@@ -405,29 +314,11 @@ function rtu.new_session(id, in_queue, out_queue, advertisement, facility_units)
end
self.periodics.last_update = util.time()
----------------------------------------------
-- pass MODBUS packets on to main out queue --
----------------------------------------------
for _ = 1, self.modbus_q.length() do
-- get the next message
local msg = self.modbus_q.pop()
if msg ~= nil then
if msg.qtype == mqueue.TYPE.PACKET then
_send_modbus(msg.message)
end
end
end
end
return self.connected
end
-- handle initial advertisement
_handle_advertisement()
return public
end

View File

@@ -1,6 +1,6 @@
local comms = require("scada-common.comms")
local log = require("scada-common.log")
local types = require("scada-common.types")
local comms = require("scada-common.comms")
local log = require("scada-common.log")
local types = require("scada-common.types")
local unit_session = require("supervisor.session.rtu.unit_session")
@@ -18,7 +18,7 @@ local TXN_TYPES = {
local TXN_TAGS = {
"boiler.build",
"boiler.state",
"boiler.tanks"
"boiler.tanks",
}
local PERIODICS = {
@@ -32,7 +32,7 @@ local PERIODICS = {
---@param unit_id integer
---@param advert rtu_advertisement
---@param out_queue mqueue
function boiler.new(session_id, unit_id, advert, out_queue)
boiler.new = function (session_id, unit_id, advert, out_queue)
-- type check
if advert.type ~= RTU_UNIT_TYPES.BOILER then
log.error("attempt to instantiate boiler RTU for type '" .. advert.type .. "'. this is a bug.")
@@ -47,7 +47,7 @@ function boiler.new(session_id, unit_id, advert, out_queue)
periodics = {
next_build_req = 0,
next_state_req = 0,
next_tanks_req = 0
next_tanks_req = 0,
},
---@class boiler_session_db
db = {
@@ -86,19 +86,19 @@ function boiler.new(session_id, unit_id, advert, out_queue)
-- PRIVATE FUNCTIONS --
-- query the build of the device
local function _request_build()
local _request_build = function ()
-- read input registers 1 through 7 (start = 1, count = 7)
self.session.send_request(TXN_TYPES.BUILD, MODBUS_FCODE.READ_INPUT_REGS, { 1, 7 })
end
-- query the state of the device
local function _request_state()
local _request_state = function ()
-- read input registers 8 through 9 (start = 8, count = 2)
self.session.send_request(TXN_TYPES.STATE, MODBUS_FCODE.READ_INPUT_REGS, { 8, 2 })
end
-- query the tanks of the device
local function _request_tanks()
local _request_tanks = function ()
-- read input registers 10 through 21 (start = 10, count = 12)
self.session.send_request(TXN_TYPES.TANKS, MODBUS_FCODE.READ_INPUT_REGS, { 10, 12 })
end
@@ -107,52 +107,51 @@ function boiler.new(session_id, unit_id, advert, out_queue)
-- handle a packet
---@param m_pkt modbus_frame
function public.handle_packet(m_pkt)
local txn_type = self.session.try_resolve(m_pkt)
public.handle_packet = function (m_pkt)
local txn_type = self.session.try_resolve(m_pkt.txn_id)
if txn_type == false then
-- nothing to do
elseif txn_type == TXN_TYPES.BUILD then
-- build response
-- load in data if correct length
if m_pkt.length == 7 then
self.db.build.boil_cap = m_pkt.data[1]
self.db.build.steam_cap = m_pkt.data[2]
self.db.build.water_cap = m_pkt.data[3]
self.db.build.hcoolant_cap = m_pkt.data[4]
self.db.build.ccoolant_cap = m_pkt.data[5]
self.db.build.superheaters = m_pkt.data[6]
self.db.build.boil_cap = m_pkt.data[1]
self.db.build.steam_cap = m_pkt.data[2]
self.db.build.water_cap = m_pkt.data[3]
self.db.build.hcoolant_cap = m_pkt.data[4]
self.db.build.ccoolant_cap = m_pkt.data[5]
self.db.build.superheaters = m_pkt.data[6]
self.db.build.max_boil_rate = m_pkt.data[7]
self.has_build = true
else
log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")")
log.debug(log_tag .. "MODBUS transaction reply length mismatch (boiler.build)")
end
elseif txn_type == TXN_TYPES.STATE then
-- state response
-- load in data if correct length
if m_pkt.length == 2 then
self.db.state.temperature = m_pkt.data[1]
self.db.state.boil_rate = m_pkt.data[2]
self.db.state.boil_rate = m_pkt.data[2]
else
log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")")
log.debug(log_tag .. "MODBUS transaction reply length mismatch (boiler.state)")
end
elseif txn_type == TXN_TYPES.TANKS then
-- tanks response
-- load in data if correct length
if m_pkt.length == 12 then
self.db.tanks.steam = m_pkt.data[1]
self.db.tanks.steam = m_pkt.data[1]
self.db.tanks.steam_need = m_pkt.data[2]
self.db.tanks.steam_fill = m_pkt.data[3]
self.db.tanks.water = m_pkt.data[4]
self.db.tanks.water = m_pkt.data[4]
self.db.tanks.water_need = m_pkt.data[5]
self.db.tanks.water_fill = m_pkt.data[6]
self.db.tanks.hcool = m_pkt.data[7]
self.db.tanks.hcool = m_pkt.data[7]
self.db.tanks.hcool_need = m_pkt.data[8]
self.db.tanks.hcool_fill = m_pkt.data[9]
self.db.tanks.ccool = m_pkt.data[10]
self.db.tanks.ccool = m_pkt.data[10]
self.db.tanks.ccool_need = m_pkt.data[11]
self.db.tanks.ccool_fill = m_pkt.data[12]
else
log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")")
log.debug(log_tag .. "MODBUS transaction reply length mismatch (boiler.tanks)")
end
elseif txn_type == nil then
log.error(log_tag .. "unknown transaction reply")
@@ -163,8 +162,8 @@ function boiler.new(session_id, unit_id, advert, out_queue)
-- update this runner
---@param time_now integer milliseconds
function public.update(time_now)
if not self.has_build and self.periodics.next_build_req <= time_now then
public.update = function (time_now)
if not self.periodics.has_build and self.periodics.next_build_req <= time_now then
_request_build()
self.periodics.next_build_req = time_now + PERIODICS.BUILD
end
@@ -178,12 +177,10 @@ function boiler.new(session_id, unit_id, advert, out_queue)
_request_tanks()
self.periodics.next_tanks_req = time_now + PERIODICS.TANKS
end
self.session.post_update()
end
-- get the unit session database
function public.get_db() return self.db end
public.get_db = function () return self.db end
return public
end

View File

@@ -1,230 +0,0 @@
local comms = require("scada-common.comms")
local log = require("scada-common.log")
local types = require("scada-common.types")
local util = require("scada-common.util")
local unit_session = require("supervisor.session.rtu.unit_session")
local boilerv = {}
local RTU_UNIT_TYPES = comms.RTU_UNIT_TYPES
local MODBUS_FCODE = types.MODBUS_FCODE
local TXN_TYPES = {
FORMED = 1,
BUILD = 2,
STATE = 3,
TANKS = 4
}
local TXN_TAGS = {
"boilerv.formed",
"boilerv.build",
"boilerv.state",
"boilerv.tanks"
}
local PERIODICS = {
FORMED = 2000,
BUILD = 1000,
STATE = 500,
TANKS = 1000
}
-- create a new boilerv rtu session runner
---@param session_id integer
---@param unit_id integer
---@param advert rtu_advertisement
---@param out_queue mqueue
function boilerv.new(session_id, unit_id, advert, out_queue)
-- type check
if advert.type ~= RTU_UNIT_TYPES.BOILER_VALVE then
log.error("attempt to instantiate boilerv RTU for type '" .. advert.type .. "'. this is a bug.")
return nil
end
local log_tag = "session.rtu(" .. session_id .. ").boilerv(" .. advert.index .. "): "
local self = {
session = unit_session.new(unit_id, advert, out_queue, log_tag, TXN_TAGS),
has_build = false,
periodics = {
next_formed_req = 0,
next_build_req = 0,
next_state_req = 0,
next_tanks_req = 0
},
---@class boilerv_session_db
db = {
formed = false,
build = {
length = 0,
width = 0,
height = 0,
min_pos = { x = 0, y = 0, z = 0 }, ---@type coordinate
max_pos = { x = 0, y = 0, z = 0 }, ---@type coordinate
boil_cap = 0.0,
steam_cap = 0,
water_cap = 0,
hcoolant_cap = 0,
ccoolant_cap = 0,
superheaters = 0,
max_boil_rate = 0.0,
env_loss = 0.0
},
state = {
temperature = 0.0,
boil_rate = 0.0
},
tanks = {
steam = { type = "mekanism:empty_gas", amount = 0 }, ---@type tank_fluid
steam_need = 0,
steam_fill = 0.0,
water = { type = "mekanism:empty_gas", amount = 0 }, ---@type tank_fluid
water_need = 0,
water_fill = 0.0,
hcool = { type = "mekanism:empty_gas", amount = 0 }, ---@type tank_fluid
hcool_need = 0,
hcool_fill = 0.0,
ccool = { type = "mekanism:empty_gas", amount = 0 }, ---@type tank_fluid
ccool_need = 0,
ccool_fill = 0.0
}
}
}
local public = self.session.get()
-- PRIVATE FUNCTIONS --
-- query if the multiblock is formed
local function _request_formed()
-- read discrete input 1 (start = 1, count = 1)
self.session.send_request(TXN_TYPES.FORMED, MODBUS_FCODE.READ_DISCRETE_INPUTS, { 1, 1 })
end
-- query the build of the device
local function _request_build()
-- read input registers 1 through 13 (start = 1, count = 13)
self.session.send_request(TXN_TYPES.BUILD, MODBUS_FCODE.READ_INPUT_REGS, { 1, 13 })
end
-- query the state of the device
local function _request_state()
-- read input registers 14 through 15 (start = 14, count = 2)
self.session.send_request(TXN_TYPES.STATE, MODBUS_FCODE.READ_INPUT_REGS, { 14, 2 })
end
-- query the tanks of the device
local function _request_tanks()
-- read input registers 16 through 27 (start = 16, count = 12)
self.session.send_request(TXN_TYPES.TANKS, MODBUS_FCODE.READ_INPUT_REGS, { 16, 12 })
end
-- PUBLIC FUNCTIONS --
-- handle a packet
---@param m_pkt modbus_frame
function public.handle_packet(m_pkt)
local txn_type = self.session.try_resolve(m_pkt)
if txn_type == false then
-- nothing to do
elseif txn_type == TXN_TYPES.FORMED then
-- formed response
-- load in data if correct length
if m_pkt.length == 1 then
self.db.formed = m_pkt.data[1]
else
log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")")
end
elseif txn_type == TXN_TYPES.BUILD then
-- build response
-- load in data if correct length
if m_pkt.length == 13 then
self.db.build.length = m_pkt.data[1]
self.db.build.width = m_pkt.data[2]
self.db.build.height = m_pkt.data[3]
self.db.build.min_pos = m_pkt.data[4]
self.db.build.max_pos = m_pkt.data[5]
self.db.build.boil_cap = m_pkt.data[6]
self.db.build.steam_cap = m_pkt.data[7]
self.db.build.water_cap = m_pkt.data[8]
self.db.build.hcoolant_cap = m_pkt.data[9]
self.db.build.ccoolant_cap = m_pkt.data[10]
self.db.build.superheaters = m_pkt.data[11]
self.db.build.max_boil_rate = m_pkt.data[12]
self.db.build.env_loss = m_pkt.data[13]
self.has_build = true
else
log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")")
end
elseif txn_type == TXN_TYPES.STATE then
-- state response
-- load in data if correct length
if m_pkt.length == 2 then
self.db.state.temperature = m_pkt.data[1]
self.db.state.boil_rate = m_pkt.data[2]
else
log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")")
end
elseif txn_type == TXN_TYPES.TANKS then
-- tanks response
-- load in data if correct length
if m_pkt.length == 12 then
self.db.tanks.steam = m_pkt.data[1]
self.db.tanks.steam_need = m_pkt.data[2]
self.db.tanks.steam_fill = m_pkt.data[3]
self.db.tanks.water = m_pkt.data[4]
self.db.tanks.water_need = m_pkt.data[5]
self.db.tanks.water_fill = m_pkt.data[6]
self.db.tanks.hcool = m_pkt.data[7]
self.db.tanks.hcool_need = m_pkt.data[8]
self.db.tanks.hcool_fill = m_pkt.data[9]
self.db.tanks.ccool = m_pkt.data[10]
self.db.tanks.ccool_need = m_pkt.data[11]
self.db.tanks.ccool_fill = m_pkt.data[12]
else
log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")")
end
elseif txn_type == nil then
log.error(log_tag .. "unknown transaction reply")
else
log.error(log_tag .. "unknown transaction type " .. txn_type)
end
end
-- update this runner
---@param time_now integer milliseconds
function public.update(time_now)
if self.periodics.next_formed_req <= time_now then
_request_formed()
self.periodics.next_formed_req = time_now + PERIODICS.FORMED
end
if self.db.formed then
if not self.has_build and self.periodics.next_build_req <= time_now then
_request_build()
self.periodics.next_build_req = time_now + PERIODICS.BUILD
end
if self.periodics.next_state_req <= time_now then
_request_state()
self.periodics.next_state_req = time_now + PERIODICS.STATE
end
if self.periodics.next_tanks_req <= time_now then
_request_tanks()
self.periodics.next_tanks_req = time_now + PERIODICS.TANKS
end
end
self.session.post_update()
end
-- get the unit session database
function public.get_db() return self.db end
return public
end
return boilerv

Some files were not shown because too many files have changed in this diff Show More