From 43c7dc211f10851480352b12bd01f3443079bb01 Mon Sep 17 00:00:00 2001
From: mrDarker <mr.darker@163.com>
Date: 星期二, 26 八月 2025 09:09:21 +0800
Subject: [PATCH] Merge branch 'clh' into liuyang

---
 SourceCode/Bond/Servo/Servo.vcxproj                       |   10 
 SourceCode/Bond/Servo/CControlJobDlg.cpp                  |  169 ++
 SourceCode/Bond/Servo/CControlJobDlg.h                    |   45 
 SourceCode/Bond/Servo/CVariable.h                         |    7 
 SourceCode/Bond/Servo/ProcessJob.cpp                      |  433 +++++++
 SourceCode/Bond/Servo/resource.h                          |    0 
 SourceCode/Bond/Servo/CControlJob.h                       |  153 ++
 SourceCode/Bond/EAPSimulator/CHsmsActive.h                |   14 
 SourceCode/Bond/EAPSimulator/EAPSimulatorDlg.h            |    4 
 SourceCode/Bond/Servo/CGlass.h                            |   46 
 SourceCode/Bond/Servo/HsmsPassive.cpp                     |  344 +++++
 SourceCode/Bond/Servo/CExpandableListCtrl.cpp             |  313 +++++
 SourceCode/Bond/Servo/CMaster.h                           |   41 
 SourceCode/Bond/Servo/CExpandableListCtrl.h               |   58 
 SourceCode/Bond/EAPSimulator/CHsmsActive.cpp              |   98 +
 SourceCode/Bond/EAPSimulator/Resource.h                   |   19 
 SourceCode/Bond/EAPSimulator/CPJsDlg.h                    |   35 
 SourceCode/Bond/EAPSimulator/EAPSimulatorDlg.cpp          |   31 
 SourceCode/Bond/Servo/TopToolbar.h                        |    1 
 SourceCode/Bond/EAPSimulator/EAPSimulator.vcxproj         |    5 
 SourceCode/Bond/Servo/Servo.rc                            |    0 
 SourceCode/Bond/Servo/CMaster.cpp                         |  310 ++++
 SourceCode/Bond/x64/Debug/ReportList.txt                  |    1 
 SourceCode/Bond/EAPSimulator/ProcessJob.h                 |  198 +++
 SourceCode/Bond/Servo/HsmsPassive.h                       |   14 
 SourceCode/Bond/Servo/ProcessJob.h                        |  216 +++
 SourceCode/Bond/Servo/CControlJob.cpp                     |  336 +++++
 SourceCode/Bond/Servo/CGlass.cpp                          |  123 ++
 SourceCode/Bond/EAPSimulator/ProcessJob.cpp               |  252 ++++
 SourceCode/Bond/EAPSimulator/EAPSimulator.rc              |    0 
 SourceCode/Bond/Servo/Model.cpp                           |   38 
 SourceCode/Bond/Servo/CLoadPort.cpp                       |   18 
 SourceCode/Bond/Servo/ServoDlg.cpp                        |   20 
 SourceCode/Bond/x64/Debug/VariableList.txt                |    4 
 SourceCode/Bond/x64/Debug/Res/ControlJob_Gray_32.ico      |    0 
 SourceCode/Bond/EAPSimulator/EAPSimulator.vcxproj.filters |   12 
 SourceCode/Bond/HSMSSDK/Include/ISECS2Item.h              |    1 
 SourceCode/Bond/Servo/Servo.vcxproj.filters               |    9 
 SourceCode/Bond/EAPSimulator/CPJsDlg.cpp                  |  161 ++
 SourceCode/Bond/x64/Debug/CollectionEventList.txt         |    1 
 SourceCode/Bond/Servo/TopToolbar.cpp                      |    8 
 SourceCode/Bond/Servo/CVariable.cpp                       |   25 
 SourceCode/Bond/Servo/SerializeUtil.h                     |   72 +
 SourceCode/Bond/x64/Debug/Res/ControlJob_High_32.ico      |    0 
 Document/Panel Bonder八零联合 SecsTest CheckList_v3.0.xlsx    |    0 
 45 files changed, 3,608 insertions(+), 37 deletions(-)

diff --git "a/Document/Panel Bonder\345\205\253\351\233\266\350\201\224\345\220\210 SecsTest CheckList_v3.0.xlsx" "b/Document/Panel Bonder\345\205\253\351\233\266\350\201\224\345\220\210 SecsTest CheckList_v3.0.xlsx"
index e062180..64eba06 100644
--- "a/Document/Panel Bonder\345\205\253\351\233\266\350\201\224\345\220\210 SecsTest CheckList_v3.0.xlsx"
+++ "b/Document/Panel Bonder\345\205\253\351\233\266\350\201\224\345\220\210 SecsTest CheckList_v3.0.xlsx"
Binary files differ
diff --git a/SourceCode/Bond/EAPSimulator/CHsmsActive.cpp b/SourceCode/Bond/EAPSimulator/CHsmsActive.cpp
index 2dc424f..b334708 100644
--- a/SourceCode/Bond/EAPSimulator/CHsmsActive.cpp
+++ b/SourceCode/Bond/EAPSimulator/CHsmsActive.cpp
@@ -3,6 +3,8 @@
 #include "Log.h"
 
 
+static unsigned int DATAID = 1;
+
 CHsmsActive::CHsmsActive()
 {
 	m_listener = {};
@@ -69,6 +71,10 @@
 		if (nStream == 5 && pHeader->function == 1) {
 			// S5F1
 			replyAck(5, 2, pMessage->getHeader()->systemBytes, 0, _T("ACK0"));
+		}
+		else if (nStream == 6 && pHeader->function == 11) {
+			// S5F1
+			replyAck(6, 12, pMessage->getHeader()->systemBytes, 0, _T("ACK0"));
 		}
 	};
 
@@ -289,6 +295,18 @@
 	return 0;
 }
 
+int CHsmsActive::hsmsSelectedEquipmentStatusRequest(unsigned int SVID)
+{
+	IMessage* pMessage = nullptr;
+	int nRet = HSMS_Create1Message(pMessage, m_nSessionId, 1 | REPLY, 3, ++m_nSystemByte);
+
+	pMessage->getBody()->addU4Item(SVID, "SVID");
+	m_pActive->sendMessage(pMessage);
+	HSMS_Destroy1Message(pMessage);
+
+	return 0;
+}
+
 int CHsmsActive::hsmsQueryPPIDList()
 {
 	IMessage* pMessage = nullptr;
@@ -331,6 +349,86 @@
 	return hsmsCarrierActionRequest(DATAID, "CarrierRelease", pszCarrierId, PTN);
 }
 
+int CHsmsActive::hsmsPRJobMultiCreate(std::vector<SERVO::CProcessJob*>& pjs)
+{
+	IMessage* pMessage = nullptr;
+	int nRet = HSMS_Create1Message(pMessage, m_nSessionId, 16 | REPLY, 15, ++m_nSystemByte);
+	char szMF[32] = {14};
+	pMessage->getBody()->addU4Item(++DATAID, "DATAID");
+	auto itemPjs = pMessage->getBody()->addItem();
+	for (auto pj : pjs) {
+		auto itemPj = itemPjs->addItem();
+		itemPj->addItem(pj->id().c_str(), "PRJOBID");
+		itemPj->addBinaryItem(szMF, 1, "MF");
+		auto itemCarriers = itemPj->addItem();
+		for (auto c : pj->carriers()) {
+			auto itemCarrier = itemCarriers->addItem();
+			itemCarrier->addItem(c.carrierId.c_str(), "CARRIERID");
+			auto itemSlots = itemCarrier->addItem();
+			for (auto s : c.slots) {
+				itemSlots->addU1Item(s, "SLOTID");
+			}
+		}
+
+		auto recipeItems = itemPj->addItem();
+		recipeItems->addU1Item(1, "PRRECIPEMETHOD");
+		recipeItems->addItem(pj->recipeSpec().c_str(), "RCPSPEC");
+		recipeItems->addItem();
+
+		itemPj->addBoolItem(false, "PRPROCESSSTART");
+		itemPj->addItem();
+	}
+
+	m_pActive->sendMessage(pMessage);
+	HSMS_Destroy1Message(pMessage);
+
+	return 0;
+}
+
+int CHsmsActive::hsmsCreateControlJob(const char* pszControlJobId, std::vector<std::string>& processJobIds)
+{
+	char szBuffer[256];
+	sprintf_s(szBuffer, 256, "ControlJob:%s>", pszControlJobId);
+
+	IMessage* pMessage = nullptr;
+	int nRet = HSMS_Create1Message(pMessage, m_nSessionId, 14 | REPLY, 9, ++m_nSystemByte);
+	pMessage->getBody()->addItem(szBuffer, "OBJSPEC");
+	pMessage->getBody()->addItem("ControlJob", "OBJTYPE");
+	auto itemAttrs = pMessage->getBody()->addItem();
+
+	{
+		auto itemAttr = itemAttrs->addItem();
+		itemAttr->addItem("Priority", "ATTRID");
+		itemAttr->addU1Item(8, "ATTRDATA");
+	}
+
+	{
+		auto itemAttr = itemAttrs->addItem();
+		itemAttr->addItem("weight", "ATTRID");
+		itemAttr->addF4Item(60.5, "ATTRDATA");
+	}
+
+	{
+		auto itemAttr = itemAttrs->addItem();
+		itemAttr->addItem("tel", "ATTRID");
+		itemAttr->addItem("15919875007", "ATTRDATA");
+	}
+
+	{
+		auto itemAttr = itemAttrs->addItem();
+		itemAttr->addItem("PRJOBLIST", "ATTRID");
+		auto itemProcessJobs = itemAttr->addItem();
+		for (auto& item : processJobIds) {
+			itemProcessJobs->addItem(item.c_str(), "");
+		}
+	}
+
+	m_pActive->sendMessage(pMessage);
+	HSMS_Destroy1Message(pMessage);
+
+	return 0;
+}
+
 int CHsmsActive::replyAck0(IMessage* pMessage)
 {
 	return 0;
diff --git a/SourceCode/Bond/EAPSimulator/CHsmsActive.h b/SourceCode/Bond/EAPSimulator/CHsmsActive.h
index b814d4b..29abeb5 100644
--- a/SourceCode/Bond/EAPSimulator/CHsmsActive.h
+++ b/SourceCode/Bond/EAPSimulator/CHsmsActive.h
@@ -4,6 +4,12 @@
 #include <map>
 #include <set>
 #include "CCollectionEvent.h"
+#include "ProcessJob.h"
+
+
+#define SVID_CJobSpace				5001
+#define SVID_PJobSpace				5002
+#define SVID_PJobQueued				5003
 
 
 typedef std::function<void(void* pFrom, ACTIVESTATE state)> STATECHANGED;
@@ -61,6 +67,9 @@
 	int hsmsTransmitSpooledData();
 	int hsmsPurgeSpooledData();
 
+	// 查询变量
+	int hsmsSelectedEquipmentStatusRequest(unsigned int SVID);
+
 	// 查询PPID List
 	int hsmsQueryPPIDList();
 
@@ -77,6 +86,11 @@
 		const char* pszCarrierId,
 		unsigned char PTN);
 
+	// S16F15
+	int hsmsPRJobMultiCreate(std::vector<SERVO::CProcessJob*>& pjs);
+
+	// S14F9
+	int hsmsCreateControlJob(const char* pszControlJobId, std::vector<std::string>& processJobIds);
 
 	// 通过的reply函数
 	void replyAck(int s, int f, unsigned int systemBytes, BYTE ack, const char* pszAckName);
diff --git a/SourceCode/Bond/EAPSimulator/CPJsDlg.cpp b/SourceCode/Bond/EAPSimulator/CPJsDlg.cpp
new file mode 100644
index 0000000..212291f
--- /dev/null
+++ b/SourceCode/Bond/EAPSimulator/CPJsDlg.cpp
@@ -0,0 +1,161 @@
+锘�// CPJsDlg.cpp: 瀹炵幇鏂囦欢
+//
+
+#include "pch.h"
+#include "EAPSimulator.h"
+#include "CPJsDlg.h"
+#include "afxdialogex.h"
+
+
+// CPJsDlg 瀵硅瘽妗�
+
+IMPLEMENT_DYNAMIC(CPJsDlg, CDialogEx)
+
+CPJsDlg::CPJsDlg(CWnd* pParent /*=nullptr*/)
+	: CDialogEx(IDD_DIALOG_PJS, pParent)
+{
+
+}
+
+CPJsDlg::~CPJsDlg()
+{
+	for (auto item : m_pjs) {
+		delete item;
+	}
+	m_pjs.clear();
+}
+
+void CPJsDlg::DoDataExchange(CDataExchange* pDX)
+{
+	CDialogEx::DoDataExchange(pDX);
+}
+
+
+BEGIN_MESSAGE_MAP(CPJsDlg, CDialogEx)
+	ON_BN_CLICKED(IDC_BUTTON_ADD, &CPJsDlg::OnBnClickedButtonAdd)
+	ON_BN_CLICKED(IDC_BUTTON_DELETE, &CPJsDlg::OnBnClickedButtonDelete)
+	ON_BN_CLICKED(IDC_BUTTON_SEND, &CPJsDlg::OnBnClickedButtonSend)
+END_MESSAGE_MAP()
+
+
+// CPJsDlg 娑堟伅澶勭悊绋嬪簭
+
+
+void CPJsDlg::OnBnClickedButtonAdd()
+{
+	// TODO: 鍦ㄦ娣诲姞鎺т欢閫氱煡澶勭悊绋嬪簭浠g爜
+}
+
+void CPJsDlg::OnBnClickedButtonDelete()
+{
+	CListCtrl* pListCtrl = (CListCtrl*)GetDlgItem(IDC_LIST1);
+	int nSel = pListCtrl->GetNextItem(-1, LVNI_SELECTED);
+
+	if (nSel != -1) {
+		SERVO::CProcessJob* pj = (SERVO::CProcessJob*)pListCtrl->GetItemData(nSel);
+		for (auto iter = m_pjs.begin(); iter != m_pjs.end(); ++iter) {
+			if (*iter == pj) {
+				delete (*iter);
+				m_pjs.erase(iter);
+				break;
+			}
+		}
+	}
+
+	pListCtrl->DeleteItem(nSel);
+}
+
+void CPJsDlg::OnBnClickedButtonSend()
+{
+	theApp.m_model.m_pHsmsActive->hsmsPRJobMultiCreate(m_pjs);
+}
+
+BOOL CPJsDlg::OnInitDialog()
+{
+	CDialogEx::OnInitDialog();
+
+	
+	// 鎶ヨ〃鎺т欢
+	CListCtrl* pListCtrl = (CListCtrl*)GetDlgItem(IDC_LIST1);
+	DWORD dwStyle = pListCtrl->GetExtendedStyle();
+	dwStyle |= LVS_EX_FULLROWSELECT;
+	dwStyle |= LVS_EX_GRIDLINES;
+	pListCtrl->SetExtendedStyle(dwStyle);
+
+	HIMAGELIST imageList = ImageList_Create(24, 24, ILC_COLOR24, 1, 1);
+	ListView_SetImageList(pListCtrl->GetSafeHwnd(), imageList, LVSIL_SMALL);
+	pListCtrl->InsertColumn(0, _T(""), LVCFMT_RIGHT, 0);
+	pListCtrl->InsertColumn(1, _T("PJ ID"), LVCFMT_LEFT, 100);
+	pListCtrl->InsertColumn(2, _T("Carrier1 & Slots"), LVCFMT_LEFT, 180);
+	pListCtrl->InsertColumn(3, _T("Carrier2 & Slots"), LVCFMT_LEFT, 180);
+	pListCtrl->InsertColumn(4, _T("Carrier3 & Slots"), LVCFMT_LEFT, 180);
+	pListCtrl->InsertColumn(5, _T("Carrier4 & Slots"), LVCFMT_LEFT, 180);
+	pListCtrl->InsertColumn(6, _T("PPID"), LVCFMT_LEFT, 180);
+
+
+
+	// 鍒涘缓鐢ㄤ簬娴嬭瘯鐨凱J
+	{
+		SERVO::CProcessJob* pj = new SERVO::CProcessJob("PJ0001");
+		std::vector<uint8_t> slots1{ 1, 2, 3 };
+		pj->addCarrier("CID1001", slots1);
+		pj->setRecipe(SERVO::RecipeMethod::NoTuning, "P1001");
+		m_pjs.push_back(pj);
+	}
+	{
+		SERVO::CProcessJob* pj = new SERVO::CProcessJob("PJ0002");
+		std::vector<uint8_t> slots1{ 1, 3 };
+		pj->addCarrier("CID1002", slots1);
+		std::vector<uint8_t> slots2{ 1};
+		pj->addCarrier("CID1003", slots2);
+		pj->setRecipe(SERVO::RecipeMethod::NoTuning, "R002");
+		m_pjs.push_back(pj);
+	}
+	{
+		SERVO::CProcessJob* pj = new SERVO::CProcessJob("PJ0003");
+		std::vector<uint8_t> slots1{ 1, 2, 3, 5 };
+		pj->addCarrier("CID1004", slots1);
+		pj->setRecipe(SERVO::RecipeMethod::NoTuning, "P1001");
+		m_pjs.push_back(pj);
+	}
+
+	// 鏄剧ず鍒版姤琛ㄤ腑
+	for (auto item : m_pjs) {
+		AddPjToListCtrl(item);
+	}
+
+
+	return TRUE;  // return TRUE unless you set the focus to a control
+				  // 寮傚父: OCX 灞炴�ч〉搴旇繑鍥� FALSE
+}
+
+void CPJsDlg::AddPjToListCtrl(SERVO::CProcessJob* pj)
+{
+	CListCtrl* pListCtrl = (CListCtrl*)GetDlgItem(IDC_LIST1);
+	pListCtrl->InsertItem(0, _T(""));
+	pListCtrl->SetItemData(0, (DWORD_PTR)pj);
+	pListCtrl->SetItemText(0, 1, pj->id().c_str());
+	pListCtrl->SetItemText(0, 6, pj->recipeSpec().c_str());
+
+	auto carries = pj->carriers();
+	for (int i = 0; i < min(4, carries.size()); i++) {
+		pListCtrl->SetItemText(0, 2 + i, GetFormatString(carries[i]));		;
+	}
+}
+
+CString CPJsDlg::GetFormatString(SERVO::CarrierSlotInfo& csi)
+{
+	CString strRet;
+	strRet.Append(csi.carrierId.c_str());
+	strRet.Append("<");
+	int size = min(8, csi.slots.size());
+	for (int i = 0; i < size; i++) {
+		strRet.Append(std::to_string(csi.slots[i]).c_str());
+		if (i != size - 1) {
+			strRet.Append(",");
+		}
+	}
+	strRet.Append(">");
+
+	return strRet;
+}
\ No newline at end of file
diff --git a/SourceCode/Bond/EAPSimulator/CPJsDlg.h b/SourceCode/Bond/EAPSimulator/CPJsDlg.h
new file mode 100644
index 0000000..2441c8b
--- /dev/null
+++ b/SourceCode/Bond/EAPSimulator/CPJsDlg.h
@@ -0,0 +1,35 @@
+锘�#pragma once
+#include "ProcessJob.h"
+#include <vector>
+
+
+// CPJsDlg 瀵硅瘽妗�
+
+class CPJsDlg : public CDialogEx
+{
+	DECLARE_DYNAMIC(CPJsDlg)
+
+public:
+	CPJsDlg(CWnd* pParent = nullptr);   // 鏍囧噯鏋勯�犲嚱鏁�
+	virtual ~CPJsDlg();
+	void AddPjToListCtrl(SERVO::CProcessJob* pj);
+	CString GetFormatString(SERVO::CarrierSlotInfo& csi);
+
+private:
+	std::vector<SERVO::CProcessJob*> m_pjs;
+
+// 瀵硅瘽妗嗘暟鎹�
+#ifdef AFX_DESIGN_TIME
+	enum { IDD = IDD_DIALOG_PJS };
+#endif
+
+protected:
+	virtual void DoDataExchange(CDataExchange* pDX);    // DDX/DDV 鏀寔
+
+	DECLARE_MESSAGE_MAP()
+public:
+	afx_msg void OnBnClickedButtonAdd();
+	afx_msg void OnBnClickedButtonDelete();
+	afx_msg void OnBnClickedButtonSend();
+	virtual BOOL OnInitDialog();
+};
diff --git a/SourceCode/Bond/EAPSimulator/EAPSimulator.rc b/SourceCode/Bond/EAPSimulator/EAPSimulator.rc
index f489bd8..53eb764 100644
--- a/SourceCode/Bond/EAPSimulator/EAPSimulator.rc
+++ b/SourceCode/Bond/EAPSimulator/EAPSimulator.rc
Binary files differ
diff --git a/SourceCode/Bond/EAPSimulator/EAPSimulator.vcxproj b/SourceCode/Bond/EAPSimulator/EAPSimulator.vcxproj
index a614abf..868c15c 100644
--- a/SourceCode/Bond/EAPSimulator/EAPSimulator.vcxproj
+++ b/SourceCode/Bond/EAPSimulator/EAPSimulator.vcxproj
@@ -93,6 +93,7 @@
       <SDLCheck>true</SDLCheck>
       <PreprocessorDefinitions>_WINDOWS;_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
       <PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
+      <LanguageStandard>stdcpp17</LanguageStandard>
     </ClCompile>
     <Link>
       <SubSystem>Windows</SubSystem>
@@ -193,6 +194,7 @@
     <ClInclude Include="CModel.h" />
     <ClInclude Include="Common.h" />
     <ClInclude Include="Context.h" />
+    <ClInclude Include="CPJsDlg.h" />
     <ClInclude Include="CReport.h" />
     <ClInclude Include="CTerminalDisplayDlg.h" />
     <ClInclude Include="CVariable.h" />
@@ -202,6 +204,7 @@
     <ClInclude Include="Log.h" />
     <ClInclude Include="LogEdit.h" />
     <ClInclude Include="pch.h" />
+    <ClInclude Include="ProcessJob.h" />
     <ClInclude Include="Resource.h" />
     <ClInclude Include="targetver.h" />
   </ItemGroup>
@@ -215,6 +218,7 @@
     <ClCompile Include="CLinkReportDlg.cpp" />
     <ClCompile Include="CModel.cpp" />
     <ClCompile Include="Context.cpp" />
+    <ClCompile Include="CPJsDlg.cpp" />
     <ClCompile Include="CReport.cpp" />
     <ClCompile Include="CTerminalDisplayDlg.cpp" />
     <ClCompile Include="CVariable.cpp" />
@@ -228,6 +232,7 @@
       <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Create</PrecompiledHeader>
       <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
     </ClCompile>
+    <ClCompile Include="ProcessJob.cpp" />
   </ItemGroup>
   <ItemGroup>
     <ResourceCompile Include="EAPSimulator.rc" />
diff --git a/SourceCode/Bond/EAPSimulator/EAPSimulator.vcxproj.filters b/SourceCode/Bond/EAPSimulator/EAPSimulator.vcxproj.filters
index dab50ba..d100ea3 100644
--- a/SourceCode/Bond/EAPSimulator/EAPSimulator.vcxproj.filters
+++ b/SourceCode/Bond/EAPSimulator/EAPSimulator.vcxproj.filters
@@ -78,6 +78,12 @@
     <ClInclude Include="CLinkReportDetailDlg.h">
       <Filter>澶存枃浠�</Filter>
     </ClInclude>
+    <ClInclude Include="CPJsDlg.h">
+      <Filter>澶存枃浠�</Filter>
+    </ClInclude>
+    <ClInclude Include="ProcessJob.h">
+      <Filter>澶存枃浠�</Filter>
+    </ClInclude>
   </ItemGroup>
   <ItemGroup>
     <ClCompile Include="EAPSimulator.cpp">
@@ -131,6 +137,12 @@
     <ClCompile Include="CLinkReportDetailDlg.cpp">
       <Filter>婧愭枃浠�</Filter>
     </ClCompile>
+    <ClCompile Include="CPJsDlg.cpp">
+      <Filter>婧愭枃浠�</Filter>
+    </ClCompile>
+    <ClCompile Include="ProcessJob.cpp">
+      <Filter>婧愭枃浠�</Filter>
+    </ClCompile>
   </ItemGroup>
   <ItemGroup>
     <ResourceCompile Include="EAPSimulator.rc">
diff --git a/SourceCode/Bond/EAPSimulator/EAPSimulatorDlg.cpp b/SourceCode/Bond/EAPSimulator/EAPSimulatorDlg.cpp
index 3af62af..e15ff68 100644
--- a/SourceCode/Bond/EAPSimulator/EAPSimulatorDlg.cpp
+++ b/SourceCode/Bond/EAPSimulator/EAPSimulatorDlg.cpp
@@ -13,6 +13,7 @@
 #include "CEDEventReportDlg.h"
 #include "CDefineReportsDlg.h"
 #include "CLinkReportDlg.h"
