Matthew's Portfolio

Opal EMG Wristband

My team's capstone proof of concept reads 16 channels of forearm muscle activity, classifies gestures on an ESP32-S3, and drives a phone's media controls over Bluetooth while the wearer holds a ski pole.

I owned gesture recognition and the browser dashboard. The final build used per-don calibration and demonstrated two commands end to end. That architecture replaced a fixed int8 CNN after custom-electrode data exposed wearer and placement shift.

Read more about this project.

Statue Shaper

A published mod for the survival game Vintage Story, and the only thing I have built that strangers use. It lets players carve statues by placing frames, picking a voxel blueprint, and chiselling along chalk lines the mod draws for them.

Written in C#, with an in-game mesh-to-voxel converter that optimises placement for how much chiselling the player has to do. About 5,000 downloads across the main mod and its two published content packs, which supply 69 sculptures voxelised from real 3D scans.

Read more about this project.

1 / 4

Partially carved voxel statue with chalk guides around the remaining blocks

RC Aircraft Design

Designing and building small fixed-wing RC aircraft from scratch rather than from a kit. The work runs the full conceptual design process: a written requirements document, a Python tool that sizes the aircraft by constraint analysis against those requirements, airfoil selection at low Reynolds number, CAD, and construction in foam and 3D print.

Two airframes came out of it. A swept flying wing came first and failed its first flight on a centre-of-gravity error. Sandpiper followed: conventional, high wing, detachable for transport. It took off from a beach, handled poorly, and crashed.

Read more about this project.

1 / 4

Completed Sandpiper high-wing RC aircraft

Context-Aware TTS

A desktop app that turns a web-novel chapter into an audiobook where every character has their own consistent voice. It scrapes the chapter, cleans up the prose with a grammar model, works out who speaks each line of dialogue with a local language model, and matches each character to a real speaker by embedding similarity on their described voice traits.

About 18,000 lines of Rust, running entirely locally. Attribution quality is measured against five hand-labelled chapters, and the engineering log records four prompt-engineering fixes that did nothing before a larger model solved it outright.

Read more about this project.

Solenoid Winding Machine

A group project for ENSC 351. We built a bench top CNC solenoid winding machine with a C-axis spindle and Z-axis linear traverse. I was responsible for the control software, written in Rust and running on a BeagleY-AI. The TUI interface displays real-time motor state and accepts movement commands over local HDMI or remote SSH.

Read more about this project.

1 / 3

Early spindle and controller test on the winding machine frame

Robot Soccer Mechanics Team

Designed to compete in the 2024 robocup small size league. Our team's website is SFU Robot Soccer

I rendered the robot soccer mechanical components in SolidWorks. I have personally designed or revised many of the mechanical components of the robot, most notably the spiral-shaped dribbler, its drive train assembly, the horizontal structural plates, and the main mounting brackets.

The team's prototyping is done with 3D printing since it is rapid and strong enough to do limited testing. To meet our goals, I printed the yellow parts with PETG.

1 / 6

SolidWorks rendering with the robot cover removed to expose its mechanisms

Personal Software Projects

Below are some screenshots of my game development projects. The first image showcases a fog of war shaders and robust selection tools. The second is a game with fully developed gameplay. And finally, the third project was developed with my friends, with a limited colour palette and procedural animations.

1 / 3

Unity strategy-game prototype with fog of war and unit selection

Code Samples

Below are some code samples for several languages I have learned. They are files taken from some of my projects.

Rust: Horde game state machine.

