#143 #103 #101 #102 work in progress auto control, added coordinator controls, save/auto load configuration, auto enable/disable on reactor PLC for auto control (untested)

This commit is contained in:
Mikayla Fischler
2023-01-26 18:26:26 -05:00
parent e808ee2be0
commit e9562a140c
17 changed files with 750 additions and 161 deletions

View File

@@ -2,10 +2,10 @@ 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 process = require("coordinator.process")
local apisessions = require("coordinator.apisessions")
local iocontrol = require("coordinator.iocontrol")
local process = require("coordinator.process")
local dialog = require("coordinator.ui.dialog")
@@ -20,6 +20,7 @@ local ESTABLISH_ACK = comms.ESTABLISH_ACK
local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES
local SCADA_CRDN_TYPES = comms.SCADA_CRDN_TYPES
local UNIT_COMMANDS = comms.UNIT_COMMANDS
local FAC_COMMANDS = comms.FAC_COMMANDS
local coordinator = {}
@@ -313,11 +314,25 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, sv_wa
return self.sv_linked
end
-- send a facility command
---@param cmd FAC_COMMANDS command
function public.send_fac_command(cmd)
_send_sv(PROTOCOLS.SCADA_CRDN, SCADA_CRDN_TYPES.FAC_CMD, { cmd })
end
-- send the auto process control configuration with a start command
---@param config coord_auto_config configuration
function public.send_auto_start(config)
_send_sv(PROTOCOLS.SCADA_CRDN, SCADA_CRDN_TYPES.FAC_CMD, {
FAC_COMMANDS.START, config.mode, config.burn_target, config.charge_target, config.gen_target, config.limits
})
end
-- send a unit command
---@param cmd UNIT_COMMANDS command
---@param unit integer unit ID
---@param option any? optional options (like burn rate)
function public.send_command(cmd, unit, option)
---@param option any? optional option options for the optional options (like burn rate) (does option still look like a word?)
function public.send_unit_command(cmd, unit, option)
_send_sv(PROTOCOLS.SCADA_CRDN, SCADA_CRDN_TYPES.UNIT_CMD, { cmd, unit, option })
end
@@ -412,6 +427,26 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, sv_wa
end
elseif packet.type == SCADA_CRDN_TYPES.FAC_CMD then
-- facility command acknowledgement
if packet.length >= 2 then
local cmd = packet.data[1]
local ack = packet.data[2] == true
if cmd == FAC_COMMANDS.SCRAM_ALL then
iocontrol.get_db().facility.scram_ack(ack)
elseif cmd == FAC_COMMANDS.STOP then
iocontrol.get_db().facility.stop_ack(ack)
elseif cmd == FAC_COMMANDS.START then
if packet.length == 7 then
process.start_ack_handle({ table.unpack(packet.data, 2) })
else
log.debug("SCADA_CRDN process start (with configuration) ack echo packet length mismatch")
end
else
log.debug(util.c("received facility command ack with unknown command ", cmd))
end
else
log.debug("SCADA_CRDN facility command ack packet length mismatch")
end
elseif packet.type == SCADA_CRDN_TYPES.UNIT_BUILDS then
-- record builds
if iocontrol.record_unit_builds(packet.data) then

View File

