change - 添加 HoistServer、HikRoBotServer启动项,由 UI 控制启动停止

dev
WenJY 16 hours ago
parent b54bad1fbb
commit d065a2b9ba

@ -17,7 +17,9 @@
"Bash(sed -i '' '/System.Console.WriteLine/d' ViewModels/Device/DeviceHostViewModel.cs)", "Bash(sed -i '' '/System.Console.WriteLine/d' ViewModels/Device/DeviceHostViewModel.cs)",
"Bash(sed -i '' '/System.Console.WriteLine/d' Views/Device/DeviceHostListView.axaml.cs)", "Bash(sed -i '' '/System.Console.WriteLine/d' Views/Device/DeviceHostListView.axaml.cs)",
"Bash(git status *)", "Bash(git status *)",
"Bash(dotnet /Users/wenxiansheng/.nuget/packages/avalonia/11.1.5/lib/netstandard2.0/Avalonia.Base.dll type list)" "Bash(dotnet /Users/wenxiansheng/.nuget/packages/avalonia/11.1.5/lib/netstandard2.0/Avalonia.Base.dll type list)",
"Bash(dotnet new *)",
"Bash(dotnet add *)"
] ]
} }
} }

@ -0,0 +1,104 @@
using System.Reflection;
using Com.Ctrip.Framework.Apollo;
using Sln.Wcs;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;
using NeoSmart.Caching.Sqlite;
using Newtonsoft.Json;
using Sln.Wcs.HikRoBotAdapter.Domain.Dto.GbTaskSubmit;
using Sln.Wcs.HikRoBotAdapter.Service;
using Sln.Wcs.HikRoBotDispatcher;
using Sln.Wcs.Model.Domain;
using Sln.Wcs.Repository;
using Sln.Wcs.Repository.service;
using Sln.Wcs.Serilog;
using ZiggyCreatures.Caching.Fusion;
using ZiggyCreatures.Caching.Fusion.Serialization.NewtonsoftJson;
var builder = WebApplication.CreateBuilder(args);
var basePath = AppContext.BaseDirectory;
// ---- 配置 ----
var localConfig = new ConfigurationBuilder()
.SetBasePath(basePath)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.Build();
var apolloConfigSection = localConfig.GetSection("apollo");
builder.Services.AddSingleton<IConfiguration>(localConfig);
var configProvider = new UpdateableConfigProvider();
configProvider.Set("PLC参数", "");
var apolloConfig = new ConfigurationBuilder()
.SetBasePath(basePath)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddApollo(apolloConfigSection)
.AddDefault()
.Add(configProvider)
.Build();
builder.Services.Remove(new ServiceDescriptor(typeof(IConfiguration), localConfig));
builder.Services.AddSingleton<IConfiguration>(apolloConfig);
// ---- DI 扫描 ----
var assemblies = new[]
{
Assembly.LoadFrom(Path.Combine(basePath, "Sln.Wcs.Common.dll")),
Assembly.LoadFrom(Path.Combine(basePath, "Sln.Wcs.Cache.dll")),
Assembly.LoadFrom(Path.Combine(basePath, "Sln.Wcs.Repository.dll")),
Assembly.LoadFrom(Path.Combine(basePath, "Sln.Wcs.HikRoBotSdk.dll")),
Assembly.LoadFrom(Path.Combine(basePath, "Sln.Wcs.HikRoBotAdapter.dll")),
Assembly.LoadFrom(Path.Combine(basePath, "Sln.Wcs.HikRoBotDispatcher.dll")),
};
builder.Services.Scan(scan => scan.FromAssemblies(assemblies)
.AddClasses().AsImplementedInterfaces().AsSelf().WithTransientLifetime());
builder.Services.AddSingleton(typeof(SerilogHelper));
builder.Services.AddSqlSugarSetup();
builder.Services.AddFusionCache()
.WithSerializer(new FusionCacheNewtonsoftJsonSerializer())
.WithDistributedCache(new SqliteCache(new SqliteCacheOptions { CachePath = apolloConfig["cachePath"]! }));
// ---- Swagger ----
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
c.SwaggerDoc("v1", new OpenApiInfo { Title = "HikRobot Engine API", Version = "v1" }));
var app = builder.Build();
// ---- 启动初始化 ----
var sp = app.Services;
sp.UseSerilogExtensions();
var log = sp.GetRequiredService<SerilogHelper>();
log.Info($"HikRoBotServer 启动, 日志:{apolloConfig["logPath"]}");
app.UseSwagger();
app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "HikRobot Engine v1"); c.RoutePrefix = "swagger"; });
// ---- HikRoBotDispatchHub.ReciveTask ----
var hub = sp.GetRequiredService<HikRoBotDispatchHub>();
var api = app.MapGroup("/api");
// Hub 方法: ReciveTask
api.MapPost("/task/receive", (ReceiveTaskRequest req) =>
{
var detail = new LiveTaskDetail
{
taskCode = req.TaskCode, deviceType = 1, taskStatus = 1,
startPoint = req.StartPoint, endPoint = req.EndPoint
};
hub.ReciveTask(detail);
return Results.Ok(new { success = true });
});
api.MapGet("/health", () => Results.Ok(new { time = DateTime.Now, status = "ok" }));
log.Info("HikRoBotServer 就绪: http://localhost:5200/swagger");
app.Run();
record ReceiveTaskRequest(string TaskCode, string StartPoint, string EndPoint);