use dupe::{Dupe, OptionDupedExt};
use horde_common::game::{card, player, score, view, Error};
use std::sync::Arc;
use horde_common::game::state::Outcome;
use crate::game::event;
#[derive(Debug, Clone)]
pub(crate) struct State {
players: Arc<[player::Player]>,
turn: usize,
pub(crate) cards: card::List,
}
impl State {
pub(crate) fn new(
players: Arc<[player::Player]>,
mut cards: card::List,
) -> Result<Self, Error> {
const HAND_SIZE: usize = 5;
if players.is_empty() {
return Err(Error::InvalidState(
"State cannot be made with zero players.".to_owned(),
));
}
let card_count = cards.count();
let card_min = HAND_SIZE * players.len();
for player in players.iter() {
if cards.draw_exact(HAND_SIZE, player.id.dupe()).is_none() {
return Err(Error::InvalidState(
format!("State must be able to distribute enough cards. Deck has {card_count} cards, it needs at least {card_min}.")
));
}
}
Ok(Self {
players,
turn: 0,
cards,
})
}
pub(crate) fn view(&self, viewer: &player::Id, outcome: Option<Outcome>) -> view::StateView {
let active = &self.active_player().id;
view::StateView::new(&self.players, &self.cards, viewer, active, outcome)
}
pub(crate) fn apply(&mut self, events: &event::EventList) -> Option<Outcome> {
for event in events.iter() {
self.apply_inner(event, 0);
}
self.make_outcome(events)
}
pub(crate) fn players(&self) -> &[player::Player] {
&self.players
}
pub(crate) fn active_player(&self) -> &player::Player {
&self.players[self.turn % self.players.len()]
}
pub(crate) fn force_outcome(&self) -> Outcome {
let scores = self.players.iter().map(|p| self.make_score(p)).collect();
Outcome { scores }
}
fn make_outcome(&self, events: &event::EventList) -> Option<Outcome> {
if !self.is_game_finished(events) {
return None;
}
let scores = self.players.iter().map(|p| self.make_score(p)).collect();
Some(Outcome { scores })
}
fn make_score(&self, player: &player::Player) -> score::Score {
let hand = self.cards.hand(&player.id);
let adversary = self.cards.hand(&player.adversary);
score::Score::create(player.name.dupe(), hand, adversary)
}
fn is_game_finished(&self, events: &event::EventList) -> bool {
if events.get_sequential_skips() >= self.players.len() {
return true;
}
if self
.cards
.hands()
.iter()
.all(|(_, cards)| cards.iter().all(|c| !c.is_playable()))
{
return true;
}
false
}
fn apply_inner(&mut self, event: &event::Event, block_layer: usize) {
match event {
event::Event::Skip => self.turn += 1,
event::Event::Ability { source, variant } => {
self.apply_ability(source, variant, block_layer)
}
}
}
fn apply_ability(
&mut self,
source: &card::Id,
variant: &event::EventVariant,
block_layer: usize,
) {
self.cards.change_state(source, card::State::Played);
if let event::EventVariant::Block { event } = variant {
return self.apply_inner(event.as_ref(), block_layer + 1);
}
self.turn += 1;
if block_layer % 2 == 1 {
return;
}
match variant {
event::EventVariant::Exchange {
discard,
keep,
drawn,
} => self.apply_exchange(source, discard, keep, drawn),
event::EventVariant::Trade {
own,
responses,
chosen,
} => self.apply_trade(own, responses, chosen),
event::EventVariant::Spy { peeked } => self.apply_spy(peeked),
event::EventVariant::Swap { own, other } => self.apply_swap(own, other),
event::EventVariant::Revive { target } => self.apply_revive(target),
event::EventVariant::ChangeAdversary { target } => {
self.apply_change_adversary(source, target)
}
event::EventVariant::Block { .. } => unreachable!("Block must be handled above."),
}
}
fn apply_exchange(
&mut self,
source: &card::Id,
discard: &[card::Id],
keep: &[card::Id],
drawn: &[card::Id],
) {
let owner = { self.cards.find(source).and_then(|c| c.owner.duped()) };
for id in discard {
self.cards.move_to(id, None);
}
for id in drawn {
self.cards.move_to(id, None);
}
for id in keep {
self.cards.move_to(id, owner.dupe())
}
}
fn apply_trade(
&mut self,
own: &card::Id,
responses: &[(player::Id, card::Id)],
chosen: &Option<card::Id>,
) {
self.cards.change_state(own, card::State::Visible);
for (_, cid) in responses {
self.cards.change_state(cid, card::State::Visible);
}
if let Some(chosen) = chosen {
self.apply_swap(own, chosen);
}
}
fn apply_spy(&mut self, peeked: &[card::Id]) {
for id in peeked {
self.cards.change_state(id, card::State::Visible);
}
}
fn apply_swap(&mut self, a: &card::Id, b: &card::Id) {
let owner_a = { self.cards.find(a).and_then(|c| c.owner.duped()) };
let owner_b = { self.cards.find(b).and_then(|c| c.owner.duped()) };
self.cards.move_to(a, owner_b);
self.cards.move_to(b, owner_a);
}
fn apply_revive(&mut self, target: &card::Id) {
self.cards.change_state(target, card::State::Visible);
}
fn apply_change_adversary(&mut self, source: &card::Id, target: &player::Id) {
let owner = self.cards.find(source).and_then(|c| c.owner);
if let Some(owner) = owner {
let players = Arc::make_mut(&mut self.players);
if let Some(player) = players.iter_mut().find(|p| &p.id == owner) {
player.adversary = target.dupe();
}
}
}
}
view raw state.rs hosted with ❤ by GitHub