@@ -24,9 +24,17 @@ function iocontrol.init(conf, comms)
---@class ioctl_facility
io.facility = {
auto_active = false,
scram = false,
auto_ramping = false,
auto_scram = false,
auto_scram_cause = "ok", ---@type auto_scram_cause
num_units = conf.num_units, ---@type integer
save_cfg_ack = function (success) end, ---@param success boolean
start_ack = function (success) end, ---@param success boolean
stop_ack = function (success) end, ---@param success boolean
scram_ack = function (success) end, ---@param success boolean
num_units = conf.num_units, ---@type integer
ps = psil.create(),
induction_ps_tbl = {},
@@ -69,7 +77,6 @@ function iocontrol.init(conf, comms)
set_waste = function (mode) process.set_waste(i, mode) end, ---@param mode integer waste processing mode
set_group = function (grp) process.set_group(i, grp) end, ---@param grp integer|0 group ID or 0
set_limit = function (lim) process.set_limit(i, lim) end, ---@param lim number burn rate limit
start_ack = function (success) end, ---@param success boolean
scram_ack = function (success) end, ---@param success boolean
@@ -195,7 +202,7 @@ function iocontrol.record_unit_builds(builds)
-- reactor build
if type(build.reactor) == "table" then
unit.reactor_data.mek_struct = build.reactor
unit.reactor_data.mek_struct = build.reactor ---@type mek_struct
for key, val in pairs(unit.reactor_data.mek_struct) do
unit.reactor_ps.publish(key, val)
end
@@ -257,11 +264,38 @@ function iocontrol.update_facility_status(status)
else
local fac = io.facility
-- auto control status information
local ctl_status = status[1]
if type(ctl_status) == "table" then
fac.auto_active = ctl_status[1] > 0
fac.auto_ramping = ctl_status[2]
fac.auto_scram = ctl_status[3]
fac.auto_scram_cause = ctl_status[4]
fac.ps.publish("auto_active", fac.auto_active)
fac.ps.publish("auto_ramping", fac.auto_ramping)
fac.ps.publish("auto_scram", fac.auto_scram)
fac.ps.publish("auto_scram_cause", fac.auto_scram_cause)
else
log.debug(log_header .. "control status not a table")
end
-- RTU statuses
local rtu_statuses = status[1]
local rtu_statuses = status[2]
if type(rtu_statuses) == "table" then
-- power statistics
if type(rtu_statuses.power) == "table" then
fac.ps.publish("avg_charge", rtu_statuses.power[1])
fac.ps.publish("avg_inflow", rtu_statuses.power[2])
fac.ps.publish("avg_outflow", rtu_statuses.power[3])
else
log.debug(log_header .. "power statistics list not a table")
end
-- induction matricies statuses
if type(rtu_statuses.induction) == "table" then
for id = 1, #fac.induction_ps_tbl do
@@ -328,6 +362,8 @@ function iocontrol.update_unit_statuses(statuses)
log.debug("iocontrol.update_unit_statuses: number of provided unit statuses does not match expected number of units")
return false
else
local burn_rate_sum = 0.0
-- get all unit statuses
for i = 1, #statuses do
local log_header = util.c("iocontrol.update_unit_statuses[unit ", i, "]: ")
@@ -369,6 +405,11 @@ function iocontrol.update_unit_statuses(statuses)
unit.reactor_data.rps_status = rps_status ---@type rps_status
unit.reactor_data.mek_status = mek_status ---@type mek_status
-- if status hasn't been received, mek_status = {}
if type(unit.reactor_data.mek_status.act_burn_rate) == "number" then
burn_rate_sum = burn_rate_sum + unit.reactor_data.mek_status.act_burn_rate
end
if unit.reactor_data.mek_status.status then
unit.reactor_ps.publish("computed_status", 5) -- running
else
@@ -596,8 +637,8 @@ function iocontrol.update_unit_statuses(statuses)
local auto_ctl_state = status[6]
if type(auto_ctl_state) == "table" then
if #auto_ctl_state == 1 then
unit.reactor_ps.publish("burn_limit", auto_ctl_state[1])
if #auto_ctl_state == 0 then
---@todo
else
log.debug(log_header .. "auto control state length mismatch")
end
@@ -606,6 +647,8 @@ function iocontrol.update_unit_statuses(statuses)
end
end
io.facility.ps.publish("burn_sum", burn_rate_sum)
-- update alarm sounder
sounder.eval(io.units)
end

View File

@@ -1,16 +1,28 @@
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 FAC_COMMANDS = comms.FAC_COMMANDS
local UNIT_COMMANDS = comms.UNIT_COMMANDS
local PROCESS = types.PROCESS
---@class process_controller
local process = {}
local self = {
io = nil, ---@type ioctl
comms = nil ---@type coord_comms
comms = nil, ---@type coord_comms
---@class coord_auto_config
config = {
mode = 0, ---@type PROCESS
burn_target = 0.0,
charge_target = 0.0,
gen_target = 0.0,
limits = {} ---@type table
}
}
--------------------------
@@ -24,11 +36,37 @@ function process.init(iocontrol, comms)
self.io = iocontrol
self.comms = comms
for i = 1, self.io.facility.num_units do
self.config.limits[i] = 0.1
end
-- load settings
if not settings.load("/coord.settings") then
log.error("process.init(): failed to load coordinator settings file")
end
local config = settings.get("PROCESS") ---@type coord_auto_config|nil
if type(config) == "table" then
self.config.mode = config.mode
self.config.burn_target = config.burn_target
self.config.charge_target = config.charge_target
self.config.gen_target = config.gen_target
self.config.limits = config.limits
self.io.facility.ps.publish("process_mode", self.config.mode)
self.io.facility.ps.publish("process_burn_target", self.config.burn_target)
self.io.facility.ps.publish("process_charge_target", self.config.charge_target)
self.io.facility.ps.publish("process_gen_target", self.config.gen_target)
for id = 1, math.min(#self.config.limits, self.io.facility.num_units) do
local unit = self.io.units[id] ---@type ioctl_unit
unit.reactor_ps.publish("burn_limit", self.config.limits[id])
end
log.info("PROCESS: loaded auto control settings from coord.settings")
end
local waste_mode = settings.get("WASTE_MODES") ---@type table|nil
if type(waste_mode) == "table" then
@@ -44,7 +82,7 @@ end
---@param id integer unit ID
function process.start(id)
self.io.units[id].control_state = true
self.comms.send_command(UNIT_COMMANDS.START, id)
self.comms.send_unit_command(UNIT_COMMANDS.START, id)
log.debug(util.c("UNIT[", id, "]: START"))
end
@@ -52,14 +90,14 @@ end
---@param id integer unit ID
function process.scram(id)
self.io.units[id].control_state = false
self.comms.send_command(UNIT_COMMANDS.SCRAM, id)
self.comms.send_unit_command(UNIT_COMMANDS.SCRAM, id)
log.debug(util.c("UNIT[", id, "]: SCRAM"))
end
-- reset reactor protection system
---@param id integer unit ID
function process.reset_rps(id)
self.comms.send_command(UNIT_COMMANDS.RESET_RPS, id)
self.comms.send_unit_command(UNIT_COMMANDS.RESET_RPS, id)
log.debug(util.c("UNIT[", id, "]: RESET RPS"))
end
@@ -67,7 +105,7 @@ end
---@param id integer unit ID
---@param rate number burn rate
function process.set_rate(id, rate)
self.comms.send_command(UNIT_COMMANDS.SET_BURN, id, rate)
self.comms.send_unit_command(UNIT_COMMANDS.SET_BURN, id, rate)
log.debug(util.c("UNIT[", id, "]: SET BURN = ", rate))
end
@@ -75,7 +113,7 @@ end
---@param id integer unit ID
---@param mode integer waste mode
function process.set_waste(id, mode)
self.comms.send_command(UNIT_COMMANDS.SET_WASTE, id, mode)
self.comms.send_unit_command(UNIT_COMMANDS.SET_WASTE, id, mode)
log.debug(util.c("UNIT[", id, "]: SET WASTE = ", mode))
local waste_mode = settings.get("WASTE_MODES") ---@type table|nil
@@ -96,7 +134,7 @@ end
-- acknowledge all alarms
---@param id integer unit ID
function process.ack_all_alarms(id)
self.comms.send_command(UNIT_COMMANDS.ACK_ALL_ALARMS, id)
self.comms.send_unit_command(UNIT_COMMANDS.ACK_ALL_ALARMS, id)
log.debug(util.c("UNIT[", id, "]: ACK ALL ALARMS"))
end
@@ -104,7 +142,7 @@ end
---@param id integer unit ID
---@param alarm integer alarm ID
function process.ack_alarm(id, alarm)
self.comms.send_command(UNIT_COMMANDS.ACK_ALARM, id, alarm)
self.comms.send_unit_command(UNIT_COMMANDS.ACK_ALARM, id, alarm)
log.debug(util.c("UNIT[", id, "]: ACK ALARM ", alarm))
end
@@ -112,7 +150,7 @@ end
---@param id integer unit ID
---@param alarm integer alarm ID
function process.reset_alarm(id, alarm)
self.comms.send_command(UNIT_COMMANDS.RESET_ALARM, id, alarm)
self.comms.send_unit_command(UNIT_COMMANDS.RESET_ALARM, id, alarm)
log.debug(util.c("UNIT[", id, "]: RESET ALARM ", alarm))
end
@@ -120,16 +158,86 @@ end
---@param unit_id integer unit ID
---@param group_id integer|0 group ID or 0 for independent
function process.set_group(unit_id, group_id)
self.comms.send_command(UNIT_COMMANDS.SET_GROUP, unit_id, group_id)
self.comms.send_unit_command(UNIT_COMMANDS.SET_GROUP, unit_id, group_id)
log.debug(util.c("UNIT[", unit_id, "]: SET GROUP ", group_id))
end
-- set the burn rate limit
---@param id integer unit ID
---@param limit number burn rate limit
function process.set_limit(id, limit)
self.comms.send_command(UNIT_COMMANDS.SET_LIMIT, id, limit)
log.debug(util.c("UNIT[", id, "]: SET LIMIT = ", limit))
--------------------------
-- AUTO PROCESS CONTROL --
--------------------------
-- facility SCRAM command
function process.fac_scram()
self.comms.send_fac_command(FAC_COMMANDS.SCRAM_ALL)
log.debug("FAC: SCRAM ALL")
end
-- stop automatic process control
function process.stop_auto()
self.comms.send_fac_command(FAC_COMMANDS.STOP)
log.debug("FAC: STOP AUTO")
end
-- start automatic process control
function process.start_auto()
self.comms.send_auto_start(self.config)
log.debug("FAC: START AUTO")
end
-- save process control settings
---@param mode PROCESS control mode
---@param burn_target number burn rate target
---@param charge_target number charge target
---@param gen_target number generation rate target
---@param limits table unit burn rate limits
function process.save(mode, burn_target, charge_target, gen_target, limits)
-- attempt to load settings
if not settings.load("/coord.settings") then
log.warning("process.save(): failed to load coordinator settings file")
end
-- config table
self.config = {
mode = mode,
burn_target = burn_target,
charge_target = charge_target,
gen_target = gen_target,
limits = limits
}
-- save config
settings.set("PROCESS", self.config)
local saved = settings.save("/coord.settings")
if not saved then
log.warning("process.save(): failed to save coordinator settings file")
end
log.debug("saved = " .. util.strval(saved))
self.io.facility.save_cfg_ack(saved)
end
-- handle a start command acknowledgement
---@param response table ack and configuration reply
function process.start_ack_handle(response)
local ack = response[1]
self.config.mode = response[2]
self.config.burn_target = response[3]
self.config.charge_target = response[4]
self.config.gen_target = response[5]
for i = 1, #response[6] do
self.config.limits[i] = response[6][i]
end
self.io.facility.ps.publish("auto_mode", self.config.mode)
self.io.facility.ps.publish("burn_target", self.config.burn_target)
self.io.facility.ps.publish("charge_target", self.config.charge_target)
self.io.facility.ps.publish("gen_target", self.config.gen_target)
self.io.facility.start_ack(ack)
end
--------------------------

View File

@@ -1,6 +1,8 @@
local tcd = require("scada-common.tcallbackdsp")
local util = require("scada-common.util")
local iocontrol = require("coordinator.iocontrol")
local process = require("coordinator.process")
local style = require("coordinator.ui.style")
@@ -36,29 +38,127 @@ local function new_view(root, x, y)
local facility = iocontrol.get_db().facility
local units = iocontrol.get_db().units
local bw_fg_bg = cpair(colors.black, colors.white)
local bw_fg_bg = cpair(colors.black, colors.white)
local hzd_fg_bg = cpair(colors.white, colors.gray)
local dis_colors = cpair(colors.white, colors.lightGray)
local proc = Div{parent=root,width=60,height=24,x=x,y=y}
local main = Div{parent=root,width=80,height=24,x=x,y=y}
local limits = Div{parent=proc,width=40,height=24,x=30,y=1}
local scram = HazardButton{parent=main,x=1,y=1,text="FAC SCRAM",accent=colors.yellow,dis_colors=dis_colors,callback=process.fac_scram,fg_bg=hzd_fg_bg}
facility.scram_ack = scram.on_response
---------------------
-- process control --
---------------------
local proc = Div{parent=main,width=54,height=24,x=27,y=1}
-----------------------------
-- process control targets --
-----------------------------
local targets = Div{parent=proc,width=31,height=24,x=1,y=1}
local burn_tag = Div{parent=targets,x=1,y=1,width=8,height=4,fg_bg=cpair(colors.black,colors.purple)}
TextBox{parent=burn_tag,x=2,y=2,text="Burn Target",width=7,height=2}
local burn_target = Div{parent=targets,x=9,y=1,width=23,height=3,fg_bg=cpair(colors.gray,colors.white)}
local b_target = SpinboxNumeric{parent=burn_target,x=11,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=bw_fg_bg}
TextBox{parent=burn_target,x=18,y=2,text="mB/t"}
local burn_sum = DataIndicator{parent=targets,x=9,y=4,label="",format="%18.1f",value=0,unit="mB/t",commas=true,lu_colors=cpair(colors.black,colors.black),width=23,fg_bg=cpair(colors.black,colors.brown)}
facility.ps.subscribe("process_burn_target", b_target.set_value)
facility.ps.subscribe("burn_sum", burn_sum.update)
local chg_tag = Div{parent=targets,x=1,y=6,width=8,height=4,fg_bg=cpair(colors.black,colors.purple)}
TextBox{parent=chg_tag,x=2,y=2,text="Charge Target",width=7,height=2}
local chg_target = Div{parent=targets,x=9,y=6,width=23,height=3,fg_bg=cpair(colors.gray,colors.white)}
local c_target = SpinboxNumeric{parent=chg_target,x=2,y=1,whole_num_precision=15,fractional_precision=0,min=0,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=bw_fg_bg}
TextBox{parent=chg_target,x=18,y=2,text="kFE"}
local cur_charge = DataIndicator{parent=targets,x=9,y=9,label="",format="%19d",value=0,unit="kFE",commas=true,lu_colors=cpair(colors.black,colors.black),width=23,fg_bg=cpair(colors.black,colors.brown)}
facility.ps.subscribe("process_charge_target", c_target.set_value)
facility.induction_ps_tbl[1].subscribe("energy", function (j) cur_charge.update(util.joules_to_fe(j) / 1000) end)
local gen_tag = Div{parent=targets,x=1,y=11,width=8,height=4,fg_bg=cpair(colors.black,colors.purple)}
TextBox{parent=gen_tag,x=2,y=2,text="Gen. Target",width=7,height=2}
local gen_target = Div{parent=targets,x=9,y=11,width=23,height=3,fg_bg=cpair(colors.gray,colors.white)}
local g_target = SpinboxNumeric{parent=gen_target,x=8,y=1,whole_num_precision=9,fractional_precision=0,min=0,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=bw_fg_bg}
TextBox{parent=gen_target,x=18,y=2,text="kFE/t"}
local cur_gen = DataIndicator{parent=targets,x=9,y=14,label="",format="%17d",value=0,unit="kFE/t",commas=true,lu_colors=cpair(colors.black,colors.black),width=23,fg_bg=cpair(colors.black,colors.brown)}
facility.ps.subscribe("process_gen_target", g_target.set_value)
facility.induction_ps_tbl[1].subscribe("last_input", function (j) cur_gen.update(util.joules_to_fe(j) / 1000) end)
-----------------
-- unit limits --
-----------------
local limit_div = Div{parent=proc,width=40,height=19,x=34,y=6}
local rate_limits = {}
for i = 1, facility.num_units do
local unit = units[i] ---@type ioctl_entry
local unit = units[i] ---@type ioctl_unit
local _y = ((i - 1) * 4) + 1
local _y = ((i - 1) * 5) + 1
TextBox{parent=limits,x=1,y=_y+1,text="Unit "..i}
local lim_ctl = Div{parent=limits,x=8,y=_y,width=20,height=3,fg_bg=cpair(colors.gray,colors.white)}
local burn_rate = SpinboxNumeric{parent=lim_ctl,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,max=0.1,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=bw_fg_bg}
unit.reactor_ps.subscribe("max_burn", burn_rate.set_max)
unit.reactor_ps.subscribe("burn_limit", burn_rate.set_value)
local unit_tag = Div{parent=limit_div,x=1,y=_y,width=8,height=4,fg_bg=cpair(colors.black,colors.lightBlue)}
TextBox{parent=unit_tag,x=2,y=2,text="Unit "..i.." Limit",width=7,height=2}
local lim_ctl = Div{parent=limit_div,x=9,y=_y,width=14,height=3,fg_bg=cpair(colors.gray,colors.white)}
rate_limits[i] = SpinboxNumeric{parent=lim_ctl,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=bw_fg_bg}
TextBox{parent=lim_ctl,x=9,y=2,text="mB/t"}
local set_burn = function () unit.set_limit(burn_rate.get_value()) end
PushButton{parent=lim_ctl,x=14,y=2,text="SAVE",min_width=6,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.white,colors.gray),callback=set_burn}
unit.reactor_ps.subscribe("max_burn", rate_limits[i].set_max)
unit.reactor_ps.subscribe("burn_limit", rate_limits[i].set_value)
local cur_burn = DataIndicator{parent=limit_div,x=9,y=_y+3,label="",format="%7.1f",value=0,unit="mB/t",commas=false,lu_colors=cpair(colors.black,colors.black),width=14,fg_bg=cpair(colors.black,colors.brown)}
unit.reactor_ps.subscribe("act_burn_rate", cur_burn.update)
end
-------------------------
-- controls and status --
-------------------------
local ctl_opts = { "Regulated", "Burn Rate", "Charge Level", "Generation Rate" }
local mode = RadioButton{parent=proc,x=34,y=1,options=ctl_opts,callback=function()end,radio_colors=cpair(colors.purple,colors.black),radio_bg=colors.gray}
facility.ps.subscribe("process_mode", mode.set_value)
local u_stat = Rectangle{parent=proc,border=border(1,colors.gray,true),thin=true,width=31,height=4,x=1,y=16,fg_bg=bw_fg_bg}
local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=31,height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=bw_fg_bg}
local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data",width=31,height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=cpair(colors.gray, colors.white)}
local auto_controls = Div{parent=proc,x=1,y=20,width=31,height=5,fg_bg=cpair(colors.gray,colors.white)}
-- save the automatic process control configuration without starting
local function _save_cfg()
local limits = {}
for i = 1, #rate_limits do limits[i] = rate_limits[i].get_value() end
process.save(mode.get_value(), b_target.get_value(), c_target.get_value(), g_target.get_value(), limits)
end
-- start automatic control after saving process control settings
local function _start_auto()
_save_cfg()
process.start_auto()
end
local save = HazardButton{parent=auto_controls,x=2,y=2,text="SAVE",accent=colors.purple,dis_colors=dis_colors,callback=_save_cfg,fg_bg=hzd_fg_bg}
local start = HazardButton{parent=auto_controls,x=13,y=2,text="START",accent=colors.lightBlue,dis_colors=dis_colors,callback=_start_auto,fg_bg=hzd_fg_bg}
local stop = HazardButton{parent=auto_controls,x=23,y=2,text="STOP",accent=colors.orange,dis_colors=dis_colors,callback=process.stop_auto,fg_bg=hzd_fg_bg}
facility.start_ack = start.on_response
facility.stop_ack = stop.on_response
function facility.save_cfg_ack(ack)
tcd.dispatch(0.2, function () save.on_response(ack) end)
end
end

View File

@@ -83,7 +83,7 @@ local function init(monitor)
-- testing
---@fixme remove test code
ColorMap{parent=main,x=2,y=(main.height()-1)}
ColorMap{parent=main,x=132,y=(main.height()-1)}
local audio = Div{parent=main,width=34,height=15,x=95,y=cnc_y_start}