2021-01-20 09:53:57 +02:00
|
|
|
-- WirePlumber
|
|
|
|
|
--
|
|
|
|
|
-- Copyright © 2021 Collabora Ltd.
|
|
|
|
|
-- @author George Kiagiadakis <george.kiagiadakis@collabora.com>
|
|
|
|
|
--
|
|
|
|
|
-- SPDX-License-Identifier: MIT
|
|
|
|
|
|
2021-02-03 12:53:50 +02:00
|
|
|
-- Receive script arguments from config.lua
|
2022-01-13 11:08:34 +02:00
|
|
|
local config = ... or {}
|
2021-02-15 18:49:57 +02:00
|
|
|
|
|
|
|
|
-- ensure config.properties is not nil
|
|
|
|
|
config.properties = config.properties or {}
|
|
|
|
|
|
2022-05-18 10:51:41 -04:00
|
|
|
-- unique device/node name tables
|
|
|
|
|
device_names_table = nil
|
|
|
|
|
node_names_table = nil
|
|
|
|
|
|
2021-02-15 18:49:57 +02:00
|
|
|
-- preprocess rules and create Interest objects
|
|
|
|
|
for _, r in ipairs(config.rules or {}) do
|
|
|
|
|
r.interests = {}
|
|
|
|
|
for _, i in ipairs(r.matches) do
|
|
|
|
|
local interest_desc = { type = "properties" }
|
|
|
|
|
for _, c in ipairs(i) do
|
|
|
|
|
c.type = "pw"
|
|
|
|
|
table.insert(interest_desc, Constraint(c))
|
|
|
|
|
end
|
|
|
|
|
local interest = Interest(interest_desc)
|
|
|
|
|
table.insert(r.interests, interest)
|
|
|
|
|
end
|
|
|
|
|
r.matches = nil
|
2021-01-20 09:53:57 +02:00
|
|
|
end
|
|
|
|
|
|
2021-02-15 18:49:57 +02:00
|
|
|
-- applies properties from config.rules when asked to
|
|
|
|
|
function rulesApplyProperties(properties)
|
|
|
|
|
for _, r in ipairs(config.rules or {}) do
|
|
|
|
|
if r.apply_properties then
|
|
|
|
|
for _, interest in ipairs(r.interests) do
|
|
|
|
|
if interest:matches(properties) then
|
|
|
|
|
for k, v in pairs(r.apply_properties) do
|
|
|
|
|
properties[k] = v
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
2021-01-20 09:53:57 +02:00
|
|
|
end
|
|
|
|
|
|
2021-02-18 10:23:07 +02:00
|
|
|
function nonempty(str)
|
|
|
|
|
return str ~= "" and str or nil
|
|
|
|
|
end
|
|
|
|
|
|
2021-01-20 09:53:57 +02:00
|
|
|
function createNode(parent, id, type, factory, properties)
|
|
|
|
|
local dev_props = parent.properties
|
2021-02-15 18:49:57 +02:00
|
|
|
|
|
|
|
|
-- set the device id and spa factory name; REQUIRED, do not change
|
|
|
|
|
properties["device.id"] = parent["bound-id"]
|
|
|
|
|
properties["factory.name"] = factory
|
|
|
|
|
|
|
|
|
|
-- set the default pause-on-idle setting
|
|
|
|
|
properties["node.pause-on-idle"] = false
|
|
|
|
|
|
|
|
|
|
-- try to negotiate the max ammount of channels
|
|
|
|
|
if dev_props["api.alsa.use-acp"] ~= "true" then
|
|
|
|
|
properties["audio.channels"] = properties["audio.channels"] or "64"
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local dev = properties["api.alsa.pcm.device"]
|
|
|
|
|
or properties["alsa.device"] or "0"
|
|
|
|
|
local subdev = properties["api.alsa.pcm.subdevice"]
|
|
|
|
|
or properties["alsa.subdevice"] or "0"
|
2021-01-20 09:53:57 +02:00
|
|
|
local stream = properties["api.alsa.pcm.stream"] or "unknown"
|
2021-02-15 18:49:57 +02:00
|
|
|
local profile = properties["device.profile.name"]
|
|
|
|
|
or (stream .. "." .. dev .. "." .. subdev)
|
2021-01-20 09:53:57 +02:00
|
|
|
local profile_desc = properties["device.profile.description"]
|
|
|
|
|
|
2021-02-15 18:49:57 +02:00
|
|
|
-- set priority
|
|
|
|
|
if not properties["priority.driver"] then
|
|
|
|
|
local priority = (dev == "0") and 1000 or 744
|
|
|
|
|
if stream == "capture" then
|
|
|
|
|
priority = priority + 1000
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
priority = priority - (tonumber(dev) * 16) - tonumber(subdev)
|
|
|
|
|
|
|
|
|
|
if profile:find("^analog%-") then
|
|
|
|
|
priority = priority + 9
|
|
|
|
|
elseif profile:find("^iec958%-") then
|
|
|
|
|
priority = priority + 8
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
properties["priority.driver"] = priority
|
|
|
|
|
properties["priority.session"] = priority
|
|
|
|
|
end
|
|
|
|
|
|
2021-01-20 09:53:57 +02:00
|
|
|
-- ensure the node has a media class
|
|
|
|
|
if not properties["media.class"] then
|
|
|
|
|
if stream == "capture" then
|
|
|
|
|
properties["media.class"] = "Audio/Source"
|
|
|
|
|
else
|
|
|
|
|
properties["media.class"] = "Audio/Sink"
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- ensure the node has a name
|
2021-02-15 18:49:57 +02:00
|
|
|
if not properties["node.name"] then
|
|
|
|
|
local name =
|
|
|
|
|
(stream == "capture" and "alsa_input" or "alsa_output")
|
|
|
|
|
.. "." ..
|
|
|
|
|
(dev_props["device.name"]:gsub("^alsa_card%.(.+)", "%1") or
|
|
|
|
|
dev_props["device.name"] or
|
|
|
|
|
"unnamed-device")
|
|
|
|
|
.. "." ..
|
|
|
|
|
profile
|
|
|
|
|
|
2021-02-15 19:18:07 +02:00
|
|
|
-- sanitize name
|
|
|
|
|
name = name:gsub("([^%w_%-%.])", "_")
|
|
|
|
|
|
2021-02-15 18:49:57 +02:00
|
|
|
properties["node.name"] = name
|
|
|
|
|
|
|
|
|
|
-- deduplicate nodes with the same name
|
|
|
|
|
for counter = 2, 99, 1 do
|
2022-05-18 10:51:41 -04:00
|
|
|
if node_names_table[properties["node.name"]] ~= true then
|
|
|
|
|
node_names_table[properties["node.name"]] = true
|
2021-02-15 18:49:57 +02:00
|
|
|
break
|
|
|
|
|
end
|
2022-05-18 10:51:41 -04:00
|
|
|
properties["node.name"] = name .. "." .. counter
|
2021-02-15 18:49:57 +02:00
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- and a nick
|
2022-03-01 12:11:25 +02:00
|
|
|
local nick = nonempty(properties["node.nick"])
|
|
|
|
|
or nonempty(properties["api.alsa.pcm.name"])
|
|
|
|
|
or nonempty(properties["alsa.name"])
|
|
|
|
|
or nonempty(profile_desc)
|
2021-01-20 09:53:57 +02:00
|
|
|
or dev_props["device.nick"]
|
2022-03-08 11:56:34 +01:00
|
|
|
if nick == "USB Audio" then
|
|
|
|
|
nick = dev_props["device.nick"]
|
|
|
|
|
end
|
2021-02-18 09:02:41 +02:00
|
|
|
-- also sanitize nick, replace ':' with ' '
|
|
|
|
|
properties["node.nick"] = nick:gsub("(:)", " ")
|
2021-01-20 09:53:57 +02:00
|
|
|
|
|
|
|
|
-- ensure the node has a description
|
|
|
|
|
if not properties["node.description"] then
|
2021-02-18 10:23:07 +02:00
|
|
|
local desc = nonempty(dev_props["device.description"]) or "unknown"
|
|
|
|
|
local name = nonempty(properties["api.alsa.pcm.name"]) or
|
|
|
|
|
nonempty(properties["api.alsa.pcm.id"]) or dev
|
2021-01-20 09:53:57 +02:00
|
|
|
|
|
|
|
|
if profile_desc then
|
2021-02-18 09:02:41 +02:00
|
|
|
desc = desc .. " " .. profile_desc
|
2021-02-18 10:23:07 +02:00
|
|
|
elseif subdev ~= "0" then
|
2021-02-18 09:02:41 +02:00
|
|
|
desc = desc .. " (" .. name .. " " .. subdev .. ")"
|
2021-02-18 10:23:07 +02:00
|
|
|
elseif dev ~= "0" then
|
2021-02-18 09:02:41 +02:00
|
|
|
desc = desc .. " (" .. name .. ")"
|
2021-01-20 09:53:57 +02:00
|
|
|
end
|
2021-02-18 09:02:41 +02:00
|
|
|
|
|
|
|
|
-- also sanitize description, replace ':' with ' '
|
|
|
|
|
properties["node.description"] = desc:gsub("(:)", " ")
|
2021-01-20 09:53:57 +02:00
|
|
|
end
|
|
|
|
|
|
2021-03-26 17:29:25 +02:00
|
|
|
-- add api.alsa.card.* properties for rule matching purposes
|
|
|
|
|
for k, v in pairs(dev_props) do
|
|
|
|
|
if k:find("^api%.alsa%.card%..*") then
|
|
|
|
|
properties[k] = v
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2021-02-15 18:49:57 +02:00
|
|
|
-- apply properties from config.rules
|
|
|
|
|
rulesApplyProperties(properties)
|
2022-01-12 12:13:08 +01:00
|
|
|
if properties["node.disabled"] then
|
|
|
|
|
return
|
|
|
|
|
end
|
2021-01-20 09:53:57 +02:00
|
|
|
|
|
|
|
|
-- create the node
|
|
|
|
|
local node = Node("adapter", properties)
|
|
|
|
|
node:activate(Feature.Proxy.BOUND)
|
|
|
|
|
parent:store_managed_object(id, node)
|
|
|
|
|
end
|
|
|
|
|
|
2021-02-15 18:49:57 +02:00
|
|
|
function createDevice(parent, id, factory, properties)
|
|
|
|
|
local device = SpaDevice(factory, properties)
|
2022-01-11 11:27:33 -05:00
|
|
|
if device then
|
|
|
|
|
device:connect("create-object", createNode)
|
2022-05-18 10:51:41 -04:00
|
|
|
device:connect("object-removed", function (parent, id)
|
|
|
|
|
local node = parent:get_managed_object(id)
|
|
|
|
|
node_names_table[node.properties["node.name"]] = nil
|
|
|
|
|
end)
|
2022-01-11 11:27:33 -05:00
|
|
|
device:activate(Feature.SpaDevice.ENABLED | Feature.Proxy.BOUND)
|
|
|
|
|
parent:store_managed_object(id, device)
|
|
|
|
|
else
|
|
|
|
|
Log.warning ("Failed to create '" .. factory .. "' device")
|
|
|
|
|
end
|
2021-02-15 18:49:57 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
function prepareDevice(parent, id, type, factory, properties)
|
|
|
|
|
-- ensure the device has an appropriate name
|
|
|
|
|
local name = "alsa_card." ..
|
|
|
|
|
(properties["device.name"] or
|
|
|
|
|
properties["device.bus-id"] or
|
|
|
|
|
properties["device.bus-path"] or
|
2021-06-03 18:58:07 +03:00
|
|
|
tostring(id)):gsub("([^%w_%-%.])", "_")
|
2021-02-15 18:49:57 +02:00
|
|
|
|
|
|
|
|
properties["device.name"] = name
|
|
|
|
|
|
|
|
|
|
-- deduplicate devices with the same name
|
|
|
|
|
for counter = 2, 99, 1 do
|
2022-05-18 10:51:41 -04:00
|
|
|
if device_names_table[properties["device.name"]] ~= true then
|
|
|
|
|
device_names_table[properties["device.name"]] = true
|
2021-02-15 18:49:57 +02:00
|
|
|
break
|
|
|
|
|
end
|
2022-05-18 10:51:41 -04:00
|
|
|
properties["device.name"] = name .. "." .. counter
|
2021-01-20 09:53:57 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- ensure the device has a description
|
|
|
|
|
if not properties["device.description"] then
|
|
|
|
|
local d = nil
|
|
|
|
|
local f = properties["device.form-factor"]
|
|
|
|
|
local c = properties["device.class"]
|
|
|
|
|
|
|
|
|
|
if f == "internal" then
|
2022-04-09 14:27:42 +03:00
|
|
|
d = I18n.gettext("Built-in Audio")
|
2021-01-20 09:53:57 +02:00
|
|
|
elseif c == "modem" then
|
2022-04-09 14:27:42 +03:00
|
|
|
d = I18n.gettext("Modem")
|
2021-01-20 09:53:57 +02:00
|
|
|
end
|
|
|
|
|
|
2021-02-15 18:49:57 +02:00
|
|
|
d = d or properties["device.product.name"]
|
|
|
|
|
or properties["api.alsa.card.name"]
|
|
|
|
|
or properties["alsa.card_name"]
|
|
|
|
|
or "Unknown device"
|
2021-01-20 09:53:57 +02:00
|
|
|
properties["device.description"] = d
|
|
|
|
|
end
|
|
|
|
|
|
2021-02-15 18:49:57 +02:00
|
|
|
-- ensure the device has a nick
|
|
|
|
|
properties["device.nick"] =
|
|
|
|
|
properties["device.nick"] or
|
2022-03-01 11:35:55 +02:00
|
|
|
properties["api.alsa.card.name"] or
|
|
|
|
|
properties["alsa.card_name"]
|
2021-02-15 18:49:57 +02:00
|
|
|
|
2021-01-20 09:53:57 +02:00
|
|
|
-- set the icon name
|
|
|
|
|
if not properties["device.icon-name"] then
|
|
|
|
|
local icon = nil
|
2021-02-15 18:49:57 +02:00
|
|
|
local icon_map = {
|
|
|
|
|
-- form factor -> icon
|
|
|
|
|
["microphone"] = "audio-input-microphone",
|
|
|
|
|
["webcam"] = "camera-web",
|
|
|
|
|
["handset"] = "phone",
|
|
|
|
|
["portable"] = "multimedia-player",
|
|
|
|
|
["tv"] = "video-display",
|
|
|
|
|
["headset"] = "audio-headset",
|
|
|
|
|
["headphone"] = "audio-headphones",
|
|
|
|
|
["speaker"] = "audio-speakers",
|
|
|
|
|
["hands-free"] = "audio-handsfree",
|
|
|
|
|
}
|
2021-01-20 09:53:57 +02:00
|
|
|
local f = properties["device.form-factor"]
|
|
|
|
|
local c = properties["device.class"]
|
|
|
|
|
local b = properties["device.bus"]
|
|
|
|
|
|
2021-02-15 18:49:57 +02:00
|
|
|
icon = icon_map[f] or ((c == "modem") and "modem") or "audio-card"
|
|
|
|
|
properties["device.icon-name"] = icon .. "-analog" .. (b and ("-" .. b) or "")
|
2021-01-20 09:53:57 +02:00
|
|
|
end
|
|
|
|
|
|
2021-02-15 18:49:57 +02:00
|
|
|
-- apply properties from config.rules
|
|
|
|
|
rulesApplyProperties(properties)
|
2022-01-12 12:13:08 +01:00
|
|
|
if properties["device.disabled"] then
|
|
|
|
|
return
|
|
|
|
|
end
|
2021-02-15 18:49:57 +02:00
|
|
|
|
2021-01-20 09:53:57 +02:00
|
|
|
-- override the device factory to use ACP
|
2021-02-15 18:49:57 +02:00
|
|
|
if properties["api.alsa.use-acp"] then
|
|
|
|
|
Log.info("Enabling the use of ACP on " .. properties["device.name"])
|
2021-01-20 09:53:57 +02:00
|
|
|
factory = "api.alsa.acp.device"
|
|
|
|
|
end
|
|
|
|
|
|
2021-01-26 17:24:52 +02:00
|
|
|
-- use device reservation, if available
|
2021-02-15 18:49:57 +02:00
|
|
|
if rd_plugin and properties["api.alsa.card"] then
|
2021-01-26 17:24:52 +02:00
|
|
|
local rd_name = "Audio" .. properties["api.alsa.card"]
|
|
|
|
|
local rd = rd_plugin:call("create-reservation",
|
2021-02-15 18:49:57 +02:00
|
|
|
rd_name,
|
|
|
|
|
config.properties["alsa.reserve.application-name"] or "WirePlumber",
|
|
|
|
|
properties["device.name"],
|
|
|
|
|
config.properties["alsa.reserve.priority"] or -20);
|
2021-01-26 17:24:52 +02:00
|
|
|
|
|
|
|
|
properties["api.dbus.ReserveDevice1"] = rd_name
|
|
|
|
|
|
|
|
|
|
-- unlike pipewire-media-session, this logic here keeps the device
|
|
|
|
|
-- acquired at all times and destroys it if someone else acquires
|
|
|
|
|
rd:connect("notify::state", function (rd, pspec)
|
|
|
|
|
local state = rd["state"]
|
|
|
|
|
|
|
|
|
|
if state == "acquired" then
|
|
|
|
|
-- create the device
|
2021-02-15 18:49:57 +02:00
|
|
|
createDevice(parent, id, factory, properties)
|
2021-01-26 17:24:52 +02:00
|
|
|
|
|
|
|
|
elseif state == "available" then
|
|
|
|
|
-- attempt to acquire again
|
|
|
|
|
rd:call("acquire")
|
|
|
|
|
|
|
|
|
|
elseif state == "busy" then
|
|
|
|
|
-- destroy the device
|
|
|
|
|
parent:store_managed_object(id, nil)
|
|
|
|
|
end
|
|
|
|
|
end)
|
|
|
|
|
|
2021-11-23 13:17:29 +01:00
|
|
|
rd:connect("release-requested", function (rd)
|
|
|
|
|
Log.info("release requested")
|
|
|
|
|
parent:store_managed_object(id, nil)
|
|
|
|
|
rd:call("release")
|
|
|
|
|
end)
|
|
|
|
|
|
2021-01-26 17:24:52 +02:00
|
|
|
if jack_device then
|
|
|
|
|
rd:connect("notify::owner-name-changed", function (rd, pspec)
|
|
|
|
|
if rd["state"] == "busy" and
|
|
|
|
|
rd["owner-application-name"] == "Jack audio server" then
|
|
|
|
|
-- TODO enable the jack device
|
|
|
|
|
else
|
|
|
|
|
-- TODO disable the jack device
|
|
|
|
|
end
|
|
|
|
|
end)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
rd:call("acquire")
|
|
|
|
|
else
|
|
|
|
|
-- create the device
|
2021-02-15 18:49:57 +02:00
|
|
|
createDevice(parent, id, factory, properties)
|
2021-01-26 17:24:52 +02:00
|
|
|
end
|
2021-01-20 09:53:57 +02:00
|
|
|
end
|
|
|
|
|
|
2021-09-23 14:58:46 -04:00
|
|
|
function createMonitor ()
|
|
|
|
|
local m = SpaDevice("api.alsa.enum.udev", config.properties)
|
|
|
|
|
if m == nil then
|
|
|
|
|
Log.message("PipeWire's SPA ALSA udev plugin(\"api.alsa.enum.udev\")"
|
2021-11-11 13:53:57 +10:00
|
|
|
.. "missing or broken. Sound Cards cannot be enumerated")
|
2021-09-23 14:58:46 -04:00
|
|
|
return nil
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- handle create-object to prepare device
|
|
|
|
|
m:connect("create-object", prepareDevice)
|
|
|
|
|
|
2022-05-18 10:51:41 -04:00
|
|
|
-- handle object-removed to destroy device reservations and recycle device name
|
|
|
|
|
m:connect("object-removed", function (parent, id)
|
|
|
|
|
local device = parent:get_managed_object(id)
|
|
|
|
|
if rd_plugin then
|
2021-09-23 14:58:46 -04:00
|
|
|
local rd_name = device.properties["api.dbus.ReserveDevice1"]
|
|
|
|
|
if rd_name then
|
|
|
|
|
rd_plugin:call("destroy-reservation", rd_name)
|
|
|
|
|
end
|
2022-05-18 10:51:41 -04:00
|
|
|
end
|
|
|
|
|
device_names_table[device.properties["device.name"]] = nil
|
|
|
|
|
end)
|
|
|
|
|
|
|
|
|
|
-- reset the name tables to make sure names are recycled
|
|
|
|
|
device_names_table = {}
|
|
|
|
|
node_names_table = {}
|
2021-09-16 16:31:59 +05:30
|
|
|
|
2021-09-23 14:58:46 -04:00
|
|
|
-- activate monitor
|
|
|
|
|
Log.info("Activating ALSA monitor")
|
|
|
|
|
m:activate(Feature.SpaDevice.ENABLED)
|
|
|
|
|
return m
|
|
|
|
|
end
|
2021-01-26 17:24:52 +02:00
|
|
|
|
2021-02-15 18:49:57 +02:00
|
|
|
-- create the JACK device (for PipeWire to act as client to a JACK server)
|
|
|
|
|
if config.properties["alsa.jack-device"] then
|
|
|
|
|
jack_device = Device("spa-device-factory", {
|
|
|
|
|
["factory.name"] = "api.jack.device",
|
|
|
|
|
["node.name"] = "JACK-Device",
|
|
|
|
|
})
|
2021-05-14 14:03:36 -04:00
|
|
|
jack_device:activate(Feature.Proxy.BOUND)
|
2021-02-15 18:49:57 +02:00
|
|
|
end
|
2021-01-26 17:24:52 +02:00
|
|
|
|
2021-02-15 19:43:07 +02:00
|
|
|
-- enable device reservation if requested
|
|
|
|
|
if config.properties["alsa.reserve"] then
|
2021-05-07 11:53:47 +03:00
|
|
|
rd_plugin = Plugin.find("reserve-device")
|
2021-01-26 17:24:52 +02:00
|
|
|
end
|
|
|
|
|
|
2021-02-03 13:00:40 +02:00
|
|
|
-- if the reserve-device plugin is enabled, at the point of script execution
|
|
|
|
|
-- it is expected to be connected. if it is not, assume the d-bus connection
|
|
|
|
|
-- has failed and continue without it
|
|
|
|
|
if rd_plugin and rd_plugin["state"] ~= "connected" then
|
2021-02-15 18:49:57 +02:00
|
|
|
Log.message("reserve-device plugin is not connected to D-Bus, "
|
|
|
|
|
.. "disabling device reservation")
|
2021-02-03 13:00:40 +02:00
|
|
|
rd_plugin = nil
|
|
|
|
|
end
|
2021-01-26 17:24:52 +02:00
|
|
|
|
2021-09-23 14:58:46 -04:00
|
|
|
-- handle rd_plugin state changes to destroy and re-create the ALSA monitor in
|
|
|
|
|
-- case D-Bus service is restarted
|
2021-02-03 13:00:40 +02:00
|
|
|
if rd_plugin then
|
2022-05-06 09:57:37 -04:00
|
|
|
local dbus = rd_plugin:call("get-dbus")
|
|
|
|
|
dbus:connect("notify::state", function (b, pspec)
|
|
|
|
|
local state = b["state"]
|
2021-09-23 14:58:46 -04:00
|
|
|
Log.info ("rd-plugin state changed to " .. state)
|
|
|
|
|
if state == "connected" then
|
|
|
|
|
Log.info ("Creating ALSA monitor")
|
|
|
|
|
monitor = createMonitor()
|
|
|
|
|
elseif state == "closed" then
|
|
|
|
|
Log.info ("Destroying ALSA monitor")
|
|
|
|
|
monitor = nil
|
2021-02-03 13:00:40 +02:00
|
|
|
end
|
|
|
|
|
end)
|
2021-01-26 17:24:52 +02:00
|
|
|
end
|
2021-02-03 13:00:40 +02:00
|
|
|
|
2021-09-23 14:58:46 -04:00
|
|
|
-- create the monitor
|
|
|
|
|
monitor = createMonitor()
|