JavaScript: Evolution project.

import { Network } from "./network.mjs";
import { Camera } from "./camera.mjs";
import { Namer } from "./namer.mjs";
import { Eye } from "./eye.mjs";
export class Creature {
x;
y;
angle;
color;
heartrate;
satiety;
eyeList;
brainNetwork;
speciesName;
generationName;
constructor(
x,
y,
angle,
color,
heartrate,
satiety,
eyeList,
brainNetwork,
speciesName,
generationName,
parentRef) {
this.x = x;
this.y = y;
this.angle = angle;
this.color = color;
this.heartrate = heartrate;
this.satiety = satiety;
this.eyeList = eyeList;
this.brainNetwork = brainNetwork;
this.speciesName = speciesName;
this.generationName = generationName;
this.parentRef = parentRef;
}
copy() {
const x = this.x;
const y = this.y;
const angle = this.angle;
const color = this.color;
const heartrate = this.heartrate;
const satiety = this.satiety;
const eyeList = this.eyeList.map(eye => eye.copy());
const brainNetwork = this.brainNetwork.copy();
const speciesName = this.speciesName;
const generationName = this.generationName;
const parentRef = this.parentRef;
return new Creature(x,
y,
angle,
color,
heartrate,
satiety,
eyeList,
brainNetwork,
speciesName,
generationName,
parentRef);
}
static speedLinearMax = 0.03;
static speedAngularMax = 0.04;
move(linear, angular, board) {
this.x += Creature.speedLinearMax * linear * Math.cos(this.angle) * this.heartrate;
this.y += Creature.speedLinearMax * linear * Math.sin(this.angle) * this.heartrate;
this.angle += Creature.speedAngularMax * angular * this.heartrate;
// Limit to boundaries
this.x = Math.max(Math.min(this.x, board.size.x), 0);
this.y = Math.max(Math.min(this.y, board.size.y), 0);
}
brainOutputs;
brainInputsMem = [];
brainSave = 0;
static brainInputsMemSize = 100;
runBrain(viewObjects) {
// Get the inputs
const inputs = [];
this.eyeList.forEach(eye => {
const values = eye.getValues(this.x, this.y, this.angle, viewObjects);
inputs.push(values.left);
inputs.push(values.right);
});
// Get internal inputs
inputs.push(0); // Hunger
inputs.push((Math.sin(Math.PI * 2 * this.heartTime) + 1) * 0.5); // Heartbeat
if (this.brainSave <= 0) {
// Save the inputs to memory
for (let index = 0; index < inputs.length; index++) {
if (!this.brainInputsMem[index]) {
this.brainInputsMem[index] = new Array();
}
this.brainInputsMem[index].push(inputs[index]);
if (this.brainInputsMem[index].length > Creature.brainInputsMemSize) {
this.brainInputsMem[index].shift();
}
}
}
this.brainSave = (this.brainSave + 1) % 3;
// Get the outputs
this.brainOutputs = this.brainNetwork.compute(inputs);
}
createMutations() {
var result = [];
// Color
result.push(() => this.color += Math.random() * 0.2 - 0.1);
// Eyes
this.eyeList.forEach(eye => result.push(...eye.createMutations()));
// Brain
result.push(...this.brainNetwork.createMutations());
return result;
}
mutate() {
const avaliableMutations = this.createMutations();
// Small mutations: { eye properties, brain axon values, brain neuron bias, body color }
const chanceSmall = 0.5; // 50% chance for each of the options seperately
avaliableMutations.forEach(mutation => {
if (Math.random() <= chanceSmall) {
mutation(); // Apply the mutation
}
});
// Large mutations: { eye count, brain neuron count }
const chanceLarge = 0.01; // 1% chance
}
createOffspring() {
const children = [];
const childCount = 1;
const childSatiety = 0.5;
for (let i = 0; i < childCount; i++) {
const child = this.copy();
child.mutate();
// Change the generation name
child.generationName = Namer.namer.getName();
child.satiety = childSatiety;
// Change the parent
child.parentRef = this;
children.push(child);
}
this.satiety -= childCount * childSatiety;
return children;
}
brainLinearMovement() {
return Math.min(Math.max(this.brainOutputs[0] - this.brainOutputs[1], 0), 1); // Clamp [0, 1]
}
brainAngularMovement() {
return Math.min(Math.max(this.brainOutputs[2] - this.brainOutputs[3], -1), 1); // Clamp [-1, 1]
}
brainHeartTarget() {
return (this.brainOutputs[4] - this.brainOutputs[5]) / 2 + 0.5;
}
tryEat(eatObject) {
// Check eaten
if (eatObject.isEaten) {
return;
}
const radius = 0.5;
// Check bounding box
const dx = eatObject.x - this.x;
const dy = eatObject.y - this.y;
if (dx < -radius || dx > radius || dy < -radius || dy > radius) {
return;
}
// Check radius
const percentRangeSquared = (dx * dx + dy * dy) / (radius * radius);
if (percentRangeSquared > 1) {
return;
}
// Eat the object
this.satiety = this.satiety + eatObject.eat(); // Clamp (-inf, 1]
}
die() {
this.isDead = true;
}
heartTime = 0;
runBody(board, eatObjects) {
// Move the creature based on brain outputs
const linear = this.brainLinearMovement();
const angular = this.brainAngularMovement();
const heartTarget = this.brainHeartTarget();
// Change the heartrate based on brain outputs
const percent = 0.005;
this.heartrate = Math.max(Math.min(this.heartrate * (1 - percent) + heartTarget * percent, 1), 0);
this.heartTime += this.heartrate * 0.02 + 0.001;
this.heartTime = this.heartTime % 1;
// Reduce satiety based on heartrate
this.satiety -= (this.heartrate + 0.4) * 0.0001;
this.move(linear, angular, board);
// Handle eating
eatObjects.forEach(eatObject => {
this.tryEat(eatObject);
});
// Handle dying
if (this.satiety <= 0) {
this.die();
}
// Handle offspring
if (this.satiety > 1) {
const offspring = this.createOffspring();
offspring.forEach(creature => {
board.addCreature(creature);
});
}
}
draw(ctx, camera) {
const focus = camera.getFocus(this);
if (focus == Camera.Focus.NONE) {
return;
}
const tileSize = camera.getTileSize();
ctx.save();
ctx.translate(this.x * tileSize, this.y * tileSize);
ctx.rotate(this.angle);
this.drawBody(ctx, tileSize, focus);
ctx.restore();
}
drawBody(ctx, tileSize, focus) {
const innerColor = "hsl(" + this.color * 360 + ",75%,75%)";
const outerColor = "hsl(" + this.color * 360 + ",75%,20%)";
// Draw the body
ctx.lineWidth = Math.round(tileSize / 30);
ctx.fillStyle = innerColor;
ctx.strokeStyle = outerColor;
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.arc(0, 0, tileSize / 3, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
// Draw eyes
ctx.strokeStyle = outerColor;
this.eyeList.drawAll(ctx, tileSize, focus);
}
static newRandom(x, y) {
const angle = Math.random() * Math.PI * 2;
const color = Math.random();
const heartrate = 0.1;
const satiety = 1;
const eyeList = new Eye.List();
const eyeCount = Math.round(Math.random() * 3);
for (let j = 0; j < eyeCount; j++) {
eyeList.add(Eye.newRandom());
}
const brainInputCount = 2 + eyeCount * 2; // [ Hunger, Heartbeat, eye1left, eye1right, eye2left ... ]
const brainOutputCount = 6; // [ goForward, goBackward, turnLeft, turnRight, increaseHeartrate, decreaseHeartrate ]
const brainNetwork = Network.newRandom([
brainInputCount,
Math.max(brainInputCount + 1, brainOutputCount + 1),
brainOutputCount,
]);
const speciesName = Namer.namer.getName();
const generationName = Namer.namer.getName();
return new Creature(
x,
y,
angle,
color,
heartrate,
satiety,
eyeList,
brainNetwork,
speciesName,
generationName
);
}
static List = class extends Array {
drawAll(context, camera) {
this.forEach((creature) => creature.draw(context, camera));
}
};
}
view raw creature.mjs hosted with ❤ by GitHub

C#: Unity game project.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class AsteroidManager : MonoBehaviour
{
[Header("References")]
[SerializeField]
private List<GameObject> asteroidPrefabs;
[Header("Spawn Area")]
[SerializeField]
private float spawnRadius = 30.0f;
[SerializeField]
private float spawnDepth = 5.0f;
[SerializeField]
private float minSpawnPeriod = 0.2f; // Seconds per spawn
[SerializeField]
private float maxSpawnPeriod = 1.0f; // Seconds per spawn
[SerializeField]
private int initialBatchSize = 10;
[Header("Spawn Properties")]
[SerializeField]
private float maxForceLinear = 1.0f; // Newton
[SerializeField]
private Vector3 maxForceOffset = new Vector3(0.0f, 0.0f, 0.0f); // { Newton, Newton, Newton }
[SerializeField]
private float maxTorque = 10.0f; // Newton * Meter
[Header("Despawn Area")]
[SerializeField]
private float despawnRadius = 40.0f;
[SerializeField]
private float despawnDepth = 60.0f;
[Header("Variables")]
public List<AsteroidController> asteroids = new List<AsteroidController>();
private Queue<AsteroidController> despawnQueue = new Queue<AsteroidController>();
private float timeToNextSpawn = 0.0f;
public static AsteroidManager manager;
private void Start()
{
if (manager != null)
{
Debug.LogError("AsteroidManager.manager Already exists. There should only be one.");
Destroy(gameObject);
return;
}
manager = this;
for (int i = 0; i < initialBatchSize; i++)
{
SpawnAsteroid();
}
}
private void FixedUpdate()
{
timeToNextSpawn -= Time.fixedDeltaTime;
bool doSpawn = timeToNextSpawn <= 0.0f;
if (doSpawn)
{
SpawnAsteroid();
timeToNextSpawn = GetRandomSpawnTime();
}
// Check despawn area
float despawnDepthSqr = despawnDepth * despawnDepth;
float despawnRadiusSqr = despawnRadius * despawnRadius;
foreach (AsteroidController asteroid in asteroids)
{
Vector3 position = asteroid.transform.position - transform.position;
Vector3 projection = Vector3.Project(position, transform.forward);
float radiusSqr = (position - projection).sqrMagnitude;
float depthSqr = projection.sqrMagnitude;
bool isBehind = Vector3.Dot(transform.forward, position) < 0;
if (depthSqr >= despawnDepthSqr || radiusSqr >= despawnRadiusSqr || isBehind)
{
despawnQueue.Enqueue(asteroid);
}
}
// Despawn everything in the queue
while (despawnQueue.Count > 0)
{
AsteroidController asteroid = despawnQueue.Dequeue();
asteroids.Remove(asteroid);
Destroy(asteroid.gameObject);
}
}
private void OnDrawGizmosSelected()
{
// Draw the spawn range
Gizmos.color = Color.green;
GizmosExpanded.DrawCylinder(transform.position, transform.position + transform.forward * spawnDepth, spawnRadius);
// Draw the despawn range
Gizmos.color = Color.red;
GizmosExpanded.DrawCylinder(transform.position, transform.position + transform.forward * despawnDepth, despawnRadius);
}
private void SpawnAsteroid()
{
// Instantiate the asteroid
GameObject prefab = GetRandomPrefab();
Vector3 position = GetRandomPosition();
Quaternion rotation = GetRandomRotation();
Transform parent = transform;
GameObject asteroidObject = Instantiate(prefab, position, rotation, parent);
AsteroidController asteroid = asteroidObject.GetComponent<AsteroidController>();
// Apply movement
Vector3 force = GetRandomForce();
Vector3 torque = GetRandomTorque();
asteroid.ApplyMovement(force, torque);
// Keep a reference
asteroids.Add(asteroid);
}
private GameObject GetRandomPrefab()
{
int index = Random.Range(0, asteroidPrefabs.Count);
GameObject prefab = asteroidPrefabs[index];
return prefab;
}
private Vector3 GetRandomPosition()
{
float angle = Random.Range(0, Mathf.PI * 2);
float radius = Random.Range(0, spawnRadius);
float depth = Random.Range(0, spawnDepth);
Vector3 depthOffset = depth * transform.forward;
Vector3 circleOffset = GizmosExpanded.GetCirclePosition(angle, radius, transform.forward);
Vector3 position = depthOffset + circleOffset + transform.position;
return position;
}
private Quaternion GetRandomRotation()
{
Quaternion rotation = Quaternion.identity;
return rotation;
}
private Vector3 GetRandomForce()
{
float forceLinear = Random.Range(0, maxForceLinear);
Vector3 forceOffset = Vector3.Scale(Random.insideUnitSphere, maxForceOffset);
Vector3 force = transform.forward * forceLinear + forceOffset;
return force;
}
private Vector3 GetRandomTorque()
{
Vector3 axis = Random.onUnitSphere;
float amount = Random.Range(0.0f, maxTorque);
Vector3 torque = axis * amount;
return torque;
}
private float GetRandomSpawnTime()
{
float time = Random.Range(minSpawnPeriod, maxSpawnPeriod);
return time;
}
}

MATLAB: Stock plotter.

% Clear everything
clear;
clc;
close all;
% Read the data from the AAPL.csv file
data = readtable("AAPL.csv", "VariableNamingRule", "preserve");
% Draw the High, Low, Close, and Volume plots within one figure
figure;
x = data.Date(:);
subplot(2,2,1); plot(x, data.High(:)); title("i) High"); xlabel("Time"); ylabel("Money");
subplot(2,2,2); plot(x, data.Low(:)); title("ii) Low"); xlabel("Time"); ylabel("Money");
subplot(2,2,3); plot(x, data.Close(:)); title("iii) Close"); xlabel("Time"); ylabel("Money");
subplot(2,2,4); plot(x, data.Volume(:)); title("iv) Volume"); xlabel("Time"); ylabel("Shares");
% Draw the High, Low, Open, Close figure
figure;
highlow(data);
% Calculate the average price
data.Average = (data.Open + data.Close)./2;
% Save the new data to the APPL-new.csv
writetable(data,"APPL-new.csv");
view raw stock-plotter.m hosted with ❤ by GitHub

VHDL: Alarm system.

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity AlarmSystem is
port(CLOCK_50: in std_logic;
SW: in std_logic_vector(17 downto 0);
KEY: in std_logic_vector(2 downto 0);
LEDG: out std_logic_vector(7 downto 0);
LEDR: out std_logic_vector(17 downto 0);
HEX7, HEX6, HEX5, HEX4, HEX2, HEX1, HEX0: out std_logic_vector(6 downto 0);
-- AUDIO STUFF
I2C_SDAT : inout std_logic;
I2C_SCLK, AUD_XCK : out std_logic;
AUD_ADCDAT : in std_logic;
AUD_DACDAT : out std_logic;
AUD_ADCLRCK, AUD_DACLRCK, AUD_BCLK : in std_logic);
end AlarmSystem;
architecture behaviour of AlarmSystem is
-- Define the component types
component BlinkSystem is
port(enable, slowClock: std_logic;
Seg3, Seg2, Seg1, Seg0: out std_logic_vector(6 downto 0);
green: out std_logic_vector(7 downto 0);
red: out std_logic_vector(17 downto 0));
end component;
component DisarmSystem is
port(clock: in std_logic;
w: in std_logic_vector(1 downto 0);
sseg2, sseg1, sseg0: out std_logic_vector(6 downto 0);
disarm: out std_logic;
stateOut: out std_logic_vector(1 downto 0));
end component;
component PreScale is
port(inClock: in std_logic; outClock: out std_logic);
end component;
-- Setup states
signal trigger, isActive, frontDoor, sideWindow, motionSensor: std_logic := '0';
signal disarm: std_logic := '1';
signal slowClock: std_logic;
-- Define the Audio types
signal AudioIn, AudioOut : signed(15 downto 0);
signal SamClk: std_logic;
component AudioInterface is
generic ( SID : integer := 100 );
port (CLOCK_50 : in std_logic;
init : in std_logic;
I2C_SDAT : inout std_logic;
I2C_SCLK, AudMclk : out std_logic;
AUD_ADCDAT : in std_logic;
AUD_DACDAT : out std_logic;
AUD_ADCLRCK, AUD_DACLRCK, AUD_BCLK : in std_logic;
SamClk : out std_logic;
AudioIn : out signed(15 downto 0);
AudioOut : in signed(15 downto 0));
end component;
component ToneGenerator is
port(clock, enable: in std_logic; Freq: in unsigned(15 downto 0); WaveOut: out signed(15 downto 0));
end component;
begin
-- Generate slow clock
scale: PreScale port map(CLOCK_50, slowClock);
-- Instantiate the major systems
ds: DisarmSystem port map(KEY(0), SW(2 downto 1), HEX2, HEX1, HEX0, disarm);
bs: BlinkSystem port map(trigger, slowClock, HEX7, HEX6, HEX5, HEX4, LEDG, LEDR);
-- Handle the states
isActive <= SW(0);
frontDoor <= SW(17);
sideWindow <= SW(16);
motionSensor <= SW(15);
process (CLOCK_50)
begin
if CLOCK_50='1' then
if disarm='1' or KEY(1)='0' then
trigger <= '0';
end if;
if (isActive and (frontDoor or sideWindow or motionSensor))='1' then
trigger <= '1';
end if;
end if;
end process;
-- Handle Audio
assm: AudioInterface generic map ( SID => 51512)
port map( Clock_50 => CLOCK_50, AudMclk => AUD_XCK, -- period is 80 ns ( 12.5 Mhz )
init => KEY(2), -- +ve edge initiates I2C data
I2C_Sclk => I2C_SCLK,
I2C_Sdat => I2C_SDAT,
AUD_BCLK => AUD_BCLK, AUD_ADCLRCK => AUD_ADCLRCK, AUD_DACLRCK => AUD_DACLRCK,
AUD_ADCDAT => AUD_ADCDAT, AUD_DACDAT => AUD_DACDAT,
AudioOut => AudioOut, AudioIn => AudioIn, SamClk => SamClk );
tg: ToneGenerator port map(clock => SamClk, enable => slowClock and trigger, Freq => "0100000000000000", waveOut => AudioOut);
end architecture behaviour;
view raw AlarmSystem.vhd hosted with ❤ by GitHub

This website is a good example of my skill with HTML, JS, and CSS.

Class Projects

The first image is a hand-soldered digital dice circuit (for the SFU class ENSC 220) with through-hole and surface mount components. The end result simulates rolling a die when you press the capacitive button.

The second and third images are of an automatic window actuation system controlled by weather forecast (for the SFU classes ENSC 100 and ENSC 105W). The project was completed from the ground up in a small team, with the goal of solving a problem we encounter in the world.

1 / 3

Hand-soldered digital dice circuit with through-hole and surface-mount components

Personal Mechanical Projects

These images show some iterations of a tiny pocket-watch-like mechanism. As a design requirement, 3D printing was the manufacturing method.

1 / 4

Assembled 3D-printed escapement mechanism with stacked gear train

Below are a few photos of some old and small personal projects:

  • Prototypes for a mechanical iris mechanism.
  • A mechanism that follows the same motion as a Strandbeest.
  • And a practical set of hangers for the back of my old door.

1 / 2

Two 3D-printed mechanical iris prototypes

mpg3@sfu.ca, linkedin · Updated 2026-08-13