mirror of
https://github.com/marcoallegretti/WEFT_OS.git
synced 2026-03-27 01:13:09 +00:00
feat(compositor): implement weft-shell-protocol server side
Add the WEFT compositor-shell Wayland protocol and wire it into the compositor state. Protocol definition: - protocol/weft-shell-unstable-v1.xml: defines zweft_shell_manager_v1 (global, bound once by servo-shell) and zweft_shell_window_v1 (per-window slot). Requests: destroy, create_window, update_metadata, set_geometry. Events: configure, focus_changed, window_closed, presentation_feedback. Generated code + bindings: - crates/weft-compositor/src/protocols/mod.rs: uses wayland-scanner generate_interfaces! inside a __interfaces sub-module and generate_server_code! at the server module level, following the wayland-protocols-wlr crate structure. Exports WeftShellState (holds the GlobalId) and WeftShellWindowData (per-window user data). Server-side dispatch (state.rs): - GlobalDispatch<ZweftShellManagerV1, ()>: binds the global, inits each bound resource with unit user data. - Dispatch<ZweftShellManagerV1, ()>: handles create_window by initialising a ZweftShellWindowV1 and sending an initial configure. - Dispatch<ZweftShellWindowV1, WeftShellWindowData>: handles update_metadata (stores advisory data) and set_geometry (echoes compositor-adjusted configure back to client). WeftCompositorState.weft_shell_state initialised in new() alongside all other protocol globals. New direct deps in weft-compositor: wayland-scanner, wayland-server, wayland-backend, bitflags (all version-matched to Smithay 0.7).
This commit is contained in:
parent
c7ad2116a0
commit
18f92cc341
6 changed files with 335 additions and 1 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
|
@ -2258,11 +2258,15 @@ name = "weft-compositor"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"bitflags 2.11.0",
|
||||||
"sd-notify",
|
"sd-notify",
|
||||||
"smithay",
|
"smithay",
|
||||||
"smithay-drm-extras",
|
"smithay-drm-extras",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
"wayland-backend",
|
||||||
|
"wayland-scanner",
|
||||||
|
"wayland-server",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,10 @@ smithay = { version = "0.7", default-features = false, features = [
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
|
wayland-scanner = "0.31"
|
||||||
|
wayland-server = "0.31"
|
||||||
|
wayland-backend = "0.3"
|
||||||
|
bitflags = "2"
|
||||||
|
|
||||||
# DRM/KMS and hardware input depend on Linux kernel interfaces; compile only on Linux.
|
# DRM/KMS and hardware input depend on Linux kernel interfaces; compile only on Linux.
|
||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
mod backend;
|
mod backend;
|
||||||
mod input;
|
mod input;
|
||||||
|
mod protocols;
|
||||||
mod state;
|
mod state;
|
||||||
|
|
||||||
fn main() -> anyhow::Result<()> {
|
fn main() -> anyhow::Result<()> {
|
||||||
|
|
|
||||||
42
crates/weft-compositor/src/protocols/mod.rs
Normal file
42
crates/weft-compositor/src/protocols/mod.rs
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
#[allow(dead_code, non_camel_case_types, unused_unsafe, unused_variables)]
|
||||||
|
#[allow(non_upper_case_globals, non_snake_case, unused_imports)]
|
||||||
|
#[allow(missing_docs, clippy::all)]
|
||||||
|
pub mod server {
|
||||||
|
use wayland_server;
|
||||||
|
use wayland_server::protocol::*;
|
||||||
|
|
||||||
|
pub mod __interfaces {
|
||||||
|
use wayland_server::protocol::__interfaces::*;
|
||||||
|
wayland_scanner::generate_interfaces!("../../protocol/weft-shell-unstable-v1.xml");
|
||||||
|
}
|
||||||
|
use self::__interfaces::*;
|
||||||
|
|
||||||
|
wayland_scanner::generate_server_code!("../../protocol/weft-shell-unstable-v1.xml");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub use server::zweft_shell_manager_v1::ZweftShellManagerV1;
|
||||||
|
pub use server::zweft_shell_window_v1::ZweftShellWindowV1;
|
||||||
|
|
||||||
|
use wayland_server::{DisplayHandle, GlobalDispatch, backend::GlobalId};
|
||||||
|
|
||||||
|
pub struct WeftShellState {
|
||||||
|
_global: GlobalId,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub struct WeftShellWindowData {
|
||||||
|
pub app_id: String,
|
||||||
|
pub title: String,
|
||||||
|
pub role: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WeftShellState {
|
||||||
|
pub fn new<D>(display: &DisplayHandle) -> Self
|
||||||
|
where
|
||||||
|
D: GlobalDispatch<ZweftShellManagerV1, ()>,
|
||||||
|
D: 'static,
|
||||||
|
{
|
||||||
|
let global = display.create_global::<D, ZweftShellManagerV1, ()>(1, ());
|
||||||
|
Self { _global: global }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
use crate::backend::drm_device::WeftDrmData;
|
use crate::backend::drm_device::WeftDrmData;
|
||||||
|
use crate::protocols::{
|
||||||
|
WeftShellState, WeftShellWindowData, ZweftShellManagerV1, ZweftShellWindowV1,
|
||||||
|
server::{zweft_shell_manager_v1, zweft_shell_window_v1},
|
||||||
|
};
|
||||||
|
|
||||||
use smithay::{
|
use smithay::{
|
||||||
backend::{input::TabletToolDescriptor, renderer::utils::on_commit_buffer_handler},
|
backend::{input::TabletToolDescriptor, renderer::utils::on_commit_buffer_handler},
|
||||||
|
|
@ -15,7 +19,7 @@ use smithay::{
|
||||||
reexports::{
|
reexports::{
|
||||||
calloop::{LoopHandle, LoopSignal},
|
calloop::{LoopHandle, LoopSignal},
|
||||||
wayland_server::{
|
wayland_server::{
|
||||||
Client, DisplayHandle,
|
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New,
|
||||||
backend::{ClientData, ClientId, DisconnectReason},
|
backend::{ClientData, ClientId, DisconnectReason},
|
||||||
protocol::{wl_buffer::WlBuffer, wl_output::WlOutput, wl_surface::WlSurface},
|
protocol::{wl_buffer::WlBuffer, wl_output::WlOutput, wl_surface::WlSurface},
|
||||||
},
|
},
|
||||||
|
|
@ -89,6 +93,9 @@ pub struct WeftCompositorState {
|
||||||
// Set to false when the compositor should exit the event loop.
|
// Set to false when the compositor should exit the event loop.
|
||||||
pub running: bool,
|
pub running: bool,
|
||||||
|
|
||||||
|
// WEFT compositor–shell protocol global.
|
||||||
|
pub weft_shell_state: WeftShellState,
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
pub drm: Option<WeftDrmData>,
|
pub drm: Option<WeftDrmData>,
|
||||||
}
|
}
|
||||||
|
|
@ -113,6 +120,7 @@ impl WeftCompositorState {
|
||||||
InputMethodManagerState::new::<Self, _>(&display_handle, |_client| true);
|
InputMethodManagerState::new::<Self, _>(&display_handle, |_client| true);
|
||||||
let pointer_constraints_state = PointerConstraintsState::new::<Self>(&display_handle);
|
let pointer_constraints_state = PointerConstraintsState::new::<Self>(&display_handle);
|
||||||
let cursor_shape_state = CursorShapeManagerState::new::<Self>(&display_handle);
|
let cursor_shape_state = CursorShapeManagerState::new::<Self>(&display_handle);
|
||||||
|
let weft_shell_state = WeftShellState::new::<Self>(&display_handle);
|
||||||
|
|
||||||
let mut seat_state = SeatState::new();
|
let mut seat_state = SeatState::new();
|
||||||
let mut seat = seat_state.new_wl_seat(&display_handle, seat_name);
|
let mut seat = seat_state.new_wl_seat(&display_handle, seat_name);
|
||||||
|
|
@ -136,6 +144,7 @@ impl WeftCompositorState {
|
||||||
input_method_state,
|
input_method_state,
|
||||||
pointer_constraints_state,
|
pointer_constraints_state,
|
||||||
cursor_shape_state,
|
cursor_shape_state,
|
||||||
|
weft_shell_state,
|
||||||
space: Space::default(),
|
space: Space::default(),
|
||||||
popups: PopupManager::default(),
|
popups: PopupManager::default(),
|
||||||
seat_state,
|
seat_state,
|
||||||
|
|
@ -404,3 +413,81 @@ impl TabletSeatHandler for WeftCompositorState {
|
||||||
|
|
||||||
// CursorShapeManagerState has no handler trait; it calls SeatHandler::cursor_image directly.
|
// CursorShapeManagerState has no handler trait; it calls SeatHandler::cursor_image directly.
|
||||||
delegate_cursor_shape!(WeftCompositorState);
|
delegate_cursor_shape!(WeftCompositorState);
|
||||||
|
|
||||||
|
// --- weft-shell-protocol ---
|
||||||
|
|
||||||
|
impl GlobalDispatch<ZweftShellManagerV1, ()> for WeftCompositorState {
|
||||||
|
fn bind(
|
||||||
|
_state: &mut Self,
|
||||||
|
_handle: &DisplayHandle,
|
||||||
|
_client: &Client,
|
||||||
|
resource: New<ZweftShellManagerV1>,
|
||||||
|
_global_data: &(),
|
||||||
|
data_init: &mut DataInit<'_, Self>,
|
||||||
|
) {
|
||||||
|
data_init.init(resource, ());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Dispatch<ZweftShellManagerV1, ()> for WeftCompositorState {
|
||||||
|
fn request(
|
||||||
|
_state: &mut Self,
|
||||||
|
_client: &Client,
|
||||||
|
_resource: &ZweftShellManagerV1,
|
||||||
|
request: zweft_shell_manager_v1::Request,
|
||||||
|
_data: &(),
|
||||||
|
_dh: &DisplayHandle,
|
||||||
|
data_init: &mut DataInit<'_, Self>,
|
||||||
|
) {
|
||||||
|
match request {
|
||||||
|
zweft_shell_manager_v1::Request::Destroy => {}
|
||||||
|
zweft_shell_manager_v1::Request::CreateWindow {
|
||||||
|
id,
|
||||||
|
app_id,
|
||||||
|
title,
|
||||||
|
role,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
} => {
|
||||||
|
let window = data_init.init(
|
||||||
|
id,
|
||||||
|
WeftShellWindowData {
|
||||||
|
app_id,
|
||||||
|
title,
|
||||||
|
role,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
window.configure(x, y, width, height, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Dispatch<ZweftShellWindowV1, WeftShellWindowData> for WeftCompositorState {
|
||||||
|
fn request(
|
||||||
|
_state: &mut Self,
|
||||||
|
_client: &Client,
|
||||||
|
resource: &ZweftShellWindowV1,
|
||||||
|
request: zweft_shell_window_v1::Request,
|
||||||
|
_data: &WeftShellWindowData,
|
||||||
|
_dh: &DisplayHandle,
|
||||||
|
_data_init: &mut DataInit<'_, Self>,
|
||||||
|
) {
|
||||||
|
match request {
|
||||||
|
zweft_shell_window_v1::Request::Destroy => {}
|
||||||
|
zweft_shell_window_v1::Request::UpdateMetadata { title, role } => {
|
||||||
|
let _ = (title, role);
|
||||||
|
}
|
||||||
|
zweft_shell_window_v1::Request::SetGeometry {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
} => {
|
||||||
|
resource.configure(x, y, width, height, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
196
protocol/weft-shell-unstable-v1.xml
Normal file
196
protocol/weft-shell-unstable-v1.xml
Normal file
|
|
@ -0,0 +1,196 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<protocol name="weft_shell_unstable_v1">
|
||||||
|
|
||||||
|
<copyright>
|
||||||
|
Copyright (C) 2026 WEFT OS contributors
|
||||||
|
|
||||||
|
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 (including the
|
||||||
|
next paragraph) 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.
|
||||||
|
</copyright>
|
||||||
|
|
||||||
|
<description summary="WEFT compositor–shell protocol">
|
||||||
|
This protocol defines the boundary between weft-compositor and
|
||||||
|
weft-servo-shell. It allows the shell to manage window slots on behalf
|
||||||
|
of application surfaces that the compositor owns.
|
||||||
|
|
||||||
|
The shell binds the weft_shell_manager_v1 global once per connection.
|
||||||
|
The manager creates weft_shell_window_v1 objects, each representing one
|
||||||
|
visual window slot. The compositor is authoritative for geometry,
|
||||||
|
focus, and stacking. The shell is authoritative for window metadata,
|
||||||
|
chrome layout, and the decision to create or destroy a window slot.
|
||||||
|
|
||||||
|
Warning: This protocol is unstable and subject to change before version
|
||||||
|
1 is finalized. Clients must check the version advertised by the
|
||||||
|
compositor and handle unknown events gracefully.
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<!-- ───────────────────────────── Manager ───────────────────────────── -->
|
||||||
|
|
||||||
|
<interface name="zweft_shell_manager_v1" version="1">
|
||||||
|
|
||||||
|
<description summary="shell session manager">
|
||||||
|
Bound once by weft-servo-shell. The manager owns the shell session.
|
||||||
|
If the binding is destroyed the compositor invalidates all window
|
||||||
|
objects that were created through it.
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<request name="destroy" type="destructor">
|
||||||
|
<description summary="destroy the manager and end the shell session">
|
||||||
|
Destroys the manager. All weft_shell_window_v1 objects created
|
||||||
|
through this manager become inert. The compositor will emit
|
||||||
|
window_closed on each before processing the destructor.
|
||||||
|
</description>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<request name="create_window">
|
||||||
|
<description summary="create a window slot">
|
||||||
|
Creates a new window slot. The compositor assigns the protocol
|
||||||
|
object identity; no separate window_id is exchanged. The
|
||||||
|
compositor will emit configure on the new object to communicate the
|
||||||
|
effective initial geometry and state.
|
||||||
|
</description>
|
||||||
|
<arg name="id" type="new_id" interface="zweft_shell_window_v1"
|
||||||
|
summary="new window object"/>
|
||||||
|
<arg name="app_id" type="string"
|
||||||
|
summary="application identifier, reverse-DNS format"/>
|
||||||
|
<arg name="title" type="string"
|
||||||
|
summary="initial window title"/>
|
||||||
|
<arg name="role" type="string"
|
||||||
|
summary="window role hint (normal, dialog, panel, overlay)"/>
|
||||||
|
<arg name="x" type="int"
|
||||||
|
summary="requested x position in compositor logical coordinates"/>
|
||||||
|
<arg name="y" type="int"
|
||||||
|
summary="requested y position in compositor logical coordinates"/>
|
||||||
|
<arg name="width" type="int"
|
||||||
|
summary="requested width in compositor logical coordinates"/>
|
||||||
|
<arg name="height" type="int"
|
||||||
|
summary="requested height in compositor logical coordinates"/>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
</interface>
|
||||||
|
|
||||||
|
<!-- ───────────────────────────── Window ────────────────────────────── -->
|
||||||
|
|
||||||
|
<interface name="zweft_shell_window_v1" version="1">
|
||||||
|
|
||||||
|
<description summary="a compositor-managed window slot">
|
||||||
|
Represents one visual window. The compositor controls the effective
|
||||||
|
geometry, stacking, and focus state. The shell controls metadata and
|
||||||
|
may request geometry changes; the compositor may reject or adjust
|
||||||
|
such requests.
|
||||||
|
|
||||||
|
A window object becomes inert after window_closed is received. The
|
||||||
|
shell must call destroy on an inert object as soon as possible.
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<enum name="state" bitfield="true">
|
||||||
|
<description summary="window state bitmask"/>
|
||||||
|
<entry name="maximized" value="1" summary="window occupies full output area"/>
|
||||||
|
<entry name="fullscreen" value="2" summary="window covers the entire output including chrome"/>
|
||||||
|
<entry name="activated" value="4" summary="window has input focus"/>
|
||||||
|
<entry name="resizing" value="8" summary="an interactive resize is in progress"/>
|
||||||
|
</enum>
|
||||||
|
|
||||||
|
<enum name="error">
|
||||||
|
<description summary="protocol errors"/>
|
||||||
|
<entry name="defunct_window" value="0"
|
||||||
|
summary="request sent after window_closed was received"/>
|
||||||
|
</enum>
|
||||||
|
|
||||||
|
<request name="destroy" type="destructor">
|
||||||
|
<description summary="destroy the window object">
|
||||||
|
Destroys the window object. If the window is still alive the
|
||||||
|
compositor will remove the corresponding window slot from the
|
||||||
|
display. The shell must not send requests on this object after
|
||||||
|
calling destroy.
|
||||||
|
</description>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<request name="update_metadata">
|
||||||
|
<description summary="update window title and role">
|
||||||
|
Updates advisory metadata. The compositor may use these values in
|
||||||
|
window decorations or accessibility trees. Updates are not
|
||||||
|
authoritative for policy decisions.
|
||||||
|
</description>
|
||||||
|
<arg name="title" type="string" summary="new window title"/>
|
||||||
|
<arg name="role" type="string" summary="new window role hint"/>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<request name="set_geometry">
|
||||||
|
<description summary="request a geometry change">
|
||||||
|
Asks the compositor to change the window geometry. The compositor
|
||||||
|
validates the request against output bounds and current policy. The
|
||||||
|
compositor is not required to honor the exact values. A configure
|
||||||
|
event will be sent with the effective geometry that results.
|
||||||
|
</description>
|
||||||
|
<arg name="x" type="int" summary="requested x position"/>
|
||||||
|
<arg name="y" type="int" summary="requested y position"/>
|
||||||
|
<arg name="width" type="int" summary="requested width"/>
|
||||||
|
<arg name="height" type="int" summary="requested height"/>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<event name="configure">
|
||||||
|
<description summary="compositor sets effective geometry and state">
|
||||||
|
Sent by the compositor when the effective geometry or state
|
||||||
|
changes. The shell must treat this as authoritative and update its
|
||||||
|
DOM layout to match. The shell must ack each configure event by
|
||||||
|
sending an update_metadata request or by doing nothing if no
|
||||||
|
metadata changed; no explicit ack request is defined in version 1.
|
||||||
|
</description>
|
||||||
|
<arg name="x" type="int" summary="effective x position"/>
|
||||||
|
<arg name="y" type="int" summary="effective y position"/>
|
||||||
|
<arg name="width" type="int" summary="effective width"/>
|
||||||
|
<arg name="height" type="int" summary="effective height"/>
|
||||||
|
<arg name="state" type="uint" summary="bitmask of state enum values"/>
|
||||||
|
</event>
|
||||||
|
|
||||||
|
<event name="focus_changed">
|
||||||
|
<description summary="input focus state changed">
|
||||||
|
Sent when surface-level focus changes for this window slot. The
|
||||||
|
shell updates visual focus state in its DOM but does not override
|
||||||
|
compositor focus policy.
|
||||||
|
</description>
|
||||||
|
<arg name="focused" type="uint" summary="1 if focused, 0 if not"/>
|
||||||
|
</event>
|
||||||
|
|
||||||
|
<event name="window_closed">
|
||||||
|
<description summary="window slot is no longer valid">
|
||||||
|
Sent by the compositor when the window object is being invalidated.
|
||||||
|
After this event the shell must call destroy on the object. Any
|
||||||
|
request sent after window_closed is a protocol error.
|
||||||
|
</description>
|
||||||
|
</event>
|
||||||
|
|
||||||
|
<event name="presentation_feedback">
|
||||||
|
<description summary="frame presentation timing">
|
||||||
|
Sent after a frame containing this window's content is presented
|
||||||
|
to the output. Allows the shell to align animations and resize
|
||||||
|
flows with compositor timing. tv_sec and tv_nsec are wall-clock
|
||||||
|
values from the compositor's monotonic clock. refresh is the
|
||||||
|
output refresh interval in nanoseconds; 0 if unknown.
|
||||||
|
</description>
|
||||||
|
<arg name="tv_sec" type="uint" summary="seconds component of presentation time"/>
|
||||||
|
<arg name="tv_nsec" type="uint" summary="nanoseconds component of presentation time"/>
|
||||||
|
<arg name="refresh" type="uint" summary="output refresh interval in nanoseconds"/>
|
||||||
|
</event>
|
||||||
|
|
||||||
|
</interface>
|
||||||
|
|
||||||
|
</protocol>
|
||||||
Loading…
Reference in a new issue