2022-08-16 06:05:55 +05:30
|
|
|
-- WirePlumber
|
|
|
|
|
|
|
|
|
|
-- Copyright © 2022 Collabora Ltd.
|
|
|
|
|
-- @author Ashok Sidipotu <ashok.sidipotu@collabora.com>
|
|
|
|
|
|
|
|
|
|
-- SPDX-License-Identifier: MIT
|
|
|
|
|
|
2023-09-12 12:25:18 +05:30
|
|
|
-- Script is a Lua Module of linking Lua utility functions
|
2022-08-16 06:05:55 +05:30
|
|
|
|
|
|
|
|
local cutils = require ("common-utils")
|
|
|
|
|
|
2024-01-03 11:10:56 +02:00
|
|
|
local lutils = {
|
2022-11-08 12:40:21 +02:00
|
|
|
si_flags = {},
|
2024-05-31 15:01:51 +05:30
|
|
|
priority_media_role_link = {},
|
2022-11-08 12:40:21 +02:00
|
|
|
}
|
2022-08-16 06:05:55 +05:30
|
|
|
|
2024-01-03 11:10:56 +02:00
|
|
|
function lutils.get_flags (self, si_id)
|
2022-11-08 12:40:21 +02:00
|
|
|
if not self.si_flags [si_id] then
|
|
|
|
|
self.si_flags [si_id] = {}
|
2022-08-16 06:05:55 +05:30
|
|
|
end
|
|
|
|
|
|
2022-11-08 12:40:21 +02:00
|
|
|
return self.si_flags [si_id]
|
2022-08-16 06:05:55 +05:30
|
|
|
end
|
|
|
|
|
|
2024-01-03 11:10:56 +02:00
|
|
|
function lutils.clear_flags (self, si_id)
|
2022-11-10 20:27:28 +02:00
|
|
|
self.si_flags [si_id] = nil
|
|
|
|
|
end
|
|
|
|
|
|
2024-05-31 15:01:51 +05:30
|
|
|
function getprio (link)
|
|
|
|
|
return tonumber (link.properties ["policy.role-based.priority"])
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
function getplugged (link)
|
|
|
|
|
return tonumber (link.properties ["item.plugged.usec"]) or 0
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
function lutils.getAction (pmrl, link)
|
|
|
|
|
local props = pmrl.properties
|
|
|
|
|
|
|
|
|
|
if getprio (pmrl) == getprio (link) then
|
|
|
|
|
return props ["policy.role-based.action.same-priority"] or "mix"
|
|
|
|
|
else
|
|
|
|
|
return props ["policy.role-based.action.lower-priority"] or "mix"
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- if the link happens to be priority one, clear it and find the next
|
|
|
|
|
-- priority.
|
|
|
|
|
function lutils.clearPriorityMediaRoleLink (link)
|
|
|
|
|
local lprops = link.properties
|
|
|
|
|
local lmc = lprops ["target.media.class"]
|
|
|
|
|
|
|
|
|
|
pmrl = lutils.getPriorityMediaRoleLink (lmc)
|
|
|
|
|
|
|
|
|
|
-- only proceed if the link happens to be priority one.
|
|
|
|
|
if pmrl ~= link then
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local prio_link = nil
|
|
|
|
|
local prio = 0
|
|
|
|
|
local plugged = 0
|
|
|
|
|
for l in cutils.get_object_manager ("session-item"):iterate {
|
|
|
|
|
type = "SiLink",
|
|
|
|
|
Constraint { "item.factory.name", "=", "si-standard-link", type = "pw-global" },
|
|
|
|
|
Constraint { "is.media.role.link", "=", true },
|
|
|
|
|
Constraint { "target.media.class", "=", lmc },
|
|
|
|
|
} do
|
|
|
|
|
local props = l.properties
|
|
|
|
|
|
|
|
|
|
-- dont consider this link as it is about to be removed.
|
|
|
|
|
if pmrl == l then
|
|
|
|
|
goto continue
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if getprio (link) > prio or
|
|
|
|
|
(getprio (link) == prio and getplugged (link) > plugged) then
|
|
|
|
|
prio = getprio (l)
|
|
|
|
|
plugged = getplugged (l)
|
|
|
|
|
prio_link = l
|
|
|
|
|
end
|
|
|
|
|
::continue::
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if prio_link then
|
|
|
|
|
setPriorityMediaRoleLink (lmc, prio_link)
|
|
|
|
|
else
|
|
|
|
|
setPriorityMediaRoleLink (lmc, nil)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- record priority media role link
|
|
|
|
|
function lutils.updatePriorityMediaRoleLink (link)
|
|
|
|
|
local lprops = link.properties
|
|
|
|
|
local mc = lprops ["target.media.class"]
|
|
|
|
|
|
|
|
|
|
if not lutils.priority_media_role_link [mc] then
|
|
|
|
|
setPriorityMediaRoleLink (mc, link)
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
pmrl = lutils.getPriorityMediaRoleLink (mc)
|
|
|
|
|
|
|
|
|
|
if getprio (link) > getprio (pmrl) or
|
|
|
|
|
(getprio (link) == getprio (pmrl) and getplugged (link) >= getplugged (pmrl)) then
|
|
|
|
|
setPriorityMediaRoleLink (mc, link)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
function lutils.getPriorityMediaRoleLink (lmc)
|
|
|
|
|
return lutils.priority_media_role_link [lmc]
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
function setPriorityMediaRoleLink (lmc, link)
|
|
|
|
|
lutils.priority_media_role_link [lmc] = link
|
|
|
|
|
if link then
|
|
|
|
|
Log.debug (
|
|
|
|
|
string.format ("update priority link(%d) media role(\"%s\") priority(%d)",
|
|
|
|
|
link.id, link.properties ["media.role"], getprio (link)))
|
|
|
|
|
else
|
|
|
|
|
Log.debug ("clear priority media role")
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2024-01-03 11:10:56 +02:00
|
|
|
function lutils.unwrap_select_target_event (self, event)
|
2022-11-08 12:40:21 +02:00
|
|
|
local source = event:get_source ()
|
|
|
|
|
local si = event:get_subject ()
|
|
|
|
|
local target = event:get_data ("target")
|
2022-11-24 13:33:30 +02:00
|
|
|
local om = source:call ("get-object-manager", "session-item")
|
2022-11-08 12:40:21 +02:00
|
|
|
local si_id = si.id
|
|
|
|
|
|
2022-11-24 13:33:30 +02:00
|
|
|
return source, om, si, si.properties, self:get_flags (si_id), target
|
2022-08-16 06:05:55 +05:30
|
|
|
end
|
|
|
|
|
|
2024-01-03 11:10:56 +02:00
|
|
|
function lutils.canPassthrough (si, si_target)
|
2022-08-16 06:05:55 +05:30
|
|
|
local props = si.properties
|
|
|
|
|
local tprops = si_target.properties
|
|
|
|
|
-- both nodes must support encoded formats
|
2023-09-30 11:02:02 +03:00
|
|
|
if not cutils.parseBool (props ["item.node.supports-encoded-fmts"])
|
|
|
|
|
or not cutils.parseBool (tprops ["item.node.supports-encoded-fmts"]) then
|
2022-08-16 06:05:55 +05:30
|
|
|
return false
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- make sure that the nodes have at least one common non-raw format
|
|
|
|
|
local n1 = si:get_associated_proxy ("node")
|
|
|
|
|
local n2 = si_target:get_associated_proxy ("node")
|
|
|
|
|
for p1 in n1:iterate_params ("EnumFormat") do
|
|
|
|
|
local p1p = p1:parse ()
|
|
|
|
|
if p1p.properties.mediaSubtype ~= "raw" then
|
|
|
|
|
for p2 in n2:iterate_params ("EnumFormat") do
|
|
|
|
|
if p1:filter (p2) then
|
|
|
|
|
return true
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
return false
|
|
|
|
|
end
|
|
|
|
|
|
2024-01-03 11:10:56 +02:00
|
|
|
function lutils.checkFollowDefault (si, si_target)
|
2022-08-16 06:05:55 +05:30
|
|
|
-- If it got linked to the default target that is defined by node
|
|
|
|
|
-- props but not metadata, start ignoring the node prop from now on.
|
|
|
|
|
-- This is what Pulseaudio does.
|
|
|
|
|
--
|
|
|
|
|
-- Pulseaudio skips here filter streams (i->origin_sink and
|
|
|
|
|
-- o->destination_source set in PA). Pipewire does not have a flag
|
|
|
|
|
-- explicitly for this, but we can use presence of node.link-group.
|
|
|
|
|
local si_props = si.properties
|
|
|
|
|
local target_props = si_target.properties
|
2023-09-30 11:02:02 +03:00
|
|
|
local reconnect = not cutils.parseBool (si_props ["node.dont-reconnect"])
|
2022-08-16 06:05:55 +05:30
|
|
|
local is_filter = (si_props ["node.link-group"] ~= nil)
|
|
|
|
|
|
2023-11-19 18:34:33 +02:00
|
|
|
if reconnect and not is_filter then
|
2023-09-30 11:02:02 +03:00
|
|
|
local def_id = cutils.getDefaultNode (si_props,
|
2022-08-16 06:05:55 +05:30
|
|
|
cutils.getTargetDirection (si_props))
|
|
|
|
|
|
|
|
|
|
if target_props ["node.id"] == tostring (def_id) then
|
2023-09-30 11:02:02 +03:00
|
|
|
local metadata = cutils.get_default_metadata_object ()
|
2022-08-16 06:05:55 +05:30
|
|
|
-- Set target.node, for backward compatibility
|
|
|
|
|
metadata:set (tonumber
|
|
|
|
|
(si_props ["node.id"]), "target.node", "Spa:Id", "-1")
|
|
|
|
|
Log.info (si, "... set metadata to follow default")
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2024-01-03 11:10:56 +02:00
|
|
|
function lutils.lookupLink (si_id, si_target_id)
|
2023-03-06 18:03:53 +05:30
|
|
|
local link = cutils.get_object_manager ("session-item"):lookup {
|
|
|
|
|
type = "SiLink",
|
2022-08-16 06:05:55 +05:30
|
|
|
Constraint { "out.item.id", "=", si_id },
|
|
|
|
|
Constraint { "in.item.id", "=", si_target_id }
|
|
|
|
|
}
|
|
|
|
|
if not link then
|
2023-03-06 18:03:53 +05:30
|
|
|
link = cutils.get_object_manager ("session-item"):lookup {
|
|
|
|
|
type = "SiLink",
|
2022-08-16 06:05:55 +05:30
|
|
|
Constraint { "in.item.id", "=", si_id },
|
|
|
|
|
Constraint { "out.item.id", "=", si_target_id }
|
|
|
|
|
}
|
|
|
|
|
end
|
|
|
|
|
return link
|
|
|
|
|
end
|
|
|
|
|
|
2024-01-03 11:10:56 +02:00
|
|
|
function lutils.isLinked (si_target)
|
2022-08-16 06:05:55 +05:30
|
|
|
local target_id = si_target.id
|
|
|
|
|
local linked = false
|
|
|
|
|
local exclusive = false
|
|
|
|
|
|
2023-03-06 18:03:53 +05:30
|
|
|
for l in cutils.get_object_manager ("session-item"):iterate {
|
|
|
|
|
type = "SiLink",
|
|
|
|
|
} do
|
2022-08-16 06:05:55 +05:30
|
|
|
local p = l.properties
|
|
|
|
|
local out_id = tonumber (p ["out.item.id"])
|
|
|
|
|
local in_id = tonumber (p ["in.item.id"])
|
|
|
|
|
linked = (out_id == target_id) or (in_id == target_id)
|
|
|
|
|
if linked then
|
2023-09-30 11:02:02 +03:00
|
|
|
exclusive = cutils.parseBool (p ["exclusive"]) or cutils.parseBool (p ["passthrough"])
|
2022-08-16 06:05:55 +05:30
|
|
|
break
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
return linked, exclusive
|
|
|
|
|
end
|
|
|
|
|
|
2023-11-15 12:43:34 -05:00
|
|
|
function lutils.getNodePeerId (node_id)
|
|
|
|
|
for l in cutils.get_object_manager ("link"):iterate() do
|
|
|
|
|
local p = l.properties
|
|
|
|
|
local in_id = tonumber(p["link.input.node"])
|
|
|
|
|
local out_id = tonumber(p["link.output.node"])
|
|
|
|
|
if in_id == node_id then
|
|
|
|
|
return out_id
|
|
|
|
|
elseif out_id == node_id then
|
|
|
|
|
return in_id
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
return nil
|
|
|
|
|
end
|
|
|
|
|
|
2024-01-03 11:10:56 +02:00
|
|
|
function lutils.canLink (properties, si_target)
|
2022-08-16 06:05:55 +05:30
|
|
|
local target_props = si_target.properties
|
|
|
|
|
|
|
|
|
|
-- nodes must have the same media type
|
|
|
|
|
if properties ["media.type"] ~= target_props ["media.type"] then
|
|
|
|
|
return false
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local function isMonitor(properties)
|
|
|
|
|
return properties ["item.node.direction"] == "input" and
|
2023-09-30 11:02:02 +03:00
|
|
|
cutils.parseBool (properties ["item.features.monitor"]) and
|
|
|
|
|
not cutils.parseBool (properties ["item.features.no-dsp"]) and
|
2022-08-16 06:05:55 +05:30
|
|
|
properties ["item.factory.name"] == "si-audio-adapter"
|
|
|
|
|
end
|
|
|
|
|
|
2023-02-03 12:09:26 -05:00
|
|
|
if properties ["item.factory.name"] == "si-audio-virtual" then
|
|
|
|
|
-- virtual nodes must have the same direction, unless the target is monitor
|
|
|
|
|
if properties ["item.node.direction"] ~= target_props ["item.node.direction"]
|
|
|
|
|
and not isMonitor (target_props) then
|
|
|
|
|
return false
|
|
|
|
|
end
|
|
|
|
|
else
|
|
|
|
|
-- nodes must have opposite direction, or otherwise they must be both input
|
|
|
|
|
-- and the target must have a monitor (so the target will be used as a source)
|
|
|
|
|
if properties ["item.node.direction"] == target_props ["item.node.direction"]
|
|
|
|
|
and not isMonitor (target_props) then
|
|
|
|
|
return false
|
|
|
|
|
end
|
2022-08-16 06:05:55 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- check link group
|
|
|
|
|
local function canLinkGroupCheck(link_group, si_target, hops)
|
|
|
|
|
local target_props = si_target.properties
|
|
|
|
|
local target_link_group = target_props ["node.link-group"]
|
|
|
|
|
|
|
|
|
|
if hops == 8 then
|
|
|
|
|
return false
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- allow linking if target has no link-group property
|
|
|
|
|
if not target_link_group then
|
|
|
|
|
return true
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- do not allow linking if target has the same link-group
|
|
|
|
|
if link_group == target_link_group then
|
|
|
|
|
return false
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- make sure target is not linked with another node with same link group
|
|
|
|
|
-- start by locating other nodes in the target's link-group, in opposite direction
|
2023-03-06 18:03:53 +05:30
|
|
|
for n in cutils.get_object_manager ("session-item"):iterate {
|
|
|
|
|
type = "SiLinkable",
|
|
|
|
|
Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
|
2022-08-16 06:05:55 +05:30
|
|
|
Constraint { "id", "!", si_target.id, type = "gobject" },
|
|
|
|
|
Constraint { "item.node.direction", "!", target_props ["item.node.direction"] },
|
|
|
|
|
Constraint { "node.link-group", "=", target_link_group },
|
|
|
|
|
} do
|
|
|
|
|
-- iterate their peers and return false if one of them cannot link
|
2023-03-06 18:03:53 +05:30
|
|
|
for silink in cutils.get_object_manager ("session-item"):iterate {
|
|
|
|
|
type = "SiLink",
|
|
|
|
|
} do
|
2022-08-16 06:05:55 +05:30
|
|
|
local out_id = tonumber (silink.properties ["out.item.id"])
|
|
|
|
|
local in_id = tonumber (silink.properties ["in.item.id"])
|
|
|
|
|
if out_id == n.id or in_id == n.id then
|
|
|
|
|
local peer_id = (out_id == n.id) and in_id or out_id
|
2023-03-06 18:03:53 +05:30
|
|
|
local peer = cutils.get_object_manager ("session-item"):lookup {
|
|
|
|
|
type = "SiLinkable",
|
|
|
|
|
Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
|
2022-08-16 06:05:55 +05:30
|
|
|
Constraint { "id", "=", peer_id, type = "gobject" },
|
|
|
|
|
}
|
|
|
|
|
if peer and not canLinkGroupCheck (link_group, peer, hops + 1) then
|
|
|
|
|
return false
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
return true
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local link_group = properties ["node.link-group"]
|
|
|
|
|
if link_group then
|
|
|
|
|
return canLinkGroupCheck (link_group, si_target, 0)
|
|
|
|
|
end
|
|
|
|
|
return true
|
|
|
|
|
end
|
|
|
|
|
|
2024-01-03 11:10:56 +02:00
|
|
|
function lutils.findDefaultLinkable (si)
|
2022-08-16 06:05:55 +05:30
|
|
|
local si_props = si.properties
|
|
|
|
|
local target_direction = cutils.getTargetDirection (si_props)
|
|
|
|
|
local def_node_id = cutils.getDefaultNode (si_props, target_direction)
|
2023-03-06 18:03:53 +05:30
|
|
|
return cutils.get_object_manager ("session-item"):lookup {
|
|
|
|
|
type = "SiLinkable",
|
|
|
|
|
Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
|
2022-08-16 06:05:55 +05:30
|
|
|
Constraint { "node.id", "=", tostring (def_node_id) }
|
|
|
|
|
}
|
|
|
|
|
end
|
|
|
|
|
|
2024-01-03 11:10:56 +02:00
|
|
|
function lutils.checkPassthroughCompatibility (si, si_target)
|
2022-08-16 06:05:55 +05:30
|
|
|
local si_must_passthrough =
|
2023-09-30 11:02:02 +03:00
|
|
|
cutils.parseBool (si.properties ["item.node.encoded-only"])
|
2022-08-16 06:05:55 +05:30
|
|
|
local si_target_must_passthrough =
|
2023-09-30 11:02:02 +03:00
|
|
|
cutils.parseBool (si_target.properties ["item.node.encoded-only"])
|
2024-01-03 11:10:56 +02:00
|
|
|
local can_passthrough = lutils.canPassthrough (si, si_target)
|
2022-08-16 06:05:55 +05:30
|
|
|
if (si_must_passthrough or si_target_must_passthrough)
|
|
|
|
|
and not can_passthrough then
|
|
|
|
|
return false, can_passthrough
|
|
|
|
|
end
|
|
|
|
|
return true, can_passthrough
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- Does the target device have any active/available paths/routes to
|
|
|
|
|
-- the physical device(spkr/mic/cam)?
|
2024-01-03 11:10:56 +02:00
|
|
|
function lutils.haveAvailableRoutes (si_props)
|
2022-08-16 06:05:55 +05:30
|
|
|
local card_profile_device = si_props ["card.profile.device"]
|
|
|
|
|
local device_id = si_props ["device.id"]
|
2023-03-06 18:03:53 +05:30
|
|
|
local device = device_id and cutils.get_object_manager ("device"):lookup {
|
2022-08-16 06:05:55 +05:30
|
|
|
Constraint { "bound-id", "=", device_id, type = "gobject" },
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if not card_profile_device or not device then
|
|
|
|
|
return true
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local found = 0
|
|
|
|
|
local avail = 0
|
|
|
|
|
|
|
|
|
|
-- First check "SPA_PARAM_Route" if there are any active devices
|
|
|
|
|
-- in an active profile.
|
|
|
|
|
for p in device:iterate_params ("Route") do
|
2022-08-26 12:22:02 +05:30
|
|
|
local route = cutils.parseParam (p, "Route")
|
2022-08-16 06:05:55 +05:30
|
|
|
if not route then
|
|
|
|
|
goto skip_route
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if (route.device ~= tonumber (card_profile_device)) then
|
|
|
|
|
goto skip_route
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if (route.available == "no") then
|
|
|
|
|
return false
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
do return true end
|
|
|
|
|
|
|
|
|
|
::skip_route::
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- Second check "SPA_PARAM_EnumRoute" if there is any route that
|
|
|
|
|
-- is available if not active.
|
|
|
|
|
for p in device:iterate_params ("EnumRoute") do
|
2022-08-26 12:22:02 +05:30
|
|
|
local route = cutils.parseParam (p, "EnumRoute")
|
2022-08-16 06:05:55 +05:30
|
|
|
if not route then
|
|
|
|
|
goto skip_enum_route
|
|
|
|
|
end
|
|
|
|
|
|
2022-09-06 19:24:33 +05:30
|
|
|
if not cutils.arrayContains
|
|
|
|
|
(route.devices, tonumber (card_profile_device)) then
|
2022-08-16 06:05:55 +05:30
|
|
|
goto skip_enum_route
|
|
|
|
|
end
|
|
|
|
|
found = found + 1;
|
|
|
|
|
if (route.available ~= "no") then
|
|
|
|
|
avail = avail + 1
|
|
|
|
|
end
|
|
|
|
|
::skip_enum_route::
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if found == 0 then
|
|
|
|
|
return true
|
|
|
|
|
end
|
|
|
|
|
if avail > 0 then
|
|
|
|
|
return true
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
end
|
|
|
|
|
|
2024-03-25 14:54:53 +01:00
|
|
|
function lutils.sendClientError (event, node, code, message)
|
2023-10-31 12:28:18 -04:00
|
|
|
local source = event:get_source ()
|
|
|
|
|
local client_id = node.properties ["client.id"]
|
|
|
|
|
if client_id then
|
|
|
|
|
local clients_om = source:call ("get-object-manager", "client")
|
|
|
|
|
local client = clients_om:lookup {
|
|
|
|
|
Constraint { "bound-id", "=", client_id, type = "gobject" }
|
|
|
|
|
}
|
|
|
|
|
if client then
|
2024-03-25 14:54:53 +01:00
|
|
|
client:send_error (node ["bound-id"], code, message)
|
2023-10-31 12:28:18 -04:00
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2024-01-03 11:10:56 +02:00
|
|
|
return lutils
|