From a2209cc432cb9c80779d83e51ef090f782e8404a Mon Sep 17 00:00:00 2001
From: LAPTOP-SNT8I5JK\Boounion <Chenluhua@qq.com>
Date: 星期四, 14 八月 2025 17:30:52 +0800
Subject: [PATCH] 1.Create PJ功能实现;

---
 SourceCode/Bond/EAPSimulator/ProcessJob.h                 |  198 ++++++++
 SourceCode/Bond/Servo/Servo.vcxproj                       |    3 
 SourceCode/Bond/Servo/HsmsPassive.h                       |    4 
 SourceCode/Bond/Servo/ProcessJob.h                        |  203 ++++++++
 SourceCode/Bond/Servo/ProcessJob.cpp                      |  260 ++++++++++
 SourceCode/Bond/EAPSimulator/ProcessJob.cpp               |  252 ++++++++++
 SourceCode/Bond/EAPSimulator/CHsmsActive.h                |    3 
 SourceCode/Bond/EAPSimulator/EAPSimulatorDlg.h            |    1 
 SourceCode/Bond/Servo/HsmsPassive.cpp                     |  108 ++++
 SourceCode/Bond/EAPSimulator/EAPSimulator.rc              |    0 
 SourceCode/Bond/Servo/CMaster.h                           |   16 
 SourceCode/Bond/Servo/Model.cpp                           |    6 
 SourceCode/Bond/Servo/CLoadPort.cpp                       |   18 
 SourceCode/Bond/EAPSimulator/EAPSimulator.vcxproj.filters |   12 
 SourceCode/Bond/EAPSimulator/CHsmsActive.cpp              |   38 +
 SourceCode/Bond/EAPSimulator/Resource.h                   |    8 
 SourceCode/Bond/Servo/Servo.vcxproj.filters               |    2 
 SourceCode/Bond/EAPSimulator/CPJsDlg.h                    |   35 +
 SourceCode/Bond/EAPSimulator/EAPSimulatorDlg.cpp          |    9 
 SourceCode/Bond/EAPSimulator/CPJsDlg.cpp                  |  161 ++++++
 SourceCode/Bond/EAPSimulator/EAPSimulator.vcxproj         |    5 
 SourceCode/Bond/Servo/CMaster.cpp                         |   74 +++
 Document/Panel Bonder八零联合 SecsTest CheckList_v3.0.xlsx    |    0 
 23 files changed, 1,409 insertions(+), 7 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..16c9afe 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 b084312..b094a6a 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 = {};
@@ -343,6 +345,42 @@
 	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::replyAck0(IMessage* pMessage)
 {
 	return 0;
diff --git a/SourceCode/Bond/EAPSimulator/CHsmsActive.h b/SourceCode/Bond/EAPSimulator/CHsmsActive.h
index 0383681..f46f279 100644
--- a/SourceCode/Bond/EAPSimulator/CHsmsActive.h
+++ b/SourceCode/Bond/EAPSimulator/CHsmsActive.h
@@ -4,6 +4,7 @@
 #include <map>
 #include <set>
 #include "CCollectionEvent.h"
+#include "ProcessJob.h"
 
 
 #define SVID_CJobSpace				5001
@@ -84,6 +85,8 @@
 		const char* pszCarrierId,
 		unsigned char PTN);
 
+	// S16F15
+	int hsmsPRJobMultiCreate(std::vector<SERVO::CProcessJob*>& pjs);
 
 	// 通过的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 53f3fad..245a5d1 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 eaed5c6..9ced073 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
@@ -92,6 +93,7 @@
 	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)
 END_MESSAGE_MAP()
 
 
@@ -283,6 +285,7 @@
 	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);	
 }
 
 void CEAPSimulatorDlg::OnBnClickedButtonConnect()
@@ -405,3 +408,9 @@
 {
 	theApp.m_model.m_pHsmsActive->hsmsCarrierRelease(DATAID++, "CSX 52078", 2);
 }
+
+void CEAPSimulatorDlg::OnBnClickedButtonCreatePj()
+{
+	CPJsDlg dlg;
+	dlg.DoModal();
+}
diff --git a/SourceCode/Bond/EAPSimulator/EAPSimulatorDlg.h b/SourceCode/Bond/EAPSimulator/EAPSimulatorDlg.h
index 42e69d2..e289190 100644
--- a/SourceCode/Bond/EAPSimulator/EAPSimulatorDlg.h
+++ b/SourceCode/Bond/EAPSimulator/EAPSimulatorDlg.h
@@ -61,4 +61,5 @@
 	afx_msg void OnBnClickedButtonCarrierRelease();
 	afx_msg void OnBnClickedButtonQueryCjSpace();
 	afx_msg void OnBnClickedButtonQueryPjSpace();
