Sign up for your FREE personalized newsletter featuring insights, trends, and news for America's Active Baby Boomers

Newsletter
New

Need Some Help Architecting A Library

Card image cap

Hey everyone,

I could use some insight here. I've been bashing my head against this brick wall for a while now without getting anywhere.

For some background, for the last couple of years I've been developing a TypeScript/Node.js library that allows communicating with wireless devices via a serial modem while also providing high level abstractions and business logic to make writing applications based on this library easy.
I'm now trying to create a Rust version of this to be able to run on more constrained devices, or even embedded systems.

There are a few challenges I need to solve for this:

  • The library needs to continuously listen to the serial port to be able to handle incoming frames
  • The library needs to be able to autonomously perform repeated or scheduled actions
  • The library needs an API for consumers to send commands, retrieve information, and handle frames that are destined for the application
  • Command/task executions are highly asynchronous in nature, often including multiple round trips via the serialport to devices and back. So being able to use async internally would be greatly beneficial.

In Node.js, this is easy - the event loop handles this for me.

I've had a first version of this running in Rust too, but it ended up being messy: multiple Tokio tasks running their own loops, passing a bunch of messages around via channels. Tons and tons of Arcs everywhere. Even the most simple tasks had to be async.

I've started reworking this with the sans-io pattern in mind. The API for interacting with the serial port now looks similar to the example at https://www.firezone.dev/blog/sans-io, where the consumer provides a serialport implementation and drives the state machine handing the interactions. This can happen purely sync or async - the consumer decides. For practical reasons, this should happen in a background task or thread.
Here's the sync version - the consumer sets up logging, opens the serial port, creates the serial adapter (state machine) and runs it until the end:

pub struct Runtime { logger: BaseLogger, port: TTYPort, serial: SerialAdapter, } impl Runtime { pub fn new(logger: BaseLogger, port: TTYPort, serial: SerialAdapter) -> Self { Self { logger, port, serial, } } pub fn run(&mut self) { let mut inputs: VecDeque = VecDeque::new(); loop { // Read all the available data from the serial port and handle it immediately match self.port.read(self.serial.prepare_read()) { // ✂️ error handling, passing data to the serial adapter } // If the adapter has something to transmit, do that before handling events while let Some(frame) = self.serial.poll_transmit() { self.port .write_all(&frame) .expect("failed to write to serialport"); } // Check if an event needs to be handled if let Some(event) = self.serial.poll_event() { // ✂️ handle events } // Pass queued events to the serial adapter if let Some(input) = inputs.pop_front() { self.serial.handle_input(input); continue; } // Event loop is empty, sleep for a bit thread::sleep(Duration::from_millis(10)); } } } 

Thinking about how to integrate this with the asynchronous and autonomous nature of the rest of the library is where I'm stuck.

Does anyone have tips or maybe even examples where a similar thing has been achieved?(

submitted by /u/AlCalzone89
[link] [comments]


Recent