twinleaf-tools 2.3.0

Tools for the Twinleaf I/O protocol for reading data from Twinleaf quantum sensors.
Documentation
//! Background-thread worker for `RpcClient` calls, for TUI hosts whose event
//! loops can't block on RPC round-trips.
//!
//! Hosts call [`spawn_rpc_worker`] once, get a request/response channel pair,
//! send [`RpcWorkerReq`] variants (fetch RPC list, execute RPC) and consume
//! [`RpcWorkerResp`] from the response side. The palette widget itself has no
//! awareness of this module — hosts wire palette events to worker requests.

use crossbeam::channel::{self, Receiver, Sender};
use twinleaf::device::{util, DeviceRoute, RpcClient, RpcList, RpcValue};

use crate::tui::rpc_palette::{RpcReq, RpcResp};

pub enum RpcWorkerReq {
    FetchList(DeviceRoute),
    Execute(RpcReq),
}

pub enum RpcWorkerResp {
    List(RpcList),
    ListErr { route: DeviceRoute, error: String },
    RpcResult(RpcResp),
}

pub fn exec_rpc(client: &RpcClient, req: &RpcReq) -> Result<String, String> {
    // Honor an explicit `-t` override; otherwise infer from metadata.
    let kind = req.req_type.unwrap_or_else(|| {
        let meta = req.meta.or_else(|| {
            client
                .rpc::<&String, u16>(&req.route, "rpc.info", &req.method)
                .ok()
        });
        util::resolve_arg_type(meta, &req.method)
    });

    let payload = if let Some(ref s) = req.arg {
        util::rpc_encode_arg(s, &kind).map_err(|e| e.to_string())?
    } else {
        Vec::new()
    };

    let reply_bytes = client
        .raw_rpc(&req.route, &req.method, &payload)
        .map_err(|e| e.to_string())?;

    if reply_bytes.is_empty() {
        return Ok("OK".to_string());
    }

    // `-T` overrides the reply type; default to decoding as the request type.
    let reply_kind = req.rep_type.unwrap_or(kind);
    let value = util::rpc_decode_reply(&reply_bytes, &reply_kind).map_err(|e| e.to_string())?;

    Ok(match &value {
        RpcValue::Str(s) => format!("\"{}\" {:?}", s, s.as_bytes()),
        RpcValue::Bytes(b) => format!("{:?}", b),
        other => format!("{}", other),
    })
}

/// Spawn a worker thread that owns the given [`RpcClient`]. The returned
/// channel pair is suitable for embedding in a `crossbeam::select!` loop.
pub fn spawn_rpc_worker(client: RpcClient) -> (Sender<RpcWorkerReq>, Receiver<RpcWorkerResp>) {
    let (req_tx, req_rx) = channel::unbounded::<RpcWorkerReq>();
    let (resp_tx, resp_rx) = channel::unbounded::<RpcWorkerResp>();

    std::thread::spawn(move || {
        while let Ok(req) = req_rx.recv() {
            let resp = match req {
                RpcWorkerReq::FetchList(route) => match client.rpc_list(&route) {
                    Ok(list) => Some(RpcWorkerResp::List(list)),
                    Err(err) => Some(RpcWorkerResp::ListErr {
                        route,
                        error: err.to_string(),
                    }),
                },
                RpcWorkerReq::Execute(rpc_req) => {
                    let result = exec_rpc(&client, &rpc_req);
                    Some(RpcWorkerResp::RpcResult(RpcResp { result }))
                }
            };
            if let Some(resp) = resp {
                if resp_tx.send(resp).is_err() {
                    return;
                }
            }
        }
    });

    (req_tx, resp_rx)
}