+	afx_msg void OnBnClickedButtonCreatePj();
 };
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 a16bb26..6f261c8 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
@@ -51,14 +52,17 @@
 #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_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         1039
+#define _APS_NEXT_CONTROL_VALUE         1042
 #define _APS_NEXT_SYMED_VALUE           101
 #endif
 #endif
diff --git a/SourceCode/Bond/Servo/CLoadPort.cpp b/SourceCode/Bond/Servo/CLoadPort.cpp
index f603990..0050200 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_PORT1_INUSE);
+				CPortStatusReport portStatusReport;
+				portStatusReport.setPortStatus(PORT_INUSE);
+				portStatusReport.setJobExistenceSlot(0xf);
+				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 b0424eb..f664b6e 100644
--- a/SourceCode/Bond/Servo/CMaster.cpp
+++ b/SourceCode/Bond/Servo/CMaster.cpp
@@ -3,6 +3,7 @@
 #include "CMaster.h"
 #include <future>
 #include <vector>
+#include "RecipeManager.h"
 
 
 namespace SERVO {
@@ -62,6 +63,11 @@
 
 	CMaster::~CMaster()
 	{
+		for (auto item : m_processJobs) {
+			delete item;
+		}
+		m_processJobs.clear();
+
 		if (m_hEventReadBitsThreadExit[0] != nullptr) {
 			::CloseHandle(m_hEventReadBitsThreadExit[0]);
 			m_hEventReadBitsThreadExit[0] = nullptr;
@@ -1154,6 +1160,15 @@
 		return nullptr;
 	}
 
+	CEquipment* CMaster::getEquipment(int id) const
+	{
+		for (auto item : m_listEquipment) {
+			if (item->getID() == id) return item;
+		}
+
+		return nullptr;
+	}
+
 	/*
 	 * 添加LoadPort1
 	 * index -- 0~3
@@ -1831,4 +1846,63 @@
 	{
 		m_nContinuousTransferCount = round;
 	}
+
+	int CMaster::setProcessJobs(std::vector<SERVO::CProcessJob*>& pjs)
+	{
+		std::vector<SERVO::CProcessJob*> temp;
+		for (auto p : pjs) {
+			if (p->validate(*this)) {
+				temp.push_back(p);
+			}
+		}
+
+		m_processJobs = temp;
+		return m_processJobs.size();
+	}
+
+	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;
+	}
+
 }
diff --git a/SourceCode/Bond/Servo/CMaster.h b/SourceCode/Bond/Servo/CMaster.h
index cf687de..f19f047 100644
--- a/SourceCode/Bond/Servo/CMaster.h
+++ b/SourceCode/Bond/Servo/CMaster.h
@@ -13,7 +13,7 @@
 #include "CArmTray.h"
 #include "CCLinkIEControl.h"
 #include "CRobotTask.h"
-
+#include "ProcessJob.h"
 
 
 #define CTStep_Unknow                   0
@@ -64,7 +64,7 @@
         ONCTROUNDEND            onCTRoundEnd;
     } MasterListener;
 
-    class CMaster
+    class CMaster : public IResourceView
     {
     public:
         CMaster();
@@ -87,6 +87,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();
@@ -104,6 +105,8 @@
         int carrierRelease(unsigned int port);
         int getContinuousTransferCount();
         void setContinuousTransferCount(int round);
+        int setProcessJobs(std::vector<SERVO::CProcessJob*>& pjs);
+        CLoadPort* getPortWithCarrierId(const std::string& carrierId) const;
 
     private:
         inline void lock() { EnterCriticalSection(&m_criticalSection); }
@@ -134,6 +137,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;
@@ -179,6 +190,7 @@
     private:
         bool m_bEnableEventReport;
         bool m_bEnableAlarmReport;
+        std::vector<SERVO::CProcessJob*> m_processJobs;
     };
 }
 
diff --git a/SourceCode/Bond/Servo/HsmsPassive.cpp b/SourceCode/Bond/Servo/HsmsPassive.cpp
index 1a18950..f44acb0 100644
--- a/SourceCode/Bond/Servo/HsmsPassive.cpp
+++ b/SourceCode/Bond/Servo/HsmsPassive.cpp
@@ -612,6 +612,9 @@
 		else if (nStream == 10 && pHeader->function == 3) {
 			replyTerminalDisplay(pMessage);
 		}
+		else if (nStream == 16 && pHeader->function == 15) {
+			replyPRJobMultiCreate(pMessage);
+		}
 	};
 
 	PassiveListener listener;
@@ -1514,6 +1517,111 @@
 	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->issue().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->issue().empty()) {
+				ISECS2Item* pItemErr = pItemErrors2->addItem();
+				pItemErr->addU4Item(p->issue()[0].code, "ERRCODE");
+				pItemErr->addItem(("<" + p->id() + ">" + p->issue()[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->issue().empty()) delete p;
+	}
+	pjs.clear();
+
+	return 0;
+}
+
 // S5F1
 int CHsmsPassive::requestAlarmReport(int ALCD, int ALID, const char* ALTX)
 {
diff --git a/SourceCode/Bond/Servo/HsmsPassive.h b/SourceCode/Bond/Servo/HsmsPassive.h
index f50de9b..ba36e0d 100644
--- a/SourceCode/Bond/Servo/HsmsPassive.h
+++ b/SourceCode/Bond/Servo/HsmsPassive.h
@@ -7,6 +7,7 @@
 #include <map>
 #include <set>
 #include "CCollectionEvent.h"
+#include "ProcessJob.h"
 
 
 #define EQCONSTANT_VALUE_MAX	64
@@ -86,6 +87,7 @@
 	const char* pszCarrierId,
 	unsigned char PTN, 
 	std::string& strErrorTxt)> CARRIERACTION;
+typedef std::function<int(void* pFrom, std::vector<SERVO::CProcessJob*>& pjs)> PRJOBMULTICREATE;
 typedef struct _SECSListener
 {
 	SECSEQOFFLINE				onEQOffLine;
@@ -98,6 +100,7 @@
 	EDALARMREPORT				onEnableDisableAlarmReport;
 	QUERYPPIDLIST				onQueryPPIDList;
 	CARRIERACTION				onCarrierAction;
+	PRJOBMULTICREATE			onPRJobMultiCreate;
 } SECSListener;
 
 
@@ -207,6 +210,7 @@
 	int replyPurgeSpooledData(IMessage* pRecv);
 	int replyQueryPPIDList(IMessage* pRecv);
 	int replyTerminalDisplay(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..260c4fc 100644
--- a/SourceCode/Bond/Servo/Model.cpp
+++ b/SourceCode/Bond/Servo/Model.cpp
@@ -175,6 +175,12 @@
 			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());
+		}
+		return m_master.setProcessJobs(pjs);
+	};
 	m_hsmsPassive.setListener(listener);
 	m_hsmsPassive.setEquipmentModelType((LPTSTR)(LPCTSTR)strModeType);
 	m_hsmsPassive.setSoftRev((LPTSTR)(LPCTSTR)strSoftRev);
diff --git a/SourceCode/Bond/Servo/ProcessJob.cpp b/SourceCode/Bond/Servo/ProcessJob.cpp
new file mode 100644
index 0000000..2ca265a
--- /dev/null
+++ b/SourceCode/Bond/Servo/ProcessJob.cpp
@@ -0,0 +1,260 @@
+#include "stdafx.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());
+    }
+
+    const std::vector<CProcessJob::ValidationIssue>& CProcessJob::issue()
+    {
+        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));
+        }
+    }
+}
diff --git a/SourceCode/Bond/Servo/ProcessJob.h b/SourceCode/Bond/Servo/ProcessJob.h
new file mode 100644
index 0000000..9cf2fc5
--- /dev/null
+++ b/SourceCode/Bond/Servo/ProcessJob.h
@@ -0,0 +1,203 @@
+#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 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:
+        // —— 构造 / 基本设置 ——
+        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;   // 文本描述
+        };
+        // 返回问题清单(空=通过)
+        bool validate(const IResourceView& rv);
+        const std::vector<ValidationIssue>& issue();
+
+        // —— 状态机(带守卫)——
+        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;   // 参数值
+
+        // 错误列表
+        std::vector<ValidationIssue> m_issues;
+    };
+}
+
+
diff --git a/SourceCode/Bond/Servo/Servo.vcxproj b/SourceCode/Bond/Servo/Servo.vcxproj
index 008f416..8246915 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>
@@ -324,6 +325,7 @@
     <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" />
@@ -470,6 +472,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..230f6df 100644
--- a/SourceCode/Bond/Servo/Servo.vcxproj.filters
+++ b/SourceCode/Bond/Servo/Servo.vcxproj.filters
@@ -176,6 +176,7 @@
     <ClCompile Include="CPageVarialbles.cpp" />
     <ClCompile Include="CPageReport.cpp" />
     <ClCompile Include="CPageCollectionEvent.cpp" />
+    <ClCompile Include="ProcessJob.cpp" />
   </ItemGroup>
   <ItemGroup>
     <ClInclude Include="AlarmManager.h" />
@@ -357,6 +358,7 @@
     <ClInclude Include="CPageVarialbles.h" />
     <ClInclude Include="CPageReport.h" />
     <ClInclude Include="CPageCollectionEvent.h" />
+    <ClInclude Include="ProcessJob.h" />
   </ItemGroup>
   <ItemGroup>
     <ResourceCompile Include="Servo.rc" />

--
Gitblit v1.9.3