RUSP — the OpenSource USP toolkit written in Rust turns 1.0

Introduction
I
n this blog post, Daniel Egger—Principal Software Engineer at Axiros and co-author of the USP standard—shares the story behind RUSP, Axiros’ Rust-based Open Source toolkit for USP, which has now reached version 1.0. As a key contributor to the USP standard and lead on Axiros’ USP products, Daniel walks you through RUSP’s evolution—from a holiday side project in 2018 to a powerful, scriptable toolkit used in production.

What is USP
USP (or TR369) is the successor to the very popular but out-of-date TR-069 standard for managing and monitoring customer devices in the telco industry. It aims to improve on every single aspect, however, there’s one catch: whereas TR-069 used (arguably) readable XML to encode its data, USP switched to using ProtoBuf, an efficient schema-based binary encoding, impossible to deal with by hand.

Why Rust
Rust is a memory-safe and ultra-efficient compiled programming language that we decided to use as the foundation of our USP Controller product. To support USP ProtoBuf serialisation in the Controller, we needed a reliable library for encoding and decoding. But since just having a library that can work with USP data is not exactly useful on its own, we didn’t stop there and also created a command line frontend.

A short history
After a few (unsuccessful) attempts to work with USP Records and Messages, I used the quiet time during the Winter festivities in 2018 to come up with the first prototype. It was clunky and didn’t support a lot of things but I could encode and decode my first USP data and decided to publish it as true OpenSource in version 0.1 under a BSD license on GitHub and also add it to the Rust package library crates.io. From there, we added more features—driven largely by the evolving needs of the USP Controller, which benefitted from continuous tweaks and optimisations. As our ecosystem matured, releases became somewhat infrequent and mostly focused on supporting newer USP versions and making small quality-of-life improvements. Until, in 2024, we decided to kick things up a notch and added a builder-style API next to the clunky fixed API with ridiculously complex nested types and in the latest version also added Rhai scripting support which is going to be the major milestone allowing us finally call it 1.0.

Tell me more about Rhai
Thanks for asking. Rhai is a scripting language written in Rust (of course!) with a Rust-like syntax and feature set. What it allows us to do is, to take our Rust library and write a convenient wrapper, allowing us to use all of the features from a script. I guess you can see where this is going? Rhai also allows us to embed its interpreter into our applications, making our USP products easily scriptable without compromising performance; so, that’s what we did. It also allowed us to replace the rusp command line tool and its finicky yet incomplete syntax by a new application called rusp-run which uses Rhai scripts instead, giving us a whole range of new functionality at one, because we can actually program it rather than just providing a reduced set of (sometimes very complex!) command line arguments.

What can we actually do with this
Very clearly the library allows us to build extremely efficient products like the USP Controller and all the related machinery by allowing us to have a clean and fast way of extracting information out of USP Records and Messages or creating new ones. With the scriptability we can also easily embed a Rhai interpreter into our applications and allow the customer not just to change the behavior via config and external interfaces but also by supplying Rhai scripts which can directly manipulate those without requiring changes to product code —which is huge.

However, the most important aspect is that we now offer a new tool that allows anyone to fire up a Rhai interpreter with rusp functionality and create USP data like a pro or inspect captured USP Records with ease.

Use cases
Allow me to show you what is possible with rusp-run. How this can be used with an embedded interpreter is left as an imaginary exercise to the reader.

Encode a USP Msg to stdout, pipe it into a reader and print it as JSON

# rusp-run -s 'rusp::msg_builder()
    .with_msg_id("Foo")
    .with_body(rusp::getsupportedprotocol_builder("1.3,1.4").build())
    .build()
    .print_protobuf()' | rusp-run -s 'print(rusp::read_msg());'
{
  "Header": {
    "msg_id": "Foo",
    "msg_type": "GET_SUPPORTED_PROTO"
  },
  "Body": {
    "Request": {
      "GetSupportedProtocol": {
        "controller_supported_protocol_versions": "1.3,1.4"
      }
    }
  }
}

Create a USP Record and pretty print it as JSON for inspection

# rusp-run -s 'let body = rusp::get_builder()
    .with_params(["Device."])
    .with_max_depth(2)
    .build();
let msg = rusp::msg_builder()
    .with_msg_id("Example")
    .with_body(body)
    .build();
let record = rusp::record_builder()
    .with_version("1.4")
    .with_to_id("doc::to")
    .with_from_id("doc::from")
    .with_no_session_context_payload(msg)
    .build();
print (record);'
{
  "version": "1.4",
  "to_id": "doc::to",
  "from_id": "doc::from",
  "payload_security": "PLAINTEXT",
  "mac_signature": [],
  "sender_cert": [],
  "payload": {
    "Header": {
      "msg_id": "Example",
      "msg_type": "GET"
    },
    "Body": {
      "Request": {
        "Get": {
          "param_paths": [
            "Device."
          ],
          "max_depth": 2
        }
      }
    }
  }
}

Decode a USP Record captured from a real device

# rusp-run -s 'print(load_record("out-2025-06-16T23:45-2.pb"));'
{
  "version": "1.4",
  "to_id": "fqdn::usp.axiros.com",
  "from_id": "self:axiros:BCD074ADD9CB",
  "payload_security": "PLAINTEXT",
  "mac_signature": [],
  "sender_cert": [],
  "payload": {
    "Header": {
      "msg_id": "not-rbmynLlIx5",
      "msg_type": "ERROR"
    },
    "Body": {
      "Request": {
        "Notify": {
          "subscription_id": "boot",
          "send_resp": true,
          "event": {
            "obj_path": "Device.",
            "event_name": "Boot!",
            "params": {
              "CommandKey": "",
              "FirmwareUpdated": "0",
              "Cause": "LocalReboot",
              "ParameterMap": "{\"Device.DeviceInfo.SoftwareVersion\":\"2025.1\",\"Device.DeviceInfo.ModelName\":\"AXACT.DUAL\"}"
            }
          }
        }
      }
    }
  }
}

Create multiple USP Msgs

# rusp-run -s 'let body = rusp::getsupportedprotocol_builder("1.4").build();
    for num in 1..=10 {
        rusp::msg_builder()
            .with_msg_id("Foo" + rand::rand())
            .with_body(body)
            .build()
            .save_protobuf("file" + num + “.pb")
    }'
# ls file*.pb
file1.pb  file10.pb file2.pb  file3.pb  file4.pb  file5.pb  file6.pb  file7.pb  file8.pb  file9.pb

How to get started with rusp
Interested? Great.

You can find all sources in our GitHub repositoryfeel free to report any issues or open PRs if you feel so inclined, and if you find it useful, please leave a star.

The easiest way to get started is by installing the binaries, which can be done by calling
# cargo install rusp
if you have Rust already installed, otherwise please check here first.

The latest information can always be found at https://crates.io/crates/rusp-lib and https://crates.io/crates/rhai-rusp , including usage examples and links to the comprehensive documentation (also containing even more examples).

We hope you find this interesting and useful and would like to hear back from you if you do!


Written by Daniel Egger
Daniel is a principal Software Engineer and Product Owner of all USP related products at Axiros. He is also one of the authors of the USP specification and a Program Stream Lead for Data Modelling (TR-181, etc.) in the Broadband Forum.

Related Info:
Knowledge Base: What is TR-369?
Products: AXACT | Embedded Connectivity and AX USP | Controller

Next
Next

TR-069 Lessons Learned: Unlock benefits with USP