@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<ItemGroup>
<ProjectReference Include="..\Sln.Wcs.HikRoBotDispatcher\Sln.Wcs.HikRoBotDispatcher.csproj" />
<ProjectReference Include="..\Sln.Wcs.Repository\Sln.Wcs.Repository.csproj" />
<ProjectReference Include="..\Sln.Wcs.Serilog\Sln.Wcs.Serilog.csproj" />
<ProjectReference Include="..\Sln.Wcs.Cache\Sln.Wcs.Cache.csproj" />
<ProjectReference Include="..\Sln.Wcs.Common\Sln.Wcs.Common.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Com.Ctrip.Framework.Apollo" Version="2.11.0" />
<PackageReference Include="Com.Ctrip.Framework.Apollo.Configuration" Version="2.11.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="Scrutor" Version="7.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>

@ -0,0 +1,14 @@
using Microsoft.Extensions.Configuration;
namespace Sln.Wcs;
internal class UpdateableConfigProvider : ConfigurationProvider, IConfigurationSource
{
public new void Set(string key, string value)
{
base.Set(key, value);
OnReload();
}
public IConfigurationProvider Build(IConfigurationBuilder builder) => this;
}

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

@ -0,0 +1,18 @@
{
"exclude": [
"**/bin",
"**/bower_components",
"**/jspm_packages",
"**/node_modules",
"**/obj",
"**/platforms"
],
"apollo": {
"AppId": "SlnWcs",
"Env": "DEV",
"MetaServer": "http://119.45.202.115:4320", // 4310
"ConfigServer": [
"http://119.45.202.115:4320"
]
}
}

