@@ -26,7 +26,7 @@ use alloc::{boxed::Box, string::String, sync::Arc as AArc, vec::Vec};
2626use pyo3:: exceptions:: { PyRuntimeError , PyValueError } ;
2727use pyo3:: prelude:: * ;
2828use pyo3:: types:: { PyBytes , PyDict , PyList , PyModule , PyTuple } ;
29- use std:: sync:: { Arc as SArc , Mutex } ;
29+ use std:: sync:: { Arc as SArc , Mutex , OnceLock } ;
3030
3131use crate :: {
3232 config:: DataEndpoint , get_needed_message_size, message_meta, router:: { BoardConfig , Clock , EndpointHandler , LeBytes , Router } ,
@@ -39,6 +39,8 @@ use crate::{
3939 MAX_VALUE_DATA_TYPE ,
4040} ;
4141
42+ static GLOBAL_ROUTER_SINGLETON : OnceLock < SArc < Mutex < Router > > > = OnceLock :: new ( ) ;
43+
4244// ============================================================================
4345// Shared helpers / constants
4446// ============================================================================
@@ -218,6 +220,170 @@ pub struct PyRouter {
218220
219221#[ pymethods]
220222impl PyRouter {
223+ // ------------------------------------------------------------------------
224+ // Singleton construction
225+ // ------------------------------------------------------------------------
226+
227+ /// Create or retrieve a per-process singleton Router.
228+ ///
229+ /// This creates a Router with:
230+ /// - no clock (timestamp=0 unless you pass explicit timestamps),
231+ /// - no endpoint handlers,
232+ /// - an optional TX callback.
233+ ///
234+ /// The first call initializes the singleton. Subsequent calls return
235+ /// another `PyRouter` object wrapping the same underlying Router.
236+ ///
237+ /// If you pass a non-None `tx` after the singleton is already created,
238+ /// an error is raised (the TX callback cannot be changed once set).
239+ #[ staticmethod]
240+ #[ pyo3( signature = ( tx=None , now_ms=None , handlers=None ) ) ]
241+ fn new_singleton (
242+ py : Python < ' _ > ,
243+ tx : Option < Py < PyAny > > ,
244+ now_ms : Option < Py < PyAny > > ,
245+ handlers : Option < & Bound < ' _ , PyAny > > ,
246+ ) -> PyResult < Self > {
247+ // ----------------------------------------------------------
248+ // 1. If the singleton already exists, return a new wrapper
249+ // ----------------------------------------------------------
250+ if let Some ( existing) = GLOBAL_ROUTER_SINGLETON . get ( ) {
251+ // Prevent changing TX / clock / handlers after initialized
252+ if tx. is_some ( ) || now_ms. is_some ( ) || handlers. is_some ( ) {
253+ return Err ( PyRuntimeError :: new_err (
254+ "Router singleton already exists; cannot modify tx/now_ms/handlers" ,
255+ ) ) ;
256+ }
257+
258+ return Ok ( PyRouter {
259+ inner : existing. clone ( ) ,
260+ _tx_cb : None ,
261+ _pkt_cbs : Vec :: new ( ) ,
262+ _ser_cbs : Vec :: new ( ) ,
263+ } ) ;
264+ }
265+
266+ // ----------------------------------------------------------
267+ // 2. FIRST CALL — build the whole router (same as __init__)
268+ // ----------------------------------------------------------
269+
270+ // Copy callbacks to keep them alive
271+ let tx_keep = tx. as_ref ( ) . map ( |p| p. clone_ref ( py) ) ;
272+ let now_keep = now_ms. as_ref ( ) . map ( |p| p. clone_ref ( py) ) ;
273+
274+ // Build transmit callback
275+ let tx_for_closure = tx_keep. as_ref ( ) . map ( |p| p. clone_ref ( py) ) ;
276+ let transmit = if let Some ( cb) = tx_for_closure {
277+ Some ( move |bytes : & [ u8 ] | -> TelemetryResult < ( ) > {
278+ Python :: attach ( |py| {
279+ let arg = PyBytes :: new ( py, bytes) ;
280+ match cb. call1 ( py, ( & arg, ) ) {
281+ Ok ( _) => Ok ( ( ) ) ,
282+ Err ( err) => {
283+ err. restore ( py) ;
284+ Err ( TelemetryError :: Io ( "tx error" ) )
285+ }
286+ }
287+ } )
288+ } )
289+ } else {
290+ None
291+ } ;
292+
293+ // Build endpoint handlers (same as __init__)
294+ let mut handlers_vec = Vec :: new ( ) ;
295+ let mut keep_pkt = Vec :: new ( ) ;
296+ let mut keep_ser = Vec :: new ( ) ;
297+
298+ if let Some ( hs) = handlers {
299+ let list = hs. cast :: < PyList > ( ) . map_err ( |_| {
300+ PyValueError :: new_err ( "handlers must be list of (endpoint, pkt_cb, ser_cb) tuples" )
301+ } ) ?;
302+
303+ for item in list. iter ( ) {
304+ let tup = item
305+ . cast :: < PyTuple > ( )
306+ . map_err ( |_| PyValueError :: new_err ( "handler must be a 3-tuple" ) ) ?;
307+ if tup. len ( ) != 3 {
308+ return Err ( PyValueError :: new_err ( "tuple arity must be 3" ) ) ;
309+ }
310+
311+ let ep_u32: u32 = tup. get_item ( 0 ) ?. extract ( ) ?;
312+ let endpoint = endpoint_from_u32 ( ep_u32) . map_err ( py_err_from) ?;
313+
314+ // Packet handler
315+ if !tup. get_item ( 1 ) ?. is_none ( ) {
316+ let cb: Py < PyAny > = tup. get_item ( 1 ) ?. extract ( ) ?;
317+ let cb_for_closure = cb. clone_ref ( py) ;
318+ keep_pkt. push ( cb) ;
319+
320+ let eh = EndpointHandler :: new_packet_handler ( endpoint, move |pkt| {
321+ Python :: attach ( |py| {
322+ let py_pkt = PyPacket { inner : pkt. clone ( ) } ;
323+ let any = Py :: new ( py, py_pkt)
324+ . map_err ( |_| TelemetryError :: Io ( "packet wrapper" ) ) ?;
325+ match cb_for_closure. call1 ( py, ( & any, ) ) {
326+ Ok ( _) => Ok ( ( ) ) ,
327+ Err ( err) => {
328+ err. restore ( py) ;
329+ Err ( TelemetryError :: Io ( "packet handler error" ) )
330+ }
331+ }
332+ } )
333+ } ) ;
334+
335+ handlers_vec. push ( eh) ;
336+ }
337+
338+ // Serialized handler
339+ if !tup. get_item ( 2 ) ?. is_none ( ) {
340+ let cb: Py < PyAny > = tup. get_item ( 2 ) ?. extract ( ) ?;
341+ let cb_for_closure = cb. clone_ref ( py) ;
342+ keep_ser. push ( cb) ;
343+
344+ let eh = EndpointHandler :: new_serialized_handler ( endpoint, move |bytes| {
345+ Python :: attach ( |py| {
346+ let arg = PyBytes :: new ( py, bytes) ;
347+ match cb_for_closure. call1 ( py, ( & arg, ) ) {
348+ Ok ( _) => Ok ( ( ) ) ,
349+ Err ( err) => {
350+ err. restore ( py) ;
351+ Err ( TelemetryError :: Io ( "serialized handler error" ) )
352+ }
353+ }
354+ } )
355+ } ) ;
356+
357+ handlers_vec. push ( eh) ;
358+ }
359+ }
360+ }
361+
362+ // Build clock callback
363+ let clock = PyClock {
364+ cb : now_keep. as_ref ( ) . map ( |p| p. clone_ref ( py) ) ,
365+ } ;
366+
367+ // Build router
368+ let cfg = BoardConfig :: new ( handlers_vec) ;
369+ let router = Router :: new ( transmit, cfg, Box :: new ( clock) ) ;
370+
371+ let arc = SArc :: new ( Mutex :: new ( router) ) ;
372+
373+ // Store it into OnceLock
374+ GLOBAL_ROUTER_SINGLETON
375+ . set ( arc. clone ( ) )
376+ . map_err ( |_existing| PyRuntimeError :: new_err ( "Router singleton already exists" ) ) ?;
377+
378+ // Return wrapper
379+ Ok ( PyRouter {
380+ inner : arc,
381+ _tx_cb : tx_keep,
382+ _pkt_cbs : keep_pkt,
383+ _ser_cbs : keep_ser,
384+ } )
385+ }
386+
221387 /// Create a new router.
222388 ///
223389 /// Parameters
0 commit comments