RSS

Posts Tagged ‘Software Development’

Rust Resources

Thursday, April 2nd, 2026

The Rust Programming Language Book Cover

Some notable resources for anyone starting Rust.

The Rust Programming Language

The third edition of The Rust Programming Language was released on 31st March 2026.

Accessing Hardware in Rust

Ferrous Systems have published a blog post covering the topic of dealing with low level hardware from the safe Rust environment. The post also points to a number of interesting tools available to firmware developers.

Microsoft Rust Training Books

Microsoft have released a number of training books covering Rust. Soe fo the books cover Rust for programmers coming from other programming languages including C/C++, C# and Python. The series of books also cover async and continuous integration.

Introduction to Rust Video Series

Digikey have released a video training series covering the Rust programming language from the basics of the benefits of Rust through to interrupt processing on a Raspberry Pi Pico microcontroller. The full play list can be found at Introduction to Rust Playlist. This series covers (at the time of writing) the following modules:

Espressif Installation Manager (EIM)

Sunday, March 29th, 2026

EIM Banner

Espressif has recently released a cross-platform installation manager for the SDK used with ESP32 boards.
ESP-IDF Installation Manager (EIM)
is available for macOS, Linux, and Windows. The aim of the tool is to simplify the installation of ESP-IDF and its associated tools.

EIM also allows multiple copies of the framework to be installed on the same machine, allowing developers to switch between versions. This is ideal for testing different IDF versions against a code base.

Various IDF archives can be found on the
EIM Offline Installer downloads page.

First Impressions

So far, the tool has delivered on the promise of providing an easy, switchable way to install the various tools and SDKs. Installation of the tool is straightforward, and the SDKs are downloaded as archives and then loaded into the system using EIM. The same tool is used to switch from one IDF version to another.

Aside from one caveat, which will be covered later, the tool does exactly what it says on the tin, offering a simple way to install and switch between IDF versions.

Installing an IDF Version

Installation of the tool itself went smoothly; only two brew commands were needed.

brew tap espressif/eim
brew install eim

It is then necessary to download and install the archives for the IDF versions of interest. This is simply a case of navigating to the
EIM Offline Installer downloads page,
selecting the desired IDF version or versions, and downloading them locally. These archives can be large, with some coming in at 2.5 GB.

Once downloaded, an archive can be installed into the EIM system with a command such as the following:

eim install --use-local-archive archive_vv6.0_macos-aarch64.zst

The downloaded archive file can be deleted once an IDF version has been installed. The full list of installed versions can be obtained using the command:

$ eim list
2026-03-29 09:00:12 -  9 - 03 - INFO - Listing installed versions...
Installed versions:
- v5.5.3 (selected) [/Users/username/.espressif/v5.5.3/esp-idf]
- v6.0 [/Users/username/.espressif/v6.0/esp-idf]

An installed version can be activated with the command:

source /Users/username/.espressif/tools/activate_idf_v5.5.3.sh

This command registers the various environment variables, Python environments, and related settings:

Added environment variable ESP_IDF_VERSION = 5.5
Added environment variable IDF_TOOLS_PATH = /Users/username/.espressif/tools
Added environment variable IDF_COMPONENT_LOCAL_STORAGE_URL = file:///Users/username/.espressif/tools
Added environment variable IDF_PATH = /Users/username/.espressif/v5.5.3/esp-idf
Added environment variable ESP_ROM_ELF_DIR = /Users/username/.espressif/tools/esp-rom-elfs/20241011
Added environment variable OPENOCD_SCRIPTS = /Users/username/.espressif/tools/openocd-esp32/v0.12.0-esp32-20251215/openocd-esp32/share/openocd/scripts
Added environment variable IDF_PYTHON_ENV_PATH = /Users/username/.espressif/tools/python/v5.5.3/venv
Added proper directory to PATH
Activated virtual environment at /Users/username/.espressif/tools/python/v5.5.3/venv
Environment setup complete for the current shell session.
These changes will be lost when you close this terminal.
You are now using IDF version 5.5.
eim select v5.5.3

Once complete, the selected IDF version is ready to use.

Scripting

Now to the caveat mentioned earlier.

Using idf.py from the command line works fine, as do all the other tools. Things are a little different when it comes to scripting.

On macOS, EIM exposes idf.py via a shell alias. In practice, it looks something like this:

alias idf.py='/Users/username/.espressif/tools/python/v5.5.3/venv/bin/python /Users/username/.espressif/v5.5.3/esp-idf/tools/idf.py'

As mentioned, this works on the command line. However, it does not work in a bash script, as aliases are not normally expanded in non-interactive bash scripts. The solution is to define a variable in the bash script that points to the idf.py Python script file:

IDF=$IDF_PATH/tools/idf.py

It then becomes a simple case of replacing idf.py with $IDF within the script. So far, this solution has worked both locally and in a GitHub Action.

EIM GUI

Espressif also offers a GUI version of the installation manager. This was installed briefly but removed after Malwarebytes detected an attempt to access a website that it identified as potentially serving a Trojan.

This was did not seem to be a problem with the command line version of EIM.

Conclusion

EIM provides a simple way to install ESP-IDF versions and switch between them. It should make the setup process more straightforward, particularly for users on platforms where ESP-IDF installation has traditionally been less convenient.

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.