@ -0,0 +1,151 @@
using System.Reflection;
using Com.Ctrip.Framework.Apollo;
using Sln.Wcs;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;
using NeoSmart.Caching.Sqlite;
using Newtonsoft.Json;
using Sln.Wcs.HoistDispatcher;
using Sln.Wcs.Model.Domain;
using Sln.Wcs.Plc;
using Sln.Wcs.Repository;
using Sln.Wcs.Repository.service;
using Sln.Wcs.Serilog;
using ZiggyCreatures.Caching.Fusion;
using ZiggyCreatures.Caching.Fusion.Serialization.NewtonsoftJson;
var builder = WebApplication.CreateBuilder(args);
var basePath = AppContext.BaseDirectory;
// ---- 配置 ----
var localConfig = new ConfigurationBuilder()
.SetBasePath(basePath)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.Build();
var apolloConfigSection = localConfig.GetSection("apollo");
builder.Services.AddSingleton<IConfiguration>(localConfig);
var configProvider = new UpdateableConfigProvider();
configProvider.Set("PLC参数", "");
var apolloConfig = new ConfigurationBuilder()
.SetBasePath(basePath)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddApollo(apolloConfigSection)
.AddDefault()
.Add(configProvider)
.Build();
builder.Services.Remove(new ServiceDescriptor(typeof(IConfiguration), localConfig));
builder.Services.AddSingleton<IConfiguration>(apolloConfig);
// ---- DI 扫描 ----
var assemblies = new[]
{
Assembly.LoadFrom(Path.Combine(basePath, "Sln.Wcs.Common.dll")),
Assembly.LoadFrom(Path.Combine(basePath, "Sln.Wcs.Cache.dll")),
Assembly.LoadFrom(Path.Combine(basePath, "Sln.Wcs.Repository.dll")),
Assembly.LoadFrom(Path.Combine(basePath, "Sln.Wcs.Plc.dll")),
Assembly.LoadFrom(Path.Combine(basePath, "Sln.Wcs.HoistSdk.dll")),
Assembly.LoadFrom(Path.Combine(basePath, "Sln.Wcs.HoistAdapter.dll")),
Assembly.LoadFrom(Path.Combine(basePath, "Sln.Wcs.HoistDispatcher.dll")),
};
builder.Services.Scan(scan => scan.FromAssemblies(assemblies)
.AddClasses().AsImplementedInterfaces().AsSelf().WithTransientLifetime());
builder.Services.AddSingleton(typeof(SerilogHelper));
builder.Services.AddSqlSugarSetup();
builder.Services.AddPlcSetup();
builder.Services.AddFusionCache()
.WithSerializer(new FusionCacheNewtonsoftJsonSerializer())
.WithDistributedCache(new SqliteCache(new SqliteCacheOptions { CachePath = apolloConfig["cachePath"]! }));
// ---- Swagger ----
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Hoist Engine API", Version = "v1" }));
var app = builder.Build();
// ---- 启动初始化 ----
var sp = app.Services;
sp.UseSerilogExtensions();
var log = sp.GetRequiredService<SerilogHelper>();
log.Info($"HoistServer 启动, 日志:{apolloConfig["logPath"]}");
var deviceInfoService = sp.GetRequiredService<IBaseDeviceInfoService>();
var list = deviceInfoService.GetDeviceInfos(x => x.isFlag == 1).ToList();
configProvider.Set("PLC参数", JsonConvert.SerializeObject(list));
log.Info($"PLC参数已加载, 共{list.Count}台设备");
app.UseSwagger();
app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "Hoist Engine v1"); c.RoutePrefix = "swagger"; });
// ---- HoistDispatchHub 四个方法 ----
var hub = sp.GetRequiredService<HoistDispatchHub>();
var api = app.MapGroup("/api");
// 1. ReceivePallet
api.MapPost("/hoist/receive-pallet", (ReceivePalletRequest req, IBaseDeviceInfoService devSvc) =>
{
var device = devSvc.Query(x => x.deviceType == 2 && x.isFlag == 1)
.FirstOrDefault(d => d.hostCode == req.HostCode);
if (device == null) return Results.Ok(new { success = false, msg = "设备不存在" });
var detail = new LiveTaskDetail
{
taskCode = req.TaskCode, palletBarcode = req.PalletBarcode,
startPoint = req.StartPoint, endPoint = req.EndPoint,
deviceType = 2, taskStatus = 1
};
hub.ReceivePallet(detail, device);
return Results.Ok(new { success = true });
});
// 2. TaskRun
api.MapPost("/hoist/task-run", (TaskRunRequest req, IBaseDeviceInfoService devSvc) =>
{
var device = devSvc.Query(x => x.deviceType == 2 && x.isFlag == 1)
.FirstOrDefault(d => d.hostCode == req.HostCode);
if (device == null) return Results.Ok(new { success = false, msg = "设备不存在" });
device.deviceSerialNo = req.SerialNo;
hub.TaskRun(device);
return Results.Ok(new { success = true });
});
// 3. TaskDispatch
api.MapPost("/task/dispatch", (TaskDispatchRequest req, IBaseDeviceInfoService devSvc) =>
{
var device = devSvc.Query(x => x.deviceType == 2 && x.isFlag == 1)
.FirstOrDefault(d => d.hostCode == req.HostCode);
if (device == null) return Results.Ok(new { success = false, msg = "设备不存在" });
var detail = new LiveTaskDetail
{
taskCode = req.TaskCode, startPoint = req.StartPoint, endPoint = req.EndPoint,
deviceType = 2, taskStatus = 1
};
hub.TaskDispatch(device, detail);
return Results.Ok(new { success = true });
});
// 4. GetFreeHoistAsync
api.MapGet("/hoist/free", async (string hostCode) =>
{
var d = await hub.GetFreeHoistAsync(hostCode);
return Results.Ok(new { found = d != null, d?.deviceCode, d?.deviceName });
});
api.MapGet("/health", () => Results.Ok(new { time = DateTime.Now, status = "ok" }));
log.Info("HoistServer 就绪: http://localhost:5100/swagger");
app.Run();
record ReceivePalletRequest(string HostCode, int SerialNo, string TaskCode, string PalletBarcode, string StartPoint, string EndPoint);
record TaskRunRequest(string HostCode, int SerialNo);
record TaskDispatchRequest(string HostCode, int SerialNo, string TaskCode, string StartPoint, string EndPoint);

@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<ItemGroup>
<ProjectReference Include="..\Sln.Wcs.HoistDispatcher\Sln.Wcs.HoistDispatcher.csproj" />
<ProjectReference Include="..\Sln.Wcs.Repository\Sln.Wcs.Repository.csproj" />
<ProjectReference Include="..\Sln.Wcs.Serilog\Sln.Wcs.Serilog.csproj" />
<ProjectReference Include="..\Sln.Wcs.Cache\Sln.Wcs.Cache.csproj" />
<ProjectReference Include="..\Sln.Wcs.Common\Sln.Wcs.Common.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Com.Ctrip.Framework.Apollo" Version="2.11.0" />
<PackageReference Include="Com.Ctrip.Framework.Apollo.Configuration" Version="2.11.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="Scrutor" Version="7.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>

@ -0,0 +1,14 @@
using Microsoft.Extensions.Configuration;
namespace Sln.Wcs;
internal class UpdateableConfigProvider : ConfigurationProvider, IConfigurationSource
{
public new void Set(string key, string value)
{
base.Set(key, value);
OnReload();
}
public IConfigurationProvider Build(IConfigurationBuilder builder) => this;
}

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

@ -0,0 +1,18 @@
{
"exclude": [
"**/bin",
"**/bower_components",
"**/jspm_packages",
"**/node_modules",
"**/obj",
"**/platforms"
],
"apollo": {
"AppId": "SlnWcs",
"Env": "DEV",
"MetaServer": "http://119.45.202.115:4320", // 4310
"ConfigServer": [
"http://119.45.202.115:4320"
]
}
}