+#include "CPJsDlg.h"
 
 
 #ifdef _DEBUG
@@ -90,6 +91,10 @@
 	ON_BN_CLICKED(IDC_BUTTON_QUERY_PPID_LIST, &CEAPSimulatorDlg::OnBnClickedButtonQueryPpidList)
 	ON_BN_CLICKED(IDC_BUTTON_PROCEED_WITH_CARRIER, &CEAPSimulatorDlg::OnBnClickedButtonProceedWithCarrier)
 	ON_BN_CLICKED(IDC_BUTTON_CARRIER_RELEASE, &CEAPSimulatorDlg::OnBnClickedButtonCarrierRelease)
+	ON_BN_CLICKED(IDC_BUTTON_QUERY_CJ_SPACE, &CEAPSimulatorDlg::OnBnClickedButtonQueryCjSpace)
+	ON_BN_CLICKED(IDC_BUTTON_QUERY_PJ_SPACE, &CEAPSimulatorDlg::OnBnClickedButtonQueryPjSpace)
+	ON_BN_CLICKED(IDC_BUTTON_CREATE_PJ, &CEAPSimulatorDlg::OnBnClickedButtonCreatePj)
+	ON_BN_CLICKED(IDC_BUTTON_CREATE_CJ, &CEAPSimulatorDlg::OnBnClickedButtonCreateCj)
 END_MESSAGE_MAP()
 
 
@@ -279,6 +284,10 @@
 	GetDlgItem(IDC_BUTTON_QUERY_PPID_LIST)->EnableWindow(enabled);	
 	GetDlgItem(IDC_BUTTON_PROCEED_WITH_CARRIER)->EnableWindow(enabled);	
 	GetDlgItem(IDC_BUTTON_CARRIER_RELEASE)->EnableWindow(enabled);
+	GetDlgItem(IDC_BUTTON_QUERY_CJ_SPACE)->EnableWindow(enabled);
+	GetDlgItem(IDC_BUTTON_QUERY_PJ_SPACE)->EnableWindow(enabled);
+	GetDlgItem(IDC_BUTTON_CREATE_PJ)->EnableWindow(enabled);	
+	GetDlgItem(IDC_BUTTON_CREATE_CJ)->EnableWindow(enabled);	
 }
 
 void CEAPSimulatorDlg::OnBnClickedButtonConnect()
@@ -376,6 +385,16 @@
 	theApp.m_model.m_pHsmsActive->hsmsPurgeSpooledData();
 }
 
+void CEAPSimulatorDlg::OnBnClickedButtonQueryCjSpace()
+{
+	theApp.m_model.m_pHsmsActive->hsmsSelectedEquipmentStatusRequest(SVID_CJobSpace);
+}
+
+void CEAPSimulatorDlg::OnBnClickedButtonQueryPjSpace()
+{
+	theApp.m_model.m_pHsmsActive->hsmsSelectedEquipmentStatusRequest(SVID_PJobQueued);
+}
+
 void CEAPSimulatorDlg::OnBnClickedButtonQueryPpidList()
 {
 	theApp.m_model.m_pHsmsActive->hsmsQueryPPIDList();
@@ -391,3 +410,15 @@
 {
 	theApp.m_model.m_pHsmsActive->hsmsCarrierRelease(DATAID++, "CSX 52078", 2);
 }
+
+void CEAPSimulatorDlg::OnBnClickedButtonCreatePj()
+{
+	CPJsDlg dlg;
+	dlg.DoModal();
+}
+
+void CEAPSimulatorDlg::OnBnClickedButtonCreateCj()
+{
+	std::vector<std::string> processJobIds = {"PJ0001", "PJ0003"};
+	theApp.m_model.m_pHsmsActive->hsmsCreateControlJob("CJ5007", processJobIds);
+}
diff --git a/SourceCode/Bond/EAPSimulator/EAPSimulatorDlg.h b/SourceCode/Bond/EAPSimulator/EAPSimulatorDlg.h
index c19a88e..e10cf1a 100644
--- a/SourceCode/Bond/EAPSimulator/EAPSimulatorDlg.h
+++ b/SourceCode/Bond/EAPSimulator/EAPSimulatorDlg.h
@@ -59,4 +59,8 @@
 	afx_msg void OnBnClickedButtonQueryPpidList();
 	afx_msg void OnBnClickedButtonProceedWithCarrier();
 	afx_msg void OnBnClickedButtonCarrierRelease();
+	afx_msg void OnBnClickedButtonQueryCjSpace();
+	afx_msg void OnBnClickedButtonQueryPjSpace();
+	afx_msg void OnBnClickedButtonCreatePj();
+	afx_msg void OnBnClickedButtonCreateCj();
 };
diff --git a/SourceCode/Bond/EAPSimulator/ProcessJob.cpp b/SourceCode/Bond/EAPSimulator/ProcessJob.cpp
new file mode 100644
index 0000000..a4935e6
--- /dev/null
+++ b/SourceCode/Bond/EAPSimulator/ProcessJob.cpp
@@ -0,0 +1,252 @@
+#include "pch.h"
+#include "ProcessJob.h"
+#include <cctype>
+
+namespace SERVO {
+    static inline std::string trimCopy(std::string s) {
+        auto notspace = [](int ch) { return !std::isspace(ch); };
+        s.erase(s.begin(), std::find_if(s.begin(), s.end(), notspace));
+        s.erase(std::find_if(s.rbegin(), s.rend(), notspace).base(), s.end());
+        return s;
+    }
+
+    CProcessJob::CProcessJob(std::string pjId)
+        : m_pjId(trimCopy(pjId))
+    {
+        clampString(m_pjId, MAX_ID_LEN);
+    }
+
+    void CProcessJob::setParentCjId(std::string cjId) {
+        m_parentCjId = trimCopy(cjId);
+        clampString(m_parentCjId, MAX_ID_LEN);
+    }
+
+    void CProcessJob::setRecipe(RecipeMethod method, std::string spec) {
+        m_recipeMethod = method;
+        m_recipeSpec = trimCopy(spec);
+        clampString(m_recipeSpec, MAX_ID_LEN);
+    }
+
+    void CProcessJob::addParam(std::string name, std::string value) {
+        name = trimCopy(name);
+        value = trimCopy(value);
+        clampString(name, MAX_PARAM_K);
+        clampString(value, MAX_PARAM_V);
+        m_params.push_back({ std::move(name), std::move(value) });
+    }
+
+    void CProcessJob::setParams(std::vector<PJParam> params) {
+        m_params.clear();
+        m_params.reserve(params.size());
+        for (auto& p : params) addParam(std::move(p.name), std::move(p.value));
+    }
+
+    void CProcessJob::addPauseEvent(uint32_t ceid) {
+        if (ceid) m_pauseEvents.push_back(ceid);
+        std::sort(m_pauseEvents.begin(), m_pauseEvents.end());
+        m_pauseEvents.erase(std::unique(m_pauseEvents.begin(), m_pauseEvents.end()), m_pauseEvents.end());
+    }
+
+    void CProcessJob::setPauseEvents(std::vector<uint32_t> ceids) {
+        m_pauseEvents = std::move(ceids);
+        std::sort(m_pauseEvents.begin(), m_pauseEvents.end());
+        m_pauseEvents.erase(std::unique(m_pauseEvents.begin(), m_pauseEvents.end()), m_pauseEvents.end());
+    }
+
+    std::vector<CProcessJob::ValidationIssue>
+        CProcessJob::validate(const IResourceView& rv) const
+    {
+        std::vector<ValidationIssue> issues;
+
+        // 让 add 同时支持 const char* 和 std::string
+        auto add = [&](uint32_t code, std::string msg) {
+            issues.push_back({ code, std::move(msg) });
+        };
+
+        // —— 基本 / 标识 ——
+        if (m_pjId.empty())            add(1001, "PJID empty");
+        if (!asciiPrintable(m_pjId))   add(1002, "PJID has non-printable chars");
+
+        if (m_parentCjId.empty())      add(1010, "Parent CJID empty");
+
+        // —— 配方(RCPSPEC / PPID)——
+        if (m_recipeSpec.empty())      add(1020, "Recipe spec (PPID) empty");
+        else if (!rv.recipeExists(m_recipeSpec)) {
+            add(1021, "PPID not found: " + m_recipeSpec);
+        }
+
+        // —— 配方方法 vs 参数 —— 1=NoTuning 禁止带参数;2=WithTuning 允许/可选
+        if (m_recipeMethod == RecipeMethod::NoTuning && !m_params.empty()) {
+            add(1022, "Params not allowed when PRRECIPEMETHOD=1 (NoTuning)");
+        }
+
+        // —— 物料选择校验 ——(二选一:Carrier+Slots 或 MIDs;两者都不填则错误)
+        const bool hasCarrierSlots = !m_carriers.empty();
+        if (hasCarrierSlots) {
+            // {L:n { CARRIERID {L:j SLOTID} }}
+            for (const auto& cs : m_carriers) {
+                if (cs.carrierId.empty()) {
+                    add(1030, "CarrierID empty");
+                    continue;
+                }
+                if (!rv.carrierPresent(cs.carrierId)) {
+                    add(1031, "Carrier not present: " + cs.carrierId);
+                }
+                if (cs.slots.empty()) {
+                    add(1032, "No slots specified for carrier: " + cs.carrierId);
+                }
+                for (auto s : cs.slots) {
+                    if (s == 0) {
+                        add(1033, "Slot 0 is invalid for carrier: " + cs.carrierId);
+                        continue;
+                    }
+                    if (!rv.slotUsable(cs.carrierId, s)) {
+                        add(1034, "Slot unusable: carrier=" + cs.carrierId + " slot=" + std::to_string(s));
+                    }
+                }
+            }
+        }
+        else {
+            add(1035, "No material selection provided (neither Carrier/Slots nor MIDs)");
+        }
+
+        // —— 暂停事件(PRPAUSEEVENTID 列表)——
+        for (auto ceid : m_pauseEvents) {
+            if (!rv.ceidDefined(ceid)) {
+                add(1050, "Pause CEID unknown: " + std::to_string(ceid));
+            }
+        }
+
+        return issues;
+    }
+
+    // —— 状态机 ——
+    // 规则可按你们协议微调
+    bool CProcessJob::queue() {
+        if (m_state != PJState::NoState) return false;
+        markQueued();
+        return true;
+    }
+
+    bool CProcessJob::enterSettingUp() {
+        if (m_state != PJState::Queued) return false;
+        m_state = PJState::SettingUp;
+        return true;
+    }
+
+    bool CProcessJob::start() {
+        if (m_state != PJState::Queued && m_state != PJState::SettingUp && m_state != PJState::Paused)
+            return false;
+        if (!m_tStart.has_value()) markStart();
+        m_state = PJState::InProcess;
+        return true;
+    }
+
+    bool CProcessJob::pause() {
+        if (m_state != PJState::InProcess) return false;
+        m_state = PJState::Paused;
+        return true;
+    }
+
+    bool CProcessJob::resume() {
+        if (m_state != PJState::Paused) return false;
+        m_state = PJState::InProcess;
+        return true;
+    }
+
+    bool CProcessJob::complete() {
+        if (m_state != PJState::InProcess && m_state != PJState::Paused) return false;
+        m_state = PJState::Completed;
+        markEnd();
+        return true;
+    }
+
+    bool CProcessJob::abort() {
+        if (m_state == PJState::Completed || m_state == PJState::Aborted || m_state == PJState::Failed)
+            return false;
+        m_state = PJState::Aborted;
+        markEnd();
+        return true;
+    }
+
+    bool CProcessJob::fail(std::string reason) {
+        m_failReason = trimCopy(reason);
+        clampString(m_failReason, 128);
+        m_state = PJState::Failed;
+        markEnd();
+        return true;
+    }
+
+    // —— 时间戳 & 工具 —— 
+    void CProcessJob::markQueued() {
+        m_state = PJState::Queued;
+        m_tQueued = std::chrono::system_clock::now();
+    }
+
+    void CProcessJob::markStart() {
+        m_tStart = std::chrono::system_clock::now();
+    }
+
+    void CProcessJob::markEnd() {
+        m_tEnd = std::chrono::system_clock::now();
+    }
+
+    void CProcessJob::clampString(std::string& s, size_t maxLen) {
+        if (s.size() > maxLen) s.resize(maxLen);
+    }
+
+    bool CProcessJob::asciiPrintable(const std::string& s) {
+        return std::all_of(s.begin(), s.end(), [](unsigned char c) {
+            return c >= 0x20 && c <= 0x7E;
+            });
+    }
+
+    void CProcessJob::setCarriers(std::vector<CarrierSlotInfo> carriers)
+    {
+        // 统一通过 addCarrier 做规范化(去空白、截断、去重、合并同 carrier)
+        m_carriers.clear();
+        m_carriers.reserve(carriers.size());
+        for (auto& cs : carriers) {
+            addCarrier(std::move(cs.carrierId), std::move(cs.slots));
+        }
+    }
+
+    void CProcessJob::addCarrier(std::string carrierId, std::vector<uint8_t> slots)
+    {
+        // 1) 规范化 carrierId:去空白 + 长度限制
+        carrierId = trimCopy(std::move(carrierId));
+        clampString(carrierId, MAX_ID_LEN);
+        if (carrierId.empty()) {
+            // 空 ID 直接忽略(也可以选择抛异常/记录日志,看你项目风格)
+            return;
+        }
+
+        // 2) 规范化 slots:去 0、排序、去重
+        //    注:SLOTID 按 1..N,0 视为非法/占位
+        slots.erase(std::remove(slots.begin(), slots.end(), 0), slots.end());
+        std::sort(slots.begin(), slots.end());
+        slots.erase(std::unique(slots.begin(), slots.end()), slots.end());
+        if (slots.empty()) {
+            // 没有有效卡位就不追加
+            return;
+        }
+
+        // 3) 如果已存在同名载具,则合并 slot 列表
+        auto it = std::find_if(m_carriers.begin(), m_carriers.end(),
+            [&](const CarrierSlotInfo& cs) { return cs.carrierId == carrierId; });
+
+        if (it != m_carriers.end()) {
+            // 合并
+            it->slots.insert(it->slots.end(), slots.begin(), slots.end());
+            std::sort(it->slots.begin(), it->slots.end());
+            it->slots.erase(std::unique(it->slots.begin(), it->slots.end()), it->slots.end());
+        }
+        else {
+            // 新增
+            CarrierSlotInfo cs;
+            cs.carrierId = std::move(carrierId);
+            cs.slots = std::move(slots);
+            m_carriers.emplace_back(std::move(cs));
+        }
+    }
+}
diff --git a/SourceCode/Bond/EAPSimulator/ProcessJob.h b/SourceCode/Bond/EAPSimulator/ProcessJob.h
new file mode 100644
index 0000000..92d70b9
--- /dev/null
+++ b/SourceCode/Bond/EAPSimulator/ProcessJob.h
@@ -0,0 +1,198 @@
+#pragma once
+#include <string>
+#include <vector>
+#include <unordered_map>
+#include <unordered_set>
+#include <algorithm>
+#include <cstdint>
+#include <chrono>
+#include <optional>
+
+namespace SERVO {
+    /// PJ 生命周期(贴近 E40 常见状态)
+    enum class PJState : uint8_t {
+        NoState = 0,
+        Queued,
+        SettingUp,
+        InProcess,
+        Paused,
+        Aborting,
+        Completed,
+        Aborted,
+        Failed
+    };
+
+    /// 配方指定方式(对应 S16F15 里 PRRECIPEMETHOD)
+    enum class RecipeMethod : uint8_t {
+        NoTuning = 1,   // 1 - recipe without variable tuning
+        WithTuning = 2  // 2 - recipe with variable tuning
+    };
+
+    /// 启动策略(对应 S16F15 里 PRPROCESSSTART)
+    enum class StartPolicy : uint8_t {
+        Queued = 0,   // 建立后排队
+        AutoStart = 1 // 条件满足则自动启动
+    };
+
+    /** 配方参数对(S16F15 中 RCPPARNM / RCPPARVAL) */
+    struct PJParam {
+        std::string name;   // RCPPARNM
+        std::string value;  // RCPPARVAL
+    };
+
+    /**
+    {L:2
+        CARRIERID
+        {L:j
+            SLOTID
+        }
+    }
+     */
+    struct CarrierSlotInfo {
+        std::string carrierId;              // CARRIERID
+        std::vector<uint8_t> slots;        // SLOTID[]
+    };
+
+    /// 简单资源视图接口:供 Validate() 查询(由设备端实现者在外部提供)
+    struct IResourceView {
+        virtual ~IResourceView() = default;
+        virtual bool recipeExists(const std::string& ppid) const = 0;
+        virtual bool carrierPresent(const std::string& carrierId) const = 0;
+        virtual bool slotUsable(const std::string& carrierId, uint16_t slot) const = 0;
+        virtual bool ceidDefined(uint32_t ceid) const = 0;
+        // 你也可以扩展:port状态、占用情况、CJ/PJ空间等
+    };
+
+    /// PJ 主类
+    /**
+     * ProcessJob —— 与 S16F15(PRJobMultiCreate)字段一一对应的承载类
+     *
+     * S16F15 结构(核心节选):
+     * {L:6
+     *   PRJOBID                -> m_pjId
+     *   MF                     -> m_mf
+     *   {L:n { CARRIERID {L:j SLOTID} } } 
+     *   {L:3
+     *     PRRECIPEMETHOD       -> m_recipeType
+     *     RCPSPEC(PPID)      -> m_recipeSpec
+     *     {L:m { RCPPARNM RCPPARVAL }}     -> m_params
+     *   }
+     *   PRPROCESSSTART         -> m_startPolicy
+     *   {L:k PRPAUSEEVENTID}   -> m_pauseEvents
+     * }
+     */
+    class CProcessJob {
+    public:
+        // —— 构造 / 基本设置 ——
+        explicit CProcessJob(std::string pjId);
+
+        const std::string& id() const noexcept { return m_pjId; }
+        const std::string& parentCjId() const noexcept { return m_parentCjId; }
+        PJState state() const noexcept { return m_state; }
+        StartPolicy startPolicy() const noexcept { return m_startPolicy; }
+        RecipeMethod recipeMethod() const noexcept { return m_recipeMethod; }
+        const std::string& recipeSpec() const noexcept { return m_recipeSpec; } // PPID 或 Spec
+
+        // 绑定父 CJ
+        void setParentCjId(std::string cjId);
+
+        // 配方
+        void setRecipe(RecipeMethod method, std::string spec);
+
+        // 启动策略
+        void setStartPolicy(StartPolicy sp) { m_startPolicy = sp; }
+
+        // 参数
+        void addParam(std::string name, std::string value);
+        void setParams(std::vector<PJParam> params);
+
+        // 暂停事件
+        void addPauseEvent(uint32_t ceid);
+        void setPauseEvents(std::vector<uint32_t> ceids);
+
+        // —— 校验 ——
+        struct ValidationIssue {
+            uint32_t code;      // 自定义错误码
+            std::string text;   // 文本描述
+        };
+        // 返回问题清单(空=通过)
+        std::vector<ValidationIssue> validate(const IResourceView& rv) const;
+
+        // —— 状态机(带守卫)——
+        bool queue();           // NoState -> Queued
+        bool start();           // Queued/SettingUp -> InProcess
+        bool enterSettingUp();  // Queued -> SettingUp
+        bool pause();           // InProcess -> Paused
+        bool resume();          // Paused -> InProcess
+        bool complete();        // InProcess -> Completed
+        bool abort();           // Any (未终态) -> Aborted
+        bool fail(std::string reason); // 任意态 -> Failed(记录失败原因)
+
+        // —— 访问器(用于上报/查询)——
+        const std::vector<PJParam>& params() const noexcept { return m_params; }
+        const std::vector<uint32_t>& pauseEvents() const noexcept { return m_pauseEvents; }
+        const std::string& failReason() const noexcept { return m_failReason; }
+
+        // 时间戳(可用于报表/追溯)
+        std::optional<std::chrono::system_clock::time_point> tQueued() const { return m_tQueued; }
+        std::optional<std::chrono::system_clock::time_point> tStart()  const { return m_tStart; }
+        std::optional<std::chrono::system_clock::time_point> tEnd()    const { return m_tEnd; }
+
+        // 长度限制工具(可在集成时统一策略)
+        static void clampString(std::string& s, size_t maxLen);
+        static bool asciiPrintable(const std::string& s);
+
+        // 清空并整体设置
+        void setCarriers(std::vector<CarrierSlotInfo> carriers);
+
+        // 追加一个载具
+        void addCarrier(std::string carrierId, std::vector<uint8_t> slots);
+
+        // 访问器
+        const std::vector<CarrierSlotInfo>& carriers() const noexcept { return m_carriers; }
+
+        // 判定是否“按载具/卡位”方式
+        bool usesCarrierSlots() const noexcept { return !m_carriers.empty(); }
+
+
+    private:
+        // 内部状态转移帮助
+        void markQueued();
+        void markStart();
+        void markEnd();
+
+    private:
+        // 标识
+        std::string m_pjId;
+        std::string m_parentCjId;
+
+        // 配方
+        RecipeMethod m_recipeMethod{ RecipeMethod::NoTuning };
+        std::string  m_recipeSpec; // PPID / Spec
+
+        // 物料
+        static constexpr uint8_t MATERIAL_FORMAT = 14; // substrate
+        std::vector<CarrierSlotInfo> m_carriers;   // {L:n { CARRIERID {L:j SLOTID} }}
+
+        // 参数 / 暂停事件
+        std::vector<PJParam>    m_params;
+        std::vector<uint32_t>   m_pauseEvents;
+
+        // 状态 & 记录
+        StartPolicy m_startPolicy{ StartPolicy::Queued }; // 0=Queued, 1=AutoStart
+        PJState m_state{ PJState::NoState };
+        std::string m_failReason;
+
+        // 时间戳
+        std::optional<std::chrono::system_clock::time_point> m_tQueued;
+        std::optional<std::chrono::system_clock::time_point> m_tStart;
+        std::optional<std::chrono::system_clock::time_point> m_tEnd;
+
+        // 约束(可按你们协议调整)
+        static constexpr size_t MAX_ID_LEN = 64;   // PJID/ CJID/ CarrierID/ MID/ PPID
+        static constexpr size_t MAX_PARAM_K = 32;   // 参数名
+        static constexpr size_t MAX_PARAM_V = 64;   // 参数值
+    };
+}
+
+
diff --git a/SourceCode/Bond/EAPSimulator/Resource.h b/SourceCode/Bond/EAPSimulator/Resource.h
index b39967c..96c191e 100644
--- a/SourceCode/Bond/EAPSimulator/Resource.h
+++ b/SourceCode/Bond/EAPSimulator/Resource.h
@@ -13,6 +13,7 @@
 #define IDD_DIALOG_ADD_IDS              135
 #define IDD_DIALOG_LINK_REPORT          137
 #define IDD_DIALOG_LINK_REPORT_DETAIL   139
+#define IDD_DIALOG_PJS                  141
 #define IDC_EDIT_LOG                    1000
 #define IDC_EDIT_IP                     1001
 #define IDC_EDIT_PORT                   1002
@@ -46,18 +47,24 @@
 #define IDC_EDIT_CE_NAME                1031
 #define IDC_BUTTON_QUERY_PPID_LIST      1032
 #define IDC_EDIT_CE_RPTID               1033
-#define IDC_BUTTON_PROCEED_WITH_CARRIER 1033
-#define IDC_BUTTON_TRANSMIT_SPOOLED_DATA 1034
-#define IDC_BUTTON_PROCEED_WITH_CARRIER2 1035
-#define IDC_BUTTON_CARRIER_RELEASE      1035
+#define IDC_BUTTON_PROCEED_WITH_CARRIER 1034
+#define IDC_BUTTON_TRANSMIT_SPOOLED_DATA 1035
+#define IDC_BUTTON_CARRIER_RELEASE      1036
+#define IDC_BUTTON_QUERY_CJ_SPACE       1037
+#define IDC_BUTTON_QUERY_PJ_SPACE       1038
+#define IDC_BUTTON_CREATE_PJ            1039
+#define IDC_BUTTON_ADD                  1040
+#define IDC_BUTTON_CREATE_PJ2           1040
+#define IDC_BUTTON_CREATE_CJ            1040
+#define IDC_BUTTON_DELETE               1041
 
 // Next default values for new objects
 // 
 #ifdef APSTUDIO_INVOKED
 #ifndef APSTUDIO_READONLY_SYMBOLS
-#define _APS_NEXT_RESOURCE_VALUE        141
+#define _APS_NEXT_RESOURCE_VALUE        143
 #define _APS_NEXT_COMMAND_VALUE         32771
-#define _APS_NEXT_CONTROL_VALUE         1035
+#define _APS_NEXT_CONTROL_VALUE         1042
 #define _APS_NEXT_SYMED_VALUE           101
 #endif
 #endif
