LightManualViewModel.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  1. using Basler.Pylon;
  2. using Prism.Commands;
  3. using Prism.Events;
  4. using Prism.Ioc;
  5. using Prism.Mvvm;
  6. using Prism.Regions;
  7. using System;
  8. using System.Collections.Generic;
  9. using System.Collections.ObjectModel;
  10. using System.Linq;
  11. using System.Threading.Tasks;
  12. using TeamAAS_VP.Core;
  13. using TeamAAS_VP.Core.Lights;
  14. using TeamAAS_VP.Data;
  15. using TeamAAS_VP.Events;
  16. using TeamAAS_VP.Interfaces;
  17. using TeamAAS_VP.Models.Lights;
  18. using TeamAAS_VP.Resources.Languages;
  19. namespace TeamAAS_VP.ViewModels.DebugMod
  20. {
  21. public class LightManualViewModel : BindableBase, INavigationAware
  22. {
  23. IContainerProvider _container;
  24. IEventAggregator _eventAggregator;
  25. ILightManagerService _light_manager_service; // fallback
  26. ILightManagerService _lightManagerService;
  27. IConfigService _configService;
  28. ISystemDatabaseService _systemDatabaseService;
  29. #region 属性
  30. private ObservableCollection<ChannelItem> _Channels = new ObservableCollection<ChannelItem>();
  31. public ObservableCollection<ChannelItem> Channels
  32. {
  33. get => _Channels; set => SetProperty(ref _Channels, value);
  34. }
  35. #endregion
  36. #region 命令
  37. private DelegateCommand _LoadedCommand;
  38. public DelegateCommand LoadedCommand =>
  39. _LoadedCommand ?? (_LoadedCommand = new DelegateCommand(ExecuteLoadedCommand));
  40. #endregion
  41. #region 事件
  42. #endregion
  43. public LightManualViewModel(IContainerProvider container, IEventAggregator ea, ILightManagerService lightManagerService, IConfigService configService, ISystemDatabaseService systemDatabaseService)
  44. {
  45. _container = container;
  46. _eventAggregator = ea;
  47. _light_manager_service = lightManagerService;
  48. _lightManagerService = lightManagerService;
  49. _configService = configService;
  50. _systemDatabaseService = systemDatabaseService;
  51. //订阅权限登录事件
  52. _eventAggregator.GetEvent<UserLoginNotification>().Subscribe(LoginChange);
  53. }
  54. #region 方法
  55. void ExecuteLoadedCommand()
  56. {
  57. if (_systemDatabaseService.GetCurrentUser().userPart == Enums.UserPart.Operator)
  58. {
  59. IsAllowEdit = false;
  60. }
  61. else
  62. {
  63. IsAllowEdit = true;
  64. }
  65. }
  66. #endregion
  67. #region 继承
  68. public void ConfirmNavigationRequest(NavigationContext navigationContext, Action<bool> continuationCallback)
  69. {
  70. continuationCallback(true);
  71. }
  72. public async void OnNavigatedTo(NavigationContext navigationContext)
  73. {
  74. // load channels from light manager service
  75. Channels.Clear();
  76. try
  77. {
  78. var globals = _lightManagerService.GlobalChannels.ToList();
  79. foreach (var kv in globals)
  80. {
  81. var item = new ChannelItem(kv.Key, (ILightChannel)kv.Value, _lightManagerService, _configService, _eventAggregator);
  82. Channels.Add(item);
  83. await item.InitializeAsync();
  84. }
  85. }
  86. catch (Exception ex)
  87. {
  88. // ignore or notify
  89. }
  90. }
  91. public bool IsNavigationTarget(NavigationContext navigationContext)
  92. {
  93. return true;
  94. }
  95. public void OnNavigatedFrom(NavigationContext navigationContext)
  96. {
  97. // nothing
  98. }
  99. #endregion
  100. private bool _IsAllowEdit = false;
  101. public bool IsAllowEdit
  102. {
  103. get { return _IsAllowEdit; }
  104. set { SetProperty(ref _IsAllowEdit, value); }
  105. }
  106. private void LoginChange(Models.User user)
  107. {
  108. if (user.userPart == Enums.UserPart.Operator)
  109. {
  110. IsAllowEdit = false;
  111. }
  112. else
  113. {
  114. IsAllowEdit = true;
  115. }
  116. }
  117. // 计划(伪代码,详细步骤):
  118. // 1. 在 ChannelItem 内增加用于去抖和写入序列化的字段:
  119. // - 一个对象锁 _writeLock 用于保护对共享字段的访问
  120. // - 一个可空整型 _latestQueuedBrightness 存储最新的待写入值
  121. // - 一个布尔 _isWriting 标识是否正在执行写入操作
  122. // - 一个 CancellationTokenSource _debounceCts 用于取消上一次的去抖任务
  123. // 2. 在滑动条变化时(ExecuteBrightnessChangedCommand):
  124. // - 解析参数得到新的亮度值
  125. // - 将新的亮度写入 _latestQueuedBrightness(覆盖之前未写入的值)
  126. // - 取消并替换之前的去抖 token,然后启动一个延迟任务(例如 200ms)
  127. // 延迟任务被触发后会调用 ProcessWritesAsync 来执行实际写入
  128. // 3. 在 ProcessWritesAsync 中:
  129. // - 使用锁检查并设置 _isWriting,保证同一时刻只有一个写入循环在跑
  130. // - 进入循环:从 _latestQueuedBrightness 读取值并清空该字段,调用写入接口写入该值
  131. // - 写入完成后再次检查 _latestQueuedBrightness 是否已有新值;如果有则继续写入最新值,否则退出并将 _isWriting 置 false
  132. // 4. 这样可以:
  133. // - 去抖:避免用户快速滑动时频繁立刻触发写入
  134. // - 合并中间值:只写入最新的有效值
  135. // - 串行化写入:避免并发写入接口导致冲突或重复
  136. //
  137. // 实现注意:
  138. // - 不假定底层 SetGlobalChannelBrightnessAsync 支持取消 token(因此不尝试中止正在进行的写入)
  139. // - 去抖超时选择 150-300ms 可以根据 UX 需求调整
  140. public class ChannelItem : BindableBase
  141. {
  142. private readonly int _globalId;
  143. private readonly ILightChannel _channel;
  144. private readonly ILightManagerService _lightManagerService;
  145. private readonly IConfigService _configService;
  146. private readonly IEventAggregator _eventAggregator;
  147. public int GlobalId => _globalId;
  148. private string _Name;
  149. public string Name { get => _Name; set => SetProperty(ref _Name, value); }
  150. private int _PendingBrightness;
  151. public int PendingBrightness { get => _PendingBrightness; set => SetProperty(ref _PendingBrightness, value); }
  152. private int _CurrentBrightness;
  153. public int CurrentBrightness { get => _CurrentBrightness; set => SetProperty(ref _CurrentBrightness, value); }
  154. private bool _IsOn;
  155. public bool IsOn { get => _IsOn; set => SetProperty(ref _IsOn, value); }
  156. public DelegateCommand SaveCommand { get; }
  157. public DelegateCommand TurnOnCommand { get; }
  158. public DelegateCommand TurnOffCommand { get; }
  159. private DelegateCommand<object> _BrightnessChangedCommand;
  160. public DelegateCommand<object> BrightnessChangedCommand =>
  161. _BrightnessChangedCommand ?? (_BrightnessChangedCommand = new DelegateCommand<object>(ExecuteBrightnessChangedCommand));
  162. // 去抖与写入控制字段
  163. private readonly object _writeLock = new object();
  164. private int? _latestQueuedBrightness = null;
  165. private bool _isWriting = false;
  166. private System.Threading.CancellationTokenSource _debounceCts;
  167. public ChannelItem(int globalId, ILightChannel channel, ILightManagerService lightManagerService, IConfigService configService, IEventAggregator eventAggregator)
  168. {
  169. _globalId = globalId;
  170. _channel = channel;
  171. _lightManagerService = lightManagerService;
  172. _configService = configService;
  173. _eventAggregator = eventAggregator;
  174. Name = channel?.ChannelName ?? $"Channel {_globalId}";
  175. PendingBrightness = 0;
  176. CurrentBrightness = 0;
  177. IsOn = false;
  178. SaveCommand = new DelegateCommand(async () => await SaveAsync());
  179. TurnOnCommand = new DelegateCommand(async () => await TurnOnAsync());
  180. TurnOffCommand = new DelegateCommand(async () => await TurnOffAsync());
  181. }
  182. public async Task InitializeAsync()
  183. {
  184. try
  185. {
  186. if (_channel != null)
  187. {
  188. var b = await _channel.GetBrightnessAsync();
  189. CurrentBrightness = b;
  190. PendingBrightness = b;
  191. IsOn = await _channel.GetStatusAsync();
  192. }
  193. }
  194. catch
  195. {
  196. }
  197. }
  198. // 当滑块变化时调用:采用去抖 + 只写最新值的策略
  199. async void ExecuteBrightnessChangedCommand(object obj)
  200. {
  201. try
  202. {
  203. var parameter = obj as Tuple<object, double, double>;
  204. if (parameter == null) return;
  205. int newValue = (int)parameter.Item2;
  206. // 更新 PendingBrightness 可即时反映到 UI(根据需要)
  207. PendingBrightness = newValue;
  208. QueueBrightnessChange(newValue);
  209. }
  210. catch (Exception)
  211. {
  212. // ignore
  213. }
  214. }
  215. // 将新的亮度值入队(覆盖之前未写入的值),并启动去抖延迟任务
  216. private void QueueBrightnessChange(int brightness)
  217. {
  218. lock (_writeLock)
  219. {
  220. _latestQueuedBrightness = brightness;
  221. // 取消之前的去抖任务
  222. try
  223. {
  224. _debounceCts?.Cancel();
  225. _debounceCts?.Dispose();
  226. }
  227. catch { }
  228. _debounceCts = new System.Threading.CancellationTokenSource();
  229. var token = _debounceCts.Token;
  230. // 启动去抖后处理(不等待)
  231. System.Threading.Tasks.Task.Run(async () =>
  232. {
  233. try
  234. {
  235. // 去抖延迟,可调整为 150-300ms
  236. await System.Threading.Tasks.Task.Delay(200, token);
  237. await ProcessWritesAsync();
  238. }
  239. catch (System.OperationCanceledException)
  240. {
  241. // 被取消,忽略
  242. }
  243. catch
  244. {
  245. // 忽略其它异常防止未观测异常
  246. }
  247. }, System.Threading.CancellationToken.None);
  248. }
  249. }
  250. // 串行化写入循环:写入当前最新值,若在写入过程中又有新值则继续写入,直到没有新值
  251. private async System.Threading.Tasks.Task ProcessWritesAsync()
  252. {
  253. // 快速检查与设置写入标志,保证只有一个写入循环执行
  254. lock (_writeLock)
  255. {
  256. if (_isWriting) return;
  257. _isWriting = true;
  258. }
  259. try
  260. {
  261. while (true)
  262. {
  263. int? toWrite = null;
  264. lock (_writeLock)
  265. {
  266. if (_latestQueuedBrightness.HasValue)
  267. {
  268. toWrite = _latestQueuedBrightness.Value;
  269. _latestQueuedBrightness = null;
  270. }
  271. }
  272. if (!toWrite.HasValue)
  273. {
  274. // 没有需要写入的值,退出循环
  275. break;
  276. }
  277. try
  278. {
  279. // 实际写入(底层方法可能不支持取消)
  280. var success = await _lightManagerService.SetGlobalChannelBrightnessAsync(_globalId, toWrite.Value);
  281. if (success)
  282. {
  283. CurrentBrightness = toWrite.Value;
  284. PendingBrightness = toWrite.Value;
  285. IsOn = toWrite.Value > 0;
  286. }
  287. }
  288. catch
  289. {
  290. // 忽略写入异常,继续检查是否有新值需要写入
  291. }
  292. }
  293. }
  294. finally
  295. {
  296. lock (_writeLock)
  297. {
  298. _isWriting = false;
  299. }
  300. }
  301. }
  302. private async Task SaveAsync()
  303. {
  304. try
  305. {
  306. var success = await _lightManagerService.SetGlobalChannelBrightnessAsync(_globalId, PendingBrightness);
  307. if (success)
  308. {
  309. CurrentBrightness = PendingBrightness;
  310. IsOn = PendingBrightness > 0;
  311. // also save as default in config
  312. _configService.UpdateChannelDefaultBrightness(_globalId, PendingBrightness);
  313. _eventAggregator.GetEvent<SnackbarMessageNotification>().Publish(new MessageParameter() { Msg = $"保存完成: {Name} -> {PendingBrightness}", Duration = 1 });
  314. }
  315. else
  316. {
  317. _eventAggregator.GetEvent<SnackbarMessageNotification>().Publish(new MessageParameter() { Msg = $"保存失败: {Name}", Duration = 1 });
  318. }
  319. }
  320. catch (Exception ex)
  321. {
  322. _eventAggregator.GetEvent<SnackbarMessageNotification>().Publish(new MessageParameter() { Msg = $"保存异常: {ex.Message}", Duration = 1 });
  323. }
  324. }
  325. private async Task TurnOnAsync()
  326. {
  327. try
  328. {
  329. var success = await _lightManagerService.TurnOnGlobalChannelAsync(_globalId);
  330. if (success)
  331. {
  332. IsOn = true;
  333. // refresh brightness
  334. var b = await _channel.GetBrightnessAsync();
  335. PendingBrightness = b;
  336. CurrentBrightness = b;
  337. _eventAggregator.GetEvent<SnackbarMessageNotification>().Publish(new MessageParameter() { Msg = $"打开通道: {Name}", Duration = 1 });
  338. }
  339. }
  340. catch (Exception ex)
  341. {
  342. _eventAggregator.GetEvent<SnackbarMessageNotification>().Publish(new MessageParameter() { Msg = $"打开异常: {ex.Message}", Duration = 1 });
  343. }
  344. }
  345. private async Task TurnOffAsync()
  346. {
  347. try
  348. {
  349. var success = await _lightManagerService.TurnOffGlobalChannelAsync(_globalId);
  350. if (success)
  351. {
  352. IsOn = false;
  353. PendingBrightness = 0;
  354. CurrentBrightness = 0;
  355. _eventAggregator.GetEvent<SnackbarMessageNotification>().Publish(new MessageParameter() { Msg = $"关闭通道: {Name}", Duration = 1 });
  356. }
  357. }
  358. catch (Exception ex)
  359. {
  360. _eventAggregator.GetEvent<SnackbarMessageNotification>().Publish(new MessageParameter() { Msg = $"关闭异常: {ex.Message}", Duration = 1 });
  361. }
  362. }
  363. }
  364. }
  365. }