[{"content":" TL;DR This is a fairly long post with a lot of code. If you don\u0026rsquo;t have much time, feel free skip to the results and if you find it interesting take a look through the whole post.\nBackground During my previous internship at Fourier, I started my Embedded Rust development journey. I had previously written small amounts of Rust, but I would by no means say I knew the language at the time. One of Fourier\u0026rsquo;s embedded systems engineers is a very passionate Embedded Rust developer. He is the one who introduced me to the type-state pattern.\nThe type-state pattern is an API design pattern that encodes information about an object’s run-time state in its compile-time type. In particular, an API using the type-state pattern will have:\nOperations on an object (such as methods or functions) that are only available when the object is in certain states, A way of encoding these states at the type level, such that attempts to use the operations in the wrong state fail to compile, State transition operations (methods or functions) that change the type-level state of objects in addition to, or instead of, changing run-time dynamic state, such that the operations in the previous state are no longer possible. This is useful because:\nIt moves certain types of errors from run-time to compile-time, giving programmers faster feedback. It interacts nicely with IDEs, which can avoid suggesting operations that are illegal in a certain state. It can eliminate run-time checks, making code faster/smaller. This great explanation is taken from Cliffle\u0026rsquo;s blog post \u0026ldquo;The Typestate Pattern in Rust\u0026rdquo;\nAdin(the aforementioned Embedded Rust co-worker), showed me how we can leverage this design pattern that is so easy to implement in Rust to extend the capabilities of the Rust compiler to validate hardware configurations. My use of the type-state pattern in the crate is heavily inspired by work done by Adin in his pursuit of creating tools to generate better HALs. Read more about Adin\u0026rsquo;s work here.\nGroundwork Let\u0026rsquo;s start by laying out some key terms that we\u0026rsquo;ll use throughout this example and how they are defined\u0026hellip;\nAll this starts with the idea of representing the hardware states within register fields as types in the Rust type system. This representation of hardware states in the fields of registers as Rust types are named type-states.\nType-states are marker types that directly correspond to hardware states. Hardware states are exposed as values of register bit-fields. For any type-state there is going to be a specific value that will be in the respective physical register field. That physical value is referred to as the raw value. For any field, all of the possible hardware states it may in-habit are represented as a variant of an enum named Variant. Type-states will implement a trait named State which contains a constant VARIANT of the type Variant and value corresponding to the raw value to achieve the represented hardware state.\nWe will use a trait named Entitled to express inter-bit-field relationships in the type system. This is the secret sauce that allows us coerce the compiler into checking our configurations.\nProperties are values that are derived from multiple hardware-states of the sensor but aren\u0026rsquo;t values that are directly written to registers.\nIn summary, Type-states express hardware states. The type relationships (as expressed by the Entitled trait) provide a proxy for the true hardware relationships. The resulting structures facilitate correct hardware usage.\nAn Example Scenario Consider: A simple sensor with a single configuration register. The register holds the configuration for the sensor\u0026rsquo;s status, either enabled or disabled, it holds the selection of the sensors measurement range, and lastly it\u0026rsquo;s power mode, either low power or normal power. This chip is very picky though, and if the sensor is disabled, it\u0026rsquo;s range must be set to a specific value otherwise it exhibits undefined behavior. Another quirk of the sensor is that some measurement ranges are only available in specific power modes.\nRegister 1: Address 0x45 Bit 7 Bit 6 Bit 5 Bit 4 Bit 3 Bit 2 Bit 1 Bit 0 - - - - Pm R1 R0 En Register 1 Description Field Description Pm Power mode select bit. Default value: 0\n(0: Low power, 1: Normal power) R[1:0] Measurement range select. Default value: 00\n(see Range Selection table for configurations) En Status select bit. Default value: 0\n(0: Disabled, 1: Enabled) Range Selection R1 R0 Selected Range Power Mode Selection 0 0 Range Disabled\nMust be set when sensor is disabled otherwise exhibits undefined behaviour. Available in all power modes 0 1 Range 1 Available in all power modes 1 0 Range 2 Available in all power modes 1 1 Range 3 Only Available in normal power mode Resolution Dependent On Power Mode Range Power Mode Resolution Range Disabled, Range 1, Range 2 Low Power 8 Bit Range Disabled, Range 1, Range 2 Normal Power 12 Bit Range 3 Normal Power 16 Bit Recall Range 3 is only available in Normal Power mode.\nImplementation Our Type-States We will begin our implementation of the pattern by defining our hardware states as type-states. Because all of these hardware states are confined to a single register, we will place them all within a single module. This module is also a convenient place to keep register specific values like it\u0026rsquo;s hardware address. Each type-state is built in a module analogous to the register bit-field, which contains the previously mentioned State trait and Variant enum.\npub mod register_1 { pub const ADDR: u8 = 0x45; pub mod status { todo!() } pub mod range { todo!() } pub mod power_mode { todo!() } } Let\u0026rsquo;s walk through the creation of our first type-state for the status select bit field.\npub mod register_1 { //... // Create the type-state in a mod named according to the bit-field. pub mod status { // We can store key field values here // For exampled, address and offset. pub const ADDR: u8 = super::ADDR; pub const OFFSET: u8 = 0; // We define our State trait that holds a const of type Variant. pub trait State { const VARIANT: Variant; } // We define our Variant enum with variants corresponding to the possible field values. // If we refer back to the register table we see that the possible field values are 0b0 for disabled, and 0b1 for enabled. #[repr(u8)] pub enum Variant { Disabled = 0b0, Enabled = 0b1, } // Now we create structs that will implement the State trait with their corresponding variant. pub struct Disabled; pub struct Enabled; impl State for Disabled { const VARIANT: Variant = Variant::Disabled; } impl State for Enabled { const VARIANT: Variant = Variant::Enabled; } } //... } We repeat this process for the other fields in the register and now we end up with the following:\npub mod register_1 { pub const ADDR: u8 = 0x45; pub mod status { pub const ADDR: u8 = super::ADDR; pub const OFFSET: u8 = 0; pub trait State { const VARIANT: Variant; } #[repr(u8)] pub enum Variant { Disabled = 0b0, Enabled = 0b1, } pub struct Disabled; pub struct Enabled; impl State for Disabled { const VARIANT: Variant = Variant::Disabled; } impl State for Enabled { const VARIANT: Variant = Variant::Enabled; } } pub mod range { pub const ADDR: u8 = super::ADDR; pub const OFFSET: u8 = 1; pub trait State { const VARIANT: Variant; } #[repr(u8)] pub enum Variant { RangeDisabled = 0b00, Range1 = 0b01, Range2 = 0b10, Range3 = 0b11, } pub struct RangeDisabled; pub struct Range1; pub struct Range2; pub struct Range3; impl State for RangeDisabled { const VARIANT: Variant = Variant::RangeDisabled; } impl State for Range1 { const VARIANT: Variant = Variant::Range1; } impl State for Range2 { const VARIANT: Variant = Variant::Range2; } impl State for Range3 { const VARIANT: Variant = Variant::Range3; } } pub mod power_mode { pub const ADDR: u8 = super::ADDR; pub const OFFSET: u8 = 3; pub trait State { const VARIANT: Variant; } #[repr(u8)] pub enum Variant { LowPower = 0b0, NormalPower = 0b1, } pub struct LowPower; pub struct NormalPower; impl State for LowPower { const VARIANT: Variant = Variant::LowPower; } impl State for NormalPower { const VARIANT: Variant = Variant::NormalPower; } } //... } Lastly we need to provide a way to access these type-states as they would appear in the register, as bits. To do so we add the straight forward associated function render_as_bytes.\npub mod register_1{ //... pub fn render_as_bytes\u0026lt;Enable, Range, PowerMode\u0026gt;() -\u0026gt; u8 where Enable: sensor_enable::State, Range: sensor_range::State + super::Entitled\u0026lt;Enable\u0026gt;, PowerMode: power_mode::State + super::Entitled\u0026lt;Range\u0026gt;, { ((Enable::VARIANT as u8) \u0026lt;\u0026lt; sensor_enable::OFFSET) | ((Range::VARIANT as u8) \u0026lt;\u0026lt; sensor_range::OFFSET) | ((PowerMode::VARIANT as u8) \u0026lt;\u0026lt; power_mode::OFFSET) } } Expressing Entitlements Now that we\u0026rsquo;ve created our type-states, we need to express their relationship with each other using the Entitled trait. From the example, if range can only be set to one of the options: {Range1, Range2, and Range3}, if status is set to Enabled, then one could say that the type-states Range1, Range2, and Range3 of range are Entitled to the type-state Enabled of status. The mandatory hardware state of the sensor range bit-field when the sensor is disabled can be enforced by the compiler using Entitlements. In code this would look like:\nmod entitlement { pub trait Sealed\u0026lt;T\u0026gt; {} } pub trait Entitled\u0026lt;T\u0026gt;: entitlement::Sealed\u0026lt;T\u0026gt; {} impl\u0026lt;T, U\u0026gt; Entitled\u0026lt;U\u0026gt; for T where T: entitlement::Sealed\u0026lt;U\u0026gt; {} // The compiler enforces that sensor 1 can only be disabled if the sensor range is set to disabled. impl entitlement::Sealed\u0026lt;status::Disabled\u0026gt; for range::RangeDisabled {} // The rest of the ranges, naturally, require the sensor to be enabled. impl entitlement::Sealed\u0026lt;status::Enabled\u0026gt; for range::Range1 {} impl entitlement::Sealed\u0026lt;status::Enabled\u0026gt; for range::Range2 {} impl entitlement::Sealed\u0026lt;status::Enabled\u0026gt; for range::Range3 {} The other inter-bit-field relationship expressed in the scenario is between power_mode and range. The highest measurement range (range::Range3) isn’t available when it is configured in low power mode (power_mode::LowPower). Again, we can tell the compiler to enforce this hardware constraint using the Entitled trait.\n// Sensor ranges 1 and 2 can be used in any power mode state. impl\u0026lt;T: power_mode::State\u0026gt; entitlement::Sealed\u0026lt;sensor_range::Range1\u0026gt; for T {} impl\u0026lt;T: power_mode::State\u0026gt; entitlement::Sealed\u0026lt;sensor_range::Range2\u0026gt; for T {} // Sensor range 3 can exclusively be used in normal power mode. impl entitlement::Sealed\u0026lt;sensor_range::Range3\u0026gt; for power_mode::NormalPower {} Now we\u0026rsquo;ve expressed all our inter-bit-field relationships to the compiler, it will now help guide us in making valid configurations. We\u0026rsquo;ll look at how these type-states are used in moment, just one more concept to introduce.\nProperties Properties are values that are derived from multiple hardware-states of the sensor but aren\u0026rsquo;t values that are directly written to registers. Continuing on from our scenario, we know the resolution is derived from the sensor measurement range and another hardware state power_mode.\nThen we can define the property like so following a very similar structure to the type-states we previously defined:\npub mod properties { pub mod resolution { use crate::register_1::{ power_mode::{ self, Variant::{LowPower, NormalPower}, }, range::{ self, Variant::{Range1, Range2, Range3, RangeDisabled}, }, }; #[derive(PartialEq)] #[repr(u8)] pub enum Variant { R8Bit = 8, R12Bit = 12, R16Bit = 16, } pub trait Property { const VARIANT: Variant; } pub struct Resolution\u0026lt;R, Pm\u0026gt; where R: range::State, Pm: power_mode::State, { _p: core::marker::PhantomData\u0026lt;(R, Pm)\u0026gt;, } #[rustfmt::skip] impl\u0026lt;R, Pm\u0026gt; Property for Resolution\u0026lt;R, Pm\u0026gt; where R: range::State, Pm: power_mode::State, { const VARIANT: Variant = { match (R::VARIANT, Pm::VARIANT) { (RangeDisabled, LowPower) =\u0026gt; Variant::R8Bit, (RangeDisabled, NormalPower) =\u0026gt; Variant::R12Bit, (Range1, LowPower) =\u0026gt; Variant::R8Bit, (Range1, NormalPower) =\u0026gt; Variant::R12Bit, (Range2, LowPower) =\u0026gt; Variant::R8Bit, (Range2, NormalPower) =\u0026gt; Variant::R12Bit, (Range3, NormalPower) =\u0026gt; Variant::R16Bit, (Range3, LowPower) =\u0026gt; unreachable!(), } }; } } } Using The Type-States There are lots of ways to write this last part of the API. Personally, I like the pattern of separate config and sensor structs, where the Sensor struct holds a valid config of type Config. So let\u0026rsquo;s go ahead and make that\u0026hellip;\nWe\u0026rsquo;ll start by making new mod. For this example everything is written in a single file, but in full implementations normally this would be a new file.\npub mod config { use crate::{ Entitled, properties, register_1::{self, power_mode, range, status}, }; todo!() } Now we\u0026rsquo;ll create our config struct with generics for each bit-field that must implement their respective State and Entitled traits. We\u0026rsquo;ll also add a byte representation of the config and a way to access it (with a single byte/register it looks a little silly, but normally there would be several).\npub mod config { //... pub struct Config\u0026lt;Enable, Range, PowerMode\u0026gt; where Enable: sensor_enable::State, Range: sensor_range::State + Entitled\u0026lt;Enable\u0026gt;, PowerMode: power_mode::State + Entitled\u0026lt;Range\u0026gt;, { pub mode: Enable, pub range: Range, pub power_mode: PowerMode, } pub struct ConfigAsBytes { register_1: u8, } impl ConfigAsBytes { pub fn as_byte_buffer(\u0026amp;self) -\u0026gt; [u8; 1] { [self.register_1] } } //... } We\u0026rsquo;ll also create a sealed convenience trait called ValidConfig that provides a way to pass valid generic configurations, rather than concrete configurations. If this trait didn\u0026rsquo;t exist, one would need to specify the many generic parameters of config, but with the trait, where T: ValidConfig can be used.\npub mod config { //... mod sealed { pub trait Sealed {} } pub trait ValidConfig: sealed::Sealed { // Type-states corresponding to the sensor\u0026#39;s Config and entitlement check. type Enable: register_1::status::State; type PowerMode: register_1::power_mode::State; type Range: register_1::range::State + Entitled\u0026lt;Self::Enable\u0026gt; + Entitled\u0026lt;Self::PowerMode\u0026gt;; // Properties corresponding to the sensor\u0026#39;s Config. type Resolution: properties::resolution::Property; /// Render some [`ValidConfig`] to bytes. fn render_as_bytes() -\u0026gt; ConfigAsBytes; } impl\u0026lt;Enable, Range, PowerMode\u0026gt; sealed::Sealed for Config\u0026lt;Enable, Range, PowerMode\u0026gt; where Enable: status::State, Range: range::State + Entitled\u0026lt;Enable\u0026gt; + Entitled\u0026lt;PowerMode\u0026gt;, PowerMode: power_mode::State, { } impl\u0026lt;Enable, Range, PowerMode\u0026gt; ValidConfig for Config\u0026lt;Enable, Range, PowerMode\u0026gt; where Enable: status::State, Range: range::State + Entitled\u0026lt;Enable\u0026gt; + Entitled\u0026lt;PowerMode\u0026gt;, PowerMode: power_mode::State, { // Type-States type Enable = Enable; type Range = Range; type PowerMode = PowerMode; // Resulting Properties: type Resolution = properties::resolution::Resolution\u0026lt;Self::Range, Self::PowerMode\u0026gt;; fn render_as_bytes() -\u0026gt; ConfigAsBytes { ConfigAsBytes { register_1: register_1::render_as_bytes::\u0026lt;Enable, Range, PowerMode\u0026gt;(), } } } } Finally, we can create our sensor struct, again with a way to access it\u0026rsquo;s config as bytes:\npub struct sensor\u0026lt;C: ValidConfig\u0026gt; { config: C, } impl\u0026lt;C: ValidConfig\u0026gt; sensor\u0026lt;C\u0026gt; { fn render_config_as_bytes(\u0026amp;self) -\u0026gt; config::ConfigAsBytes { C::render_as_bytes() } } Results Now let\u0026rsquo;s see the fruits of our labour.\nfn main() { let my_config = Config { mode: sensor_enable::SensorEnabled, range: sensor_range::Range3, power_mode: power_mode::NormalPower, }; let my_sensor = sensor { config: my_config }; let my_config_as_bytes: config::ConfigAsBytes = my_sensor.render_config_as_bytes(); let config_to_write = my_config_as_bytes.as_byte_buffer(); println!( \u0026#34;Bus write: {:#b} to {:#x}\u0026#34;, config_to_write[0], register_1::ADDR ); } Output:\nBus write: 0b1111 to 0x45 Sensor resolution: 16 And if we try with an invalid config where we try and use Range3 in LowPower mode\u0026hellip;\nfn main() { let my_invalid_config = Config { mode: status::Enabled, range: range::Range3, power_mode: power_mode::LowPower, }; //... } Output:\nerror[E0277]: the trait bound `Range3: Entitled\u0026lt;LowPower\u0026gt;` is not satisfied --\u0026gt; src/main.rs:276:16 | 276 | range: range::Range3, | ^^^^^^^^^^^^^ the trait `entitlement::Sealed\u0026lt;LowPower\u0026gt;` is not implemented for `Range3` | = help: the following other types implement trait `entitlement::Sealed\u0026lt;T\u0026gt;`: `Range3` implements `entitlement::Sealed\u0026lt;Enabled\u0026gt;` `Range3` implements `entitlement::Sealed\u0026lt;NormalPower\u0026gt;` note: required for `Range3` to implement `Entitled\u0026lt;LowPower\u0026gt;` We\u0026rsquo;ve done it! The compiler is now able to help us enforce valid hardware configurations at compile time. Moreover, it\u0026rsquo;s error message tells us how the sensor has been misconfigured: \u0026ldquo;required for Range3 to implement Entitled\u0026lt;LowPower\u0026gt;\u0026rdquo;. It\u0026rsquo;s then clear to the programmer that Range3 and LowPower in combination are not a valid configuration. The message also tells us \u0026ldquo;Range3 implements entitlement::Sealed\u0026lt;NormalPower\u0026gt;\u0026rdquo; telling the programmer an alternative configuration that is valid! Wouldn\u0026rsquo;t it be great if all HALs could do this!\nClosing Thoughts I made use of the type-state API to make my driver for the lis3dh accelerometer. The lis3dh has many intertwined hardware configuration options which has caused many of the available crates to not expose these features. I can\u0026rsquo;t know for certain why features were not exposed, but to me it seems that without the type-state pattern, there isn\u0026rsquo;t a way to guarantee correct configuration of the inter-dependent features without performing several run-time checks. The type-state pattern solves this issue.\nWhile this pattern is a great way to design a safe hardware driver, it adds a lot more code. A lot of the boiler plate can be reduced with macros which I made use of in the repo linked below if you\u0026rsquo;re curious. As many people of all disciplines do, you end up with the classic trade-off of quality vs. time. One important quality is guaranteed correct hardware configuration (if the driver is written correctly) which can play a major role in safety.\nWhen lives are on the line, the cost of laziness far outweighs the cost of effort.\n\u0026ndash; my good friend Adin\nA Rust playground link of the example from this post for you to run and experiment with is available here: Playground Link\nA partial implementation of the design pattern for the lis3dh can be found in the repo below. SimonGorbot/lis3dh-driver A Rust driver for the lis3dh accelerometer using the type-state design pattern. Rust 2 0 ","date":"5 August 2025","externalUrl":null,"permalink":"/projects/lis3dh_driver/","section":"Projects","summary":"Using the type-state design pattern with Rust\u0026rsquo;s compiler to enforce proper hardware configuration.","title":"Safe Sensor Drivers Using Rust's Type System","type":"project"},{"content":" Project Summary SiTerm is an adaptor accompanied by a TUI that allows you to interface your laptop via USB to downstream embedded systems and transfer serial messages using I2C, SPI, and UART. The goal is to have a set of commands that allow you to easily send and read serial protocol messages as an extension to assist with debugging or initial testing of embedded systems.\nCurrent WIP Demo Status LED Signals Colour Solid Blinking 🟢 Unused Command executed successfully 🟡 Unused Waiting for TUI connection 🔴 Unused Error executing command 🔵 Idle Unused This is still very much so a work in progress. I\u0026rsquo;m really trying to find time to work on it between classes and capstone. Never the less, here\u0026rsquo;s some info about the project so far:\nEverything is programmed in Rust. I\u0026rsquo;m using ratatui.rs for my tui. The MCU I\u0026rsquo;m using as the adapter is the RP2040-Zero board from WaveShare I picked this board specifically because it\u0026rsquo;s tiny, has USB-C, all the peripherals necessary, and the potential to do something cool with PIO. Once all the code is complete I might make a shield for even easier connections to downstream embedded systems. I\u0026rsquo;m using the Embassy framework for my firmware. The name of the project comes from an abbreviated form of Serial Interface Terminal. It\u0026rsquo;s also the first two letters of my name Simon Terminal. There\u0026rsquo;s a lot of unused code in the project currently because I started from a template. I\u0026rsquo;m in the process of trimming all of it out. Command List All commands follow the general format: protocol action payload. Payloads are action specific depending on the command you are writting.\nI2C Leader Single Byte Read Protocol Action Payload Example Complete i2c r device_address register_address 1 i2c r 0x1A 0x0F 1 ✅ Single Byte Write Protocol Action Payload Example Complete i2c w device_address register_address value_to_write i2c r 0x1A 0x0F 0xFF ✅ Batch Read Protocol Action Payload Example Complete i2c r device_address starting_register_address num_reads i2c r 0x1A 0x0F 3 🚧 Batch Write Protocol Action Payload Example Complete i2c r device_address starting_register_address value_to_write_1 \u0026hellip; value_to_write_n i2c r 0x1A 0x0F 0x0A 0x0B 0x0C 🚧 Follower Listen coming soon\nSPI Leader Single Byte Read coming soon\nSingle Byte Write coming soon\nBatch Read coming soon\nBatch Write coming soon\nUART Send String coming soon\nSend Bytes coming soon\nRead Number Bytes coming soon\nRead Until Byte/Bytes coming soon\nPWM Set Duty Cycle coming soon\nSimonGorbot/SiTerm A development tool for quick debugging and testing of serial communication devices. Uses a TUI based serial monitor and writer to interact with a micro-controller to easily perform simple protocol operations and return results. Rust 0 0 ","date":"10 October 2025","externalUrl":null,"permalink":"/projects/siterm/","section":"Projects","summary":"A TUI+Adapter that allows you to interface your laptop via USB to downstream systems and transfer serial messages using I2C, SPI, and UART.","title":"SiTerm: Embedded Development Tool","type":"project"},{"content":" Background I built an embedded gesture-recognition “wand” that classifies 6 IMU gestures on an STM32F411RE in real time, then used it as a testbed to compare opaque AutoML (in this case NanoEdgeAI) against a fully transparent Scikit-learn → emlearn → C pipeline. AutoML tools can be fast, but when model internals aren’t visible, debugging and long-term maintenance can be painful. The core question here was: can an ML newbie use an open pipeline to create models that match (or beat) NanoEdgeAI on real hardware accuracy, latency, and memory while staying reproducible and inspectable?\nHardware + Dataset MCU: NUCLEO-F411RE (Cortex-M4F, 512 KB flash / 128 KB SRAM) IMU: MPU9250 (accel + gyro used; mag ignored) over I2C Sampling: 200 Hz, accel ±16 g, gyro ±2000 dps, 16-bit Gestures: circle, lightning, swipe up/down/left/right After cleaning + standardizing duration, the final dataset used for training was 1200 labeled samples (200 per gesture). Each sample was resampled to 100 time steps and flattened into a 600-element feature vector (6 channels × 100). Visualization of gestures to be classified. Acceleration readings per axis by gesture (95% confidence interval). Two model pipelines 1) NanoEdgeAI (opaque / generated) Generated and ranked many candidates; I exported the top SVM, RF, and MLP by NanoEdgeAI’s “quality index” and deployed the generated C to the MCU. 2) Transparent pipeline (reproducible) Trained Scikit-learn MLP + RF variants, selected configs using a weighted score prioritizing balanced accuracy (with small penalties for estimated compute + memory), then converted to C using emlearn for on-device inference. Final evaluation was on-device using a new participant (not in the training set), 50 trials per gesture, recording predicted class + inference time.\nKey results (on-device) Model On-device accuracy On-device inference time Compiled flash Custom MLP (Scikit→emlearn) 91.78% 3178 µs 115.836 KB NanoEdgeAI MLP 91.00% 824 µs 35.880 KB NanoEdgeAI SVM 81.67% 454 µs 29.004 KB Custom RF (Scikit→emlearn) 76.32% 15.27 µs 30.292 KB NanoEdgeAI RF 51.00% 412 µs 37.484 KB Transparent pipeline MLP model embedded inference performance. Transparent pipeline RF model embedded inference performance. Two practical observations mattered more than the headline numbers:\n“Blind spots” happened. NanoEdgeAI’s SVM and RF failed completely on the circle gesture during live testing (0% correct), and NanoEdgeAI RF also showed a blind spot for lightning. Estimated performance ≠ deployed performance. Across models, real on-device behavior diverged from tool-reported estimates—especially for NanoEdgeAI. What I’d do differently next Quantize / fixed-point custom models to shrink flash (the custom MLP used float weights, inflating size). A lot more time could be put into the implementation of the custom models. Due to the short-timeline it was deemed out of scope. Collect a bigger, more variable dataset (inter-user motion inconsistency was a real limiter). Explore Burn \u0026#x1f980; Full write-up If you want the full methodology, plots, and confusion matrices, the complete paper is here: Cracking the Code: Achieving High-Performance Embedded Gesture Recognition with Transparent Pipelines\n","date":"15 December 2025","externalUrl":null,"permalink":"/projects/tinyml_exploration/","section":"Projects","summary":"Can an ML newbie use an open pipeline to create models that match (or beat) NanoEdgeAI on real hardware accuracy, latency, and memory while staying reproducible and inspectable?","title":"TinyML Exploration: IMU Based Gesture Recognition","type":"project"},{"content":" Background In our final year of engineering at the University of Waterloo, students form groups to create a project over the span of 8-months that is meant to apply the knowledge and skills learned in the classroom and on co-op work terms. Since my four group mates and I have benefited so much from the open-source hardware community, we decided we want to contribute to the space. We’re building a fully open-source 16DOF/8DOA humanoid hand with integrated force sensing that will help give research labs and smaller robotics companies access to a high quality hardware platform to interact with the world built for human hands. We’ve dubbed the project “Banana Hand”. Why? Because we all like bananas.\nThis project is still very much so a work in progress and will be for quite a while. I\u0026rsquo;ll be updating progress in a weekly dev log here if you\u0026rsquo;re interested: coming soon\nMy Contribution To The Team I will be primarily focused on writing the firmware for the hand (in Rust) as well as the design of all electrical sub-systems.\nProblem Statement Grasping remains one of the hardest unsolved problems in robotics, as it underpins a robot’s ability to interact with the human world. Yet researchers lack access to a humanoid hand that is affordable, proportional, robust, and well-documented.\nThrough conversations with many different robotics researchers at multiple universities, we learned that most researchers are interested in human-like robotic grasping, however, almost all turn away from solving the problem because of the high barrier to entry. On one end, the most accurate commercial robot hand solutions with the appropriate dexterity and force sensing are massively out of their price range. On the other end, the more affordable options are fragile and often create more headache than contribution to their research. Across this landscape, no option provides the right combination of accessibility, proportional design, sensing capability, and usability. This leads to many researchers settling for basic off-the-shelf two-fingered or 3-fingered grippers, which are extremely limited in human-like object manipulation, force sensing feedback, and grasping complex objects such as power tools and cooking utensils. Without a practical platform to explore dexterous robotic manipulation, academic labs, startups, and industry researchers are forced to abandon the problem — a major bottleneck for progress in humanoid robotics and embodied AI.\nMeanwhile, industry leaders are investing heavily in humanoid robotics — for example, Figure AI has raised over $1 billion at a $39 billion valuation to accelerate development of general-purpose humanoids. This underscores the importance and urgency of the field, yet outside of these highly funded efforts, academic labs and startups are left without practical tools for studying dexterous manipulation.\nIf developments in the robotics scene are moving towards integrating robots into everyday life to save people time and energy, then accessible human-like multipurpose grasping is a problem that absolutely must be solved.\nOur Solution Our team is developing an open-source humanoid hand that combines proportional design, dexterity, tactile sensing, and affordability — attributes that existing solutions fail to deliver in a single platform. The hand is designed to perform the five fundamental human grips (pinch, tripod, hook, cylindrical, spherical), making it a versatile tool for studying real-world manipulation.\nThe hand will balance complexity and accessibility with 16 degrees of freedom and 8 actuated joints, enabling human-like motion without excessive size or cost. This includes a dexterous thumb, a dexterous index finger, and powerful grip from the latter 3 fingers, to maximize versatility and strength in a diverse range of applications. Integrated force sensing in the fingertips and palm will provide both touch detection and localized force feedback, allowing researchers to achieve more precise, closed-loop control during grasping tasks.\nAll design files — including CAD, firmware, ROS2 integration, and testing protocols — will be released under an open-source license, ensuring that researchers can both use and extend the platform. By targeting a cost of $2k–3k and leveraging accessible fabrication methods, the project lowers the barrier to entry for labs, startups, and industry researchers who want to explore dexterous robotic manipulation.\nHow does our solution improve on existing technologies or fill a void in the marketplace? Existing humanoid hands each succeed in one or two areas but fall short in others. High-end options like the Shadow Hand achieve excellent dexterity but cost nearly $100,000 and require large forearms, making them inaccessible to most researchers. Mid-tier open-source solutions such as the ORCA Hand ($8,000) provide dexterity but remain bulky and lack tactile sensing. The Leap Hand is more affordable but compromises proportionality and omits sensing, while ultra-low-cost options like the Amazing Hand ($250) are mechanically simple and unsuitable for serious research due to limited functionality and robustness.\nOur solution fills this gap by combining four attributes that have never been offered together:\nAffordability: A target cost of $2–3k, an order of magnitude less than high-end systems. Proportionality: Human-like kinematics that make interaction with everyday objects realistic. Dexterity: Support for the five fundamental grips with 16 DoF / 8 actuated joints. Tactile Sensing: Force feedback in both the fingertips and palm for closed-loop control. By uniting these features in an open-source platform, our hand becomes the first practical, community-driven research tool for humanoid grasping. It lowers the barrier to entry for labs, startups, and industry researchers, filling a critical void in the marketplace between prohibitively expensive commercial products and under-powered hobbyist models.\n","date":"5 August 2025","externalUrl":null,"permalink":"/projects/banana_hand/","section":"Projects","summary":"An open-source humanoid hand that combines proportional design, dexterity, tactile sensing, and affordability.","title":"BananaHand: Open Source Anthropomorphic Robotic Hand","type":"project"},{"content":" Submission Video Background Developed at MakeUofT 2022 by myself and 3 other group members, the “Anti-Anti Masker Mask” was a light-hearted yet technically complex hackathon project inspired by pandemic-era distancing rules. The system combined computer vision, embedded control, and custom mechanical design to autonomously respond when an unmasked person approached within two meters. System Overview The prototype consisted of three main subsystems: mechanical, sensing and vision, and embedded control.\n1. Mechanical Design The dart launcher, loading system, and shoulder frame were custom-modeled in SolidWorks and fabricated with FDM 3D printing. Because we had no fasteners on hand, we used dovetail joints inspired by woodworking to make the assembly fully snap-fit.\nThe launcher used two brushless drone motors as flywheels, powered by a small Li-Po battery. Darts were fed into the chamber via a rack-and-pinion system driven by a servo motor. Everything was orchestrated by an Arduino Nano, which received firing commands from a laptop.\n2. Sensing and Vision A Time-of-Flight (TOF) distance sensor continuously measured the distance to the nearest person. A laptop ran an OpenCV-based vision pipeline that accessed a live camera feed, performed face detection, and identified whether the detected face was masked.\nIf an unmasked person entered a configurable “danger zone” (roughly 0.8 m – 1.5 m), a command was sent via UART to the Arduino to activate the launcher.\nCode The Arduino handled motor timing, servo control, and safety interlocks. All communication between the laptop and launcher was done over serial, using a simple message protocol for distance and trigger events. We prioritized responsiveness — latency from detection to launch averaged under 200 ms in our demo tests.\nChallenges and Adaptation Our initial plan was to run the entire system TOF sensing, computer vision, and motor control on a Raspberry Pi for a fully self-contained, battery-powered build. Reality struck when we discovered the Pi Zero couldn’t run OpenCV in real-time. It started as a miscommunication as we borrowed what we thought was going to be a Raspberry Pi 3 from a friend, but it ended up being a Pi Zero.\nWith only a few hours left, we refactored the architecture into a split system: the laptop handled sensing and vision, while the Arduino managed actuation. This quick pivot allowed us to finish with a fully functional (and crowd-pleasing) prototype by demo time.\nMore For more info on the challenges we faced, lessons we learned, and future improvements please check out the AAMM DevPost link below : https://devpost.com/software/anti-anti-masker-mask\n","date":"5 March 2019","externalUrl":null,"permalink":"/projects/aamm/","section":"Projects","summary":"A wearable system that detects nearby unmasked individuals and automatically launches soft darts to enforce social distancing. Built in 24 hours during covid.","title":"Anti-Anti Masker Mask [Hackathon: Winner]","type":"project"},{"content":" Background For my first coop placement in my engineering program (May ‘22 - August ‘22), I had the pleasure of working with Philip Beesley (PBSI) at the Living Architecture Systems Group (LASG). It’s a small team of ~15, consisting of engineers, architects, and artists that design and build responsive environments and interactive sculptures. The studio is based out of Toronto Canada but has large scale tech art installations around the world.\nThe research group aims can be summarized with the following question:\nHow can we design kinetic, living architecture that engages with visitors during extended interactions and enhances human experience in an immersive environment?\nMy Work This was my first internship outside of high-school and I was very eager to combine my love for art and engineering together, so the role of a technical industrial designer was perfect. The studio uses a handful of actuators that emit light, movement or sound to evoke the feeling of a living being. I was tasked with creating a new sculptural vibration actuator that was cheap and could be placed in the hundreds within sculptors to achieve field like behaviour. The inspiration for the projects was blades of grass blowing in the wind: Sculpture Actuator - Blade of Grass The studio has successfully used vibration actuators in past to bring life to the sculptures using a device dubbed the Moth. The Moth is an incredibly beautiful actuator but it does have its shortcomings, it has a large part count of 20+ individual pieces for hand assembly, assembly itself is quite difficult, and they cost too much to place thousands of them in the sculpture making field behaviour difficult to achieve.\nThat’s where my project the Blade of Grass comes in. It’s a brand new vibration actuator designed to fill the gaps the Moth has.\nHardware and Design The design criteria for the Blade of Grass were as follows:\nCheap (below 10$/actuator) Low part count Easy assembly Meet aesthetic approval from lead artists and architects The blade of grass is made up of only 5 components; the 3 mm acrylic bow, the mylar blade, the PCB, the vibration motor, and the resin-printed motor sleeve. The low part count and simplicity of the design lead to a low cost of approximately 8$/actuator @ 100 units and total assmebly time of under 2-minutes. The design of the actuator was a very iterative process. Going back and forth with designers refining every last details from the shape of the blade to the radius of fillets on the outline of the PCB. With every prototype I ran stress tests, characterized motion, and created a presentation portfolio to give to the lead architects.\nConclusion While it wasn’t my most technical internship, it was one of the most eye-opening. Collaborating with industrial designers and architects—people who deliberate over every visual and spatial detail—was a completely different rhythm from working with engineers. I found myself sketching, iterating, and participating in design critiques far more than usual, and it deepened my appreciation for the precision and artistry behind this type of work. Not to mention the opportunity to fly across world to install tech art is pretty damn cool. ","date":"10 May 2022","externalUrl":null,"permalink":"/projects/lasg/","section":"Projects","summary":"Overview of my 4-months working at LASG making tech art for installations around the world.","title":"Tech Art @ Living Architecture Systems Group","type":"project"},{"content":"Hi I\u0026rsquo;m Simon \u0026#x1f44b;, a senior studying Mechatronics @ UWaterloo. I like writing firmware (in rust \u0026#x1f980;) and designing PCBs for robots \u0026#x1f916; and green-tech 🌳.\nI\u0026rsquo;m currently working on an Open-Source humanoid hand 🦾\nLooking for new grad opportunities.\nFeatured Projects Safe Sensor Drivers Using Rust's Type System Rust Embedded Using the type-state design pattern with Rust\u0026rsquo;s compiler to enforce proper hardware configuration. SiTerm: Embedded Development Tool Rust Embedded WIP A TUI+Adapter that allows you to interface your laptop via USB to downstream systems and transfer serial messages using I2C, SPI, and UART. ","date":"15 December 2025","externalUrl":null,"permalink":"/","section":"","summary":"","title":"","type":"page"},{"content":"","date":"15 December 2025","externalUrl":null,"permalink":"/tags/c/","section":"Tags","summary":"","title":"C","type":"tags"},{"content":"","date":"15 December 2025","externalUrl":null,"permalink":"/tags/ml/","section":"Tags","summary":"","title":"ML","type":"tags"},{"content":" Here\u0026rsquo;s a collection of some of the projects I\u0026rsquo;ve worked on in my free time, as well as some work I can talk about. ","date":"15 December 2025","externalUrl":null,"permalink":"/projects/","section":"Projects","summary":"","title":"Projects","type":"projects"},{"content":"","date":"15 December 2025","externalUrl":null,"permalink":"/tags/python/","section":"Tags","summary":"","title":"Python","type":"tags"},{"content":"","date":"15 December 2025","externalUrl":null,"permalink":"/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"},{"content":"","date":"15 December 2025","externalUrl":null,"permalink":"/tags/tinyml/","section":"Tags","summary":"","title":"TinyML","type":"tags"},{"content":"","date":"10 October 2025","externalUrl":null,"permalink":"/tags/embedded/","section":"Tags","summary":"","title":"Embedded","type":"tags"},{"content":"","date":"10 October 2025","externalUrl":null,"permalink":"/tags/rust/","section":"Tags","summary":"","title":"Rust","type":"tags"},{"content":"","date":"10 October 2025","externalUrl":null,"permalink":"/tags/wip/","section":"Tags","summary":"","title":"WIP","type":"tags"},{"content":"","date":"5 August 2025","externalUrl":null,"permalink":"/tags/kicad/","section":"Tags","summary":"","title":"KiCAD","type":"tags"},{"content":"","date":"5 August 2025","externalUrl":null,"permalink":"/tags/robotics/","section":"Tags","summary":"","title":"Robotics","type":"tags"},{"content":"","date":"10 May 2022","externalUrl":null,"permalink":"/tags/art/","section":"Tags","summary":"","title":"Art","type":"tags"},{"content":"","date":"10 May 2022","externalUrl":null,"permalink":"/tags/eagle/","section":"Tags","summary":"","title":"Eagle","type":"tags"},{"content":"","date":"10 May 2022","externalUrl":null,"permalink":"/tags/fusion360/","section":"Tags","summary":"","title":"Fusion360","type":"tags"},{"content":"","date":"5 March 2019","externalUrl":null,"permalink":"/tags/3d-printing/","section":"Tags","summary":"","title":"3D Printing","type":"tags"},{"content":"","date":"5 March 2019","externalUrl":null,"permalink":"/tags/c++/","section":"Tags","summary":"","title":"C++","type":"tags"},{"content":"","date":"5 March 2019","externalUrl":null,"permalink":"/tags/hackathon/","section":"Tags","summary":"","title":"Hackathon","type":"tags"},{"content":"","date":"5 March 2019","externalUrl":null,"permalink":"/tags/opencv/","section":"Tags","summary":"","title":"OpenCV","type":"tags"},{"content":" Hi I\u0026rsquo;m Simon, a senior studying Mechatronics Engineering at the University of Waterloo.\nI love working with and designing embedded systems, both the firmware and hardware.\nIn my free time I like to play volleyball, climb, drink a good coffee, spend time with my cat, and enjoy the great outdoors!\nrefresh for a new random portrait\n","externalUrl":null,"permalink":"/about/","section":"","summary":"","title":"About Me","type":"page"},{"content":"","externalUrl":null,"permalink":"/authors/","section":"Authors","summary":"","title":"Authors","type":"authors"},{"content":"","externalUrl":null,"permalink":"/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":" Here are some non-technical things I want to share with the world wide web. ","externalUrl":null,"permalink":"/more/","section":"More","summary":"","title":"More","type":"more"},{"content":"Page dedicated to my Siberian cat Hobbes. He is named after the tiger from the classic comic strip Calvin and Hobbes\n","externalUrl":null,"permalink":"/more/cat/","section":"More","summary":"Some photos of my cat Hobbes.","title":"My Cat: Hobbes","type":"more"},{"content":"coming soon\n","externalUrl":null,"permalink":"/more/photography/","section":"More","summary":"Some photos I\u0026rsquo;ve taken that I like.","title":"Photography","type":"more"},{"content":"","externalUrl":null,"permalink":"/series/","section":"Series","summary":"","title":"Series","type":"series"}]