using System; using System.Collections.ObjectModel; using System.Linq; using System.Threading.Tasks; using TeamAAS.Feeder.Interfaces; using TeamAAS.Feeder.Models; using TeamAAS.Feeder.UI; using TeamAAS.Feeder.Enums; namespace TeamAAS.ViewModels { public partial class SettingViewModel { public ObservableCollection Feeders { get; } private System.ComponentModel.PropertyChangedEventHandler _feederInfoHandler; private FeederInfo _selectedFeeder; public FeederInfo SelectedFeeder { get { return _selectedFeeder; } set { if (SetProperty(ref _selectedFeeder, value)) { // 监听选中供料器的品牌变化:品牌决定连接方式约束与调试页 if (_feederInfoHandler == null) { _feederInfoHandler = (s, e) => { if (e.PropertyName == nameof(FeederInfo.FeederBrand)) { // Team 品牌只支持 TCP 连接 if (_selectedFeeder?.FeederBrand == FeederBrand.Team && _selectedFeeder.ConnectType != FeederConnectType.TCP) { _selectedFeeder.ConnectType = FeederConnectType.TCP; } RaisePropertyChanged(nameof(ShowSerialConnection)); RefreshFeederDebugHost(); } }; } if (_selectedFeeder != null) { _selectedFeeder.PropertyChanged -= _feederInfoHandler; _selectedFeeder.PropertyChanged += _feederInfoHandler; } RaisePropertyChanged(nameof(HasSelectedFeeder)); RaisePropertyChanged(nameof(CurrentAction)); RaisePropertyChanged(nameof(HasConnectedDevice)); RaisePropertyChanged(nameof(CurrentSequences)); RaisePropertyChanged(nameof(ShowSerialConnection)); SelectedSequence = _selectedFeeder?.Sequences?.FirstOrDefault(); RefreshFeederDebugHost(); } } } private object _currentFeederDebugPage; /// 品牌专属调试页(由 [Feeder] 特性 DebugPageType 创建);null = 使用通用手动调试面板 public object CurrentFeederDebugPage { get { return _currentFeederDebugPage; } private set { if (SetProperty(ref _currentFeederDebugPage, value)) RaisePropertyChanged(nameof(HasFeederCustomPage)); } } /// 选中品牌是否声明了专属调试页(true=显示专属页,false=显示通用手动调试面板) public bool HasFeederCustomPage => _currentFeederDebugPage != null; /// 是否显示串口连接参数(Team 品牌仅支持 TCP,不显示) public bool ShowSerialConnection => _selectedFeeder != null && _selectedFeeder.FeederBrand != FeederBrand.Team; /// 按选中的供料器刷新调试区:有专属页则创建并绑定,否则回退通用手动调试面板 private void RefreshFeederDebugHost() { (_currentFeederDebugPage as IFeederDebugPage)?.UnbindFeeder(); CurrentFeederDebugPage = null; if (_selectedFeeder == null) return; try { var feeder = _feederManager.GetOrCreateDevice(_selectedFeeder); if (feeder == null) return; CurrentFeederDebugPage = FeederDebugPageFactory.Create(feeder); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"加载供料器调试页失败: {ex.Message}"); } } public bool HasSelectedFeeder => _selectedFeeder != null; private IFeeder _activeDevice; public bool HasConnectedDevice => _activeDevice?.IsConnected ?? false; #region 调试属性 public int[] VibrationSetList { get; } = Enumerable.Range(1, 11).ToArray(); public string[] ActionLetterList { get; } = { "← A 左", "↙ B 左下", "↖ C 左上", "↓ D 下", "↑ E 上", "→ F 右", "↘ G 右下", "↗ H 右上", "⤨ I 散开", "⇔ J 水平聚集", "⇕ K 垂直聚集" }; public WaveShape[] WaveShapeList { get; } = { WaveShape.None, WaveShape.SignWave, WaveShape.SawToothWave, WaveShape.TriangleWave }; private int _selectedVibrationSetIndex; public int SelectedVibrationSetIndex { get { return _selectedVibrationSetIndex; } set { if (SetProperty(ref _selectedVibrationSetIndex, value)) RaisePropertyChanged(nameof(CurrentAction)); } } private int _selectedActionIndex; public int SelectedActionIndex { get { return _selectedActionIndex; } set { if (SetProperty(ref _selectedActionIndex, value)) RaisePropertyChanged(nameof(CurrentAction)); } } private int _currentDuration = 500; public int CurrentDuration { get { return _currentDuration; } set { SetProperty(ref _currentDuration, value); } } public SingleActionParam CurrentAction { get { if (SelectedFeeder == null || SelectedFeeder.ActionGroups == null) return null; int idx = SelectedVibrationSetIndex < 0 ? 0 : SelectedVibrationSetIndex > 10 ? 10 : SelectedVibrationSetIndex; return SelectedFeeder.ActionGroups[idx]; } } private bool _isSyncChanging; public bool IsSyncChanging { get { return _isSyncChanging; } set { SetProperty(ref _isSyncChanging, value); } } private int _globalAmplitude = 50; public int GlobalAmplitude { get { return _globalAmplitude; } set { if (SetProperty(ref _globalAmplitude, value) && IsSyncChanging && CurrentAction != null) { CurrentAction.Motor1.Amplitude = (ushort)value; CurrentAction.Motor2.Amplitude = (ushort)value; CurrentAction.Motor3.Amplitude = (ushort)value; CurrentAction.Motor4.Amplitude = (ushort)value; } } } private int _globalFrequency = 100; public int GlobalFrequency { get { return _globalFrequency; } set { if (SetProperty(ref _globalFrequency, value) && IsSyncChanging && CurrentAction != null) { CurrentAction.Motor1.Frequency = (ushort)value; CurrentAction.Motor2.Frequency = (ushort)value; CurrentAction.Motor3.Frequency = (ushort)value; CurrentAction.Motor4.Frequency = (ushort)value; } } } private string _feederStatusText = "未连接"; public string FeederStatusText { get { return _feederStatusText; } set { SetProperty(ref _feederStatusText, value); } } #endregion private void AddFeeder() { try { int maxNo = Feeders.Count > 0 ? Feeders.Max(f => f.FeederNo) : 0; var newFeeder = new FeederInfo { FeederNo = maxNo + 1, FeederName = $"供料器{maxNo + 1}", }; _feederManager.Add(newFeeder); RefreshFeederList(); SelectedFeeder = Feeders.FirstOrDefault(f => f.Id == newFeeder.Id); } catch (Exception ex) { DialogHelper.ShowError("添加供料器失败", ex); } } private void DeleteFeeder() { if (SelectedFeeder == null) return; if (!DialogHelper.ConfirmYesNo( $"确认删除 \"{SelectedFeeder.FeederName}\" 吗?", "确认删除")) { return; } try { _feederManager.Remove(SelectedFeeder.Id); RefreshFeederList(); SelectedFeeder = null; } catch (Exception ex) { DialogHelper.ShowError("删除供料器失败", ex); } } private void SaveFeederConfig() { try { if (_feederManager.SaveConfig()) DialogHelper.Success("供料器配置已保存"); else DialogHelper.ShowError("供料器配置保存失败"); } catch (Exception ex) { DialogHelper.ShowError("保存供料器配置失败", ex); } } public void ApplyFeederEdit(FeederInfo edited) { try { _feederManager.Add(edited); _feederManager.SaveConfig(); RefreshFeederList(); DialogHelper.Success("供料器属性已更新并保存"); } catch (Exception ex) { DialogHelper.ShowError("应用供料器属性失败", ex); } } private void RefreshFeederList() { // 只恢复用户此前显式选中的供料器;列表加载本身不产生选中项 //(WPF 选择器默认会自动选中首项,导致未选设备时调试页就被创建) Guid? selectedId = _selectedFeeder?.Id; Feeders.Clear(); var infos = _feederManager.GetAll(); foreach (var info in infos) Feeders.Add(info); if (selectedId.HasValue) SelectedFeeder = Feeders.FirstOrDefault(f => f.Id == selectedId.Value); DeleteFeederCommand?.RaiseCanExecuteChanged(); } #region 硬件通讯 private async Task ConnectFeederAsync() { if (SelectedFeeder == null) return; try { FeederStatusText = $"正在连接 {SelectedFeeder.FeederName} ({SelectedFeeder.IP}:{SelectedFeeder.Port})..."; _activeDevice = _feederManager.GetOrCreateDevice(SelectedFeeder); _activeDevice.MessageExchanged += OnFeederMessageExchanged; _activeDevice.ConnectionChanged += (dev, online) => { RaisePropertyChanged(nameof(HasConnectedDevice)); FeederStatusText = online ? $"{dev.Name} 已就绪" : "连接断开"; }; await _activeDevice.ConnectAsync(); AppLogger.Info($"供料器已连接: {SelectedFeeder.FeederName} ({SelectedFeeder.IP}:{SelectedFeeder.Port})", "Feeder"); FeederStatusText = $"{SelectedFeeder.FeederName} 已就绪"; RaisePropertyChanged(nameof(HasConnectedDevice)); } catch (Exception ex) { FeederStatusText = "连接失败"; DialogHelper.ShowError("供料器连接失败", ex); } } private async void DisconnectFeeder() { if (_activeDevice == null) return; try { await _activeDevice.DisconnectAsync(); AppLogger.Info($"供料器已断开: {_activeDevice.Name}", "Feeder"); FeederStatusText = "已断开"; } catch (Exception ex) { DialogHelper.ShowError("断开连接失败", ex); } RaisePropertyChanged(nameof(HasConnectedDevice)); } private async void ToggleBacklight() { if (_activeDevice == null || !_activeDevice.IsConnected) { DialogHelper.Warning("请先连接供料器"); return; } try { bool currentlyOn = await _activeDevice.QueryBacklightAsync(); if (currentlyOn) { await _activeDevice.CloseBacklightAsync(); SelectedFeeder.IsBacklightOn = false; FeederStatusText = "背光关"; } else { await _activeDevice.OpenBacklightAsync(); SelectedFeeder.IsBacklightOn = true; FeederStatusText = "背光开"; } } catch (Exception ex) { DialogHelper.ShowError("背光控制失败", ex); } } private async Task ExecuteDirectionAsync(int dir) { if (_activeDevice == null || !_activeDevice.IsConnected) { DialogHelper.Warning("请先连接供料器"); return; } try { var action = (FeederAction)dir; FeederStatusText = $"执行方向: {action}"; await _activeDevice.RunDirectionAsync(action, CurrentDuration); AppLogger.Info($"供料器方向执行: {action} 持续 {CurrentDuration}ms", "Feeder"); FeederStatusText = $"方向 {action} 执行完成"; } catch (Exception ex) { FeederStatusText = "执行失败"; DialogHelper.ShowError("方向执行失败", ex); } } private async Task StopFeederAsync() { if (_activeDevice == null) return; try { await _activeDevice.StopAsync(); FeederStatusText = "停止振动"; } catch (Exception ex) { DialogHelper.ShowError("停止失败", ex); } } private async Task ExecuteCurrentActionAsync() { if (_activeDevice == null || !_activeDevice.IsConnected) { DialogHelper.Warning("请先连接供料器"); return; } try { // 先下载当前振动集参数到设备 await _activeDevice.SetActionParamAsync(SelectedVibrationSetIndex, CurrentAction); // 再执行 await _activeDevice.RunDirectionAsync((FeederAction)SelectedActionIndex, CurrentDuration); FeederStatusText = $"执行振动集 {SelectedVibrationSetIndex + 1} ({ActionLetterList[SelectedActionIndex]})"; } catch (Exception ex) { FeederStatusText = "执行失败"; DialogHelper.ShowError("执行动作失败", ex); } } private async Task DownloadFeederParamsAsync() { if (_activeDevice == null || !_activeDevice.IsConnected) { DialogHelper.Warning("请先连接供料器"); return; } if (SelectedFeeder == null) return; try { FeederStatusText = "下载参数中..."; await _activeDevice.DownloadAllParamsAsync(SelectedFeeder); AppLogger.Info($"供料器下载振动集参数: {SelectedFeeder.FeederName}", "Feeder"); await _activeDevice.SaveToDeviceAsync(); FeederStatusText = "参数已下载并保存到设备"; DialogHelper.Success("参数已下载到设备"); } catch (Exception ex) { FeederStatusText = "下载失败"; DialogHelper.ShowError("下载参数失败", ex); } } #endregion #region 配方 private ObservableCollection _recipes = new ObservableCollection(); public ObservableCollection Recipes { get { return _recipes; } set { SetProperty(ref _recipes, value); } } private FeederRecipe _selectedRecipe; public FeederRecipe SelectedRecipe { get { return _selectedRecipe; } set { SetProperty(ref _selectedRecipe, value); } } private void SaveCurrentAsRecipe() { if (SelectedFeeder == null) { DialogHelper.Warning("请先选择供料器"); return; } string name = DialogHelper.Input("配方名称", "保存配方", SelectedFeeder.FeederName + "_配方"); if (string.IsNullOrWhiteSpace(name)) return; if (Recipes.Any(r => string.Equals(r.RecipeName, name, StringComparison.OrdinalIgnoreCase))) { DialogHelper.Warning($"配方 \"{name}\" 已存在,请换一个名称"); return; } try { var recipe = new FeederRecipe { RecipeName = name, Description = $"从 {SelectedFeeder.FeederName} 导出", Brand = SelectedFeeder.FeederBrand, }; foreach (var g in SelectedFeeder.ActionGroups) recipe.ActionGroups.Add(g.Clone()); foreach (var s in SelectedFeeder.Sequences) recipe.Sequences.Add(s.Clone()); if (_feederManager.SaveRecipe(recipe)) { DialogHelper.Success($"配方 \"{name}\" 已保存"); RefreshRecipeList(); } else { DialogHelper.ShowError("配方保存失败"); } } catch (Exception ex) { DialogHelper.ShowError("保存配方失败", ex); } } private void LoadRecipeToCurrent() { if (SelectedRecipe == null) { DialogHelper.Warning("请先选择配方"); return; } if (SelectedFeeder == null) { DialogHelper.Warning("请先选择供料器"); return; } try { SelectedFeeder.ActionGroups.Clear(); foreach (var g in SelectedRecipe.ActionGroups) SelectedFeeder.ActionGroups.Add(g.Clone()); SelectedFeeder.Sequences.Clear(); foreach (var s in SelectedRecipe.Sequences) SelectedFeeder.Sequences.Add(s.Clone()); SelectedSequence = SelectedFeeder.Sequences.FirstOrDefault(); RaisePropertyChanged(nameof(CurrentSequences)); SaveFeederConfigSilent(); DialogHelper.Success($"配方 \"{SelectedRecipe.RecipeName}\" 已加载到 {SelectedFeeder.FeederName}"); } catch (Exception ex) { DialogHelper.ShowError("加载配方失败", ex); } } private async Task ApplyRecipeToDevice() { if (SelectedRecipe == null) { DialogHelper.Warning("请先选择配方"); return; } if (_activeDevice == null || !_activeDevice.IsConnected) { DialogHelper.Warning("请先连接供料器"); return; } try { FeederStatusText = $"应用配方 \"{SelectedRecipe.RecipeName}\"..."; for (int i = 0; i < SelectedRecipe.ActionGroups.Count && i < 11; i++) { await _activeDevice.SetActionParamAsync(i, SelectedRecipe.ActionGroups[i]); } await _activeDevice.SaveToDeviceAsync(); FeederStatusText = "配方已应用到设备"; DialogHelper.Success("配方已下载到设备"); } catch (Exception ex) { FeederStatusText = "配方应用失败"; DialogHelper.ShowError("配方应用失败", ex); } } private void DeleteSelectedRecipe() { if (SelectedRecipe == null) return; if (!DialogHelper.ConfirmYesNo($"确认删除配方 \"{SelectedRecipe.RecipeName}\" 吗?", "确认删除")) return; if (_feederManager.DeleteRecipe(SelectedRecipe.RecipeName)) { DialogHelper.Success("配方已删除"); RefreshRecipeList(); } } private void RefreshRecipeList() { Recipes.Clear(); foreach (var r in _feederManager.GetRecipes()) Recipes.Add(r); } #endregion #region 序列集(送料流程) public ObservableCollection CurrentSequences => _selectedFeeder?.Sequences; private SequenceSet _selectedSequence; public SequenceSet SelectedSequence { get { return _selectedSequence; } set { if (SetProperty(ref _selectedSequence, value)) { RaisePropertyChanged(nameof(CurrentSequenceSteps)); } } } public ObservableCollection CurrentSequenceSteps => _selectedSequence?.Steps; private SequenceStep _selectedStep; public SequenceStep SelectedStep { get { return _selectedStep; } set { if (SetProperty(ref _selectedStep, value)) RaisePropertyChanged(nameof(HasSelectedStep)); } } public bool HasSelectedStep => _selectedStep != null; private string _sequenceStatusText = ""; public string SequenceStatusText { get { return _sequenceStatusText; } set { SetProperty(ref _sequenceStatusText, value); } } private System.Threading.CancellationTokenSource _sequenceCts; private void AddSequence() { if (SelectedFeeder == null) { DialogHelper.Warning("请先选择供料器"); return; } string name = DialogHelper.Input("流程名称", "新建流程", "新流程"); if (string.IsNullOrWhiteSpace(name)) return; if (SelectedFeeder.Sequences.Any(s => string.Equals(s.Name, name, StringComparison.OrdinalIgnoreCase))) { DialogHelper.Warning($"流程 \"{name}\" 已存在,请换一个名称"); return; } var seq = new SequenceSet { Name = name }; SelectedFeeder.Sequences.Add(seq); SelectedSequence = seq; RenumberSteps(); SaveFeederConfigSilent(); } private void DeleteSequence() { if (SelectedSequence == null) return; if (!DialogHelper.ConfirmYesNo($"确认删除流程 \"{SelectedSequence.Name}\" 吗?", "确认")) return; var list = CurrentSequences; int idx = list.IndexOf(SelectedSequence); list.Remove(SelectedSequence); SelectedSequence = list.Count > 0 ? list[Math.Min(idx, list.Count - 1)] : null; RaisePropertyChanged(nameof(CurrentSequenceSteps)); RenumberSteps(); SaveFeederConfigSilent(); } private void RenameSequence() { if (SelectedSequence == null) return; string name = DialogHelper.Input("流程名称", "重命名", SelectedSequence.Name); if (string.IsNullOrWhiteSpace(name)) return; if (!string.Equals(name, SelectedSequence.Name, StringComparison.OrdinalIgnoreCase) && SelectedFeeder.Sequences.Any(s => string.Equals(s.Name, name, StringComparison.OrdinalIgnoreCase))) { DialogHelper.Warning($"流程 \"{name}\" 已存在,请换一个名称"); return; } SelectedSequence.Name = name; SaveFeederConfigSilent(); } private void AddSequenceStep(string type) { if (SelectedSequence == null) { DialogHelper.Warning("请先选择或创建一个流程"); return; } var step = new SequenceStep { StepType = type == "Delay" ? SequenceStepType.Delay : SequenceStepType.Direction, TimeMs = type == "Delay" ? 300 : 500, Direction = FeederAction.Right, Enabled = true, }; SelectedSequence.Steps.Add(step); RenumberSteps(); SaveFeederConfigSilent(); } private void MoveUpStep() { if (SelectedStep == null || CurrentSequenceSteps == null) return; int idx = CurrentSequenceSteps.IndexOf(SelectedStep); if (idx <= 0) return; CurrentSequenceSteps.Move(idx, idx - 1); SelectedStep = CurrentSequenceSteps[idx - 1]; RenumberSteps(); SaveFeederConfigSilent(); } private void MoveDownStep() { if (SelectedStep == null || CurrentSequenceSteps == null) return; int idx = CurrentSequenceSteps.IndexOf(SelectedStep); if (idx < 0 || idx >= CurrentSequenceSteps.Count - 1) return; CurrentSequenceSteps.Move(idx, idx + 1); SelectedStep = CurrentSequenceSteps[idx + 1]; RenumberSteps(); SaveFeederConfigSilent(); } private void DeleteStep() { if (SelectedStep == null || CurrentSequenceSteps == null) return; CurrentSequenceSteps.Remove(SelectedStep); SelectedStep = null; RenumberSteps(); SaveFeederConfigSilent(); } private void RenumberSteps() { if (CurrentSequenceSteps == null) return; for (int i = 0; i < CurrentSequenceSteps.Count; i++) CurrentSequenceSteps[i].Index = i + 1; } private void SaveFeederConfigSilent() { if (SelectedFeeder == null) return; try { _feederManager.SaveConfig(); } catch { } } private async Task ExecuteSequenceAsync() { if (SelectedSequence == null) { DialogHelper.Warning("请先选择流程"); return; } if (_activeDevice == null || !_activeDevice.IsConnected) { DialogHelper.Warning("请先连接供料器"); return; } _sequenceCts = new System.Threading.CancellationTokenSource(); var token = _sequenceCts.Token; try { int stepIdx = 0; foreach (var step in SelectedSequence.Steps) { stepIdx++; if (!step.Enabled) continue; if (token.IsCancellationRequested) break; if (step.StepType == SequenceStepType.Delay) { SequenceStatusText = $"等待 {step.TimeMs}ms... ({stepIdx}/{SelectedSequence.Steps.Count})"; await Task.Delay(step.TimeMs, token); } else { SequenceStatusText = $"{step.Direction} 振动 {step.TimeMs}ms ({stepIdx}/{SelectedSequence.Steps.Count})"; await _activeDevice.RunDirectionAsync(step.Direction, step.TimeMs > 0 ? step.TimeMs : (int?)null); } } if (token.IsCancellationRequested) SequenceStatusText = "流程已停止"; else SequenceStatusText = "流程执行完成"; } catch (TaskCanceledException) { SequenceStatusText = "流程已停止"; } catch (Exception ex) { SequenceStatusText = "流程执行异常"; DialogHelper.ShowError("流程执行异常", ex); } } private void StopSequence() { _sequenceCts?.Cancel(); try { _activeDevice?.StopAsync(); } catch { } } #endregion } }