@ -9,6 +9,7 @@ using Microsoft.Extensions.DependencyInjection;
using NeoSmart.Caching.Sqlite; using NeoSmart.Caching.Sqlite;
using Sln.Wcs.Repository; using Sln.Wcs.Repository;
using Sln.Wcs.Serilog; using Sln.Wcs.Serilog;
using Sln.Wcs.UI.Services;
using Sln.Wcs.UI.ViewModels; using Sln.Wcs.UI.ViewModels;
using Sln.Wcs.UI.Views; using Sln.Wcs.UI.Views;
using ZiggyCreatures.Caching.Fusion; using ZiggyCreatures.Caching.Fusion;
@ -92,6 +93,10 @@ public partial class App : Application
services.AddTransient<ViewModels.Task.TaskQueueViewModel>(); services.AddTransient<ViewModels.Task.TaskQueueViewModel>();
services.AddTransient<ViewModels.Task.TaskDetailViewModel>(); services.AddTransient<ViewModels.Task.TaskDetailViewModel>();
// 引擎进程管理
services.AddSingleton<IEngineProcessService, EngineProcessService>();
services.AddSingleton<IAgvEngineProcessService, AgvEngineProcessService>();
var serviceProvider = services.BuildServiceProvider(); var serviceProvider = services.BuildServiceProvider();
// ---- 启动后初始化 ---- // ---- 启动后初始化 ----

@ -0,0 +1,131 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
namespace Sln.Wcs.UI.Services;
public interface IAgvEngineProcessService
{
bool IsRunning { get; }
string StatusText { get; }
string? LastError { get; }
event Action<string>? OutputReceived;
event Action? StateChanged;
Task StartAsync();
void Stop();
}
public class AgvEngineProcessService : IAgvEngineProcessService
{
private Process? _process;
private readonly HttpClient _http = new() { Timeout = TimeSpan.FromSeconds(3) };
private const string BaseUrl = "http://localhost:5200";
private readonly StringBuilder _outputBuffer = new();
public bool IsRunning { get; private set; }
public string StatusText => IsRunning ? "运行中" : "已停止";
public string? LastError { get; private set; }
public event Action<string>? OutputReceived;
public event Action? StateChanged;
public async Task StartAsync()
{
if (IsRunning) return;
LastError = null;
_outputBuffer.Clear();
var dllPath = Path.Combine(AppContext.BaseDirectory, "Sln.Wcs.HikRoBotServer.dll");
if (!File.Exists(dllPath))
throw new InvalidOperationException($"找不到 {dllPath}");
_process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "dotnet",
Arguments = $"\"{dllPath}\" --urls {BaseUrl}",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
},
EnableRaisingEvents = true,
};
_process.OutputDataReceived += (_, e) =>
{
if (e.Data != null) { _outputBuffer.AppendLine(e.Data); OutputReceived?.Invoke(e.Data); }
};
_process.ErrorDataReceived += (_, e) =>
{
if (e.Data != null) { _outputBuffer.AppendLine(e.Data); OutputReceived?.Invoke(e.Data); }
};
_process.Exited += (_, _) =>
{
IsRunning = false;
StateChanged?.Invoke();
_process?.Dispose();
_process = null;
};
_process.Start();
_process.BeginOutputReadLine();
_process.BeginErrorReadLine();
for (int i = 0; i < 40; i++)
{
await Task.Delay(500);
if (_process.HasExited)
{
LastError = _outputBuffer.ToString();
_process.Dispose();
_process = null;
throw new InvalidOperationException($"进程异常退出:\n{LastError}");
}
if (await HealthCheckAsync())
{
IsRunning = true;
StateChanged?.Invoke();
return;
}
}
LastError = _outputBuffer.ToString();
Stop();
throw new InvalidOperationException($"启动超时 (20s):\n{LastError}");
}
public void Stop()
{
if (_process == null) return;
IsRunning = false;
try
{
if (!_process.HasExited)
{
_process.Kill();
_process.WaitForExit(3000);
}
_process.Dispose();
}
catch { }
finally { _process = null; }
StateChanged?.Invoke();
}
private async Task<bool> HealthCheckAsync()
{
try
{
var res = await _http.GetAsync($"{BaseUrl}/api/health");
return res.IsSuccessStatusCode;
}
catch { return false; }
}
}

