RSS

Posts Tagged ‘Software Development’

Getting Started with Rust – Raspberry Pi VS Code Extension

Friday, November 7th, 2025

Run and Debug

The Raspberry Pi Foundation have recently announced an update to the Visual Studio Code (VS Code) extension. The update now supports developing embedded firmware for Pico using Rust or Zephyr. The announcement is timely given my current aim of learning Rust with the aim of developing on embedded platforms.

Time to give the extension a spin.

TL;DR It just works.

Updates and Pre-requisites

The post starts by giving an overview of what the extension can do as well as:

  • Overview of Rust
  • Overview of Zephyr
  • Getting (or updating) the extension
  • Installing any pre-requisites

The development platform used when researching this post is MacOS and a previous version of the VS Code extension had already been installed on the platform. Upgrading the extension was as easy as simply using the VS Code Check for Updates… menu item.

Creating a Rust Project

First step in creating an application is to activate the Raspberry Pi extension by clicking on the Pico Extension icon on the Primary Side bar:

Pico Extension Icon

Next select the New Rust Project:

Pico Extension Options

This will show the New Rust Project options:

Create New Rust Project

Here we just need to supply a name for the project and the location for the project files. One point to note is that the project name appears to be restricted:

  • Lower case letters only
  • Start with a letter
  • Numbers
  • – or _

It was necessary to check and deploy the SDK when this was run for the first time and this took a minute or so.

VS Code installing Pico Rust Components

Subsequent runs took about 5 seconds to create and configure a new project.

Finally, VS Code will open the Directory view which shows all of the project files:

Rust File List

Running the Application

By default the project appears to be configured as a 2350 project. The original post from Raspberry Pi states:

supports both RP2040– and RP2350-based devices — including the RISC-V target — which you can later select in the toolbar, next to the Run button.

I must admit, this took a little while to locate. The Run button appears at the far right on the bottom toolbar:

Run Button in Toolbar

Clicking on the chip name allows the target chip to be selected in the command palette:

Switch target chip

Final steps, setting some breakpoints in the application and debugging the application. This is achieved using the Debug and Run facility in Debug view (alternatively press F5):

Run and Debug

A few seconds later deployment had completed and the first breakpoint was hit:

Debugging on the Pico 2

Additional Components

The post recommends installing three optional components/extensions:

  • Probe-rs
  • Cortex Debug
  • Rust analyzer

These had already been deployed to the development machine and are simple to install.

Conclusion

There were a couple of points of confusion, one when the extensions downloaded a few components (this made the creation of the first application take a while). The second was changing the target chip type, it took a minute or so to find the Run button.

Having installed several embedded toolchains over the years I think this was the simplest and probably the best experience and is on a par with commercial toolchains such as Keil.

Getting Started with Rust – Enums

Tuesday, November 4th, 2025

Enums in Rust Banner

Enums in Rust have some properties over and above those found in C/C++

  • Data can be associated with them
  • Methods can be implemented for an enum

The final item in the list was only briefly covered in chapter 6 of The Rust Programming Language book so we will expand on this here.

Simple Enums

As with C/C++, simple enums are symbolic representations of a concept or value. You don’t necessarily need to know what the value is, you can simply use the symbolic name.

enum Message {
    MoveByOffset,
    MoveTo
}

The above code could represent an operation in a game to move a sprite/character etc. This is very much like C/C++, it becomes more interesting when we associate data to an enum.

Enums Can be Associated with Data

Enums can also have data associated with them. So the above enum could be embellished to have the offset and coordinate data associated with the move operations:

enum Message {
    MoveByOffset { x: i32, y: i32 },
    MoveTo { x: i32, y: i32 }
}

Another feature of enums is that not all of the symbolic names (variants) need to have the same data type associated with them. The Message enum could be expanded to encompass more operations:

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

Here we have messages with different data associated with them and even one without any data. We can even have methods implemented for an enum:

impl Message {
    fn call(&self) {
        // method body would be defined here
    }
}

let m = Message::Write(String::from("hello"));
m.call();

Here, we have a call method that can be executed for a Message enum.

Differentiating Between Enums

So far everything looks pretty much like the code in chapter 6 of the Rust book.

The thing that is not covered at this point is how to access the data in the different messages and just as importantly, determine the exact variant of the enum that has been instantiated. For this we need the match statement along with the self parameter in the call method. This is best illustrated by an example:

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

impl Message {
    fn call(&self) {
        match self {
            Message::Quit => println!("Quit message received"),
            Message::Move { x, y } => println!("Move to coordinates: ({}, {})", x, y),
            Message::Write(text) => println!("Write message: {}", text),
            Message::ChangeColor(r, g, b) => { println!("Change color to RGB({}, {}, {})", r, g, b) }
        }
    }
}

fn main() {
    let m1 = Message::Write(String::from("Hello"));
    m1.call();

    let m2 = Message::Move { x: 10, y: 20 };
    m2.call();

    let m3 = Message::ChangeColor(255, 0, 0);
    m3.call();

    let m4 = Message::Quit;
    m4.call();
}

Running this application results in the following output:

Write message: Hello
Move to coordinates: (10, 20)
Change color to RGB(255, 0, 0)
Quit message received

