├── .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 | ![rp2040.png](images/rp2040.png) 29 | ![st7735.jpg](images/st7735.png) 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 | ![st7789.jpg](images/st7789.png) 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 | } --------------------------------------------------------------------------------