├── .cargo
└── config.toml
├── .gitattributes
├── .gitignore
├── .vscode
└── settings.json
├── 135x240_black_1fps.screen
├── 160x128_10fps.screen
├── 160x128_black_0.5fps.screen
├── 160x128_black_10fps.screen
├── 160x128_black_1fps.screen
├── 160x128_video2_10fps.screen
├── 240x240_black_1fps.screen
├── 320x240_10fps.screen
├── 320x240_1fps.screen
├── 320x240_2fps.screen
├── 320x240_black_10fps.screen
├── 320x240_black_1fps.screen
├── 480x320_5fps.screen
├── Cargo.lock
├── Cargo.toml
├── Cross.toml
├── LICENSE
├── OpenHardwareMonitorService
├── .gitignore
├── App.config
├── OpenHardwareMonitorService.csproj
├── OpenHardwareMonitorService.sln
├── Program.cs
├── Properties
│ ├── AssemblyInfo.cs
│ ├── Resources.Designer.cs
│ ├── Resources.resx
│ ├── Settings.Designer.cs
│ └── Settings.settings
├── app.manifest
├── bin
│ └── Release
│ │ ├── OpenHardwareMonitorService.exe
│ │ ├── OpenHardwareMonitorService.exe.config
│ │ └── OpenHardwareMonitorService.pdb
├── libs
│ ├── Newtonsoft.Json.dll
│ └── OpenHardwareMonitorLib.dll
└── packages.config
├── README.md
├── build-aarch64-musl.sh.md
├── build-x86_64_linux.sh
├── build-x86_64_windows.cmd
├── build.rs
├── cities.json
├── fonts
└── VonwaonBitmap-16px.ttf
├── images
├── 0.png
├── 1.png
├── 10.png
├── 11.png
├── 12.png
├── 13.png
├── 14.png
├── 15.png
├── 16.png
├── 17.png
├── 18.png
├── 19.png
├── 2.png
├── 20.png
├── 21.png
├── 22.png
├── 23.png
├── 24.png
├── 25.png
├── 26.png
├── 27.png
├── 28.png
├── 29.png
├── 3.png
├── 30.png
├── 31.png
├── 32.png
├── 4.png
├── 5.png
├── 6.png
├── 7.png
├── 8.png
├── 9.png
├── crosschair.png
├── icon_clock.png
├── icon_cpu.png
├── icon_date1.png
├── icon_date2.png
├── icon_download.png
├── icon_drive.png
├── icon_fan.png
├── icon_font.png
├── icon_host.png
├── icon_ip.png
├── icon_lunar1.png
├── icon_lunar2.png
├── icon_percent.png
├── icon_photo.png
├── icon_photo_blue.png
├── icon_pointer.png
├── icon_process.png
├── icon_ram.png
├── icon_rotate.png
├── icon_swap.png
├── icon_system.png
├── icon_temperature.png
├── icon_text.png
├── icon_text_blue.png
├── icon_time.png
├── icon_upload.png
├── icon_version1.png
├── icon_version2.png
├── icon_weather.png
├── icon_webcam.png
├── monitor.ico
├── monitor.png
├── picker.png
├── rp2040.png
├── st7735.png
└── st7789.png
├── run.cmd
├── run.sh
├── src
├── editor.rs
├── main.rs
├── monitor.rs
├── nmc.rs
├── rgb565.rs
├── screen.rs
├── usb_screen.rs
├── utils.rs
├── widgets.rs
├── wifi_screen.rs
└── yuv422.rs
└── view
├── main.slint
└── widgets
├── abutton.slint
└── widgets.slint
/.cargo/config.toml:
--------------------------------------------------------------------------------
1 |
2 | [target.x86_64-pc-windows-msvc]
3 | rustflags = ["-C", "target-feature=+crt-static"]
4 |
5 | [target.i686-pc-windows-msvc]
6 | rustflags = ["-C", "target-feature=+crt-static"]
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | /.history
3 | #git rm -r --cached OpenHardwareMonitorService/obj
4 | /OpenHardwareMonitorService/obj
5 | /dist
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "cSpell.words": [
3 | "adcode",
4 | "airpressure",
5 | "amap",
6 | "argb",
7 | "blit",
8 | "codegen",
9 | "consts",
10 | "COUNTERVALUE",
11 | "DOENVSUBST",
12 | "feelst",
13 | "femtovg",
14 | "HINSTANCE",
15 | "hkey",
16 | "HWND",
17 | "icomfort",
18 | "libloading",
19 | "MJPEG",
20 | "nheight",
21 | "NOCLOSEPROCESS",
22 | "nusb",
23 | "nwidth",
24 | "PCSTR",
25 | "psutil",
26 | "rcomfort",
27 | "repr",
28 | "runas",
29 | "serde",
30 | "SHELLEXECUTEINFOA",
31 | "tungstenite",
32 | "Uninit",
33 | "Vonwaon",
34 | "winapi",
35 | "winddirection",
36 | "windpower",
37 | "winres",
38 | "YUYV"
39 | ],
40 | "dotnet.preferCSharpExtension": true
41 | }
--------------------------------------------------------------------------------
/135x240_black_1fps.screen:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/135x240_black_1fps.screen
--------------------------------------------------------------------------------
/160x128_10fps.screen:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/160x128_10fps.screen
--------------------------------------------------------------------------------
/160x128_black_0.5fps.screen:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/160x128_black_0.5fps.screen
--------------------------------------------------------------------------------
/160x128_black_10fps.screen:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/160x128_black_10fps.screen
--------------------------------------------------------------------------------
/160x128_black_1fps.screen:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/160x128_black_1fps.screen
--------------------------------------------------------------------------------
/160x128_video2_10fps.screen:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/160x128_video2_10fps.screen
--------------------------------------------------------------------------------
/240x240_black_1fps.screen:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/240x240_black_1fps.screen
--------------------------------------------------------------------------------
/320x240_10fps.screen:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/320x240_10fps.screen
--------------------------------------------------------------------------------
/320x240_1fps.screen:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/320x240_1fps.screen
--------------------------------------------------------------------------------
/320x240_2fps.screen:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/320x240_2fps.screen
--------------------------------------------------------------------------------
/320x240_black_10fps.screen:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/320x240_black_10fps.screen
--------------------------------------------------------------------------------
/320x240_black_1fps.screen:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/320x240_black_1fps.screen
--------------------------------------------------------------------------------
/480x320_5fps.screen:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/480x320_5fps.screen
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "USB-Screen"
3 | version = "1.1.11"
4 | edition = "2021"
5 |
6 | [features]
7 | # aarch64 linux
8 | default = ["v4l-webcam", "usb-serial"]
9 | # windows
10 | # default = ["editor", "tray", "nokhwa-webcam", "usb-serial"]
11 | # x86_64 linux
12 | # default = ["editor", "v4l-webcam", "usb-serial"]
13 | # 飞牛OS
14 | # default = ["v4l-webcam", "usb-serial"]
15 | nokhwa-webcam = ["nokhwa"]
16 | v4l-webcam = ["v4l"]
17 | editor = ["slint"]
18 | tray = ["tray-icon", "tao"]
19 | usb-serial = ["serialport"]
20 |
21 | [dependencies]
22 | anyhow = "1"
23 | sysinfo = "0.30.12"
24 | chrono = "0.4.39"
25 | rust-ephemeris = "0.1.0"
26 | chinese-number = "0.7.7"
27 | precord-core = "0.7.11"
28 | serde_json = "1.0"
29 | once_cell = "1.20.3"
30 | reqwest = { version = "0.12.15", default-features = false, features = ["rustls-tls", "blocking", "json"] }
31 | offscreen-canvas = { git = "https://github.com/planet0104/offscreen-canvas", tag = "0.1.9"}
32 | # bincode = "2.0.0-rc.3"
33 | lz4_flex = "0.11.3"
34 | serde = { version = "1", features = ["derive"] }
35 | uuid = { version = "1.13.1", features = [ "v4" ]}
36 | image = "0.25.1"
37 | hex_color = "3.0.0"
38 | rfd = "0.15.2"
39 | gif = "0.13.1"
40 | gif-dispose = "5.0.1"
41 | env_logger = "0.11.6"
42 | log = "0.4.25"
43 | num_cpus = "1"
44 | ttf-parser = "0.25.1"
45 | local-ip-address = "0.6.3"
46 | nusb = "0.1.12"
47 | futures-lite = "2.6.0"
48 | serialport = { version="4.7.0", optional = true }
49 | slint = { version="1.9.2", optional = true, default-features = false, features = [
50 | "std",
51 | "backend-default",
52 | "renderer-femtovg",
53 | "renderer-software",
54 | "compat-1-2",
55 | ] }
56 | nokhwa = { version="0.10.7", features = ["input-native"], optional = true }
57 | human-repr = "1.1.0"
58 | fast_image_resize = "5.1.1"
59 | async-std = { version = "1", features = ["attributes"] }
60 | crossbeam-channel = "0.5.14"
61 | tungstenite = "0.26.1"
62 | rustls = { version = "0.23.26", registry = "crates-io" }
63 | # embedded-graphics = "0.8.1"
64 | # byteorder = "1"
65 |
66 | [target.'cfg(not(target_os = "linux"))'.dependencies]
67 | tray-icon = { version="0.19.2", optional = true }
68 | tao = { version="0.31.1", optional = true }
69 |
70 | [target.'cfg(windows)'.dependencies]
71 | windows = { version = "0.59", features = [ "Win32_System_Performance", "Win32_UI_WindowsAndMessaging", "Win32_System_Threading", "Win32_Security", "Win32_UI_Shell", "Win32_System_Registry" ]}
72 | tiny_http = "0.12"
73 |
74 | [target.'cfg(not(windows))'.dependencies]
75 | psutil = "3.3.0"
76 |
77 | [target.'cfg(target_os = "linux")'.dependencies]
78 | v4l = { version="0.14.0", default-features = false, features = ["v4l2"], optional = true }
79 |
80 | [build-dependencies]
81 | slint-build = "1.9.2"
82 |
83 | [target.'cfg(windows)'.build-dependencies]
84 | winres = "0.1.12"
85 |
86 | [profile.release]
87 | strip = true
88 | opt-level = "z"
89 | lto = true
90 | panic = "abort"
91 | codegen-units = 1
--------------------------------------------------------------------------------
/Cross.toml:
--------------------------------------------------------------------------------
1 | [target.aarch64-unknown-linux-gnu]
2 | pre-build = [
3 | "dpkg --add-architecture $CROSS_DEB_ARCH",
4 | "apt-get update && apt-get install -y libclang-dev libv4l-dev libudev-dev:$CROSS_DEB_ARCH",
5 | ]
6 |
7 | [target.x86_64-unknown-linux-gnu]
8 | pre-build = [
9 | "dpkg --add-architecture $CROSS_DEB_ARCH",
10 | "apt-get update && apt-get install -y libclang-dev libv4l-dev libudev-dev:$CROSS_DEB_ARCH",
11 | ]
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Jia Ye
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/OpenHardwareMonitorService/.gitignore:
--------------------------------------------------------------------------------
1 | /bin/Debug
2 | /.vs
3 | /obj
4 | /packages
--------------------------------------------------------------------------------
/OpenHardwareMonitorService/App.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/OpenHardwareMonitorService/OpenHardwareMonitorService.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | AnyCPU
7 | {866B2D60-AE07-43EE-937E-F4129843CD6F}
8 | WinExe
9 | OpenHardwareMonitorService
10 | OpenHardwareMonitorService
11 | v4.5.1
12 | 512
13 | true
14 | true
15 |
16 |
17 | AnyCPU
18 | true
19 | full
20 | false
21 | bin\Debug\
22 | DEBUG;TRACE
23 | prompt
24 | 4
25 |
26 |
27 | AnyCPU
28 | pdbonly
29 | true
30 | bin\Release\
31 | TRACE
32 | prompt
33 | 4
34 |
35 |
36 | app.manifest
37 |
38 |
39 |
40 |
41 |
42 |
43 | packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll
44 | False
45 |
46 |
47 | libs\OpenHardwareMonitorLib.dll
48 | False
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | ResXFileCodeGenerator
67 | Resources.Designer.cs
68 | Designer
69 |
70 |
71 | True
72 | Resources.resx
73 |
74 |
75 |
76 |
77 | SettingsSingleFileGenerator
78 | Settings.Designer.cs
79 |
80 |
81 | True
82 | Settings.settings
83 | True
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
--------------------------------------------------------------------------------
/OpenHardwareMonitorService/OpenHardwareMonitorService.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.9.34723.18
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenHardwareMonitorService", "OpenHardwareMonitorService.csproj", "{866B2D60-AE07-43EE-937E-F4129843CD6F}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Release|Any CPU = Release|Any CPU
12 | EndGlobalSection
13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
14 | {866B2D60-AE07-43EE-937E-F4129843CD6F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15 | {866B2D60-AE07-43EE-937E-F4129843CD6F}.Debug|Any CPU.Build.0 = Debug|Any CPU
16 | {866B2D60-AE07-43EE-937E-F4129843CD6F}.Release|Any CPU.ActiveCfg = Release|Any CPU
17 | {866B2D60-AE07-43EE-937E-F4129843CD6F}.Release|Any CPU.Build.0 = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(SolutionProperties) = preSolution
20 | HideSolutionNode = FALSE
21 | EndGlobalSection
22 | GlobalSection(ExtensibilityGlobals) = postSolution
23 | SolutionGuid = {D8A6215C-2B80-4DD4-88D2-0F7357A7AE42}
24 | EndGlobalSection
25 | EndGlobal
26 |
--------------------------------------------------------------------------------
/OpenHardwareMonitorService/Program.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 | using OpenHardwareMonitor.Hardware;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Diagnostics;
6 | using System.IO;
7 | using System.Net.Http;
8 | using System.Reflection;
9 | using System.Text;
10 | using System.Threading;
11 | using System.Threading.Tasks;
12 | using System.Windows.Forms;
13 |
14 | namespace OpenHardwareMonitorService
15 | {
16 | public class UpdateVisitor : IVisitor
17 | {
18 | public void VisitComputer(IComputer computer)
19 | {
20 | computer.Traverse(this);
21 | }
22 | public void VisitHardware(IHardware hardware)
23 | {
24 | hardware.Update();
25 | foreach (IHardware subHardware in hardware.SubHardware) subHardware.Accept(this);
26 | }
27 | public void VisitSensor(ISensor sensor) { }
28 | public void VisitParameter(IParameter parameter) { }
29 | }
30 |
31 | public class HardwareInfo
32 | {
33 | public readonly List fans = new List();
34 | public readonly List temperatures = new List();
35 | public readonly List loads = new List();
36 | public readonly List clocks = new List();
37 | public readonly List powers = new List();
38 | public float package_power = 0;
39 | public float cores_power = 0;
40 | public float total_load = 0;
41 | public float total_temperature = 0;
42 | public float memory_load = 0;
43 | public float memory_total = 0;
44 | }
45 |
46 | internal static class Program
47 | {
48 | static string BaseUrl = "http://localhost/";
49 | private static readonly HttpClient httpClient = new HttpClient();
50 |
51 | static Program()
52 | {
53 | //内嵌OpenHardwareMonitor的DLL
54 | AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
55 | }
56 |
57 | private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs e)
58 | {
59 | string _resName = "OpenHardwareMonitorService.libs." + new AssemblyName(e.Name).Name + ".dll";
60 | Console.WriteLine("_resName:" + _resName);
61 | using (var _stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(_resName))
62 | {
63 | byte[] _data = new byte[_stream.Length];
64 | _stream.Read(_data, 0, _data.Length);
65 | return Assembly.Load(_data);
66 | }
67 | }
68 |
69 | //private static void WriteLog(string message)
70 |
71 | //{
72 |
73 | // string logFile = "LogFile.txt";
74 |
75 | // File.AppendAllText(logFile, DateTime.Now.ToString() + ": " + message + Environment.NewLine);
76 |
77 | //}
78 |
79 | static void Main(string[] args)
80 | {
81 | if(args.Length > 0)
82 | {
83 | BaseUrl = "http://localhost:"+args[0]+"/";
84 | }
85 | foreach (string arg in args)
86 | {
87 | Console.WriteLine("参数:" + args[0]);
88 | }
89 |
90 | System.Threading.Mutex mutex = new System.Threading.Mutex(true, Application.ProductName, out bool ret);
91 | if (!ret)
92 | {
93 | MessageBox.Show("OpenHardwareMonitorService经运行!");
94 | Application.Exit();
95 | return;
96 | }
97 |
98 | //开始监测硬件
99 | UpdateVisitor updateVisitor = new UpdateVisitor();
100 | Computer computer = new Computer();
101 | computer.Open();
102 | computer.CPUEnabled = true;
103 | computer.GPUEnabled = true;
104 |
105 | while (true)
106 | {
107 | computer.Accept(updateVisitor);
108 |
109 | var cpu_infos = new List();
110 | var gpu_infos = new List();
111 |
112 | foreach (var hardware in computer.Hardware)
113 | {
114 | var hardware_info = new HardwareInfo();
115 |
116 | foreach (var sensor in hardware.Sensors)
117 | {
118 | if (sensor.SensorType == SensorType.Temperature)
119 | {
120 | if (sensor.Name.Contains("Package"))
121 | {
122 | hardware_info.total_temperature = sensor.Value.Value;
123 | }
124 | else
125 | {
126 | hardware_info.temperatures.Add(sensor.Value.Value);
127 | }
128 | }
129 | else if(sensor.SensorType == SensorType.Control){
130 | }
131 | else if (sensor.SensorType == SensorType.Fan)
132 | {
133 | hardware_info.fans.Add(sensor.Value.Value);
134 | }
135 | else if (sensor.SensorType == SensorType.Clock)
136 | {
137 | //Console.WriteLine("sensor.Name=" + sensor.Name);
138 | //Console.WriteLine("sensor.SensorType=" + sensor.SensorType);
139 | if (sensor.Name.Contains("Bus"))
140 | {
141 | continue;
142 | }
143 | if (sensor.Name.Contains("Memory"))
144 | {
145 | continue;
146 | }
147 | if (sensor.Name.Contains("Shader")){
148 | continue;
149 | }
150 | hardware_info.clocks.Add(sensor.Value.Value);
151 | }
152 | else if (sensor.SensorType == SensorType.Load)
153 | {
154 | if (sensor.Name.Contains("Total"))
155 | {
156 | hardware_info.total_load = sensor.Value.Value;
157 | }
158 | else if(sensor.Name.Contains("Core"))
159 | {
160 | hardware_info.loads.Add(sensor.Value.Value);
161 | }else if(sensor.Name.Contains("Memory")){
162 | hardware_info.memory_load = sensor.Value.Value;
163 | }
164 | }else if(sensor.SensorType == SensorType.Power)
165 | {
166 | if (sensor.Name.Contains("Package"))
167 | {
168 | hardware_info.package_power = sensor.Value.Value;
169 | }else if (sensor.Name.Contains("Cores"))
170 | {
171 | hardware_info.cores_power = sensor.Value.Value;
172 | }
173 | else
174 | {
175 | hardware_info.powers.Add(sensor.Value.Value);
176 | }
177 | }else if(sensor.SensorType == SensorType.SmallData){
178 | if(sensor.Name == "GPU Memory Total"){
179 | hardware_info.memory_total = sensor.Value.Value;
180 | }
181 | }
182 | }
183 |
184 | if (hardware.HardwareType == HardwareType.CPU)
185 | {
186 | cpu_infos.Add(hardware_info);
187 | }
188 |
189 | if (hardware.HardwareType == HardwareType.GpuAti || hardware.HardwareType == HardwareType.GpuNvidia)
190 | {
191 | gpu_infos.Add(hardware_info);
192 | }
193 | }
194 |
195 | var jsonData = JsonConvert.SerializeObject(new { cpu_infos, gpu_infos });
196 |
197 | //Console.WriteLine(jsonData);
198 | //Thread.Sleep(1000*10);
199 |
200 | // 发送数据到Rust
201 | /*
202 | 1、调用http://localhost/isOpen 返回true继续,超时或返回false则结束进程
203 | 2、调用http://localhost/upload 发送cpu、gpu的json数据
204 | */
205 | if (!CheckIsOpen())
206 | {
207 | Console.WriteLine("服务不可用,结束进程...");
208 | computer.Close();
209 | Application.Exit();
210 | return;
211 | }
212 |
213 | SendData(jsonData);
214 |
215 | Thread.Sleep(1000);
216 | }
217 | }
218 |
219 | private static bool CheckIsOpen()
220 | {
221 | try
222 | {
223 | using (var httpClient = new HttpClient())
224 | {
225 | httpClient.Timeout = TimeSpan.FromSeconds(3);
226 |
227 | var request = new HttpRequestMessage(HttpMethod.Get, $"{BaseUrl}isOpen");
228 |
229 | var result = httpClient.SendAsync(request).Result;
230 |
231 | if (result.StatusCode == System.Net.HttpStatusCode.OK)
232 | {
233 | string content = result.Content.ReadAsStringAsync().Result;
234 | return content.Trim().ToLower() == "true";
235 | }
236 | }
237 | }catch (Exception ex)
238 | {
239 | Debug.WriteLine(ex);
240 | }
241 | return false;
242 | }
243 |
244 | private static void SendData(string jsonData)
245 | {
246 | try
247 | {
248 | using (var httpClient = new HttpClient())
249 | {
250 | httpClient.Timeout = TimeSpan.FromSeconds(3);
251 |
252 | var content = new StringContent(jsonData, Encoding.UTF8, "application/json");
253 | var request = new HttpRequestMessage(HttpMethod.Post, $"{BaseUrl}upload") { Content = content };
254 |
255 | var response = httpClient.SendAsync(request).Result;
256 | if (response.IsSuccessStatusCode)
257 | {
258 | Console.WriteLine("数据上传成功!");
259 | }
260 | else
261 | {
262 | Console.WriteLine($"上传数据失败,HTTP状态码:{response.StatusCode}");
263 | }
264 | }
265 | }
266 | catch(Exception ex) { Debug.WriteLine(ex); }
267 | }
268 | }
269 | }
270 |
--------------------------------------------------------------------------------
/OpenHardwareMonitorService/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 | using System.Runtime.InteropServices;
4 |
5 | // 有关程序集的一般信息由以下
6 | // 控制。更改这些特性值可修改
7 | // 与程序集关联的信息。
8 | [assembly: AssemblyTitle("OpenHardwareMonitorService")]
9 | [assembly: AssemblyDescription("")]
10 | [assembly: AssemblyConfiguration("")]
11 | [assembly: AssemblyCompany("")]
12 | [assembly: AssemblyProduct("OpenHardwareMonitorService")]
13 | [assembly: AssemblyCopyright("Copyright © 2024")]
14 | [assembly: AssemblyTrademark("")]
15 | [assembly: AssemblyCulture("")]
16 |
17 | // 将 ComVisible 设置为 false 会使此程序集中的类型
18 | //对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型
19 | //请将此类型的 ComVisible 特性设置为 true。
20 | [assembly: ComVisible(false)]
21 |
22 | // 如果此项目向 COM 公开,则下列 GUID 用于类型库的 ID
23 | [assembly: Guid("866b2d60-ae07-43ee-937e-f4129843cd6f")]
24 |
25 | // 程序集的版本信息由下列四个值组成:
26 | //
27 | // 主版本
28 | // 次版本
29 | // 生成号
30 | // 修订号
31 | //
32 | //可以指定所有这些值,也可以使用“生成号”和“修订号”的默认值
33 | //通过使用 "*",如下所示:
34 | // [assembly: AssemblyVersion("1.0.*")]
35 | [assembly: AssemblyVersion("1.0.0.0")]
36 | [assembly: AssemblyFileVersion("1.0.0.0")]
37 |
--------------------------------------------------------------------------------
/OpenHardwareMonitorService/Properties/Resources.Designer.cs:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | //
3 | // 此代码由工具生成。
4 | // 运行时版本: 4.0.30319.42000
5 | //
6 | // 对此文件的更改可能导致不正确的行为,如果
7 | // 重新生成代码,则所做更改将丢失。
8 | //
9 | //------------------------------------------------------------------------------
10 |
11 | namespace OpenHardwareMonitorService.Properties
12 | {
13 |
14 |
15 | ///
16 | /// 强类型资源类,用于查找本地化字符串等。
17 | ///
18 | // 此类是由 StronglyTypedResourceBuilder
19 | // 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。
20 | // 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen
21 | // (以 /str 作为命令选项),或重新生成 VS 项目。
22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
25 | internal class Resources
26 | {
27 |
28 | private static global::System.Resources.ResourceManager resourceMan;
29 |
30 | private static global::System.Globalization.CultureInfo resourceCulture;
31 |
32 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
33 | internal Resources()
34 | {
35 | }
36 |
37 | ///
38 | /// 返回此类使用的缓存 ResourceManager 实例。
39 | ///
40 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
41 | internal static global::System.Resources.ResourceManager ResourceManager
42 | {
43 | get
44 | {
45 | if ((resourceMan == null))
46 | {
47 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("OpenHardwareMonitorService.Properties.Resources", typeof(Resources).Assembly);
48 | resourceMan = temp;
49 | }
50 | return resourceMan;
51 | }
52 | }
53 |
54 | ///
55 | /// 重写当前线程的 CurrentUICulture 属性,对
56 | /// 使用此强类型资源类的所有资源查找执行重写。
57 | ///
58 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
59 | internal static global::System.Globalization.CultureInfo Culture
60 | {
61 | get
62 | {
63 | return resourceCulture;
64 | }
65 | set
66 | {
67 | resourceCulture = value;
68 | }
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/OpenHardwareMonitorService/Properties/Resources.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 | text/microsoft-resx
107 |
108 |
109 | 2.0
110 |
111 |
112 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
113 |
114 |
115 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
116 |
117 |
--------------------------------------------------------------------------------
/OpenHardwareMonitorService/Properties/Settings.Designer.cs:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | //
3 | // This code was generated by a tool.
4 | // Runtime Version:4.0.30319.42000
5 | //
6 | // Changes to this file may cause incorrect behavior and will be lost if
7 | // the code is regenerated.
8 | //
9 | //------------------------------------------------------------------------------
10 |
11 | namespace OpenHardwareMonitorService.Properties
12 | {
13 |
14 |
15 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
16 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")]
17 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase
18 | {
19 |
20 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
21 |
22 | public static Settings Default
23 | {
24 | get
25 | {
26 | return defaultInstance;
27 | }
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/OpenHardwareMonitorService/Properties/Settings.settings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/OpenHardwareMonitorService/app.manifest:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
54 |
62 |
63 |
64 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/OpenHardwareMonitorService/bin/Release/OpenHardwareMonitorService.exe:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/OpenHardwareMonitorService/bin/Release/OpenHardwareMonitorService.exe
--------------------------------------------------------------------------------
/OpenHardwareMonitorService/bin/Release/OpenHardwareMonitorService.exe.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/OpenHardwareMonitorService/bin/Release/OpenHardwareMonitorService.pdb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/OpenHardwareMonitorService/bin/Release/OpenHardwareMonitorService.pdb
--------------------------------------------------------------------------------
/OpenHardwareMonitorService/libs/Newtonsoft.Json.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/OpenHardwareMonitorService/libs/Newtonsoft.Json.dll
--------------------------------------------------------------------------------
/OpenHardwareMonitorService/libs/OpenHardwareMonitorLib.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/OpenHardwareMonitorService/libs/OpenHardwareMonitorLib.dll
--------------------------------------------------------------------------------
/OpenHardwareMonitorService/packages.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # USB Screen
2 | USB屏幕&编辑器
3 |
4 | # 图文教程:
5 |
6 | # [https://zhuanlan.zhihu.com/p/698789562](https://zhuanlan.zhihu.com/p/698789562)
7 |
8 | # 视频教程
9 | # [https://www.bilibili.com/video/BV1eTTwe6EFU/?vd_source=a2700de3db7bd5f0117df32bdd5cef9f](https://www.bilibili.com/video/BV1eTTwe6EFU/?vd_source=a2700de3db7bd5f0117df32bdd5cef9f)
10 |
11 | # 硬件
12 |
13 | ## 支持的屏幕型号
14 |
15 | 目前支持 ST7735 128x160 和 ST7789 320x240两种屏幕
16 |
17 | ### ST7735接线方式
18 | ```
19 | GND <=> GND
20 | VCC <=> 3V3
21 | SCL <=> SCLK(GPIO6)
22 | SDA <=> MOSI(GPIO7)
23 | RES <=> RST(GPIO14)
24 | DC <=> DC(GPIO13)
25 | CS <=> GND
26 | BLK <=> 不连接
27 | ```
28 | 
29 | 
30 |
31 | ### ST7789接线方式
32 | ```
33 | GND <=> GND
34 | VCC <=> 3V3
35 | SCL <=> PIN6(clk)
36 | SDA <=> PIN7(mosi)
37 | RESET <=> PIN14(rst)
38 | AO <=> PIN13
39 | CS <=> PIN9
40 | BL <=> 5V
41 | ```
42 | 
43 |
44 | ### ST7789 240x240 接线方式
45 | ```
46 | GND <=> GND
47 | VCC <=> 3V3
48 | SCL <=> PIN6(clk)
49 | SDA <=> PIN7(mosi)
50 | RESET <=> PIN14(rst)
51 | DC <=> PIN13
52 | CS <=> PIN9
53 | BL <=> 5V
54 | ```
55 |
56 | ## 固件源码
57 | https://github.com/planet0104/rp2040_usb_screen
58 |
59 | ## 接线方式
60 |
61 |
62 | # 编译
63 |
64 | ## 编译aarch64-linux
65 |
66 | 1、设置default features,启用 v4l-webcam
67 |
68 | ```toml
69 | [features]
70 | default = ["v4l-webcam", "usb-serial"]
71 | ```
72 |
73 | 2、启动 DockerDesktop
74 |
75 | 3、进入 wsl2 Ubuntu
76 |
77 | 4、安装 cross
78 |
79 | ```shell
80 | cargo install cross --git https://github.com/cross-rs/cross
81 | ```
82 |
83 | 5、编译
84 |
85 | 注意 Cross.toml 中的配置
86 |
87 | ```shell
88 | # rustup component add rust-src --toolchain nightly
89 | RUSTFLAGS="-Zlocation-detail=none" cross +nightly build -Z build-std=std,panic_abort \
90 | -Z build-std-features=panic_immediate_abort \
91 | -Z build-std-features="optimize_for_size" \
92 | --target aarch64-unknown-linux-gnu --release
93 | ```
94 |
95 | # 运行编辑器
96 |
97 | ## windows中运行
98 |
99 | 设置 deault features
100 |
101 | ```toml
102 | [features]
103 | default = ["editor", "tray", "nokhwa-webcam"]
104 | ```
105 |
106 | ```cmd
107 | ./run.cmd
108 | ```
109 |
110 | ## Ubuntu中运行
111 |
112 | 设置 deault features
113 |
114 | ```toml
115 | [features]
116 | default = ["editor", "v4l-webcam"]
117 | ```
118 |
119 | ```bash
120 | # export https_proxy=http://192.168.1.25:6003;export http_proxy=http://192.168.1.25:6003;export all_proxy=socks5://192.168.1.25:6003
121 | # export https_proxy=;export http_proxy=;export all_proxy=;
122 | sudo apt-get install -y libclang-dev libv4l-dev libudev-dev
123 |
124 | sh run.sh
125 | # sudo ./target/debug/USB-Screen
126 | # sudo ./target/debug/USB-Screen editor
127 |
128 | ## v4l utils
129 | ## sudo apt install v4l-utils
130 | ## v4l2-ctl --list-formats -d /dev/video0
131 | ## v4l2-ctl --list-formats-ext -d /dev/video0
132 | ```
133 |
134 | ## 飞牛私有云 fnOS 编译
135 |
136 | ```bash
137 | # 切换到root模式(登录 planet,root123)
138 | sudo -i
139 | # 首先安装rust
140 | # ...
141 | # 飞牛OS编译前需要升级libc6=2.36-9+deb12u9
142 | sudo apt-get install aptitude
143 | aptitude install libc6=2.36-9+deb12u9
144 | apt install build-essential
145 | #安装依赖库
146 | apt install pkg-config
147 | sudo apt-get install -y libclang-dev libv4l-dev libudev-dev
148 | # 打开x86_64 linux编译特征
149 | # !!注意关闭 editor特征!!
150 | # x86_64 linux
151 | # default = ["v4l-webcam", "usb-serial"]
152 | # 克隆然后编译
153 | rm Cargo.lock
154 | cargo build --release
155 | ```
--------------------------------------------------------------------------------
/build-aarch64-musl.sh.md:
--------------------------------------------------------------------------------
1 | # 必须在linux/wsl2 linux中执行编译
2 | #
3 | # 修改默认features后再编译:
4 | # [features]
5 | # default = ["v4l-webcam", "usb-serial"]
6 | #
7 |
8 | # 启动docker后运行
9 | # 复制出来再运行
10 | bash
11 | rm Cargo.lock
12 | cargo install cross --git https://github.com/cross-rs/cross
13 | cross build --target aarch64-unknown-linux-musl --release #如果编译失败,使用 crates.io 编译!不要用rxproxy
14 |
15 | # openwrp配置花生壳教程
16 | https://service.oray.com/question/20547.html
17 |
18 | # 开机运行执行:
19 | ln -s /etc/init.d/mystart /etc/rc.d/S99mystart
20 | #ln -s /etc/init.d/mystart /etc/rc.d/K15mystart
21 | # 查看启动日志
22 | logread > log.txt
23 |
24 | # openwrt防火墙设置(网络->防火墙)
25 |
26 | https://www.bilibili.com/read/cv12684340/
27 |
28 | # openwrt配置usb设备
29 |
30 | https://openwrt.org/docs/guide-user/storage/usb-installing
31 |
32 | ```shell
33 | opkg update
34 | echo host > /sys/kernel/debug/usb/ci_hdrc.0/role
35 | opkg install kmod-usb-net kmod-usb-net-rndis kmod-usb-net-cdc-ether usbutils
36 | lsusb
37 | #=====================
38 |
39 | #获取已安装的 USB 软件包列表
40 | opkg list-installed *usb*
41 | #安装 USB 核心包(所有 USB 版本),如果前面的 list-output 未列出它
42 | opkg install kmod-usb-core
43 | insmod usbcore
44 | #安装 USB 存储包(所有 USB 版本),如果前面的 list-output 未列出它
45 | opkg install kmod-usb-storage
46 | #要安装 USB 1.1 驱动程序,请先尝试 UHCI 驱动程序
47 | opkg install kmod-usb-uhci
48 | insmod uhci_hcd
49 | #如果此操作失败并显示错误“No such device”,请尝试安装 USB 1.1 的替代 OHCI 驱动程序
50 |
51 | ```
--------------------------------------------------------------------------------
/build-x86_64_linux.sh:
--------------------------------------------------------------------------------
1 | # 必须在linux/wsl2 linux中执行编译
2 | # 二进制大小: 6.38M
3 | #
4 | # 修改默认features后再编译:
5 | # [features]
6 | # default = ["editor", "v4l-webcam", usb-serial]
7 | #
8 | # cargo install cross --git https://github.com/cross-rs/cross
9 | # cross build --target x86_64-unknown-linux-gnu --release
10 | bash
11 | cargo build --target x86_64-unknown-linux-gnu --release
12 |
13 | # https://github.com/johnthagen/min-sized-rust
14 | # rustup component add rust-src --toolchain nightly
15 | # RUSTFLAGS="-Zlocation-detail=none" cross +nightly build -Z build-std=std,panic_abort \
16 | # -Z build-std-features=panic_immediate_abort \
17 | # -Z build-std-features="optimize_for_size" \
18 | # --target x86_64-unknown-linux-gnu --release
--------------------------------------------------------------------------------
/build-x86_64_windows.cmd:
--------------------------------------------------------------------------------
1 | :: 以管理员身份启动控制台编译
2 | cargo zbuild --target x86_64-pc-windows-msvc
--------------------------------------------------------------------------------
/build.rs:
--------------------------------------------------------------------------------
1 | fn main() {
2 | let style = "fluent-dark";
3 | slint_build::compile_with_config(
4 | "view/main.slint",
5 | slint_build::CompilerConfiguration::new().with_style(style.into()),
6 | )
7 | .unwrap();
8 |
9 | #[cfg(windows)]
10 | {
11 | let mut res = winres::WindowsResource::new();
12 | res.set_icon("images/monitor.ico");
13 | res.compile().unwrap();
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/fonts/VonwaonBitmap-16px.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/fonts/VonwaonBitmap-16px.ttf
--------------------------------------------------------------------------------
/images/0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/0.png
--------------------------------------------------------------------------------
/images/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/1.png
--------------------------------------------------------------------------------
/images/10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/10.png
--------------------------------------------------------------------------------
/images/11.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/11.png
--------------------------------------------------------------------------------
/images/12.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/12.png
--------------------------------------------------------------------------------
/images/13.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/13.png
--------------------------------------------------------------------------------
/images/14.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/14.png
--------------------------------------------------------------------------------
/images/15.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/15.png
--------------------------------------------------------------------------------
/images/16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/16.png
--------------------------------------------------------------------------------
/images/17.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/17.png
--------------------------------------------------------------------------------
/images/18.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/18.png
--------------------------------------------------------------------------------
/images/19.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/19.png
--------------------------------------------------------------------------------
/images/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/2.png
--------------------------------------------------------------------------------
/images/20.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/20.png
--------------------------------------------------------------------------------
/images/21.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/21.png
--------------------------------------------------------------------------------
/images/22.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/22.png
--------------------------------------------------------------------------------
/images/23.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/23.png
--------------------------------------------------------------------------------
/images/24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/24.png
--------------------------------------------------------------------------------
/images/25.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/25.png
--------------------------------------------------------------------------------
/images/26.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/26.png
--------------------------------------------------------------------------------
/images/27.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/27.png
--------------------------------------------------------------------------------
/images/28.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/28.png
--------------------------------------------------------------------------------
/images/29.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/29.png
--------------------------------------------------------------------------------
/images/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/3.png
--------------------------------------------------------------------------------
/images/30.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/30.png
--------------------------------------------------------------------------------
/images/31.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/31.png
--------------------------------------------------------------------------------
/images/32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/32.png
--------------------------------------------------------------------------------
/images/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/4.png
--------------------------------------------------------------------------------
/images/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/5.png
--------------------------------------------------------------------------------
/images/6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/6.png
--------------------------------------------------------------------------------
/images/7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/7.png
--------------------------------------------------------------------------------
/images/8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/8.png
--------------------------------------------------------------------------------
/images/9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/9.png
--------------------------------------------------------------------------------
/images/crosschair.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/crosschair.png
--------------------------------------------------------------------------------
/images/icon_clock.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/icon_clock.png
--------------------------------------------------------------------------------
/images/icon_cpu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/icon_cpu.png
--------------------------------------------------------------------------------
/images/icon_date1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/icon_date1.png
--------------------------------------------------------------------------------
/images/icon_date2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/icon_date2.png
--------------------------------------------------------------------------------
/images/icon_download.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/icon_download.png
--------------------------------------------------------------------------------
/images/icon_drive.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/icon_drive.png
--------------------------------------------------------------------------------
/images/icon_fan.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/icon_fan.png
--------------------------------------------------------------------------------
/images/icon_font.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/icon_font.png
--------------------------------------------------------------------------------
/images/icon_host.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/icon_host.png
--------------------------------------------------------------------------------
/images/icon_ip.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/icon_ip.png
--------------------------------------------------------------------------------
/images/icon_lunar1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/icon_lunar1.png
--------------------------------------------------------------------------------
/images/icon_lunar2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/icon_lunar2.png
--------------------------------------------------------------------------------
/images/icon_percent.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/icon_percent.png
--------------------------------------------------------------------------------
/images/icon_photo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/icon_photo.png
--------------------------------------------------------------------------------
/images/icon_photo_blue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/icon_photo_blue.png
--------------------------------------------------------------------------------
/images/icon_pointer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/icon_pointer.png
--------------------------------------------------------------------------------
/images/icon_process.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/icon_process.png
--------------------------------------------------------------------------------
/images/icon_ram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/icon_ram.png
--------------------------------------------------------------------------------
/images/icon_rotate.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/icon_rotate.png
--------------------------------------------------------------------------------
/images/icon_swap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/icon_swap.png
--------------------------------------------------------------------------------
/images/icon_system.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/icon_system.png
--------------------------------------------------------------------------------
/images/icon_temperature.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/icon_temperature.png
--------------------------------------------------------------------------------
/images/icon_text.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/icon_text.png
--------------------------------------------------------------------------------
/images/icon_text_blue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/icon_text_blue.png
--------------------------------------------------------------------------------
/images/icon_time.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/icon_time.png
--------------------------------------------------------------------------------
/images/icon_upload.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/icon_upload.png
--------------------------------------------------------------------------------
/images/icon_version1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/icon_version1.png
--------------------------------------------------------------------------------
/images/icon_version2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/icon_version2.png
--------------------------------------------------------------------------------
/images/icon_weather.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/icon_weather.png
--------------------------------------------------------------------------------
/images/icon_webcam.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/icon_webcam.png
--------------------------------------------------------------------------------
/images/monitor.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/monitor.ico
--------------------------------------------------------------------------------
/images/monitor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/monitor.png
--------------------------------------------------------------------------------
/images/picker.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/picker.png
--------------------------------------------------------------------------------
/images/rp2040.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/rp2040.png
--------------------------------------------------------------------------------
/images/st7735.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/st7735.png
--------------------------------------------------------------------------------
/images/st7789.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/planet0104/USB-Screen/c9e6039c1c552a1508ae65eb54365293cdf41654/images/st7789.png
--------------------------------------------------------------------------------
/run.cmd:
--------------------------------------------------------------------------------
1 | cargo run editor
--------------------------------------------------------------------------------
/run.sh:
--------------------------------------------------------------------------------
1 | cargo build
2 | sudo ./target/debug/USB-Screen editor
--------------------------------------------------------------------------------
/src/main.rs:
--------------------------------------------------------------------------------
1 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
2 |
3 | use std::{path::Path, process::Command, time::{Duration, Instant}};
4 |
5 | use anyhow::{anyhow, Result};
6 | use image::{buffer::ConvertBuffer, RgbImage};
7 | use log::{error, info};
8 | #[cfg(feature = "tray")]
9 | use tao::event_loop::ControlFlow;
10 |
11 | use usb_screen::find_and_open_a_screen;
12 |
13 | use crate::screen::ScreenRender;
14 | #[cfg(feature = "editor")]
15 | mod editor;
16 | mod monitor;
17 | mod nmc;
18 | mod rgb565;
19 | mod screen;
20 | mod usb_screen;
21 | mod wifi_screen;
22 | mod utils;
23 | mod widgets;
24 | #[cfg(all(not(windows),feature = "v4l-webcam"))]
25 | mod yuv422;
26 |
27 | fn main() -> Result<()> {
28 | // env_logger::init();
29 | let _ = env_logger::builder()
30 | .filter_level(log::LevelFilter::Info)
31 | .try_init();
32 |
33 | #[cfg(windows)]
34 | {
35 | #[cfg(not(debug_assertions))]
36 | {
37 | let exe_path = std::env::current_exe()?;
38 | std::env::set_current_dir(exe_path.parent().unwrap())?;
39 | }
40 | }
41 |
42 | let args: Vec = std::env::args().skip(1).collect();
43 |
44 | let screen_file = match args.len() {
45 | 0 => read_screen_file(),
46 | 1 => Some(args[0].to_string()),
47 | _ => None,
48 | };
49 |
50 | info!("screen_file={:?}", screen_file);
51 |
52 | if let Some(file) = screen_file {
53 | #[cfg(feature = "editor")]
54 | if file != "editor"{
55 | create_tray_icon(file)?;
56 | return Ok(());
57 | }
58 |
59 | #[cfg(not(feature = "editor"))]
60 | create_tray_icon(file)?;
61 | }
62 |
63 | #[cfg(feature = "editor")]
64 | {
65 | info!("editor start!");
66 | editor::run()?;
67 | monitor::clean();
68 | }
69 | Ok(())
70 | }
71 |
72 | fn open_usb_screen(file: String) -> Result<()>{
73 | info!("打开屏幕文件:{file}");
74 | let f = std::fs::read(file)?;
75 | let mut render = ScreenRender::new_from_file(&f)?;
76 |
77 | render.setup_monitor()?;
78 |
79 | let mut usb_screen = None;
80 |
81 | if let Some(ip) = render.device_ip.as_ref(){
82 | info!("设置了ip地址,使用wifi屏幕..");
83 | }else {
84 | info!("未设置ip地址,使用 USB屏幕...");
85 | usb_screen = usb_screen::find_and_open_a_screen();
86 | }
87 |
88 | info!("USB Screen是否已打开: {}", usb_screen.is_some());
89 | let mut last_draw_time = Instant::now();
90 | let frame_duration = (1000./render.fps) as u128;
91 | info!("帧时间:{}ms", frame_duration);
92 | //设置系统信息更新延迟
93 | let _ = monitor::set_update_delay(frame_duration);
94 | loop {
95 | if last_draw_time.elapsed().as_millis() < frame_duration{
96 | std::thread::sleep(Duration::from_millis(5));
97 | continue;
98 | }
99 | last_draw_time = Instant::now();
100 | render.render();
101 | let frame: RgbImage = render.canvas.image_data().convert();
102 | //旋转
103 | let frame = if render.rotate_degree == 90 {
104 | image::imageops::rotate90(&frame)
105 | }else if render.rotate_degree == 180{
106 | image::imageops::rotate180(&frame)
107 | }else if render.rotate_degree == 270{
108 | image::imageops::rotate270(&frame)
109 | }else{
110 | frame
111 | };
112 | // let rgb565 = rgb888_to_rgb565_u16(&frame, frame.width() as usize, frame.height() as usize);
113 | if let Some(ip) = render.device_ip.as_ref(){
114 | //连接wifi屏幕
115 | if let Ok(wifi_scr_status) = wifi_screen::get_status(){
116 | match wifi_scr_status.status{
117 | wifi_screen::Status::NotConnected | wifi_screen::Status::ConnectFail
118 | | wifi_screen::Status::Disconnected => {
119 | std::thread::sleep(Duration::from_secs(2));
120 | let _ = wifi_screen::send_message(wifi_screen::Message::Connect(ip.to_string()));
121 | }
122 | wifi_screen::Status::Connected => {
123 | let _ = wifi_screen::send_message(wifi_screen::Message::Image(frame.convert()));
124 | }
125 | wifi_screen::Status::Connecting => {
126 |
127 | }
128 | }
129 | }
130 | }else{
131 | if usb_screen.is_none() {
132 | std::thread::sleep(Duration::from_millis(2000));
133 | info!("open USB Screen...");
134 | usb_screen = find_and_open_a_screen();
135 | } else {
136 | let screen = usb_screen.as_mut().unwrap();
137 | if let Err(err) = screen.draw_rgb_image(
138 | 0,
139 | 0,
140 | &frame
141 | )
142 | {
143 | error!("屏幕绘制失败:{err:?}");
144 | usb_screen = None;
145 | }
146 | }
147 | }
148 | }
149 | }
150 |
151 | fn create_tray_icon(file: String) -> Result<()> {
152 |
153 | #[cfg(not(feature = "editor"))]
154 | {
155 | let ret = open_usb_screen(file);
156 | error!("{:?}", ret);
157 | return Ok(());
158 | }
159 |
160 | #[cfg(feature = "tray")]
161 | {
162 | std::thread::spawn(move ||{
163 | let ret = open_usb_screen(file);
164 | error!("{:?}", ret);
165 | });
166 |
167 | // 图标必须运行在UI线程上
168 | let event_loop = tao::event_loop::EventLoopBuilder::new().build();
169 |
170 | let tray_menu = Box::new(tray_icon::menu::Menu::new());
171 | let quit_i = tray_icon::menu::MenuItem::new("退出", true, None);
172 | let editor_i = tray_icon::menu::MenuItem::new("编辑器", true, None);
173 | let _ = tray_menu.append(&quit_i);
174 | let _ = tray_menu.append(&editor_i);
175 | let mut tray_icon = None;
176 | let mut menu_channel = None;
177 |
178 | event_loop.run(move |event, _, control_flow| {
179 | // We add delay of 16 ms (60fps) to event_loop to reduce cpu load.
180 | // This can be removed to allow ControlFlow::Poll to poll on each cpu cycle
181 | // Alternatively, you can set ControlFlow::Wait or use TrayIconEvent::set_event_handler,
182 | // see https://github.com/tauri-apps/tray-icon/issues/83#issuecomment-1697773065
183 | *control_flow = ControlFlow::WaitUntil(
184 | std::time::Instant::now() + std::time::Duration::from_millis(16),
185 | );
186 |
187 | if let tao::event::Event::NewEvents(tao::event::StartCause::Init) = event {
188 | //创建图标
189 | let icon = image::load_from_memory(include_bytes!("../images/monitor.png")).unwrap().to_rgba8();
190 | let (width, height) = icon.dimensions();
191 |
192 |
193 | if let Ok(icon) = tray_icon::Icon::from_rgba(icon.into_raw(), width, height){
194 | if let Ok(i) = tray_icon::TrayIconBuilder::new()
195 | .with_tooltip("USB Screen")
196 | .with_menu(tray_menu.clone())
197 | .with_icon(icon)
198 | .build(){
199 | tray_icon = Some(i);
200 | menu_channel = Some(tray_icon::menu::MenuEvent::receiver());
201 | }
202 | }
203 |
204 | // We have to request a redraw here to have the icon actually show up.
205 | // Tao only exposes a redraw method on the Window so we use core-foundation directly.
206 | #[cfg(target_os = "macos")]
207 | unsafe {
208 | use core_foundation::runloop::{CFRunLoopGetMain, CFRunLoopWakeUp};
209 |
210 | let rl = CFRunLoopGetMain();
211 | CFRunLoopWakeUp(rl);
212 | }
213 | }
214 |
215 | if let (Some(_tray_icon), Some(menu_channel)) = (tray_icon.as_mut(), menu_channel.as_mut()){
216 | if let Ok(event) = menu_channel.try_recv() {
217 | if event.id == quit_i.id() {
218 | *control_flow = ControlFlow::Exit;
219 | }else if event.id == editor_i.id() {
220 | //启动自身
221 | if let Ok(_) = run_as_editor(){
222 | //退出托盘
223 | *control_flow = ControlFlow::Exit;
224 | }
225 | }
226 | }
227 | }
228 | });
229 | }
230 | Ok(())
231 | }
232 |
233 | fn read_screen_file() -> Option {
234 | // #[cfg(debug_assertions)]
235 | // {
236 | // return None;
237 | // }
238 | //在当前目录下查找.screen文件
239 | let path = Path::new("./"); // 这里以当前目录为例,你可以替换为任何你想要列出的目录路径
240 | // 使用read_dir函数读取目录条目
241 | if let Ok(entries) = std::fs::read_dir(path) {
242 | for entry in entries {
243 | if let Ok(entry) = entry {
244 | let path = entry.path();
245 | if path.is_file() {
246 | if let Some(extension) = path.extension() {
247 | if extension == "screen" {
248 | if let Some(str) = path.to_str() {
249 | return Some(str.to_string());
250 | }
251 | }
252 | }
253 | }
254 | }
255 | }
256 | }
257 | None
258 | }
259 |
260 | #[cfg(windows)]
261 | pub fn is_run_as_admin() -> Result {
262 | use std::mem::MaybeUninit;
263 | use windows::Win32::{
264 | Foundation::{CloseHandle, HANDLE},
265 | Security::{GetTokenInformation, TokenElevation, TOKEN_ELEVATION, TOKEN_QUERY},
266 | System::Threading::{GetCurrentProcess, OpenProcessToken},
267 | };
268 | unsafe {
269 | let mut token_handle: HANDLE = HANDLE(std::ptr::null_mut());
270 | let process_handle = GetCurrentProcess();
271 |
272 | // 打开进程令牌
273 | OpenProcessToken(process_handle, TOKEN_QUERY, &mut token_handle)?;
274 | if token_handle.is_invalid() {
275 | return Ok(false);
276 | }
277 |
278 | // 获取令牌信息
279 | let mut elevation_buffer_size: u32 = 0;
280 | let mut elevation_info: MaybeUninit = MaybeUninit::uninit();
281 | let elevation_info_ptr = elevation_info.as_mut_ptr() as *mut _;
282 | let expect_size = std::mem::size_of::() as u32;
283 | GetTokenInformation(
284 | token_handle,
285 | TokenElevation,
286 | Some(elevation_info_ptr),
287 | expect_size,
288 | &mut elevation_buffer_size,
289 | )?;
290 | // 检查 TokenIsElevated 标志
291 | let elevation = elevation_info.assume_init();
292 | let is_elevated = elevation.TokenIsElevated != 0;
293 | // 关闭令牌句柄
294 | CloseHandle(token_handle)?;
295 | return Ok(is_elevated);
296 | }
297 | }
298 |
299 | #[cfg(windows)]
300 | pub fn run_as_admin(params: Option<&str>) -> Result<()> {
301 | use anyhow::anyhow;
302 | use windows::{
303 | core::{s, PCSTR},
304 | Win32::{
305 | Foundation::{HANDLE, HINSTANCE, HWND},
306 | System::Registry::HKEY,
307 | UI::Shell::{
308 | ShellExecuteExA, SEE_MASK_DOENVSUBST, SEE_MASK_FLAG_NO_UI, SEE_MASK_NOCLOSEPROCESS,
309 | SHELLEXECUTEINFOA, SHELLEXECUTEINFOA_0,
310 | },
311 | },
312 | };
313 |
314 | let exe_path = std::env::current_exe()?;
315 | let exe_path = exe_path.to_str();
316 | if exe_path.is_none() {
317 | return Err(anyhow!("exe path error!"));
318 | }
319 | let mut exe_path = exe_path.unwrap().to_string();
320 | exe_path.push('\0');
321 |
322 | let params_ptr = if let Some(s) = params {
323 | let mut s = s.to_string();
324 | s.push('\n');
325 | PCSTR::from_raw(s.as_ptr())
326 | } else {
327 | PCSTR::from_raw(std::ptr::null())
328 | };
329 |
330 | info!("Executable path: {exe_path}");
331 | unsafe {
332 | let mut sh_exec_info = SHELLEXECUTEINFOA {
333 | cbSize: std::mem::size_of::() as u32,
334 | fMask: SEE_MASK_NOCLOSEPROCESS | SEE_MASK_DOENVSUBST | SEE_MASK_FLAG_NO_UI,
335 | hwnd: HWND(std::ptr::null_mut()),
336 | lpVerb: s!("runas"),
337 | lpFile: PCSTR::from_raw(exe_path.as_ptr()),
338 | lpParameters: params_ptr,
339 | lpDirectory: PCSTR::null(),
340 | nShow: 0,
341 | hInstApp: HINSTANCE(std::ptr::null_mut()),
342 | lpIDList: std::ptr::null_mut(),
343 | lpClass: PCSTR::null(),
344 | hkeyClass: HKEY(std::ptr::null_mut()),
345 | dwHotKey: 0,
346 | hProcess: HANDLE(std::ptr::null_mut()),
347 | Anonymous: SHELLEXECUTEINFOA_0::default(),
348 | };
349 |
350 | ShellExecuteExA(&mut sh_exec_info)?;
351 | }
352 | Ok(())
353 | }
354 |
355 |
356 | pub fn run_as_editor() -> Result<()> {
357 | let exe_path = std::env::current_exe()?;
358 | let exe_path = exe_path.to_str();
359 | if exe_path.is_none() {
360 | return Err(anyhow!("exe path error!"));
361 | }
362 | let mut command = Command::new(exe_path.unwrap());
363 | command.arg("editor");
364 | command.spawn()?;
365 | Ok(())
366 | }
367 |
--------------------------------------------------------------------------------
/src/nmc.rs:
--------------------------------------------------------------------------------
1 | use std::collections::HashMap;
2 |
3 | use anyhow::Result;
4 | use image::RgbaImage;
5 | use log::info;
6 | use once_cell::sync::Lazy;
7 | use reqwest::header::{HeaderMap, HeaderValue, USER_AGENT};
8 | use serde::{Deserialize, Serialize};
9 |
10 | pub const CITIES: Lazy> =
11 | Lazy::new(|| serde_json::from_str(include_str!("../cities.json")).unwrap());
12 |
13 | pub const ICONS: Lazy> = Lazy::new(|| {
14 | vec![
15 | image::load_from_memory(include_bytes!("../images/0.png"))
16 | .unwrap()
17 | .to_rgba8(),
18 | image::load_from_memory(include_bytes!("../images/1.png"))
19 | .unwrap()
20 | .to_rgba8(),
21 | image::load_from_memory(include_bytes!("../images/2.png"))
22 | .unwrap()
23 | .to_rgba8(),
24 | image::load_from_memory(include_bytes!("../images/3.png"))
25 | .unwrap()
26 | .to_rgba8(),
27 | image::load_from_memory(include_bytes!("../images/4.png"))
28 | .unwrap()
29 | .to_rgba8(),
30 | image::load_from_memory(include_bytes!("../images/5.png"))
31 | .unwrap()
32 | .to_rgba8(),
33 | image::load_from_memory(include_bytes!("../images/6.png"))
34 | .unwrap()
35 | .to_rgba8(),
36 | image::load_from_memory(include_bytes!("../images/7.png"))
37 | .unwrap()
38 | .to_rgba8(),
39 | image::load_from_memory(include_bytes!("../images/8.png"))
40 | .unwrap()
41 | .to_rgba8(),
42 | image::load_from_memory(include_bytes!("../images/9.png"))
43 | .unwrap()
44 | .to_rgba8(),
45 | image::load_from_memory(include_bytes!("../images/10.png"))
46 | .unwrap()
47 | .to_rgba8(),
48 | image::load_from_memory(include_bytes!("../images/11.png"))
49 | .unwrap()
50 | .to_rgba8(),
51 | image::load_from_memory(include_bytes!("../images/12.png"))
52 | .unwrap()
53 | .to_rgba8(),
54 | image::load_from_memory(include_bytes!("../images/13.png"))
55 | .unwrap()
56 | .to_rgba8(),
57 | image::load_from_memory(include_bytes!("../images/14.png"))
58 | .unwrap()
59 | .to_rgba8(),
60 | image::load_from_memory(include_bytes!("../images/15.png"))
61 | .unwrap()
62 | .to_rgba8(),
63 | image::load_from_memory(include_bytes!("../images/16.png"))
64 | .unwrap()
65 | .to_rgba8(),
66 | image::load_from_memory(include_bytes!("../images/17.png"))
67 | .unwrap()
68 | .to_rgba8(),
69 | image::load_from_memory(include_bytes!("../images/18.png"))
70 | .unwrap()
71 | .to_rgba8(),
72 | image::load_from_memory(include_bytes!("../images/19.png"))
73 | .unwrap()
74 | .to_rgba8(),
75 | image::load_from_memory(include_bytes!("../images/20.png"))
76 | .unwrap()
77 | .to_rgba8(),
78 | image::load_from_memory(include_bytes!("../images/21.png"))
79 | .unwrap()
80 | .to_rgba8(),
81 | image::load_from_memory(include_bytes!("../images/22.png"))
82 | .unwrap()
83 | .to_rgba8(),
84 | image::load_from_memory(include_bytes!("../images/23.png"))
85 | .unwrap()
86 | .to_rgba8(),
87 | image::load_from_memory(include_bytes!("../images/24.png"))
88 | .unwrap()
89 | .to_rgba8(),
90 | image::load_from_memory(include_bytes!("../images/25.png"))
91 | .unwrap()
92 | .to_rgba8(),
93 | image::load_from_memory(include_bytes!("../images/26.png"))
94 | .unwrap()
95 | .to_rgba8(),
96 | image::load_from_memory(include_bytes!("../images/27.png"))
97 | .unwrap()
98 | .to_rgba8(),
99 | image::load_from_memory(include_bytes!("../images/28.png"))
100 | .unwrap()
101 | .to_rgba8(),
102 | image::load_from_memory(include_bytes!("../images/29.png"))
103 | .unwrap()
104 | .to_rgba8(),
105 | image::load_from_memory(include_bytes!("../images/30.png"))
106 | .unwrap()
107 | .to_rgba8(),
108 | image::load_from_memory(include_bytes!("../images/31.png"))
109 | .unwrap()
110 | .to_rgba8(),
111 | image::load_from_memory(include_bytes!("../images/32.png"))
112 | .unwrap()
113 | .to_rgba8(),
114 | ]
115 | });
116 |
117 | #[derive(Debug, Serialize, Deserialize)]
118 | pub struct Province {
119 | code: String,
120 | name: String,
121 | url: String,
122 | }
123 |
124 | #[derive(Debug, Clone, Serialize, Deserialize)]
125 | pub struct City {
126 | pub code: String,
127 | pub province: String,
128 | pub city: String,
129 | pub url: String,
130 | }
131 |
132 | #[derive(Debug, Serialize, Deserialize)]
133 | pub struct WeatherResp {
134 | msg: String,
135 | code: i32,
136 | data: WeatherData,
137 | }
138 |
139 | #[derive(Debug, Serialize, Deserialize)]
140 | pub struct WeatherData {
141 | real: RealWeather,
142 | }
143 |
144 | #[derive(Debug, Clone, Serialize, Deserialize)]
145 | pub struct RealWeather {
146 | pub station: City,
147 | pub publish_time: String,
148 | pub weather: Weather,
149 | pub wind: Wind,
150 | }
151 |
152 | #[derive(Debug, Clone, Serialize, Deserialize)]
153 | pub struct Weather {
154 | pub temperature: f32,
155 | #[serde(rename = "temperatureDiff")]
156 | pub temperature_diff: f32,
157 | pub airpressure: f32,
158 | pub humidity: f32,
159 | pub rain: f32,
160 | pub rcomfort: f32,
161 | pub icomfort: f32,
162 | pub info: String,
163 | pub img: String,
164 | pub feelst: f32,
165 | }
166 |
167 | #[derive(Debug, Clone, Serialize, Deserialize)]
168 | pub struct Wind {
169 | pub direct: String,
170 | pub degree: f32,
171 | pub power: String,
172 | pub speed: f32,
173 | }
174 |
175 | #[allow(unused)]
176 | pub fn query_province() -> Result> {
177 | let json = reqwest::blocking::get("http://www.nmc.cn/rest/province")?.text()?;
178 | info!("获取省份:");
179 | info!("{json}");
180 | Ok(serde_json::from_str(&json)?)
181 | }
182 |
183 | #[allow(unused)]
184 | pub fn query_city() -> Result> {
185 | let mut cities = vec![];
186 | for p in query_province()? {
187 | let json = reqwest::blocking::get(format!("http://www.nmc.cn/rest/province/{}", p.code))?
188 | .text()?;
189 | info!("获取城市({}):{json}", p.name);
190 | for city in serde_json::from_str::>(&json)? {
191 | cities.push(city);
192 | }
193 | }
194 | Ok(cities)
195 | }
196 |
197 | pub fn query_weather(station_id: &str) -> Result {
198 | let client = reqwest::blocking::Client::new();
199 | let res = client.get(format!("http://www.nmc.cn/rest/weather?stationid={station_id}"))
200 | .header(USER_AGENT, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0")
201 | .header(reqwest::header::HOST, "www.nmc.cn")
202 | .send()?;
203 |
204 | let json = res.text()?;
205 |
206 | // info!("天气:{json}");
207 | let resp = serde_json::from_str::(&json)?;
208 | Ok(resp.data.real)
209 | }
210 |
211 | #[test]
212 | fn download_city() -> Result<()> {
213 | use std::io::Write;
214 | env_logger::builder()
215 | .filter_level(log::LevelFilter::Info)
216 | .try_init()?;
217 | let cities = query_city()?;
218 | let json = serde_json::to_string(&cities)?;
219 | let mut file = std::fs::File::create("cities.json")?;
220 | file.write_all(json.as_bytes())?;
221 | Ok(())
222 | }
223 |
224 | #[test]
225 | fn test_weather() -> Result<()> {
226 | env_logger::builder()
227 | .filter_level(log::LevelFilter::Info)
228 | .try_init()?;
229 | info!("城市数量:{}", CITIES.len());
230 | for city in CITIES.iter() {
231 | if city.city.contains("松江") {
232 | let weather = query_weather(&city.code)?;
233 |
234 | let weather_info = weather.weather.info;
235 | let temperature = weather.weather.temperature;
236 | let winddirection = weather.wind.direct;
237 | let windpower = weather.wind.speed;
238 |
239 | let info = format!("{weather_info} {temperature}℃ {winddirection}{windpower}级");
240 |
241 | info!("{info}");
242 | break;
243 | }
244 | }
245 | Ok(())
246 | }
247 |
--------------------------------------------------------------------------------
/src/rgb565.rs:
--------------------------------------------------------------------------------
1 | #[inline]
2 | pub fn rgb_to_rgb565(r: u8, g: u8, b: u8) -> u16 {
3 | ((r as u16 & 0b11111000) << 8) | ((g as u16 & 0b11111100) << 3) | (b as u16 >> 3)
4 | }
5 |
6 | pub fn rgb888_to_rgb565_be(img: &[u8], width: usize, height: usize) -> Vec{
7 | let mut rgb565 = Vec::with_capacity(width * height * 2);
8 | for p in img.chunks(3){
9 | let rgb565_pixel = rgb_to_rgb565(p[0], p[1], p[2]);
10 | rgb565.extend_from_slice(&rgb565_pixel.to_be_bytes());
11 | }
12 | rgb565
13 | }
--------------------------------------------------------------------------------
/src/screen.rs:
--------------------------------------------------------------------------------
1 | use std::{collections::HashMap, path::PathBuf};
2 |
3 | use crate::{
4 | monitor::{self, WebcamInfo},
5 | nmc::CITIES,
6 | widgets::{ImageWidget, SaveableWidget, TextWidget, Widget},
7 | };
8 | use anyhow::{anyhow, Result};
9 | use async_std::fs;
10 | use log::info;
11 | use lz4_flex::{compress_prepend_size, decompress_size_prepended};
12 | use offscreen_canvas::{Font, FontSettings, OffscreenCanvas, BLACK};
13 | use serde::{Deserialize, Serialize};
14 |
15 | pub static DEFAULT_FONT: &[u8] = include_bytes!("../fonts/VonwaonBitmap-16px.ttf");
16 |
17 | #[derive(Clone, Debug)]
18 | pub struct ScreenSize {
19 | pub name: String,
20 | pub width: u32,
21 | pub height: u32,
22 | }
23 |
24 | #[derive(Clone, Deserialize, Serialize)]
25 | pub struct SaveableScreen {
26 | pub width: u32,
27 | pub height: u32,
28 | pub model: String,
29 | //最大刷新帧率
30 | pub fps: f32,
31 | //指定链接设备编号
32 | pub device_address: Option,
33 | pub widgets: Vec,
34 | pub font: Option>,
35 | pub font_name: String,
36 | pub rotate_degree: Option,
37 | //指定设备IP地址
38 | pub device_ip: Option,
39 | }
40 |
41 | #[derive(Clone, Deserialize, Serialize)]
42 | pub struct SaveableScreenV10 {
43 | pub width: u32,
44 | pub height: u32,
45 | pub model: String,
46 | pub widgets: Vec,
47 | pub font: Option>,
48 | pub font_name: String,
49 | }
50 |
51 | pub struct ScreenRender {
52 | pub width: u32,
53 | pub height: u32,
54 | pub model: String,
55 | pub widgets: Vec>,
56 | pub canvas: OffscreenCanvas,
57 | pub font_name: String,
58 | pub font: Option>,
59 | pub fps: f32,
60 | pub rotate_degree: i32,
61 | pub device_address: Option,
62 | pub device_ip: Option,
63 | }
64 |
65 | impl ScreenRender {
66 | pub fn new(
67 | model: String,
68 | width: u32,
69 | height: u32,
70 | font_file: Option<&[u8]>,
71 | font_name: String,
72 | ) -> Result {
73 | let font_file_clone = font_file.clone();
74 | let font_file = font_file.unwrap_or(DEFAULT_FONT);
75 | let font =
76 | Font::from_bytes(font_file, FontSettings::default()).map_err(|err| anyhow!("{err}"))?;
77 | Ok(Self {
78 | rotate_degree: 0,
79 | canvas: OffscreenCanvas::new(width, height, font),
80 | width,
81 | height,
82 | model,
83 | font_name,
84 | font: font_file_clone.map(|v| v.to_vec()),
85 | widgets: vec![],
86 | fps: 10.,
87 | device_address: None,
88 | device_ip: None,
89 | })
90 | }
91 |
92 | pub fn is_vertical(&self) -> bool{
93 | self.rotate_degree == 90 || self.rotate_degree == 270
94 | }
95 |
96 | pub fn is_horizontal(&self) -> bool{
97 | self.rotate_degree == 0 || self.rotate_degree == 180
98 | }
99 |
100 | pub fn set_font(&mut self, font_file: Option<&[u8]>, font_name: String) -> Result<()> {
101 | let font_file_clone = font_file.clone();
102 | let font_file = font_file.unwrap_or(DEFAULT_FONT);
103 | let font =
104 | Font::from_bytes(font_file, FontSettings::default()).map_err(|err| anyhow!("{err}"))?;
105 | self.canvas = OffscreenCanvas::new(self.width, self.height, font);
106 | self.font = font_file_clone.map(|v| v.to_vec());
107 | self.font_name = font_name;
108 | Ok(())
109 | }
110 |
111 | pub fn setup_monitor(&mut self) -> Result<()> {
112 | //在点击的地方添加一个对象
113 | for widget in &mut self.widgets {
114 | info!("setup_monitor:{}", widget.type_name());
115 | match widget.type_name() {
116 | "memory" | "memory_total" | "memory_percent" | "swap" | "swap_percent" => {
117 | monitor::watch_memory(true)?
118 | }
119 | "webcam" =>{
120 | if let Some(widget) = widget.as_any_mut().downcast_mut::() {
121 | info!("webcam: tag1={:?}", widget.tag1);
122 | monitor::watch_webcam(Some(WebcamInfo{
123 | width: self.width,
124 | height: self.height,
125 | index: widget.tag1.as_ref().unwrap_or(&String::new()).parse().unwrap_or(0),
126 | fps: self.fps as u32
127 | }))?
128 | }
129 | }
130 | "cpu" | "cpu_usage" => monitor::watch_cpu(true)?,
131 | "cpu_freq" => monitor::watch_cpu_clock_speed(true)?,
132 | "cpu_temp." => monitor::watch_cpu_temperatures(true)?,
133 | "cpu_cores_power" | "gpu_cores_power" => monitor::watch_cpu_power(true)?,
134 | "cpu_package_power" | "gpu_package_power" => monitor::watch_cpu_power(true)?,
135 | "cpu_fan" => monitor::watch_cpu_fan(true)?,
136 | "gpu_fan" => monitor::watch_gpu_fan(true)?,
137 | "gpu_clock" => monitor::watch_gpu_clock_speed(true)?,
138 | "gpu_load" | "gpu_memory_load" | "gpu_memory_total_mb" | "gpu_memory_total_gb" => monitor::watch_gpu_load(true)?,
139 | "gpu_temp." => monitor::watch_gpu_temperatures(true)?,
140 | "num_process" => monitor::watch_process(true)?,
141 | "disk_usage" => monitor::watch_disk(true)?,
142 | "net_ip" | "net_ip_info" => monitor::watch_net_ip(true)?,
143 | "disk_read_speed" => monitor::watch_disk_speed(true)?,
144 | "disk_write_speed" => monitor::watch_disk_speed(true)?,
145 | "received_speed" => monitor::watch_network_speed(true)?,
146 | "transmitted_speed" => monitor::watch_network_speed(true)?,
147 | "weather" => {
148 | if let Some(widget) = widget.as_any_mut().downcast_mut::() {
149 | if widget.tag2.len() > 0 {
150 | //查询对应的城市
151 | info!("更新天气,查询对应的城市: tag2={}", widget.tag2);
152 | if let Some(city) = CITIES.iter().find(|c| c.city == widget.tag2) {
153 | monitor::watch_weather(Some(city.clone()))?
154 | }
155 | }
156 | }
157 | }
158 | _ => (),
159 | }
160 | }
161 | Ok(())
162 | }
163 |
164 | pub fn render(&mut self) {
165 | //更新索引
166 | let mut map = HashMap::new();
167 | for w in self.widgets.iter_mut() {
168 | if !map.contains_key(w.type_name()) {
169 | map.insert(w.type_name().to_string(), 0);
170 | } else {
171 | *map.get_mut(w.type_name()).unwrap() += 1;
172 | }
173 | w.set_index(*map.get_mut(w.type_name()).unwrap());
174 | }
175 | for w in self.widgets.iter_mut() {
176 | w.set_num_widget(*map.get_mut(w.type_name()).unwrap());
177 | }
178 | self.canvas.clear(BLACK);
179 | for widget in &mut self.widgets {
180 | widget.draw(&mut self.canvas);
181 | }
182 | }
183 |
184 | pub fn add_widget(
185 | &mut self,
186 | type_name: &str,
187 | type_label: &str,
188 | x: i32,
189 | y: i32,
190 | ) -> Option {
191 | if type_name.len() == 0 {
192 | return None;
193 | }
194 |
195 | let widget: Box = if type_name == "images" || type_name == "webcam" {
196 | Box::new(ImageWidget::new(x, y, &type_name))
197 | } else {
198 | let mut text_index = 1;
199 | for w in self.widgets.iter_mut() {
200 | if let Some(_) = w.as_any_mut().downcast_mut::() {
201 | text_index += 1;
202 | }
203 | }
204 | Box::new(TextWidget::new_with_text(
205 | x,
206 | y,
207 | &type_name,
208 | &type_label,
209 | &format!("文本{text_index}"),
210 | ))
211 | };
212 | let id = widget.id().to_string();
213 | self.widgets.push(widget);
214 | Some(id)
215 | }
216 |
217 | pub fn find_widget(&mut self, uuid: &str) -> Option<(usize, &mut Box)> {
218 | self.widgets
219 | .iter_mut()
220 | .enumerate()
221 | .find(|(_idx, w)| w.id() == uuid)
222 | }
223 |
224 | #[allow(unused)]
225 | pub fn find_widget_by_index(&mut self, index: usize) -> Option<(usize, &mut Box)> {
226 | self.widgets
227 | .iter_mut()
228 | .enumerate()
229 | .find(|(idx, w)| *idx == index)
230 | }
231 |
232 | pub fn width(&self) -> u32 {
233 | self.canvas.width()
234 | }
235 |
236 | pub fn height(&self) -> u32 {
237 | self.canvas.height()
238 | }
239 |
240 | pub async fn decompress_screen_file(file: PathBuf) -> Result>{
241 | let compressed = fs::read(file).await?;
242 | Ok(decompress_size_prepended(&compressed)?)
243 | }
244 |
245 | //尝试使用bindcode解析老版本screen文件
246 | pub fn load_from_file(&mut self, uncompressed: Vec) -> Result<()> {
247 | self.load_from_file_v2(&uncompressed)
248 | }
249 |
250 | //使用json解析screen文件
251 | pub fn load_from_file_v2(&mut self, uncompressed: &[u8]) -> Result<()> {
252 | let saveable:SaveableScreen = serde_json::from_str(&String::from_utf8(uncompressed.to_vec())?)?;
253 | // let saveable: Result<(SaveableScreen, usize), bincode::error::DecodeError> =
254 | // bincode::decode_from_slice(&uncompressed, bincode::config::standard());
255 | // let (saveable, _) = saveable?;
256 | self.width = saveable.width;
257 | self.height = saveable.height;
258 | self.fps = saveable.fps;
259 | self.rotate_degree = saveable.rotate_degree.unwrap_or(0);
260 | self.device_address = saveable.device_address;
261 | self.device_ip = saveable.device_ip;
262 | self.canvas =
263 | OffscreenCanvas::new(saveable.width, saveable.height, self.canvas.font().clone());
264 | if let Some(font) = saveable.font {
265 | self.set_font(Some(&font), saveable.font_name)?;
266 | }
267 | self.widgets.clear();
268 | for w in saveable.widgets {
269 | match w {
270 | SaveableWidget::TextWidget(txt) => {
271 | self.widgets.push(Box::new(txt));
272 | }
273 | SaveableWidget::ImageWidget(img) => {
274 | self.widgets.push(Box::new(img));
275 | }
276 | }
277 | }
278 | Ok(())
279 | }
280 |
281 | pub fn new_from_file(file: &[u8]) -> Result {
282 | let uncompressed = decompress_size_prepended(&file)?;
283 | return Self::new_from_file_v2(&uncompressed);
284 | }
285 |
286 | pub fn new_from_file_v2(uncompressed: &[u8]) -> Result {
287 | let saveable:SaveableScreen = serde_json::from_str(&String::from_utf8(uncompressed.to_vec())?)?;
288 |
289 | let model = saveable.model;
290 | let mut render =
291 | ScreenRender::new(model, saveable.width, saveable.height, None, String::new())?;
292 | if let Some(font) = saveable.font {
293 | render.set_font(Some(&font), saveable.font_name)?;
294 | }
295 | render.fps = saveable.fps;
296 | render.device_address = saveable.device_address;
297 | render.device_ip = saveable.device_ip;
298 | render.rotate_degree = saveable.rotate_degree.unwrap_or(0);
299 | render.widgets.clear();
300 | for w in saveable.widgets {
301 | match w {
302 | SaveableWidget::TextWidget(txt) => {
303 | render.widgets.push(Box::new(txt));
304 | }
305 | SaveableWidget::ImageWidget(img) => {
306 | render.widgets.push(Box::new(img));
307 | }
308 | }
309 | }
310 | Ok(render)
311 | }
312 |
313 | //改为json格式存储,这样添加了新的字段不影响解析原有格式的screen文件
314 | pub fn to_json(&mut self) -> Result> {
315 | let mut font = self.font.clone();
316 | let font_name = self.font_name.clone();
317 | if font_name == "凤凰点阵"{
318 | font = None;
319 | }
320 | let mut saveable = SaveableScreen {
321 | rotate_degree: Some(self.rotate_degree),
322 | width: self.width,
323 | height: self.height,
324 | model: self.model.clone(),
325 | font,
326 | font_name,
327 | widgets: vec![],
328 | fps: self.fps,
329 | device_address: self.device_address.clone(),
330 | device_ip: self.device_ip.clone(),
331 | };
332 | for idx in 0..self.widgets.len() {
333 | if let Some(widget) = self.widgets[idx].as_any_mut().downcast_mut::() {
334 | saveable
335 | .widgets
336 | .push(SaveableWidget::TextWidget(widget.clone()));
337 | }
338 | if let Some(widget) = self.widgets[idx].as_any_mut().downcast_mut::() {
339 | saveable
340 | .widgets
341 | .push(SaveableWidget::ImageWidget(widget.clone()));
342 | }
343 | }
344 | let json = serde_json::to_string(&saveable)?;
345 | let contents = json.as_bytes();
346 | info!("压缩前:{}k", contents.len() / 1024);
347 | //压缩
348 | let compressed = compress_prepend_size(contents);
349 | info!("压缩后:{}k", compressed.len() / 1024);
350 | Ok(compressed)
351 | }
352 |
353 | //改为json格式存储,这样添加了新的字段不影响解析原有格式的screen文件
354 | pub fn to_savable(&mut self) -> Result {
355 | let mut font = self.font.clone();
356 | let font_name = self.font_name.clone();
357 | if font_name == "凤凰点阵"{
358 | font = None;
359 | }
360 | let mut saveable = SaveableScreen {
361 | rotate_degree: Some(self.rotate_degree),
362 | width: self.width,
363 | height: self.height,
364 | model: self.model.clone(),
365 | font,
366 | font_name,
367 | widgets: vec![],
368 | fps: self.fps,
369 | device_address: self.device_address.clone(),
370 | device_ip: self.device_ip.clone()
371 | };
372 | for idx in 0..self.widgets.len() {
373 | if let Some(widget) = self.widgets[idx].as_any_mut().downcast_mut::() {
374 | saveable
375 | .widgets
376 | .push(SaveableWidget::TextWidget(widget.clone()));
377 | }
378 | if let Some(widget) = self.widgets[idx].as_any_mut().downcast_mut::() {
379 | saveable
380 | .widgets
381 | .push(SaveableWidget::ImageWidget(widget.clone()));
382 | }
383 | }
384 | Ok(saveable)
385 | }
386 |
387 | pub fn saveable_to_compressed_json(saveable: &SaveableScreen) -> Result>{
388 | let json = serde_json::to_string(&saveable)?;
389 | // info!("保存:{json}");
390 | let contents = json.as_bytes();
391 | info!("压缩前:{}k", contents.len() / 1024);
392 | //压缩
393 | let compressed = compress_prepend_size(contents);
394 | info!("压缩后:{}k", compressed.len() / 1024);
395 | Ok(compressed)
396 | }
397 | }
398 |
--------------------------------------------------------------------------------
/src/usb_screen.rs:
--------------------------------------------------------------------------------
1 | use std::time::Duration;
2 |
3 | use futures_lite::future::block_on;
4 | use image::{Rgb, RgbImage};
5 | use log::{info, warn};
6 | use nusb::Interface;
7 | use anyhow::{anyhow, Result};
8 | #[cfg(feature = "usb-serial")]
9 | use serialport::{SerialPort, SerialPortInfo, SerialPortType};
10 |
11 | use crate::rgb565::rgb888_to_rgb565_be;
12 |
13 | // use crate::rgb565::rgb888_to_rgb565_be;
14 |
15 | const BULK_OUT_EP: u8 = 0x01;
16 | const BULK_IN_EP: u8 = 0x81;
17 |
18 | #[derive(Clone, Debug)]
19 | pub struct UsbScreenInfo{
20 | pub label: String,
21 | pub address: String,
22 | pub width: u16,
23 | pub height: u16,
24 | }
25 |
26 | pub enum UsbScreen{
27 | USBRaw((UsbScreenInfo, Interface)),
28 | #[cfg(feature = "usb-serial")]
29 | USBSerial((UsbScreenInfo, Box))
30 | }
31 |
32 | impl UsbScreen{
33 | pub fn draw_rgb_image(&mut self, x: u16, y: u16, img:&RgbImage) -> anyhow::Result<()>{
34 | //如果图像比屏幕大, 不绘制,否则会RP2040死机导致卡住
35 | match self{
36 | UsbScreen::USBRaw((info, interface)) => {
37 | if img.width() <= info.width as u32 && img.height() <= info.height as u32{
38 | draw_rgb_image(x, y, img, interface)?;
39 | }
40 | }
41 |
42 | #[cfg(feature = "usb-serial")]
43 | UsbScreen::USBSerial((info, port)) => {
44 | if img.width() <= info.width as u32 && img.height() <= info.height as u32{
45 | draw_rgb_image_serial(x, y, img, port.as_mut())?;
46 | }
47 | }
48 | }
49 | Ok(())
50 | }
51 |
52 | pub fn open(info: UsbScreenInfo) -> Result{
53 | info!("打开屏幕:label={} addr={} {}x{}", info.label, info.address, info.width, info.height);
54 | let addr = info.address.clone();
55 | if info.label.contains("Screen"){
56 | //USB Raw设备, addr是device_address
57 | Ok(Self::USBRaw((info, open_usb_raw_device(&addr)?)))
58 | }else{
59 | #[cfg(feature = "usb-serial")]
60 | {
61 | //USB串口设备, addr是串口名称
62 | let screen = serialport::new(&info.address, 115_200).open()?;
63 | Ok(Self::USBSerial((info, screen)))
64 | }
65 | #[cfg(not(feature = "usb-serial"))]
66 | {
67 | Err(anyhow!("此平台不支持 USB串口设备"))
68 | }
69 | }
70 | }
71 | }
72 |
73 | pub fn find_and_open_a_screen() -> Option{
74 | //先查找串口设备
75 | let devices = find_all_device();
76 | for info in devices{
77 | if let Ok(screen) = UsbScreen::open(info){
78 | return Some(screen);
79 | }
80 | }
81 | None
82 | }
83 |
84 | pub fn open_usb_raw_device(device_address: &str) -> Result{
85 | let di = nusb::list_devices()?;
86 | for d in di{
87 | if d.serial_number().unwrap_or("").starts_with("USBSCR") && d.device_address() == device_address.parse::()?{
88 | let device = d.open()?;
89 | let interface = device.claim_interface(0)?;
90 | return Ok(interface);
91 | }
92 | }
93 | Err(anyhow!("设备地址未找到"))
94 | }
95 |
96 | fn get_screen_size_from_serial_number(serial_number:&str) -> (u16, u16){
97 | //从串号中读取屏幕大小
98 | let screen_size = &serial_number[6..serial_number.find(";").unwrap_or(13)];
99 | let screen_size = screen_size.replace("X", "x");
100 | let mut arr = screen_size.split("x");
101 | let width = arr.next().unwrap_or("160").parse::().unwrap_or(160);
102 | let height = arr.next().unwrap_or("128").parse::().unwrap_or(128);
103 | (width, height)
104 | }
105 |
106 | // 查询所有USB屏幕设备
107 | // 对于USB Raw返回的第2个参数是 device_address
108 | // 对于USB Serial, 返回的第2个参数是串口名称
109 | pub fn find_all_device() -> Vec{
110 | let mut devices = vec![];
111 | if let Ok(di) = nusb::list_devices(){
112 | for d in di{
113 | #[cfg(not(windows))]
114 | info!("USB Raw设备:{:?}", d);
115 | let serial_number = d.serial_number().unwrap_or("");
116 | if d.product_string().unwrap_or("") == "USB Screen" && serial_number.starts_with("USBSCR"){
117 | let label = format!("USB Screen({})", d.device_address());
118 | let address = format!("{}", d.device_address());
119 | let (width, height) = get_screen_size_from_serial_number(serial_number);
120 | devices.push(UsbScreenInfo{
121 | label,
122 | address,
123 | width,
124 | height,
125 | });
126 | }
127 | }
128 | }
129 | // println!("USB Raw设备数量:{}", devices.len());
130 | #[cfg(feature = "usb-serial")]
131 | devices.extend_from_slice(&find_usb_serial_device());
132 | #[cfg(not(windows))]
133 | info!("所有usb 设备:{:?}", devices);
134 |
135 | if devices.len() == 0{
136 | warn!("no available device!");
137 | }
138 |
139 | devices
140 | }
141 |
142 | #[cfg(feature = "usb-serial")]
143 | pub fn find_usb_serial_device() -> Vec{
144 | let ports: Vec = serialport::available_ports().unwrap_or(vec![]);
145 | let mut devices = vec![];
146 | for p in ports {
147 | #[cfg(not(windows))]
148 | info!("USB Serial 设备:{:?}", p);
149 | match p.port_type.clone(){
150 | SerialPortType::UsbPort(port) => {
151 | let serial_number = port.serial_number.unwrap_or("".to_string());
152 | if serial_number.starts_with("USBSCR"){
153 | let port_name = p.port_name.clone();
154 | let (width, height) = get_screen_size_from_serial_number(&serial_number);
155 | devices.push(UsbScreenInfo{
156 | label: format!("USB {port_name}"), address: port_name.to_string(),
157 | width,
158 | height,
159 | });
160 | continue;
161 | }
162 | }
163 | _ => ()
164 | }
165 | }
166 | devices
167 | }
168 |
169 | pub fn clear_screen(color: Rgb, interface:&Interface, width: u16, height: u16) -> anyhow::Result<()>{
170 | let mut img = RgbImage::new(width as u32, height as u32);
171 | for p in img.pixels_mut(){
172 | *p = color;
173 | }
174 | draw_rgb_image(0, 0, &img, interface)
175 | }
176 |
177 | #[cfg(feature = "usb-serial")]
178 | pub fn clear_screen_serial(color: Rgb, port:&mut dyn SerialPort, width: u16, height: u16) -> anyhow::Result<()>{
179 | let mut img = RgbImage::new(width as u32, height as u32);
180 | for p in img.pixels_mut(){
181 | *p = color;
182 | }
183 | draw_rgb_image_serial(0, 0, &img, port)
184 | }
185 |
186 | pub fn draw_rgb_image(x: u16, y: u16, img:&RgbImage, interface:&Interface) -> anyhow::Result<()>{
187 | //ST7789驱动使用的是Big-Endian
188 | let rgb565 = rgb888_to_rgb565_be(&img, img.width() as usize, img.height() as usize);
189 | draw_rgb565(&rgb565, x, y, img.width() as u16, img.height() as u16, interface)
190 | }
191 |
192 | pub fn draw_rgb565(rgb565:&[u8], x: u16, y: u16, width: u16, height: u16, interface:&Interface) -> anyhow::Result<()>{
193 | // info!("压缩前大小:{}", rgb565.len());
194 | let rgb565_u8_slice = lz4_flex::compress_prepend_size(rgb565);
195 | // info!("压缩后大小:{}", rgb565_u8_slice.len());
196 | if rgb565_u8_slice.len() >1024*28 {
197 | return Err(anyhow!("图像太大了!"));
198 | }
199 | const IMAGE_AA:u64 = 7596835243154170209;
200 | const BOOT_USB:u64 = 7093010483740242786;
201 | const IMAGE_BB:u64 = 7596835243154170466;
202 |
203 | let img_begin = &mut [0u8; 16];
204 | img_begin[0..8].copy_from_slice(&IMAGE_AA.to_be_bytes());
205 | img_begin[8..10].copy_from_slice(&width.to_be_bytes());
206 | img_begin[10..12].copy_from_slice(&height.to_be_bytes());
207 | img_begin[12..14].copy_from_slice(&x.to_be_bytes());
208 | img_begin[14..16].copy_from_slice(&y.to_be_bytes());
209 | // info!("绘制:{x}x{y} {width}x{height}");
210 | // block_on(interface.bulk_out(BULK_OUT_EP, img_begin.into())).status?;
211 | block_on(async {
212 | async_std::future::timeout(Duration::from_millis(100), interface.bulk_out(BULK_OUT_EP, img_begin.into()))
213 | .await
214 | })?.status?;
215 | //读取
216 | // let result = block_on(interface.bulk_in(BULK_IN_EP, RequestBuffer::new(64))).data;
217 | // let msg = String::from_utf8(result)?;
218 | // println!("{msg}ms");
219 | // block_on(interface.bulk_out(BULK_OUT_EP, rgb565_u8_slice.into())).status?;
220 | block_on(async {
221 | async_std::future::timeout(Duration::from_millis(100), interface.bulk_out(BULK_OUT_EP, rgb565_u8_slice.into()))
222 | .await
223 | })?.status?;
224 | // block_on(interface.bulk_out(BULK_OUT_EP, IMAGE_BB.to_be_bytes().into())).status?;
225 | block_on(async {
226 | async_std::future::timeout(Duration::from_millis(100), interface.bulk_out(BULK_OUT_EP, IMAGE_BB.to_be_bytes().into()))
227 | .await
228 | })?.status?;
229 | // info!("绘制成功..");
230 | Ok(())
231 | }
232 |
233 | #[cfg(feature = "usb-serial")]
234 | pub fn draw_rgb_image_serial(x: u16, y: u16, img:&RgbImage, port:&mut dyn SerialPort) -> anyhow::Result<()>{
235 | //ST7789驱动使用的是Big-Endian
236 | let rgb565 = rgb888_to_rgb565_be(&img, img.width() as usize, img.height() as usize);
237 | draw_rgb565_serial(&rgb565, x, y, img.width() as u16, img.height() as u16, port)
238 | }
239 |
240 | // 320x240屏幕连接到usb,然后在编辑器中一边添加多张gif,一边保存时,有时候rp2040会死机,同时编辑器也会卡死。
241 | //第一:首先解决usb死机后,软件卡死问题
242 | //第二:找到硬件代码死机问题,增加判断逻辑
243 |
244 | #[cfg(feature = "usb-serial")]
245 | pub fn draw_rgb565_serial(rgb565:&[u8], x: u16, y: u16, width: u16, height: u16, port:&mut dyn SerialPort) -> anyhow::Result<()>{
246 |
247 | let rgb565_u8_slice = lz4_flex::compress_prepend_size(rgb565);
248 |
249 | const IMAGE_AA:u64 = 7596835243154170209;
250 | const BOOT_USB:u64 = 7093010483740242786;
251 | const IMAGE_BB:u64 = 7596835243154170466;
252 |
253 | let img_begin = &mut [0u8; 16];
254 | img_begin[0..8].copy_from_slice(&IMAGE_AA.to_be_bytes());
255 | img_begin[8..10].copy_from_slice(&width.to_be_bytes());
256 | img_begin[10..12].copy_from_slice(&height.to_be_bytes());
257 | img_begin[12..14].copy_from_slice(&x.to_be_bytes());
258 | img_begin[14..16].copy_from_slice(&y.to_be_bytes());
259 | // println!("draw:{x}x{y} {width}x{height} len={}", rgb565_u8_slice.len());
260 |
261 | port.write(img_begin)?;
262 | port.flush()?;
263 | port.write(&rgb565_u8_slice)?;
264 | port.flush()?;
265 | port.write(&IMAGE_BB.to_be_bytes())?;
266 | port.flush()?;
267 | Ok(())
268 | }
269 |
270 | #[cfg(not(windows))]
271 | fn list_acm_devices() -> Vec {
272 | let dir_path = std::path::Path::new("/dev");
273 | let entries = match std::fs::read_dir(dir_path){
274 | Err(err) => {
275 | log::error!("error list /dev/ {:?}", err);
276 | return vec![];
277 | }
278 | Ok(e) => e
279 | };
280 | entries.filter_map(|entry| {
281 | entry.ok().and_then(|e| {
282 | let path = e.path();
283 | if let Some(file_name) = path.file_name() {
284 | if let Some(name) = file_name.to_str() {
285 | if name.starts_with("ttyACM") {
286 | return Some(format!("/dev/{name}"));
287 | }
288 | }
289 | }
290 | None
291 | })
292 | }).collect()
293 | }
--------------------------------------------------------------------------------
/src/utils.rs:
--------------------------------------------------------------------------------
1 | use std::{io, path::PathBuf, process::{Command, Stdio}};
2 | #[cfg(target_os = "windows")]
3 | use std::os::windows::process::CommandExt;
4 |
5 | use image::{imageops::FilterType, RgbaImage};
6 |
7 | #[cfg(target_os = "windows")]
8 | pub fn execute_user_command(command: &str) -> io::Result {
9 | let output =
10 | // 如果是 PowerShell 命令(以 `powershell` 开头),使用 `powershell.exe`
11 | if command.trim().to_lowercase().starts_with("powershell") {
12 | Command::new("powershell")
13 | .args(&["-Command", command.trim()])
14 | .stdout(Stdio::piped())
15 | .stderr(Stdio::piped())
16 | .creation_flags(0x08000000)
17 | .output()?
18 | } else {
19 | // 否则使用 `cmd.exe`
20 | Command::new("cmd")
21 | .args(&["/C", command.trim()])
22 | .stdout(Stdio::piped())
23 | .stderr(Stdio::piped())
24 | .creation_flags(0x08000000)
25 | .output()?
26 | };
27 |
28 | if output.status.success() {
29 | let stdout = String::from_utf8_lossy(&output.stdout).to_string();
30 | Ok(stdout)
31 | } else {
32 | let stderr = String::from_utf8_lossy(&output.stderr).to_string();
33 | Err(io::Error::new(io::ErrorKind::Other, stderr))
34 | }
35 | }
36 |
37 | #[cfg(not(target_os = "windows"))]
38 | pub fn execute_user_command(command: &str) -> io::Result {
39 | let output =
40 | // Linux/macOS 使用 `sh`
41 | Command::new("sh")
42 | .arg("-c")
43 | .arg(command.trim())
44 | .stdout(Stdio::piped())
45 | .stderr(Stdio::piped())
46 | .output()?;
47 |
48 | if output.status.success() {
49 | let stdout = String::from_utf8_lossy(&output.stdout).to_string();
50 | Ok(stdout)
51 | } else {
52 | let stderr = String::from_utf8_lossy(&output.stderr).to_string();
53 | Err(io::Error::new(io::ErrorKind::Other, stderr))
54 | }
55 | }
56 |
57 | pub fn degrees_to_radians(degrees: f32) -> f32 {
58 | degrees * std::f32::consts::PI / 180.0
59 | }
60 |
61 | pub fn resize_image(
62 | image: &RgbaImage,
63 | max_width: u32,
64 | max_height: u32,
65 | filter: FilterType,
66 | ) -> RgbaImage {
67 | let (width, height) = image.dimensions();
68 |
69 | // 计算缩放比例
70 | let scale_factor = if width > height {
71 | max_width as f32 / width as f32
72 | } else {
73 | max_height as f32 / height as f32
74 | };
75 |
76 | // 计算新的尺寸,确保不会超过最大值
77 | let new_width = (width as f32 * scale_factor).round() as u32;
78 | let new_height: u32 = (height as f32 * scale_factor).round() as u32;
79 |
80 | // 使用resize方法进行缩放
81 | let img = image::imageops::resize(image, new_width, new_height, filter);
82 | img
83 | }
84 |
85 | pub fn test_resize_image(width: u32, height: u32, max_width: u32, max_height: u32) -> (u32, u32) {
86 | // 计算缩放比例
87 | let scale_factor = if width > height {
88 | max_width as f32 / width as f32
89 | } else {
90 | max_height as f32 / height as f32
91 | };
92 |
93 | // 计算新的尺寸,确保不会超过最大值
94 | let new_width = (width as f32 * scale_factor).round() as u32;
95 | let new_height: u32 = (height as f32 * scale_factor).round() as u32;
96 |
97 | (new_width, new_height)
98 | }
99 |
100 | //解析字体名称
101 | pub fn get_font_name(ttf: PathBuf, max_char: usize) -> anyhow::Result {
102 | // 初始化系统字体源
103 | let font_data = std::fs::read(ttf)?;
104 |
105 | let face = ttf_parser::Face::parse(&font_data, 0)?;
106 |
107 | let mut family_names = Vec::new();
108 | for name in face.names() {
109 | if name.name_id == ttf_parser::name_id::FULL_NAME && name.is_unicode() {
110 | if let Some(family_name) = name.to_string() {
111 | let language = name.language();
112 | family_names.push(format!(
113 | "{} ({}, {})",
114 | family_name,
115 | language.primary_language(),
116 | language.region()
117 | ));
118 | }
119 | }
120 | }
121 |
122 | let family_name = if family_names.len() > 1 && family_names[1].contains("Chinese") {
123 | family_names[1].to_string()
124 | } else {
125 | family_names.get(0).unwrap_or(&String::new()).to_string()
126 | };
127 |
128 | let mut new_name = String::new();
129 | for c in family_name.chars() {
130 | if new_name.chars().count() < max_char {
131 | new_name.push(c);
132 | } else {
133 | break;
134 | }
135 | }
136 | Ok(new_name)
137 | }
138 |
139 | #[cfg(windows)]
140 | pub mod register_app_for_startup{
141 | use anyhow::{anyhow, Result};
142 | use std::path::Path;
143 | use std::io::Write;
144 | use windows::Win32::{
145 | Foundation::MAX_PATH,
146 | UI::{
147 | Shell::{SHGetSpecialFolderPathW, CSIDL_STARTUP},
148 | WindowsAndMessaging::GetDesktopWindow
149 | }
150 | };
151 |
152 | static TEMPLATE: &str = r"[InternetShortcut]
153 | URL=--
154 | IconIndex=0
155 | IconFile=--
156 | ";
157 |
158 | pub fn register_app_for_startup(app_name: &str) -> Result<()> {
159 | let hwnd = unsafe { GetDesktopWindow() };
160 | let mut path: [u16; MAX_PATH as usize] = [0; MAX_PATH as usize];
161 | unsafe { SHGetSpecialFolderPathW(Some(hwnd), &mut path, CSIDL_STARTUP as i32, false) };
162 | let path = String::from_utf16(&path)?.replace("\u{0}", "");
163 | let url_file = format!("{}\\{}.url", path, app_name);
164 | //写入url文件
165 | let mut file = std::fs::File::create(url_file)?;
166 | let exe_path = ::std::env::current_exe()?;
167 | if let Some(exe_path) = exe_path.to_str() {
168 | file.write_all(TEMPLATE.replace("--", exe_path).as_bytes())?;
169 | Ok(())
170 | } else {
171 | Err(anyhow!("exe路径读取失败!"))
172 | }
173 | }
174 |
175 | pub fn is_app_registered_for_startup(app_name: &str) -> Result {
176 | let hwnd = unsafe { GetDesktopWindow() };
177 | let mut path: [u16; MAX_PATH as usize] = [0; MAX_PATH as usize];
178 | unsafe { SHGetSpecialFolderPathW(Some(hwnd), &mut path, CSIDL_STARTUP as i32, false) };
179 | let path = String::from_utf16(&path)?.replace("\u{0}", "");
180 | Ok(Path::new(&format!("{}\\{}.url", path, app_name)).exists())
181 | }
182 |
183 | pub fn remove_app_for_startup(app_name: &str) -> Result<()> {
184 | let hwnd = unsafe { GetDesktopWindow() };
185 | let mut path: [u16; MAX_PATH as usize] = [0; MAX_PATH as usize];
186 | unsafe { SHGetSpecialFolderPathW(Some(hwnd), &mut path, CSIDL_STARTUP as i32, false) };
187 | let path = String::from_utf16(&path)?.replace("\u{0}", "");
188 | std::fs::remove_file(format!("{}\\{}.url", path, app_name))?;
189 | Ok(())
190 | }
191 | }
--------------------------------------------------------------------------------
/src/widgets.rs:
--------------------------------------------------------------------------------
1 | use crate::{
2 | monitor::{self, system_uptime, webcam_frame},
3 | nmc::ICONS,
4 | utils::{degrees_to_radians, execute_user_command, resize_image, test_resize_image},
5 | };
6 | use anyhow::Result;
7 | use image::{
8 | buffer::ConvertBuffer, imageops::{resize, FilterType}, Rgba, RgbaImage
9 | };
10 | use log::error;
11 | use offscreen_canvas::{measure_text, OffscreenCanvas, ResizeOption, RotateOption, WHITE};
12 | use serde::{Deserialize, Serialize};
13 | use std::{any::Any, sync::{atomic::{AtomicPtr, Ordering}, Arc, Mutex}};
14 | use uuid::Uuid;
15 |
16 | static DEFAULT_IMAGE: &[u8] = include_bytes!("../images/icon_photo.png");
17 |
18 | #[derive(Debug, Clone, Default, Deserialize, Serialize)]
19 | pub struct Rect {
20 | pub left: i32,
21 | pub top: i32,
22 | pub right: i32,
23 | pub bottom: i32,
24 | }
25 |
26 | impl Rect {
27 | pub fn new(left: i32, top: i32, right: i32, bottom: i32) -> Rect {
28 | Rect {
29 | left,
30 | top,
31 | right,
32 | bottom,
33 | }
34 | }
35 |
36 | pub fn from(x: i32, y: i32, width: i32, height: i32) -> Rect {
37 | Rect {
38 | left: x,
39 | top: y,
40 | right: x + width,
41 | bottom: y + height,
42 | }
43 | }
44 |
45 | pub fn width(&self) -> i32 {
46 | self.right - self.left
47 | }
48 |
49 | pub fn height(&self) -> i32 {
50 | self.bottom - self.top
51 | }
52 |
53 | /** 扩大 */
54 | pub fn inflate(&mut self, dx: i32, dy: i32) {
55 | self.left -= dx;
56 | self.right += dx;
57 | self.top -= dy;
58 | self.bottom += dy;
59 | }
60 |
61 | pub fn deflate(&mut self, dx: i32, dy: i32) {
62 | self.left += dx;
63 | self.right -= dx;
64 | self.top += dy;
65 | self.bottom -= dy;
66 | }
67 |
68 | // 平移矩形
69 | pub fn offset(&mut self, dx: i32, dy: i32) {
70 | self.left += dx;
71 | self.right += dx;
72 | self.top += dy;
73 | self.bottom += dy;
74 | }
75 |
76 | pub fn contain(&self, x: i32, y: i32) -> bool {
77 | x >= self.left && x <= self.right && y >= self.top && y <= self.bottom
78 | }
79 |
80 | pub fn center(&self) -> (i32, i32) {
81 | (self.left + self.width() / 2, self.top + self.height() / 2)
82 | }
83 |
84 | // 设置矩形中心点
85 | pub fn set_center(&mut self, center_x: i32, center_y: i32) {
86 | let width = (self.right - self.left) / 2;
87 | let height = (self.bottom - self.top) / 2;
88 | self.left = center_x - width;
89 | self.right = center_x + width;
90 | self.top = center_y - height;
91 | self.bottom = center_y + height;
92 | }
93 |
94 | // 设置矩形左上角位置
95 | pub fn set_position(&mut self, left: i32, top: i32) {
96 | let width = self.right - self.left;
97 | let height = self.bottom - self.top;
98 | self.left = left;
99 | self.right = left + width;
100 | self.top = top;
101 | self.bottom = top + height;
102 | }
103 |
104 | // 设置矩形的尺寸(宽高)
105 | pub fn set_size(&mut self, width: i32, height: i32) {
106 | let center_x = (self.left + self.right) / 2;
107 | let center_y = (self.top + self.bottom) / 2;
108 | self.left = center_x - width / 2;
109 | self.right = center_x + width / 2;
110 | self.top = center_y - height / 2;
111 | self.bottom = center_y + height / 2;
112 | }
113 |
114 | // 设置矩形的尺寸(宽高)
115 | pub fn set_width_and_height(&mut self, width: i32, height: i32) {
116 | self.right = self.left + width;
117 | self.bottom = self.top + height;
118 | }
119 | }
120 |
121 | pub trait Widget {
122 | fn draw(&mut self, context: &mut OffscreenCanvas);
123 | fn id(&self) -> &str;
124 | fn index(&self) -> usize;
125 | fn set_index(&mut self, idx: usize);
126 | fn num_widget(&self) -> usize;
127 | fn set_num_widget(&mut self, num: usize);
128 | fn position(&self) -> &Rect;
129 | fn position_mut(&mut self) -> &mut Rect;
130 | fn type_name(&self) -> &str;
131 | fn as_any_mut(&mut self) -> &mut dyn Any;
132 | fn is_text(&self) -> bool{
133 | self.type_name() != "images" && self.type_name() != "webcam"
134 | }
135 | fn is_image(&self) -> bool{
136 | self.type_name() == "images"
137 | }
138 | fn is_webcam(&self) -> bool{
139 | self.type_name() == "webcam"
140 | }
141 | fn get_label(&self) -> &str{
142 | if self.is_image() {
143 | "图像"
144 | }else if self.is_webcam() {
145 | "摄像头"
146 | } else {
147 | "文本"
148 | }
149 | }
150 | }
151 |
152 | #[derive(Default, Clone)]
153 | pub struct CustomScriptStatus{
154 | pub loading: bool,
155 | pub result: String,
156 | }
157 |
158 | #[derive(Clone, Deserialize, Serialize)]
159 | pub struct TextWidget {
160 | pub id: String,
161 | pub text: String,
162 | pub prefix: String,
163 | pub color: [u8; 4],
164 | pub font_size: f32,
165 | pub position: Rect,
166 | pub type_name: String,
167 | // 在本类组件中,排序第几
168 | pub num_widget_index: usize,
169 | // 一共有多少个当前类型的组件
170 | pub num_widget: usize,
171 | pub tag1: String,
172 | pub tag2: String,
173 | pub width: Option,
174 | pub height: Option,
175 | // 对齐方式 居中, 居左, 居右
176 | pub alignment: Option,
177 | //自定义内容脚本(执行脚本后,获取到数据)
178 | pub custom_script: Option,
179 | //这是执行命令完成后获得的数据
180 | #[serde(skip_serializing, skip_deserializing)]
181 | pub custom_script_data: Arc>
182 | }
183 |
184 | impl TextWidget {
185 | #[allow(unused)]
186 | pub fn new(x: i32, y: i32, type_name: &str, type_label: &str) -> Self {
187 | Self::new_with_text(x, y, type_name, type_label, "文本")
188 | }
189 |
190 | pub fn new_with_text(x: i32, y: i32, type_name: &str, type_label: &str, text: &str) -> Self {
191 |
192 | Self {
193 | id: Uuid::new_v4().to_string(),
194 | text: text.to_string(),
195 | prefix: if type_label.len() > 0 {
196 | format!("{type_label}:")
197 | } else {
198 | String::new()
199 | },
200 | color: WHITE.0,
201 | font_size: 14.,
202 | position: Rect::new(x, y, x + 1, y + 1),
203 | type_name: type_name.to_string(),
204 | num_widget_index: 0,
205 | num_widget: 1,
206 | tag1: "".to_string(),
207 | tag2: "".to_string(),
208 | alignment: None,
209 | width: None,
210 | height: None,
211 | custom_script: None,
212 | custom_script_data: Arc::new(Mutex::new(CustomScriptStatus{ loading: false, result: String::new()}))
213 | }
214 | }
215 |
216 | pub fn execute_user_command(&self, command:String){
217 | // 启动子线程,每秒更新 JSON 数据
218 | let data_clone = self.custom_script_data.clone();
219 | std::thread::spawn(move || {
220 | {
221 | //锁定
222 | let mut data = match data_clone.lock(){
223 | Err(err) => {
224 | error!("custom_script_data lock error:{err:?}");
225 | return;
226 | }
227 | Ok(v) => v
228 | };
229 | data.loading = true;
230 | }
231 | // let t = Instant::now();
232 | let result = format!("{}", execute_user_command(&command).unwrap_or(String::from("脚本运行失败"))).replace("\r\n", "").replace("\n", "").replace("\r", "");
233 | // info!("脚本执行时间:{}ms {result}", t.elapsed().as_millis());
234 | {
235 | //锁定
236 | let mut data = match data_clone.lock(){
237 | Err(err) => {
238 | error!("custom_script_data lock error:{err:?}");
239 | return;
240 | }
241 | Ok(v) => v
242 | };
243 | data.loading = false;
244 | data.result = result;
245 | }
246 | });
247 | }
248 | }
249 |
250 | impl Widget for TextWidget {
251 | fn draw(&mut self, context: &mut OffscreenCanvas) {
252 |
253 | let mut custom_script = None;
254 | if let Some(script) = self.custom_script.as_ref(){
255 | if script.trim().len() > 0{
256 | custom_script = Some(script);
257 | }
258 | }
259 | // 从自定义脚本中获取text
260 | if let Some(command) = custom_script{
261 | if let Ok(custom_script_data) = self.custom_script_data.try_lock(){
262 | if !custom_script_data.loading{
263 | self.execute_user_command(command.clone());
264 | }
265 | self.text = custom_script_data.result.clone();
266 | }
267 | }else{
268 | if self.type_name != "text" {
269 | if let Some(text) = match self.type_name.as_str() {
270 | "cpu" => monitor::cpu_brand(),
271 | "memory" => monitor::memory_info(),
272 | "memory_total" => monitor::memory_total(),
273 | "memory_percent" => monitor::memory_percent(),
274 | "swap" => monitor::swap_info(),
275 | "swap_percent" => monitor::swap_percent(),
276 | "system" => monitor::system_name(),
277 | "version" => monitor::os_version(),
278 | "kernel" => monitor::kernel_version(),
279 | "host" => monitor::host_name(),
280 | "cpu_freq" => monitor::cpu_clock_speed(None),
281 | "cpu_usage" => {
282 | if self.num_widget == 1 {
283 | monitor::cpu_usage()
284 | } else {
285 | monitor::cpu_usage_percpu(self.num_widget_index)
286 | }
287 | }
288 | "cpu_temp." => {
289 | Some(monitor::cpu_temperature().unwrap_or(monitor::EMPTY_STRING.to_string()))
290 | }
291 | "cpu_cores_power" => {
292 | Some(monitor::cpu_cores_power().unwrap_or(monitor::EMPTY_STRING.to_string()))
293 | }
294 | "cpu_package_power" => {
295 | Some(monitor::cpu_package_power().unwrap_or(monitor::EMPTY_STRING.to_string()))
296 | }
297 | "cpu_fan" => Some(monitor::cpu_fan().unwrap_or(monitor::EMPTY_STRING.to_string())),
298 | "gpu_fan" => Some(
299 | monitor::gpu_fan(self.num_widget_index)
300 | .unwrap_or(monitor::EMPTY_STRING.to_string()),
301 | ),
302 | "gpu_clock" => Some(
303 | monitor::gpu_clocks(self.num_widget_index)
304 | .unwrap_or(monitor::EMPTY_STRING.to_string()),
305 | ),
306 | "gpu_load" => Some(
307 | monitor::gpu_load(self.num_widget_index)
308 | .unwrap_or(monitor::EMPTY_STRING.to_string()),
309 | ),
310 | "gpu_memory_load" => Some(
311 | monitor::gpu_memory_load(self.num_widget_index)
312 | .unwrap_or(monitor::EMPTY_STRING.to_string()),
313 | ),
314 | "gpu_memory_total_mb" => Some(
315 | monitor::gpu_memory_total_mb(self.num_widget_index)
316 | .unwrap_or(monitor::EMPTY_STRING.to_string()),
317 | ),
318 | "gpu_memory_total_gb" => Some(
319 | monitor::gpu_memory_total_gb(self.num_widget_index)
320 | .unwrap_or(monitor::EMPTY_STRING.to_string()),
321 | ),
322 | "gpu_temp." => Some(
323 | monitor::gpu_temperature(self.num_widget_index)
324 | .unwrap_or(monitor::EMPTY_STRING.to_string()),
325 | ),
326 | "gpu_cores_power" => {
327 | Some(monitor::gpu_cores_power().unwrap_or(monitor::EMPTY_STRING.to_string()))
328 | }
329 | "gpu_package_power" => {
330 | Some(monitor::gpu_package_power().unwrap_or(monitor::EMPTY_STRING.to_string()))
331 | }
332 | "num_cpu" => monitor::num_cpus(),
333 | "num_process" => monitor::num_process(),
334 | "disk_usage" => monitor::disk_usage(self.num_widget_index),
335 | "date" => Some(monitor::date()),
336 | "local_ip" => monitor::local_ip_addresses(),
337 | "net_ip" => monitor::net_ip_address(),
338 | "net_ip_info" => monitor::net_ip_info(),
339 | "time" => Some(monitor::time()),
340 | "weekday" => Some(monitor::chinese_weekday()),
341 | "lunar_year" => Some(monitor::lunar_year()),
342 | "lunar_date" => Some(monitor::lunar_date()),
343 | "weather" => match monitor::weather_info() {
344 | None => Some(monitor::EMPTY_STRING.to_string()),
345 | Some(w) => {
346 | match self.tag1.as_str() {
347 | "1" => Some(format!("{}", w.station.city)), //城市
348 | "2" => Some(format!("{}℃", w.weather.temperature)), //气温
349 | "3" => Some(format!("{}℃", w.wind.direct)), //风向
350 | "4" => Some(format!("{}", w.wind.power)), //风力
351 | "5" => Some(format!("{}级", w.wind.speed)), //风级
352 | "6" => Some(format!("{}", w.weather.img)), //图标
353 | _ => Some(format!("{}", w.weather.info)),
354 | }
355 | }
356 | },
357 | "uptime" => {
358 | let uptime = system_uptime();
359 | let uptime_str = match self.tag1.as_str() {
360 | //运行分钟数
361 | "1" => Some(format!("{}", uptime.minutes)),
362 | //运行小时数
363 | "2" => Some(format!("{}", uptime.hours)),
364 | //运行天数
365 | "3" => Some(format!("{}", uptime.days)),
366 | //运行秒数
367 | _ => Some(format!("{}", uptime.seconds)),
368 | };
369 | uptime_str
370 | },
371 | "disk_read_speed" => monitor::disk_speed_per_sec().map(|(r, _w)| r),
372 | "disk_write_speed" => monitor::disk_speed_per_sec().map(|(_r, w)| w),
373 | "received_speed" => monitor::network_speed_per_sec().map(|(r, _t)| r),
374 | "transmitted_speed" => monitor::network_speed_per_sec().map(|(_r, t)| t),
375 | _ => None,
376 | } {
377 | if self.text != text && text != monitor::EMPTY_STRING {
378 | self.text = text;
379 | }
380 | }
381 | }
382 | }
383 |
384 | //天气渲染成图标
385 | if self.type_name == "weather" && self.tag1 == "6" {
386 | let img_idx = self.text.parse::().unwrap_or(0);
387 | let o = ResizeOption {
388 | nwidth: self.font_size as u32,
389 | nheight: self.font_size as u32,
390 | filter: FilterType::Triangle,
391 | };
392 | let (mut x, mut y) = self.position.center();
393 | x -= self.font_size as i32 / 2;
394 | y -= self.font_size as i32 / 2;
395 | context.draw_image_at(&ICONS[img_idx], x, y, Some(o), None);
396 | } else if self.type_name != "weather" && self.type_name != "uptime" && (self.tag1 == "1" || self.tag1 == "2") {
397 | //是否渲染成进度条
398 | let percent = self
399 | .text
400 | .replace("%", "")
401 | .replace("°C", "")
402 | .parse::()
403 | .unwrap_or(0.);
404 |
405 | let width = self.width.unwrap_or(self.font_size as i32 * 5);
406 | let height = self.height.unwrap_or(self.font_size as i32);
407 |
408 | //水平进度条
409 | if self.tag1 == "1" {
410 | let mut rect_width = (width as f32 * (percent / 100.)) as i32;
411 | if rect_width <= 0 {
412 | rect_width = 1;
413 | }
414 | if self.font_size <= 2. {
415 | self.font_size = 2.;
416 | }
417 | let rect = offscreen_canvas::Rect::from(
418 | self.position.left,
419 | self.position.top,
420 | rect_width,
421 | height,
422 | );
423 | context.fill_rect(rect, Rgba(self.color));
424 | }else{
425 | //垂直进度条
426 | let mut rect_height = (height as f32 * (percent / 100.)) as i32;
427 | if rect_height <= 0 {
428 | rect_height = 1;
429 | }
430 | if self.font_size <= 2. {
431 | self.font_size = 2.;
432 | }
433 | let rect = offscreen_canvas::Rect::from(
434 | self.position.left,
435 | self.position.top+(height-rect_height),
436 | width,
437 | rect_height,
438 | );
439 | context.fill_rect(rect, Rgba(self.color));
440 | }
441 | } else {
442 | if self.font_size <= 4. {
443 | self.font_size = 4.;
444 | }
445 | let text = format!("{}{}", self.prefix, self.text);
446 | let text_rect = context.measure_text(&text, self.font_size);
447 | let width = self.width.unwrap_or(text_rect.width());
448 | let height = self.height.unwrap_or(text_rect.height());
449 | let alignment = self.alignment.clone().unwrap_or("".to_string());
450 | if self.width.is_some() && alignment.len() > 0{
451 | self.position.set_width_and_height(width, height);
452 | let text_rect = measure_text(&text, self.font_size, context.font());
453 | if alignment == "居中"{
454 | context.draw_text(
455 | &text,
456 | Rgba(self.color),
457 | self.font_size,
458 | self.position.center().0 - text_rect.width()/2,
459 | self.position.top,
460 | );
461 | }else if alignment == "居左"{
462 | context.draw_text(
463 | &text,
464 | Rgba(self.color),
465 | self.font_size,
466 | self.position.left,
467 | self.position.top,
468 | );
469 | }else if alignment == "居右"{
470 | context.draw_text(
471 | &text,
472 | Rgba(self.color),
473 | self.font_size,
474 | self.position.right - text_rect.width(),
475 | self.position.top,
476 | );
477 | }
478 | }else{
479 | //居中方式调整文本位置
480 | self.position.set_size(width, height);
481 | context.draw_text(
482 | &text,
483 | Rgba(self.color),
484 | self.font_size,
485 | self.position.left,
486 | self.position.top,
487 | );
488 | }
489 | }
490 | }
491 |
492 | fn id(&self) -> &str {
493 | &self.id
494 | }
495 |
496 | fn position_mut(&mut self) -> &mut Rect {
497 | &mut self.position
498 | }
499 |
500 | fn type_name(&self) -> &str {
501 | &self.type_name
502 | }
503 |
504 | fn as_any_mut(&mut self) -> &mut dyn Any {
505 | self
506 | }
507 |
508 | fn position(&self) -> &Rect {
509 | &self.position
510 | }
511 |
512 | fn index(&self) -> usize {
513 | self.num_widget_index
514 | }
515 |
516 | fn set_index(&mut self, idx: usize) {
517 | self.num_widget_index = idx;
518 | }
519 |
520 | fn num_widget(&self) -> usize {
521 | self.num_widget
522 | }
523 |
524 | fn set_num_widget(&mut self, num: usize) {
525 | self.num_widget = num;
526 | }
527 | }
528 |
529 | #[derive(Default, Clone, Deserialize, Serialize)]
530 | pub struct ImageData {
531 | pub width: u32,
532 | pub height: u32,
533 | pub frames: Vec>,
534 | }
535 |
536 | impl ImageData {
537 | pub fn load(data: &[u8], max_size: (u32, u32)) -> Result {
538 | let format = image::guess_format(data)?;
539 | Ok(match format {
540 | image::ImageFormat::Gif => {
541 | let mut frames = vec![];
542 |
543 | let mut gif_opts = gif::DecodeOptions::new();
544 | // Important:
545 | gif_opts.set_color_output(gif::ColorOutput::Indexed);
546 | let mut decoder = gif_opts.read_info(data)?;
547 |
548 | //计算最大图像大小
549 | let (width, height) = test_resize_image(
550 | decoder.width() as u32,
551 | decoder.height() as u32,
552 | max_size.0,
553 | max_size.1,
554 | );
555 | let scale = width as f32 / decoder.width() as f32;
556 |
557 | let mut screen = gif_dispose::Screen::new_decoder(&decoder);
558 |
559 | while let Some(frame) = decoder.read_next_frame()? {
560 | screen.blit_frame(&frame)?;
561 | let rgba = screen.pixels_rgba();
562 | let mut pixels = Vec::with_capacity(rgba.width() * rgba.height() * 4);
563 | for pixel in rgba.pixels() {
564 | pixels.extend_from_slice(&[pixel.r, pixel.g, pixel.b, pixel.a]);
565 | }
566 | let img =
567 | RgbaImage::from_raw(rgba.width() as u32, rgba.height() as u32, pixels)
568 | .unwrap();
569 | //等比例缩放
570 | let nw = img.width() as f32 * scale;
571 | let nh = img.height() as f32 * scale;
572 | let img: RgbaImage = img;
573 | let img =
574 | image::imageops::resize(&img, nw as u32, nh as u32, FilterType::Triangle);
575 | frames.push(img.into_raw());
576 | }
577 |
578 | Self {
579 | width,
580 | height,
581 | frames,
582 | }
583 | }
584 | _ => {
585 | let image = image::load_from_memory(data).unwrap().to_rgba8();
586 | let resized = resize_image(
587 | &image,
588 | max_size.0,
589 | max_size.1,
590 | image::imageops::FilterType::Triangle,
591 | );
592 | Self {
593 | width: resized.width(),
594 | height: resized.height(),
595 | frames: vec![resized.to_vec()],
596 | }
597 | }
598 | })
599 | }
600 | }
601 |
602 | #[derive(Clone, Deserialize, Serialize)]
603 | pub struct ImageWidget {
604 | pub id: String,
605 | pub image_data: ImageData,
606 | pub rotation: f32,
607 | pub position: Rect,
608 | pub type_name: String,
609 | pub frame_index: usize,
610 | //是否为纯色
611 | pub color: Option<[u8; 4]>,
612 | pub num_widget_index: usize,
613 | // 一共有多少个当前类型的组件
614 | pub num_widget: usize,
615 | pub tag1: Option,
616 | pub tag2: Option,
617 | }
618 |
619 | impl ImageWidget {
620 | pub fn from_v10(img:v10::ImageWidget) -> Self{
621 | Self { id: img.id, image_data: img.image_data, rotation: img.rotation, position: img.position, type_name: img.type_name, frame_index: img.frame_index, color: img.color,
622 | num_widget_index: img.num_widget_index, num_widget: img.num_widget, tag1: None, tag2: None }
623 | }
624 |
625 | pub fn new(x: i32, y: i32, type_name: &str) -> Self {
626 | let image = image::load_from_memory(DEFAULT_IMAGE).unwrap().to_rgba8();
627 | let image = resize(&image, 50, 50, FilterType::Nearest);
628 | let (w, h) = (image.width(), image.height());
629 | Self {
630 | id: Uuid::new_v4().to_string(),
631 | image_data: ImageData {
632 | width: w,
633 | height: h,
634 | frames: vec![image.to_vec()],
635 | },
636 | rotation: 0.,
637 | position: Rect::from(x - w as i32 / 2, y - h as i32 / 2, w as i32, h as i32),
638 | type_name: type_name.to_string(),
639 | color: None,
640 | frame_index: 0,
641 | num_widget_index: 0,
642 | num_widget: 1,
643 | tag1: None,
644 | tag2: None,
645 | }
646 | }
647 | }
648 |
649 | impl Widget for ImageWidget {
650 | fn draw(&mut self, context: &mut OffscreenCanvas) {
651 | if let Some(color) = self.color.as_ref() {
652 | let rect = offscreen_canvas::Rect::from(
653 | self.position.left,
654 | self.position.top,
655 | self.position.width(),
656 | self.position.height(),
657 | );
658 | context.fill_rect(rect, Rgba(*color));
659 | }
660 | //是否是相机
661 | else if self.type_name == "webcam"{
662 | //获取相机图像
663 | if let Some(image) = webcam_frame(){
664 | let src =
665 | offscreen_canvas::Rect::new(0, 0, image.width() as i32, image.height() as i32);
666 |
667 | //按照宽度比例绘制
668 | let width = self.position.width();
669 | let height = ((image.height() as f32 / image.width() as f32)*width as f32) as i32;
670 |
671 | let pos = offscreen_canvas::Rect::from(
672 | self.position.left,
673 | self.position.top,
674 | width,
675 | height,
676 | );
677 |
678 | context.draw_image_with_src_and_dst(&image.convert(), &src, &pos, FilterType::Nearest);
679 | }else{
680 | //未打开相机,显示白色
681 | let rect = offscreen_canvas::Rect::from(
682 | self.position.left,
683 | self.position.top,
684 | self.position.width(),
685 | self.position.height(),
686 | );
687 | context.fill_rect(rect, WHITE);
688 | }
689 | }else {
690 | if self.frame_index >= self.image_data.frames.len(){
691 | self.frame_index = self.image_data.frames.len()-1;
692 | }
693 | let image = RgbaImage::from_raw(
694 | self.image_data.width,
695 | self.image_data.height,
696 | self.image_data.frames[self.frame_index].clone(),
697 | ).unwrap_or(RgbaImage::new(30, 30));
698 | let src =
699 | offscreen_canvas::Rect::new(0, 0, image.width() as i32, image.height() as i32);
700 | let pos = offscreen_canvas::Rect::from(
701 | self.position.left,
702 | self.position.top,
703 | self.position.width(),
704 | self.position.height(),
705 | );
706 |
707 | if self.rotation == 0.{
708 | //不旋转
709 | context.draw_image_with_src_and_dst(&image, &src, &pos, FilterType::Nearest);
710 | }else{
711 | let option = RotateOption::from(
712 | (
713 | self.position.width() as f32 / 2.,
714 | self.position.height() as f32 / 2.,
715 | ),
716 | degrees_to_radians(self.rotation),
717 | );
718 | context.draw_image_with_src_and_dst_and_rotation(&image, &src, &pos, option);
719 | }
720 | self.frame_index += 1;
721 | if self.frame_index >= self.image_data.frames.len() {
722 | self.frame_index = 0;
723 | }
724 | }
725 | }
726 |
727 | fn id(&self) -> &str {
728 | &self.id
729 | }
730 |
731 | fn position_mut(&mut self) -> &mut Rect {
732 | &mut self.position
733 | }
734 |
735 | fn type_name(&self) -> &str {
736 | &self.type_name
737 | }
738 |
739 | fn as_any_mut(&mut self) -> &mut dyn Any {
740 | self
741 | }
742 |
743 | fn position(&self) -> &Rect {
744 | &self.position
745 | }
746 |
747 | fn index(&self) -> usize {
748 | self.num_widget_index
749 | }
750 |
751 | fn set_index(&mut self, idx: usize) {
752 | self.num_widget_index = idx;
753 | }
754 |
755 | fn num_widget(&self) -> usize {
756 | self.num_widget
757 | }
758 |
759 | fn set_num_widget(&mut self, num: usize) {
760 | self.num_widget = num;
761 | }
762 | }
763 |
764 | #[derive(Clone, Deserialize, Serialize)]
765 | pub enum SaveableWidget {
766 | TextWidget(TextWidget),
767 | ImageWidget(ImageWidget),
768 | }
769 |
770 | //老版本
771 | pub mod v10{
772 | use super::*;
773 |
774 | #[derive(Clone, Deserialize, Serialize)]
775 | pub enum SaveableWidget {
776 | TextWidget(super::TextWidget),
777 | ImageWidget(ImageWidget),
778 | }
779 |
780 | #[derive(Clone, Deserialize, Serialize)]
781 | pub struct ImageWidget {
782 | pub id: String,
783 | pub image_data: ImageData,
784 | pub rotation: f32,
785 | pub position: Rect,
786 | pub type_name: String,
787 | pub frame_index: usize,
788 | //是否为纯色
789 | pub color: Option<[u8; 4]>,
790 | pub num_widget_index: usize,
791 | // 一共有多少个当前类型的组件
792 | pub num_widget: usize,
793 | }
794 |
795 | impl Widget for ImageWidget {
796 | fn draw(&mut self, context: &mut OffscreenCanvas) {
797 | if let Some(color) = self.color.as_ref() {
798 | let rect = offscreen_canvas::Rect::from(
799 | self.position.left,
800 | self.position.top,
801 | self.position.width(),
802 | self.position.height(),
803 | );
804 | context.fill_rect(rect, Rgba(*color));
805 | }else {
806 | if self.frame_index >= self.image_data.frames.len(){
807 | self.frame_index = self.image_data.frames.len()-1;
808 | }
809 | let image = RgbaImage::from_raw(
810 | self.image_data.width,
811 | self.image_data.height,
812 | self.image_data.frames[self.frame_index].clone(),
813 | ).unwrap_or(RgbaImage::new(30, 30));
814 | let src =
815 | offscreen_canvas::Rect::new(0, 0, image.width() as i32, image.height() as i32);
816 | let pos = offscreen_canvas::Rect::from(
817 | self.position.left,
818 | self.position.top,
819 | self.position.width(),
820 | self.position.height(),
821 | );
822 |
823 | if self.rotation == 0.{
824 | //不旋转
825 | context.draw_image_with_src_and_dst(&image, &src, &pos, FilterType::Nearest);
826 | }else{
827 | let option = RotateOption::from(
828 | (
829 | self.position.width() as f32 / 2.,
830 | self.position.height() as f32 / 2.,
831 | ),
832 | degrees_to_radians(self.rotation),
833 | );
834 | context.draw_image_with_src_and_dst_and_rotation(&image, &src, &pos, option);
835 | }
836 | self.frame_index += 1;
837 | if self.frame_index >= self.image_data.frames.len() {
838 | self.frame_index = 0;
839 | }
840 | }
841 | }
842 |
843 | fn id(&self) -> &str {
844 | &self.id
845 | }
846 |
847 | fn position_mut(&mut self) -> &mut Rect {
848 | &mut self.position
849 | }
850 |
851 | fn type_name(&self) -> &str {
852 | &self.type_name
853 | }
854 |
855 | fn as_any_mut(&mut self) -> &mut dyn Any {
856 | self
857 | }
858 |
859 | fn position(&self) -> &Rect {
860 | &self.position
861 | }
862 |
863 | fn index(&self) -> usize {
864 | self.num_widget_index
865 | }
866 |
867 | fn set_index(&mut self, idx: usize) {
868 | self.num_widget_index = idx;
869 | }
870 |
871 | fn num_widget(&self) -> usize {
872 | self.num_widget
873 | }
874 |
875 | fn set_num_widget(&mut self, num: usize) {
876 | self.num_widget = num;
877 | }
878 | }
879 | }
--------------------------------------------------------------------------------
/src/wifi_screen.rs:
--------------------------------------------------------------------------------
1 | use std::{net::{Ipv4Addr, TcpStream}, sync::Mutex, time::{Duration, Instant}};
2 |
3 | use crossbeam_channel::{bounded, Receiver, Sender};
4 | use fast_image_resize::{images::Image, Resizer};
5 | use image::{buffer::ConvertBuffer, imageops::overlay, RgbImage, RgbaImage};
6 | use log::info;
7 | use once_cell::sync::Lazy;
8 | use anyhow::{anyhow, Result};
9 | use serde::{Deserialize, Serialize};
10 | use tungstenite::{connect, stream::MaybeTlsStream, WebSocket};
11 |
12 | use crate::rgb565::rgb888_to_rgb565_be;
13 |
14 | #[derive(Serialize, Deserialize, Debug)]
15 | struct DisplayConfig{
16 | display_type: Option,
17 | rotated_width: u32,
18 | rotated_height: u32
19 | }
20 |
21 | pub enum Message{
22 | Connect(String),
23 | Disconnect,
24 | Image(RgbaImage)
25 | }
26 |
27 | #[derive(Debug, Clone)]
28 | pub struct StatusInfo{
29 | pub ip: Option,
30 | pub status: Status,
31 | pub delay_ms: u64,
32 | }
33 |
34 | #[derive(Debug, Clone)]
35 | pub enum Status{
36 | NotConnected,
37 | Connected,
38 | ConnectFail,
39 | Disconnected,
40 | Connecting,
41 | }
42 |
43 | impl Status{
44 | pub fn name(&self) -> &str{
45 | match self{
46 | Status::NotConnected => "未连接",
47 | Status::Connected => "连接成功",
48 | Status::ConnectFail => "连接失败",
49 | Status::Disconnected => "连接断开",
50 | Status::Connecting => "正在连接",
51 | }
52 | }
53 | }
54 |
55 | static CONFIG: Lazy)>> = Lazy::new(|| {
56 | let (sender, recv) = bounded(1);
57 | let _ = std::thread::spawn(move ||{
58 | start(recv);
59 | });
60 | Mutex::new((StatusInfo{
61 | ip: None,
62 | status: Status::NotConnected,
63 | delay_ms: 150,
64 | }, sender))
65 | });
66 |
67 | fn set_status(ip: Option, status: Status) -> Result<()>{
68 | let mut config = CONFIG.lock().map_err(|err| anyhow!("{err:?}"))?;
69 | config.0.status = status;
70 | config.0.ip = ip;
71 | Ok(())
72 | }
73 |
74 | pub fn set_delay_ms(delay_ms: u64) -> Result<()>{
75 | let mut config = CONFIG.lock().map_err(|err| anyhow!("{err:?}"))?;
76 | config.0.delay_ms = delay_ms;
77 | Ok(())
78 | }
79 |
80 | pub fn send_message(msg: Message) -> Result<()>{
81 | let sender = {
82 | let config = CONFIG.lock().map_err(|err| anyhow!("{err:?}"))?;
83 | let s = config.1.clone();
84 | drop(config);
85 | s
86 | };
87 | sender.send(msg)?;
88 | Ok(())
89 | }
90 |
91 | pub fn try_send_message(msg: Message) -> Result<()>{
92 | let config = CONFIG.lock().map_err(|err| anyhow!("{err:?}"))?;
93 | config.1.try_send(msg)?;
94 | Ok(())
95 | }
96 |
97 | pub fn get_status() -> Result{
98 | let config = CONFIG.lock().map_err(|err| anyhow!("{err:?}"))?;
99 | Ok(config.0.clone())
100 | }
101 |
102 | fn get_display_config(ip: &str) -> Result{
103 | //获取显示器大小
104 | let resp = reqwest::blocking::Client::builder()
105 | .timeout(Duration::from_secs(2))
106 | .build()?
107 | .get(&format!("http://{ip}/display_config"))
108 | .send()?
109 | .json::()?;
110 | Ok(resp)
111 | }
112 |
113 | fn start(receiver: Receiver){
114 | let mut socket: Option>> = None;
115 | let mut screen_ip = String::new();
116 |
117 | println!("启动upload线程...");
118 |
119 | let mut display_config = None;
120 | let mut connected = false;
121 |
122 | loop{
123 | match receiver.recv(){
124 | Ok(msg) => {
125 | match msg{
126 | Message::Disconnect => {
127 | screen_ip = String::new();
128 | if let Ok(mut cfg) = CONFIG.lock(){
129 | cfg.0.status = Status::Disconnected
130 | }
131 | if let Some(mut s) = socket.take(){
132 | let _ = s.close(None);
133 | }
134 | }
135 | Message::Connect(ip) => {
136 | screen_ip = ip.clone();
137 | if let Ok(cfg) = get_display_config(&ip){
138 | display_config = Some(cfg);
139 | }else{
140 | eprintln!("display config获取失败!");
141 | }
142 | println!("接收到 serverIP...");
143 | connected = connect_socket(ip, &mut socket).is_ok();
144 | }
145 | Message::Image(mut image) => {
146 | let delay_ms = {
147 | if let Ok(mut cfg) = CONFIG.try_lock(){
148 | cfg.0.status = if connected{
149 | Status::Connected
150 | }else{
151 | Status::Disconnected
152 | };
153 | let v = cfg.0.delay_ms;
154 | drop(cfg);
155 | v
156 | }else{
157 | 150
158 | }
159 | };
160 | if display_config.is_none(){
161 | match get_display_config(&screen_ip){
162 | Ok(cfg) => {
163 | display_config = Some(cfg);
164 | }
165 | Err(err) => {
166 | eprintln!("Message::Image display config获取失败!");
167 | eprintln!("err:?");
168 | std::thread::sleep(Duration::from_secs(3));
169 | let screen_ip_clone = screen_ip.clone();
170 | std::thread::spawn(move ||{
171 | let r = send_message(Message::Connect(screen_ip_clone));
172 | println!("重新连接 SetIp {r:?}...");
173 | });
174 | }
175 | }
176 | }
177 | let (dst_width, dst_height) = match display_config.as_ref(){
178 | Some(c) => (c.rotated_width, c.rotated_height),
179 | None => continue,
180 | };
181 |
182 | //检查socket 是否断开
183 |
184 | if let Some(s) = socket.as_mut(){
185 | if s.can_write(){
186 | connected = true;
187 | }
188 | }
189 | if connected{
190 | if let Some(s) = socket.as_mut(){
191 | let t1 = Instant::now();
192 | //压缩
193 | let img = match fast_resize(&mut image, dst_width, dst_height){
194 | Ok(v) => v,
195 | Err(err) => {
196 | eprintln!("图片压缩失败:{}", err.root_cause());
197 | continue;
198 | }
199 | };
200 | let out = rgb888_to_rgb565_be(&img, img.width() as usize, img.height() as usize);
201 | let out = lz4_flex::compress_prepend_size(&out);
202 | println!("resize+转rgb565+lz4压缩:{}ms {}bytes {}x{}", t1.elapsed().as_millis(), out.len(), img.width(), img.height());
203 |
204 | //发送
205 | let ret1 = s.write(tungstenite::Message::Binary(out.into()));
206 | let ret2 = s.flush();
207 | if ret1.is_err() && ret2.is_err(){
208 | info!("ws write:{ret1:?}");
209 | info!("ws flush:{ret2:?}");
210 | connected = false;
211 | let _ = socket.take();
212 | }
213 | std::thread::sleep(Duration::from_millis(delay_ms));
214 | }
215 | }else{
216 | if let Some(mut s) = socket.take(){
217 | let _ = s.close(None);
218 | }
219 | let _ = set_status(None, Status::Disconnected);
220 | //3秒后重连
221 | println!("连接断开 3秒后重连:{screen_ip}");
222 | if screen_ip.len() > 0{
223 | std::thread::sleep(Duration::from_secs(3));
224 | let screen_ip_clone = screen_ip.clone();
225 | std::thread::spawn(move ||{
226 | let r = send_message(Message::Connect(screen_ip_clone));
227 | println!("重新连接 SetIp {r:?}...");
228 | });
229 | }
230 | }
231 | }
232 | }
233 | }
234 | Err(_err) => {
235 | std::thread::sleep(Duration::from_millis(10));
236 | }
237 | }
238 | }
239 | }
240 |
241 | fn connect_socket(ip: String, old_socket: &mut Option>>) -> Result<()>{
242 | //关闭原有连接
243 | if let Some(mut s) = old_socket.take(){
244 | let _ = s.close(None);
245 | }
246 | let _ = set_status(Some(ip.clone()), Status::Connecting);
247 | let url = format!("ws://{ip}/ws");
248 | println!("开始连接:{url}");
249 | if let Ok((s, _resp)) = connect(url){
250 | *old_socket = Some(s);
251 | let ret = set_status(None, Status::Connected);
252 | println!("连接成功{ip}.. 设置状态:{ret:?}");
253 | }else{
254 | println!("连接失败{ip}..");
255 | let _ = set_status(None, Status::ConnectFail);
256 | }
257 | Ok(())
258 | }
259 |
260 | fn fast_resize(src: &mut RgbaImage, dst_width: u32, dst_height: u32) -> Result{
261 | let mut dst_image = Image::new(
262 | dst_width,
263 | dst_height,
264 | fast_image_resize::PixelType::U8x3,
265 | );
266 | let mut src:RgbImage = src.convert();
267 | if src.width() != dst_width || src.height() != dst_height{
268 | let v = Image::from_slice_u8(src.width(), src.height(), src.as_mut(), fast_image_resize::PixelType::U8x3)?;
269 | let mut resizer = Resizer::new();
270 | resizer.resize(&v, &mut dst_image, None)?;
271 | Ok(RgbImage::from_raw(dst_image.width(), dst_image.height(), dst_image.buffer().to_vec()).unwrap())
272 | }else{
273 | Ok(src.convert())
274 | }
275 | }
276 |
277 | //获取wifi屏幕参数,测试是否可以连接成功
278 | pub fn test_screen_sync(ip: String) -> Result<()>{
279 | let resp = reqwest::blocking::get(&format!("http://{ip}/display_config"))?
280 | .json::()?;
281 | println!("屏幕大小:{}x{}", resp.rotated_width, resp.rotated_height);
282 | //显示hello
283 | let json = r#"[{"Rectangle":{"fill_color":"black","height":240,"width":240,"stroke_width":0,"left":0,"top":0}},{"Text":{"color":"white","size":20,"text":"Hello!","x":10,"y":15}},{"Text":{"color":"white","size":20,"text":"USB Screen","x":10,"y":40}}]"#;
284 | //绘制
285 | let _resp = reqwest::blocking::Client::new()
286 | .post(&format!("http://{ip}/draw_canvas"))
287 | .body(json.as_bytes())
288 | .send()?
289 | .text()?;
290 | Ok(())
291 | }
--------------------------------------------------------------------------------
/src/yuv422.rs:
--------------------------------------------------------------------------------
1 | use anyhow::{anyhow, Result};
2 | use v4l::FourCC;
3 |
4 | pub const YUYV:FourCC = FourCC{repr: [89, 85, 89, 86] };
5 | pub const RGB3:FourCC = FourCC{repr: [82, 71, 66, 51] };
6 | pub const MJPG:FourCC = FourCC{repr: [77, 74, 80, 71] };
7 |
8 | // For those maintaining this, I recommend you read: https://docs.microsoft.com/en-us/windows/win32/medfound/recommended-8-bit-yuv-formats-for-video-rendering#yuy2
9 | // https://en.wikipedia.org/wiki/YUV#Converting_between_Y%E2%80%B2UV_and_RGB
10 | // and this too: https://stackoverflow.com/questions/16107165/convert-from-yuv-420-to-imagebgr-byte
11 | // The YUY2(YUYV) format is a 16 bit format. We read 4 bytes at a time to get 6 bytes of RGB888.
12 | // First, the YUY2 is converted to YCbCr 4:4:4 (4:2:2 -> 4:4:4)
13 | // then it is converted to 6 bytes (2 pixels) of RGB888
14 | /// Converts a YUYV 4:2:2 datastream to a RGB888 Stream. [For further reading](https://en.wikipedia.org/wiki/YUV#Converting_between_Y%E2%80%B2UV_and_RGB)
15 | /// # Errors
16 | /// This may error when the data stream size is not divisible by 4, a i32 -> u8 conversion fails, or it fails to read from a certain index.
17 | #[inline]
18 | pub fn yuyv422_to_rgb(data: &[u8]) -> Result> {
19 | let pixel_size = 3;
20 | // yuyv yields 2 3-byte pixels per yuyv chunk
21 | let rgb_buf_size = (data.len() / 4) * (2 * pixel_size);
22 |
23 | let mut dest = vec![0; rgb_buf_size];
24 | buf_yuyv422_to_rgb(data, &mut dest)?;
25 |
26 | Ok(dest)
27 | }
28 |
29 | /// Same as [`yuyv422_to_rgb`] but with a destination buffer instead of a return `Vec`
30 | /// # Errors
31 | /// If the stream is invalid YUYV, or the destination buffer is not large enough, this will error.
32 | #[inline]
33 | pub fn buf_yuyv422_to_rgb(data: &[u8], dest: &mut [u8]) -> Result<()> {
34 | if data.len() % 4 != 0 {
35 | return Err(anyhow!("Assertion failure, the YUV stream isn't 4:2:2! (wrong number of bytes)"));
36 | }
37 |
38 | let pixel_size = 3;
39 | // yuyv yields 2 3-byte pixels per yuyv chunk
40 | let rgb_buf_size = (data.len() / 4) * (2 * pixel_size);
41 |
42 | if dest.len() != rgb_buf_size {
43 | return Err(anyhow!(format!("Assertion failure, the destination RGB buffer is of the wrong size! [expected: {rgb_buf_size}, actual: {}]", dest.len())));
44 | }
45 |
46 | let iter = data.chunks_exact(4);
47 |
48 | let mut iter = iter
49 | .flat_map(|yuyv| {
50 | let y1 = i32::from(yuyv[0]);
51 | let u = i32::from(yuyv[1]);
52 | let y2 = i32::from(yuyv[2]);
53 | let v = i32::from(yuyv[3]);
54 | let pixel1 = yuyv444_to_rgb(y1, u, v);
55 | let pixel2 = yuyv444_to_rgb(y2, u, v);
56 | [pixel1, pixel2]
57 | })
58 | .flatten();
59 |
60 | for i in dest.iter_mut().take(rgb_buf_size) {
61 | *i = match iter.next() {
62 | Some(v) => v,
63 | None => {
64 | return Err(anyhow!("Ran out of RGB YUYV values! (this should not happen, please file an issue: l1npengtul/nokhwa)"))
65 | }
66 | }
67 | }
68 |
69 | Ok(())
70 | }
71 |
72 | // equation from https://en.wikipedia.org/wiki/YUV#Converting_between_Y%E2%80%B2UV_and_RGB
73 | /// Convert `YCbCr` 4:4:4 to a RGB888. [For further reading](https://en.wikipedia.org/wiki/YUV#Converting_between_Y%E2%80%B2UV_and_RGB)
74 | #[allow(clippy::many_single_char_names)]
75 | #[allow(clippy::cast_possible_truncation)]
76 | #[allow(clippy::cast_sign_loss)]
77 | #[must_use]
78 | #[inline]
79 | pub fn yuyv444_to_rgb(y: i32, u: i32, v: i32) -> [u8; 3] {
80 | let c298 = (y - 16) * 298;
81 | let d = u - 128;
82 | let e = v - 128;
83 | let r = (c298 + 409 * e + 128) >> 8;
84 | let g = (c298 - 100 * d - 208 * e + 128) >> 8;
85 | let b = (c298 + 516 * d + 128) >> 8;
86 | [clamp_255(r), clamp_255(g), clamp_255(b)]
87 | }
88 |
89 | #[inline]
90 | pub fn clamp_255(i: i32) -> u8{
91 | if i>255{
92 | 255
93 | }else if i<0{
94 | 0
95 | }else{
96 | i as u8
97 | }
98 | }
--------------------------------------------------------------------------------
/view/widgets/abutton.slint:
--------------------------------------------------------------------------------
1 |
2 | import { HorizontalBox } from "std-widgets.slint";
3 | export component AButton inherits Rectangle {
4 | border-color: self.enabled? #ccc : focus.has-focus?white: #9b9b9b;
5 | border-radius: 2px;
6 | border-width: 1px;
7 | background: touch.pressed?#666 : touch.has-hover? #454545 : #333;
8 | in-out property icon;
9 | in-out property icon-size: 0px;
10 | in-out property icon-opacity: 1.0;
11 | in-out property icon-colorize: white;
12 | in-out property use-icon;
13 | in-out property text <=> text.text;
14 | in-out property text-color: white;
15 | in-out property enabled: true;
16 | callback clicked();
17 | if use-icon : image := Image{
18 | x: 5px;
19 | colorize: icon-colorize;
20 | opacity: icon-opacity;
21 | width: icon-size == 0? 12px : icon-size;
22 | source: icon;
23 | }
24 | text := Text {
25 | x: use-icon ? ( icon-size==0? 20px : icon-size+8px ) : root.width/2-self.width/2;
26 | color: text-color;
27 | text: "Button";
28 | }
29 | focus := FocusScope {
30 | touch := TouchArea {
31 | clicked => {
32 | if(enabled){
33 | clicked();
34 | }
35 | }
36 | }
37 | }
38 | }
39 |
40 | export component SmallButton inherits Rectangle {
41 | border-color: self.enabled? #ccc : focus.has-focus?white: #9b9b9b;
42 | border-radius: 2px;
43 | border-width: 1px;
44 | background: touch.pressed?#666 : touch.has-hover? #454545 : #333;
45 | in-out property icon;
46 | in-out property icon-size: 0px;
47 | in-out property icon-opacity: 1.0;
48 | in-out property icon-colorize: white;
49 | in-out property use-icon;
50 | in-out property text <=> text.text;
51 | in-out property text-color: white;
52 | in-out property enabled: true;
53 | callback clicked();
54 | if use-icon : image := Image{
55 | x: 5px;
56 | colorize: icon-colorize;
57 | opacity: icon-opacity;
58 | width: icon-size == 0? 12px : icon-size;
59 | source: icon;
60 | }
61 | text := Text {
62 | x: use-icon ? ( icon-size==0? 20px : icon-size+8px ) : root.width/2-self.width/2;
63 | color: text-color;
64 | font-size: 10px;
65 | text: "Button";
66 | }
67 | focus := FocusScope {
68 | touch := TouchArea {
69 | clicked => {
70 | if(enabled){
71 | clicked();
72 | }
73 | }
74 | }
75 | }
76 | }
77 |
78 | export component AButtonWhite inherits AButton {
79 | //默认背景fdfdfd 边框 d0d0d0
80 | //鼠标指向e0eef9 边框 0078d4
81 | //按下背景cce4f7 边框 005499
82 | background: touch.pressed?#cce4f7 : touch.has-hover? #e0eef9 : #fdfdfd;
83 | border-color: touch.pressed?#005499 : touch.has-hover? #0078d4 : #d0d0d0;
84 | text-color: black;
85 | border-radius: 3px;
86 | touch := TouchArea {
87 | width: 100%;
88 | height: 100%;
89 | }
90 | }
--------------------------------------------------------------------------------
/view/widgets/widgets.slint:
--------------------------------------------------------------------------------
1 | import { ListView, StandardButton, VerticalBox, HorizontalBox, Slider } from "std-widgets.slint";
2 | import { AButton, AButtonWhite } from "abutton.slint";
3 | export component Span10px inherits Rectangle {
4 | width: 10px;
5 | height: 10px;
6 | }
7 |
8 |
9 | export struct Screen {
10 | name: string,
11 | width: length,
12 | height: length,
13 | }
14 |
15 | // 组件类型
16 | export struct WidgetType {
17 | name: string,
18 | icon: image,
19 | text: string,
20 | }
21 |
22 | // 组件可编辑属性
23 | export struct WidgetConfig{
24 | uuid: string,
25 | name: string,
26 | x: int,
27 | y: int,
28 | text: string,
29 | text-size: int,
30 | prefix: string,
31 | rotation: float,
32 | width: int,
33 | height: int,
34 | color_str: string,
35 | color: color,
36 | image: image,
37 | }
38 |
39 | // 对象列表
40 | export struct WidgetObject{
41 | index: int, //索引
42 | uuid: string, //uuid
43 | name: string, //名字 (图像/文本
44 | type_name: string, // text文本 images图像
45 | text: string,
46 | prefix: string,
47 | tag1: string,
48 | tag2: string,
49 | }
50 |
51 | export component Toast inherits Rectangle {
52 | width: 100%;
53 | height: 100%;
54 | background: rgba(0, 0, 0, 0.5);
55 | in-out property message: "请稍候...";
56 | TouchArea {
57 | width: 100%;
58 | height: 100%;
59 | clicked => {}
60 | Rectangle {
61 | HorizontalBox {
62 | alignment: center;
63 | VerticalBox {
64 | alignment: center;
65 | Rectangle {
66 | min-height: 100px;
67 | min-width: 200px;
68 | border-radius: 10px;
69 | background: #333;
70 | Text {
71 | color: white;
72 | text: message;
73 | }
74 | }
75 | }
76 | }
77 | width: 100%;
78 | height: 100%;
79 | }
80 | }
81 | }
82 |
83 |
84 | export component ConfirmDialog inherits Rectangle {
85 | in-out property title: "温馨提示";
86 | in-out property message: "确定要进行此操作吗?";
87 | callback on-close(bool);
88 |
89 | background: #f0f0f0;
90 | border-width: 1px;
91 | border-color: rgba(0, 0, 0, 60);
92 | border-radius: 10px;
93 | clip: true;
94 |
95 | VerticalLayout {
96 | padding: 1px;
97 | Rectangle {
98 | background: #f1f3f9;
99 | height: 28px;
100 | Text { text: title; x: 10px; color:black; horizontal-alignment: left; }
101 | }
102 | Rectangle {
103 | background: white;
104 | Text { text: message; min-width: 260px; min-height: 60px; vertical-alignment: center; horizontal-alignment: center; color:black;}
105 | }
106 | Rectangle { height: 1px; background: #dfdfdf; }
107 | HorizontalBox {
108 | padding-top: 5px;
109 | padding-bottom: 5px;
110 | height: 42px;
111 | AButtonWhite { text: "确定"; height: 25px; clicked => { on-close(true) } }
112 | AButtonWhite { text: "取消"; height: 25px; clicked => { on-close(false) } }
113 | }
114 | }
115 | }
116 |
117 |
118 | export component GradientSlider inherits Rectangle {
119 | in-out property maximum: 100;
120 | in-out property minimum: 0;
121 | in-out property value;
122 |
123 | callback value-changed(float);
124 |
125 | min-height: 100px;
126 | min-width: 24px;
127 | horizontal-stretch: 0;
128 | vertical-stretch: 1;
129 |
130 | border-radius: root.width/2;
131 | background: @linear-gradient(180deg, #612efe 0%, black 100%);
132 | border-width: 3px;
133 | border-color: #bbbbbb;
134 |
135 | handle := Rectangle {
136 | width: parent.width;
137 | height: 20px;
138 | y: (root.height - handle.height) * (root.value - root.minimum)/(root.maximum - root.minimum);
139 | Rectangle {
140 | width: parent.width+8px;
141 | border-width: 7px;
142 | border-color: white;
143 | Rectangle {
144 | width: parent.width - 5px;
145 | height: parent.height - 5px;
146 | border-width: 3px;
147 | border-color: #4c4c4c;
148 | }
149 | }
150 | }
151 | touch := TouchArea {
152 | property pressed-value;
153 | pointer-event(event) => {
154 | if (event.button == PointerEventButton.left && event.kind == PointerEventKind.down) {
155 | self.pressed-value = root.value;
156 | }
157 | }
158 | clicked => {
159 | root.value = max(root.minimum, min(root.maximum,
160 | (touch.mouse-y - handle.height/2) * (root.maximum - root.minimum) / (root.height - handle.height)));
161 | value-changed(root.value);
162 | }
163 | moved => {
164 | if (self.enabled && self.pressed) {
165 | root.value = max(root.minimum, min(root.maximum,
166 | self.pressed-value + (touch.mouse-y - touch.pressed-y) * (root.maximum - root.minimum) / (root.height - handle.height)));
167 | value-changed(root.value);
168 | }
169 | }
170 | }
171 | }
172 |
173 | export component ColorPicker inherits Rectangle {
174 | width: 300px;
175 | height: 221px;
176 | //背景图片用户取色
177 | in-out property background-image: @image-url("../../images/picker.png");
178 | //颜色拾取后,设置滚动条背景渐变色
179 | in-out property slider-color <=> slider.background;
180 |
181 | //回调函数,拾取了像素坐标,在代码中获取坐标处颜色
182 | callback choose-color(length, length);
183 | //回调函数,设置颜色亮度
184 | callback choose-brightness(float);
185 | callback on-click-close();
186 |
187 | Image {
188 | width: 100%;
189 | height: 100%;
190 | source: background-image;
191 | }
192 | slider := GradientSlider{
193 | y: 14px;
194 | x: 260px;
195 | width: 30px;
196 | height: 132px;
197 | border-radius: 1px;
198 | value-changed => {
199 | choose-brightness(slider.value);
200 | }
201 | }
202 | crosshair := Image {
203 | width: 30px;
204 | height: 30px;
205 | source: @image-url("../../images/crosschair.png");
206 | }
207 | Rectangle {
208 | width: 20px;
209 | height: 20px;
210 | background: close-touch.pressed? #ccc: close-touch.has-hover? rgb(220, 73, 73) : rgb(255, 73, 73);
211 | border-radius: 10px;
212 | border-width: 2px;
213 | border-color: white;
214 | x: root.width - self.width/1.5;
215 | y: - self.height/3;
216 | Text {
217 | color: white;
218 | text: "❌";
219 | font-size: 8px;
220 | y: 7px;
221 | }
222 | close-touch := TouchArea {
223 | clicked => {on-click-close()}
224 | }
225 | }
226 | Rectangle {
227 | x: 16px;
228 | y: 17px;
229 | border-width: 0.4px;
230 | width: 229px;
231 | height: 126px;
232 | touch := TouchArea {
233 | clicked => {
234 | crosshair.x = touch.mouse-x - crosshair.width/2 + parent.x;
235 | crosshair.y = touch.mouse-y - crosshair.height/2 + parent.y;
236 | choose-color(parent.x+crosshair.x, parent.y + crosshair.y);
237 | }
238 | moved => {
239 | if(touch.pressed){
240 | crosshair.x = touch.mouse-x - crosshair.width/2 + parent.x;
241 | crosshair.y = touch.mouse-y - crosshair.height/2 + parent.y;
242 | if(crosshair.x < parent.x - crosshair.width/2){
243 | crosshair.x = 0;
244 | }
245 | if(crosshair.y < parent.y - crosshair.height/2){
246 | crosshair.y = 0;
247 | }
248 | if(crosshair.x > parent.width){
249 | crosshair.x = parent.width;
250 | }
251 | if(crosshair.y > parent.height){
252 | crosshair.y = parent.height;
253 | }
254 | choose-color(parent.x+crosshair.x, parent.y + crosshair.y);
255 | }
256 | }
257 | }
258 | }
259 | Rectangle {
260 | x: 16px;
261 | y: 160px;
262 | border-width: 1px;
263 | width: 270px;
264 | height: 45px;
265 | touch1 := TouchArea {
266 | clicked => {
267 | choose-color(parent.x+touch1.mouse-x, parent.y + touch1.mouse-y);
268 | }
269 | }
270 | }
271 | }
--------------------------------------------------------------------------------