Here, self is used to determine which variant of the Message is calling the method call and match ensures that the appropriate action is taken.

Conclusion

The above application is a trivial illustration of using match to work with enums with different variants but it was not covered very well in the text. A little investigation was required.

Next Up

Project Structure.

C/C++ Constness

Saturday, November 1st, 2025

Constness Post Banner

An aide-memoire post about using the const keyword in function declarations in C/C++. This keyword can have two meanings for pointers depending upon where it is placed:

  • The pointer is constant and cannot be changed
  • The data pointed to by the pointer is constant and cannot be changed

The following application will be used to illustrate the various cases:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void func(char *string)
{
    printf("%s\n", string);
    printf("%s\n", ++string);
    *string = 's';
    printf("%s\n", string);
}

int main(int argc, char**argv)
{
    char *str = strdup("Hello, World!");
    func(str);
    
    free(str);
    return(0);
}

The focus on the definition for func to illustrates the differences. The code fragments that follow will focus on just the definition of func as the rest of the code remains unchanged.

Simple Cases – No const, All const

There are two simple cases:

  • No const – both pointers and data can be changed
  • Both parameters are const – both data and pointer cannot be changed

In the first case, no const, we have the following code:

void func(char *string)
{
    printf("%s\n", string);
    printf("%s\n", ++string);
    *string = 's';
    printf("%s\n", string);
}

Compiling and running this application results in the following output:

Hello, World!
ello, World!
sllo, World!

As we can see, the pointer string and the data pointed to by string can both be changed.

Modifying the code to make make both the pointer and the data pointed to by the pointer string constant, the function definition becomes:

void func(const char * const string)
{
    printf("%s\n", string);
    printf("%s\n", ++string);
    *string = 's';
    printf("%s\n", string);
}

Compiling the application generates the following output:

Const.c:8:20: error: cannot assign to variable 'string' with const-qualified type 'const char *const'
    8 |     printf("%s\n", ++string);
      |                    ^ ~~~~~~
Const.c:5:30: note: variable 'string' declared const here
    5 | void func(const char * const string)
      |           ~~~~~~~~~~~~~~~~~~~^~~~~~
Const.c:9:13: error: read-only variable is not assignable
    9 |     *string = 's';
      |     ~~~~~~~ ^
2 errors generated.

This shows that these lines contain logic errors:

printf("%s\n", ++string);
*string = 's';

This is expected as printf(“%s\n”, ++string); attempts to modify the pointer and *string = ‘s’; attempts to modify the data.

Making the Pointer Constant

The definition of func can be modified to make just the pointer constant and allow the data to be modified:

void func(char * const string)

Compiling the application generates the following output from the compiler:

Const.c:8:20: error: cannot assign to variable 'string' with const-qualified type 'char *const'
    8 |     printf("%s\n", ++string);
      |                    ^ ~~~~~~
Const.c:5:24: note: variable 'string' declared const here
    5 | void func(char * const string)
      |           ~~~~~~~~~~~~~^~~~~~
1 error generated.

Making the Data Constant

There are two ways of allowing the pointer to change wile maintaining the integrity of the data:

void func(const char *string)

and

void func(char const *string)

Compiling an application with either of these function definitions generates and error such as:

Const.c:9:13: error: read-only variable is not assignable
    9 |     *string = 's';
      |     ~~~~~~~ ^
1 error generated.

Function definitions such as the above are useful for functions such as strlen where we want to walk through an array of data in a read-only mode.

Conclusion

This post aims to clear up the confusion about what is constant given that const can appear on both the left and right side of a variable in a function definition. The following table summaraises what is constant for the various options for const:

Definition Description
void func(char *string) Both the data and the data pointed to by string can be changed.
void func(const char * const string)
void func(char const * const string)
The pointer string and the data pointed to by void func(char const * const string) are both constant.
void func(char * const string) The pointer is constant and cannot be changed.
void func(const char *string)
void func(char const *string)
The data pointed to by string is constant.

So the general rule is the item to the left of the const key word is the item that cannot be changed. With one exception and that is when a variables type is prefixed by const. In which case the const applies to the type and not any pointer definition.

Getting Started with Rust – Ownership, Borrowing and References

Wednesday, October 29th, 2025

Ownership Banner

In the words of Connor MacLeod from Highlander

There can be only one.

Every value in a Rust application has one owner and only one owner. When the owner of an value goes out of scope then the value is disposed of and is no longer valid.

The Rust Programming Language (Chapters 4 and 5)

These chapters cover the following topics:

  • Ownership
  • References and Borrowing
  • Slices
  • Structs and Methods

Ownership, references and borrowing determine how an object can be accessed and potentially modified. The strict rules are designed to reduce the possibility of common C/C++ issues such as use after free and general pointer access violations.

Structs and methods are the start of the journey into object orientated programming.

Scopes

Scoping rules pretty much follow the same rules as C/C++ and as such are familiar. Any pair of braces open and close a new scope. So a function introduces a new scope. A new scope can also be created with a pair of braces inside an existing scope.

Ownership, References and Borrowing

As Connor MacLeod said, “There can be only one” and in this case, there can be only one owner of a value.