@ -0,0 +1,132 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
namespace Sln.Wcs.UI.Services;
public interface IEngineProcessService
{
bool IsRunning { get; }
string StatusText { get; }
string? LastError { get; }
event Action<string>? OutputReceived;
event Action? StateChanged;
Task StartAsync();
void Stop();
}
public class EngineProcessService : IEngineProcessService
{
private Process? _process;
private readonly HttpClient _http = new() { Timeout = TimeSpan.FromSeconds(3) };
private const string BaseUrl = "http://localhost:5100";
private readonly StringBuilder _outputBuffer = new();
public bool IsRunning { get; private set; }
public string StatusText => IsRunning ? "运行中" : "已停止";
public string? LastError { get; private set; }
public event Action<string>? OutputReceived;
public event Action? StateChanged;
public async Task StartAsync()
{
if (IsRunning) return;
LastError = null;
_outputBuffer.Clear();
var dllPath = Path.Combine(AppContext.BaseDirectory, "Sln.Wcs.HoistServer.dll");
if (!File.Exists(dllPath))
throw new InvalidOperationException($"找不到 {dllPath}");
_process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "dotnet",
Arguments = $"\"{dllPath}\" --urls {BaseUrl}",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
},
EnableRaisingEvents = true,
};
_process.OutputDataReceived += (_, e) =>
{
if (e.Data != null) { _outputBuffer.AppendLine(e.Data); OutputReceived?.Invoke(e.Data); }
};
_process.ErrorDataReceived += (_, e) =>
{
if (e.Data != null) { _outputBuffer.AppendLine(e.Data); OutputReceived?.Invoke(e.Data); }
};
_process.Exited += (_, _) =>
{
IsRunning = false;
StateChanged?.Invoke();
_process?.Dispose();
_process = null;
};
_process.Start();
_process.BeginOutputReadLine();
_process.BeginErrorReadLine();
// 等待健康检查,最多 20 秒
for (int i = 0; i < 40; i++)
{
await Task.Delay(500);
if (_process.HasExited)
{
LastError = _outputBuffer.ToString();
_process.Dispose();
_process = null;
throw new InvalidOperationException($"进程异常退出:\n{LastError}");
}
if (await HealthCheckAsync())
{
IsRunning = true;
StateChanged?.Invoke();
return;
}
}
LastError = _outputBuffer.ToString();
Stop();
throw new InvalidOperationException($"启动超时 (20s):\n{LastError}");
}
public void Stop()
{
if (_process == null) return;
IsRunning = false;
try
{
if (!_process.HasExited)
{
_process.Kill();
_process.WaitForExit(3000);
}
_process.Dispose();
}
catch { }
finally { _process = null; }
StateChanged?.Invoke();
}
private async Task<bool> HealthCheckAsync()
{
try
{
var res = await _http.GetAsync($"{BaseUrl}/api/health");
return res.IsSuccessStatusCode;
}
catch { return false; }
}
}

@ -37,8 +37,23 @@
<ProjectReference Include="..\Sln.Wcs.Model\Sln.Wcs.Model.csproj" /> <ProjectReference Include="..\Sln.Wcs.Model\Sln.Wcs.Model.csproj" />
<ProjectReference Include="..\Sln.Wcs.Repository\Sln.Wcs.Repository.csproj" /> <ProjectReference Include="..\Sln.Wcs.Repository\Sln.Wcs.Repository.csproj" />
<ProjectReference Include="..\Sln.Wcs.Serilog\Sln.Wcs.Serilog.csproj" /> <ProjectReference Include="..\Sln.Wcs.Serilog\Sln.Wcs.Serilog.csproj" />
<ProjectReference Include="..\Sln.Wcs.HoistServer\Sln.Wcs.HoistServer.csproj" ReferenceOutputAssembly="false" />
</ItemGroup> </ItemGroup>
<Target Name="CopyHoistServer" AfterTargets="Build">
<ItemGroup>
<HoistFiles Include="..\Sln.Wcs.HoistServer\bin\$(Configuration)\net8.0\*" />
</ItemGroup>
<Copy SourceFiles="@(HoistFiles)" DestinationFolder="$(OutputPath)" SkipUnchangedFiles="true" />
</Target>
<Target Name="CopyHikRoBotServer" AfterTargets="Build">
<ItemGroup>
<HikFiles Include="..\Sln.Wcs.HikRoBotServer\bin\$(Configuration)\net8.0\*" />
</ItemGroup>
<Copy SourceFiles="@(HikFiles)" DestinationFolder="$(OutputPath)" SkipUnchangedFiles="true" />
</Target>
<ItemGroup> <ItemGroup>
<Content Include="appsettings.json"> <Content Include="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>

