Code Monkey home page Code Monkey logo

serial-keel's Introduction

Serial Keel Build

Serial Keel

A server which helps ease working with TTYs.

boat

Thanks DALL-E! The keel does not look very serial-y, though.

Features:

  • Server continuously monitors TTYs

    • No messages lost!
  • An endpoint (e.g. /dev/ttyACM0) can have any number of listeners (called observers)

    • This means even though an endpoint is in use, others may observe what's going on
  • Clients can await exclusive control over an endpoint

    • This provides write access
  • Endpoints can be put into logical groups

    • Exclusive access now implies exclusivity over the whole group
  • Any number of clients can wait for write access- the server automatically queues them (FIFO)

  • Endpoints can be mocked

    • Write to it to instruct it to send back messages the same way a real device would
    • Allows separating TTY message logic from actual devices for rapid prototyping
  • Communication between server and client is done via the WebSocket Protocol

    • Allows the server to work independently of which language the client is written in, since all request-responses are sent as JSON messages over the websocket
    • Why a web socket and not a TCP or UDP socket? Web sockets are a higher level abstraction and allows patterns such as:
# Python
async for message in websocket:
    # Do thing with message,
    # allow other async code to progress while waiting
// Rust
while let Some(Ok(msg)) = websocket.next().await {
    // Do thing with message,
    // allow other async code to progress while waiting
}
// Javascript
websocket.addEventListener('message', (event) => {
    // Do thing with message
});

Running a server

Start the server. Choose one of:

  • cargo r --bin serial-keel, or cargo r --release --bin serial-keel (see cargo features for additional install configurations)
  • Install, e.g.: cargo install --path core, then run it: serial-keel
    • The core is the server.
  • Precompiled: ./bin/serial-keel (TODO: Update this bin. TODO: The build-musl fails, investigate)

If you have installed serial keel, please use serial-keel help to explore what's available.

Use the environment variable RUST_LOG to control log verbosity, e.g. export RUST_LOG=warn or export RUST_LOG=debug.

Note: Without using a configuration file Serial Keel will not open any TTYs. It might still be useful since mock endpoints can be created at runtime. See the next section for using configuration files.

Using a configuration file

Run serial-keel examples config to see an example configuration file. You can store the output of this as my-config.ron to get started.

A short summary of the configuration file is:

  • Allows choosing exactly which TTYs to open
  • Allows grouping endpoints together
  • Allows giving labels to groups and endpoints

Webserver HTTP endpoints

Assuming you have a server running at localhost:3123.

localhost:3123/config

curl -X GET localhost:3123/config

This dumps the configuration the server is running with. Useful to see endpoints, groups, and labels.

Currently it dumps it in the "native" format- RON.

localhost:3123/version

curl -X GET localhost:3123/version

This dumps the version the server version as definied in the project's manifest file. Serialized as JSON (although it's just a string, e.g. "0.1.0").

Cargo Features

mocks-share-endpoints

If a client connects and asks for control of mock-foo, then this endpoint is created on the spot. This is to support mocking and not needing a separate API just to create mock endpoints.

However, when we want to test clients trying to queue over the same resources, we need to make mock endpoints shared. This means two clients trying to access mock-foo, one is granted access and the other is queued.

How it works

Concepts

Message Format

All communication between clients and a server is done via serialized JSON messages.

Run serial-keel examples to get a list of examples you can run to show how these messages look like.

Client

The actor initiating a websocket connection to some active server. Sends requests to the server, which replies with responses.

Server

Continuously listens to TTYs. Serves clients over websockets. The clients typically observe endpoints to receive serial messages, and/or control endpoints to send serial messages.

Endpoint

A thing which may produce serial messages and accepts being written to.

For example the endpoint /dev/ttyACM0 or COM0 represents real TTYs which can read and write messages.

Endpoints may also be mocked, and thus have any name e.g. mock-1 or specific-mocked-device-30.

Control

Having "control" over an endpoint means having exclusive access to it, which implies write access.

Observer

Observing an endpoint means the server will send serial messages to the client when messages arrive on that observed endpoint. All clients have observe rights, even over endpoints controlled by other clients.

Group

A group is a logical collection of endpoints. Endpoints which are grouped together are gained write access to as a group. This means a client gaining access to one member of a group simultaneously gets access to other members as well.

