This commit is contained in:
backuppc
2025-12-12 17:27:50 +09:00
parent 07ddc0425f
commit 4e9d29d22f
44 changed files with 7173 additions and 61 deletions

View File

@@ -1775,6 +1775,11 @@ namespace AGVNavigationCore.Controls
/// </summary>
private Color GetAGVCenterColor(AGVState state)
{
// Stop-Mark 상태 (Error 상태와 유사하게 처리하거나 별도 처리)
// 여기서는 AGVState에 StopMark가 없으므로, 외부에서 상태를 Error로 설정하거나
// 별도의 플래그를 확인해야 함. 하지만 IAGV 인터페이스에는 플래그가 없음.
// 따라서 fMain에서 상태를 Error로 설정하는 것을 권장.
switch (state)
{
case AGVState.Moving: return Color.White;
@@ -1794,7 +1799,7 @@ namespace AGVNavigationCore.Controls
{
case AGVState.Moving: return Color.DarkGreen;
case AGVState.Charging: return Color.DarkBlue;
case AGVState.Error: return Color.DarkRed;
case AGVState.Error: return Color.DarkRed; // Stop-Mark 시 Error 상태 사용
case AGVState.Docking: return Color.DarkOrange;
default: return Color.DarkGray;
}

View File

@@ -17,6 +17,16 @@ namespace AGVNavigationCore.Controls
var worldPoint = ScreenToWorld(e.Location);
var hitNode = GetNodeAt(worldPoint);
// 에뮬레이터 모드 처리
if (_canvasMode == CanvasMode.Emulator)
{
if (e.Button == MouseButtons.Right && hitNode != null)
{
NodeRightClicked?.Invoke(this, hitNode);
}
return;
}
// 🔥 어떤 모드에서든 노드/빈 공간 클릭 시 선택 이벤트 발생 (속성창 업데이트)
bool ctrlPressed = (ModifierKeys & Keys.Control) == Keys.Control;

View File

@@ -36,7 +36,8 @@ namespace AGVNavigationCore.Controls
public enum CanvasMode
{
Edit, // 편집 가능 (맵 에디터)
Sync // 동기화 모드 (장비 설정 동기화)
Sync, // 동기화 모드 (장비 설정 동기화)
Emulator // 에뮬레이터 모드
}
/// <summary>
@@ -146,6 +147,9 @@ namespace AGVNavigationCore.Controls
// 컨텍스트 메뉴
private ContextMenuStrip _contextMenu;
// 이벤트
public event EventHandler<MapNode> NodeRightClicked;
#endregion
#region Events

View File

@@ -81,6 +81,13 @@ namespace AGVNavigationCore.Models
private List<string> _detectedRfids = new List<string>(); // 감지된 RFID 목록
private bool _isPositionConfirmed = false; // 위치 확정 여부 (RFID 2개 이상 감지)
// 에뮬레이터용 추가 속성
public double Angle { get; set; } = 0; // 0 = Right, 90 = Down, 180 = Left, 270 = Up (Standard Math)
// But AGV Direction: Forward usually means "Front of AGV".
// Let's assume Angle is the orientation of the AGV in degrees.
public bool IsStopMarkOn { get; set; } = false;
#endregion
#region Properties
@@ -433,6 +440,27 @@ namespace AGVNavigationCore.Models
OnError("긴급 정지가 실행되었습니다.");
}
/// <summary>
/// 일시 정지 (경로 유지)
/// </summary>
public void Pause()
{
_isMoving = false;
_currentSpeed = 0;
}
/// <summary>
/// 이동 재개
/// </summary>
public void Resume()
{
if (_currentPath != null && _remainingNodes != null && _remainingNodes.Count > 0)
{
_isMoving = true;
SetState(AGVState.Moving);
}
}
#endregion
#region Public Methods - ( )

View File

@@ -15,6 +15,8 @@ using Newtonsoft.Json;
using AGVNavigationCore.PathFinding.Planning;
using AGVNavigationCore.PathFinding.Core;
using AGVSimulator.Models;
using System.IO.Ports;
using System.Text;
namespace AGVSimulator.Forms
{
@@ -25,6 +27,86 @@ namespace AGVSimulator.Forms
{
#region Fields
// Emulator Fields
private SerialPort _emulatorPort;
private ComboBox _portCombo;
private Button _connectButton;
private bool _isEmulatorConnected = false;
// Emulator State Fields
private UInt16 _emu_system0 = 0;
private UInt16 _emu_system1 = 0;
private UInt16 _emu_error = 0;
private byte _emu_signal = 0;
private char _emu_sts_bunki = 'S';
private char _emu_sts_speed = 'L';
private char _emu_sts_dir = 'F';
private char _emu_sts_sensor = '1';
private string _lastSentNodeId = null;
public enum esystemflag0
{
Memory_RW_State = 5,
EXT_IO_Conn_State,
RFID_Conn_State,
M5E_Module_Run_State = 8,
Front_Ultrasonic_Conn_State,
Front_Untrasonic_Sensor_State,
Side_Ultrasonic_Conn_State,
Side_Ultrasonic_Sensor_State = 12,
Front_Guide_Sensor_State,
Rear_Guide_Sensor_State,
Battery_Level_Check
}
public enum esystemflag1
{
Side_Detect_Ignore = 3,
Melody_check,
Mark2_check,
Mark1_check,
gateout_check,
Battery_charging = 8,
re_Start,
front_detect_ignore,
front_detect_check,
stop_by_front_detect = 12,
stop_by_cross_in,
agv_stop,
agv_run
}
public enum eerror
{
Emergency = 0,
Overcurrent,
Charger_run_error,
Charger_pos_error,
line_out_error = 4,
runerror_by_no_magent_line,
controller_comm_error = 11,
arrive_ctl_comm_error,
door_ctl_comm_error,
charger_comm_error,
cross_ctrl_comm_error,
}
public enum esignal
{
front_gate_out = 0,
rear_sensor_out,
mark_sensor_1,
mark_sensor_2,
front_left_sensor,
front_right_sensor,
front_center_sensor,
charger_align_sensor,
}
public enum estsvaluetype
{
bunki,
speed,
direction,
sensor
}
private UnifiedAGVCanvas _simulatorCanvas;
private List<MapNode> _mapNodes;
private AGVPathfinder _advancedPathfinder;
@@ -92,6 +174,9 @@ namespace AGVSimulator.Forms
// 방향 콤보박스 초기화
InitializeDirectionCombo();
// 에뮬레이터 UI 초기화
InitializeEmulatorUI();
// 초기 상태 설정
UpdateUI();
@@ -716,12 +801,32 @@ namespace AGVSimulator.Forms
foreach (var agv in _agvList)
{
agv.Update(100); // 100ms 간격으로 업데이트
// Emulator Tag Logic
if (_isEmulatorConnected && agv == _agvList.FirstOrDefault())
{
if (agv.CurrentNodeId != null && agv.CurrentNodeId != _lastSentNodeId)
{
var rfid = GetRfidByNodeId(agv.CurrentNodeId);
if (!string.IsNullOrEmpty(rfid))
{
SendTag(rfid);
_lastSentNodeId = agv.CurrentNodeId;
}
}
}
}
}
// UI 업데이트
UpdateUI();
_simulatorCanvas.Invalidate(); // 화면 다시 그리기
// 에뮬레이터 상태 전송
if (_isEmulatorConnected)
{
SendEmulatorStatus();
}
}
#endregion
@@ -2005,6 +2110,412 @@ namespace AGVSimulator.Forms
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
#region Emulator Logic
private void InitializeEmulatorUI()
{
// 에뮬레이터 제어 패널 생성
var emulatorPanel = new GroupBox();
emulatorPanel.Text = "AGV Emulator (RS232)";
emulatorPanel.Height = 60;
emulatorPanel.Dock = DockStyle.Top;
var layout = new FlowLayoutPanel();
layout.Dock = DockStyle.Fill;
layout.Padding = new Padding(5);
layout.AutoSize = true;
// 포트 선택 콤보박스
_portCombo = new ComboBox();
_portCombo.Width = 100;
_portCombo.DropDownStyle = ComboBoxStyle.DropDownList;
_portCombo.DropDown += (s, e) => {
_portCombo.Items.Clear();
_portCombo.Items.AddRange(SerialPort.GetPortNames());
};
_portCombo.Items.AddRange(SerialPort.GetPortNames());
if (_portCombo.Items.Count > 0) _portCombo.SelectedIndex = 0;
// 연결 버튼
_connectButton = new Button();
_connectButton.Text = "Connect";
_connectButton.Click += OnConnectEmulator_Click;
layout.Controls.Add(new Label { Text = "Port:", AutoSize = true, Margin = new Padding(3, 8, 3, 3) });
layout.Controls.Add(_portCombo);
layout.Controls.Add(_connectButton);
emulatorPanel.Controls.Add(layout);
// 폼의 최상단에 추가
this.Controls.Add(emulatorPanel);
emulatorPanel.BringToFront();
}
private void OnConnectEmulator_Click(object sender, EventArgs e)
{
if (_isEmulatorConnected)
{
DisconnectEmulator();
}
else
{
if (_portCombo.SelectedItem == null)
{
MessageBox.Show("포트를 선택해주세요.");
return;
}
ConnectEmulator(_portCombo.SelectedItem.ToString());
}
}
private void ConnectEmulator(string portName)
{
try
{
_emulatorPort = new SerialPort(portName, 115200, Parity.None, 8, StopBits.One);
_emulatorPort.DataReceived += OnEmulatorDataReceived;
_emulatorPort.Open();
_isEmulatorConnected = true;
_connectButton.Text = "Disconnect";
_portCombo.Enabled = false;
MessageBox.Show($"에뮬레이터 시작: {portName}");
}
catch (Exception ex)
{
MessageBox.Show($"연결 실패: {ex.Message}");
}
}
private void DisconnectEmulator()
{
try
{
if (_emulatorPort != null && _emulatorPort.IsOpen)
{
_emulatorPort.Close();
}
_isEmulatorConnected = false;
_connectButton.Text = "Connect";
_portCombo.Enabled = true;
}
catch (Exception ex)
{
MessageBox.Show($"해제 실패: {ex.Message}");
}
}
private StringBuilder _recvBuffer = new StringBuilder();
private void OnEmulatorDataReceived(object sender, SerialDataReceivedEventArgs e)
{
try
{
string data = _emulatorPort.ReadExisting();
_recvBuffer.Append(data);
string buffer = _recvBuffer.ToString();
int stxIndex = buffer.IndexOf((char)0x02);
while (stxIndex >= 0)
{
int etxIndex = buffer.IndexOf((char)0x03, stxIndex);
if (etxIndex > stxIndex)
{
string packet = buffer.Substring(stxIndex + 1, etxIndex - stxIndex - 1);
ProcessEmulatorPacket(packet);
// 처리된 패킷 제거
buffer = buffer.Substring(etxIndex + 1);
stxIndex = buffer.IndexOf((char)0x02);
}
else
{
break; // ETX 아직 안옴
}
}
_recvBuffer.Clear();
_recvBuffer.Append(buffer);
}
catch (Exception ex)
{
Console.WriteLine($"Emulator Recv Error: {ex.Message}");
}
}
#region Emulator Helpers
private bool GetBit(ref UInt16 _value, int idx)
{
var offset = (UInt16)(1 << idx);
return (_value & offset) != 0;
}
private bool SetBit(ref UInt16 _value, int idx, bool value)
{
var oldvalue = GetBit(ref _value, idx);
if (value)
{
var offset = (UInt16)(1 << idx);
_value = (UInt16)(_value | offset);
}
else
{
var offset = (UInt16)(~(1 << idx));
_value = (UInt16)(_value & offset);
}
return oldvalue != value;
}
private bool SetBit(ref byte _value, int idx, bool value)
{
var offset = (byte)(1 << idx);
if (value) _value |= offset;
else _value &= (byte)~offset;
return true;
}
private void SetAGV(esystemflag0 flag, bool value)
{
SetBit(ref _emu_system0, (int)flag, value);
}
private void SetAGV(esystemflag1 flag, bool value)
{
SetBit(ref _emu_system1, (int)flag, value);
}
private void SetAGV(eerror flag, bool value)
{
SetBit(ref _emu_error, (int)flag, value);
}
private void SetAGV(esignal flag, bool value)
{
SetBit(ref _emu_signal, (int)flag, value);
}
private void SetSTS(estsvaluetype target, char value)
{
switch (target)
{
case estsvaluetype.sensor: _emu_sts_sensor = value; break;
case estsvaluetype.direction: _emu_sts_dir = value; break;
case estsvaluetype.speed: _emu_sts_speed = value; break;
case estsvaluetype.bunki: _emu_sts_bunki = value; break;
}
}
#endregion
private void ProcessEmulatorPacket(string packet)
{
// Packet: CMD(3) + DATA(...) + Checksum(2)
// But here packet is substring between STX and ETX.
// Example: STS...Checksum
if (packet.Length < 3) return;
string cmd = packet.Substring(0, 3);
string data = "";
if (packet.Length > 5) // CMD + Checksum(2)
{
data = packet.Substring(3, packet.Length - 5);
}
// AGV 제어 (첫 번째 AGV 대상)
var agv = _agvList.FirstOrDefault();
this.Invoke(new Action(() => {
switch (cmd)
{
case "CRN": // 기동명령
if (data.Length > 0) SetSTS(estsvaluetype.direction, data[0]);
SetAGV(esystemflag1.agv_stop, false);
SetAGV(esystemflag1.agv_run, true);
if (agv != null) agv.Resume();
break;
case "CST": // 중지명령
if (data.StartsWith("M"))
{
// Mark Stop
// TODO: Implement Mark Stop logic in VirtualAGV if needed
// For now, just log it
Console.WriteLine("Mark Stop Command Received");
}
else
{
SetAGV(esystemflag1.agv_run, false);
SetAGV(esystemflag1.agv_stop, true);
if (agv != null) agv.Pause();
}
break;
case "CBR": // 분기명령
// FSL1
if (data.Length >= 4)
{
SetSTS(estsvaluetype.direction, data[0]);
SetSTS(estsvaluetype.bunki, data[1]);
SetSTS(estsvaluetype.speed, data[2]);
SetSTS(estsvaluetype.sensor, data[3]);
}
break;
case "CRT": // 수동제어
if (data.Length >= 4)
{
_emu_sts_dir = data[0];
_emu_sts_bunki = data[1];
_emu_sts_speed = data[2];
_emu_sts_sensor = data[3];
SetAGV(esystemflag1.agv_stop, false);
SetAGV(esystemflag1.agv_run, true);
if (agv != null) agv.Resume();
}
break;
case "SFR": // Reset
SetAGV(eerror.Emergency, false);
SetAGV(eerror.line_out_error, false);
SetAGV(eerror.Overcurrent, false);
SetAGV(esystemflag1.agv_run, false);
SetAGV(esystemflag1.agv_stop, true);
if (agv != null) agv.Pause();
break;
case "CBT": // 충전
SetAGV(esystemflag1.Battery_charging, true);
if (data.Length >= 5)
{
var cmdChar = data[4];
if (cmdChar == 'I') SetAGV(esystemflag1.Battery_charging, true);
else SetAGV(esystemflag1.Battery_charging, false);
}
break;
case "ACK":
// Log ACK
break;
default:
// Send ACK for other commands
SendCmd("ACK", cmd);
break;
}
}));
}
private void SendCmd(string cmd, string value)
{
if (_emulatorPort == null || !_emulatorPort.IsOpen) return;
var barr = new List<byte>();
barr.Add(0x02);
barr.AddRange(System.Text.Encoding.Default.GetBytes(cmd));
barr.AddRange(System.Text.Encoding.Default.GetBytes(value));
barr.Add((byte)'*');
barr.Add((byte)'*');
barr.Add(0x03);
try
{
_emulatorPort.Write(barr.ToArray(), 0, barr.Count);
}
catch { }
}
public void SendTag(string tagno)
{
if (_emulatorPort == null || !_emulatorPort.IsOpen) return;
tagno = tagno.PadLeft(6, '0');
if (tagno.Length > 6) tagno = tagno.Substring(0, 6);
var barr = new List<byte>();
barr.Add(0x02);
barr.Add((byte)'T');
barr.Add((byte)'A');
barr.Add((byte)'G');
barr.AddRange(System.Text.Encoding.Default.GetBytes(tagno));
barr.Add((byte)'*');
barr.Add((byte)'*');
barr.Add(0x03);
try
{
_emulatorPort.Write(barr.ToArray(), 0, barr.Count);
}
catch { }
}
private void SendEmulatorStatus()
{
if (_emulatorPort == null || !_emulatorPort.IsOpen) return;
var agv = _agvList.FirstOrDefault();
// Sync state from VirtualAGV
if (agv != null)
{
// Update Battery
// Update Position/Tag?
}
// STS Packet Construction
// STS(3) + Volt(3) + Sys0(4) + Sys1(4) + Err(4) + Spd(1) + Bunki(1) + Dir(1) + Sensor(1) + Signal(2) + Checksum(2)
// Default buffer
var sample = "02 53 54 53 32 35 38 46 46 46 46 34 30 30 30 30 30 30 30 4C 53 46 30 30 30 30 30 30 33 41 03";
var barr = sample.Split(' ').Select(t => Convert.ToByte(t, 16)).ToArray();
// Volt (255 for now)
var voltstr = "255";
var bufarr = System.Text.Encoding.Default.GetBytes(voltstr);
Array.Copy(bufarr, 0, barr, 4, bufarr.Length);
// System0
bufarr = System.Text.Encoding.Default.GetBytes(_emu_system0.ToString("X2").PadLeft(4, '0'));
Array.Copy(bufarr, 0, barr, 7, bufarr.Length);
// System1
bufarr = System.Text.Encoding.Default.GetBytes(_emu_system1.ToString("X2").PadLeft(4, '0'));
Array.Copy(bufarr, 0, barr, 11, bufarr.Length);
// Error
bufarr = System.Text.Encoding.Default.GetBytes(_emu_error.ToString("X2").PadLeft(4, '0'));
Array.Copy(bufarr, 0, barr, 15, bufarr.Length);
// Status Chars
barr[19] = (byte)_emu_sts_speed;
barr[20] = (byte)_emu_sts_bunki;
barr[21] = (byte)_emu_sts_dir;
barr[22] = (byte)_emu_sts_sensor;
// Signal
bufarr = System.Text.Encoding.Default.GetBytes(_emu_signal.ToString("X2").PadLeft(2, '0'));
Array.Copy(bufarr, 0, barr, 23, bufarr.Length);
// Checksum (**)
barr[barr.Length - 3] = (byte)'*';
barr[barr.Length - 2] = (byte)'*';
try
{
_emulatorPort.Write(barr, 0, barr.Length);
}
catch { }
}
private string CalculateChecksum(string data)
{
int sum = 0;
foreach (char c in data) sum += c;
// 16진수 변환 후 뒤 2자리
string hex = sum.ToString("X");
if (hex.Length >= 2) return hex.Substring(hex.Length - 2);
return hex.PadLeft(2, '0');
}
#endregion
}
/// <summary>