@ -4,28 +4,51 @@ using System.IO;
using System.Text; using System.Text;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using Sln.Wcs.UI.Services;
namespace Sln.Wcs.UI.ViewModels; namespace Sln.Wcs.UI.ViewModels;
public partial class SystemMonitorViewModel : ObservableObject public partial class SystemMonitorViewModel : ObservableObject
{ {
private readonly IEngineProcessService _hoistEngine;
private readonly IAgvEngineProcessService _agvEngine;
[ObservableProperty] [ObservableProperty]
private ObservableCollection<LogEntry> _logs = new(); private ObservableCollection<LogEntry> _logs = new();
[ObservableProperty] [ObservableProperty]
private int _logCount; private int _logCount;
[ObservableProperty]
private string _filterText = string.Empty;
[ObservableProperty] [ObservableProperty]
private bool _autoScroll = true; private bool _autoScroll = true;
// ---- Hoist ----
[ObservableProperty] private bool _isHoistRunning;
[ObservableProperty] private string _hoistStatusText = "已停止";
public string HoistStatusColor => IsHoistRunning ? "#00E676" : "#FF5252";
// ---- AGV ----
[ObservableProperty] private bool _isAgvRunning;
[ObservableProperty] private string _agvStatusText = "已停止";
public string AgvStatusColor => IsAgvRunning ? "#00E676" : "#FF5252";
private const int MaxLogs = 5000; private const int MaxLogs = 5000;
public SystemMonitorViewModel() public SystemMonitorViewModel(IEngineProcessService hoistEngine, IAgvEngineProcessService agvEngine)
{ {
// 安装控制台输出拦截器 _hoistEngine = hoistEngine;
_agvEngine = agvEngine;
_hoistEngine.StateChanged += () => RefreshHoist();
_hoistEngine.OutputReceived += msg => AddLog($"[提升机] {msg}");
_agvEngine.StateChanged += () => RefreshAgv();
_agvEngine.OutputReceived += msg => AddLog($"[AGV] {msg}");
StartHoistCommand = new AsyncRelayCommand(StartHoistAsync, () => !IsHoistRunning);
StopHoistCommand = new RelayCommand(StopHoist, () => IsHoistRunning);
StartAgvCommand = new AsyncRelayCommand(StartAgvAsync, () => !IsAgvRunning);
StopAgvCommand = new RelayCommand(StopAgv, () => IsAgvRunning);
var original = Console.Out; var original = Console.Out;
var writer = new LogTextWriter(original, entry => var writer = new LogTextWriter(original, entry =>
{ {
@ -39,18 +62,61 @@ public partial class SystemMonitorViewModel : ObservableObject
Console.SetOut(writer); Console.SetOut(writer);
} }
[RelayCommand] private void RefreshHoist()
private void Clear()
{ {
Logs.Clear(); Avalonia.Threading.Dispatcher.UIThread.Post(() =>
LogCount = 0; {
IsHoistRunning = _hoistEngine.IsRunning;
HoistStatusText = _hoistEngine.StatusText;
OnPropertyChanged(nameof(HoistStatusColor));
StartHoistCommand.NotifyCanExecuteChanged();
StopHoistCommand.NotifyCanExecuteChanged();
});
} }
[RelayCommand] private void RefreshAgv()
private void ToggleAutoScroll()
{ {
AutoScroll = !AutoScroll; Avalonia.Threading.Dispatcher.UIThread.Post(() =>
{
IsAgvRunning = _agvEngine.IsRunning;
AgvStatusText = _agvEngine.StatusText;
OnPropertyChanged(nameof(AgvStatusColor));
StartAgvCommand.NotifyCanExecuteChanged();
StopAgvCommand.NotifyCanExecuteChanged();
});
} }
// ---- Hoist Commands ----
public IAsyncRelayCommand StartHoistCommand { get; }
public IRelayCommand StopHoistCommand { get; }
private async System.Threading.Tasks.Task StartHoistAsync()
{
try { AddLog("正在启动 HoistServer..."); await _hoistEngine.StartAsync(); AddLog("HoistServer 启动成功"); }
catch (Exception ex) { AddLog($"Hoist 启动失败: {ex.Message}", "ERROR"); if (_hoistEngine.LastError != null) AddLog(_hoistEngine.LastError, "ERROR"); }
}
private void StopHoist() { _hoistEngine.Stop(); AddLog("HoistServer 已停止"); }
// ---- AGV Commands ----
public IAsyncRelayCommand StartAgvCommand { get; }
public IRelayCommand StopAgvCommand { get; }
private async System.Threading.Tasks.Task StartAgvAsync()
{
try { AddLog("正在启动 HikRoBotServer..."); await _agvEngine.StartAsync(); AddLog("HikRoBotServer 启动成功"); }
catch (Exception ex) { AddLog($"AGV 启动失败: {ex.Message}", "ERROR"); if (_agvEngine.LastError != null) AddLog(_agvEngine.LastError, "ERROR"); }
}
private void StopAgv() { _agvEngine.Stop(); AddLog("HikRoBotServer 已停止"); }
private void AddLog(string msg, string level = "INFO")
{
Logs.Add(new LogEntry { Time = DateTime.Now, Message = msg, Level = level });
if (Logs.Count > MaxLogs) Logs.RemoveAt(0);
LogCount = Logs.Count;
}
[RelayCommand] private void Clear() { Logs.Clear(); LogCount = 0; }
[RelayCommand] private void ToggleAutoScroll() { AutoScroll = !AutoScroll; }
} }
public class LogEntry public class LogEntry
@ -61,52 +127,14 @@ public class LogEntry
public string Level { get; set; } = "INFO"; public string Level { get; set; } = "INFO";
} }
/// <summary>
/// 拦截 Console.WriteLine 输出,同时写入原始输出和 ObservableCollection
/// </summary>
internal class LogTextWriter : TextWriter internal class LogTextWriter : TextWriter
{ {
private readonly TextWriter _original; private readonly TextWriter _original;
private readonly Action<LogEntry> _onWrite; private readonly Action<LogEntry> _onWrite;
private readonly StringBuilder _buffer = new(); private readonly StringBuilder _buffer = new();
public LogTextWriter(TextWriter o, Action<LogEntry> cb) { _original = o; _onWrite = cb; }
public LogTextWriter(TextWriter original, Action<LogEntry> onWrite)
{
_original = original;
_onWrite = onWrite;
}
public override Encoding Encoding => Encoding.UTF8; public override Encoding Encoding => Encoding.UTF8;
public override void Write(char v) { _original.Write(v); if (v == '\n') Flush(); else if (v != '\r') _buffer.Append(v); }
public override void Write(char value) public override void WriteLine(string? m) { _original.WriteLine(m); if (m != null) _onWrite(new LogEntry { Time = DateTime.Now, Message = m, Level = "INFO" }); }
{ private void Flush() { var m = _buffer.ToString(); _buffer.Clear(); if (m.Length > 0) _onWrite(new LogEntry { Time = DateTime.Now, Message = m, Level = "INFO" }); }
_original.Write(value);
if (value == '\n')
{
FlushBuffer();
}
else if (value != '\r')
{
_buffer.Append(value);
}
}
public override void WriteLine(string? message)
{
_original.WriteLine(message);
if (message != null)
{
_onWrite(new LogEntry { Time = DateTime.Now, Message = message, Level = "INFO" });
}
}
private void FlushBuffer()
{
var msg = _buffer.ToString();
_buffer.Clear();
if (msg.Length > 0)
{
_onWrite(new LogEntry { Time = DateTime.Now, Message = msg, Level = "INFO" });
}
}
} }

@ -1,12 +1,48 @@
<UserControl x:CompileBindings="False" xmlns="https://github.com/avaloniaui" <UserControl x:CompileBindings="False" xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Sln.Wcs.UI.Views.SystemMonitorView"> x:Class="Sln.Wcs.UI.Views.SystemMonitorView">
<Grid RowDefinitions="Auto,*" Margin="20,14,20,14" VerticalAlignment="Stretch"> <Grid RowDefinitions="Auto,Auto,Auto,*" Margin="20,14,20,14" VerticalAlignment="Stretch">
<!-- Hoist Engine -->
<Border Grid.Row="0" Background="{DynamicResource CardBgBrush}" Padding="10,8" CornerRadius="6"
BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1" Margin="0,0,0,8">
<Grid ColumnDefinitions="Auto,*,Auto,Auto">
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center" Margin="0,0,12,0">
<Ellipse Width="10" Height="10" Fill="{Binding HoistStatusColor}" VerticalAlignment="Center" />
<TextBlock Text="提升机调度引擎" FontSize="13" FontWeight="SemiBold" Foreground="{DynamicResource PrimaryTextBrush}" VerticalAlignment="Center" />
</StackPanel>
<Border Grid.Column="1" Background="{DynamicResource PrimaryBgBrush}" CornerRadius="3" Padding="6,3" VerticalAlignment="Center" HorizontalAlignment="Left">
<TextBlock Text="{Binding HoistStatusText}" FontSize="11" Foreground="{DynamicResource AccentTextBrush}" />
</Border>
<Button Grid.Column="2" Content="&#x25B6; 启动" Command="{Binding StartHoistCommand}"
Background="#1B5E20" Foreground="White" FontSize="11" Padding="12,5" Margin="0,0,6,0" />
<Button Grid.Column="3" Content="&#x25A0; 停止" Command="{Binding StopHoistCommand}"
Background="#B71C1C" Foreground="White" FontSize="11" Padding="12,5" />
</Grid>
</Border>
<!-- AGV Engine -->
<Border Grid.Row="1" Background="{DynamicResource CardBgBrush}" Padding="10,8" CornerRadius="6"
BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1" Margin="0,0,0,10">
<Grid ColumnDefinitions="Auto,*,Auto,Auto">
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center" Margin="0,0,12,0">
<Ellipse Width="10" Height="10" Fill="{Binding AgvStatusColor}" VerticalAlignment="Center" />
<TextBlock Text="AGV 调度引擎" FontSize="13" FontWeight="SemiBold" Foreground="{DynamicResource PrimaryTextBrush}" VerticalAlignment="Center" />
</StackPanel>
<Border Grid.Column="1" Background="{DynamicResource PrimaryBgBrush}" CornerRadius="3" Padding="6,3" VerticalAlignment="Center" HorizontalAlignment="Left">
<TextBlock Text="{Binding AgvStatusText}" FontSize="11" Foreground="{DynamicResource AccentTextBrush}" />
</Border>
<Button Grid.Column="2" Content="&#x25B6; 启动" Command="{Binding StartAgvCommand}"
Background="#1B5E20" Foreground="White" FontSize="11" Padding="12,5" Margin="0,0,6,0" />
<Button Grid.Column="3" Content="&#x25A0; 停止" Command="{Binding StopAgvCommand}"
Background="#B71C1C" Foreground="White" FontSize="11" Padding="12,5" />
</Grid>
</Border>
<!-- Toolbar --> <!-- Toolbar -->
<Grid Grid.Row="0" ColumnDefinitions="Auto,*,Auto,Auto" Margin="0,0,0,8"> <Grid Grid.Row="2" ColumnDefinitions="Auto,*,Auto,Auto,Auto" Margin="0,0,0,8">
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="6"> <StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="6">
<Ellipse Width="8" Height="8" Fill="#00E676" VerticalAlignment="Center" /> <Ellipse Width="8" Height="8" Fill="#00E676" VerticalAlignment="Center" />
<TextBlock Text="系统监控" FontSize="15" FontWeight="SemiBold" Foreground="{DynamicResource PrimaryTextBrush}" VerticalAlignment="Center" /> <TextBlock Text="控制台日志" FontSize="15" FontWeight="SemiBold" Foreground="{DynamicResource PrimaryTextBrush}" VerticalAlignment="Center" />
</StackPanel> </StackPanel>
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="6" Margin="0,0,10,0"> <StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="6" Margin="0,0,10,0">
<TextBlock Text="{Binding LogCount, StringFormat='共 {0} 条'}" FontSize="12" Foreground="{DynamicResource MutedTextBrush}" VerticalAlignment="Center" /> <TextBlock Text="{Binding LogCount, StringFormat='共 {0} 条'}" FontSize="12" Foreground="{DynamicResource MutedTextBrush}" VerticalAlignment="Center" />
@ -18,19 +54,16 @@
</Grid> </Grid>
<!-- Log List --> <!-- Log List -->
<Border Grid.Row="1" Background="{DynamicResource CardBgBrush}" CornerRadius="6" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1"> <Border Grid.Row="3" Background="{DynamicResource CardBgBrush}" CornerRadius="6" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1">
<ListBox x:Name="LogList" ItemsSource="{Binding Logs}" <ListBox ItemsSource="{Binding Logs}" Background="Transparent" Foreground="#BCC8D6" BorderThickness="0">
Background="Transparent" Foreground="#BCC8D6" BorderThickness="0"
>
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
<DataTemplate> <DataTemplate>
<Border Padding="10,3" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="0,0,0,1"> <Border Padding="10,3" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="0,0,0,1">
<StackPanel Orientation="Horizontal" Spacing="10"> <StackPanel Orientation="Horizontal" Spacing="10">
<TextBlock Text="{Binding TimeText}" <TextBlock Text="{Binding TimeText}" FontSize="11" Foreground="#4B5E7A"
FontSize="11" Foreground="#4B5E7A" FontFamily="Menlo,Consolas,monospace" FontFamily="Menlo,Consolas,monospace" VerticalAlignment="Center" Width="100" />
VerticalAlignment="Center" Width="100" /> <TextBlock Text="{Binding Message}" FontSize="11"
<TextBlock Text="{Binding Message}" Foreground="{DynamicResource SecondaryTextBrush}" FontFamily="Menlo,Consolas,monospace"
FontSize="11" Foreground="{DynamicResource SecondaryTextBrush}" FontFamily="Menlo,Consolas,monospace"
TextWrapping="Wrap" VerticalAlignment="Center" /> TextWrapping="Wrap" VerticalAlignment="Center" />
</StackPanel> </StackPanel>
</Border> </Border>

@ -34,6 +34,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sln.Wcs.HikRoBotDispatcher"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sln.Wcs.UI", "Sln.Wcs.UI\Sln.Wcs.UI.csproj", "{1AD871EB-0EAA-4F53-8CE7-691EEBEE3F22}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sln.Wcs.UI", "Sln.Wcs.UI\Sln.Wcs.UI.csproj", "{1AD871EB-0EAA-4F53-8CE7-691EEBEE3F22}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sln.Wcs.HoistServer", "Sln.Wcs.HoistServer\Sln.Wcs.HoistServer.csproj", "{B92D4DE0-C2D2-4CD9-B63D-0AAE990BFA5A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sln.Wcs.HikRoBotServer", "Sln.Wcs.HikRoBotServer\Sln.Wcs.HikRoBotServer.csproj", "{7E431EFD-BFC1-4D94-BB04-DE4EA57AA2E8}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -104,6 +108,14 @@ Global
{1AD871EB-0EAA-4F53-8CE7-691EEBEE3F22}.Debug|Any CPU.Build.0 = Debug|Any CPU {1AD871EB-0EAA-4F53-8CE7-691EEBEE3F22}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1AD871EB-0EAA-4F53-8CE7-691EEBEE3F22}.Release|Any CPU.ActiveCfg = Release|Any CPU {1AD871EB-0EAA-4F53-8CE7-691EEBEE3F22}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1AD871EB-0EAA-4F53-8CE7-691EEBEE3F22}.Release|Any CPU.Build.0 = Release|Any CPU {1AD871EB-0EAA-4F53-8CE7-691EEBEE3F22}.Release|Any CPU.Build.0 = Release|Any CPU
{B92D4DE0-C2D2-4CD9-B63D-0AAE990BFA5A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B92D4DE0-C2D2-4CD9-B63D-0AAE990BFA5A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B92D4DE0-C2D2-4CD9-B63D-0AAE990BFA5A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B92D4DE0-C2D2-4CD9-B63D-0AAE990BFA5A}.Release|Any CPU.Build.0 = Release|Any CPU
{7E431EFD-BFC1-4D94-BB04-DE4EA57AA2E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7E431EFD-BFC1-4D94-BB04-DE4EA57AA2E8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7E431EFD-BFC1-4D94-BB04-DE4EA57AA2E8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7E431EFD-BFC1-4D94-BB04-DE4EA57AA2E8}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

Loading…
Cancel
Save