Calling a function can change the ownership of a variable depending upon the type of the variable. Some types implement the copy trait which makes a copy of the variable on the stack. Types that do not implement the copy trait will have their ownership transferred to the function. The transfer of ownership means that the original variable is no longer valid when the function returns.

Consider the following code:

fn print_integer(x: i32) {
    println!("Integer value: {}", x);
}

fn print_string(s: String) {
    println!("String value: {}", s);
}

fn main() {
    let x: i32 = 42;
    println!("Original integer: {}", x);
    print_integer(x);
    println!("Original integer after function call: {}", x); // This line works fine since integers implement the Copy trait.

    let s: String = String::from("Hello, world!");
    println!("Original string: {}", s);
    print_string(s);
    println!("Original string after function call: {}", s); // This line will cause a compile-time error due to ownership rules.
}

Compiling the above code generates the following error output:

error[E0382]: borrow of moved value: `s`
  --> src/main.rs:24:57
   |
21 |     let s = String::from("Hello, world!");
   |         - move occurs because `s` has type `String`, which does not implement the `Copy` trait
22 |     println!("Original string: {}", s);
23 |     print_string(s);
   |                  - value moved here
24 |     println!("Original string after function call: {}", s); // This line will cause a compile-time error due to owne...
   |                                                         ^ value borrowed here after move
   |
note: consider changing this parameter type in function `print_string` to borrow instead if owning the value isn't necessary
  --> src/main.rs:16:20
   |
