Code Examples

The following examples demonstrate practical usage of the freemdu protocol library for low-level diagnostics, reverse-engineering, and security research.

All examples provided below are included in the repository under the protocol/examples/ directory. You can execute them directly via Cargo. Ensure your hardware adapter is flashed in bridge mode and connected via USB.

cargo run --all-features --example <EXAMPLE_NAME>

1. Brute-Forcing Unlock Keys (find_keys.rs)

Context: Miele does not publish the memory access keys for their appliances. Every unique Software ID has a specific pair of 16-bit security keys: one for read access, and one for full (write/execute) access.

This script demonstrates how to sequentially brute-force the keys space ($0x0000$ to $0xFFFF$). It attempts an unlock, verifies success by trying to read memory, and handles the necessary timeouts and retries.

use freemdu::{Interface, serial::Port};
use std::{error::Error, thread, time::Duration};
use tokio::time;

const UNLOCK_TIMEOUT: Duration = Duration::from_millis(500);
const ERROR_RETRY_DELAY: Duration = Duration::from_secs(4);
const CHECK_TIMEOUT: Duration = Duration::from_millis(100);

async fn find_read_access_key(intf: &mut Interface<Port>) -> Result<u16, Box<dyn Error>> {
    for i in 0x0000..=0xffff {
        println!("Attempting Read Access Key: {i:04x}");

        // Attempt connection with timeout to handle unresponsive states
        while let Err(err) = time::timeout(UNLOCK_TIMEOUT, async {
            intf.query_software_id().await?;
            intf.unlock_read_access(i).await
        })
        .await
        {
            eprintln!("Protocol Error: {err}");
            thread::sleep(ERROR_RETRY_DELAY);
        }

        // Verification: If we can successfully read memory 0x0000, the key is correct
        if let Ok(Ok(_)) = time::timeout(CHECK_TIMEOUT, intf.read_memory::<u8, _>(0x0000)).await {
            return Ok(i);
        }
    }

    Err("Failed to discover read access key".into())
}

async fn find_full_access_key(
    intf: &mut Interface<Port>,
    read_key: u16,
) -> Result<u16, Box<dyn Error>> {
    for i in 0x0000..=0xffff {
        println!("Attempting Full Access Key: {i:04x} (Using Read Key: {read_key:04x})");

        while let Err(err) = time::timeout(UNLOCK_TIMEOUT, async {
            intf.query_software_id().await?;
            // Read access must be unlocked before attempting full access
            intf.unlock_read_access(read_key).await?;
            intf.unlock_full_access(i).await
        })
        .await
        {
            eprintln!("Protocol Error: {err}");
            thread::sleep(ERROR_RETRY_DELAY);
        }

        // Verification: If we can issue the halt command, full access is granted
        if let Ok(Ok(())) = time::timeout(CHECK_TIMEOUT, intf.halt()).await {
            return Ok(i);
        }
    }

    Err("Failed to discover full access key".into())
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    env_logger::init();

    let port = freemdu::serial::open("/dev/ttyACM0")?;
    let mut intf = Interface::new(port);

    println!("Starting brute-force attack...");
    let read_key = find_read_access_key(&mut intf).await?;
    println!("--> Found Read Access Key: {read_key:04x}");

    let full_key = find_full_access_key(&mut intf, read_key).await?;
    println!("--> Found Full Access Key: {full_key:04x}");

    Ok(())
}

2. Dumping Volatile Memory (dump_memory.rs)

Context: Once an appliance is unlocked, you can read the internal RAM state to reverse-engineer where specific sensor values (like temperature or program phase) are mapped in memory. This script extracts data in 128-byte chunks and appends it safely to a binary file.

Best Practice: Dumping memory over a 2400-baud serial line is slow. This script automatically checks the existing file size and resumes the dump using the offset variable if it gets interrupted.

use std::{
    error::Error,
    fs::OpenOptions,
    io::{Seek, SeekFrom, Write},
};

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    env_logger::init();

    // Adjust these addresses based on the target microcontroller datasheet
    const START: u32 = 0x0000_0000;
    const END: u32 = 0x0000_ffff;

    let mut port = freemdu::serial::open("/dev/ttyACM0")?;
    // Using the high-level Device abstraction automatically handles the unlocking phase
    let mut dev = freemdu::device::connect(&mut port).await?;

    let mut file = OpenOptions::new()
        .create(true)
        .append(true)
        .open("memory_dump.bin")?;

    // Resume dumping process if previously interrupted
    let offset: u32 = file.seek(SeekFrom::End(0))?.try_into()?;

    for addr in (START + offset..=END).step_by(0x80) {
        println!("Reading RAM chunk at address {addr:08x}");

        // Fall back to the low-level interface for raw reading
        let data: [u8; 0x80] = dev.interface().read_memory(addr).await?;

        file.write_all(&data)?;
    }

    Ok(())
}

3. Dumping Non-Volatile EEPROM (dump_eeprom.rs)

Context: The EEPROM stores long-term configuration data, persistent fault logs, and calibration values. Dumping it is crucial for fully backing up an appliance's configuration.

Caution: Do not mistakenly use the write_eeprom function. EEPROM chips have limited write cycles, and arbitrarily overwriting data here can permanently brick the appliance's control board.

use std::{
    error::Error,
    fs::OpenOptions,
    io::{Seek, SeekFrom, Write},
};

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    env_logger::init();

    // Adjust address range here based on the EEPROM capacity
    const START: u16 = 0x0000;
    const END: u16 = 0x07ff;

    let mut port = freemdu::serial::open("/dev/ttyACM0")?;
    let mut dev = freemdu::device::connect(&mut port).await?;

    let mut file = OpenOptions::new()
        .create(true)
        .append(true)
        .open("eeprom_dump.bin")?;

    // Resume dumping process if previously interrupted
    let offset: u16 = file.seek(SeekFrom::End(0))?.try_into()?;

    // Step by 128 bytes (0x80) at a time
    for addr in (START + offset..=END).step_by(0x80) {
        println!("Reading EEPROM chunk at address {addr:04x}");

        // Note: Some older models address EEPROM in 16-bit words rather than bytes.
        // We divide the address by 2 to account for this word-alignment.
        let data: [u8; 0x80] = dev.interface().read_eeprom(addr / 2).await?;

        file.write_all(&data)?;
    }

    Ok(())
}