Skip to content

Commit 7875ea3

Browse files
Merge sedsprintf_rs upstream main
2 parents dc230ab + 95025b8 commit 7875ea3

File tree

4 files changed

+189
-3
lines changed

4 files changed

+189
-3
lines changed

sedsprintf_rs/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "sedsprintf_rs_2026"
3-
version = "1.0.6"
3+
version = "1.0.8"
44
edition = "2024"
55
build = "build.rs"
66
authors = ["Rylan Meilutis <[email protected]>"]

sedsprintf_rs/header_templates/sedsprintf_rs.pyi.txt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,26 @@ class Router:
9696
serialized_handler: Optional[Callable[[bytes], None]])
9797
"""
9898

99+
@staticmethod
100+
def new_singleton(tx: Optional[TxCallback] = ...,
101+
now_ms: Optional[NowMsCallback] = ...,
102+
handlers: Optional[
103+
Sequence[Tuple[int, Optional[PacketHandler], Optional[SerializedHandler]]]
104+
] = ...,
105+
) -> "Router":
106+
"""
107+
Create or retrieve a per-process singleton Router.
108+
109+
The first call creates the singleton with the given `tx` callback
110+
(no clock and no endpoint handlers). Subsequent calls return another
111+
Router object wrapping the same underlying singleton instance.
112+
113+
Passing a non-None `tx` after the singleton has already been created
114+
will raise a RuntimeError.
115+
"""
116+
...
117+
118+
99119
def __init__(
100120
self,
101121
tx: Optional[TxCallback] = ...,

sedsprintf_rs/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "maturin"
44

55
[project]
66
name = "sedsprintf_rs_2026"
7-
version = "1.0.6"
7+
version = "1.0.8"
88
description = "Rust telemetry and serialization library"
99
authors = [{ name = "Rylan Meilutis", email = "[email protected]" }]
1010
readme = "README.md"

sedsprintf_rs/src/python_api.rs

Lines changed: 167 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ use alloc::{boxed::Box, string::String, sync::Arc as AArc, vec::Vec};
2626
use pyo3::exceptions::{PyRuntimeError, PyValueError};
2727
use pyo3::prelude::*;
2828
use 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

3131
use 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]
220222
impl 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

Comments
 (0)