16 | fn print_string(s: String) {
   |    ------------    ^^^^^^ this parameter takes ownership of the value
   |    |
   |    in this function
   = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value if the performance cost is acceptable
   |
23 |     print_string(s.clone());
   |                   ++++++++

For more information about this error, try `rustc --explain E0382`.
error: could not compile `hello_world` (bin "hello_world") due to 1 previous error

An awful lot of output to digest.

Calls to print_integer work because the i32 type is simple and this implements the copy trait. This means a copy of the value in x is made on the stack. The function print_integer therefore operates on the copied value and not the variable x defined in main.

print_string works differently as it is operating on a complex value that does not implement the copy trait. Complex values are usually allocated on the heap. Calling print_string moves the ownership of s to the print_string function and the object is dropped at the end of the function making future uses of s in main invalid.

References

The compiler suggested one option for the problem in the above code, to clone the string and pass this to the print_string function. Another solution is to use references. Here the application allows the use of the object without transferring ownership. In this case the object passed is not dropped at the end of the function.

This is known as borrowing.

The above code can be modified to use references:

fn print_string(s: &String) {
    println!("String value: {}", s);
}

fn main() {
    let s: String = String::from("Hello, world!");
    println!("Original string: {}", s);
    print_string(&s);
    println!("Original string after function call: {}", s); // This line will cause a compile-time error due to ownership rules.
}

Running this code with cargo run generates the following output:

Original string: Hello, world!
String value: Hello, world!
Original string after function call: Hello, world!

No more compiler errors.

Function parameters can also be declared as mutable meaning that the original variable can be modified by the function. Modifying the above code to the following:

fn print_string(s: &mut String) {
    println!("String value: {}", s);
    s.push_str(", world!");
}

fn main() {
    let mut s = String::from("Hello");
    println!("Original string: {}", s);
    print_string(&mut s);
    println!("Original string after function call: {}", s);
 }

Running the application results in the following output:

Original string: Hello
String value: Hello
Original string after function call: Hello, world!

print_string is now able to borrow the parameter and modify the value.

I suspect that a lot of time (initially) will be spent fighting the borrow checker.

Structs and Methods

Structs provide the ability to collect together related data items and are similar to structs in C/C++. Methods are functions related to a struct giving us the basics of object orientated programming. The methods are implemented against a type:

struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

In the above code, the area method is implemented against the Rectangle structure.

Language Highlights

One stand out feature with structs is the ability to easily copy unchanged fields from one structure to a new version of the same type of structure. This is best illustrated with an example.

#[derive(Debug)]
struct Person {
    name: String,
    house_number: u16,
    street: String,         // Rest of address fields omitted for brevity.
    mobile_number: String
}

fn update_mobile(person: Person, new_mobile_number: String) -> Person {
    Person {
        mobile_number: new_mobile_number,
        ..person
    }
}

fn main() {
    let person = Person {
        name: String::from("Fred Smith"),
        house_number: 87,
        street: String::from("Main Street"),
        mobile_number: String::from("+44 7777 777777")
    };
    println!("Before update: {:?}", person);
    let updated_person = update_mobile(person, String::from("+44 8888 888888"));
    println!("After update: {:?}", updated_person);
}

Running this application with cargo run generates the following output:

Before update: Person { name: "Fred Smith", house_number: 87, street: "Main Street", mobile_number: "+44 7777 777777" }
After update: Person { name: "Fred Smith", house_number: 87, street: "Main Street", mobile_number: "+44 8888 888888" }

A contrived example maybe but it illustrates the use of ..person to copy the unchanged fields into the new new Person structure.

Another nice feature is the field initialisation for structure members. If a field has the same name as the value being used to initialise it then we can omit the field name. For instance we could modify the update_mobile function above to the following:

fn update_mobile(person: Person, mobile_number: String) -> Person {
    Person {
        mobile_number,
        ..person
    }
}

Note the change to the parameter name to mobile_number to match the field name in the Person struct.

Conclusion

The borrow checker is going to be frustrating for a while with the benefit that if the code compiles it is highly likely to be bug free. The borrow checker also aid in the safety of multithreaded applications.

New Links

Came across a new tool for code linting: Clippy.

Consider the following function:

fn print_integer_and_increment(x: &mut i32) {
    println!("Integer value: {}", x);
    *x = *x + 1;
}

This code compiles without error and works as expected. Running the command cargo clippy to lint the code results in the following output:

warning: manual implementation of an assign operation
  --> src/main.rs:18:5
   |
18 |     *x = *x + 1;
   |     ^^^^^^^^^^^ help: replace it with: `*x += 1`
   |
   = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#assign_op_pattern
   = note: `#[warn(clippy::assign_op_pattern)]` on by default

warning: `hello_world` (bin "hello_world") generated 1 warning (run `cargo clippy --fix --bin "hello_world"` to apply 1 suggestion)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.11s

Running the command cargo clippy –fix –bin “hello_world” –allow-dirty makes the suggested change automatically.

Next Up

Enums, Pattern Matching, Modules and Project structure.

Getting Started with Rust – Variables, Functions and Loops

Wednesday, October 15th, 2025

Getting Started with Rust (Week One) Banner

With the tools installed it is time to start learning some language basics:

  • Variables
  • Functions
  • Control structures (if and loops)

The above are covered in the first three chapters of The Rust Programming Language.

Installing the Tools (Update)

Installation of the tools went pretty smoothly and took only a few hours. The Rust in Visual Studio Code page proved to be a nice addition to the links in the blog post.

The page provides information on:

  • Intellisense
  • Linting
  • Refactoring
  • Debugging

plus more.

The Rust Programming Language (Chapters 1 through 3)

The initial chapters of the The Rust Programming Language covers the basics of Rust:

  • Variables
  • Immutability and mutability
  • Functions
  • Control flow

It was interesting to discover that Rust has a greater degree of distinction between expressions and statements:

Function bodies are made up of a series of statements optionally ending in an expression. So far, the functions we’ve covered haven’t included an ending expression, but you have seen an expression as part of a statement. Because Rust is an expression-based language, this is an important distinction to understand. Other languages don’t have the same distinctions, so let’s look at what statements and expressions are and how their differences affect the bodies of functions.

This difference means that there is no analogy to the following C code:

int x, y;
x = y = 1024;

The basic rule is statements perform actions and expressions return a result. So functions that return a result must return an expression. The general rule being that a statement also ends with a semicolon and an expression does not need one.

Now consider this simple (and admittedly contrived example):

fn add_or_multiply(value : i32) -> i32 {
    if value > 5 {
        return value * 2;
    }
    //  Maybe do some other stuff...
    value + 1
}

fn main() {
    for number in 0..10 {
        let result = add_or_multiply(number);
        println!("Number: {number}, Result: {result}");
    }
}

Running the above generates the following output:

     Running `target/debug/hello_world`
Number: 0, Result: 1
Number: 1, Result: 2
Number: 2, Result: 3
Number: 3, Result: 4
Number: 4, Result: 5
Number: 5, Result: 6
Number: 6, Result: 12
Number: 7, Result: 14
Number: 8, Result: 16
Number: 9, Result: 18

The line return value * 2; can be changed to:

return value * 2

Note the semicolon has been removed. Running the resultant application also generates the same output. Further investigation is required to determine why this works and also what is considered best practice amongst the Rust community.

Language Highlights

From a C/C++ programmers perspective, two Rust constructs are appealing due to their convenience and ability to make code tidier:

  • Using if statements in assignments
  • Labelled loops

The first construct is alien to the C/C++ developer but should be familiar to Python developers. This is the ability to use an if statement in an assignment operation:

let x = if y <= MAXIMUM { MAXIMUM } else { y };

This means the trivial function add_or_multiply in the above application could have been written as:

const MAXIMUM: i32  = 5;

fn add_or_multiply(value : i32) -> i32 {
    let result = if value > MAXIMUM { value * 2 } else { value + 1 };
    result
}

Nice little feature that can make code more compact and readable.

The second nice feature is the ability to label loops. This allows the application to break in an inner loop to also break an outer loop.

'outer_loop: loop {
    //  Setup for the inner loop...
    loop {
        if remaining == MAXIMUM {
            break;
        }
        if (count % 2) == 0 {
            break 'outer_loop;  // Ignore even numbers.
        }
        // More inner loop processing...
    }
    // More outer loop processing...
}

The inner loop may be a contrived version of a while loop but it serves to illustrate the language feature. The break ‘outer_loop allows the inner loop to skip even numbers without the need run unnecessary nested if statements.

Conclusion

A slow start but some interesting language features:

  • Immutability by default
  • Using if statements in assignments
  • Labelled loops

Next up is ownership.

Rust – Installing the Tools

Sunday, October 5th, 2025

Bacon running in a terminal

This week was a gentle start with Rust just installing the toolchain and some browsing for possibly useful tools.

Installing Rust

First step, install the compiler so lets head over to the Getting Started page. According to the page we just need to execute the command:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Which generates the following output:

info: downloading installer

Welcome to Rust!

This will download and install the official compiler for the Rust
programming language, and its package manager, Cargo.

Rustup metadata and toolchains will be installed into the Rustup
home directory, located at:

  /home/tester/.rustup

This can be modified with the RUSTUP_HOME environment variable.

The Cargo home directory is located at:

  /home/tester/.cargo

This can be modified with the CARGO_HOME environment variable.

The cargo, rustc, rustup and other commands will be added to
Cargo's bin directory, located at:

  /home/tester/.cargo/bin

This path will then be added to your PATH environment variable by
modifying the profile files located at:

  /home/tester/.profile
  /home/tester/.bashrc
  /home/tester/.zshenv

You can uninstall at any time with rustup self uninstall and
these changes will be reverted.

Current installation options:


   default host triple: aarch64-unknown-linux-gnu
     default toolchain: stable (default)
               profile: default
  modify PATH variable: yes

1) Proceed with standard installation (default - just press enter)
2) Customize installation
3) Cancel installation
>

Lets go with option 1, the default install:

info: profile set to 'default'
info: default host triple is aarch64-unknown-linux-gnu
info: syncing channel updates for 'stable-aarch64-unknown-linux-gnu'
info: latest update on 2025-09-18, rust version 1.90.0 (1159e78c4 2025-09-14)
info: downloading component 'cargo'
info: downloading component 'clippy'
info: downloading component 'rust-docs'
info: downloading component 'rust-std'
info: downloading component 'rustc'
info: downloading component 'rustfmt'
info: installing component 'cargo'
info: installing component 'clippy'
info: installing component 'rust-docs'
 20.5 MiB /  20.5 MiB (100 %)   8.3 MiB/s in  2s         
info: installing component 'rust-std'
 29.1 MiB /  29.1 MiB (100 %)  14.0 MiB/s in  2s         
info: installing component 'rustc'
 58.5 MiB /  58.5 MiB (100 %)  14.1 MiB/s in  4s         
info: installing component 'rustfmt'
info: default toolchain set to 'stable-aarch64-unknown-linux-gnu'

  stable-aarch64-unknown-linux-gnu installed - rustc 1.90.0 (1159e78c4 2025-09-14)


Rust is installed now. Great!

To get started you may need to restart your current shell.
This would reload your PATH environment variable to include
Cargo's bin directory ($HOME/.cargo/bin).

To configure your current shell, you need to source
the corresponding env file under $HOME/.cargo.

This is usually done by running one of the following (note the leading DOT):
. "$HOME/.cargo/env"            # For sh/bash/zsh/ash/dash/pdksh
source "$HOME/.cargo/env.fish"  # For fish
source $"($nu.home-path)/.cargo/env.nu"  # For nushell

Following the instructions to add rust to the PATH:

. "$HOME/.cargo/env"

Checking that the compiler has been installed:

$ rustup
rustup 1.28.2 (e4f3ad6f8 2025-04-28)

First Application – Hello, World

The classic way to test a new toolchain is to write Hello, world. The cargo build system has a simple way to do this:

cargo new hello_world

    Creating binary (application) `hello_world` package
note: see more `Cargo.toml` keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

cargo should have created a new directory with the name hello_world along with any necessary support files for a Rust project, including default git files.

cd hello_world
total 16
drwxr-xr-x 5 tester tester 4.0K Oct  1 10:11 .
drwxr-xr-x 3 tester tester 4.0K Oct  1 10:10 ..
-rw-r--r-- 1 tester tester   82 Oct  1 10:10 Cargo.toml
drwxr-xr-x 6 tester tester 4.0K Oct  1 10:11 .git
-rw-r--r-- 1 tester tester    8 Oct  1 10:10 .gitignore
drwxr-xr-x 2 tester tester 4.0K Oct  1 10:10 src

The source file for the project is in the src directory with the entry point to the application in the src/main.rs file:

cat < src/main.rs

fn main() {
    println!("Hello, world!");
}

The application can be run with the cargo run command:

cargo run

   Compiling hello_world v0.1.0 (/home/tester/Rust101/hello_world)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.24s
     Running `target/debug/hello_world`
Hello, world!

Supplementary Tools

A little web browsing highlighted a couple of tools that might prove useful:

  • Bacon – Background Analyser
  • Visual Studio Code Extension – rust-analyzer

Let’s install these tools.

Bacon – Background Analyser

Bacon runs in a terminal and it scans the file system for any changes. It then runs cargo and checks the project source code for any errors. These are then displayed in the terminal. This means that the developer gets fast feedback of any issues throughout the development cycle.

Installation is simple:

cargo install --locked bacon

To run the application simply open a new terminal and run the command:

bacon --all-features

Visual Studio Code: rust-analyzer

Rust-analyzer is a popular extension for Visual Studio Code providing features such as:

  • Syntax highlighting
  • Code completion
  • Hints when hovering over variables, types etc.
  • Goto definition

The extension can be installed from the Visual Studio Marketplace or through Visual Studio Code itself.

Project