Use this when there is a dependence between endpoints in some way. For example, some embedded devices are able to expose several TTYs to the host operating system. These are natural to group together.

Examples

Client TTY session

Shows a client observing a single TTY endpoint. The concept works the same for more endpoints.

  • The TTY may at any point produce a message to the server
  • The server will forward messages to any observer(s)
  • Clients starting to observe will receive messages from that point on.
┌────────┐                ┌────────┐      ┌────────────┐
│ client │                │ server │      │/dev/ttyACM0│
└───┬────┘                └────┬───┘      └─────┬──────┘
    │                          │                │
    │                          │                │
    │                          │    "LOREM"     │
    │              no observers│◄───────────────┤
    │                    x─────┤                │
    │                          │                │
    │                          │                │
    │  observe("/dev/ttyACM0") │                │
    ├─────────────────────────►│                │
    │                          │                │
    │                          │                │
    │                          │                │
    │   message("LOREM")       │    "LOREM"     │
    │   from "/dev/ttyACM0"    │◄───────────────┤
    │◄─────────────────────────┤                │
    │                          │                │
    │   message("IPSUM")       │    "IPSUM"     │
    │   from "/dev/ttyACM0"    │◄───────────────┤
    │◄─────────────────────────┤                │
    │                          │                │
    │   message("HELLO")       │    "HELLO"     │
    │   from "/dev/ttyACM0"    │◄───────────────┤
    │◄─────────────────────────┤                │
    │                          │                │
    │                          │                │
   ─┴─                         │                │
client                         │                │
disconnects                    │                │

Client mock session

This example shows the message passing between a client and server for a mock session.

When a user asks to control a mock, the mock is created on the spot. The mock endpoint (here mock-foo) is also unique for this user. The mock cannot know what to emulate without being instructed on what to do. Therefore write commands to a mock is echoed back, but split by lines.

This allows writing a whole text file which is then sent back line by line.

When the user disconnects the mock is removed leaving no state.

Note: Requests from the client are always responded right away, but messages on an endpoint are sent asynchronously to the client.

┌────────┐                ┌────────┐
│ client │                │ server │
└───┬────┘                └────┬───┘
    │                          │
    │                          │
    │     control("mock-foo")  │
    ├─────────────────────────►│
    │                          ├───────────┐
    │                          │ initialize│
    │                          │"mock-foo" │
    │     control granted      │◄──────────┘
    │◄─────────────────────────┤
    │                          │
    │                          │
    │     observe("mock-foo")  │
    ├─────────────────────────►│
    │            ok            │
    │◄─────────────────────────┤
    │                          │
    │                          │
    │                          │
    │write("LOREM\nIPSUM\nFOO")│
    │  to endpoint "mock-foo"  │
    │                          │
    ├─────────────────────────►│
    │                          ├───────────┐
    │                          │ "mock-foo"│
    │                          │ receives  │
    │                          │ text      │
    │        write ok          │◄──────────┘
    │◄─────────────────────────┤
    │                          │
    │       message("LOREM")   │
    │       from "mock-foo"    │
    │◄─────────────────────────┤
    │                          │
    │       message("IPSUM")   │
    │       from "mock-foo"    │
    │◄─────────────────────────┤
    │                          │
    │       message("FOO")     │
    │       from "mock-foo"    │
    │◄─────────────────────────┤
    │                          │
    │                          │
   ─┴─                         │
client                         │
disconnects                    │
                       remove  │
                     "mock-foo"│
                               │

Labelled group control

This example is a bit more involved.

  • Three clients
  • Four endpoints
  • Endpoints are grouped into two groups
  • The groups share an arbitrary label "device-combo"

The concept shown here is that clients may ask to control any endpoint (or group) which matches some label. Labels are set via the server configuration file (see using a configuration file).

The other concept shown is that control is tied to the connection of the client. When a client disconnects control is released and the next in queue (if any) gets control.

See below the diagram for an explanation of the numbered events in this example.

