using System; using System.Globalization; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Input; using System.Windows.Media; using TeamAAS.Global.Devices; using TeamAAS.Motion.Models; using TeamAAS.Motion.Motions; namespace TeamAAS.Motion { /// /// 运动卡通用调试页:轴选择 + 使能/回零/点动/定位/急停 + 本地/EtherCAT IO。 /// public partial class MotionDebugView : UserControl { private IMotionCard _motion; private IMotionSafetyHost _safety; private IMotionIoHost _io; private IAxis _selectedAxis; private bool _jogActive; private bool _updatingIo; private int _ioLayoutKey = int.MinValue; private readonly System.Windows.Threading.DispatcherTimer _refreshTimer; public MotionDebugView() { InitializeComponent(); _refreshTimer = new System.Windows.Threading.DispatcherTimer { Interval = TimeSpan.FromMilliseconds(200) }; _refreshTimer.Tick += (_, __) => RefreshStatus(); Unloaded += (_, __) => { _refreshTimer.Stop(); UnhookSafety(); }; } public void Bind(object motion) { UnhookSafety(); _motion = motion as IMotionCard; _safety = _motion as IMotionSafetyHost; _io = _motion as IMotionIoHost; _selectedAxis = null; _jogActive = false; _ioLayoutKey = int.MinValue; CmbAxis.ItemsSource = null; if (_motion == null) { OpsRoot.IsEnabled = false; IoRoot.IsEnabled = false; _refreshTimer.Stop(); TxtStatus.Text = "---"; TxtMessage.Text = "未绑定运动卡"; ClearAxisMetrics(); SetConnDot(false); ApplySafetyBanner(); RebuildIoPanels(); return; } if (_safety != null) _safety.SafetyChanged += OnSafetyChanged; var axes = _motion.GetAxes(); CmbAxis.ItemsSource = axes; if (axes != null && axes.Count > 0) CmbAxis.SelectedIndex = 0; OpsRoot.IsEnabled = _motion.IsConnected; IoRoot.IsEnabled = _motion.IsConnected; _refreshTimer.Start(); RefreshStatus(); } private void UnhookSafety() { if (_safety != null) _safety.SafetyChanged -= OnSafetyChanged; _safety = null; } private void OnSafetyChanged(object sender, EventArgs e) { Dispatcher.BeginInvoke(new Action(RefreshStatus)); } private IAxis CurrentAxis { get { if (_selectedAxis != null) return _selectedAxis; return CmbAxis.SelectedItem as IAxis; } } private void CmbAxis_SelectionChanged(object sender, SelectionChangedEventArgs e) { _selectedAxis = CmbAxis.SelectedItem as IAxis; RefreshAxisInfo(); } private double ParseDouble(TextBox box, double fallback) { return double.TryParse(box.Text, NumberStyles.Any, CultureInfo.InvariantCulture, out var v) ? v : fallback; } private void Msg(string message) => TxtMessage.Text = message; private bool TryGetReadyAxis(out IAxis axis, string action) { axis = CurrentAxis; if (axis == null) { Msg("未选择轴"); return false; } if (_motion == null || !_motion.IsConnected) { Msg("运动卡未连接"); return false; } if (!axis.IsEnabled) { Msg($"轴未使能,无法{action}"); return false; } if (_safety != null && !_safety.AllowMotion(out var reason)) { Msg(reason); return false; } return true; } private void RefreshStatus() { if (_motion == null) { TxtStatus.Text = "---"; SetConnDot(false); ClearAxisMetrics(); return; } try { var connected = _motion.IsConnected; TxtStatus.Text = connected ? "已连接" : "未连接"; SetConnDot(connected); OpsRoot.IsEnabled = connected; IoRoot.IsEnabled = connected && _io != null; RefreshAxisInfo(); RebuildIoPanels(); RefreshIoBits(); } catch { } } private void RefreshAxisInfo() { var axis = CurrentAxis; if (axis == null) { ClearAxisMetrics(); return; } TxtAxisTitle.Text = $"{axis.Name} · 轴 {axis.AxisNo}"; TxtPosActual.Text = axis.ActualPosition.ToString("F3"); TxtPosCommand.Text = axis.CommandPosition.ToString("F3"); TxtVelocity.Text = axis.ActualVelocity.ToString("F3"); ApplyBanner(axis); ApplySafetyBanner(); SetTile(TileEnable, TxtEnable, axis.IsEnabled, "已使能", "未使能", "#2E7D32", "#C62828"); SetTile(TileMoving, TxtMoving, axis.IsMoving, "运动中", "静止", "#EF6C00", "#757575"); SetTile(TileHomed, TxtHomed, axis.IsHomed, "已回零", "未回零", "#1565C0", "#757575"); var enabled = axis.IsEnabled; bool estop = _safety != null && _safety.IsEstopActive; bool stop = _safety != null && _safety.IsStopActive; BtnEnable.IsEnabled = !enabled && !estop; BtnDisable.IsEnabled = enabled; BtnStop.IsEnabled = axis.IsMoving; MotionOps.IsEnabled = enabled && !estop && !stop; if (estop) { TxtNeedEnable.Text = "急停有效:禁止使能与运动"; TxtNeedEnable.Visibility = Visibility.Visible; } else if (stop) { TxtNeedEnable.Text = "停止信号有效:禁止运动"; TxtNeedEnable.Visibility = Visibility.Visible; } else { TxtNeedEnable.Text = "当前未使能,请先点击「使能」后再点动或定位"; TxtNeedEnable.Visibility = enabled ? Visibility.Collapsed : Visibility.Visible; } } private void ApplySafetyBanner() { if (_safety == null || _motion == null || !_motion.IsConnected) { BannerSafety.Visibility = Visibility.Collapsed; return; } if (_safety.IsEstopActive) { BannerSafety.Visibility = Visibility.Visible; BannerSafety.Background = BrushOf("#B71C1C"); TxtSafetyHeadline.Text = "急停有效"; TxtSafetyHint.Text = "所有轴已下使能。手动调试与流程自动使能/运动均无法成功,请先复位急停按钮。"; return; } if (_safety.IsStopActive) { BannerSafety.Visibility = Visibility.Visible; BannerSafety.Background = BrushOf("#E65100"); TxtSafetyHeadline.Text = "停止信号有效"; TxtSafetyHint.Text = "所有轴禁止运动。手动调试与流程自动定位/点动/回零均无法成功,请先松开停止按钮。"; return; } BannerSafety.Visibility = Visibility.Collapsed; } private void ApplyBanner(IAxis axis) { string headline; string hint; string color; if (_motion == null || !_motion.IsConnected) { headline = "未连接"; hint = "请先在上方连接控制卡"; color = "#616161"; } else if (_safety != null && _safety.IsEstopActive) { headline = "急停有效"; hint = "禁止使能与运动,请先复位急停"; color = "#B71C1C"; } else if (_safety != null && _safety.IsStopActive) { headline = "停止信号有效"; hint = "禁止运动,请先松开停止按钮"; color = "#E65100"; } else if (axis.State == AxisState.Error) { headline = "轴故障"; hint = "请检查报警后重新使能"; color = "#B71C1C"; } else if (!axis.IsEnabled || axis.State == AxisState.Off) { headline = "伺服未使能"; hint = "当前无法运动,请先点击右侧「使能」"; color = "#C62828"; } else if (axis.State == AxisState.Homing) { headline = "正在回零"; hint = "回零过程中请勿点动或定位"; color = "#1565C0"; } else if (axis.State == AxisState.ContinuousMotion || (axis.IsMoving && _jogActive)) { headline = "点动中"; hint = "松开按钮或点击「停止」即可停下"; color = "#EF6C00"; } else if (axis.State == AxisState.DiscreteMotion || axis.IsMoving) { headline = "定位运动中"; hint = "可点击「停止」或「急停」中断"; color = "#EF6C00"; } else { headline = axis.IsHomed ? "已使能 · 静止" : "已使能 · 未回零"; hint = axis.IsHomed ? "可以点动或定位" : "建议先回零,再做定位"; color = "#2E7D32"; } TxtStateHeadline.Text = headline; TxtStateHint.Text = hint; BannerState.Background = BrushOf(color); } private void ClearAxisMetrics() { TxtAxisTitle.Text = "---"; TxtPosActual.Text = "---"; TxtPosCommand.Text = "---"; TxtVelocity.Text = "---"; TxtStateHeadline.Text = _motion == null ? "未绑定运动卡" : (_motion.IsConnected ? "未选择轴" : "未连接"); TxtStateHint.Text = "请选择轴并连接控制卡"; BannerState.Background = BrushOf("#616161"); SetTile(TileEnable, TxtEnable, false, "已使能", "未使能", "#2E7D32", "#757575"); SetTile(TileMoving, TxtMoving, false, "运动中", "静止", "#EF6C00", "#757575"); SetTile(TileHomed, TxtHomed, false, "已回零", "未回零", "#1565C0", "#757575"); BtnEnable.IsEnabled = false; BtnDisable.IsEnabled = false; BtnStop.IsEnabled = false; MotionOps.IsEnabled = false; TxtNeedEnable.Visibility = Visibility.Collapsed; ApplySafetyBanner(); } private static void SetTile(Border tile, TextBlock label, bool on, string onText, string offText, string onColor, string offColor) { label.Text = on ? onText : offText; tile.Background = BrushOf(on ? onColor : offColor); } private static SolidColorBrush BrushOf(string hex) => new SolidColorBrush((Color)ColorConverter.ConvertFromString(hex)); private void SetConnDot(bool connected) { DotConn.Fill = BrushOf(connected ? "#43A047" : "#9E9E9E"); } private void BtnEnable_Click(object sender, RoutedEventArgs e) { var axis = CurrentAxis; if (axis == null) { Msg("未选择轴"); return; } if (_safety != null && !_safety.AllowEnable(out var reason)) { Msg(reason); return; } try { axis.Enable(); Msg(axis.IsEnabled ? "已使能" : "使能失败"); RefreshAxisInfo(); } catch (Exception ex) { Msg("使能失败: " + ex.Message); } } private void BtnDisable_Click(object sender, RoutedEventArgs e) { var axis = CurrentAxis; if (axis == null) { Msg("未选择轴"); return; } try { StopJogIfActive(); axis.Disable(); Msg("已下使能"); RefreshAxisInfo(); } catch (Exception ex) { Msg("下使能失败: " + ex.Message); } } private void BtnHome_Click(object sender, RoutedEventArgs e) { if (!TryGetReadyAxis(out var axis, "回零")) return; try { StopJogIfActive(); axis.Home(); Msg("回零完成"); RefreshAxisInfo(); } catch (Exception ex) { Msg("回零失败: " + ex.Message); } } private void BtnStop_Click(object sender, RoutedEventArgs e) { try { StopJogIfActive(); CurrentAxis?.Stop(); Msg("已停止"); RefreshAxisInfo(); } catch (Exception ex) { Msg("停止失败: " + ex.Message); } } private void BtnEStop_Click(object sender, RoutedEventArgs e) { try { _jogActive = false; _motion?.EmergencyStop(); Msg("急停已触发"); RefreshAxisInfo(); } catch (Exception ex) { Msg("急停失败: " + ex.Message); } } private void BtnJog_Down(object sender, MouseButtonEventArgs e) { if (!TryGetReadyAxis(out var axis, "点动")) return; var btn = sender as UIElement; var tag = (sender as Button)?.Tag as string; double vel = ParseDouble(TxtJogVel, 50); if (tag == "-") vel = -vel; try { axis.Jog(vel); _jogActive = true; btn?.CaptureMouse(); Msg($"点动 {tag} 速度 {vel:F1}"); } catch (Exception ex) { Msg("点动失败: " + ex.Message); } } private void BtnJog_Up(object sender, MouseButtonEventArgs e) { var btn = sender as UIElement; if (btn != null && btn.IsMouseCaptured) btn.ReleaseMouseCapture(); else StopJogIfActive(); } private void BtnJog_LostCapture(object sender, MouseEventArgs e) { StopJogIfActive(); } private void StopJogIfActive() { if (!_jogActive) return; _jogActive = false; try { CurrentAxis?.Stop(); Msg("点动停止"); RefreshAxisInfo(); } catch (Exception ex) { Msg("停止失败: " + ex.Message); } } private void BtnMoveAbs_Click(object sender, RoutedEventArgs e) { if (!TryGetReadyAxis(out var axis, "定位")) return; try { var target = ParseDouble(TxtTargetPos, 0); axis.Velocity = ParseDouble(TxtMoveVel, 100); axis.MoveTo(target); Msg($"绝对定位 → {target:F3}"); } catch (Exception ex) { Msg("绝对定位失败: " + ex.Message); } } private void BtnMoveRel_Click(object sender, RoutedEventArgs e) { if (!TryGetReadyAxis(out var axis, "定位")) return; try { var dist = ParseDouble(TxtRelativeDist, 10); axis.Velocity = ParseDouble(TxtMoveVel, 100); axis.MoveBy(dist); Msg($"相对定位 {dist:F3}"); } catch (Exception ex) { Msg("相对定位失败: " + ex.Message); } } private void BtnSetZero_Click(object sender, RoutedEventArgs e) { var axis = CurrentAxis; if (axis == null) { Msg("未选择轴"); return; } try { axis.SetPosition(0); Msg("当前位置已清零"); RefreshAxisInfo(); } catch (Exception ex) { Msg("清零失败: " + ex.Message); } } private MotionDeviceConfig CurrentIoConfig { get { if (_motion is SimulatedMotionCard sim) return sim.DeviceConfig; if (_motion is InovanceMotionCard imc) return imc.DeviceConfig; return null; } } private void RebuildIoPanels() { int diL = _io == null ? 0 : _io.GetDiCount(MotionIoBank.Local); int doL = _io == null ? 0 : _io.GetDoCount(MotionIoBank.Local); int diE = _io == null ? 0 : _io.GetDiCount(MotionIoBank.EtherCat); int doE = _io == null ? 0 : _io.GetDoCount(MotionIoBank.EtherCat); int key = diL | (doL << 8) | (diE << 16) | (doE << 24); if (_io != null && _io.CanSimulateDi) key ^= 0x40000000; if (key == _ioLayoutKey) return; _ioLayoutKey = key; FillIoPanel(PanelLocalDi, MotionIoBank.Local, false, diL); FillIoPanel(PanelLocalDo, MotionIoBank.Local, true, doL); FillIoPanel(PanelEcatDi, MotionIoBank.EtherCat, false, diE); FillIoPanel(PanelEcatDo, MotionIoBank.EtherCat, true, doE); } private void FillIoPanel(WrapPanel panel, MotionIoBank bank, bool isDo, int count) { panel.Children.Clear(); if (count <= 0) { panel.Children.Add(new TextBlock { Text = "无", FontSize = 12, Foreground = BrushOf("#9E9E9E"), Margin = new Thickness(0, 4, 0, 0) }); return; } string prefix = isDo ? "DO" : "DI"; bool canWrite = isDo || (_io != null && _io.CanSimulateDi); for (int i = 0; i < count; i++) { var btn = new ToggleButton { Content = prefix + i, Width = 52, Height = 34, Margin = new Thickness(0, 0, 6, 6), FontSize = 11, Tag = new IoBitTag { Bank = bank, IsOutput = isDo, Index = i, CanWrite = canWrite }, Cursor = canWrite ? Cursors.Hand : Cursors.Arrow }; btn.Style = (Style)FindResource("IoBitButton"); btn.Click += IoBit_Click; panel.Children.Add(btn); } } private void RefreshIoBits() { if (_io == null) return; _updatingIo = true; try { RefreshIoPanel(PanelLocalDi, MotionIoBank.Local, false); RefreshIoPanel(PanelLocalDo, MotionIoBank.Local, true); RefreshIoPanel(PanelEcatDi, MotionIoBank.EtherCat, false); RefreshIoPanel(PanelEcatDo, MotionIoBank.EtherCat, true); } finally { _updatingIo = false; } } private void RefreshIoPanel(WrapPanel panel, MotionIoBank bank, bool isDo) { foreach (var child in panel.Children) { var btn = child as ToggleButton; if (btn == null) continue; var tag = btn.Tag as IoBitTag; if (tag == null) continue; bool on = isDo ? _io.ReadDO(bank, tag.Index) : _io.ReadDI(bank, tag.Index); btn.IsChecked = on; string role = SafetyRole(bank, tag.Index, isDo); if (role == "estop") btn.Background = BrushOf(on ? "#B71C1C" : "#7F1D1D"); else if (role == "stop") btn.Background = BrushOf(on ? "#E65100" : "#BF360C"); else btn.Background = BrushOf(on ? "#2E7D32" : "#757575"); btn.Foreground = Brushes.White; btn.ToolTip = role == "estop" ? "急停 DI" : role == "stop" ? "停止 DI" : (isDo ? "输出" : "输入"); } } private string SafetyRole(MotionIoBank bank, int index, bool isDo) { if (isDo) return null; var cfg = CurrentIoConfig; if (cfg == null) return null; if (cfg.EstopDiIndex == index && cfg.EstopDiBank == bank) return "estop"; if (cfg.StopDiIndex == index && cfg.StopDiBank == bank) return "stop"; return null; } private void IoBit_Click(object sender, RoutedEventArgs e) { if (_updatingIo) return; var btn = sender as ToggleButton; var tag = btn == null ? null : btn.Tag as IoBitTag; if (tag == null || _io == null || _motion == null || !_motion.IsConnected) return; bool on = btn.IsChecked == true; bool ok; if (tag.IsOutput) { ok = _io.WriteDO(tag.Bank, tag.Index, on); Msg(ok ? $"{BankName(tag.Bank)} DO{tag.Index} = {(on ? 1 : 0)}" : "写 DO 失败"); } else if (tag.CanWrite) { ok = _io.WriteDI(tag.Bank, tag.Index, on); Msg(ok ? $"{BankName(tag.Bank)} DI{tag.Index} = {(on ? 1 : 0)}" : "写 DI 失败"); } else { RefreshIoBits(); return; } if (!ok) RefreshIoBits(); } private static string BankName(MotionIoBank bank) => bank == MotionIoBank.EtherCat ? "EtherCAT" : "本地"; private sealed class IoBitTag { public MotionIoBank Bank; public bool IsOutput; public int Index; public bool CanWrite; } } /// 运动卡通用调试页工厂。 public class MotionDebugPageFactory : TeamAAS.Global.Devices.IDeviceDebugPageFactory { public string DeviceCategory => "运动卡"; public object CreatePage(object device) { var view = new MotionDebugView(); view.Bind(device); return view; } } }