The best way to learn a new language is to reproduce an application / project that you have developed. This makes writing the application a little simpler as the problem is already understood, the only new element to the project is the new language.

  • Command line application
  • Process the command line
  • Using a directory passed through the command line, generate a list of all files in the directory
  • If a directory is found add to a list and recurse through the directory structure list all the files found

Short, simple problem maybe but it should be enough to get started.

Linking Local and Remote Repositories

Tuesday, September 2nd, 2025

GitHub Repositories Header

Quick memo to self as I always forget how to connect a remote repository to an existing local one.

The scenario here is an idea has been worked on locally and git is being used to track progress. The project eventually reaches the point where it might be useful to others. The most obvious way to do this is to use GitHub to publish the project.

Local Repository

Creating a local repository is usually performed using a git init command:

git init .

which will result in output like:

Initialized empty Git repository in /Users/username/GitHub/ProjectName/.git/

From here on in it is a case of following the normal development methodology for the project in question.

Create the Remote Repository

Next up to create the remote repository:

  • Login to GitHub using your credentials
  • Click on the Repositories link
  • Click on the New button
  • Fill in the details for the repository

At this point we have a new remote repository.

Linking the Local and Remote Repositories

At this point we should have two repositories:

  • Local repository with some work and the associated history
  • Remote repository with a small amount of content (readme, maybe licence etc.)

The two can be linked as follows:

git remote add origin https://github.com/NevynUK/TemporaryTest.git
git branch -M main
git push -u origin main

Conclusion

The two repositories should now be linked and the local content should have been synchronised with the remote repository.

No revelation here but something that is often forgotten.

Installing an Email Server

Saturday, May 31st, 2025

Mailhog Banner

The last post looked at Installing Mosquitto MQTT Server in a test environment, namely a Raspberry Pi. This post looks at a new server into the environment, an email server, again into the same Raspberry Pi environment.

Lightweight Mail Server

The requirement is to provide a portable SMTP email server to accept email from a test application. It is also desirable to provide a mechanism for checking if the email has been sent correctly. A quick search resulted in a docker container for Mailhog, a simple SMTP server that also exposes a web interface to show the messages that have been received. Let’s give it a go.

Building the image is simple, run the command:

docker run --platform linux/amd64 -d -p 1025:1025 -p 8025:8025 --rm --name MailhogEmailServer mailhog/mailhog

This should pull the necessary components and build the local image and run the server.

So far everything looks good. Starting a web browser on the local machine and navigating to localhost:8025 shows a simple web mail page.

Next step, move to a Raspberry Pi and try to start the server and check the service using the same docker command as used above:

docker run --platform linux/amd64 -d -p 1025:1025 -p 8025:8025 --rm --name MailhogEmailServer mailhog/mailhog

This command above results in the following output:

WARNING: The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm64/v8) and no specific platform was requested
d28ed1f93bdcf656f27843014f37b65ab12b5ad510f8d50faec96a57fc090056

This makes sense, the image will be referring to an amd64 image and docker is running on a Raspberry Pi, so arm64. This appears odd as original test was performed on an Apple Silicon Mac which is also arm64. At first glance it does look like the image may be running as we have a UUID for the container. Checking the status of the containers with:

docker ps -a

Shows the following information about the container:

CONTAINER ID   IMAGE             COMMAND     CREATED         STATUS                       PORTS     NAMES
d28ed1f93bdc   mailhog/mailhog   "MailHog"   5 seconds ago   Exited (255) 4 seconds ago             mailhog

Looks like we have a problem as the container is not running correctly.

Solution

The solution is to build a new Dockerfile and create a new image. The new docker file is based upon the Dockerfile in docker hub:

FROM --platform=${BUILDPLATFORM} golang:1.18-alpine AS builder

RUN set -x \
  && apk add --update git musl-dev gcc \
  && GOPATH=/tmp/gocode go install github.com/mailhog/MailHog@latest

FROM --platform=${BUILDPLATFORM} alpine:latest
WORKDIR /bin
COPY --from=builder tmp/gocode/bin/MailHog /bin/MailHog
EXPOSE 1025 8025
ENTRYPOINT ["MailHog"]

The container is built using the docker command above:

docker run --platform linux/amd64 -d -p 1025:1025 -p 8025:8025 --rm --name MailhogEmailServer mailhog-local

No errors, time to check the container status:

docker ps -a

shows:

CONTAINER ID   IMAGE           COMMAND     CREATED         STATUS         PORTS                                            NAMES
dc9e943b7097   mailhog-local   "MailHog"   4 seconds ago   Up 3 seconds   0.0.0.0:1025->1025/tcp, 0.0.0.0:8025->8025/tcp   MailhogEmailServer

Time for some testing.

Testing

Two features of the server need to be checked out:

  • Ability to receive email from a client
  • Verify that the email has been received by the server

The status of the email server can be tested using the web interface by browsing to testserver500.local:8025 (replace testserver500.local with the address/name of your Raspberry Pi), this time we see the simple web interface:

Mailhog Wemail interface showing empty mailbox

Sending email can be tested using telnet, issuing the command:

telnet raspberrypi.local 1025

should result in something like the following response:

Trying fe80::........
Connected to raspberrypi.local.
Escape character is '^]'.
220 mailhog.example ESMTP MailHog

A basic email message can be put together using the following command/response sequence:

