
Donguru Coming Q1–Q2 2027
A USB-C dongle with a USB hub, a serial port, a power switch and a power meter.
- Hub
- USB, on board
- Serial
- TX/RX flip
- Power
- switch + meter
Boards · Rust firmware · Open source
Boards, Rust firmware and the open-source software that ties them together, from Norway.
draft Copy not yet read by Marketing and PM. Board renders follow.

01Products
Three tools for bring-up, debugging and test, each with a Rust driver, a CLI and a GUI.

A USB-C dongle with a USB hub, a serial port, a power switch and a power meter.

Digital and analog I/O over USB. No programming needed.

USB and Ethernet bridges to isolated CAN FD, with SD card logging, and the adapters around them.
02Donguru · B153
Donguru sits between your computer and a board with a USB-C port. One cable carries USB, a serial console and power you can switch and measure.
A USB hub and a UART on the same cable. TX and RX are flipped for you.
Electronic switches on VBUS and the USB data lines, a power meter, and a mechanical switch for the boot mode.
A fast CLI, donguru or dg, with a Rust driver and Python behind it.
For development, debugging and production flashing of devices with a USB-C port.
03Software
IOWeaver plots what your device says. RockFace debugs the firmware that says it. WireWeaver keeps both sides speaking the same API.

// The API, once, for both sides (ww_uart):
#[ww_trait]
#[claim(exclusive)]
pub trait Uart {
#[counted]
stream!(rx: RxChunk<'i>);
#[credit]
sink!(tx: MaxLen<RefVec<'i, u8>, UART_TX_MAX>);
fn capabilities() -> Capabilities<'i>;
}
// A device with several ports lists them as one array:
ww_impl!(uart[]: ww_uart::Uart)
Protocol analyzer and grapher. Record, decode, plot and send. It ships with rePort and Donguru.
Firmware debugger: logs, stack and CPU use, zero-cost profiling, logs from a fleet. First paid release planned for January 2027.
One API definition for device and host: RPC, streams and properties, backwards and forwards compatible. Open source.
04From the lab
Four pieces from our own bench: Ethernet and USB to CAN FD, a laser scanner and a firmware debugger.

Ethernet to two isolated CAN FD buses, with SD card logging.

USB High Speed to isolated CAN FD, with SD card logging.

Optical inspection of solder paste and placed parts with a scanning green laser line.

A firmware debugger for running systems. First paid release planned for January 2027.
05Open source
The products stand on open crates, all Rust: the API and its wire format, units checked at compile time, and a bare-metal firmware template.
WireWeaver designs no_std APIs for devices: RPC, streams and properties, built from composable API traits and compatible backwards and forwards. It generates the server and the client, blocking or async, for Rust and Python, over USB, Ethernet and CAN bus. Introspection gives dynamic Python clients, and a standard library of traits covers GPIO, CAN, firmware update, logs and more. It is approaching its alpha release.
#[ww_trait]
#[claim(exclusive)]
pub trait Uart {
#[counted]
stream!(rx: RxChunk<'i>);
#[credit]
sink!(tx: MaxLen<RefVec<'i, u8>,
UART_TX_MAX>);
fn capabilities() -> Capabilities<'i>;
}#[ww_trait]
#[claim(exclusive)]
pub trait Uart {
#[counted]
stream!(rx: RxChunk<'i>);
#[credit]
sink!(tx: MaxLen<RefVec<'i, u8>, UART_TX_MAX>);
fn capabilities() -> Capabilities<'i>;
}ShrinkWrap is the binary wire format under WireWeaver. It packs data to 1-bit, 4-bit and 1-byte alignment without compression, so useful messages fit into a CAN frame. Structs, enums with data, strings and vectors of any size work without an allocator, and reading is zero-copy. An older device and newer software read each other's data, and the other way round.
// 1 + 7 or 1 + 10 bits
#[derive_shrink_wrap(ww_repr = u1)]
enum I2cAddress { Seven(U7), Ten(U10) }
#[derive_shrink_wrap]
struct I2cWrite<'i> {
// 0x50: 0 1010000
addr: I2cAddress,
stop: bool, // 1 bit
fast: bool, // 1 bit
retries: Nibble, // 3: 0011
// Some(500): 1 0111110100
timeout_ms: Option<U10>,
data: RefVec<'i, u8>, // ab cd
}
// 50 83 be 80 ab cd 02: 7 bytes, the
// count (02) at the back. repr(C): 20.// 1 + 7 or 1 + 10 bits
#[derive_shrink_wrap(ww_repr = u1)]
enum I2cAddress { Seven(U7), Ten(U10) }
#[derive_shrink_wrap]
struct I2cWrite<'i> {
// 0x50: 0 1010000
addr: I2cAddress,
stop: bool, // 1 bit
fast: bool, // 1 bit
retries: Nibble, // 3: 0011
// Some(500): 1 0111110100
timeout_ms: Option<U10>,
data: RefVec<'i, u8>, // ab cd
}
// 50 83 be 80 ab cd 02: 7 bytes, the count (02) at the back. repr(C): 20.ExactUnits puts a number's unit, scale and storage into its type and checks them at compile time. A conversion is exact or refused, never rounded through a float, and the type is the same bytes as the plain integer, no_std and no alloc. It covers the cases other unit crates leave out: a temperature is a point against an origin, and a voltage gain is a different kind from a power gain.
// exact or refused, never rounded
let t: Micros<u32> = Fixed::new(1500);
assert_eq!(t.to::<Milli, u16>(), None);
// 1500 µs is not a whole number of ms
// V/V and W/W are different kinds,
// and the kind picks the dB formula
type V = Per<Volt, Volt>;
type W = Per<Watt, Watt>;
Fixed::<V, One, f64>::new(2.0).to_db();
// 6.0206 dB: 20·lg 2
Fixed::<W, One, f64>::new(2.0).to_db();
// 3.0103 dB: 10·lg 2
// a threshold in dB: an integer, exact,
// 501 mV/V but 251 mW/W
let v: Fixed<V, Milli, u16> = db!(-6);
let p: Fixed<W, Milli, u16> = db!(-6);
// 0.5 duty is not -3 dB: a Ratio has no dB
// Fixed::<Ratio, One, f64>::new(0.5)
// .to_db();
// error[E0080]: decibels are for W/W or
// V/V, A/A, Pa/Pa: a plain Ratio has no
// level form// exact or refused, never rounded
let t: Micros<u32> = Fixed::new(1500);
assert_eq!(t.to::<Milli, u16>(), None); // 1500 µs is not a whole number of ms
// V/V and W/W are different kinds, and the kind picks the dB formula
type V = Per<Volt, Volt>;
type W = Per<Watt, Watt>;
Fixed::<V, One, f64>::new(2.0).to_db(); // 6.0206 dB: 20·lg 2
Fixed::<W, One, f64>::new(2.0).to_db(); // 3.0103 dB: 10·lg 2
// a threshold in dB: an integer, exact, 501 mV/V but 251 mW/W
let v: Fixed<V, Milli, u16> = db!(-6);
let p: Fixed<W, Milli, u16> = db!(-6);
// 0.5 duty is not -3 dB: a Ratio has no dB
// Fixed::<Ratio, One, f64>::new(0.5).to_db();
// error[E0080]: decibels are for W/W or V/V, A/A, Pa/Pa: a plain Ratio has no
// level formA bare-metal firmware template with ruggedizing, debugging and logging built in, and optional embassy, RTIC and HAL support. The template is an agent skill that generates a firmware for a chip, with its errata notes, blink codes on the status LED when it cannot start, and a clock a host keeps in step across boards. Most STM32s and a few MCUs from other vendors are covered.
use bedrock_led::code::*;
use bedrock_sync::{Config, Sync};
use cortex_m::asm::delay;
// A firmware that will not start
// says why on its status LED, then
// waits in the ROM bootloader.
if !guard.runs_board() {
let pause = |n| delay(n * KHZ);
blink(Code::UnknownRework,
led, pause);
reboot_into_rom_bootloader();
}
// One clock on all boards, steered
// by the host over WireWeaver:
// Acquiring, Locked within 2 ms.
// `ww sync` locks the boards in
// 1.2-1.6 s. On Donguru it costs
// about 1.5 KB of flash.
static SYNC: Sync =
Sync::new(Config::DEFAULT);
// clock.steer, once a second
let r = SYNC.with(|clock, now| {
server.steer(clock, now, &p)
});
// all boards act at the same µs
SYNC.wait_until_shared(t).await?;use bedrock_led::code::*;
use bedrock_sync::{Config, Sync};
use cortex_m::asm::delay;
// A firmware that will not start says why on its status LED, then
// waits in the ROM bootloader.
if !guard.runs_board() {
let pause = |n| delay(n * KHZ);
blink(Code::UnknownRework, led, pause);
reboot_into_rom_bootloader();
}
// One clock on all boards, steered by the host over WireWeaver:
// Acquiring, Locked within 2 ms. `ww sync` locks the boards in
// 1.2-1.6 s. On Donguru it costs about 1.5 KB of flash.
static SYNC: Sync = Sync::new(Config::DEFAULT);
// clock.steer, once a second
let r = SYNC.with(|clock, now| { server.steer(clock, now, &p) });
// all boards act at the same µs
SYNC.wait_until_shared(t).await?;When logging is not an option, count. Each count statement gets its own counter in a RAM array, through a linker trick like defmt's, without a critical section, so it is safe in any interrupt; on Cortex-M3 and up a counter shared with an interrupt never loses an update. The cnt tool reads the counters over the debug probe, once, as a watch or in a TUI, with the defmt log beside them.
// one RAM counter per call site,
// no critical section, safe in an IRQ
fn high_frequency_irq() {
let r = do_something();
cnt::cnt_if!(r.is_err(),
unpack_errors: u64);
}
fn process_packet() {
cnt::cnt!(packet_count: u32);
}
// on the PC, over the debug probe:
// $ cnt tui --chip STM32H533RE// one RAM counter per call site,
// no critical section, safe in an IRQ
fn high_frequency_irq() {
let r = do_something();
cnt::cnt_if!(r.is_err(), unpack_errors: u64);
}
fn process_packet() {
cnt::cnt!(packet_count: u32);
}
// on the PC, over the debug probe:
// $ cnt tui --chip STM32H533RE06Stay in the loop
Donguru launches on Crowd Supply. Its sign-up list opens with the pre-launch page; until then, send a line and you hear first. draft
Contact
Questions about the boards, the software or a project of your own: hello@vhrd.tech.
Engineering services are rare right now. What I do and how I work