diff --git a/SourceCode/Bond/HSMSSDK/Include/ISECS2Item.h b/SourceCode/Bond/HSMSSDK/Include/ISECS2Item.h
index b3bfa28..09b9ed0 100644
--- a/SourceCode/Bond/HSMSSDK/Include/ISECS2Item.h
+++ b/SourceCode/Bond/HSMSSDK/Include/ISECS2Item.h
@@ -70,5 +70,6 @@
 	virtual void setBinary(const char* pszData, unsigned int len, const char* pszNote) = 0;
 	virtual void setString(const char* pszText, const char* pszNote) = 0;
 	virtual void setU1(unsigned char value, const char* pszNote) = 0;
+	virtual void setBool(bool value, const char* pszNote) = 0;
 	virtual ISECS2Item* addItem() = 0;
 };
diff --git a/SourceCode/Bond/Servo/CControlJob.cpp b/SourceCode/Bond/Servo/CControlJob.cpp
new file mode 100644
index 0000000..8dc2b18
--- /dev/null
+++ b/SourceCode/Bond/Servo/CControlJob.cpp
@@ -0,0 +1,336 @@
+#include "stdafx.h"
+#include "CControlJob.h"
+#include <cctype>
+#include "SerializeUtil.h"
+
+static inline std::string trimCopy(std::string s) {
+    auto notspace = [](int ch) { return !std::isspace(ch); };
+    s.erase(s.begin(), std::find_if(s.begin(), s.end(), notspace));
+    s.erase(std::find_if(s.rbegin(), s.rend(), notspace).base(), s.end());
+    return s;
+}
+
+namespace SERVO {
+    CControlJob::CControlJob()
+    {
+
+    }
+
+    CControlJob::CControlJob(std::string cjId)
+        : m_cjId(trimCopy(std::move(cjId)))
+    {
+        clampString(m_cjId, MAX_ID_LEN);
+    }
+
+    CControlJob::CControlJob(CControlJob& src)
+    {
+        m_cjId = src.m_cjId;
+        clampString(m_cjId, MAX_ID_LEN);
+        m_priority = src.m_priority;
+        m_pjIds = src.m_pjIds;
+        m_state = src.m_state;
+        m_failReason = src.m_failReason;
+        m_tQueued = src.m_tQueued;
+        m_tStart = src.m_tStart;
+        m_tEnd = src.m_tEnd;
+    }
+
+    bool CControlJob::addPJ(const std::string& pjId) {
+        if (pjId.empty()) return false;
+        auto id = pjId;
+        auto it = std::find(m_pjIds.begin(), m_pjIds.end(), id);
+        if (it != m_pjIds.end()) return false;
+        clampString(id, MAX_ID_LEN);
+        m_pjIds.push_back(std::move(id));
+        return true;
+    }
+
+    bool CControlJob::addPJs(const std::vector<std::string>& ids) {
+        bool added = false;
+        for (auto& s : ids) added |= addPJ(s);
+        return added;
+    }
+
+    bool CControlJob::setPJs(const std::vector<CProcessJob*>& pjs)
+    {
+        m_pjs = pjs;
+        return true;
+    }
+
+    bool CControlJob::removePJ(const std::string& pjId) {
+        auto it = std::find(m_pjIds.begin(), m_pjIds.end(), pjId);
+        if (it == m_pjIds.end()) return false;
+        m_pjIds.erase(it);
+        return true;
+    }
+
+    bool CControlJob::containsPJ(const std::string& pjId) const {
+        return std::find(m_pjIds.begin(), m_pjIds.end(), pjId) != m_pjIds.end();
+    }
+
+    const std::vector<CControlJob::ValidationIssue>& CControlJob::issues()
+    {
+        return m_issues;
+    }
+
+    bool CControlJob::validateForCreate(
+            const std::function<bool(uint32_t& code, std::string& msg)>& canCreateCjFn,
+            const std::function<bool(const std::string&)>& getPjExistsFn,
+            const std::function<bool(const std::string&)>& canJoinFn
+        )
+    {
+        m_issues.clear();
+
+        auto add = [&](uint32_t code, std::string msg) { m_issues.push_back({ code, std::move(msg) }); };
+
+        // 是否能创建CJ, 由上层根据当前任务,机器状态等检验
+        uint32_t cc;
+        std::string mm;
+        if (!canCreateCjFn(cc, mm)) {
+            add(cc, mm);
+        }
+
+
+        // CJID 基础校验
+        if (m_cjId.empty())           add(1101, "CJID empty");
+        if (!asciiPrintable(m_cjId))  add(1102, "CJID has non-printable chars");
+
+        // PJ 列表校验
+        if (m_pjIds.empty())          add(1110, "PRJOBLIST empty");
+        for (const auto& pj : m_pjIds) {
+            if (!getPjExistsFn(pj))   add(1111, "PJ not found: " + pj);
+            else if (!canJoinFn(pj))  add(1112, "PJ not joinable: " + pj);
+        }
+
+        return m_issues.empty();
+    }
+
+    // 应用创建/更新(用于 S14F9 → S14F10 路径)
+    CControlJob::CreateResult
+        CControlJob::applyCreate(
+            const CreateRequest& req,
+            const std::function<bool(const std::string&)>& getPjExistsFn,
+            const std::function<bool(const std::string&)>& canJoinFn
+        )
+    {
+        CreateResult r;
+
+        // 覆盖优先级(如提供)
+        if (req.priority.has_value()) {
+            m_priority = *req.priority;
+        }
+
+        // 逐 PJ 判定
+        for (const auto& pjIdRaw : req.requestedPjIds) {
+            std::string pjId = trimCopy(pjIdRaw);
+            clampString(pjId, MAX_ID_LEN);
+
+            if (!getPjExistsFn(pjId)) {
+                r.errors.push_back({ 2001, "PRJOBLIST: " + pjId + " not found" });
+                continue;
+            }
+            if (!canJoinFn(pjId)) {
+                r.errors.push_back({ 2002, "PRJOBLIST: " + pjId + " not joinable (state)" });
+                continue;
+            }
+            if (containsPJ(pjId)) {
+                // 已在列表,视作成功(幂等)
+                r.acceptedPjIds.push_back(pjId);
+                continue;
+            }
+            // 加入 CJ
+            m_pjIds.push_back(pjId);
+            r.acceptedPjIds.push_back(std::move(pjId));
+        }
+
+        // 归并 ACK
+        if (r.errors.empty())               r.objack = 0; // 全成功
+        else if (!r.acceptedPjIds.empty())  r.objack = 1; // 部分成功
+        else                                r.objack = 2; // 全失败
+
+        return r;
+    }
+
+    // —— 状态机 —— //
+    bool CControlJob::queue() {
+        if (m_state != CJState::NoState) return false;
+        markQueued();
+        return true;
+    }
+
+    bool CControlJob::start() {
+        if (m_state != CJState::Queued) return false;
+        m_state = CJState::Executing;
+        if (!m_tStart.has_value()) markStart();
+        return true;
+    }
+
+    bool CControlJob::pause() {
+        if (m_state != CJState::Executing) return false;
+        m_state = CJState::Paused;
+        return true;
+    }
+
+    bool CControlJob::resume() {
+        if (m_state != CJState::Paused) return false;
+        m_state = CJState::Executing;
+        return true;
+    }
+
+    bool CControlJob::complete() {
+        if (m_state != CJState::Executing && m_state != CJState::Paused) return false;
+        m_state = CJState::Completed;
+        markEnd();
+        return true;
+    }
+
+    bool CControlJob::abort() {
+        if (m_state == CJState::Completed || m_state == CJState::Aborted || m_state == CJState::Failed)
+            return false;
+        m_state = CJState::Aborted;
+        markEnd();
+        return true;
+    }
+
+    bool CControlJob::fail(std::string reason) {
+        m_failReason = trimCopy(reason);
+        clampString(m_failReason, 128);
+        m_state = CJState::Failed;
+        markEnd();
+        return true;
+    }
+
+    // —— 聚合完成判断 —— //
+    bool CControlJob::tryAggregateComplete(
+        const std::function<bool(const std::string&)>& isPjCompletedFn
+    ) {
+        if (m_pjIds.empty()) return false;
+        for (const auto& pj : m_pjIds) {
+            if (!isPjCompletedFn(pj)) return false;
+        }
+        // 所有 PJ 已完成 → CJ 完成
+        return complete();
+    }
+
+    // —— 时间戳 —— //
+    void CControlJob::markQueued() { m_state = CJState::Queued;    m_tQueued = std::chrono::system_clock::now(); }
+    void CControlJob::markStart() { m_tStart = std::chrono::system_clock::now(); }
+    void CControlJob::markEnd() { m_tEnd = std::chrono::system_clock::now(); }
+
+    // —— 工具 —— //
+    void CControlJob::clampString(std::string& s, size_t maxLen) {
+        if (s.size() > maxLen) s.resize(maxLen);
+    }
+
+    bool CControlJob::asciiPrintable(const std::string& s) {
+        return std::all_of(s.begin(), s.end(), [](unsigned char c) {
+            return c >= 0x20 && c <= 0x7E;
+            });
+    }
+
+    void CControlJob::serialize(std::ostream& os) const {
+        write_pod(os, CJ_MAGIC);
+        write_pod(os, CJ_VERSION);
+
+        // 标识/优先级/状态/失败原因
+        write_string(os, id());                             // 或 m_cjId
+        write_pod<uint8_t>(os, priority());                 // 或 m_priority
+        write_pod<uint8_t>(os, static_cast<uint8_t>(state())); // 或 m_state
+        write_string(os, failReason());                     // 或 m_failReason
+
+        // 时间戳
+        write_opt_time(os, tQueued());
+        write_opt_time(os, tStart());
+        write_opt_time(os, tEnd());
+
+        // 关联 PJ 列表
+        write_vec_str(os, pjIds());                         // 或 m_pjIds
+    }
+
+    bool CControlJob::deserialize(std::istream& is, CControlJob& out, std::string* err) {
+        auto fail = [&](const char* msg) { if (err) *err = msg; return false; };
+
+        uint32_t magic = 0; if (!read_pod(is, magic)) return fail("read CJ magic");
+        if (magic != CJ_MAGIC) return fail("bad CJ magic");
+
+        uint16_t ver = 0; if (!read_pod(is, ver)) return fail("read CJ version");
+        if (ver != CJ_VERSION) return fail("unsupported CJ version");
+
+        std::string cjId;
+        if (!read_string(is, cjId)) return fail("read CJID");
+
+        uint8_t prio = 0;
+        if (!read_pod(is, prio)) return fail("read Priority");
+
+        uint8_t st = 0;
+        if (!read_pod(is, st))   return fail("read State");
+
+        std::string failText;
+        if (!read_string(is, failText)) return fail("read failReason");
+
+        std::optional<std::chrono::system_clock::time_point> tQ, tS, tE;
+        if (!read_opt_time(is, tQ)) return fail("read tQueued");
+        if (!read_opt_time(is, tS)) return fail("read tStart");
+        if (!read_opt_time(is, tE)) return fail("read tEnd");
+
+        std::vector<std::string> pjIds;
+        if (!read_vec_str(is, pjIds)) return fail("read PJIDs");
+
+        // —— 写回对象(直接改成员,或通过 setter)——
+        // 若你有 setter:out.setId(...)/setPriority(...)/setState(...)/setFailReason(...)
+        out = CControlJob(cjId);
+        out.setPriority(prio);
+
+        // 直接恢复内部状态(若你要求走状态机,可在这里按合法过渡调用 queue()/start()/...)
+        // 简化:直接赋值(你在 CControlJob.cpp 内部,可访问私有成员)
+        struct Access : CControlJob {
+            using CControlJob::m_state;
+            using CControlJob::m_failReason;
+            using CControlJob::m_tQueued;
+            using CControlJob::m_tStart;
+            using CControlJob::m_tEnd;
+            using CControlJob::m_pjIds;
+        };
+        auto& a = reinterpret_cast<Access&>(out);
+        a.m_state = static_cast<CJState>(st);
+        a.m_failReason = std::move(failText);
+        a.m_tQueued = std::move(tQ);
+        a.m_tStart = std::move(tS);
+        a.m_tEnd = std::move(tE);
+        a.m_pjIds = std::move(pjIds);
+
+        return true;
+    }
+
+    std::string CControlJob::getStateText()
+    {
+        switch (m_state)
+        {
+        case SERVO::CJState::NoState:
+            return "NoState";
+            break;
+        case SERVO::CJState::Queued:
+            return "Queued";
+            break;
+        case SERVO::CJState::Executing:
+            return "Executing";
+            break;
+        case SERVO::CJState::Paused:
+            return "Paused";
+            break;
+        case SERVO::CJState::Completed:
+            return "Completed";
+            break;
+        case SERVO::CJState::Aborted:
+            return "Aborted";
+            break;
+        case SERVO::CJState::Failed:
+            return "Failed";
+            break;
+        default:
+            break;
+        }
+
+        return "";
+    }
+}
diff --git a/SourceCode/Bond/Servo/CControlJob.h b/SourceCode/Bond/Servo/CControlJob.h
new file mode 100644
index 0000000..8533018
--- /dev/null
+++ b/SourceCode/Bond/Servo/CControlJob.h
@@ -0,0 +1,153 @@
+#pragma once
+#include <string>
+#include <vector>
+#include <algorithm>
+#include <cstdint>
+#include <optional>
+#include <chrono>
+#include <functional>
+#include "ProcessJob.h"
+
+
+// —— 标准/项目内约定的属性名(A:40)——
+// 说明:不以空格开头/结尾,字符范围 0x20-0x7E,且不能包含 > : ? * ~
+inline constexpr const char* CJ_ATTR_CJID = "CJID";       // A:n
+inline constexpr const char* CJ_ATTR_PRIORITY = "Priority";   // U1
+inline constexpr const char* CJ_ATTR_PRJOBLIST = "PRJOBLIST";  // L:n of A:n (PJID[])
+
+namespace SERVO {
+    /// CJ 状态(贴近 E40 语义)
+    enum class CJState : uint8_t {
+        NoState = 0,
+        Queued,
+        Executing,
+        Paused,
+        Completed,
+        Aborted,
+        Failed
+    };
+
+    /// 创建/修改结果中的错误项
+    struct CJError {
+        uint16_t    code;   // 自定义错误码(例:2001=PJ_NOT_FOUND, 2002=NOT_JOINABLE)
+        std::string text;   // 建议包含定位信息(ATTRID/PJID)
+    };
+
+    /// CControlJob:Control Job 管理类
+    class CControlJob {
+    public:
+        CControlJob();
+        explicit CControlJob(std::string cjId);
+        explicit CControlJob(CControlJob& src);
+
+        // —— 基本属性 —— //
+        const std::string& id()     const noexcept { return m_cjId; }
+        CJState            state()  const noexcept { return m_state; }
+        uint8_t            priority() const noexcept { return m_priority; }
+        void               setPriority(uint8_t p) noexcept { m_priority = p; }
+        std::string getStateText();
+
+        // —— PJ 列表维护(去重)—— //
+        bool addPJ(const std::string& pjId);                // 已存在则不重复添加
+        bool addPJs(const std::vector<std::string>& ids);   // 返回是否有新增
+        bool removePJ(const std::string& pjId);             // 存在则移除
+        bool containsPJ(const std::string& pjId) const;
+        const std::vector<std::string>& pjIds() const noexcept { return m_pjIds; }
+        bool setPJs(const std::vector<CProcessJob*>& pjs);
+        void clearPJs() { m_pjIds.clear(); }
+        const std::vector<CProcessJob*>& getPjs() { return m_pjs; };
+
+        // —— 校验 —— //
+        struct ValidationIssue { uint32_t code; std::string text; };
+
+        // 校验 CJ 是否可创建/更新(例如:PJ 是否存在、是否可加入)
+        // getPjExistsFn(pjId)->bool:PJ 是否存在
+        // canJoinFn(pjId)->bool     :PJ 当前是否允许加入 CJ(如 PJ 状态为 Queued 等)
+        bool validateForCreate(
+            const std::function<bool(uint32_t& code, std::string& msg)>& canCreateCjFn,
+            const std::function<bool(const std::string&)>& getPjExistsFn,
+            const std::function<bool(const std::string&)>& canJoinFn
+        );
+        const std::vector<CControlJob::ValidationIssue>& CControlJob::issues();
+
+        // —— S14F9 → S14F10 的“应用结果”模型 —— //
+        struct CreateRequest {
+            std::optional<uint8_t>           priority;      // 若有则覆盖
+            std::vector<std::string>         requestedPjIds;// 想要绑定的 PJ 列表
+        };
+        struct CreateResult {
+            std::vector<std::string> acceptedPjIds; // 成功绑定的 PJ(用于回显 PRJOBLIST)
+            std::vector<CJError>     errors;        // 失败项(含 PJID 说明)
+            uint8_t                  objack{ 0 };     // 0=全成功, 1=部分成功, 2=全失败
+        };
+
+        // 应用创建/更新请求:只绑定允许的 PJ,并生成 OBJACK/错误清单
+        // getPjExistsFn / canJoinFn 同上
+        CreateResult applyCreate(
+            const CreateRequest& req,
+            const std::function<bool(const std::string&)>& getPjExistsFn,
+            const std::function<bool(const std::string&)>& canJoinFn
+        );
+
+        // —— 状态机 —— //
+        bool queue();          // NoState -> Queued
+        bool start();          // Queued  -> Executing
+        bool pause();          // Executing -> Paused
+        bool resume();         // Paused -> Executing
+        bool complete();       // Executing/Paused -> Completed
+        bool abort();          // 非终态 -> Aborted
+        bool fail(std::string reason); // 任意 -> Failed
+
+        const std::string& failReason() const noexcept { return m_failReason; }
+
+        // —— 时间戳 —— //
+        std::optional<std::chrono::system_clock::time_point> tQueued() const { return m_tQueued; }
+        std::optional<std::chrono::system_clock::time_point> tStart()  const { return m_tStart; }
+        std::optional<std::chrono::system_clock::time_point> tEnd()    const { return m_tEnd; }
+
+        // —— 汇总状态辅助(可选)—— //
+        // 根据外部提供的“PJ 是否已完成”判断,尝试把 CJ 聚合置为 Completed。
+        // isPjCompletedFn(pjId)->bool
+        bool tryAggregateComplete(const std::function<bool(const std::string&)>& isPjCompletedFn);
+
+        // 工具:统一字符串限制
+        static void clampString(std::string& s, size_t maxLen);
+        static bool asciiPrintable(const std::string& s);
+
+        static constexpr uint32_t CJ_MAGIC = 0x434A5031; // "CJP1"
+        static constexpr uint16_t CJ_VERSION = 0x0001;
+
+        void serialize(std::ostream& os) const;
+        static bool deserialize(std::istream& is, CControlJob& out, std::string* err = nullptr);
+
+    private:
+        void markQueued();
+        void markStart();
+        void markEnd();
+
+    protected:
+        // —— 标识 & 配置 —— //
+        std::string m_cjId;
+        uint8_t     m_priority{ 5 }; // 缺省优先级(自定)
+
+        // —— 组成 —— //
+        std::vector<std::string> m_pjIds;
+        std::vector<CProcessJob*> m_pjs;
+
+        // —— 状态 / 文本 —— //
+        CJState     m_state{ CJState::NoState };
+        std::string m_failReason;
+
+        // —— 时间戳 —— //
+        std::optional<std::chrono::system_clock::time_point> m_tQueued;
+        std::optional<std::chrono::system_clock::time_point> m_tStart;
+        std::optional<std::chrono::system_clock::time_point> m_tEnd;
+
+        // —— 统一约束 —— //
+        static constexpr size_t MAX_ID_LEN = 64; // CJID / PJID 等
+
+        // 错误列表
+        std::vector<ValidationIssue> m_issues;
+    };
+}
+
diff --git a/SourceCode/Bond/Servo/CControlJobDlg.cpp b/SourceCode/Bond/Servo/CControlJobDlg.cpp
new file mode 100644
index 0000000..7365c3d
--- /dev/null
+++ b/SourceCode/Bond/Servo/CControlJobDlg.cpp
@@ -0,0 +1,169 @@
+锘�// CControlJobDlg.cpp: 瀹炵幇鏂囦欢
+//
+
+#include "stdafx.h"
+#include "Servo.h"
+#include "CControlJobDlg.h"
+#include "afxdialogex.h"
+
+
+// CControlJobDlg 瀵硅瘽妗�
+
+IMPLEMENT_DYNAMIC(CControlJobDlg, CDialogEx)
+
+CControlJobDlg::CControlJobDlg(CWnd* pParent /*=nullptr*/)
+	: CDialogEx(IDD_DIALOG_CONTROL_JOB, pParent)
+{
+    m_pControlJob = nullptr;
+}
+
+CControlJobDlg::~CControlJobDlg()
+{
+}
+
+void CControlJobDlg::DoDataExchange(CDataExchange* pDX)
+{
+	CDialogEx::DoDataExchange(pDX);
+	DDX_Control(pDX, IDC_LIST1, m_listCtrl);
+}
+
+
+BEGIN_MESSAGE_MAP(CControlJobDlg, CDialogEx)
+    ON_WM_SIZE()
+END_MESSAGE_MAP()
+
+
+void CControlJobDlg::SetControlJob(SERVO::CControlJob* pControlJob)
+{
+    m_pControlJob = pControlJob;
+}
+
+// CControlJobDlg 娑堟伅澶勭悊绋嬪簭
+BOOL CControlJobDlg::OnInitDialog()
+{
+	CDialogEx::OnInitDialog();
+
+
+    // label瀛椾綋
+    LOGFONT lf{};
+    GetFont()->GetLogFont(&lf);
+    lf.lfHeight = -20;
+    lf.lfWeight = FW_BOLD;
+    _tcscpy_s(lf.lfFaceName, _T("Arial"));
+    m_fontNoJob.CreateFontIndirect(&lf);
+    GetDlgItem(IDC_LABEL_NO_JOB)->SetFont(&m_fontNoJob);
+
+
+    // 鍒楄〃鎺т欢
+    HIMAGELIST imageList = ImageList_Create(24, 24, ILC_COLOR24, 1, 1);
+    ListView_SetImageList(m_listCtrl.GetSafeHwnd(), imageList, LVSIL_SMALL);
+    m_listCtrl.ModifyStyle(0, LVS_REPORT | LVS_SINGLESEL | LVS_SHOWSELALWAYS);
+    m_listCtrl.InsertColumn(0, _T("ID"), LVCFMT_LEFT, 180);
+    m_listCtrl.InsertColumn(1, _T("绫诲瀷"), LVCFMT_LEFT, 120);
+    m_listCtrl.InsertColumn(2, _T("鐘舵��"), LVCFMT_LEFT, 120);
+    m_listCtrl.InsertColumn(3, _T("閰嶆柟"), LVCFMT_LEFT, 120);
+    m_listCtrl.InsertColumn(4, _T("Port / Carrier / Slot"), LVCFMT_LEFT, 180);
+    m_listCtrl.InsertColumn(5, _T("鎻忚堪"), LVCFMT_LEFT, 220);
+
+
+    // 鎺т欢鐘舵��
+    Resize();
+    ShowGroup1(m_pControlJob == nullptr);
+    ShowGroup2(m_pControlJob != nullptr);
+    LoadData();
+
+	return TRUE;  // return TRUE unless you set the focus to a control
+				  // 寮傚父: OCX 灞炴�ч〉搴旇繑鍥� FALSE
+}
+
+void CControlJobDlg::OnSize(UINT nType, int cx, int cy)
+{
+    CDialogEx::OnSize(nType, cx, cy);
+    if (GetDlgItem(IDC_LIST1) == nullptr) return;
+    Resize();
+}
+
+void CControlJobDlg::Resize()
+{
+    CRect rcClient, rcItem;
+    CWnd* pItem;
+
+    GetClientRect(rcClient);
+
+
+    // 鍏抽棴鎸夐挳
+    int y = rcClient.bottom - 12;
+    pItem = GetDlgItem(IDCANCEL);
+    pItem->GetClientRect(&rcItem);
+    pItem->MoveWindow(rcClient.right - 12 - rcItem.Width(),
+        y - rcItem.Height(),
+        rcItem.Width(), rcItem.Height());
+    y -= rcItem.Height();
+    y -= 12;
+
+
+    // 绾�
+    pItem = GetDlgItem(IDC_LINE1);
+    pItem->MoveWindow(12, y, rcClient.Width() - 24, 2);
+    y -= 2;
+
+
+    // Label
+    pItem = GetDlgItem(IDC_LABEL_NO_JOB);
+    pItem->GetClientRect(&rcItem);
+    pItem->MoveWindow((rcClient.Width() - rcItem.Width()) / 2,
+        (y - rcItem.Height()) / 2,
+        rcItem.Width(), rcItem.Height());
+
+
+    // ListCtrl
+    pItem = GetDlgItem(IDC_LIST1);
+    pItem->MoveWindow(12, 12, rcClient.Width() - 24, y - 12);
+}
+
+void CControlJobDlg::ShowGroup1(BOOL bShow)
+{
+    GetDlgItem(IDC_LABEL_NO_JOB)->ShowWindow(bShow ? SW_SHOW : SW_HIDE);
+    GetDlgItem(IDC_LINE1)->ShowWindow(bShow ? SW_SHOW : SW_HIDE);
+}
+
+void CControlJobDlg::ShowGroup2(BOOL bShow)
+{
+    GetDlgItem(IDC_LIST1)->ShowWindow(bShow ? SW_SHOW : SW_HIDE);
+}
+
+void CControlJobDlg::LoadData()
+{
+    m_listCtrl.DeleteAllItems();
+
+    if (m_pControlJob != nullptr) {
+        auto* root1 = m_listCtrl.InsertRoot({ m_pControlJob->id().c_str(), _T("ControlJob"), 
+            m_pControlJob->getStateText().c_str(), _T("") });
+        auto pjs = m_pControlJob->getPjs();
+        for (auto pj : pjs) {
+            auto* root2 = m_listCtrl.InsertChild(root1, {pj->id().c_str(),  _T("ProcessJob"),
+                pj->getStateText().c_str(), pj->recipeSpec().c_str(), _T(""), _T(""), _T("") });
+            auto cs = pj->carriers();
+            for (auto c : cs) {
+                for (auto g : c.contexts) {
+                    SERVO::CGlass* pGlass = (SERVO::CGlass*)g;
+                    if (pGlass != nullptr) {
+                        int port, slot;
+                        pGlass->getOrginPort(port, slot);
+                        std::string carrier = c.carrierId + " / Port" + std::to_string(port + 1) + " / Slot" + std::to_string(slot + 1);
+                        m_listCtrl.InsertChild(root2, { pGlass->getID().c_str(), _T("Glass"),
+                            pGlass->getStateText().c_str(), _T(""), carrier.c_str(), _T("") });
+                    }
+                    else {
+                        m_listCtrl.InsertChild(root2, { "Null", _T("Glass"), _T(""), _T(""), c.carrierId.c_str(), _T("") });
+                    }
+                }
+            }
+            root2->expanded = true;
+        }
+        root1->expanded = true;
+
+
+        m_listCtrl.RebuildVisible();
+    }
+}
diff --git a/SourceCode/Bond/Servo/CControlJobDlg.h b/SourceCode/Bond/Servo/CControlJobDlg.h
new file mode 100644
index 0000000..410f3ec
--- /dev/null
+++ b/SourceCode/Bond/Servo/CControlJobDlg.h
@@ -0,0 +1,45 @@
+锘�#pragma once
+#include "CExpandableListCtrl.h"
+#include "CControlJob.h"
+
+
+// CControlJobDlg 瀵硅瘽妗�
+
+class CControlJobDlg : public CDialogEx
+{
+	DECLARE_DYNAMIC(CControlJobDlg)
+
+public:
+	CControlJobDlg(CWnd* pParent = nullptr);   // 鏍囧噯鏋勯�犲嚱鏁�
+	virtual ~CControlJobDlg();
+
+public:
+	void SetControlJob(SERVO::CControlJob* pControlJob);
+
+private:
+	void Resize();
+	void ShowGroup1(BOOL bShow);
+	void ShowGroup2(BOOL bShow);
+	void LoadData();
+
+private:
+	SERVO::CControlJob* m_pControlJob;
+	CFont m_fontNoJob;
+
+protected:
+	CExpandableListCtrl m_listCtrl;
+
+
+// 瀵硅瘽妗嗘暟鎹�
+#ifdef AFX_DESIGN_TIME
+	enum { IDD = IDD_DIALOG_CONTROL_JOB };
+#endif
+
+protected:
+	virtual void DoDataExchange(CDataExchange* pDX);    // DDX/DDV 鏀寔
+
+	DECLARE_MESSAGE_MAP()
+public:
+	virtual BOOL OnInitDialog();
+	afx_msg void OnSize(UINT nType, int cx, int cy);
+};
diff --git a/SourceCode/Bond/Servo/CExpandableListCtrl.cpp b/SourceCode/Bond/Servo/CExpandableListCtrl.cpp
new file mode 100644
index 0000000..11470ce
--- /dev/null
+++ b/SourceCode/Bond/Servo/CExpandableListCtrl.cpp
@@ -0,0 +1,313 @@
+锘�#include "stdafx.h"
+#include "CExpandableListCtrl.h"
+
+IMPLEMENT_DYNAMIC(CExpandableListCtrl, CListCtrl)
+
+CExpandableListCtrl::CExpandableListCtrl() {}
+CExpandableListCtrl::~CExpandableListCtrl() {}
+
+BEGIN_MESSAGE_MAP(CExpandableListCtrl, CListCtrl)
+    ON_WM_CREATE()
+    ON_NOTIFY_REFLECT(NM_CLICK, &CExpandableListCtrl::OnClick)
+    ON_NOTIFY_REFLECT(NM_CUSTOMDRAW, &CExpandableListCtrl::OnCustomDraw)
+END_MESSAGE_MAP()
+
+int CExpandableListCtrl::OnCreate(LPCREATESTRUCT lpCreateStruct)
+{
+    if (CListCtrl::OnCreate(lpCreateStruct) == -1)
+        return -1;
+
+    // 鎶ヨ〃椋庢牸鍒椾妇渚�
+    SetExtendedStyle(GetExtendedStyle()
+        | LVS_EX_FULLROWSELECT | LVS_EX_HEADERDRAGDROP | LVS_EX_GRIDLINES | LVS_EX_DOUBLEBUFFER);
+
+    return 0;
+}
+
+void CExpandableListCtrl::PreSubclassWindow()
+{
+    // 鎶ヨ〃椋庢牸鍒椾妇渚�
+    SetExtendedStyle(GetExtendedStyle()
+        | LVS_EX_FULLROWSELECT | LVS_EX_HEADERDRAGDROP | LVS_EX_GRIDLINES | LVS_EX_DOUBLEBUFFER);
+
+    CListCtrl::PreSubclassWindow();
+}
+
+CExpandableListCtrl::Node* CExpandableListCtrl::InsertRoot(const std::vector<CString>& cols)
+{
+    auto n = std::make_unique<Node>((int)max(1, (int)cols.size()));
+    for (size_t i = 0; i < cols.size(); ++i) n->cols[i] = cols[i];
+    n->level = 0;
+    Node* raw = n.get();
+    m_roots.emplace_back(std::move(n));
+    return raw;
+}
+
+CExpandableListCtrl::Node* CExpandableListCtrl::InsertChild(Node* parent, const std::vector<CString>& cols)
+{
+    ASSERT(parent);
+    auto n = std::make_unique<Node>((int)max(1, (int)cols.size()));
+    for (size_t i = 0; i < cols.size(); ++i) n->cols[i] = cols[i];
+    n->parent = parent;
+    n->level = parent->level + 1;
+    Node* raw = n.get();
+    parent->children.emplace_back(std::move(n));
+    return raw;
+}
+
+void CExpandableListCtrl::appendVisible(Node* n)
+{
+    m_visible.push_back(n);
+    if (n->expanded) {
+        for (auto& ch : n->children) {
+            appendVisible(ch.get());
+        }
+    }
+}
+
+void CExpandableListCtrl::RebuildVisible()
+{
+    // 1) 閲嶅缓鍙搴忓垪
+    m_visible.clear();
+    for (auto& r : m_roots) appendVisible(r.get());
+
+    // 2) 閲嶇粯/閲嶅~鏁版嵁
+    SetRedraw(FALSE);
+    DeleteAllItems();
+
+    // 鎻掑叆鍙琛�
+    for (int i = 0; i < (int)m_visible.size(); ++i) {
+        Node* n = m_visible[i];
+        LVITEM lvi{};
+        lvi.mask = LVIF_TEXT;
+        lvi.iItem = i;
+        lvi.iSubItem = 0;
+        lvi.pszText = const_cast<LPTSTR>((LPCTSTR)(n->cols.empty() ? _T("") : n->cols[0]));
+        InsertItem(&lvi);
+
+        for (int col = 1; col < GetHeaderCtrl()->GetItemCount(); ++col) {
+            CString txt = (col < (int)n->cols.size()) ? n->cols[col] : _T("");
+            SetItemText(i, col, txt);
+        }
+    }
+    SetRedraw(TRUE);
+    Invalidate();
+}
+
+void CExpandableListCtrl::Expand(Node* n)
+{
+    if (!n || n->children.empty()) return;
+    if (!n->expanded) { n->expanded = true; RebuildVisible(); }
+}
+
+void CExpandableListCtrl::Collapse(Node* n)
+{
+    if (!n || n->children.empty()) return;
+    if (n->expanded) { n->expanded = false; RebuildVisible(); }
+}
+
+void CExpandableListCtrl::Toggle(Node* n)
+{
+    if (!n || n->children.empty()) return;
+    n->expanded = !n->expanded;
+    RebuildVisible();
+}
+
+CExpandableListCtrl::Node* CExpandableListCtrl::GetNodeByVisibleIndex(int i) const
+{
+    if (i < 0 || i >= (int)m_visible.size()) return nullptr;
+    return m_visible[i];
+}
+
+CRect CExpandableListCtrl::expanderRectForRow(int row) const
+{
+    CRect rcLabel;
+    if (!const_cast<CExpandableListCtrl*>(this)->GetSubItemRect(row, 0, LVIR_LABEL, rcLabel))
+        return CRect(0, 0, 0, 0);
+
+    Node* n = const_cast<CExpandableListCtrl*>(this)->GetNodeByVisibleIndex(row);
+    if (!n || n->children.empty())
+        return CRect(0, 0, 0, 0); // 鍙跺瓙涓嶅崰浣嶏紝鏂囨湰灏变笉浼氳澶氭帹涓�鏍�
+
+    const int indent = n->level;
+    const int left = rcLabel.left + m_expanderPadding + indent * 16;
+
+    return CRect(
+        left,
+        rcLabel.CenterPoint().y - m_expanderSize / 2,
+        left + m_expanderSize,
+        rcLabel.CenterPoint().y + m_expanderSize / 2
+    );
+}
+
+
+void CExpandableListCtrl::OnClick(NMHDR* pNMHDR, LRESULT* pResult)
+{
+    LPNMITEMACTIVATE pia = reinterpret_cast<LPNMITEMACTIVATE>(pNMHDR);
+    if (pia->iItem >= 0) {
+        CPoint pt = pia->ptAction;
+
+        // 鍛戒腑灞曞紑鎸夐挳锛�
+        CRect expRc = expanderRectForRow(pia->iItem);
+        if (expRc.PtInRect(pt)) {
+            Node* n = GetNodeByVisibleIndex(pia->iItem);
+            if (n && !n->children.empty()) {
+                Toggle(n);
+            }
+        }
+    }
+    *pResult = 0;
+}
+
+void CExpandableListCtrl::OnCustomDraw(NMHDR* pNMHDR, LRESULT* pResult)
+{
+    LPNMLVCUSTOMDRAW pCD = reinterpret_cast<LPNMLVCUSTOMDRAW>(pNMHDR);
+
+    switch (pCD->nmcd.dwDrawStage)
+    {
+    case CDDS_PREPAINT:
+        *pResult = CDRF_NOTIFYITEMDRAW | CDRF_NOTIFYSUBITEMDRAW;
+        return;
+
+    case CDDS_ITEMPREPAINT:
+        *pResult = CDRF_NOTIFYSUBITEMDRAW;
+        return;
+
+    case CDDS_ITEMPREPAINT | CDDS_SUBITEM:
+    {
+        const int row = (int)pCD->nmcd.dwItemSpec;
+        const int col = pCD->iSubItem;
+        CDC* pDC = CDC::FromHandle(pCD->nmcd.hdc);
+
+        if (col == 0)
+        {
+            CRect rc; GetSubItemRect(row, 0, LVIR_LABEL, rc);
+            Node* n = GetNodeByVisibleIndex(row);
+            if (!n) { *pResult = CDRF_DODEFAULT; return; }
+
+            // 1) 鑳屾櫙/鍓嶆櫙棰滆壊锛氭寜鏄惁閫変腑
+            const bool selected = (GetItemState(row, LVIS_SELECTED) & LVIS_SELECTED) != 0;
+            const bool focusOnCtrl = (GetSafeHwnd() == ::GetFocus());
+            COLORREF bk = selected ? GetSysColor(focusOnCtrl ? COLOR_HIGHLIGHT : COLOR_3DFACE)
+                : ListView_GetBkColor(m_hWnd);
+            COLORREF txt = selected ? GetSysColor(focusOnCtrl ? COLOR_HIGHLIGHTTEXT : COLOR_WINDOWTEXT)
+                : ListView_GetTextColor(m_hWnd);
+
+            // 浠呭湪闇�瑕佹椂濉厖鑳屾櫙锛堥伩鍏嶁�滈粦涓�鐗団�濓級
+            CBrush bkBrush(bk);
+            pDC->FillRect(rc, &bkBrush);
+
+            // 2) 灞曞紑/鎶樺彔鎸囩ず锛堝弬鑰冩棫椤圭洰鐨勫彸瀵归綈鍧愭爣娉曪紝鍋氬儚绱犲榻愶紝绾疓DI锛�
+            if (!n->children.empty())
+            {
+                CRect box = expanderRectForRow(row);
+
+                // ---- 鍙皟鍙傛暟锛氫笌鏃т唬鐮佸懡鍚嶄竴鑷� ----
+                // 鍙充晶鐣欑櫧锛堜笌鏂囨湰闂撮殭/缃戞牸绾夸繚鎸佽窛绂伙級
+                const int ROFFSET = 2;
+                // 闂悎/灞曞紑鐨勨�滃搴︹�濊缃細濂囨暟鏇撮『鐪硷紙9/11 閮借锛�
+                const int WIDE = max(9, min(min(box.Width(), box.Height()), 13)); // 鈻� 鐨勮竟闀�
+                const int WIDE2 = WIDE / 2;                                        // 涓�鍗�
+                const int EXPANDED_WIDE = WIDE;                                           // 鈻� 鐨勮竟闀�
+
+                // 杞诲井鍐呯缉锛岄伩鍏嶈创杈癸紙涓庝綘鏃т唬鐮佲�滄寜閽鍒蜂竴涓嬧�濆悓鏁堬級
+                box.DeflateRect(1, 1);
+
+                // 缁熶竴鍋氬伓鏁板榻愶紝鍑忓皯鍗婂儚绱犻敮榻�
+                auto even = [](int v) { return (v & 1) ? (v - 1) : v; };
+
+                // 璁$畻鈥滆嚜涓嬪悜涓娾�濈殑鍩哄噯鍋忕Щ锛屼笌鏃� TreeCtrl 涓�鑷�
+                // 杩欓噷鐢� box 浣滀负 pRect
+                POINT pt[3];
+                if (n->expanded) {
+                    // 鈻�
+                    int nBottomOffset = (box.Height() - EXPANDED_WIDE) / 2;
+                    pt[0].x = box.right - ROFFSET - EXPANDED_WIDE;
+                    pt[0].y = box.bottom - nBottomOffset;
+                    pt[1].x = box.right - ROFFSET;
+                    pt[1].y = box.bottom - nBottomOffset;
+                    pt[2].x = box.right - ROFFSET;
+                    pt[2].y = box.bottom - nBottomOffset - EXPANDED_WIDE;
+                }
+                else {
+                    // 鈻�
+                    int nBottomOffset = (box.Height() - WIDE) / 2;
+
+                    pt[0].x = box.right - ROFFSET - WIDE2;
+                    pt[0].y = box.bottom - nBottomOffset - WIDE;
+                    pt[1].x = box.right - ROFFSET - WIDE2;
+                    pt[1].y = box.bottom - nBottomOffset;
+                    pt[2].x = box.right - ROFFSET;
+                    pt[2].y = box.bottom - nBottomOffset - WIDE2;
+                }
+
+                // 浠呭~鍏咃紝涓嶆弿杈癸紙鎻忚竟浼氬姞閲嶅彴闃舵劅锛夛紱棰滆壊鐢� txt 涓庝富棰樹竴鑷�
+                HGDIOBJ oldPen = pDC->SelectObject(GetStockObject(NULL_PEN));
+                HBRUSH   hBrush = CreateSolidBrush(txt);
+                HGDIOBJ oldBrush = pDC->SelectObject(hBrush);
+
+                pDC->Polygon(pt, 3);
+
+                pDC->SelectObject(oldPen);
+                pDC->SelectObject(oldBrush);
+                DeleteObject(hBrush);
+            }
+
+
+
+            // 3) 鏂囨湰锛氬熀浜庨鍒楀尯鍩熷彸绉伙紙鍖哄垎鏄惁鏈夊瓙鑺傜偣锛�
+            const int indentPx = n->level * 14;
+            const int baseLeft = rc.left + m_expanderPadding + indentPx;
+
+            CRect textRc = rc;
+            if (!n->children.empty()) {
+                // 鏈夊瓙椤癸細棰勭暀鎸夐挳浣� + 鏂囨湰闂撮殭
+                textRc.left = baseLeft + m_expanderSize + m_textGap;
+            }
+            else {
+                // 鍙跺瓙琛岋細涓嶉鐣欐寜閽綅锛屽彧缁欎竴鐐圭偣鍙跺瓙闂撮殭锛堣灞傜骇缂╄繘浠嶇劧鐢熸晥锛�
+                constexpr int kLeafGap = 2; // 浣犲彲璋� 0~4
+                textRc.left = baseLeft + kLeafGap;
+            }
+
+            pDC->SetBkMode(TRANSPARENT);
+            pDC->SetTextColor(txt);
+            CString txt0 = n->cols.empty() ? _T("") : n->cols[0];
+            pDC->DrawText(txt0, textRc, DT_SINGLELINE | DT_VCENTER | DT_NOPREFIX | DT_END_ELLIPSIS);
+
+
+            // 鈥斺�� 鐢诲畬涓夎涓庢枃鏈箣鍚庯紝琛ヤ竴鏉¤琛岀殑搴曢儴妯悜缃戞牸绾� 鈥斺��
+            // 浠呭綋寮�鍚簡 LVS_EX_GRIDLINES 鎵嶇粯鍒�
+            if (GetExtendedStyle() & LVS_EX_GRIDLINES)
+            {
+                // 鐢ㄦ暣琛� bounds锛屼繚璇佹í绾胯疮绌挎墍鏈夊垪鐨勫彲瑙佸搴�
+                CRect rcRow;
+                GetSubItemRect(row, 0, LVIR_BOUNDS, rcRow);
+
+                // 搴曡竟 y 鍧愭爣锛堜笌绯荤粺缃戞牸绾垮榻愶級
+                const int y = rcRow.bottom - 1;
+
+                // 棰滆壊涓庣郴缁熼鏍兼帴杩戯紱鑻ヨ寰楀亸娴咃紝鍙崲 COLOR_3DSHADOW
+                CPen pen(PS_SOLID, 1, GetSysColor(COLOR_3DLIGHT));
+                CPen* oldPen = pDC->SelectObject(&pen);
+
+                // 妯嚎浠庤宸﹀埌琛屽彸锛堝綋鍓嶅彲瑙佸尯鍩燂級
+                pDC->MoveTo(rcRow.left, y);
+                pDC->LineTo(rcRow.right, y);
+
+                pDC->SelectObject(oldPen);
+            }
+
+            *pResult = CDRF_SKIPDEFAULT;
+            return;
+        }
+
+        // 鍏朵粬鍒楅粯璁ょ粯鍒�
+        *pResult = CDRF_DODEFAULT;
+        return;
+    }
+
+    }
+
+    *pResult = CDRF_DODEFAULT;
+}
diff --git a/SourceCode/Bond/Servo/CExpandableListCtrl.h b/SourceCode/Bond/Servo/CExpandableListCtrl.h
new file mode 100644
index 0000000..57d606d
--- /dev/null
+++ b/SourceCode/Bond/Servo/CExpandableListCtrl.h
@@ -0,0 +1,58 @@
+#pragma once
+#include <vector>
+#include <memory>
+
+class CExpandableListCtrl : public CListCtrl
+{
+    DECLARE_DYNAMIC(CExpandableListCtrl)
+
+public:
+    struct Node {
+        Node* parent = nullptr;
+        std::vector<std::unique_ptr<Node>> children;
+        std::vector<CString> cols; // 各列文本
+        bool expanded = false;
+        int level = 0; // 缩进层级
+
+        Node(int nCols = 1) : cols(nCols) {}
+    };
+
+    CExpandableListCtrl();
+    virtual ~CExpandableListCtrl();
+
+    // 数据构建
+    Node* InsertRoot(const std::vector<CString>& cols);
+    Node* InsertChild(Node* parent, const std::vector<CString>& cols);
+
+    // 展开/折叠
+    void Expand(Node* n);
+    void Collapse(Node* n);
+    void Toggle(Node* n);
+
+    // 刷新可见列表
+    void RebuildVisible();
+
+    // 便捷:通过可见行号取 Node*
+    Node* GetNodeByVisibleIndex(int i) const;
+
+private:
+    void appendVisible(Node* n);
+    CRect expanderRectForRow(int row) const;        // 首列展开按钮区域
+    virtual void PreSubclassWindow();
+
+protected:
+    // 消息
+    afx_msg int  OnCreate(LPCREATESTRUCT lpCreateStruct);
+    afx_msg void OnClick(NMHDR* pNMHDR, LRESULT* pResult);
+    afx_msg void OnCustomDraw(NMHDR* pNMHDR, LRESULT* pResult);
+    DECLARE_MESSAGE_MAP()
+
+private:
+    std::vector<std::unique_ptr<Node>> m_roots;     // 顶层节点
+    std::vector<Node*>                 m_visible;   // 展开后的可见节点顺序
+    int  m_expanderPadding = 6;                     // 首列内侧边距
+    int  m_expanderSize = 10;                       // 小三角/方块大小
+    int  m_textGap = 6;
+};
+
+
diff --git a/SourceCode/Bond/Servo/CGlass.cpp b/SourceCode/Bond/Servo/CGlass.cpp
index 6c1b30f..974859c 100644
--- a/SourceCode/Bond/Servo/CGlass.cpp
+++ b/SourceCode/Bond/Servo/CGlass.cpp
@@ -11,6 +11,7 @@
 		m_nOriginPort = 0;
 		m_nOriginSlot = 0;
 		m_bScheduledForProcessing = FALSE;