Trying 172.17.0.1...
Connected to testserver500.local.
Escape character is '^]'.
220 mailhog.example ESMTP MailHog
HELO testserver500.local
250 Hello testserver500.local
mail from:<tester@testserver500.local> 
250 Sender tester@testserver500.local ok
rcpt to:<user@testserver500.local>
250 Recipient user@testserver500.local ok
data
354 End data with <CR><LF>.<CR><LF>
From: "Tester" <tester@testserver500.local>
To: "User" <user@testserver500.local>
Date: Thu, 1 May 2025 09:45:01 +0100
Subject: Testing the local mail server

Hello,

This is a quick message to test the local SMTP server (Mailhog) running on a Raspberry Pi.

Regards,
Tester
.
250 Ok: queued as mlsU6a9iplWWgg1RILcbGWP6NphswR26_64Pdf98WBo=@mailhog.example
quit
221 Bye
Connection closed by foreign host.

Over to the web interface to see if the email has been received correctly:

Mailhog Wemail interface showing one email in mailbox

And clicking on the message we see the message contents…

Mailhog webmail showing message

The last step is to verify that the container can be stopped and restarted to allow for the system to be automated. The following commands remove the container and allow the container name, MailhogEmailServer to be reused:

docker container stop MailhogEmailServer
docker remove MailhogEmailServer

Following this, the docker run command was executed once more and the container restarted as expected.

Conclusion

There is probably a better way to solve this problem using native docker command line arguments but lack of docker knowledge hindered any investigation. However, the solution presented works and allows for testing to continue.

Final step is to automate the deployment of this solution using ansible.

Installing Mosquitto MQTT Server

Sunday, April 13th, 2025

Steampunk Mosquito

Recently came across a customer problem which needed access to a MQTT server. Here is how it went.

Requirement

The aim is to provide a cross platform way of providing a MQTT server for testing with the following characteristics:

It should be stressed that this is a disposable test environment running on a local network. The system will not be exposed to the Internet and so security and robustness are not going to be an issue.

  • Cross platform, running on Mac and Raspberry Pi
  • Persistence of data is not necessary as the system will be started and stopped as needed
  • Running on a local network with no access to the Internet (reduced need for security)
  • Simple to configure across platforms

Looking at the above it is apparent that a full installation should not be required. In fact it may be overkill. as would a dedicated server. In fact this use case points in the direction of a docker container with some shared configuration.

Installation and Configuration

Mosquitto is a lightweight MQTT server available as a native application for the target platforms and also as a docker image. Using docker will ensure a consistent deployment across multiple platforms and the Eclipse image will be the one used there.

The docker image can be installed with the command:

docker pull eclipse-mosquitto

The download only takes a few seconds with a moderate speed connection.

The Mosquitto client tools are also required for testing. These are installed with the following commands, for Raspberry Pi:

sudo apt update && sudo apt upgrade
sudo apt install mosquitto-clients

and for Mac (assumes Homebrew is already installed, if not, Homebrew installation instructions can be found here):

brew install mosquitto

Note that on a Mac this installs both the client and the server components although we will only need the client applications for testing the system setup.

The Eclipse image page for the docker container describes a simple directory structure for configuration:

/mosquitto/config
/mosquitto/data
/mosquitto/log

This can be replicated by creating a local directory structure and then mapping this when we start the docker container. The local structure will look like this (note the missing / at the start of the directory names):

mosquitto/config
mosquitto/data
mosquitto/log

Time to run and test the server.

Testing

The client tools will allow the installation to be tested. The server will be installed on a Raspberry Pi with the name testserver500.local. The server is started by logging on to the Raspberry Pi and executing the following command:

docker run -it -p 1883:1883 -v "$PWD/mosquitto/config:/mosquitto/config" -v "$PWD/mosquitto/data:/mosquitto/data" -v "$PWD/mosquitto/log:/mosquitto/log" eclipse-mosquitto

This will start the downloaded image and register the config, data and log directories with the image.

First problem, we also need a configuration file (mosquitto.conf) in the mosquitto/config. A quick scan suggests that the following is all that is required:

persistence true
persistence_location /mosquitto/data/
log_dest file /mosquitto/log/mosquitto.log

Two open shells are required for testing, one for receiving MQTT messages (subscription) and one for sending MQTT messages (publishing). In the first shell we subscribe to notifications with the command:

mosquitto_sub -h testserver500.local -t test/debug

Second problem, we get an error message Error: Connection refused. Some googling suggests that anonymous login is also required. The configuration file needs a couple more options adding to the file:

persistence true
persistence_location /mosquitto/data/
log_dest file /mosquitto/log/mosquitto.log
allow_anonymous true
listener 1883 0.0.0.0

Stopping the docker container and then restarting it applies the changes. Subscribing again is successful.

Time to try and publish some data. In a third terminal enter the command:

mosquitto_pub -h testserver500.local -t test/debug -m "Hello, world" -d

The subscription terminal should now display the message just sent:

clusteruser@TestServer500:~ $ mosquitto_sub -h testserver500.local -t test/debug
Hello, world

Success!

One final test, publish / subscribe from a remote machine.

Conclusion

