Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
R
RoboPLC
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
黄新宇
RoboPLC
Commits
b0078bf3
提交
b0078bf3
authored
2月 27, 2025
作者:
Serhij S
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
hmi
上级
7f8ec5d2
隐藏空白字符变更
内嵌
并排
正在显示
4 个修改的文件
包含
442 行增加
和
1 行删除
+442
-1
Cargo.toml
Cargo.toml
+10
-1
hmi.rs
examples/hmi.rs
+146
-0
hmi.rs
src/hmi.rs
+283
-0
lib.rs
src/lib.rs
+3
-0
没有找到文件。
Cargo.toml
浏览文件 @
b0078bf3
...
...
@@ -55,6 +55,9 @@ parking_lot = { version = "0.12.3", optional = true }
parking_lot_rt
=
{
version
=
"0.12.1"
,
optional
=
true
}
serde_json
=
{
version
=
"1.0.134"
,
optional
=
true
}
rmp-serde
=
{
version
=
"1.3.0"
,
optional
=
true
}
egui
=
{
version
=
"0.31.0"
,
optional
=
true
}
eframe
=
{
version
=
"0.31.0"
,
optional
=
true
}
winit
=
{
version
=
"0.30.9"
,
optional
=
true
}
[target.'cfg(windows)'.dependencies]
parking_lot_rt
=
{
version
=
"0.12.1"
}
...
...
@@ -67,7 +70,8 @@ rflow = ["dep:rflow"]
modbus
=
["dep:rmodbus"]
metrics
=
[
"dep:metrics"
,
"dep:metrics-exporter-prometheus"
,
"dep:metrics-exporter-scope"
,
"dep:tokio"
]
async
=
["dep:parking_lot_rt"]
full
=
[
"eapi"
,
"modbus"
,
"metrics"
,
"pipe"
,
"rvideo"
,
"rflow"
,
"async"
,
"json"
,
"msgpack"
]
full
=
[
"eapi"
,
"modbus"
,
"metrics"
,
"pipe"
,
"rvideo"
,
"rflow"
,
"async"
,
"json"
,
"msgpack"
,
"hmi"
]
hmi
=
[
"dep:egui"
,
"dep:eframe"
,
"dep:winit"
,
"dep:once_cell"
]
locking-default
=
[
"dep:parking_lot"
,
"rtsc/parking_lot"
,
"rvideo?/locking-default"
,
"rflow?/locking-default"
]
locking-rt
=
[
"dep:parking_lot_rt"
,
"rvideo?/locking-rt"
,
"rflow?/locking-rt"
]
...
...
@@ -126,3 +130,8 @@ required-features = ["eapi"]
name
=
"snmp-modbus"
path
=
"examples/snmp-modbus.rs"
required-features
=
[
"modbus"
,
"snmp2"
]
[[example]]
name
=
"hmi"
path
=
"examples/hmi.rs"
required-features
=
["hmi"]
examples/hmi.rs
0 → 100644
浏览文件 @
b0078bf3
use
std
::
sync
::
atomic
;
use
std
::
thread
;
use
roboplc
::
controller
::
prelude
::
*
;
use
roboplc
::
hmi
;
use
roboplc
::
prelude
::
*
;
use
rtsc
::
time
::
interval
;
use
tracing
::{
error
,
info
};
const
SHUTDOWN_TIMEOUT
:
Duration
=
Duration
::
from_secs
(
5
);
const
MAX
:
usize
=
600
;
type
Message
=
();
#[derive(Default)]
struct
Variables
{
counter1
:
atomic
::
AtomicUsize
,
}
// A simple worker which increments the counter every second
#[derive(WorkerOpts)]
#[worker_opts(cpu
=
0
,
priority
=
50
,
scheduling
=
"fifo"
,
blocking
=
true
)]
struct
CounterWorker
{}
impl
Worker
<
Message
,
Variables
>
for
CounterWorker
{
fn
run
(
&
mut
self
,
context
:
&
Context
<
Message
,
Variables
>
)
->
WResult
{
for
_
in
interval
(
Duration
::
from_secs
(
1
))
{
if
context
.variables
()
.counter1
.fetch_add
(
1
,
atomic
::
Ordering
::
Relaxed
)
==
MAX
{
context
.variables
()
.counter1
.store
(
0
,
atomic
::
Ordering
::
Relaxed
);
}
info!
(
"+1"
);
}
Ok
(())
}
}
fn
main
()
->
Result
<
(),
Box
<
dyn
std
::
error
::
Error
>>
{
roboplc
::
setup_panic
();
roboplc
::
configure_logger
(
roboplc
::
LevelFilter
::
Info
);
if
!
roboplc
::
is_production
()
{
roboplc
::
set_simulated
();
}
roboplc
::
thread_rt
::
prealloc_heap
(
10
_000_000
)
?
;
let
mut
controller
=
Controller
::
<
Message
,
Variables
>
::
new
();
controller
.spawn_worker
(
CounterWorker
{})
?
;
controller
.spawn_worker
(
HmiWorker
{})
?
;
controller
.register_signals
(
SHUTDOWN_TIMEOUT
)
?
;
controller
.block_while_online
();
hmi
::
stop
();
Ok
(())
}
// A worker which runs the HMI application
#[derive(WorkerOpts)]
#[worker_opts(cpu
=
1
,
priority
=
90
,
scheduling
=
"fifo"
,
blocking
=
true
)]
struct
HmiWorker
{}
impl
Worker
<
Message
,
Variables
>
for
HmiWorker
{
fn
run
(
&
mut
self
,
context
:
&
Context
<
Message
,
Variables
>
)
->
WResult
{
loop
{
let
mut
opts
=
hmi
::
AppOptions
::
default
();
if
roboplc
::
is_production
()
{
// For production - spawn Weston server
opts
=
opts
.with_server_options
(
hmi
::
ServerKind
::
Weston
.options
()
.with_kill_delay
(
Duration
::
from_secs
(
2
))
.with_spawn_delay
(
Duration
::
from_secs
(
5
)),
);
}
else
{
// For development - run windowed app with no server spawned
opts
=
opts
.windowed
();
}
if
let
Err
(
error
)
=
hmi
::
run
(
MyHmiApp
{},
context
,
opts
)
{
error!
(
"HMI error: {}"
,
error
);
}
thread
::
sleep
(
Duration
::
from_secs
(
5
));
}
}
}
struct
MyHmiApp
{}
// The application is basically equal to a typical egui app, the only difference is that the update
// function gets the controller context as an argument.
impl
hmi
::
App
for
MyHmiApp
{
type
M
=
Message
;
type
V
=
Variables
;
fn
update
(
&
mut
self
,
ctx
:
&
egui
::
Context
,
_frame
:
&
mut
eframe
::
Frame
,
plc_context
:
&
Context
<
Self
::
M
,
Self
::
V
>
,
)
{
egui
::
CentralPanel
::
default
()
.show
(
ctx
,
|
ui
|
{
ui
.with_layout
(
egui
::
Layout
::
left_to_right
(
egui
::
Align
::
Center
),
|
ui
|
{
ui
.vertical
(|
ui
|
{
ui
.label
(
egui
::
RichText
::
new
(
format!
(
"Counter: {}"
,
plc_context
.variables
()
.counter1
.load
(
atomic
::
Ordering
::
Relaxed
)
))
.size
(
48.0
),
);
ui
.horizontal
(|
ui
|
{
let
button_text
=
egui
::
RichText
::
new
(
"RESET"
)
.size
(
32.0
)
.strong
()
.color
(
egui
::
Color32
::
BLACK
);
let
button
=
egui
::
Button
::
new
(
button_text
)
.fill
(
egui
::
Color32
::
ORANGE
);
if
ui
.add
(
button
)
.clicked
()
{
plc_context
.variables
()
.counter1
.store
(
0
,
atomic
::
Ordering
::
Relaxed
);
ctx
.request_repaint_after
(
Duration
::
from_millis
(
10
));
}
let
button_text
=
egui
::
RichText
::
new
(
"SHUTDOWN PLC"
)
.size
(
32.0
)
.strong
()
.color
(
egui
::
Color32
::
BLACK
);
let
button
=
egui
::
Button
::
new
(
button_text
)
.fill
(
egui
::
Color32
::
LIGHT_RED
);
if
ui
.add
(
button
)
.clicked
()
{
plc_context
.terminate
();
}
});
});
});
});
ctx
.request_repaint_after
(
Duration
::
from_millis
(
200
));
}
}
src/hmi.rs
0 → 100755
浏览文件 @
b0078bf3
use
std
::{
collections
::
BTreeMap
,
ffi
::{
OsStr
,
OsString
},
path
::
Path
,
process
::
Child
,
thread
,
time
::
Duration
,
};
use
crate
::
locking
::
Mutex
;
use
crate
::{
prelude
::
Context
,
DataDeliveryPolicy
};
use
crate
::{
Error
,
Result
};
use
eframe
::
EventLoopBuilderHook
;
use
once_cell
::
sync
::
Lazy
;
use
tracing
::{
error
,
warn
};
static
SERVER_INSTANCE
:
Lazy
<
Mutex
<
Option
<
Child
>>>
=
Lazy
::
new
(||
Mutex
::
new
(
None
));
/// Graphics server options
#[derive(Clone,
Debug,
Eq,
PartialEq)]
pub
struct
ServerOptions
{
command
:
OsString
,
kill_command
:
Option
<
OsString
>
,
env
:
BTreeMap
<
String
,
String
>
,
wait_for
:
Option
<
OsString
>
,
kill_delay
:
Duration
,
spawn_delay
:
Duration
,
}
impl
ServerOptions
{
/// Creates a new server options with the given launch command
pub
fn
new
<
C
:
AsRef
<
OsStr
>>
(
command
:
C
)
->
Self
{
Self
{
command
:
command
.as_ref
()
.to_owned
(),
kill_command
:
None
,
env
:
<
_
>
::
default
(),
wait_for
:
None
,
spawn_delay
:
Duration
::
from_secs
(
5
),
kill_delay
:
Duration
::
from_secs
(
5
),
}
}
/// The command is executed to terminate the previous server instance if there is a conflict
/// (e.g. the previous program instance crashed and left the server running).
pub
fn
with_terminate_previous_command
<
C
:
AsRef
<
OsStr
>>
(
mut
self
,
kill_command
:
C
)
->
Self
{
self
.kill_command
=
Some
(
kill_command
.as_ref
()
.to_owned
());
self
}
/// Adds an environment variable to the HMI thread after the server is started
pub
fn
with_env
(
mut
self
,
key
:
&
str
,
value
:
&
str
)
->
Self
{
self
.env
.insert
(
key
.to_string
(),
value
.to_string
());
self
}
/// Wait for a file (server socket) before starting the application
pub
fn
with_wait_for
<
C
:
AsRef
<
OsStr
>>
(
mut
self
,
wait_for
:
C
)
->
Self
{
self
.wait_for
=
Some
(
wait_for
.as_ref
()
.to_owned
());
self
}
/// Delay before starting the application after the server is started
pub
fn
with_spawn_delay
(
mut
self
,
delay
:
Duration
)
->
Self
{
self
.spawn_delay
=
delay
;
self
}
/// Delay after the server is killed to ensure that TTY is released
pub
fn
with_kill_delay
(
mut
self
,
delay
:
Duration
)
->
Self
{
self
.kill_delay
=
delay
;
self
}
}
/// Graphics server kind
#[derive(Copy,
Clone,
Debug,
Eq,
PartialEq)]
pub
enum
ServerKind
{
/// Weston server
Weston
,
/// Legacy weston server, adds `--tty=1` to the command line
WestonLegacy
,
/// Xorg server
Xorg
,
}
impl
ServerKind
{
/// Returns the server options for the given server kind
pub
fn
options
(
self
:
ServerKind
)
->
ServerOptions
{
match
self
{
ServerKind
::
Weston
|
ServerKind
::
WestonLegacy
=>
{
let
mut
opts
=
if
self
==
ServerKind
::
Weston
{
ServerOptions
::
new
(
"weston"
)
}
else
{
ServerOptions
::
new
(
"weston --tty=1"
)
};
opts
=
opts
.with_env
(
"WAYLAND_DISPLAY"
,
"wayland-1"
)
.with_wait_for
(
"/run/user/0/wayland-1"
)
.with_terminate_previous_command
(
"pkill -KILL weston"
);
opts
}
ServerKind
::
Xorg
=>
{
let
mut
opts
=
ServerOptions
::
new
(
"Xorg :0"
);
opts
=
opts
.with_env
(
"DISPLAY"
,
":0"
)
.with_wait_for
(
"/tmp/.X11-unix/X0"
)
.with_terminate_previous_command
(
"pkill -KILL Xorg"
);
opts
}
}
}
}
/// HMI application options
#[derive(Clone,
Debug)]
pub
struct
AppOptions
{
fullscreen
:
bool
,
title
:
String
,
dimensions
:
Option
<
(
u16
,
u16
)
>
,
server_options
:
Option
<
ServerOptions
>
,
}
impl
Default
for
AppOptions
{
fn
default
()
->
Self
{
Self
{
fullscreen
:
true
,
title
:
"HMI"
.to_string
(),
dimensions
:
None
,
server_options
:
None
,
}
}
}
impl
AppOptions
{
/// Creates a new HMI application options
pub
fn
new
()
->
Self
{
Self
::
default
()
}
/// Runs the HMI application in windowed mode (default is fullscreen)
pub
fn
windowed
(
mut
self
)
->
Self
{
self
.fullscreen
=
false
;
self
}
/// Sets the title of the HMI application window (required for Xorg)
pub
fn
with_dimensions
(
mut
self
,
width
:
u16
,
height
:
u16
)
->
Self
{
self
.dimensions
=
Some
((
width
,
height
));
self
}
/// Sets the server options
pub
fn
with_server_options
(
mut
self
,
opts
:
ServerOptions
)
->
Self
{
self
.server_options
=
Some
(
opts
);
self
}
}
/// HMI application, a wrapper around an eframe application
pub
trait
App
{
/// Context message
type
M
:
DataDeliveryPolicy
+
Send
+
Sync
+
Clone
;
/// Context variables
type
V
:
Send
;
/// UI update, similar to eframe::App::update but with PLC program context
fn
update
(
&
mut
self
,
ctx
:
&
egui
::
Context
,
frame
:
&
mut
eframe
::
Frame
,
plc_context
:
&
Context
<
Self
::
M
,
Self
::
V
>
,
);
}
/// Stop HMI server if running
pub
fn
stop
()
{
if
let
Some
(
child
)
=
SERVER_INSTANCE
.lock
()
.take
()
{
let
pid
=
child
.id
();
#[allow(clippy
::
cast_possible_wrap)]
crate
::
thread_rt
::
kill_pstree
(
pid
as
i32
,
true
,
None
);
}
}
/// Start HMI server (for own use, not required for the HMI application)
pub
fn
start_server
(
server_options
:
ServerOptions
)
{
if
let
Some
(
kill_command
)
=
&
server_options
.kill_command
{
match
std
::
process
::
Command
::
new
(
"sh"
)
.args
([
OsString
::
from
(
"-c"
),
kill_command
.to_owned
()])
.spawn
()
{
Ok
(
mut
child
)
=>
{
let
_
=
child
.wait
();
thread
::
sleep
(
server_options
.kill_delay
);
}
Err
(
error
)
=>
{
warn!
(
?
error
,
"Failed to terminate previous server instance"
);
}
}
}
#[cfg(target_os
=
"linux"
)]
{
let
uid
=
unsafe
{
libc
::
getuid
()
};
if
let
Err
(
error
)
=
std
::
fs
::
create_dir_all
(
Path
::
new
(
"/run/user"
)
.join
(
uid
.to_string
()))
{
error!
(
?
error
,
"Failed to create /run/user/<uid> directory"
);
}
}
std
::
env
::
set_var
(
"XDG_RUNTIME_DIR"
,
"/run/user/0"
);
let
child
=
match
std
::
process
::
Command
::
new
(
"sh"
)
.args
([
OsString
::
from
(
"-c"
),
server_options
.command
.clone
()])
.spawn
()
{
Ok
(
c
)
=>
c
,
Err
(
error
)
=>
{
error!
(
?
error
,
"Failed to start graphics server"
);
loop
{
thread
::
park
();
}
}
};
*
SERVER_INSTANCE
.lock
()
=
Some
(
child
);
for
(
key
,
value
)
in
&
server_options
.env
{
std
::
env
::
set_var
(
key
,
value
);
}
if
let
Some
(
wait_for
)
=
server_options
.wait_for
{
let
wait_for
=
Path
::
new
(
&
wait_for
);
loop
{
if
wait_for
.exists
()
{
break
;
}
thread
::
sleep
(
Duration
::
from_millis
(
100
));
}
}
thread
::
sleep
(
server_options
.spawn_delay
);
}
/// Run HMI application.
///
/// Starts the HMI server if required, then runs the HMI application.
pub
fn
run
<
A
,
M
,
V
>
(
app
:
A
,
plc_context
:
&
Context
<
M
,
V
>
,
options
:
AppOptions
)
->
Result
<
()
>
where
A
:
App
<
M
=
M
,
V
=
V
>
,
M
:
DataDeliveryPolicy
+
Send
+
Sync
+
Clone
+
'static
,
V
:
Send
,
{
stop
();
if
let
Some
(
opts
)
=
options
.server_options
{
start_server
(
opts
);
};
let
event_loop_builder
:
Option
<
EventLoopBuilderHook
>
=
Some
(
Box
::
new
(|
event_loop_builder
|
{
winit
::
platform
::
wayland
::
EventLoopBuilderExtWayland
::
with_any_thread
(
event_loop_builder
,
true
,
);
}));
let
mut
viewport
=
egui
::
ViewportBuilder
::
default
()
.with_fullscreen
(
options
.fullscreen
);
if
let
Some
((
width
,
height
))
=
options
.dimensions
{
viewport
=
viewport
.with_inner_size
((
f32
::
from
(
width
),
f32
::
from
(
height
)));
}
let
e_options
=
eframe
::
NativeOptions
{
viewport
,
event_loop_builder
,
..
Default
::
default
()
};
let
plc_context
=
plc_context
.clone
();
eframe
::
run_native
(
&
options
.title
,
e_options
,
Box
::
new
(|
_cc
|
Ok
(
Box
::
new
(
Hmi
{
app
,
plc_context
}))),
)
.map_err
(
Error
::
failed
)
}
struct
Hmi
<
A
,
M
,
V
>
where
A
:
App
<
M
=
M
,
V
=
V
>
,
M
:
DataDeliveryPolicy
+
Send
+
Sync
+
Clone
+
'static
,
V
:
Send
,
{
app
:
A
,
plc_context
:
Context
<
M
,
V
>
,
}
impl
<
A
,
M
,
V
>
eframe
::
App
for
Hmi
<
A
,
M
,
V
>
where
A
:
App
<
M
=
M
,
V
=
V
>
,
M
:
DataDeliveryPolicy
+
Send
+
Sync
+
Clone
+
'static
,
V
:
Send
,
{
fn
update
(
&
mut
self
,
ctx
:
&
egui
::
Context
,
_frame
:
&
mut
eframe
::
Frame
)
{
self
.app
.update
(
ctx
,
_frame
,
&
self
.plc_context
);
}
}
src/lib.rs
浏览文件 @
b0078bf3
...
...
@@ -95,6 +95,9 @@ pub use rtsc::data_policy::{DataDeliveryPolicy, DeliveryPolicy};
pub
mod
comm
;
/// Controller and workers
pub
mod
controller
;
/// HMI (Human-Machine Interface) API
#[cfg(feature
=
"hmi"
)]
pub
mod
hmi
;
/// In-process data communication pub/sub hub, synchronous edition
pub
mod
hub
;
/// In-process data communication pub/sub hub, asynchronous edition
...
...
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论