+		m_pProcessJob = nullptr;
 	}
 
 	CGlass::~CGlass()
@@ -89,6 +90,16 @@
 	void CGlass::setScheduledForProcessing(BOOL bProcessing)
 	{
 		m_bScheduledForProcessing = bProcessing;
+	}
+
+	CProcessJob* CGlass::getProcessJob()
+	{
+		return m_pProcessJob;
+	}
+
+	void CGlass::setProcessJob(CProcessJob* pProcessJob)
+	{
+		m_pProcessJob = pProcessJob;
 	}
 
 	CPath* CGlass::getPath()
@@ -246,4 +257,116 @@
 
 		return pPath->getInspResult();
 	}
+
+	std::string CGlass::getStateText()
+	{
+		switch (m_state)
+		{
+		case SERVO::GlsState::NoState:
+			return "NoState";
+			break;
+		case SERVO::GlsState::Queued:
+			return "Queued";
+			break;
+		case SERVO::GlsState::InProcess:
+			return "InProcess";
+			break;
+		case SERVO::GlsState::Paused:
+			return "Queued";
+			break;
+		case SERVO::GlsState::Completed:
+			return "Queued";
+			break;
+		case SERVO::GlsState::Aborted:
+			return "Aborted";
+			break;
+		case SERVO::GlsState::Failed:
+			return "Failed";
+			break;
+		default:
+			break;
+		}
+
+		return "";
+	}
+
+	bool CGlass::queue() {
+		if (m_state != GlsState::NoState) return false;
+		markQueued();
+		return true;
+	}
+
+	bool CGlass::start() {
+		if (m_state != GlsState::Queued && m_state != GlsState::Paused)
+			return false;
+		if (!m_tStart.has_value()) markStart();
+		m_state = GlsState::InProcess;
+		return true;
+	}
+
+	bool CGlass::pause() {
+		if (m_state != GlsState::InProcess) return false;
+		m_state = GlsState::Paused;
+		return true;
+	}
+
+	bool CGlass::resume() {
+		if (m_state != GlsState::Paused) return false;
+		m_state = GlsState::InProcess;
+		return true;
+	}
+
+	bool CGlass::complete() {
+		if (m_state != GlsState::InProcess && m_state != GlsState::Paused) return false;
+		m_state = GlsState::Completed;
+		markEnd();
+		return true;
+	}
+
+	bool CGlass::abort() {
+		if (m_state == GlsState::Completed || m_state == GlsState::Aborted || m_state == GlsState::Failed)
+			return false;
+		m_state = GlsState::Aborted;
+		markEnd();
+		return true;
+	}
+
+	bool CGlass::fail(std::string reason)
+	{
+		m_failReason = trimCopy(reason);
+		clampString(m_failReason, 128);
+		m_state = GlsState::Failed;
+		markEnd();
+		return true;
+	}
+
+	std::string CGlass::trimCopy(std::string s)
+	{
+		auto notspace = [](int ch) { return !std::isspace(ch); };
+		s.erase(s.begin(), std::find_if(s.begin(), s.end(), notspace));
+		s.erase(std::find_if(s.rbegin(), s.rend(), notspace).base(), s.end());
+		return s;
+	}
+
+	void CGlass::clampString(std::string& s, size_t maxLen)
+	{
+		if (s.size() > maxLen) s.resize(maxLen);
+	}
+
+	// —— 时间戳 & 工具 —— 
+	void CGlass::markQueued() 
+	{
+		m_state = GlsState::Queued;
+		m_tQueued = std::chrono::system_clock::now();
+	}
+
+	void CGlass::markStart()
+	{
+		m_tStart = std::chrono::system_clock::now();
+	}
+
+	void CGlass::markEnd()
+	{
+		m_tEnd = std::chrono::system_clock::now();
+	}
 }