Using a docker image makes it possible to use a standardised setup on any platform, including Windows (although this is not in scope here). The only additional set up required is to supply any specialised configuration and possibly data and log directories should data retention be required.

It should also be remembered that the configuration here is really only suitable for a low risk network (i.e. isolated testing systems) and should not be used in production.

Docker File Sharing

Tuesday, June 11th, 2024

Docker File Sharing Banner

I use Docker fairly often for a number of projects. I find it useful for getting access to tools that are not available on my host system (Mac running on Apple silicon) and also for duplicating the environments I use for CI on GitHub. I also use it to access some legacy tools that I can no longer install locally on my system but where the vendor has provided a Docker image containing those tools.

Docker Image

The docker containers I use most often are the espressif/idf containers, specifically the espressif/idf:release-v4.2 container. This is used to access the development tools for the ESP-IDF release 4.2 tools and libraries to support maintenance of legacy code whilst it is being ported to a newer version of the SDK.

Access to the tools is through the docker command:

docker run --platform linux/amd64 --rm -it -v $PWD:/project -w /project espressif/idf:release-v4.2

Running this command starts the container and sets the PATH etc allowing interactive access to the tools.

Detecting the Python interpreter
Checking "python" ...
Python 3.6.9
"python" has been detected
Adding ESP-IDF tools to PATH...
Using Python interpreter in /opt/esp/python_env/idf4.2_py3.6_env/bin/python
Checking if Python packages are up to date...
Python requirements from /opt/esp/idf/requirements.txt are satisfied.
Added the following directories to PATH:
  /opt/esp/idf/components/esptool_py/esptool
  /opt/esp/idf/components/espcoredump
  /opt/esp/idf/components/partition_table
  /opt/esp/idf/components/app_update
  /opt/esp/tools/xtensa-esp32-elf/esp-2020r3-8.4.0/xtensa-esp32-elf/bin
  /opt/esp/tools/xtensa-esp32s2-elf/esp-2020r3-8.4.0/xtensa-esp32s2-elf/bin
  /opt/esp/tools/esp32ulp-elf/2.28.51-esp-20191205/esp32ulp-elf-binutils/bin
  /opt/esp/tools/esp32s2ulp-elf/2.28.51-esp-20191205/esp32s2ulp-elf-binutils/bin
  /opt/esp/tools/cmake/3.16.4/bin
  /opt/esp/tools/openocd-esp32/v0.11.0-esp32-20220706/openocd-esp32/bin
  /opt/esp/python_env/idf4.2_py3.6_env/bin
  /opt/esp/idf/tools
Done! You can now compile ESP-IDF projects.
Go to the project directory and run:

  idf.py build

root@a76f973ca31c:/project#

It is now possible to use all of the usual Espressif tools from the command prompt with the usual caveats for USB port access on Mac systems. Still, development remains possible even if deployment requires some additional steps.

Workflow

For a while now the working workflow has been as follows:

  • Edit the code, build scripts in VS Code on Mac
  • Build the code in the docker container running in a terminal session
  • Deploy the code from a second terminal session where the latest libraries are installed and configured

This worked flawlessly for several months.

Until the last few days.

The Problem

A recent requirement change necessitated the modification of the source code for the application built using the workflow described above. All seemed to start well, the code was edited, the docker container started and the code hit the first compilation of the day for a syntax check.

This was closely followed by come code changes and a second compilation. All seemed well and the code was committed to source control and rebuilt. At this point I noticed something odd, the automatically generated build number did not increase. The system used for this repository changes the build number based upon the number of commits. The first two compilations would not have increased the build number as there was no commit. The build after the commit would normally generate an increment in the build number and it clearly was not doing so.

Let’s introduce a syntax error, deleting a semicolon should do it, and try rebuilding. The code compiled with no errors. How strange.

Investigating further using more to check the contents of the files on the host machine against the docker container revealed that the changes on the host system were not being reflected in the docker image.

Docker File System Access

Something odd is happening to the file system. Changes on the host are clearly not being reflected in the mounted volume in the docker container. Time to try a few things with the syntax error still in place:

  • Exit the docker container and restart it and build the code – no change, the code still compiles
  • Exit the docker container and run it none interactively – still no change
  • Change the mount method from a volume to a mount option – the code still compiles
  • Delete the docker image and restart (this will rebuild from scratch) – compilation gives a syntax error

So finally, the code change is reflected in the docker volume. Now we need to remove the syntax error by reverting the file change and we can recompile and move on. Doing this resulted in a compilation failure, the change had once again not been applied to the mounted volume.

At this point a colleague checked on their system that they could change files and see the changes reflected in the mounted volume and yes they could. Time for a comparison of the system settings and the obvious one to pick up is the File System settings. A quick check showed a difference between the two systems. Changing my system to match theirs and restarting everything resolved the issue.

The difference was in the File sharing implementation for your containers.

File Sharing Selection

File Sharing Selection

My local system had this configured for gRPC FUSE. Changing to the above setting, VirtioFS, and restarting Docker Desktop and the docker container seems to have fixed the issue.

Conclusion

I still do not know why the change to the way the file system was accessed changed or why the system stopped reflecting changes to the files in the mounted volume. I don’t think I will ever find out but maybe the note will help others (maybe even myself in the future).