初步版本251204

This commit is contained in:
2025-12-04 18:39:34 +08:00
parent cd1ec78a11
commit 9dd458ae8b
17 changed files with 1089 additions and 522 deletions

View File

@@ -3,6 +3,8 @@ using FATrace.HKNetLib.Hardware;
using FATrace.HKNetLib.Wrapper;
using FATrace.Model;
using FATrace.OEMApp.Services;
using FATrace.OEMApp.Model;
using System.Threading.Tasks;
using LibVLCSharp.Shared;
using NLog;
using ReaLTaiizor.Forms;
@@ -51,8 +53,8 @@ namespace FATrace.OEMApp
LvLog.GridLines = true;
LvLog.HeaderStyle = ColumnHeaderStyle.Nonclickable;
LvLog.Columns.Add("时间", 150);
LvLog.Columns.Add("级别", 60);
LvLog.Columns.Add("消息", 720);
LvLog.Columns.Add("级别", 100);
LvLog.Columns.Add("消息", 800);
}
finally
{
@@ -98,6 +100,7 @@ namespace FATrace.OEMApp
/// </summary>
private PLCDataService PLCDataService { get; set; }
private TimeClearDataService TimeClearService { get; set; }
private System.Windows.Forms.Timer _statusTimer;
/// <summary>
/// 主窗体加载:
@@ -143,8 +146,13 @@ namespace FATrace.OEMApp
{
DownloadTaskWorker.Instance.Start(HkCameraClient);
LogInfo("下载队列服务已启动");
JellyfinMonitorQueueService.Instance.Start();
LogInfo("Jellyfin 监控服务已启动");
try { DownloadTaskWorker.Instance.DownloadFileNameChanged += OnDownloadFileNameChanged; } catch { }
try { DownloadTaskWorker.Instance.TaskStarted += OnTaskStatusChanged; } catch { }
try { DownloadTaskWorker.Instance.TaskCompleted += OnTaskStatusChanged; } catch { }
try { DownloadTaskWorker.Instance.TaskFailed += OnTaskFailed; } catch { }
// JellyfinMonitorQueueService.Instance.Start();
// LogInfo("Jellyfin 监控服务已启动");
LogInfo("Jellyfin 监控服务已停用");
TimeClearService = new TimeClearDataService();
TimeClearService.Info += (m) => LogInfo($"[清理]{m}");
TimeClearService.Start();
@@ -158,9 +166,15 @@ namespace FATrace.OEMApp
// 初始化 gridRULog 并启动 UI 定时刷新
InitRuLogGrid();
// 立即刷新一次,避免首次 1s 空白
// 仅初始化一次,当天数据
RefreshRuLogGrid();
StartTaskUiTimer();
// 初始化底部连接状态(立即检测一次 + 定时轻量检测)
SafeSetStatus(tslPlcConnection, PLCDataService?.PlcConnected == true, "Plc连接状态");
_ = UpdateDbStatusAsync();
_ = UpdateNasStatusAsync();
_ = UpdateNvrStatusAsync();
StartStatusTimer();
//materialListView1.DataBindings
try
@@ -194,17 +208,30 @@ namespace FATrace.OEMApp
/// <param name="Code"></param>
private void PLCDataService_ScanCodeEventHandler(object? sender, string Code)
{
//解析Code条码数据内包条码数据
CurParsedCodeInfo = NVRCom.ParseCodeFull(Code);
var taskId = DownloadTaskWorker.Instance.Enqueue(
CurParsedCodeInfo,
user: CurUserName,
start: DateTime.Now,
end: DateTime.Now.AddSeconds(DownloadTaskWorker.VideoTime+2)
);
//MessageBox.Show($"[Test] 已入队下载任务Id={taskId}");
LogInfo($"扫码: {Code}");
}
/// <summary>
/// PLC数据服务PLC连接
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <summary>
/// PLC数据服务PLC连接
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void PLCDataService_PlcConnectedEventHandler(object? sender, string e)
{
LogInfo($"PLC连接: {e}");
var ok = string.Equals(e, "OK", StringComparison.OrdinalIgnoreCase);
SafeSetStatus(tslPlcConnection, ok, "Plc连接状态");
}
private void NVRLogin()
@@ -220,10 +247,12 @@ namespace FATrace.OEMApp
{
MessageBox.Show("登录成功");
LogInfo("NVR 登录成功");
_ = UpdateNvrStatusAsync();
return;
}
MessageBox.Show($"登录失败:{HkCameraClient.LastMsgErr}");
LogError($"NVR 登录失败: {HkCameraClient.LastMsgErr}");
SafeSetStatus(tslNVRConnection, false, "NVR连接状态");
return;
}
@@ -250,15 +279,24 @@ namespace FATrace.OEMApp
#region
/// <summary>
/// 内包条码
/// </summary>
public string CurInBagCode { get; set; } = "AAAAAA";
///// <summary>
///// 内包条码
///// </summary>
//public string CurInBagCode { get; set; } = "AAAAAA";
///// <summary>
///// 内包条码 原料名称
///// </summary>
//public string CurRawName { get; set; } = "添加剂Test";
///// <summary>
///// 内包条码 原料代码
///// </summary>
//public string CurRawCode { get; set; } = "ASASASS";
/// <summary>
/// 内包条码 原料名称
/// 内包条码 解析结果
/// </summary>
public string CurInBagRawName { get; set; } = "添加剂Test";
public ParsedCodeInfo CurParsedCodeInfo { get; set; }
#endregion
@@ -282,25 +320,6 @@ namespace FATrace.OEMApp
return;
}
/// <summary>
/// 下载按钮:仅将请求入队为 DownloadTask由后台 DownloadTaskWorker 顺序处理。
/// 注意:不会在此处直接调用 SDK 下载,也不会订阅 SDK 事件,避免并发与重复。
/// </summary>
private void btnLoadVideo_Click(object sender, EventArgs e)
{
// 仅入队下载任务,由后台 DownloadTaskWorker 顺序处理
var taskId = DownloadTaskWorker.Instance.Enqueue(
code: CurInBagCode,
rawName: CurInBagRawName,
user: CurUserName,
start: DateTime.Now.AddSeconds(-30),
end: DateTime.Now
);
MessageBox.Show($"已入队下载任务Id={taskId}");
LogInfo($"下载任务入队 Id={taskId} Code={CurInBagCode}");
}
private void HkCameraClient_NVRLoadVideoProcessEventHandler(object? sender, short value)
{
this.BeginInvoke(new Action(() =>
@@ -310,7 +329,7 @@ namespace FATrace.OEMApp
if (value >= 100 && _lastProgressLogged != 100)
{
_lastProgressLogged = 100;
LogInfo("下载进度 100%");
LogInfo($"{DownloadTaskWorker.Instance.CurDownloadTask.Code} - 下载进度 100%");
}
}
@@ -592,13 +611,12 @@ namespace FATrace.OEMApp
private void btnTestAction_Click(object sender, EventArgs e)
{
var taskId = DownloadTaskWorker.Instance.Enqueue(
code: CurInBagCode,
rawName: CurInBagRawName,
CurParsedCodeInfo,
user: CurUserName,
start: DateTime.Now.AddSeconds(-100),
end: DateTime.Now
start: DateTime.Now,
end: DateTime.Now.AddSeconds(DownloadTaskWorker.VideoTime)
);
MessageBox.Show($"[Test] 已入队下载任务Id={taskId}");
//MessageBox.Show($"[Test] 已入队下载任务Id={taskId}");
}
/// <summary>
@@ -628,59 +646,12 @@ namespace FATrace.OEMApp
}
LogInfo($"下载完成: {localNameOrPath}");
// 先保存当前的信息,记录主键 Id
var rawUse = new OEMRawUse()
{
InBagCode = CurInBagCode,
RawName = CurInBagRawName,
User = CurUserName,
UrlState = false,
VideoUrl = string.Empty
};
long rawUseId = 0;
try
{
rawUseId = FSqlContext.FDb.Insert<OEMRawUse>(rawUse).ExecuteIdentity();
_downloadProcessingKeys[key] = rawUseId;
}
catch (Exception ex)
{
_downloadProcessingKeys.TryRemove(key, out _);
System.Diagnostics.Debug.WriteLine($"[NVRLoadVideoComplete] 插入 OEMRawUse 失败: {ex.Message}");
return;
}
// 后台执行,避免阻塞 UI 线程
Task.Run(async () =>
{
try
{
var monitor = new JellyfinMonitorService();
await monitor.MonitorAndUpdateAfterDownloadAsync(
oemRawUseId: rawUseId,
videoLocalPathOrName: localNameOrPath,
code: CurInBagCode,
rawName: CurInBagRawName,
userName: CurUserName,
cancellationToken: CancellationToken.None
).ConfigureAwait(false);
}
catch (Exception ex1)
{
System.Diagnostics.Debug.WriteLine($"[JellyfinMonitor] 异常: {ex1.Message}");
}
finally
{
// 移除去重 key允许后续相同文件再次处理如需重试
_downloadProcessingKeys.TryRemove(key, out _);
}
});
// Jellyfin 监控已停用:仅移除去重 key
_downloadProcessingKeys.TryRemove(key, out _);
}
#region gridRULog / DataGridView
private System.Windows.Forms.Timer _taskUiTimer;
private BindingSource ruLogBindingSource;
private BindingList<RuLogRow> ruLogBindingList;
@@ -691,6 +662,7 @@ namespace FATrace.OEMApp
{
public string TimeText { get; set; } = string.Empty;
public string Type { get; set; } = string.Empty; // 下载/监听
public string Code { get; set; } = string.Empty;
public long TaskId { get; set; }
public string StatusText { get; set; } = string.Empty;
public string? ProgressText { get; set; }
@@ -726,6 +698,14 @@ namespace FATrace.OEMApp
AutoSizeMode = DataGridViewAutoSizeColumnMode.None
});
gridRULog.Columns.Add(new DataGridViewTextBoxColumn
{
Name = "colCode",
HeaderText = "条码",
DataPropertyName = nameof(RuLogRow.Code),
Width = 400,
AutoSizeMode = DataGridViewAutoSizeColumnMode.None
});
gridRULog.Columns.Add(new DataGridViewTextBoxColumn
{
Name = "colTaskId",
HeaderText = "任务Id",
@@ -772,7 +752,6 @@ namespace FATrace.OEMApp
gridRULog.ColumnHeadersDefaultCellStyle.ForeColor = Color.Black;
gridRULog.RowHeadersVisible = false;
// 使用 Designer 中的 Location/Size 与 Anchor不额外 Dock
gridRULog.BringToFront();
// 跟随父容器尺寸变化进行定位与大小调整
materialCard2.Resize -= MaterialCard2_Resize;
@@ -810,7 +789,6 @@ namespace FATrace.OEMApp
var height = Math.Max(120, client.Height - top - 9);
gridRULog.Location = new Point(left, top);
gridRULog.Size = new Size(width, height);
gridRULog.BringToFront();
}
catch { }
}
@@ -820,40 +798,22 @@ namespace FATrace.OEMApp
LayoutRuLogGrid();
}
/// <summary>
/// 启动 UI 定时器,定时刷新下载与 Jellyfin 监听任务状态
/// </summary>
private void StartTaskUiTimer()
{
_taskUiTimer = new System.Windows.Forms.Timer();
_taskUiTimer.Interval = 1000; // 1s 刷新
_taskUiTimer.Tick += TaskUiTimer_Tick;
_taskUiTimer.Start();
}
private void TaskUiTimer_Tick(object? sender, EventArgs e)
{
RefreshRuLogGrid();
}
// 定时刷新已移除:仅在下载开始/完成/失败时按需更新
/// <summary>
/// 刷新 gridRULog下载 + 监听
/// 刷新 gridRULog仅当天下载)
/// </summary>
private void RefreshRuLogGrid()
{
try
{
var db = FSqlContext.FDb;
var today = DateTime.Today;
var downloads = db.Select<DownloadTask>()
.Where(a => a.UpdateTime >= today)
.OrderByDescending(a => a.UpdateTime)
.Limit(50)
.ToList();
var monitors = db.Select<JellyfinMonitorTask>()
.OrderByDescending(a => a.UpdateTime)
.Limit(50)
.ToList();
var rows = new List<RuLogRow>(downloads.Count + monitors.Count);
var rows = new List<RuLogRow>(downloads.Count);
// 运行中优先
foreach (var t in downloads.Where(x => x.Status == TaskStatus.Running))
@@ -862,24 +822,14 @@ namespace FATrace.OEMApp
{
TimeText = t.UpdateTime.ToString("HH:mm:ss"),
Type = "下载",
Code = t.Code ?? string.Empty,
TaskId = t.Id,
StatusText = t.Status.ToString(),
ProgressText = t.Progress.ToString(),
Remark = string.IsNullOrWhiteSpace(t.Error) ? (t.VideoFilePath ?? string.Empty) : t.Error!
});
}
foreach (var t in monitors.Where(x => x.Status == TaskStatus.Running))
{
rows.Add(new RuLogRow
{
TimeText = t.UpdateTime.ToString("HH:mm:ss"),
Type = "监听",
TaskId = t.Id,
StatusText = t.Status.ToString(),
ProgressText = string.Empty,
Remark = string.IsNullOrWhiteSpace(t.Error) ? (t.FoundItemId ?? t.LocalFileNameOrPath ?? string.Empty) : t.Error!
});
}
// 监听任务展示已移除
// 其余
foreach (var t in downloads.Where(x => x.Status != TaskStatus.Running))
@@ -888,26 +838,16 @@ namespace FATrace.OEMApp
{
TimeText = t.UpdateTime.ToString("HH:mm:ss"),
Type = "下载",
Code = t.Code ?? string.Empty,
TaskId = t.Id,
StatusText = t.Status.ToString(),
ProgressText = t.Progress.ToString(),
Remark = string.IsNullOrWhiteSpace(t.Error) ? (t.VideoFilePath ?? string.Empty) : t.Error!
});
}
foreach (var t in monitors.Where(x => x.Status != TaskStatus.Running))
{
rows.Add(new RuLogRow
{
TimeText = t.UpdateTime.ToString("HH:mm:ss"),
Type = "监听",
TaskId = t.Id,
StatusText = t.Status.ToString(),
ProgressText = string.Empty,
Remark = string.IsNullOrWhiteSpace(t.Error) ? (t.FoundItemId ?? t.LocalFileNameOrPath ?? string.Empty) : t.Error!
});
}
// 监听任务展示已移除
// 批量更新绑定列表,尽量减少闪烁
// 批量更新绑定列表
ruLogBindingList.RaiseListChangedEvents = false;
ruLogBindingList.Clear();
foreach (var r in rows)
@@ -923,6 +863,138 @@ namespace FATrace.OEMApp
}
}
private void OnTaskStatusChanged(DownloadTask t)
{
try
{
if (t.UpdateTime.Date != DateTime.Today) return;
if (InvokeRequired)
{
BeginInvoke(new Action(() => UpsertRuLogRowFromTask(t)));
}
else
{
UpsertRuLogRowFromTask(t);
}
if (t.Status == TaskStatus.Completed || t.Status == TaskStatus.Failed)
{
ResetProgressBar();
}
if (t.Status == TaskStatus.Completed)
{
_ = SaveCsvForTaskAsync(t);
}
}
catch { }
}
private void OnTaskFailed(DownloadTask t, string? error)
{
try
{
if (t.UpdateTime.Date != DateTime.Today) return; // 仅当天
if (InvokeRequired)
{
BeginInvoke(new Action(() => UpsertRuLogRowFromTask(t)));
}
else
{
UpsertRuLogRowFromTask(t);
}
// 失败同样清零进度
ResetProgressBar();
}
catch { }
}
private void UpsertRuLogRowFromTask(DownloadTask t)
{
if (ruLogBindingList == null) return;
var existing = ruLogBindingList.FirstOrDefault(x => x.TaskId == t.Id);
if (existing == null)
{
ruLogBindingList.Insert(0, new RuLogRow
{
TimeText = t.UpdateTime.ToString("HH:mm:ss"),
Type = "下载",
Code = t.Code ?? string.Empty,
TaskId = t.Id,
StatusText = t.Status.ToString(),
ProgressText = t.Progress.ToString(),
Remark = string.IsNullOrWhiteSpace(t.Error) ? (t.VideoFilePath ?? string.Empty) : t.Error!
});
}
else
{
existing.TimeText = t.UpdateTime.ToString("HH:mm:ss");
existing.StatusText = t.Status.ToString();
existing.ProgressText = t.Progress.ToString();
existing.Code = t.Code ?? string.Empty;
existing.Remark = string.IsNullOrWhiteSpace(t.Error) ? (t.VideoFilePath ?? string.Empty) : t.Error!;
}
ruLogBindingList.ResetBindings();
}
private void ResetProgressBar()
{
try
{
if (DownloadProgressBarMain == null || DownloadProgressBarMain.IsDisposed) return;
if (InvokeRequired)
{
BeginInvoke(new Action(() => DownloadProgressBarMain.Value = 0));
}
else
{
DownloadProgressBarMain.Value = 0;
}
_lastProgressLogged = -1;
}
catch { }
}
private async Task SaveCsvForTaskAsync(DownloadTask t)
{
try
{
var dto = new RawUseCsvDto
{
RawCode = t.RawCode,
RawName = t.RawName,
InBagCode = t.Code,
OpUser = t.User,
VideoSavePath = t.VideoFilePath,
UseTime = t.UpdateTime
};
var svc = new CsvService();
var path = await Task.Run(() => svc.ExportSingle(dto));
SafeSetCsvState($"CSV保存成功: {path}", true);
}
catch (Exception ex)
{
SafeSetCsvState($"CSV保存失败: {ex.Message}", false);
}
}
private void SafeSetCsvState(string text, bool ok)
{
try
{
if (txtCsvSaveState == null || txtCsvSaveState.IsDisposed) return;
if (InvokeRequired)
{
BeginInvoke(new Action(() => { txtCsvSaveState.Text = text ?? string.Empty; txtCsvSaveState.ForeColor = ok ? Color.ForestGreen : Color.DarkRed; }));
}
else
{
txtCsvSaveState.Text = text ?? string.Empty;
txtCsvSaveState.ForeColor = ok ? Color.ForestGreen : Color.DarkRed;
}
}
catch { }
}
private static void SetDataGridViewDoubleBuffered(DataGridView dgv)
{
try
@@ -947,8 +1019,112 @@ namespace FATrace.OEMApp
protected override void OnFormClosing(FormClosingEventArgs e)
{
try { DownloadTaskWorker.Instance.DownloadFileNameChanged -= OnDownloadFileNameChanged; } catch { }
try { DownloadTaskWorker.Instance.TaskStarted -= OnTaskStatusChanged; } catch { }
try { DownloadTaskWorker.Instance.TaskCompleted -= OnTaskStatusChanged; } catch { }
try { DownloadTaskWorker.Instance.TaskFailed -= OnTaskFailed; } catch { }
try { _statusTimer?.Stop(); _statusTimer?.Dispose(); _statusTimer = null; } catch { }
try { TimeClearService?.Stop(); } catch { }
base.OnFormClosing(e);
}
/// <summary>
/// 当前下载文件的名称改变
/// </summary>
/// <param name="name"></param>
private void OnDownloadFileNameChanged(string name)
{
try
{
if (DownloadFileName == null || DownloadFileName.IsDisposed) return;
if (InvokeRequired)
{
try { BeginInvoke(new Action(() => DownloadFileName.Text = name ?? string.Empty)); } catch { }
}
else
{
try { BeginInvoke(new Action(() => DownloadFileName.Text = name ?? string.Empty)); } catch { }
}
}
catch { }
}
#region PLC/DB/NAS/NVR
private void StartStatusTimer()
{
try
{
_statusTimer = new System.Windows.Forms.Timer();
_statusTimer.Interval = 30000; // 30s 轻量检测
_statusTimer.Tick += async (s, e) =>
{
await UpdateDbStatusAsync();
await UpdateNasStatusAsync();
await UpdateNvrStatusAsync();
};
_statusTimer.Start();
}
catch { }
}
private void SafeSetStatus(ToolStripStatusLabel lbl, bool ok, string title)
{
try
{
string text = ok ? $"{title}: 正常" : $"{title}: 异常";
var fore = ok ? Color.ForestGreen : Color.DarkRed;
if (InvokeRequired)
{
BeginInvoke(new Action(() => { lbl.Text = text; lbl.ForeColor = fore; }));
}
else
{
lbl.Text = text; lbl.ForeColor = fore;
}
}
catch { }
}
private async Task UpdateDbStatusAsync()
{
bool ok = false;
try
{
var db = FSqlContext.FDb;
var ret = await db.Ado.ExecuteScalarAsync("SELECT 1");
ok = Convert.ToString(ret) == "1";
}
catch { ok = false; }
SafeSetStatus(tslSqlConnection, ok, "服务连接状态");
}
private async Task UpdateNasStatusAsync()
{
bool ok = false;
try
{
var path = HkCameraClient?.NVRVideoSavePath;
if (!string.IsNullOrWhiteSpace(path))
{
ok = await Task.Run(() => System.IO.Directory.Exists(path));
}
}
catch { ok = false; }
SafeSetStatus(tslNasConnection, ok, "NAS连接状态");
}
private async Task UpdateNvrStatusAsync()
{
bool ok = false;
try
{
ok = HkCameraClient != null && HkCameraClient.NVRLoginState && HkCameraClient.IsOnline();
}
catch { ok = false; }
await Task.Yield();
SafeSetStatus(tslNVRConnection, ok, "NVR连接状态");
}
#endregion
}
}