diff --git a/SourceCode/Bond/Servo/CGlass.h b/SourceCode/Bond/Servo/CGlass.h
index 844822b..21aaf34 100644
--- a/SourceCode/Bond/Servo/CGlass.h
+++ b/SourceCode/Bond/Servo/CGlass.h
@@ -7,9 +7,21 @@
 #include "CJobDataC.h"
 #include "CJobDataS.h"
 #include "ServoCommo.h"
+#include "ProcessJob.h"
 
 
 namespace SERVO {
+	/// PJ 生命周期(贴近 E40 常见状态)
+	enum class GlsState : uint8_t {
+		NoState = 0,
+		Queued,
+		InProcess,
+		Paused,
+		Completed,
+		Aborted,
+		Failed
+	};
+
 	class CGlass : public CContext
 	{
 	public:
@@ -28,6 +40,8 @@
 		void getOrginPort(int& port, int& slot);
 		BOOL isScheduledForProcessing();
 		void setScheduledForProcessing(BOOL bProcessing);
+		CProcessJob* getProcessJob();
+		void setProcessJob(CProcessJob* pProcessJob);
 		CPath* getPathWithEq(unsigned int nEqId, unsigned int nUnit);
 		CPath* getPath();
 		void addPath(unsigned int nEqId, unsigned int nUnit);
@@ -44,6 +58,37 @@
 		int setInspResult(unsigned int nEqId, unsigned int nUnit, InspResult result);
 		InspResult getInspResult(unsigned int nEqId, unsigned int nUnit);
 
+	public:
+		// 新增状态
+		GlsState state() const noexcept { return m_state; }
+		std::string getStateText();
+		GlsState m_state{ GlsState::NoState };
+		static void clampString(std::string& s, size_t maxLen);
+		static std::string trimCopy(std::string s);
+		std::string m_failReason;
+
+		// —— 状态机(带守卫)——
+		bool queue();           // NoState -> Queued
+		bool start();           // Queued -> InProcess
+		bool pause();           // InProcess -> Paused
+		bool resume();          // Paused -> InProcess
+		bool complete();        // InProcess -> Completed
+		bool abort();           // Any (未终态) -> Aborted
+		bool fail(std::string reason); // 任意态 -> Failed(记录失败原因)
+
+		// 时间戳
+		std::optional<std::chrono::system_clock::time_point> m_tQueued;
+		std::optional<std::chrono::system_clock::time_point> m_tStart;
+		std::optional<std::chrono::system_clock::time_point> m_tEnd;
+
+		// 时间戳(可用于报表/追溯)
+		std::optional<std::chrono::system_clock::time_point> tQueued() const { return m_tQueued; }
+		std::optional<std::chrono::system_clock::time_point> tStart()  const { return m_tStart; }
+		std::optional<std::chrono::system_clock::time_point> tEnd()    const { return m_tEnd; }
+		void markQueued();
+		void markStart();
+		void markEnd();
+
 	private:
 		MaterialsType m_type;
 		std::string m_strID;
@@ -54,6 +99,7 @@
 		int m_nOriginPort;
 		int m_nOriginSlot;
 		BOOL m_bScheduledForProcessing;			/* 是否将加工处理 */
+		CProcessJob* m_pProcessJob;
 	};
 }
 
diff --git a/SourceCode/Bond/Servo/CLoadPort.cpp b/SourceCode/Bond/Servo/CLoadPort.cpp
index f603990..7cc6006 100644
--- a/SourceCode/Bond/Servo/CLoadPort.cpp
+++ b/SourceCode/Bond/Servo/CLoadPort.cpp
@@ -363,7 +363,6 @@
 
 
 		// 模拟测试
-		/*
 		if (m_nIndex == 0) {
 			static int ii = 0;
 			ii++;
@@ -373,12 +372,25 @@
 				CPortStatusReport portStatusReport;
 				portStatusReport.setPortStatus(PORT_INUSE);
 				portStatusReport.setJobExistenceSlot(0xf);
-				portStatusReport.setCassetteId("CID1984113");
+				portStatusReport.setCassetteId("CID1001");
 				int nRet = portStatusReport.serialize(szBuffer, 64);
 				decodePortStatusReport(pStep, szBuffer, 64);
 			}
 		}
-		*/
+		if (m_nIndex == 1) {
+			static int ii = 0;
+			ii++;
+			if (ii == 55) {
+				char szBuffer[64] = { 0 };
+				CStep* pStep = getStepWithName(STEP_EQ_PORT2_INUSE);
+				CPortStatusReport portStatusReport;
+				portStatusReport.setPortStatus(PORT_INUSE);
+				portStatusReport.setJobExistenceSlot(0xff );
+				portStatusReport.setCassetteId("CID1004");
+				int nRet = portStatusReport.serialize(szBuffer, 64);
+				decodePortStatusReport(pStep, szBuffer, 64);
+			}
+		}
 	}
 
 	void CLoadPort::serialize(CArchive& ar)
diff --git a/SourceCode/Bond/Servo/CMaster.cpp b/SourceCode/Bond/Servo/CMaster.cpp
index 455804a..e01b180 100644
--- a/SourceCode/Bond/Servo/CMaster.cpp
+++ b/SourceCode/Bond/Servo/CMaster.cpp
@@ -3,6 +3,9 @@
 #include "CMaster.h"
 #include <future>
 #include <vector>
