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;
}
}
}