┌─────────┐ ┌─────────┐ ┌─────────┐             ┌────────┐ ┌────────────────────────────────┐ ┌────────────────────────────────┐
│ client1 │ │ client2 │ │ client3 │             │ server │ │ group1, label: "device-combo"  │ │ group2, label: "device-combo"  │
└───┬─────┘ └───┬─────┘ └───┬─────┘             └────┬───┘ │                                │ │                                │
    │           │           │ control-any(           │     │ ┌────────────┐  ┌────────────┐ │ │ ┌────────────┐  ┌────────────┐ │
    │ 1.        │           │   "device-combo")      │     │ │/dev/ttyACM0│  │/dev/ttyACM1│ │ │ │/dev/ttyACM3│  │/dev/ttyACM4│ │
    ├───────────┼───────────┼───────────────────────►│     │ └─────┬──────┘  └─────┬──────┘ │ │ └─────┬──────┘  └─────┬──────┘ │
    │           │           │  control granted       │     │       │               │        │ │       │               │        │
    │ 2.        │           │  (group1)              │     └───────┼───────────────┼────────┘ └───────┼───────────────┼────────┘
    │◄──────────┼───────────┼────────────────────────┤             │               │                  │               │
    │           │           │                        │             │               │                  │               │
    │           │           │                        │             │               │                  │               │
    │           │           │                        │             │               │                  │               │
    │           │           │                        │             │               │                  │               │
    │           │           │                        │             │               │                  │               │
    │           │           │  control-any(          │             │               │                  │               │
    │           │ 3.        │    "device-combo")     │             │               │                  │               │
    │           ├───────────┼───────────────────────►│             │               │                  │               │
    │           │           │  control granted       │             │               │                  │               │
    │           │ 4.        │  (group2)              │             │               │                  │               │
    │           │◄──────────┼────────────────────────┤             │               │                  │               │
    │           │           │                        │             │               │                  │               │
    │           │           │                        │             │               │                  │               │
    │           │           │                        │             │               │                  │               │
    │           │           │                        │             │               │                  │               │
    │           │           │  control-any(          │             │               │                  │               │
    │           │           │    "device-combo")     │             │               │                  │               │
    │           │        5. ├───────────────────────►│             │               │                  │               │
    │           │           │                        │             │               │                  │               │
    │           │           │  queued                │             │               │                  │               │
    │           │        6. │◄───────────────────────┤             │               │                  │               │
    │           │           │                        │             │               │                  │               │
    │           │           │                        │             │               │                  │               │
    │           │           │                        │             │               │                  │               │
    │           │           │                        │             │               │                  │               │
    │           │           │  write("hello world")  │             │               │                  │               │
    │           │ 7.        │  to "/dev/ttyACM3"     │  "hello     │               │                  │               │
    │           ├───────────┼───────────────────────►│   world"    │               │                  │               │
    │           │           │                        ├─────────────┼───────────────┼─────────────────►│               │
    │           │ 8.        │  write ok              │             │               │                  │               │
    │           │◄──────────┼────────────────────────┤             │               │                  │               │
    │           │           │                        │             │               │                  │               │
    │           │           │                        │             │               │                  │               │
    │       9. ─┴─          │                        │             │               │                  │               │
    │        client         │                        │             │               │                  │               │
    │        disconnects    │                        │             │               │                  │               │
    │        (group2 now    │                        │             │               │                  │               │
    │         available)    │                        │             │               │                  │               │
    │                       │                        │             │               │                  │               │
    │                       │                        │             │               │                  │               │
    │                       │  control granted       │             │               │                  │               │
    │                       │  (group2)              │             │               │                  │               │
    │                   10. │◄───────────────────────┤             │               │                  │               │
    │                       │                        │             │               │                  │               │
    │                       │                        │             │               │                  │               │
 11.│                   12. │                        │             │               │                  │               │
   ─┴─                     ─┴─                       │             │               │                  │               │
client                  client                       │             │               │                  │               │
disconnects             disconnects
(group1 now             (group2 now
 available)              available)