+#include "RecipeManager.h"
+#include <fstream>
+#include "SerializeUtil.h"
 
 
 namespace SERVO {
@@ -56,12 +59,23 @@
 		m_bEnableAlarmReport = true;
 		m_bContinuousTransfer = false;
 		m_nContinuousTransferCount = 0;
-		m_nContinuousTransferStep = CTStep_begin;
+		m_nContinuousTransferStep = CTStep_Unknow;
+		m_pControlJob = nullptr;
 		InitializeCriticalSection(&m_criticalSection);
 	}
 
 	CMaster::~CMaster()
 	{
+		// 释放Job相关
+		for (auto item : m_processJobs) {
+			delete item;
+		}
+		m_processJobs.clear();
+		if (m_pControlJob != nullptr) {
+			delete m_pControlJob;
+			m_pControlJob = nullptr;
+		}
+
 		if (m_hEventReadBitsThreadExit[0] != nullptr) {
 			::CloseHandle(m_hEventReadBitsThreadExit[0]);
 			m_hEventReadBitsThreadExit[0] = nullptr;
@@ -708,13 +722,15 @@
 				// Measurement -> LoadPort
 				for (int s = 0; s < 4; s++) {
 					PortType pt = pLoadPorts[s]->getPortType();
-					if (!rmd.armState[0] && pLoadPorts[s]->isEnable()
+					if ((m_nContinuousTransferStep == CTStep_Unknow || m_nContinuousTransferStep == CTStep_BakeCooling_Measurement)
+						&& !rmd.armState[0] && pLoadPorts[s]->isEnable()
 						&& (pt == PortType::Unloading || pt == PortType::Both)
 						&& pLoadPorts[s]->getPortStatus() == PORT_INUSE) {
 						for (int slot = 0; slot < SLOT_MAX; slot++) {
 							m_pActiveRobotTask = createTransferTask_continuous_transfer(pMeasurement,
 								0, pLoadPorts[s], slot);
 							if (m_pActiveRobotTask != nullptr) {
+								m_nContinuousTransferStep = CTStep_Measurement_LoadPort;
 								m_nContinuousTransferStep = CTStep_end;
 								goto CT_PORT_PUT;
 							}
@@ -727,10 +743,12 @@
 
 
 				// BakeCooling ->Measurement
-				if (!rmd.armState[0]) {
+				if ((m_nContinuousTransferStep == CTStep_Unknow || m_nContinuousTransferStep == CTStep_BakeCooling_BakeCooling3)
+					&& !rmd.armState[0]) {
 					m_pActiveRobotTask = createTransferTask_continuous_transfer(pBakeCooling,
 						3, pMeasurement, 0);
 					if (m_pActiveRobotTask != nullptr) {
+						m_nContinuousTransferStep = CTStep_BakeCooling_Measurement;
 						LOGI("<ContinuousTransfer>千传测试,开始搬送任务(BakeCooling -> Measurement)...");
 					}
 					CHECK_RUN_ACTIVE_ROBOT_TASK(m_pActiveRobotTask);
@@ -738,86 +756,104 @@
 
 				
 				// BakeCooling内部
-				if (!rmd.armState[0]) {
+				if ((m_nContinuousTransferStep == CTStep_Unknow || m_nContinuousTransferStep == CTStep_BakeCooling_BakeCooling2)
+					&& !rmd.armState[0]) {
 					m_pActiveRobotTask = createTransferTask_continuous_transfer(pBakeCooling,
 						2, pBakeCooling, 3);
 					if (m_pActiveRobotTask != nullptr) {
+						m_nContinuousTransferStep = CTStep_BakeCooling_BakeCooling3;
 						LOGI("<ContinuousTransfer>千传测试,开始搬送任务(BakeCooling-2 -> BakeCooling-3)...");
 					}
 					CHECK_RUN_ACTIVE_ROBOT_TASK(m_pActiveRobotTask);
 				}
-				if (!rmd.armState[0]) {
+				if ((m_nContinuousTransferStep == CTStep_Unknow || m_nContinuousTransferStep == CTStep_BakeCooling_BakeCooling1)
+					&& !rmd.armState[0]) {
 					m_pActiveRobotTask = createTransferTask_continuous_transfer(pBakeCooling,
 						1, pBakeCooling, 2);
 					if (m_pActiveRobotTask != nullptr) {
+						m_nContinuousTransferStep = CTStep_BakeCooling_BakeCooling2;
 						LOGI("<ContinuousTransfer>千传测试,开始搬送任务(BakeCooling-1 -> BakeCooling-2)...");
 					}
 					CHECK_RUN_ACTIVE_ROBOT_TASK(m_pActiveRobotTask);
 				}
-				if (!rmd.armState[0]) {
+				if ((m_nContinuousTransferStep == CTStep_Unknow || m_nContinuousTransferStep == CTStep_VacuumBake_BakeCooling)
+					&& !rmd.armState[0]) {
 					m_pActiveRobotTask = createTransferTask_continuous_transfer(pBakeCooling,
 						0, pBakeCooling, 1);
 					if (m_pActiveRobotTask != nullptr) {
+						m_nContinuousTransferStep = CTStep_BakeCooling_BakeCooling1;
 						LOGI("<ContinuousTransfer>千传测试,开始搬送任务(BakeCooling-0 -> BakeCooling-1)...");
 					}
 					CHECK_RUN_ACTIVE_ROBOT_TASK(m_pActiveRobotTask);
 				}
 
 				// VacuumBake(G1) -> BakeCooling
-				if (!rmd.armState[0]) {
+				if ((m_nContinuousTransferStep == CTStep_Unknow || m_nContinuousTransferStep == CTStep_VacuumBake_VacuumBake)
+					&& !rmd.armState[0]) {
 					m_pActiveRobotTask = createTransferTask_continuous_transfer(pVacuumBake,
 						1, pBakeCooling, 0);
 					if (m_pActiveRobotTask != nullptr) {
+						m_nContinuousTransferStep = CTStep_VacuumBake_BakeCooling;
 						LOGI("<ContinuousTransfer>千传测试,开始搬送任务(VacuumBake(G1) -> BakeCooling)...");
 					}
 					CHECK_RUN_ACTIVE_ROBOT_TASK(m_pActiveRobotTask);
 				}
 
 				// VacuumBake(G1) -> VacuumBake(G1)
-				if (!rmd.armState[0]) {
+				if ((m_nContinuousTransferStep == CTStep_Unknow || m_nContinuousTransferStep == CTStep_Bonder2_VacuumBake)
+					&& !rmd.armState[0]) {
 					m_pActiveRobotTask = createTransferTask_continuous_transfer(pVacuumBake,
 						0, pVacuumBake, 1);
 					if (m_pActiveRobotTask != nullptr) {
+						m_nContinuousTransferStep = CTStep_VacuumBake_VacuumBake;
 						LOGI("<ContinuousTransfer>千传测试,开始搬送任务(VacuumBake(G1-0) -> VacuumBake(G1-1))...");
 					}
 					CHECK_RUN_ACTIVE_ROBOT_TASK(m_pActiveRobotTask);
 				}
 
 				// Bonder2 -> VacuumBake(G1)
-				if (!rmd.armState[0]) {
+				if ((m_nContinuousTransferStep == CTStep_Unknow || m_nContinuousTransferStep == CTStep_Bonder1_Bonder2)
+					&& !rmd.armState[0]) {
 					m_pActiveRobotTask = createTransferTask_continuous_transfer(pBonder2,
 						1, pVacuumBake, 0);
 					if (m_pActiveRobotTask != nullptr) {
+						m_nContinuousTransferStep = CTStep_Bonder2_VacuumBake;
 						LOGI("<ContinuousTransfer>千传测试,开始搬送任务(Bonder2 -> VacuumBake(G1))...");
 					}
 					CHECK_RUN_ACTIVE_ROBOT_TASK(m_pActiveRobotTask);
 				}
 				
 				// Bonder1 -> Bonder2
-				if (!rmd.armState[0] && !pBonder2->hasBondClass()) {
+				if ((m_nContinuousTransferStep == CTStep_Unknow || m_nContinuousTransferStep == CTStep_Fliper_Bonder1)
+					&& !rmd.armState[0] && !pBonder2->hasBondClass()) {
 					m_pActiveRobotTask = createTransferTask_continuous_transfer(pBonder1,
 						1, pBonder2, 1);
 					if (m_pActiveRobotTask != nullptr) {
+						m_nContinuousTransferStep = CTStep_Bonder1_Bonder2;
 						LOGI("<ContinuousTransfer>千传测试,开始搬送任务(Bonder1 -> Bonder2)...");
 					}
 					CHECK_RUN_ACTIVE_ROBOT_TASK(m_pActiveRobotTask);
 				}
 
 				// Fliper(G2) -> Bonder1
-				if (!rmd.armState[0] && !pBonder1->hasBondClass()) {
+				if ((m_nContinuousTransferStep == CTStep_Unknow || m_nContinuousTransferStep == CTStep_Aligner_Fliper)
+					&&!rmd.armState[0] && !pBonder1->hasBondClass()) {
 					m_pActiveRobotTask = createTransferTask_continuous_transfer(pFliper,
-						0, pBonder1, 1, 2);
+						0, pBonder1, 1);
 					if (m_pActiveRobotTask != nullptr) {
+						m_nContinuousTransferStep = CTStep_Fliper_Bonder1;
 						LOGI("<ContinuousTransfer>千传测试,开始搬送任务(Fliper(G2) -> Bonder1)...");
 					}
 					CHECK_RUN_ACTIVE_ROBOT_TASK(m_pActiveRobotTask);
 				}
 
 				// Aligner -> Fliper(G2)
-				if (!rmd.armState[1]) {
+				if ((m_nContinuousTransferStep == CTStep_Unknow || m_nContinuousTransferStep == CTStep_LoadPort_Aligner)
+					&& !rmd.armState[1]) {
 					m_pActiveRobotTask = createTransferTask_continuous_transfer(pAligner,
 						0, pFliper, 0);
 					if (m_pActiveRobotTask != nullptr) {
+						m_nContinuousTransferStep = CTStep_Aligner_Fliper;
 						LOGI("<ContinuousTransfer>千传测试,开始搬送任务(Aligner -> Fliper(G2))...");
 					}
 					CHECK_RUN_ACTIVE_ROBOT_TASK(m_pActiveRobotTask);
@@ -826,13 +862,15 @@
 				// LoadPort -> Aligner
 				for (int s = 0; s < 4; s++) {
 					PortType pt = pLoadPorts[s]->getPortType();
-					if (!rmd.armState[0] && pLoadPorts[s]->isEnable()
+					if ((m_nContinuousTransferStep == CTStep_Unknow || m_nContinuousTransferStep == CTStep_end)
+						&& !rmd.armState[0] && pLoadPorts[s]->isEnable()
 						&& (pt == PortType::Loading || pt == PortType::Both)
 						&& pLoadPorts[s]->getPortStatus() == PORT_INUSE) {
 						for (int slot = 0; slot < SLOT_MAX; slot++) {
 							m_pActiveRobotTask = createTransferTask_continuous_transfer(pLoadPorts[s], 
 								slot, pAligner, 0);
 							if (m_pActiveRobotTask != nullptr) {
+								m_nContinuousTransferStep = CTStep_LoadPort_Aligner;
 								LOGI("<ContinuousTransfer>千传测试,开始搬送任务(LoadPort -> Aligner)...");
 								pEFEM->setContext(m_pActiveRobotTask->getContext());
 								goto CT_PORT_GET;
@@ -1105,6 +1143,25 @@
 		};
 		listener.onPortStatusChanged = [&](void* pEquipment, short status, __int64 data) {
 			LOGE("<Master-%s>onPortStatusChanged。status=%d, data=%lld", ((CEquipment*)pEquipment)->getName().c_str(), status);
+			if (status == PORT_INUSE && m_pControlJob != nullptr) {
+				CLoadPort* pPort = (CLoadPort*)pEquipment;
+				auto pjs = m_pControlJob->getPjs();
+				for (auto pj : pjs) {
+					auto carrier = pj->getCarrier(pPort->getCassetteId());
+					if (carrier != nullptr) {
+						for (auto slot : carrier->slots) {
+							CGlass* pGlass = pPort->getGlassFromSlot(slot);
+							carrier->contexts.push_back((void*)pGlass);
+							if (pGlass != nullptr) {
+								pGlass->setProcessJob(pj);
+							}
+						}
+					}
+				}
+
+
+			}
+			
 			if (m_listener.onLoadPortStatusChanged != nullptr) {
 				m_listener.onLoadPortStatusChanged(this, (CEquipment*)pEquipment, status, data);
 			}
@@ -1122,6 +1179,15 @@
 	}
 
 	CEquipment* CMaster::getEquipment(int id)
+	{
+		for (auto item : m_listEquipment) {
+			if (item->getID() == id) return item;
+		}
+
+		return nullptr;
+	}
+
+	CEquipment* CMaster::getEquipment(int id) const
 	{
 		for (auto item : m_listEquipment) {
 			if (item->getID() == id) return item;
@@ -1807,4 +1873,220 @@
 	{
 		m_nContinuousTransferCount = round;
 	}
+
+	int CMaster::setProcessJobs(std::vector<CProcessJob*>& pjs)
+	{
+		std::vector<SERVO::CProcessJob*> temp;
+		for (auto p : pjs) {
+			if (p->validate(*this)) {
+				p->queue();
+				temp.push_back(p);
+			}
+		}
+
+		m_processJobs = temp;
+		this->saveState();
+
+		return (int)m_processJobs.size();
+	}
+
+	std::vector<CProcessJob*>& CMaster::getProcessJobs()
+	{
+		return m_processJobs;
+	}
+
+	CProcessJob* CMaster::getProcessJob(const std::string& id)
+	{
+		for (auto item : m_processJobs) {
+			if (item->id().compare(id) == 0) return item;
+		}
+
+		return nullptr;
+	}
+
+	int CMaster::setControlJob(CControlJob& controlJob)
+	{
+		// 回调:是否参创建ControlJob
+		auto canCreateCjFn = [&](uint32_t& cc, std::string& mm) -> bool {
+			if (m_pControlJob != nullptr) {
+				cc = 1100;
+				mm = "当前ControlJob未结批,不能创建新的ControlJob";
+				return false;
+			}
+			return true;
+		};
+
+
+		// 回调:是否存在
+		auto pjExists = [&](const std::string& id) -> bool {
+			return getProcessJob(id) != nullptr;
+		};
+
+		// 回调:是否可加入 CJ(这里定义:必须是 Queued)
+		auto pjJoinable = [&](const std::string& id) -> bool {
+			auto pj = getProcessJob(id);
+			if (pj == nullptr) return false;
+			return pj->state() == PJState::Queued;
+		};
+
+		bool bRet = controlJob.validateForCreate(canCreateCjFn, pjExists, pjJoinable);
+		if (!bRet) return -1;
+
+		std::vector<CProcessJob*> temps;
+		m_pControlJob = new CControlJob(controlJob);
+		auto pjIds = controlJob.pjIds();
+		for (auto id : pjIds) {
+			auto pj = getProcessJob(id);
+			if (pj != nullptr) {
+				temps.push_back(pj);
+			}
+		}
+		m_pControlJob->setPJs(temps);
+		this->saveState();
+
+
+		return 0;
+	}
+
+	CControlJob* CMaster::getControlJob()
+	{
+		return m_pControlJob;
+	}
+
+	CLoadPort* CMaster::getPortWithCarrierId(const std::string& carrierId) const
+	{
+		CLoadPort* pPort;
+		int eqid[] = { EQ_ID_LOADPORT1, EQ_ID_LOADPORT2, EQ_ID_LOADPORT3, EQ_ID_LOADPORT4};
+		for (int i = 0; i < 4; i++) {
+			pPort = (CLoadPort*)getEquipment(eqid[i]);
+			ASSERT(pPort);
+			if (pPort->getCassetteId().compare(carrierId) == 0) return pPort;
+		}
+
+		return nullptr;
+	}
+
+	bool CMaster::isProcessJobsEmpty() const
+	{
+		return m_processJobs.empty();
+	}
+
+	bool CMaster::recipeExists(const std::string& ppid) const
+	{
+		std::vector<std::string> vecRecipe = RecipeManager::getInstance().getAllPPID();
+		bool exists = std::find(vecRecipe.begin(), vecRecipe.end(), ppid) != vecRecipe.end();
+		return exists;
+	}
+
+	bool CMaster::carrierPresent(const std::string& carrierId) const
+	{
+		CLoadPort* pPort = getPortWithCarrierId(carrierId);
+		return pPort != nullptr;
+	}
+
+	bool CMaster::slotUsable(const std::string& carrierId, uint16_t slot) const
+	{
+		CLoadPort* pPort = getPortWithCarrierId(carrierId);
+		if(pPort == nullptr) return false;
+		CSlot* pSlot = pPort->getSlot(slot);
+		if (pSlot == nullptr) return false;
+		return pSlot->isEnable();
+	}
+
+	bool CMaster::ceidDefined(uint32_t ceid) const
+	{
+		return true;
+	}
+
+	bool CMaster::saveState() const
+	{
+		std::ofstream ofs(m_strStatePath, std::ios::binary);
+		if (!ofs) return false;
+
+		// 文件头
+		uint32_t magic = 0x4D415354; // 'MAST'
+		uint16_t version = 1;
+		ofs.write(reinterpret_cast<const char*>(&magic), sizeof(magic));
+		ofs.write(reinterpret_cast<const char*>(&version), sizeof(version));
+
+		// 保存 ControlJob
+		bool hasCJ = (m_pControlJob != nullptr);
+		ofs.write(reinterpret_cast<const char*>(&hasCJ), sizeof(hasCJ));
+		if (hasCJ) {
+			m_pControlJob->serialize(ofs);
+		}
+
+		// 保存 ProcessJob 列表
+		uint32_t count = static_cast<uint32_t>(m_processJobs.size());
+		ofs.write(reinterpret_cast<const char*>(&count), sizeof(count));
+		for (const auto& job : m_processJobs) {
+			job->serialize(ofs);
+		}
+
+		// 以后可以在这里追加新字段
+		return true;
+	}
+
+	bool CMaster::loadState(const std::string& path)
+	{
+		// 保存文件路径
+		m_strStatePath = path;
+
+
+		std::ifstream ifs(path, std::ios::binary);
+		if (!ifs) return false;
+
+		// 文件头
+		uint32_t magic = 0;
+		uint16_t version = 0;
+		ifs.read(reinterpret_cast<char*>(&magic), sizeof(magic));
+		ifs.read(reinterpret_cast<char*>(&version), sizeof(version));
+
+		if (magic != 0x4D415354) {
+			// 文件不合法
+			return false;
+		}
+
+		if (m_pControlJob != nullptr) {
+			delete m_pControlJob;
+			m_pControlJob = nullptr;
+		}
+
+		// 读取 ControlJob
+		bool hasCJ = false;
+		ifs.read(reinterpret_cast<char*>(&hasCJ), sizeof(hasCJ));
+		if (hasCJ) {
+			m_pControlJob = new CControlJob();
+			if (!CControlJob::deserialize(ifs, *m_pControlJob)) return false;
+		}
+
+
+		// 读取 ProcessJob 列表
+		uint32_t count = 0;
+		ifs.read(reinterpret_cast<char*>(&count), sizeof(count));
+		m_processJobs.clear();
+		for (uint32_t i = 0; i < count; i++) {
+			CProcessJob* pProcessJob = new CProcessJob();
+			if (!CProcessJob::deserialize(ifs, *pProcessJob)) return false;
+			m_processJobs.push_back(pProcessJob);
+		}
+
+
+		// 找到CProcessJob指针加入列表中
+		std::vector<CProcessJob*> tempPjs;
+		auto ids = m_pControlJob->pjIds();
+		for (auto id : ids) {
+			auto pj = getProcessJob(id);
+			if (pj != nullptr) {
+				tempPjs.push_back(pj);
+			}
+		}
+		m_pControlJob->setPJs(tempPjs);
+
+
+		// 如果版本升级,可在这里判断 version 来加载新字段
+
+
+		return true;
+	}
 }
diff --git a/SourceCode/Bond/Servo/CMaster.h b/SourceCode/Bond/Servo/CMaster.h
index e5f868d..e8c7236 100644
--- a/SourceCode/Bond/Servo/CMaster.h
+++ b/SourceCode/Bond/Servo/CMaster.h
@@ -13,10 +13,25 @@
 #include "CArmTray.h"
 #include "CCLinkIEControl.h"
 #include "CRobotTask.h"
+#include "ProcessJob.h"
+#include "CControlJob.h"
 
 
-#define CTStep_begin        0
-#define CTStep_end          99
+#define CTStep_Unknow                   0
+#define CTStep_LoadPort_Aligner         1
+#define CTStep_Aligner_Fliper           2
+#define CTStep_Fliper_Bonder1           3
+#define CTStep_Bonder1_Bonder2          4
+#define CTStep_Bonder2_VacuumBake       5
+#define CTStep_VacuumBake_VacuumBake    6
+#define CTStep_VacuumBake_BakeCooling   7
+#define CTStep_BakeCooling_BakeCooling1 8
+#define CTStep_BakeCooling_BakeCooling2 9
+#define CTStep_BakeCooling_BakeCooling3 10
+#define CTStep_BakeCooling_Measurement  11
+#define CTStep_Measurement_LoadPort     12
+#define CTStep_begin                    CTStep_LoadPort_Aligner
+#define CTStep_end                      CTStep_Measurement_LoadPort
 
 namespace SERVO {
     enum class MASTERSTATE {
@@ -50,7 +65,7 @@
         ONCTROUNDEND            onCTRoundEnd;
     } MasterListener;
 
-    class CMaster
+    class CMaster : public IResourceView
     {
     public:
         CMaster();
@@ -73,6 +88,7 @@
         void onTimer(UINT nTimerid);
         std::list<CEquipment*>& getEquipmentList();
         CEquipment* getEquipment(int id);
+        CEquipment* getEquipment(int id) const;
         void setCacheFilepath(const char* pszFilepath);
         int abortCurrentTask();
         int restoreCurrentTask();
@@ -90,6 +106,14 @@
         int carrierRelease(unsigned int port);
         int getContinuousTransferCount();
         void setContinuousTransferCount(int round);
+        int setProcessJobs(std::vector<CProcessJob*>& pjs);
+        std::vector<CProcessJob*>& getProcessJobs();
+        CProcessJob* getProcessJob(const std::string& id);
+        int setControlJob(CControlJob& controlJob);
+        CControlJob* getControlJob();
+        CLoadPort* getPortWithCarrierId(const std::string& carrierId) const;
+        bool saveState() const;
+        bool loadState(const std::string& path);
 
     private:
         inline void lock() { EnterCriticalSection(&m_criticalSection); }
@@ -120,6 +144,14 @@
         CRobotTask* createTransferTask_restore(CEquipment* pEqSrc, CLoadPort** pPorts);
         CRobotTask* createTransferTask_continuous_transfer(CEquipment* pSrcEq, int nSrcSlot,
             CEquipment* pTarEq, int nTarSlot, int armNo = 1);
+
+    public:
+        // —— IResourceView 覆写 ——(注意 const)
+        bool isProcessJobsEmpty() const override;
+        bool recipeExists(const std::string& ppid) const override;
+        bool carrierPresent(const std::string& carrierId) const override;
+        bool slotUsable(const std::string& carrierId, uint16_t slot) const override;
+        bool ceidDefined(uint32_t ceid) const override;
 
     private:
         CRITICAL_SECTION m_criticalSection;
@@ -165,6 +197,9 @@
     private:
         bool m_bEnableEventReport;
         bool m_bEnableAlarmReport;
+        SERVO::CControlJob* m_pControlJob;
+        std::vector<SERVO::CProcessJob*> m_processJobs;
+        std::string m_strStatePath;
     };
 }
 
diff --git a/SourceCode/Bond/Servo/CVariable.cpp b/SourceCode/Bond/Servo/CVariable.cpp
index e9c5d83..e7b916c 100644
--- a/SourceCode/Bond/Servo/CVariable.cpp
+++ b/SourceCode/Bond/Servo/CVariable.cpp
@@ -43,6 +43,9 @@
 		if (_strcmpi("A20", pszFormat) == 0) {
 			return SVFromat::A20;
 		}
+		if (_strcmpi("L", pszFormat) == 0) {
+			return SVFromat::L;
+		}
 
 		return SVFromat::U1;
 	}
@@ -64,6 +67,9 @@
 		}
 		if (SVFromat::A20 == format) {
 			return "A20";
+		}
+		if (SVFromat::L == format) {
+			return "L";
 		}
 
 		return "U1";
@@ -113,6 +119,11 @@
 		m_strValue = strTemp;
 	}
 
+	void CVariable::setValue(std::vector<CVariable>& vars)
+	{
+		m_varsValue = vars;
+	}
+
 	std::string CVariable::getValue()
 	{
 		std::string strRet;
@@ -125,4 +136,18 @@
 
 		return strRet;
 	}
+
+	__int64 CVariable::getIntValue()
+	{
+		if (m_format == SVFromat::U1 || m_format == SVFromat::U2 || m_format == SVFromat::I2) {
+			return m_nValue;
+		}
+
+		return 0;
+	}
+
+	std::vector<CVariable>& CVariable::getVarsValue()
+	{
+		return m_varsValue;
+	}
 }
\ No newline at end of file
diff --git a/SourceCode/Bond/Servo/CVariable.h b/SourceCode/Bond/Servo/CVariable.h
index beeadd2..597b84d 100644
--- a/SourceCode/Bond/Servo/CVariable.h
+++ b/SourceCode/Bond/Servo/CVariable.h
@@ -9,7 +9,8 @@
 		U2,
 		I2,
 		A20,
-		A50
+		A50,
+		L
 	};
 
 	class CVariable
@@ -28,7 +29,10 @@
 		std::string& getRemark();
 		void setValue(__int64 value);
 		void setValue(const char* pszValue);
+		void setValue(std::vector<CVariable>& vars);
 		std::string getValue();
+		__int64 getIntValue();
+		std::vector<CVariable>& getVarsValue();
 
 	private:
 		unsigned int m_nVarialbeId;
@@ -37,6 +41,7 @@
 		std::string m_strRemark;
 		__int64 m_nValue;
 		std::string m_strValue;
+		std::vector<CVariable> m_varsValue;
 	};
 }
 
diff --git a/SourceCode/Bond/Servo/HsmsPassive.cpp b/SourceCode/Bond/Servo/HsmsPassive.cpp
index 7a80fd5..689920b 100644
--- a/SourceCode/Bond/Servo/HsmsPassive.cpp
+++ b/SourceCode/Bond/Servo/HsmsPassive.cpp
@@ -103,6 +103,43 @@
 
 }
 
+void CHsmsPassive::addVariableValueToItem(ISECS2Item* pParent, SERVO::CVariable* pVariable)
+{
+	ASSERT(pParent);
+	ASSERT(pVariable);
+
+
+	ISECS2Item* pItemList;
+	SERVO::SVFromat format = pVariable->getFormat();
+	switch (format)
+	{
+	case SERVO::SVFromat::U1:
+		pParent->addU1Item((unsigned char)pVariable->getIntValue(), "SV");
+		break;
+	case SERVO::SVFromat::U2:
+		pParent->addU2Item((unsigned char)pVariable->getIntValue(), "SV");
+		break;
+	case SERVO::SVFromat::I2:
+		pParent->addI2Item((unsigned char)pVariable->getIntValue(), "SV");
+		break;
+	case SERVO::SVFromat::A20:
+	case SERVO::SVFromat::A50:
+		pParent->addItem(pVariable->getValue().c_str(), "SV");
+		break;
+	case SERVO::SVFromat::L:
+		pItemList = pParent->addItem();
+		{
+			auto vars = pVariable->getVarsValue();
+			for (auto v : vars) {
+				addVariableValueToItem(pItemList, &v);
+			}
+		}
+		break;
+	default:
+		break;
+	}
+}
+
 void CHsmsPassive::linkEventReport(unsigned int CEID, unsigned int RPTID)
 {
 	SERVO::CCollectionEvent* pEvent = getEvent(CEID);
@@ -288,6 +325,14 @@
 	auto v = getVariable(pszName);
 	if (v != nullptr) {
 		v->setValue(value);
+	}
+}
+
+void CHsmsPassive::setVariableValue(const char* pszName, std::vector<SERVO::CVariable>& vars)
+{
+	auto v = getVariable(pszName);
+	if (v != nullptr) {
+		v->setValue(vars);
 	}
 }
 
@@ -536,6 +581,9 @@
 			// S1F1
 			replyAreYouThere(pMessage);
 		}
+		else if (nStream == 1 && pHeader->function == 3) {
+			replySelectedEquipmentStatusData(pMessage);
+		}
 		else if (nStream == 1 && pHeader->function == 13) {
 			replyEstablishCommunications(pMessage);
 		}
@@ -583,6 +631,12 @@
 		}
 		else if (nStream == 10 && pHeader->function == 3) {
 			replyTerminalDisplay(pMessage);
+		}
+		else if (nStream == 14 && pHeader->function == 9) {
+			replyCreateObj(pMessage);
+		}
+		else if (nStream == 16 && pHeader->function == 15) {
+			replyPRJobMultiCreate(pMessage);
 		}
 	};
 
@@ -898,6 +952,46 @@
 	m_pPassive->sendMessage(pMessage);
 	LOGI("<HSMS>[SECS Msg SEND]%s", pMessage->toString());
 	HSMS_Destroy1Message(pMessage);
+
+	return 0;
+}
+
+// S1F3
+int CHsmsPassive::replySelectedEquipmentStatusData(IMessage* pRecv)
+{
+	if (m_pPassive == NULL || STATE::SELECTED != m_pPassive->getState()) {
+		return ER_NOTSELECT;
+	}
+
+	IMessage* pMessage = NULL;
+	HSMS_Create1Message(pMessage, m_nSessionId, 1, 4, pRecv->getHeader()->systemBytes);
+	ASSERT(pMessage);
+
+	unsigned char SVU1 = 0;
+	unsigned int SVID = 0;
+	ISECS2Item* pBody = pRecv->getBody();
+	if (pBody == nullptr || pBody->getType() != SITYPE::L) {
+		pMessage->getBody()->addU1Item(SVU1, "SV");
+		goto MYREPLY;
+	}
+	if (!pBody->getSubItemU4(0, SVID)) {
+		pMessage->getBody()->addU1Item(SVU1, "SV");
+		goto MYREPLY;
+	}
+
+	SERVO::CVariable* pVariable = getVariable(SVID);
+	if (pVariable == nullptr) {
+		pMessage->getBody()->addU1Item(SVU1, "SV");
+		goto MYREPLY;
+	}
+	addVariableValueToItem(pMessage->getBody(), pVariable);
+
+MYREPLY:
+	m_pPassive->sendMessage(pMessage);
+	LOGI("<HSMS>[SECS Msg SEND]%s", pMessage->toString());
+	HSMS_Destroy1Message(pMessage);
+
+
 
 	return 0;
 }
@@ -1447,6 +1541,245 @@
 	return 0;
 }
 
+// S14F9
+int CHsmsPassive::replyCreateObj(IMessage* pRecv)
+{
+	if (m_pPassive == NULL || STATE::SELECTED != m_pPassive->getState()) {
+		return ER_NOTSELECT;
+	}
+	ISECS2Item* pBody = pRecv->getBody();
+	if (pBody == nullptr || pBody->getType() != SITYPE::L) ER_PARAM_ERROR;
+	
+
+	// 鏄惁鍒涘缓鎴愬姛骞跺噯澶囧洖澶嶆姤鏂�
+	bool bCreateOk = false;
+	IMessage* pReply = NULL;
+	HSMS_Create1Message(pReply, m_nSessionId, 14, 10, ++m_nSystemByte);
+	ASSERT(pReply);
+
+
+
+	// 瑙i噴鏁版嵁锛屽緱鍒癈ontrolJob
+	ISECS2Item* pItemAttrs, * pItemAttr, *pItemAttrData;
+	const char* pszObjSpec, *pszObjType, *pszAttrId, *pszProcessJobId;
+	std::string strObjName, strObjId;
+	if (!pBody->getSubItemString(0, pszObjSpec)) return ER_PARAM_ERROR;
+	if (!pBody->getSubItemString(1, pszObjType)) return ER_PARAM_ERROR;
+
+	pReply->getBody()->addItem(pszObjSpec, "OBJSPEC");
+	ISECS2Item* pReplyItemAttrs = pReply->getBody()->addItem();
+	ISECS2Item* pReplyItemAcks = pReply->getBody()->addItem();
+	ISECS2Item* pReplyItemAck = pReplyItemAcks->addU1Item(0, "OBJACK");
+	ISECS2Item* pReplyItemErrs = pReplyItemAcks->addItem();
+
+	// 褰撳墠鍙鐞嗙被鍚勪负ControlJob
+	if (_strcmpi(pszObjType, "ControlJob") == 0) {
+
+		// 绫籭d
+		std::regex re("^([^:]+):([^>]+)>");
+		std::smatch match;
+		std::string strObjSpec(pszObjSpec);
+		if (!std::regex_search(strObjSpec, match, re)) {
+			ISECS2Item* pItemError = pReplyItemErrs->addItem();
+			pItemError->addU4Item(2001, "ERRCODE");
+			pItemError->addItem("鍙傛暟鎴栨姤鏂囦笉姝g‘", "ERRTEXT");
+			goto MYREPLY;
+		}
+
+		if (match[1].compare("ControlJob") != 0) {
+			ISECS2Item* pItemError = pReplyItemErrs->addItem();
+			pItemError->addU4Item(2001, "ERRCODE");
+			pItemError->addItem("涓嶆敮鎸佺殑OBJ", "ERRTEXT");
+			goto MYREPLY;
+		}
+		strObjId = match[2];
+
+		// 鍒涘缓绫籆ControlJob
+		SERVO::CControlJob controlJob(strObjId);
+
+		// 绫诲睘鎬�
+		pItemAttrs = pBody->getSubItem(2);
+		if (pItemAttrs == nullptr) return ER_PARAM_ERROR;
+		for (int i = 0; i < pItemAttrs->getSubItemSize(); i++) {
+			pItemAttr = pItemAttrs->getSubItem(i);
+			if (pItemAttr == nullptr) continue;
+			if (!pItemAttr->getSubItemString(0, pszAttrId)) continue;
+			if (_strcmpi(pszAttrId, CJ_ATTR_PRIORITY) == 0) {
+				uint8_t priority;
+				if (pItemAttr->getSubItemU1(1, priority)) {
+					controlJob.setPriority(priority);
+				}
+			}
+			else if (_strcmpi(pszAttrId, CJ_ATTR_PRJOBLIST) == 0) {
+				pItemAttrData = pItemAttr->getSubItem(1);
+				if (pItemAttrData != nullptr && pItemAttrData->getType() == SITYPE::L) {
+					for (int i = 0; i < pItemAttrData->getSubItemSize(); i++) {
+						if (pItemAttrData->getSubItemString(i, pszProcessJobId)) {
+							std::string strProcessJobId(pszProcessJobId);
+							controlJob.addPJ(strProcessJobId);
+						}
+					}
+				}
+			}
+		}
+
+
+		ASSERT(m_listener.onControlJobCreate != nullptr);
+		int nRet = m_listener.onControlJobCreate(this, controlJob);
+		bCreateOk = nRet == 0;
+
+		// 娣诲姞鏂板缓绫荤殑鍚勭灞炴�у埌鍥炲鎶ユ枃涓�
+		if(bCreateOk) {
+			{
+				ISECS2Item* pReplyItemAttr = pReplyItemAttrs->addItem();
+				pReplyItemAttr->addItem(CJ_ATTR_PRIORITY, "ATTRID");
+				pReplyItemAttr->addU1Item(controlJob.priority(), "ATTRDATA");
+			}
+
+			{
+				ISECS2Item* pReplyItemAttr = pReplyItemAttrs->addItem();
+				pReplyItemAttr->addItem(CJ_ATTR_PRJOBLIST, "ATTRID");
+				ISECS2Item* pItemPjs = pReplyItemAttr->addItem();
+				auto pjIds = controlJob.pjIds();
+				for (auto id : pjIds) {
+					pItemPjs->addItem(id.c_str(), "PRJOBID");
+				}
+			}
+		}
+		else {
+			auto issues = controlJob.issues();
+			for (auto i : issues) {
+				ISECS2Item* pItemError = pReplyItemErrs->addItem();
+				pItemError->addU4Item(i.code, "ERRCODE");
+				pItemError->addItem(i.text.c_str(), "ERRTEXT");
+			}
+		}
+	}
+
+
+	else {
+		ISECS2Item* pItemError = pReplyItemErrs->addItem();
+		pItemError->addU4Item(2001, "ERRCODE");
+		pItemError->addItem("涓嶆敮鎸佺殑OBJ", "ERRTEXT");
+	}
+
+
+	// 瀹屽杽鎶ユ枃骞跺洖澶�
+MYREPLY:
+	pReplyItemAck->setU1(bCreateOk ? 0 : 1, "OBJACK");
+	m_pPassive->sendMessage(pReply);
+	LOGI("<HSMS>[SECS Msg SEND]S14F10 (SysByte=%u)", pReply->getHeader()->systemBytes);
+	HSMS_Destroy1Message(pReply);
+
+
+	return 0;
+}
+
+// S16F15
+int CHsmsPassive::replyPRJobMultiCreate(IMessage* pRecv)
+{
+	if (m_pPassive == NULL || STATE::SELECTED != m_pPassive->getState()) {
+		return ER_NOTSELECT;
+	}
+	ISECS2Item* pBody = pRecv->getBody();
+	if (pBody == nullptr || pBody->getType() != SITYPE::L) ER_PARAM_ERROR;
+
+
+	// 瑙i噴鏁版嵁锛屽緱鍒癈ProcessJob
+	ISECS2Item* pItemPjs, * pItemPj,* pItemCarriers, * pItemCarrier, *pItemSlots, *pItemRecipes;
+	unsigned int DATAID;
+	const char* pszPrjobid, *pszMF, *pszCarrierId, *pszRecipeName;
+	std::string strCarrierId;
+	unsigned int len;
+	unsigned char slot, PRRECIPEMETHOD;
+	std::vector<unsigned char> slots;
+	std::vector<SERVO::CProcessJob*> pjs;
+
+	if (!pBody->getSubItemU4(0, DATAID)) return ER_PARAM_ERROR;
+	pItemPjs = pBody->getSubItem(1);
+	if (pItemPjs == nullptr) return ER_PARAM_ERROR;
+	for (int i = 0; i < pItemPjs->getSubItemSize(); i++) {
+		pItemPj = pItemPjs->getSubItem(i);
+		if (pItemPj == nullptr) continue;
+		if (!pItemPj->getSubItemString(0, pszPrjobid)) continue;
+		if (!pItemPj->getSubItemBinary(1, pszMF, len)) continue;
+		pItemCarriers = pItemPj->getSubItem(2);
+		if (pItemCarriers == nullptr) continue;
+		pItemRecipes = pItemPj->getSubItem(3);
+		if (pItemRecipes == nullptr) continue;
+		SERVO::CProcessJob* pj = new SERVO::CProcessJob(pszPrjobid);
+		int size = pItemCarriers->getSubItemSize();
+		for (int j = 0; j < size; j++) {
+			pItemCarrier = pItemCarriers->getSubItem(j);
+			strCarrierId.clear();
+			if (pItemCarrier->getSubItemString(0, pszCarrierId)) {
+				strCarrierId = pszCarrierId;
+			}
+
+			slots.clear();
+			pItemSlots = pItemCarrier->getSubItem(1);
+			if (pItemSlots != nullptr) {
+				int size2 = pItemSlots->getSubItemSize();
+				for (int k = 0; k < size2; k++) {
+					if (pItemSlots->getSubItemU1(k, slot)) {
+						slots.push_back(slot);
+					}
+				}
+			}
+			pj->addCarrier(strCarrierId, slots);
+		}
+		if (pItemRecipes->getSubItemU1(0, PRRECIPEMETHOD)
+			&& pItemRecipes->getSubItemString(1, pszRecipeName)) {
+			pj->setRecipe(SERVO::RecipeMethod(PRRECIPEMETHOD), std::string(pszRecipeName));
+		}
+
+		pjs.push_back(pj);
+	}
+
+	ASSERT(m_listener.onPRJobMultiCreate != nullptr);
+	int nRet = m_listener.onPRJobMultiCreate(this, pjs);
+
+
+	// 鍥炲鎶ユ枃
+	IMessage* pMessage = NULL;
+	HSMS_Create1Message(pMessage, m_nSessionId, 16, 16, ++m_nSystemByte);
+	ASSERT(pMessage);
+	ISECS2Item* pItemPrjobIds = pMessage->getBody()->addItem();
+	ISECS2Item* pItemErrors = pMessage->getBody()->addItem();
+	bool bHasError = false;
+	for (auto p : pjs) {
+		if (p->issues().empty()) {
+			pItemPrjobIds->addItem(p->id().c_str(), "PRJOBID");
+		}
+		else {
+			bHasError = true;
+		}
+	}
+	if (bHasError) {
+		pItemErrors->addBoolItem(false, "ACKA");
+		ISECS2Item* pItemErrors2 = pItemErrors->addItem();
+		for (auto p : pjs) {
+			if (!p->issues().empty()) {
+				ISECS2Item* pItemErr = pItemErrors2->addItem();
+				pItemErr->addU4Item(p->issues()[0].code, "ERRCODE");
+				pItemErr->addItem(("<" + p->id() + ">" + p->issues()[0].text).c_str(), "ERRTEXT");
+			}
+		}
+	}
+	m_pPassive->sendMessage(pMessage);
+	LOGI("<HSMS>[SECS Msg SEND]S16F16 (SysByte=%u)", pMessage->getHeader()->systemBytes);
+	HSMS_Destroy1Message(pMessage);
+
+
+	// 閲婃斁鏈夐棶棰�(鏈坊鍔犲埌master)鐨勫唴瀛�
+	for (auto p : pjs) {
+		if(!p->issues().empty()) delete p;
+	}
+	pjs.clear();
+
+	return 0;
+}
+
 // S5F1
 int CHsmsPassive::requestAlarmReport(int ALCD, int ALID, const char* ALTX)
 {
@@ -1504,9 +1837,9 @@
 	pItemList2->addU4Item(pReport->getReportId(), "RPTID");
 	ISECS2Item* pItemList3 = pItemList2->addItem();
 
-	auto values = pReport->getVariables();
-	for (auto item : values) {
-		pItemList3->addItem(item->getValue().c_str(), "V");
+	auto vars = pReport->getVariables();
+	for (auto var : vars) {
+		addVariableValueToItem(pItemList3, var);
 	}
 	pAction->setSendMessage(pMessage);
 	if (m_pPassive == NULL || STATE::SELECTED != m_pPassive->getState()) {
@@ -1541,6 +1874,11 @@
 	return requestEventReportSend("CarrierID_Readed");
 }
 
+int CHsmsPassive::requestEventReportSend_PJ_Queued()
+{
+	return requestEventReportSend("PJ_Queued");
+}
+
 
 
 
diff --git a/SourceCode/Bond/Servo/HsmsPassive.h b/SourceCode/Bond/Servo/HsmsPassive.h
index f542cf7..0f5a636 100644
--- a/SourceCode/Bond/Servo/HsmsPassive.h
+++ b/SourceCode/Bond/Servo/HsmsPassive.h
@@ -7,6 +7,8 @@
 #include <map>
 #include <set>
 #include "CCollectionEvent.h"
+#include "ProcessJob.h"
+#include "CControlJob.h"
 
 
 #define EQCONSTANT_VALUE_MAX	64
@@ -86,6 +88,8 @@
 	const char* pszCarrierId,
 	unsigned char PTN, 
 	std::string& strErrorTxt)> CARRIERACTION;
+typedef std::function<int(void* pFrom, std::vector<SERVO::CProcessJob*>& pjs)> PRJOBMULTICREATE;
+typedef std::function<int(void* pFrom, SERVO::CControlJob& controlJob)> CONTROLJOBCREATE;
 typedef struct _SECSListener
 {
 	SECSEQOFFLINE				onEQOffLine;
@@ -98,6 +102,8 @@
 	EDALARMREPORT				onEnableDisableAlarmReport;
 	QUERYPPIDLIST				onQueryPPIDList;
 	CARRIERACTION				onCarrierAction;
+	PRJOBMULTICREATE			onPRJobMultiCreate;
+	CONTROLJOBCREATE			onControlJobCreate;
 } SECSListener;
 
 
@@ -114,6 +120,9 @@
 
 	/* 设置软件版本号 最大长度 20 bytes */
 	void setSoftRev(const char* pszRev);
+
+	/* 添加变量值到ISECS2Item */
+	void addVariableValueToItem(ISECS2Item* pParent, SERVO::CVariable* pVariable);
 
 	// 连接Report
 	void linkEventReport(unsigned int CEID, unsigned int RPTID);
@@ -141,6 +150,7 @@
 	// 设置变量值
 	void setVariableValue(const char* pszName, __int64 value);
 	void setVariableValue(const char* pszName, const char* value);
+	void setVariableValue(const char* pszName, std::vector<SERVO::CVariable>& vars);
 
 	// 从文件中加载CReport列表
 	int loadReports(const char* pszFilepath);
@@ -181,6 +191,7 @@
 	int requestEventReportSend(unsigned int CEID);
 	int requestEventReportSend(const char* pszEventName);
 	int requestEventReportSend_CarrierID_Readed();
+	int requestEventReportSend_PJ_Queued();
 
 private:
 	void replyAck(int s, int f, unsigned int systemBytes, BYTE ack, const char* pszAckName);
@@ -188,6 +199,7 @@
 	/* reply开头的函数为回复函数 */
 	int replyAreYouThere(IMessage* pRecv);
 	int replyEstablishCommunications(IMessage* pRecv);
+	int replySelectedEquipmentStatusData(IMessage* pRecv);
 	int replyOnLine(IMessage* pRecv);
 	int replyOffLine(IMessage* pRecv);
 	int replyEquipmentConstantRequest(IMessage* pRecv);
@@ -203,6 +215,8 @@
 	int replyPurgeSpooledData(IMessage* pRecv);
 	int replyQueryPPIDList(IMessage* pRecv);
 	int replyTerminalDisplay(IMessage* pRecv);
+	int replyCreateObj(IMessage* pRecv);
+	int replyPRJobMultiCreate(IMessage* pRecv);
 
 private:
 	inline void Lock() { EnterCriticalSection(&m_criticalSection); }
diff --git a/SourceCode/Bond/Servo/Model.cpp b/SourceCode/Bond/Servo/Model.cpp
index a6299e5..8b854ee 100644
--- a/SourceCode/Bond/Servo/Model.cpp
+++ b/SourceCode/Bond/Servo/Model.cpp
@@ -88,7 +88,7 @@
 	::CreateDirectory(strLogDir, NULL);
 	CLog::GetLog()->SetOnLogCallback([&](int level, const char* pszMessage) -> void {
 		notifyTextAndInt(RX_CODE_LOG, pszMessage, level);
-	});
+		});
 	CLog::GetLog()->SetAutoAppendTimeString(TRUE);
 	CLog::GetLog()->SetOutputTarget(OT_FILE);
 	CLog::GetLog()->SetLogsDir(strLogDir);
@@ -112,7 +112,7 @@
 	listener.onEQConstantRequest = [&](void* pFrom, std::vector<EQConstant>& eqcs) -> void {
 		// 在此填充常量值,目前仅是加1后返回
 		for (auto& item : eqcs) {
-			sprintf_s(item.szValue, 256, "Test%d", item.id+1);
+			sprintf_s(item.szValue, 256, "Test%d", item.id + 1);
 		}
 	};
 	listener.onEQConstantSend = [&](void* pFrom, std::vector<EQConstant>& eqcs) -> void {
@@ -151,8 +151,8 @@
 		}
 		return ppids;
 	};
-	listener.onCarrierAction = [&](void* pFrom, 
-		unsigned int DATAID, 
+	listener.onCarrierAction = [&](void* pFrom,
+		unsigned int DATAID,
 		const char* pszCarrierAction,
 		const char* pszCarrierId,
 		unsigned char PTN,
@@ -174,6 +174,28 @@
 			strErrorTxt = "rejected - invalid state";
 			return CAACK_5;
 			LOGI("<Model>onCarrierAction %d, %s, %d, %d", DATAID, pszCarrierAction, pszCarrierId, PTN);
+	};
+	listener.onPRJobMultiCreate = [&](void* pFrom, std::vector<SERVO::CProcessJob*>& pjs) -> int {
+		for (auto p : pjs) {
+			LOGI("<Model>onPRJobMultiCreate %s %s", p->id().c_str(), p->recipeSpec().c_str());
+		}
+		int nRet = m_master.setProcessJobs(pjs);
+		auto processJobs = m_master.getProcessJobs();
+		std::vector<SERVO::CVariable> vars;
+		for (auto pj : processJobs) {
+			SERVO::CVariable var("", "PRJOBID", "A50", "PRJOBID");
+			var.setValue(pj->id().c_str());
+			vars.push_back(var);
+		}
+
+		m_hsmsPassive.setVariableValue("PJQueued", vars);
+		m_hsmsPassive.requestEventReportSend_PJ_Queued();
+		return nRet;
+	};
+	listener.onControlJobCreate = [&](void* pFrom, SERVO::CControlJob& controlJob) -> int {
+		LOGI("<Model>onControlJobCreate %s %d", controlJob.id().c_str(), controlJob.priority());
+		int nRet = m_master.setControlJob(controlJob);
+		return nRet;
 	};
 	m_hsmsPassive.setListener(listener);
 	m_hsmsPassive.setEquipmentModelType((LPTSTR)(LPCTSTR)strModeType);
@@ -375,6 +397,14 @@
 	m_master.setCacheFilepath((LPTSTR)(LPCTSTR)strMasterDataFile);
 	m_master.setCompareMapsBeforeProceeding(m_configuration.isCompareMapsBeforeProceeding());
 
+	// 加截Job
+	strMasterDataFile.Format(_T("%s\\MasterState.dat"), (LPTSTR)(LPCTSTR)m_strWorkDir);
+	std::string strPath = std::string((LPTSTR)(LPCTSTR)strMasterDataFile);
+	if (!m_master.loadState(strPath)) {
+		LOGE("<Master>加载MasterState.dat文件失败.");
+	}
+
+
 	// 加载警告信息
 	AlarmManager& alarmManager = AlarmManager::getInstance();
 	char szBuffer[MAX_PATH];
diff --git a/SourceCode/Bond/Servo/ProcessJob.cpp b/SourceCode/Bond/Servo/ProcessJob.cpp
new file mode 100644
index 0000000..b672e27
--- /dev/null
+++ b/SourceCode/Bond/Servo/ProcessJob.cpp
@@ -0,0 +1,433 @@
+#include "stdafx.h"
+#include "ProcessJob.h"
+#include <cctype>
+#include <fstream>
+#include "SerializeUtil.h"
+
+
+namespace SERVO {
+    static inline std::string trimCopy(std::string s) {
+        auto notspace = [](int ch) { return !std::isspace(ch); };
+        s.erase(s.begin(), std::find_if(s.begin(), s.end(), notspace));
+        s.erase(std::find_if(s.rbegin(), s.rend(), notspace).base(), s.end());
+        return s;
+    }
+
+    CProcessJob::CProcessJob()
+    {
+
+    }
+
+    CProcessJob::CProcessJob(std::string pjId)
+        : m_pjId(trimCopy(pjId))
+    {
+        clampString(m_pjId, MAX_ID_LEN);
+    }
+
+    void CProcessJob::setParentCjId(std::string cjId) {
+        m_parentCjId = trimCopy(cjId);
+        clampString(m_parentCjId, MAX_ID_LEN);
+    }
+
+    void CProcessJob::setRecipe(RecipeMethod method, std::string spec) {
+        m_recipeMethod = method;
+        m_recipeSpec = trimCopy(spec);
+        clampString(m_recipeSpec, MAX_ID_LEN);
+    }
+
+    void CProcessJob::addParam(std::string name, std::string value) {
+        name = trimCopy(name);
+        value = trimCopy(value);
+        clampString(name, MAX_PARAM_K);
+        clampString(value, MAX_PARAM_V);
+        m_params.push_back({ std::move(name), std::move(value) });
+    }
+
+    void CProcessJob::setParams(std::vector<PJParam> params) {
+        m_params.clear();
+        m_params.reserve(params.size());
+        for (auto& p : params) addParam(std::move(p.name), std::move(p.value));
+    }
+
+    void CProcessJob::addPauseEvent(uint32_t ceid) {
+        if (ceid) m_pauseEvents.push_back(ceid);
+        std::sort(m_pauseEvents.begin(), m_pauseEvents.end());
+        m_pauseEvents.erase(std::unique(m_pauseEvents.begin(), m_pauseEvents.end()), m_pauseEvents.end());
+    }
+
+    void CProcessJob::setPauseEvents(std::vector<uint32_t> ceids) {
+        m_pauseEvents = std::move(ceids);
+        std::sort(m_pauseEvents.begin(), m_pauseEvents.end());
+        m_pauseEvents.erase(std::unique(m_pauseEvents.begin(), m_pauseEvents.end()), m_pauseEvents.end());
+    }
+
+    const std::vector<CProcessJob::ValidationIssue>& CProcessJob::issues()
+    {
+        return m_issues;
+    }
+
+    bool CProcessJob::validate(const IResourceView& rv)
+    {
+        m_issues.clear();
+
+        // 让 add 同时支持 const char* 和 std::string
+        auto add = [&](uint32_t code, std::string msg) {
+            m_issues.push_back({ code, std::move(msg) });
+        };
+
+        if (!rv.isProcessJobsEmpty()) {
+            add(1000, "ProcessJobs Conflict!");
+        }
+
+        // —— 基本 / 标识 ——
+        if (m_pjId.empty())            add(1001, "PJID empty");
+        if (!asciiPrintable(m_pjId))   add(1002, "PJID has non-printable chars");
+
+        // if (m_parentCjId.empty())      add(1010, "Parent CJID empty");
+
+        // —— 配方(RCPSPEC / PPID)——
+        if (m_recipeSpec.empty())      add(1020, "Recipe spec (PPID) empty");
+        else if (!rv.recipeExists(m_recipeSpec)) {
+            add(1021, "PPID not found: " + m_recipeSpec);
+        }
+
+        // —— 配方方法 vs 参数 —— 1=NoTuning 禁止带参数;2=WithTuning 允许/可选
+        if (m_recipeMethod == RecipeMethod::NoTuning && !m_params.empty()) {
+            add(1022, "Params not allowed when PRRECIPEMETHOD=1 (NoTuning)");
+        }
+
+        // —— 物料选择校验 ——(二选一:Carrier+Slots 或 MIDs;两者都不填则错误)
+        const bool hasCarrierSlots = !m_carriers.empty();
+        if (hasCarrierSlots) {
+            // {L:n { CARRIERID {L:j SLOTID} }}
+            for (const auto& cs : m_carriers) {
+                if (cs.carrierId.empty()) {
+                    add(1030, "CarrierID empty");
+                    continue;
+                }
+                if (!rv.carrierPresent(cs.carrierId)) {
+                    add(1031, "Carrier not present: " + cs.carrierId);
+                }
+                if (cs.slots.empty()) {
+                    add(1032, "No slots specified for carrier: " + cs.carrierId);
+                }
+                for (auto s : cs.slots) {
+                    if (s == 0) {
+                        add(1033, "Slot 0 is invalid for carrier: " + cs.carrierId);
+                        continue;
+                    }
+                    if (!rv.slotUsable(cs.carrierId, s)) {
+                        add(1034, "Slot unusable: carrier=" + cs.carrierId + " slot=" + std::to_string(s));
+                    }
+                }
+            }
+        }
+        else {
+            add(1035, "No material selection provided (neither Carrier/Slots nor MIDs)");
+        }
+
+        // —— 暂停事件(PRPAUSEEVENTID 列表)——
+        for (auto ceid : m_pauseEvents) {
+            if (!rv.ceidDefined(ceid)) {
+                add(1050, "Pause CEID unknown: " + std::to_string(ceid));
+            }
+        }
+
+        return m_issues.empty();
+    }
+
+    // —— 状态机 ——
+    // 规则可按你们协议微调
+    bool CProcessJob::queue() {
+        if (m_state != PJState::NoState) return false;
+        markQueued();
+        return true;
+    }
+
+    bool CProcessJob::enterSettingUp() {
+        if (m_state != PJState::Queued) return false;
+        m_state = PJState::SettingUp;
+        return true;
+    }
+
+    bool CProcessJob::start() {
+        if (m_state != PJState::Queued && m_state != PJState::SettingUp && m_state != PJState::Paused)
+            return false;
+        if (!m_tStart.has_value()) markStart();
+        m_state = PJState::InProcess;
+        return true;
+    }
+
+    bool CProcessJob::pause() {
+        if (m_state != PJState::InProcess) return false;
+        m_state = PJState::Paused;
+        return true;
+    }
+
+    bool CProcessJob::resume() {
+        if (m_state != PJState::Paused) return false;
+        m_state = PJState::InProcess;
+        return true;
+    }
+
+    bool CProcessJob::complete() {
+        if (m_state != PJState::InProcess && m_state != PJState::Paused) return false;
+        m_state = PJState::Completed;
+        markEnd();
+        return true;
+    }
+
+    bool CProcessJob::abort() {
+        if (m_state == PJState::Completed || m_state == PJState::Aborted || m_state == PJState::Failed)
+            return false;
+        m_state = PJState::Aborted;
+        markEnd();
+        return true;
+    }
+
+    bool CProcessJob::fail(std::string reason) {
+        m_failReason = trimCopy(reason);
+        clampString(m_failReason, 128);
+        m_state = PJState::Failed;
+        markEnd();
+        return true;
+    }
+
+    // —— 时间戳 & 工具 —— 
+    void CProcessJob::markQueued() {
+        m_state = PJState::Queued;
+        m_tQueued = std::chrono::system_clock::now();
+    }
+
+    void CProcessJob::markStart() {
+        m_tStart = std::chrono::system_clock::now();
+    }
+
+    void CProcessJob::markEnd() {
+        m_tEnd = std::chrono::system_clock::now();
+    }
+
+    void CProcessJob::clampString(std::string& s, size_t maxLen) {
+        if (s.size() > maxLen) s.resize(maxLen);
+    }
+
+    bool CProcessJob::asciiPrintable(const std::string& s) {
+        return std::all_of(s.begin(), s.end(), [](unsigned char c) {
+            return c >= 0x20 && c <= 0x7E;
+            });
+    }
+
+    void CProcessJob::setCarriers(std::vector<CarrierSlotInfo> carriers)
+    {
+        // 统一通过 addCarrier 做规范化(去空白、截断、去重、合并同 carrier)
+        m_carriers.clear();
+        m_carriers.reserve(carriers.size());
+        for (auto& cs : carriers) {
+            addCarrier(std::move(cs.carrierId), std::move(cs.slots));
+        }
+    }
+
+    void CProcessJob::addCarrier(std::string carrierId, std::vector<uint8_t> slots)
+    {
+        // 1) 规范化 carrierId:去空白 + 长度限制
+        carrierId = trimCopy(std::move(carrierId));
+        clampString(carrierId, MAX_ID_LEN);
+        if (carrierId.empty()) {
+            // 空 ID 直接忽略(也可以选择抛异常/记录日志,看你项目风格)
+            return;
+        }
+
+        // 2) 规范化 slots:去 0、排序、去重
+        //    注:SLOTID 按 1..N,0 视为非法/占位
+        slots.erase(std::remove(slots.begin(), slots.end(), 0), slots.end());
+        std::sort(slots.begin(), slots.end());
+        slots.erase(std::unique(slots.begin(), slots.end()), slots.end());
+        if (slots.empty()) {
+            // 没有有效卡位就不追加
+            return;
+        }
+
+        // 3) 如果已存在同名载具,则合并 slot 列表
+        auto it = std::find_if(m_carriers.begin(), m_carriers.end(),
+            [&](const CarrierSlotInfo& cs) { return cs.carrierId == carrierId; });
+
+        if (it != m_carriers.end()) {
+            // 合并
+            it->slots.insert(it->slots.end(), slots.begin(), slots.end());
+            std::sort(it->slots.begin(), it->slots.end());
+            it->slots.erase(std::unique(it->slots.begin(), it->slots.end()), it->slots.end());
+        }
+        else {
+            // 新增
+            CarrierSlotInfo cs;
+            cs.carrierId = std::move(carrierId);
+            cs.slots = std::move(slots);
+            m_carriers.emplace_back(std::move(cs));
+        }
+    }
+
+    // --------- 核心:serialize/deserialize ---------
+    void CProcessJob::serialize(std::ostream& os) const {
+        // 头
+        write_pod(os, PJ_FILE_MAGIC);
+        write_pod(os, PJ_FILE_VERSION);
+
+        // 基本
+        write_string(os, m_pjId);
+        write_string(os, m_parentCjId);
+
+        // 配方
+        uint8_t recipeType = static_cast<uint8_t>(m_recipeMethod);
+        write_pod(os, m_recipeMethod);
+        write_string(os, m_recipeSpec);
+
+        // 物料(多 Carrier & Slot)
+        {
+            uint32_t n = static_cast<uint32_t>(m_carriers.size());
+            write_pod(os, n);
+            for (const auto& cs : m_carriers) {
+                write_string(os, cs.carrierId);
+                write_vec<uint8_t>(os, cs.slots);
+            }
+        }
+
+        // 参数
+        {
+            uint32_t n = static_cast<uint32_t>(m_params.size());
+            write_pod(os, n);
+            for (const auto& p : m_params) {
+                write_string(os, p.name);
+                write_string(os, p.value);
+            }
+        }
+
+        // 暂停事件
+        write_vec<uint32_t>(os, m_pauseEvents);
+
+        // 启动策略 & 状态
+        uint8_t startPolicy = static_cast<uint8_t>(m_startPolicy);
+        uint8_t st = static_cast<uint8_t>(m_state);
+        write_pod(os, startPolicy);
+        write_pod(os, st);
+
+        // 失败原因
+        write_string(os, m_failReason);
+
+        // 时间戳
+        write_opt_time(os, m_tQueued);
+        write_opt_time(os, m_tStart);
+        write_opt_time(os, m_tEnd);
+    }
+
+    bool CProcessJob::deserialize(std::istream& is, CProcessJob& out, std::string* err) {
+        auto fail = [&](const char* msg) { if (err) *err = msg; return false; };
+
+        uint32_t magic = 0; if (!read_pod(is, magic)) return fail("read magic failed");
+        if (magic != PJ_FILE_MAGIC) return fail("bad magic");
+
+        uint16_t ver = 0; if (!read_pod(is, ver)) return fail("read version failed");
+        if (ver != PJ_FILE_VERSION) return fail("unsupported version");
+
+        // 基本
+        if (!read_string(is, out.m_pjId))        return fail("read pjId");
+        if (!read_string(is, out.m_parentCjId))  return fail("read parentCjId");
+
+        // 配方
+        uint8_t recipeType = 0; if (!read_pod(is, recipeType)) return fail("read recipeType");
+        out.m_recipeMethod = static_cast<RecipeMethod>(recipeType);
+        if (!read_string(is, out.m_recipeSpec)) return fail("read recipeSpec");
+
+        // 物料
+        {
+            uint32_t n = 0; if (!read_pod(is, n)) return fail("read carriers count");
+            out.m_carriers.clear(); out.m_carriers.reserve(n);
+            for (uint32_t i = 0; i < n; ++i) {
+                CarrierSlotInfo cs;
+                if (!read_string(is, cs.carrierId)) return fail("read carrierId");
+                if (!read_vec<uint8_t>(is, cs.slots)) return fail("read slots");
+                out.m_carriers.emplace_back(std::move(cs));
+            }
+        }
+
+        // 参数
+        {
+            uint32_t n = 0; if (!read_pod(is, n)) return fail("read params count");
+            out.m_params.clear(); out.m_params.reserve(n);
+            for (uint32_t i = 0; i < n; ++i) {
+                PJParam p;
+                if (!read_string(is, p.name))  return fail("read param name");
+                if (!read_string(is, p.value)) return fail("read param value");
+                out.m_params.emplace_back(std::move(p));
+            }
+        }
+
+        // 暂停事件
+        if (!read_vec<uint32_t>(is, out.m_pauseEvents)) return fail("read pauseEvents");
+
+        // 启动策略 & 状态
+        uint8_t startPolicy = 0, st = 0;
+        if (!read_pod(is, startPolicy)) return fail("read startPolicy");
+        if (!read_pod(is, st))          return fail("read state");
+        out.m_startPolicy = static_cast<StartPolicy>(startPolicy);
+        out.m_state = static_cast<PJState>(st);
+
+        // 失败原因
+        if (!read_string(is, out.m_failReason)) return fail("read failReason");
+
+        // 时间戳
+        if (!read_opt_time(is, out.m_tQueued)) return fail("read tQueued");
+        if (!read_opt_time(is, out.m_tStart))  return fail("read tStart");
+        if (!read_opt_time(is, out.m_tEnd))    return fail("read tEnd");
+
+        return true;
+    }
+
+    std::string CProcessJob::getStateText()
+    {
+        switch (m_state)
+        {
+        case SERVO::PJState::NoState:
+            return "NoState";
+            break;
+        case SERVO::PJState::Queued:
+            return "Queued";
+            break;
+        case SERVO::PJState::SettingUp:
+            return "SettingUp";
+            break;
+        case SERVO::PJState::InProcess:
+            return "InProcess";
+            break;
+        case SERVO::PJState::Paused:
+            return "Queued";
+            break;
+        case SERVO::PJState::Aborting:
+            return "Aborting";
+            break;
+        case SERVO::PJState::Completed:
+            return "Queued";
+            break;
+        case SERVO::PJState::Aborted:
+            return "Aborted";
+            break;
+        case SERVO::PJState::Failed:
+            return "Failed";
+            break;
+        default:
+            break;
+        }
+
+        return "";
+    }
+
+    CarrierSlotInfo* CProcessJob::getCarrier(std::string& strId)
+    {
+        for (auto& item : m_carriers) {
+            if (item.carrierId.compare(strId) == 0) {
+                return &item;
+            }
+        }
+
+        return nullptr;
+    }
+}
diff --git a/SourceCode/Bond/Servo/ProcessJob.h b/SourceCode/Bond/Servo/ProcessJob.h
new file mode 100644
index 0000000..1300b46
--- /dev/null
+++ b/SourceCode/Bond/Servo/ProcessJob.h
@@ -0,0 +1,216 @@
+#pragma once
+#include <string>
+#include <vector>
+#include <unordered_map>
+#include <unordered_set>
+#include <algorithm>
+#include <cstdint>
+#include <chrono>
+#include <optional>
+
+namespace SERVO {
+    /// PJ 生命周期(贴近 E40 常见状态)
+    enum class PJState : uint8_t {
+        NoState = 0,
+        Queued,
+        SettingUp,
+        InProcess,
+        Paused,
+        Aborting,
+        Completed,
+        Aborted,
+        Failed
+    };
+
+    /// 配方指定方式(对应 S16F15 里 PRRECIPEMETHOD)
+    enum class RecipeMethod : uint8_t {
+        NoTuning = 1,   // 1 - recipe without variable tuning
+        WithTuning = 2  // 2 - recipe with variable tuning
+    };
+
+    /// 启动策略(对应 S16F15 里 PRPROCESSSTART)
+    enum class StartPolicy : uint8_t {
+        Queued = 0,   // 建立后排队
+        AutoStart = 1 // 条件满足则自动启动
+    };
+
+    /** 配方参数对(S16F15 中 RCPPARNM / RCPPARVAL) */
+    struct PJParam {
+        std::string name;   // RCPPARNM
+        std::string value;  // RCPPARVAL
+    };
+
+    /**
+    {L:2
+        CARRIERID
+        {L:j
+            SLOTID
+        }
+    }
+     */
+    struct CarrierSlotInfo {
+        std::string carrierId;          // CARRIERID
+        std::vector<uint8_t> slots;     // SLOTID[]
+        std::vector<void*> contexts;    // Glass
+    };
+
+    /// 简单资源视图接口:供 Validate() 查询(由设备端实现者在外部提供)
+    struct IResourceView {
+        virtual ~IResourceView() = default;
+        virtual bool isProcessJobsEmpty() const = 0;
+        virtual bool recipeExists(const std::string& ppid) const = 0;
+        virtual bool carrierPresent(const std::string& carrierId) const = 0;
+        virtual bool slotUsable(const std::string& carrierId, uint16_t slot) const = 0;
+        virtual bool ceidDefined(uint32_t ceid) const = 0;
+        // 你也可以扩展:port状态、占用情况、CJ/PJ空间等
+    };
+
+    /// PJ 主类
+    /**
+     * ProcessJob —— 与 S16F15(PRJobMultiCreate)字段一一对应的承载类
+     *
+     * S16F15 结构(核心节选):
+     * {L:6
+     *   PRJOBID                -> m_pjId
+     *   MF                     -> m_mf
+     *   {L:n { CARRIERID {L:j SLOTID} } } 
+     *   {L:3
+     *     PRRECIPEMETHOD       -> m_recipeType
+     *     RCPSPEC(PPID)      -> m_recipeSpec
+     *     {L:m { RCPPARNM RCPPARVAL }}     -> m_params
+     *   }
+     *   PRPROCESSSTART         -> m_startPolicy
+     *   {L:k PRPAUSEEVENTID}   -> m_pauseEvents
+     * }
+     */
+    class CProcessJob {
+    public:
+        // —— 构造 / 基本设置 ——
+        CProcessJob();
+        explicit CProcessJob(std::string pjId);
+
+        const std::string& id() const noexcept { return m_pjId; }
+        const std::string& parentCjId() const noexcept { return m_parentCjId; }
+        PJState state() const noexcept { return m_state; }
+        StartPolicy startPolicy() const noexcept { return m_startPolicy; }
+        RecipeMethod recipeMethod() const noexcept { return m_recipeMethod; }
+        const std::string& recipeSpec() const noexcept { return m_recipeSpec; } // PPID 或 Spec
+        std::string getStateText();
+
+        // 绑定父 CJ
+        void setParentCjId(std::string cjId);
+
+        // 配方
+        void setRecipe(RecipeMethod method, std::string spec);
+
+        // 启动策略
+        void setStartPolicy(StartPolicy sp) { m_startPolicy = sp; }
+
+        // 参数
+        void addParam(std::string name, std::string value);
+        void setParams(std::vector<PJParam> params);
+
+        // 暂停事件
+        void addPauseEvent(uint32_t ceid);
+        void setPauseEvents(std::vector<uint32_t> ceids);
+
+        // —— 校验 ——
+        struct ValidationIssue {
+            uint32_t code;      // 自定义错误码
+            std::string text;   // 文本描述
+        };
+        // 返回问题清单(空=通过)
+        bool validate(const IResourceView& rv);
+        const std::vector<ValidationIssue>& issues();
+
+        // —— 状态机(带守卫)——
+        bool queue();           // NoState -> Queued
+        bool start();           // Queued/SettingUp -> InProcess
+        bool enterSettingUp();  // Queued -> SettingUp
+        bool pause();           // InProcess -> Paused
+        bool resume();          // Paused -> InProcess
+        bool complete();        // InProcess -> Completed
+        bool abort();           // Any (未终态) -> Aborted
+        bool fail(std::string reason); // 任意态 -> Failed(记录失败原因)
+
+        // —— 访问器(用于上报/查询)——
+        const std::vector<PJParam>& params() const noexcept { return m_params; }
+        const std::vector<uint32_t>& pauseEvents() const noexcept { return m_pauseEvents; }
+        const std::string& failReason() const noexcept { return m_failReason; }
+
+        // 时间戳(可用于报表/追溯)
+        std::optional<std::chrono::system_clock::time_point> tQueued() const { return m_tQueued; }
+        std::optional<std::chrono::system_clock::time_point> tStart()  const { return m_tStart; }
+        std::optional<std::chrono::system_clock::time_point> tEnd()    const { return m_tEnd; }
+
+        // 长度限制工具(可在集成时统一策略)
+        static void clampString(std::string& s, size_t maxLen);
+        static bool asciiPrintable(const std::string& s);
+
+        // 清空并整体设置
+        void setCarriers(std::vector<CarrierSlotInfo> carriers);
+
+        // 追加一个载具
+        void addCarrier(std::string carrierId, std::vector<uint8_t> slots);
+
+        // 访问器
+        const std::vector<CarrierSlotInfo>& carriers() const noexcept { return m_carriers; }
+        CarrierSlotInfo* getCarrier(std::string& strId);
+
+        // 判定是否“按载具/卡位”方式
+        bool usesCarrierSlots() const noexcept { return !m_carriers.empty(); }
+
+
+    public:
+        // ====== 版本头常量(建议保留,便于兼容)======
+        static constexpr uint32_t PJ_FILE_MAGIC = 0x504A4A31; // "PJJ1"
+        static constexpr uint16_t PJ_FILE_VERSION = 0x0001;
+
+        // ====== 流式序列化接口 ======
+        void serialize(std::ostream& os) const;
+        static bool deserialize(std::istream& is, CProcessJob& out, std::string* err = nullptr);
+
+    private:
+        // 内部状态转移帮助
+        void markQueued();
+        void markStart();
+        void markEnd();
+
+    private:
+        // 标识
+        std::string m_pjId;
+        std::string m_parentCjId;
+
+        // 配方
+        RecipeMethod m_recipeMethod{ RecipeMethod::NoTuning };
+        std::string  m_recipeSpec; // PPID / Spec
+
+        // 物料
+        static constexpr uint8_t MATERIAL_FORMAT = 14; // substrate
+        std::vector<CarrierSlotInfo> m_carriers;   // {L:n { CARRIERID {L:j SLOTID} }}
+
+        // 参数 / 暂停事件
+        std::vector<PJParam>    m_params;
+        std::vector<uint32_t>   m_pauseEvents;
+
+        // 状态 & 记录
+        StartPolicy m_startPolicy{ StartPolicy::Queued }; // 0=Queued, 1=AutoStart
+        PJState m_state{ PJState::NoState };
+        std::string m_failReason;
+
+        // 时间戳
+        std::optional<std::chrono::system_clock::time_point> m_tQueued;
+        std::optional<std::chrono::system_clock::time_point> m_tStart;
+        std::optional<std::chrono::system_clock::time_point> m_tEnd;
+
+        // 约束(可按你们协议调整)
+        static constexpr size_t MAX_ID_LEN = 64;   // PJID/ CJID/ CarrierID/ MID/ PPID
+        static constexpr size_t MAX_PARAM_K = 32;   // 参数名
+        static constexpr size_t MAX_PARAM_V = 64;   // 参数值
+
+        // 错误列表
+        std::vector<ValidationIssue> m_issues;
+    };
+}
+
+
diff --git a/SourceCode/Bond/Servo/SerializeUtil.h b/SourceCode/Bond/Servo/SerializeUtil.h
new file mode 100644
index 0000000..fe5fdf7
--- /dev/null
+++ b/SourceCode/Bond/Servo/SerializeUtil.h
@@ -0,0 +1,72 @@
+#pragma once
+#include <fstream>
+
+
+// --------- 私有/内部:读写工具 ---------
+namespace SERVO {
+    template<typename T>
+    inline void write_pod(std::ostream& os, const T& v) {
+        os.write(reinterpret_cast<const char*>(&v), sizeof(T));
+    }
+    template<typename T>
+    inline bool read_pod(std::istream& is, T& v) {
+        return bool(is.read(reinterpret_cast<char*>(&v), sizeof(T)));
+    }
+
+    inline void write_string(std::ostream& os, const std::string& s) {
+        uint32_t n = static_cast<uint32_t>(s.size());
+        write_pod(os, n);
+        if (n) os.write(s.data(), n);
+    }
+    inline bool read_string(std::istream& is, std::string& s) {
+        uint32_t n = 0; if (!read_pod(is, n)) return false;
+        s.resize(n);
+        if (n) return bool(is.read(&s[0], n));
+        return true;
+    }
+
+    template<typename T>
+    inline void write_vec(std::ostream& os, const std::vector<T>& v) {
+        uint32_t n = static_cast<uint32_t>(v.size());
+        write_pod(os, n);
+        if (n) os.write(reinterpret_cast<const char*>(v.data()), sizeof(T) * n);
+    }
+    template<typename T>
+    inline bool read_vec(std::istream& is, std::vector<T>& v) {
+        uint32_t n = 0; if (!read_pod(is, n)) return false;
+        v.resize(n);
+        if (n) return bool(is.read(reinterpret_cast<char*>(v.data()), sizeof(T) * n));
+        return true;
+    }
+
+    // vector<string> 特化写读
+    inline void write_vec_str(std::ostream& os, const std::vector<std::string>& v) {
+        uint32_t n = static_cast<uint32_t>(v.size());
+        write_pod(os, n);
+        for (const auto& s : v) write_string(os, s);
+    }
+    inline bool read_vec_str(std::istream& is, std::vector<std::string>& v) {
+        uint32_t n = 0; if (!read_pod(is, n)) return false;
+        v.clear(); v.reserve(n);
+        for (uint32_t i = 0; i < n; ++i) { std::string s; if (!read_string(is, s)) return false; v.emplace_back(std::move(s)); }
+        return true;
+    }
+
+    // optional<time_point> → bool + int64 (ms since epoch)
+    inline void write_opt_time(std::ostream& os, const std::optional<std::chrono::system_clock::time_point>& tp) {
+        uint8_t has = tp.has_value() ? 1 : 0;
+        write_pod(os, has);
+        if (has) {
+            auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(tp->time_since_epoch()).count();
+            int64_t v = static_cast<int64_t>(ms);
+            write_pod(os, v);
+        }
+    }
+    inline bool read_opt_time(std::istream& is, std::optional<std::chrono::system_clock::time_point>& tp) {
+        uint8_t has = 0; if (!read_pod(is, has)) return false;
+        if (!has) { tp.reset(); return true; }
+        int64_t v = 0; if (!read_pod(is, v)) return false;
+        tp = std::chrono::system_clock::time_point(std::chrono::milliseconds(v));
+        return true;
+    }
+}
diff --git a/SourceCode/Bond/Servo/Servo.rc b/SourceCode/Bond/Servo/Servo.rc
index c1df135..ac1bebb 100644
--- a/SourceCode/Bond/Servo/Servo.rc
+++ b/SourceCode/Bond/Servo/Servo.rc
Binary files differ
diff --git a/SourceCode/Bond/Servo/Servo.vcxproj b/SourceCode/Bond/Servo/Servo.vcxproj
index 008f416..ec95643 100644
--- a/SourceCode/Bond/Servo/Servo.vcxproj
+++ b/SourceCode/Bond/Servo/Servo.vcxproj
@@ -117,6 +117,7 @@
       <PreprocessorDefinitions>_WINDOWS;_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
       <SDLCheck>true</SDLCheck>
       <AdditionalIncludeDirectories>.;..;..\DatabaseSDK\include;..\MELSECSDK\include;.\CCLinkPerformance;.\GridControl;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
+      <LanguageStandard>stdcpp17</LanguageStandard>
     </ClCompile>
     <Link>
       <SubSystem>Windows</SubSystem>
@@ -200,9 +201,12 @@
   </ItemGroup>
   <ItemGroup>
     <ClInclude Include="CBaseDlg.h" />
+    <ClInclude Include="CControlJob.h" />
+    <ClInclude Include="CControlJobDlg.h" />
     <ClInclude Include="CCustomCheckBox.h" />
     <ClInclude Include="CCollectionEvent.h" />
     <ClInclude Include="CEquipmentPage3.h" />
+    <ClInclude Include="CExpandableListCtrl.h" />
     <ClInclude Include="CGlassPool.h" />
     <ClInclude Include="ChangePasswordDlg.h" />
     <ClInclude Include="CMyStatusbar.h" />
@@ -324,12 +328,14 @@
     <ClInclude Include="PageRobotCmd.h" />
     <ClInclude Include="PageTransferLog.h" />
     <ClInclude Include="PortConfigurationDlg.h" />
+    <ClInclude Include="ProcessJob.h" />
     <ClInclude Include="ProductionLogManager.h" />
     <ClInclude Include="RecipeDeviceBindDlg.h" />
     <ClInclude Include="RecipeManager.h" />
     <ClInclude Include="Resource.h" />
     <ClInclude Include="SECSRuntimeManager.h" />
     <ClInclude Include="SecsTestDlg.h" />
+    <ClInclude Include="SerializeUtil.h" />
     <ClInclude Include="Servo.h" />
     <ClInclude Include="ServoCommo.h" />
     <ClInclude Include="ServoDlg.h" />
@@ -349,9 +355,12 @@
   </ItemGroup>
   <ItemGroup>
     <ClCompile Include="CBaseDlg.cpp" />
+    <ClCompile Include="CControlJob.cpp" />
+    <ClCompile Include="CControlJobDlg.cpp" />
     <ClCompile Include="CCustomCheckBox.cpp" />
     <ClCompile Include="CCollectionEvent.cpp" />
     <ClCompile Include="CEquipmentPage3.cpp" />
+    <ClCompile Include="CExpandableListCtrl.cpp" />
     <ClCompile Include="CGlassPool.cpp" />
     <ClCompile Include="ChangePasswordDlg.cpp" />
     <ClCompile Include="CMyStatusbar.cpp" />
@@ -470,6 +479,7 @@
     <ClCompile Include="PageRobotCmd.cpp" />
     <ClCompile Include="PageTransferLog.cpp" />
     <ClCompile Include="PortConfigurationDlg.cpp" />
+    <ClCompile Include="ProcessJob.cpp" />
     <ClCompile Include="ProductionLogManager.cpp" />
     <ClCompile Include="RecipeDeviceBindDlg.cpp" />
     <ClCompile Include="RecipeManager.cpp" />
diff --git a/SourceCode/Bond/Servo/Servo.vcxproj.filters b/SourceCode/Bond/Servo/Servo.vcxproj.filters
index c82ddab..1d90814 100644
--- a/SourceCode/Bond/Servo/Servo.vcxproj.filters
+++ b/SourceCode/Bond/Servo/Servo.vcxproj.filters
@@ -176,6 +176,10 @@
     <ClCompile Include="CPageVarialbles.cpp" />
     <ClCompile Include="CPageReport.cpp" />
     <ClCompile Include="CPageCollectionEvent.cpp" />
+    <ClCompile Include="ProcessJob.cpp" />
+    <ClCompile Include="CControlJob.cpp" />
+    <ClCompile Include="CExpandableListCtrl.cpp" />
+    <ClCompile Include="CControlJobDlg.cpp" />
   </ItemGroup>
   <ItemGroup>
     <ClInclude Include="AlarmManager.h" />
@@ -357,6 +361,11 @@
     <ClInclude Include="CPageVarialbles.h" />
     <ClInclude Include="CPageReport.h" />
     <ClInclude Include="CPageCollectionEvent.h" />
+    <ClInclude Include="ProcessJob.h" />
+    <ClInclude Include="CControlJob.h" />
+    <ClInclude Include="SerializeUtil.h" />
+    <ClInclude Include="CExpandableListCtrl.h" />
+    <ClInclude Include="CControlJobDlg.h" />
   </ItemGroup>
   <ItemGroup>
     <ResourceCompile Include="Servo.rc" />
diff --git a/SourceCode/Bond/Servo/ServoDlg.cpp b/SourceCode/Bond/Servo/ServoDlg.cpp
index 746549b..400712c 100644
--- a/SourceCode/Bond/Servo/ServoDlg.cpp
+++ b/SourceCode/Bond/Servo/ServoDlg.cpp
@@ -26,6 +26,7 @@
 #include "CPageVarialbles.h"
 #include "CPageReport.h"
 #include "CPageCollectionEvent.h"
+#include "CControlJobDlg.h"
 
 
 #ifdef _DEBUG
@@ -39,6 +40,8 @@
 /* 运行时间定时器 */
 #define TIMER_ID_UPDATE_RUMTIME			2
 
+/* Test */
+#define TIMER_ID_TEST					3
 
 
 // 用于应用程序“关于”菜单项的 CAboutDlg 对话框
@@ -332,7 +335,7 @@
 
 	// model init
 	theApp.m_model.init();
-
+	SetTimer(TIMER_ID_TEST, 1000, nullptr);
 
 	// 菜单
 	CMenu menu;
@@ -349,7 +352,8 @@
 	HMENU hMenu = m_pTopToolbar->GetOperatorMenu();
 	ASSERT(hMenu);
 	::EnableMenuItem(hMenu, ID_OPEATOR_SWITCH, MF_BYCOMMAND | MF_DISABLED | MF_GRAYED);
-	
+	m_pTopToolbar->GetBtn(IDC_BUTTON_JOBS)->EnableWindow(TRUE);
+
 
 	// Tab
 	m_pPageGraph1 = new CPageGraph1();
@@ -875,6 +879,13 @@
 		m_pMyStatusbar->setRunTimeText((LPTSTR)(LPCTSTR)strText);
 	}
 
+	else if(TIMER_ID_TEST == nIDEvent){
+		static __int64 tttt = 0;
+		tttt++;
+		theApp.m_model.m_hsmsPassive.setVariableValue("CJobSpace", tttt % 10);
+		theApp.m_model.m_hsmsPassive.setVariableValue("PJobSpace", tttt % 5);
+	}
+
 
 	CDialogEx::OnTimer(nIDEvent);
 }
@@ -968,6 +979,11 @@
 			m_pTopToolbar->GetBtn(IDC_BUTTON_STOP)->EnableWindow(FALSE);
 		}
 	}
+	else if (id == IDC_BUTTON_JOBS) {
+		CControlJobDlg dlg;
+		dlg.SetControlJob(theApp.m_model.m_master.getControlJob());
+		dlg.DoModal();
+	}
 	else if (id == IDC_BUTTON_PORT_CONFIG) {
 		CPortConfigurationDlg dlg;
 		dlg.DoModal();
diff --git a/SourceCode/Bond/Servo/TopToolbar.cpp b/SourceCode/Bond/Servo/TopToolbar.cpp
index 0208ece..a1d80db 100644
--- a/SourceCode/Bond/Servo/TopToolbar.cpp
+++ b/SourceCode/Bond/Servo/TopToolbar.cpp
@@ -29,6 +29,7 @@
 	DDX_Control(pDX, IDC_BUTTON_RUN, m_btnRun);
 	DDX_Control(pDX, IDC_BUTTON_RUN_CT, m_btnRunCt);
 	DDX_Control(pDX, IDC_BUTTON_STOP, m_btnStop);
+	DDX_Control(pDX, IDC_BUTTON_JOBS, m_btnCJobs);
 	DDX_Control(pDX, IDC_BUTTON_ALARM, m_btnAlarm);
 	DDX_Control(pDX, IDC_BUTTON_SETTINGS, m_btnSettings);
 	DDX_Control(pDX, IDC_BUTTON_PORT_CONFIG, m_btnPortConfig);
@@ -56,6 +57,7 @@
 	InitBtn(m_btnRunCt, "RunCt_High_32.ico", "RunCt_Gray_32.ico");
 	InitBtn(m_btnStop, "Stop_High_32.ico", "Stop_Gray_32.ico");
 	InitBtn(m_btnAlarm, "Alarm_o_32.ico", "Alarm_gray_32.ico");
+	InitBtn(m_btnCJobs, "ControlJob_High_32.ico", "ControlJob_Gray_32.ico");
 	InitBtn(m_btnSettings, "Settings_High_32.ico", "Settings_Gray_32.ico");
 	InitBtn(m_btnRobot, "Robot_High_32.ico", "Robot_Gray_32.ico");
 	InitBtn(m_btnPortConfig, "PortConfig_High_32.ico", "PortConfig_Gray_32.ico");
@@ -124,6 +126,11 @@
 	x += 2;
 
 	pItem = GetDlgItem(IDC_BUTTON_STOP);
+	pItem->MoveWindow(x, y, BTN_WIDTH, nBthHeight);
+	x += BTN_WIDTH;
+	x += 2;
+
+	pItem = GetDlgItem(IDC_BUTTON_JOBS);
 	pItem->MoveWindow(x, y, BTN_WIDTH, nBthHeight);
 	x += BTN_WIDTH;
 	x += 2;
@@ -199,6 +206,7 @@
 	case IDC_BUTTON_RUN:
 	case IDC_BUTTON_RUN_CT:
 	case IDC_BUTTON_STOP:
+	case IDC_BUTTON_JOBS:
 	case IDC_BUTTON_ALARM:
 	case IDC_BUTTON_SETTINGS:
 	case IDC_BUTTON_PORT_CONFIG:
diff --git a/SourceCode/Bond/Servo/TopToolbar.h b/SourceCode/Bond/Servo/TopToolbar.h
index 6400443..d93c9e6 100644
--- a/SourceCode/Bond/Servo/TopToolbar.h
+++ b/SourceCode/Bond/Servo/TopToolbar.h
@@ -33,6 +33,7 @@
 	CBlButton m_btnRun;
 	CBlButton m_btnRunCt;
 	CBlButton m_btnStop;
+	CBlButton m_btnCJobs;
 	CBlButton m_btnAlarm;
 	CBlButton m_btnSettings;
 	CBlButton m_btnPortConfig;
diff --git a/SourceCode/Bond/Servo/resource.h b/SourceCode/Bond/Servo/resource.h
index 3658421..c911fc0 100644
--- a/SourceCode/Bond/Servo/resource.h
+++ b/SourceCode/Bond/Servo/resource.h
Binary files differ
diff --git a/SourceCode/Bond/x64/Debug/CollectionEventList.txt b/SourceCode/Bond/x64/Debug/CollectionEventList.txt
index 8732aaf..44350fa 100644
--- a/SourceCode/Bond/x64/Debug/CollectionEventList.txt
+++ b/SourceCode/Bond/x64/Debug/CollectionEventList.txt
@@ -38,3 +38,4 @@
 40000,E90_SPSM_NoState_To_NeedsProcessing,,(40000)
 40001,E90_SPSM_InProcess_To_ProcessCompleted,,(40000)
 50000,CarrierID_Readed,,(50000)
+50001,PJ_Queued,,(50001)
diff --git a/SourceCode/Bond/x64/Debug/ReportList.txt b/SourceCode/Bond/x64/Debug/ReportList.txt
index fe7fc97..ce240e3 100644
--- a/SourceCode/Bond/x64/Debug/ReportList.txt
+++ b/SourceCode/Bond/x64/Debug/ReportList.txt
@@ -16,4 +16,5 @@
 31000,(1,31000,31001)
 40000,(1,10203,20000)
 50000,(5000)
+50001,(5003)
 
diff --git a/SourceCode/Bond/x64/Debug/Res/ControlJob_Gray_32.ico b/SourceCode/Bond/x64/Debug/Res/ControlJob_Gray_32.ico
new file mode 100644
index 0000000..a4ca240
--- /dev/null
+++ b/SourceCode/Bond/x64/Debug/Res/ControlJob_Gray_32.ico
Binary files differ
diff --git a/SourceCode/Bond/x64/Debug/Res/ControlJob_High_32.ico b/SourceCode/Bond/x64/Debug/Res/ControlJob_High_32.ico
new file mode 100644
index 0000000..4787246
--- /dev/null
+++ b/SourceCode/Bond/x64/Debug/Res/ControlJob_High_32.ico
Binary files differ
diff --git a/SourceCode/Bond/x64/Debug/VariableList.txt b/SourceCode/Bond/x64/Debug/VariableList.txt
index 1a93f88..02fd14e 100644
--- a/SourceCode/Bond/x64/Debug/VariableList.txt
+++ b/SourceCode/Bond/x64/Debug/VariableList.txt
@@ -8,7 +8,6 @@
 701,PreviousProcessState,U1,
 800,EFEMPPExecName,A20,
 801,EQPPExecName,A20,
-1000,CJobSpace,U1,
 2000,RbRAxisTorque,I2,机器人R轴扭矩
 2001,RbLAxisTorque,l2,机器人L轴扭矩
 2002,RbZAxisTorque,l2,机器人Z轴扭矩
@@ -36,3 +35,6 @@
 2024,CCDEnable,U2,"CCD使能:O:开启 1:屏蔽"
 2025,FFUParameter,U2,FFU设定值
 5000,CarrierID,A20,卡匣ID
+5001,CJobSpace,U1,CJ Space
+5002,PJobSpace,U1,PJ Space
+5003,PJQueued,L,PJ Queued
\ No newline at end of file

--
Gitblit v1.9.3