33/**
44 * Co-simulation REST routes.
55 *
6- * Provides endpoints for session management, MQTT participant discovery ,
7- * and historian queries .
6+ * Provides endpoints for session management, participant enrollment ,
7+ * MQTT participant discovery, variable couplings, and orchestrator control .
88 */
99
1010import type { CosimMqttClient } from "@modelscript/cosim" ;
11- import { Orchestrator , SessionManager } from "@modelscript/cosim" ;
11+ import { FmuJsParticipant , FmuStorage , Orchestrator , SessionManager } from "@modelscript/cosim" ;
1212import express from "express" ;
1313
1414const sessionManager = new SessionManager ( ) ;
15+ const fmuStorage = new FmuStorage ( ) ;
16+
17+ /** Track running orchestrators by session ID for pause/stop. */
18+ const orchestrators = new Map < string , Orchestrator > ( ) ;
1519
1620/**
1721 * Create the co-simulation router.
@@ -46,6 +50,73 @@ export function cosimRouter(mqttClient: CosimMqttClient | null): express.Router
4650 res . json ( session . toJSON ( ) ) ;
4751 } ) ;
4852
53+ // ── Participant Enrollment ──
54+
55+ // POST /api/v1/cosim/sessions/:id/participants/fmu — Add an FMU participant
56+ router . post ( "/sessions/:id/participants/fmu" , ( req , res ) => {
57+ const session = sessionManager . getSession ( req . params [ "id" ] ?? "" ) ;
58+ if ( ! session ) {
59+ return res . status ( 404 ) . json ( { error : "Session not found" } ) ;
60+ }
61+
62+ const { fmuId, participantId } = req . body as { fmuId ?: string ; participantId ?: string } ;
63+ if ( ! fmuId ) {
64+ return res . status ( 400 ) . json ( { error : "Missing required field: fmuId" } ) ;
65+ }
66+
67+ const pid = participantId ?? `fmu-${ fmuId } -${ Math . random ( ) . toString ( 36 ) . slice ( 2 , 6 ) } ` ;
68+
69+ try {
70+ const participant = new FmuJsParticipant ( {
71+ id : pid ,
72+ fmuId,
73+ storage : fmuStorage ,
74+ } ) ;
75+ session . addParticipant ( participant ) ;
76+ res . status ( 201 ) . json ( {
77+ ok : true ,
78+ participantId : pid ,
79+ modelName : participant . modelName ,
80+ variables : participant . metadata . variables . length ,
81+ } ) ;
82+ } catch ( err : unknown ) {
83+ res . status ( 400 ) . json ( { error : err instanceof Error ? err . message : String ( err ) } ) ;
84+ }
85+ } ) ;
86+
87+ // GET /api/v1/cosim/sessions/:id/participants — List session participants
88+ router . get ( "/sessions/:id/participants" , ( req , res ) => {
89+ const session = sessionManager . getSession ( req . params [ "id" ] ?? "" ) ;
90+ if ( ! session ) {
91+ return res . status ( 404 ) . json ( { error : "Session not found" } ) ;
92+ }
93+
94+ const participants = Array . from ( session . participants . values ( ) ) . map ( ( p ) => ( {
95+ id : p . id ,
96+ modelName : p . modelName ,
97+ type : p . metadata . type ,
98+ variables : p . metadata . variables . length ,
99+ } ) ) ;
100+ res . json ( { participants } ) ;
101+ } ) ;
102+
103+ // DELETE /api/v1/cosim/sessions/:id/participants/:pid — Remove a participant
104+ router . delete ( "/sessions/:id/participants/:pid" , ( req , res ) => {
105+ const session = sessionManager . getSession ( req . params [ "id" ] ?? "" ) ;
106+ if ( ! session ) {
107+ return res . status ( 404 ) . json ( { error : "Session not found" } ) ;
108+ }
109+
110+ try {
111+ session . removeParticipant ( req . params [ "pid" ] ?? "" ) ;
112+ res . json ( { ok : true } ) ;
113+ } catch ( err : unknown ) {
114+ res . status ( 400 ) . json ( { error : err instanceof Error ? err . message : String ( err ) } ) ;
115+ }
116+ } ) ;
117+
118+ // ── Couplings ──
119+
49120 // POST /api/v1/cosim/sessions/:id/couplings — Define variable couplings
50121 router . post ( "/sessions/:id/couplings" , ( req , res ) => {
51122 const session = sessionManager . getSession ( req . params [ "id" ] ?? "" ) ;
@@ -73,6 +144,8 @@ export function cosimRouter(mqttClient: CosimMqttClient | null): express.Router
73144 }
74145 } ) ;
75146
147+ // ── Orchestrator Control ──
148+
76149 // POST /api/v1/cosim/sessions/:id/start — Start co-simulation
77150 router . post ( "/sessions/:id/start" , ( req , res ) => {
78151 const session = sessionManager . getSession ( req . params [ "id" ] ?? "" ) ;
@@ -87,12 +160,16 @@ export function cosimRouter(mqttClient: CosimMqttClient | null): express.Router
87160 const orchestrator = new Orchestrator ( session , mqttClient , {
88161 onComplete : ( ) => {
89162 console . log ( `[cosim] Session ${ session . sessionId } completed` ) ;
163+ orchestrators . delete ( session . sessionId ) ;
90164 } ,
91165 onError : ( err : Error ) => {
92166 console . error ( `[cosim] Session ${ session . sessionId } error:` , err . message ) ;
167+ orchestrators . delete ( session . sessionId ) ;
93168 } ,
94169 } ) ;
95170
171+ orchestrators . set ( session . sessionId , orchestrator ) ;
172+
96173 // Run asynchronously — don't await
97174 void orchestrator . run ( ) ;
98175
@@ -101,25 +178,43 @@ export function cosimRouter(mqttClient: CosimMqttClient | null): express.Router
101178
102179 // POST /api/v1/cosim/sessions/:id/pause — Pause simulation
103180 router . post ( "/sessions/:id/pause" , ( req , res ) => {
104- const session = sessionManager . getSession ( req . params [ "id" ] ?? "" ) ;
105- if ( ! session ) {
106- return res . status ( 404 ) . json ( { error : "Session not found" } ) ;
181+ const sessionId = req . params [ "id" ] ?? "" ;
182+ const orchestrator = orchestrators . get ( sessionId ) ;
183+ if ( ! orchestrator ) {
184+ return res . status ( 404 ) . json ( { error : "No running orchestrator for this session" } ) ;
107185 }
108- res . json ( { ok : true , state : session . state } ) ;
186+ orchestrator . pause ( ) ;
187+ res . json ( { ok : true , state : "paused" } ) ;
188+ } ) ;
189+
190+ // POST /api/v1/cosim/sessions/:id/resume — Resume simulation
191+ router . post ( "/sessions/:id/resume" , ( req , res ) => {
192+ const sessionId = req . params [ "id" ] ?? "" ;
193+ const orchestrator = orchestrators . get ( sessionId ) ;
194+ if ( ! orchestrator ) {
195+ return res . status ( 404 ) . json ( { error : "No running orchestrator for this session" } ) ;
196+ }
197+ orchestrator . resume ( ) ;
198+ res . json ( { ok : true , state : "running" } ) ;
109199 } ) ;
110200
111201 // POST /api/v1/cosim/sessions/:id/stop — Stop and terminate
112202 router . post ( "/sessions/:id/stop" , ( req , res ) => {
113- const session = sessionManager . getSession ( req . params [ "id" ] ?? "" ) ;
114- if ( ! session ) {
115- return res . status ( 404 ) . json ( { error : "Session not found" } ) ;
203+ const sessionId = req . params [ "id" ] ?? "" ;
204+ const orchestrator = orchestrators . get ( sessionId ) ;
205+ if ( ! orchestrator ) {
206+ return res . status ( 404 ) . json ( { error : "No running orchestrator for this session" } ) ;
116207 }
117- res . json ( { ok : true , state : session . state } ) ;
208+ orchestrator . abort ( ) ;
209+ orchestrators . delete ( sessionId ) ;
210+ res . json ( { ok : true , state : "stopping" } ) ;
118211 } ) ;
119212
120213 // DELETE /api/v1/cosim/sessions/:id — Remove completed/failed session
121214 router . delete ( "/sessions/:id" , ( req , res ) => {
122- sessionManager . removeSession ( req . params [ "id" ] ?? "" ) ;
215+ const sessionId = req . params [ "id" ] ?? "" ;
216+ orchestrators . delete ( sessionId ) ;
217+ sessionManager . removeSession ( sessionId ) ;
123218 res . json ( { ok : true } ) ;
124219 } ) ;
125220
@@ -185,7 +280,6 @@ export function mqttParticipantsRouter(mqttClient: CosimMqttClient | null): expr
185280 return res . status ( 404 ) . json ( { error : "Participant not found" } ) ;
186281 }
187282
188- // Return in the same format as the library tree nodes
189283 const treeNode = {
190284 id : `mqtt://${ meta . participantId } ` ,
191285 name : meta . modelName ,
0 commit comments