Explanation of events:

  1. client1 asks to control anything matching the label "device-combo".
  2. The server grants control to all endpoints in group1 since it matched the label and was available. Note that the server might as well have given access to group2 here.
  3. client2 asks control over any "device-combo" too.
  4. group2 matched and was available and is granted.
  5. client3 asks control over any "device-combo".
  6. The server sees two groups matching, but all are taken. It queues the client.
  7. client2 has control over endpoints in group2. The server sent information about those endpoints (not shown), but client2 therefore knows /dev/ttyACM3 is controllable. client2 writes a message to /dev/ttyACM3.
  8. The server saw that client2 had write access to /dev/ttyACM3. It wrote the message to the endpoint, i.e. it put the message "on wire". It therefore sends a "write ok" message back to the client.
  9. client2 leaves. The server notices and frees the resources client2 had, which means group2 is now available again.
  10. The server saw that a resource matching what client3 wants is now available, and grants client3 control over it.
  11. client1 leaves (without ever using its resources just to make the example simpler). This frees group1.
  12. client3 leaves. This frees group2.

Python client

There is an async Python client for Serial Keel. See the README for the serial-keel python client first before continuing below.

Install the serial keel python client libraries with pip install serialkeel

To build the serialkeel python client locally, install the build pip package, cd into the ./py directory and run

python3 -m build

This should create a .whl file in a new ./py/dist/ folder. Install it with pip install ./py/dist/*.whl

Pytest via vscode

With the serialkeel python package installed, and if you tell vscode to use Pytest, we can get a nice interface:

TODO: Not displayed in rust doc

vscode image

To enable this, add .vscode/settings.json to this workspace and add these contents:

{
  "python.testing.pytestArgs": ["py"],
  "python.testing.unittestEnabled": false,
  "python.testing.pytestEnabled": true
}

You can run all tests, individual tests, or debug tests like this.

Rust client

Here is a very simple mock session using the Rust client.

rust session

Command line example client

This is not meant to be practical- it's just to show that Serial Keel is client agnostic.

Serial Keel communicates over websockets. The server and client sends JSON messages back and forth. Therefore we can have a session on the command line.

This uses websocat.

websocat session

JavaScript example client

There is no real JavaScript client. Just a short example in JavaScript.

javascript session

Observability Frontend

There is a WIP GUI which can be started via (from the root folder) cargo r --bin frontend-iced.

iced image

It's quite barebones right now, but the goal is the be able to attach to any running Serial Keel instance and see the user events and also the raw logs from each endpoint in real time.

Serial Keel as a systemd service

You can setup Serial Keel to run as a systemd service on startup. In scripts/systemd, you'll find a template .service file and an install_serial_keel.sh script.

Running the install script with the template file, specified branch (defaults to main if not specified) and path to the config file will create a user service. It will also build serial-keel and install it.

You can invoke this script as below:

./scripts/systemd/install_serial_keel.sh <branch-name> <path-to-config-file>
  • Once done, the serial-keel service should be running. Running systemctl --user status serial-keel.service should let you know whether the service started successfully.

  • Logs are accessed via journalctl. Check man journalctl for usage. Here is an example:

Example:

journalctl --since "2 days ago" --user-unit serial-keel --output=cat
- You can access the latest logs of the serial-keel service with:

### **Note**
- The `check_and_upgrade_serial_keel.sh` is executed every time the serial-keel service restarts,
which checks for new changes on remote and performs an upgrade if necessary.

serial-keel's People

Contributors

stephen-nordic avatar torsteingrindvik avatar

Stargazers

 avatar  avatar

Watchers

 avatar  avatar

serial-keel's Issues

Investigate panics

image

These three locations:

core/src/control_center.rs:412:14
core/src/peer.rs:164
core/src/peer.rs:151

Figure out which assumption is bad, and resolve.

gRPC

This might fix a problem we don't have, but we should consider it long term.

gRPC:

  • Standardized errors, which is more predictable
  • Standardized health checks, something we don't have yet
  • Makes it clearer which messages may pass between servers and clients due to these being clearly defined in files
  • Should increase perf due to not using a self-described format
  • Serial Keel now is only type-safe in both ends if the Rust client is used. The server always is, but e.g. the Python client may be out of sync if messages change format and the Py client is not updated to match. With gRPC and proto files this should be better in more languages (depending on the tooling of the lang)

Make `mocks-share-endpoints` a default feature

Since tests use this feature and break if it's missing, it feels like it should be default.
This way, if a user really needs unique per-user mocks, they can disable the feature.

  • Make it default
  • Change README to reflect that it's now opt-out

Consider allowing "binary streams" type usage

Right now SK clients are relatively string-oriented.
Both the Rust and Py clients await string-like types.

Add some configurability on the clients to allow byte streams instead when we get a use case for it.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.