diff --git a/apps/secs_server.cpp b/apps/secs_server.cpp index ee65f66..e515a31 100644 --- a/apps/secs_server.cpp +++ b/apps/secs_server.cpp @@ -886,6 +886,45 @@ int main(int argc, char** argv) { " -> S16F14 HCACK=" + std::to_string(static_cast(ack))); return gem::s16f14_pr_job_dequeue_ack(ack); }); + router.on(16, 7, [model, logfn](const s2::Message& msg) { + auto req = gem::parse_s16f7(msg); + if (!req) return gem::s16f8_pr_job_monitor_ack(gem::HostCmdAck::ParameterInvalid); + bool any_bad = false; + for (const auto& e : req->entries) { + const bool enable = (e.pralert & 0x80) != 0; + if (!model->process_jobs.set_alert(e.prjobid, enable)) any_bad = true; + } + const auto ack = any_bad ? gem::HostCmdAck::InvalidObject : gem::HostCmdAck::Accept; + logfn("S16F7 monitor " + std::to_string(req->entries.size()) + + " jobs -> S16F8 HCACK=" + std::to_string(static_cast(ack))); + return gem::s16f8_pr_job_monitor_ack(ack); + }); + router.on(16, 15, [model, logfn](const s2::Message& msg) { + auto req = gem::parse_s16f15(msg); + if (!req) + return gem::s16f16_pr_job_create_multi_ack(std::vector{}); + std::vector results; + results.reserve(req->jobs.size()); + for (const auto& job : req->jobs) { + auto r = model->process_jobs.create( + job.prjobid, job.ppid, job.mtrloutspec, + [model](const std::string& ppid) { + return model->recipes.get(ppid).has_value(); + }); + gem::HostCmdAck ack = gem::HostCmdAck::Accept; + switch (r) { + case gem::ProcessJobStore::CreateResult::Created: break; + case gem::ProcessJobStore::CreateResult::Denied_AlreadyExists: + ack = gem::HostCmdAck::Rejected; break; + case gem::ProcessJobStore::CreateResult::Denied_InvalidPpid: + ack = gem::HostCmdAck::ParameterInvalid; break; + } + results.push_back({job.prjobid, ack}); + } + logfn("S16F15 multi-create " + std::to_string(req->jobs.size()) + + " jobs -> S16F16"); + return gem::s16f16_pr_job_create_multi_ack(results); + }); router.on(16, 5, [model, logfn](const s2::Message& msg) { auto req = gem::parse_s16f5(msg); if (!req) return gem::s16f6_pr_job_command_ack(gem::HostCmdAck::ParameterInvalid); diff --git a/data/messages.yaml b/data/messages.yaml index 4fbba27..84256a0 100644 --- a/data/messages.yaml +++ b/data/messages.yaml @@ -1541,6 +1541,80 @@ messages: builder: s16f14_pr_job_dequeue_ack body: {kind: scalar, item_type: BINARY_BYTE, enum: HostCmdAck, param: ack} + # S16F7 / S16F8 โ€” PRJobMonitor. Host enables or disables S16F9 + # PRJobAlert emission per PRJOBID. PRALERT is a 1-byte boolean + # (0 = disable, 0x80 = enable, per E40-0705 ยง10). + - id: S16F7 + stream: 16 + function: 7 + w: true + builder: s16f7_pr_job_monitor + parser: parse_s16f7 + body: + kind: list + struct_name: PRJobMonitorRequest + fields: + - name: entries + shape: + kind: list_of + element: + kind: list + struct_name: PRJobMonitorEntry + fields: + - {name: prjobid, shape: {kind: scalar, item_type: ASCII}} + - {name: pralert, shape: {kind: scalar, item_type: BINARY_BYTE}} + + - id: S16F8 + stream: 16 + function: 8 + builder: s16f8_pr_job_monitor_ack + body: {kind: scalar, item_type: BINARY_BYTE, enum: HostCmdAck, param: ack} + + # S16F15 / S16F16 โ€” PRJobCreateMultiple. Bulk create variant of + # S16F11; the host posts a batch of jobs in one round-trip. The + # equipment processes them in order and replies with a per-job ACK + # list so the host can tell which subset failed. + - id: S16F15 + stream: 16 + function: 15 + w: true + builder: s16f15_pr_job_create_multi + parser: parse_s16f15 + body: + kind: list + struct_name: PRJobCreateMultiRequest + fields: + - name: jobs + shape: + kind: list_of + element: + kind: list + struct_name: PRJobCreateEntry + fields: + - {name: prjobid, shape: {kind: scalar, item_type: ASCII}} + - {name: ppid, shape: {kind: scalar, item_type: ASCII}} + - name: mtrloutspec + shape: {kind: list_of, element: {kind: scalar, item_type: ASCII}} + + - id: S16F16 + stream: 16 + function: 16 + builder: s16f16_pr_job_create_multi_ack + parser: parse_s16f16 + body: + kind: list + struct_name: PRJobCreateMultiAck + fields: + - name: results + shape: + kind: list_of + element: + kind: list + struct_name: PRJobCreateMultiResult + fields: + - {name: prjobid, shape: {kind: scalar, item_type: ASCII}} + - {name: ack, shape: {kind: scalar, item_type: BINARY_BYTE, enum: HostCmdAck}} + # S16F27 / F28 โ€” CJobCommand (E94). Host issues Start / Pause / # Resume / Stop / Abort against an existing CJ. - id: S16F27 diff --git a/tests/test_messages.cpp b/tests/test_messages.cpp index 879bf0e..e7a7bb3 100644 --- a/tests/test_messages.cpp +++ b/tests/test_messages.cpp @@ -649,6 +649,48 @@ TEST_CASE("S16F9 PRJobAlert round-trip with typed state byte") { CHECK(parsed->prjobstate == ProcessJobState::Processing); } +TEST_CASE("S16F7 / S16F8 PRJobMonitor round-trip") { + std::vector entries{ + {"PJ-1", 0x80}, // enable + {"PJ-2", 0x00}, // disable + }; + auto m = s16f7_pr_job_monitor(entries); + CHECK(m.stream == 16); + CHECK(m.function == 7); + CHECK(m.reply_expected); + + auto parsed = parse_s16f7(m); + REQUIRE(parsed.has_value()); + REQUIRE(parsed->entries.size() == 2); + CHECK(parsed->entries[0].prjobid == "PJ-1"); + CHECK(parsed->entries[0].pralert == 0x80); + + CHECK(*ack_byte(s16f8_pr_job_monitor_ack(HostCmdAck::Accept)) == 0); +} + +TEST_CASE("S16F15 / S16F16 PRJobCreateMultiple round-trip") { + std::vector jobs{ + {"PJ-A", "RECIPE-1", {"W1"}}, + {"PJ-B", "RECIPE-2", {"W2", "W3"}}, + }; + auto m = s16f15_pr_job_create_multi(jobs); + auto parsed = parse_s16f15(m); + REQUIRE(parsed.has_value()); + REQUIRE(parsed->jobs.size() == 2); + CHECK(parsed->jobs[0].prjobid == "PJ-A"); + CHECK(parsed->jobs[1].mtrloutspec.size() == 2); + + std::vector results{ + {"PJ-A", HostCmdAck::Accept}, + {"PJ-B", HostCmdAck::Rejected}, + }; + auto ackmsg = s16f16_pr_job_create_multi_ack(results); + auto pack = parse_s16f16(ackmsg); + REQUIRE(pack.has_value()); + REQUIRE(pack->results.size() == 2); + CHECK(pack->results[1].ack == HostCmdAck::Rejected); +} + TEST_CASE("S16F27 / S16F28 CJobCommand round-trip") { auto req = s16f27_cj_command("CJ-1", "CJSTART"); auto parsed = parse_s16f27(req);