[{"data":1,"prerenderedAt":1218},["ShallowReactive",2],{"projects":3},[4,89,237,663,750,857,995],{"id":5,"title":6,"body":7,"date":71,"description":72,"extension":73,"icon":74,"image":75,"live":76,"meta":77,"navigation":78,"path":79,"repo":76,"seo":80,"stem":81,"tags":82,"__hash__":88},"projects\u002Fprojects\u002Faudio-steganography.md","Audio Steganography",{"type":8,"value":9,"toc":62},"minimark",[10,15,24,28,35,38,42,45,49,52,56,59],[11,12,14],"h2",{"id":13},"overview","Overview",[16,17,18,19,23],"p",{},"Steganography is the practice of hiding information within an ordinary, non-secret file so that the existence of the hidden message is concealed. This project implemented ",[20,21,22],"strong",{},"audio steganography"," — embedding text messages inside WAV audio files in a way that is imperceptible to human listeners.",[11,25,27],{"id":26},"approach","Approach",[16,29,30,31,34],{},"Rather than using the standard LSB (Least Significant Bit) sequential method — which is detectable by steganalysis tools that look for patterns in bit distributions — this implementation uses a ",[20,32,33],{},"seeded pseudo-random number generator (PRNG)"," to scatter the payload bits across the audio samples in a randomised order.",[16,36,37],{},"The same seed is required for extraction, acting as a shared secret between sender and receiver.",[11,39,41],{"id":40},"extraction","Extraction",[16,43,44],{},"The extraction process mirrors embedding: initialise the PRNG with the same seed, regenerate the identical random index sequence, read the LSB from each indexed sample, and reconstruct the byte stream.",[11,46,48],{"id":47},"results","Results",[16,50,51],{},"Embedding a 256-byte payload into a 44.1 kHz, 16-bit mono WAV file resulted in a Signal-to-Noise Ratio (SNR) degradation of less than 0.003 dB — well below the threshold of human auditory perception (~1 dB). Spectral analysis in Audacity showed no visible artefacts compared to the original.",[11,53,55],{"id":54},"learnings","Learnings",[16,57,58],{},"The project highlighted how subtle changes to raw audio data — flipping only the least significant bit of each sample — are mathematically tiny (a change of 1 in 65536 for 16-bit audio) and yet sufficient to carry meaningful information. The RNG-based scatter approach significantly reduces the statistical signature of the payload compared to sequential LSB methods.",[60,61],"ui-git-hub-card",{},{"title":63,"searchDepth":64,"depth":64,"links":65},"",2,[66,67,68,69,70],{"id":13,"depth":64,"text":14},{"id":26,"depth":64,"text":27},{"id":40,"depth":64,"text":41},{"id":47,"depth":64,"text":48},{"id":54,"depth":64,"text":55},"Aug 2023","An implementation of audio steganography that hides secret messages inside audio files using a random number generator for bit placement.","md","music_note","\u002Fassets\u002Fproj-audio.jpg",null,{},true,"\u002Fprojects\u002Faudio-steganography",{"title":6,"description":72},"projects\u002Faudio-steganography",[83,84,85,86,87],"Python","Cryptography","DSP","Steganography","RNG","VYfR_LTm63lftkEuvN_kjL2KPKfz4YKHucjRjXfXr_w",{"id":90,"title":91,"body":92,"date":223,"description":224,"extension":73,"icon":225,"image":226,"live":76,"meta":227,"navigation":78,"path":228,"repo":76,"seo":229,"stem":230,"tags":231,"__hash__":236},"projects\u002Fprojects\u002Fcovid-infection-tracker.md","COVID-19 Infection Tracker",{"type":8,"value":93,"toc":217},[94,96,99,106,110,115,131,136,153,157,183,185,188,214],[11,95,14],{"id":13},[16,97,98],{},"Built in 2020 during the height of the COVID-19 pandemic, this full-stack application aggregated real-time infection statistics from trusted global data sources and presented them through an interactive, country-searchable dashboard. The backend (Python) normalised data from multiple sources into a unified REST API, while the frontend (Vue.js) provided live visualisations and statistical metrics.",[16,100,101],{},[102,103],"img",{"alt":104,"src":105},"COVID-19 Dashboard","\u002Fassets\u002Fcoronavirus.jpg",[11,107,109],{"id":108},"architecture","Architecture",[16,111,112],{},[20,113,114],{},"Backend (Python)",[116,117,118,122,125,128],"ul",{},[119,120,121],"li",{},"Aggregated COVID-19 data from multiple trusted sources",[119,123,124],{},"Normalised inconsistent data formats into a standardised schema",[119,126,127],{},"Exposed a RESTful API for frontend consumption",[119,129,130],{},"Scheduled periodic data refreshes to maintain near real-time statistics",[16,132,133],{},[20,134,135],{},"Frontend (Vue.js)",[116,137,138,141,144,147,150],{},[119,139,140],{},"Responsive dashboard with global and country-level statistics",[119,142,143],{},"Real-time search and filtering by country",[119,145,146],{},"Colour-coded stat cards for quick visual assessment (cases, deaths, recoveries, critical cases)",[119,148,149],{},"Per-million metrics for contextual comparison across regions",[119,151,152],{},"Mobile-optimised layout",[11,154,156],{"id":155},"key-features","Key Features",[116,158,159,165,171,177],{},[119,160,161,164],{},[20,162,163],{},"Global summary"," — aggregate worldwide totals across all metrics",[119,166,167,170],{},[20,168,169],{},"Searchable country index"," — instant lookup of any country's current statistics",[119,172,173,176],{},[20,174,175],{},"Stat cards"," — total cases, active cases, recoveries, deaths, critical cases, new cases\u002Fdeaths, and per-1M ratios",[119,178,179,182],{},[20,180,181],{},"Responsive design"," — seamless experience across desktop and mobile devices",[11,184,55],{"id":54},[16,186,187],{},"This project was formative in understanding full-stack data pipelines. Key takeaways included:",[116,189,190,196,202,208],{},[119,191,192,195],{},[20,193,194],{},"Data normalisation"," — reconciling inconsistent reporting formats from different sources",[119,197,198,201],{},[20,199,200],{},"API design"," — building reliable endpoints that serve rapidly changing data",[119,203,204,207],{},[20,205,206],{},"Component-based UI"," — structuring Vue.js components for reusability and maintainability",[119,209,210,213],{},[20,211,212],{},"Real-time systems"," — handling data freshness, caching, and user expectations during a global crisis",[16,215,216],{},"The experience reinforced the value of clean, accessible dashboards during critical events — users relied on this application to make informed decisions about health and safety.",{"title":63,"searchDepth":64,"depth":64,"links":218},[219,220,221,222],{"id":13,"depth":64,"text":14},{"id":108,"depth":64,"text":109},{"id":155,"depth":64,"text":156},{"id":54,"depth":64,"text":55},"May 2020","A full-stack data aggregation and visualisation platform tracking real-time COVID-19 statistics globally, with a Python backend and Vue.js frontend.","public","\u002Fassets\u002Fproj-covid.jpg",{},"\u002Fprojects\u002Fcovid-infection-tracker",{"title":91,"description":224},"projects\u002Fcovid-infection-tracker",[83,232,233,234,235],"Vue.js","Data Aggregation","Visualisation","API","8_Y6g_l1BJI_K9tIxIcEszWUJ9I-rrB7OLC7Naqg1_c",{"id":238,"title":239,"body":240,"date":646,"description":647,"extension":73,"icon":648,"image":649,"live":76,"meta":650,"navigation":78,"path":651,"repo":76,"seo":652,"stem":653,"tags":654,"__hash__":662},"projects\u002Fprojects\u002Fdroid-slam-hpc-port.md","DROID-SLAM HPC Port",{"type":8,"value":241,"toc":633},[242,244,254,258,261,292,296,303,308,484,488,503,507,518,522,529,575,590,594,597,604,607,609,624,626,629],[11,243,14],{"id":13},[16,245,246,253],{},[247,248,252],"a",{"href":249,"rel":250},"https:\u002F\u002Fgithub.com\u002Fprinceton-vl\u002FDROID-SLAM",[251],"nofollow","DROID-SLAM"," is a deep-learning-based visual odometry system developed at Princeton. During my research internship at the University of Cape Town (UCT), I was tasked with making it run reliably on UCT's HPC cluster and building tooling to make the whole workflow manageable from a local machine.",[11,255,257],{"id":256},"the-challenges","The Challenges",[16,259,260],{},"Running deep-learning pipelines on an HPC cluster is rarely plug-and-play:",[116,262,263,274,280,286],{},[119,264,265,268,269,273],{},[20,266,267],{},"No X server"," — the cluster has no display server. DROID-SLAM's built-in visualiser cannot run; ",[270,271,272],"code",{},"--disable_vis"," is mandatory, which meant the standard reconstruction export path was broken",[119,275,276,279],{},[20,277,278],{},"No outbound internet on compute nodes"," — all dependencies and container images must be pre-staged",[119,281,282,285],{},[20,283,284],{},"SLURM job queues"," — GPU jobs are submitted as batch scripts with no interactive debugging",[119,287,288,291],{},[20,289,290],{},"Dependency isolation"," — DROID-SLAM requires specific PyTorch and CUDA extension versions incompatible with the cluster's global modules",[11,293,295],{"id":294},"droidslamcli-the-go-cli","DROIDSLAMCLI — The Go CLI",[16,297,298,299,302],{},"The core deliverable was a full ",[20,300,301],{},"Go CLI application"," built with the Cobra and Viper frameworks. It wraps the entire DROID-SLAM workflow — validating inputs, templating SLURM scripts, uploading data to the cluster, submitting jobs, monitoring progress, and pulling results back — all over SSH\u002FSFTP from a local machine.",[304,305,307],"h3",{"id":306},"commands","Commands",[309,310,314],"pre",{"className":311,"code":312,"language":313,"meta":63,"style":63},"language-bash shiki shiki-themes github-light github-dark","# Submit an inference job\ndroidslamcli infer --config config.yaml\n\n# Submit a training job\ndroidslamcli train --config config.yaml\n\n# Check running jobs\ndroidslamcli status\n\n# Stream stdout from a specific job\ndroidslamcli status --jobID=\u003CjobID>\n\n# Extract results for a specific job\ndroidslamcli extract infer --jobId=\u003CjobID> --location=.\u002Fresults --config config.yaml\n\n# Extract results for all jobs\ndroidslamcli extract infer -a --location=.\u002Fresults --config config.yaml\n","bash",[270,315,316,325,342,348,354,366,371,377,385,390,396,417,422,428,455,460,466],{"__ignoreMap":63},[317,318,321],"span",{"class":319,"line":320},"line",1,[317,322,324],{"class":323},"sJ8bj","# Submit an inference job\n",[317,326,327,331,335,339],{"class":319,"line":64},[317,328,330],{"class":329},"sScJk","droidslamcli",[317,332,334],{"class":333},"sZZnC"," infer",[317,336,338],{"class":337},"sj4cs"," --config",[317,340,341],{"class":333}," config.yaml\n",[317,343,345],{"class":319,"line":344},3,[317,346,347],{"emptyLinePlaceholder":78},"\n",[317,349,351],{"class":319,"line":350},4,[317,352,353],{"class":323},"# Submit a training job\n",[317,355,357,359,362,364],{"class":319,"line":356},5,[317,358,330],{"class":329},[317,360,361],{"class":333}," train",[317,363,338],{"class":337},[317,365,341],{"class":333},[317,367,369],{"class":319,"line":368},6,[317,370,347],{"emptyLinePlaceholder":78},[317,372,374],{"class":319,"line":373},7,[317,375,376],{"class":323},"# Check running jobs\n",[317,378,380,382],{"class":319,"line":379},8,[317,381,330],{"class":329},[317,383,384],{"class":333}," status\n",[317,386,388],{"class":319,"line":387},9,[317,389,347],{"emptyLinePlaceholder":78},[317,391,393],{"class":319,"line":392},10,[317,394,395],{"class":323},"# Stream stdout from a specific job\n",[317,397,399,401,404,407,411,414],{"class":319,"line":398},11,[317,400,330],{"class":329},[317,402,403],{"class":333}," status",[317,405,406],{"class":337}," --jobID=",[317,408,410],{"class":409},"szBVR","\u003C",[317,412,413],{"class":337},"jobID",[317,415,416],{"class":409},">\n",[317,418,420],{"class":319,"line":419},12,[317,421,347],{"emptyLinePlaceholder":78},[317,423,425],{"class":319,"line":424},13,[317,426,427],{"class":323},"# Extract results for a specific job\n",[317,429,431,433,436,438,441,443,445,448,451,453],{"class":319,"line":430},14,[317,432,330],{"class":329},[317,434,435],{"class":333}," extract",[317,437,334],{"class":333},[317,439,440],{"class":337}," --jobId=",[317,442,410],{"class":409},[317,444,413],{"class":337},[317,446,447],{"class":409},">",[317,449,450],{"class":337}," --location=.\u002Fresults",[317,452,338],{"class":337},[317,454,341],{"class":333},[317,456,458],{"class":319,"line":457},15,[317,459,347],{"emptyLinePlaceholder":78},[317,461,463],{"class":319,"line":462},16,[317,464,465],{"class":323},"# Extract results for all jobs\n",[317,467,469,471,473,475,478,480,482],{"class":319,"line":468},17,[317,470,330],{"class":329},[317,472,435],{"class":333},[317,474,334],{"class":333},[317,476,477],{"class":337}," -a",[317,479,450],{"class":337},[317,481,338],{"class":337},[317,483,341],{"class":333},[304,485,487],{"id":486},"ssh-sftp","SSH \u002F SFTP",[16,489,490,491,494,495,498,499,502],{},"The CLI communicates with the HPC cluster entirely over SSH. Commands like ",[270,492,493],{},"sbatch",", ",[270,496,497],{},"squeue",", and path setup are executed as remote shell commands, while files (images, calibration data, model weights) are transferred and results retrieved via SFTP. This meant the entire workflow — from job submission to results extraction — could be driven from a local Windows or macOS machine without needing to manually ",[270,500,501],{},"scp"," files or log in to the cluster.",[304,504,506],{"id":505},"singularity-instead-of-docker","Singularity instead of Docker",[16,508,509,510,513,514,517],{},"Most HPC clusters don't allow Docker because it requires daemon-level (root) access, which is a security risk in a shared multi-user environment. Instead, the cluster supports ",[20,511,512],{},"Singularity",", which runs containers as the current user with no elevated privileges. The CLI and SLURM runner scripts were built around Singularity — pulling the DROID-SLAM Docker image from DockerHub and converting it to a Singularity Image File (",[270,515,516],{},".sif",") on first use, then reusing the cached image for subsequent jobs.",[304,519,521],{"id":520},"inference-pipeline","Inference Pipeline",[16,523,524,525,528],{},"When ",[270,526,527],{},"droidslamcli infer"," is run:",[530,531,532,546,552,558,567],"ol",{},[119,533,534,537,538,541,542,545],{},[20,535,536],{},"Validates"," local input files — PNG images directory, ",[270,539,540],{},".txt"," calibration file, ",[270,543,544],{},".pth"," model weights",[119,547,548,551],{},[20,549,550],{},"Generates a UUID"," as the job ID for isolation",[119,553,554,557],{},[20,555,556],{},"Creates the remote directory structure"," and uploads calibration file, model weights, and all images via SFTP",[119,559,560,563,564],{},[20,561,562],{},"Templates"," the SLURM header and runner scripts with config values (account, time limit, GPU type, partition) using Go's ",[270,565,566],{},"text\u002Ftemplate",[119,568,569,572,573],{},[20,570,571],{},"Submits"," the job via ",[270,574,493],{},[16,576,577,578,581,582,585,586,589],{},"The SLURM runner script on the compute node then downloads the Singularity image, clones the ",[270,579,580],{},"headless_changes"," fork, compiles it inside the container, and executes inference with ",[270,583,584],{},"--save_headless --disable_vis",". Results are pulled back to the local machine via ",[270,587,588],{},"droidslamcli extract",".",[11,591,593],{"id":592},"forking-droid-slam-making-it-headless","Forking DROID-SLAM — Making it Headless",[16,595,596],{},"DROID-SLAM was never designed to run without a screen. Its visualiser — the part that shows you the 3D reconstruction being built in real time — and the code that actually saves the output were tangled together. On an HPC cluster there's no screen, so the visualiser crashes immediately, and with it, any chance of getting results out.",[16,598,599,600,603],{},"The fix was to fork the project and separate these two concerns. I wrote a new headless export module that does everything the visualiser does internally — reading the 3D data DROID-SLAM builds up during processing — but instead of drawing it on screen, it just saves the output straight to files: a point cloud of the reconstructed scene and a record of where the camera was at each moment. A new ",[270,601,602],{},"--save_headless"," flag was added so you could opt into this behaviour from the command line.",[16,605,606],{},"A few other papercuts were fixed along the way: the output path logic had a bug that was nesting results in an unintended subfolder, the trajectory data wasn't being saved at all, and the training pipeline was hardcoded to only work with one specific dataset format. That last fix meant the system could now be trained on custom-captured footage rather than being locked to a single public dataset.",[11,608,48],{"id":47},[116,610,611,618,621],{},[119,612,613,614,617],{},"Successfully ran DROID-SLAM inference on the ",[20,615,616],{},"TUM RGB-D"," dataset on the HPC cluster's A100 GPU nodes",[119,619,620],{},"Reproduced trajectory results matching the paper's benchmarks",[119,622,623],{},"The fork, CLI, and containerised workflow were documented and handed off for continued use by the UCT robotics research group",[11,625,55],{"id":54},[16,627,628],{},"This project touched a wide stack — SLURM job scheduling, Singularity containers, SSH automation in Go, and the internals of a deep-learning visual odometry system. The headless export problem was the most interesting challenge: to solve it I had to understand how DROID-SLAM builds up its 3D reconstruction internally and find a way to get that data out without any of the display infrastructure it was designed around.",[630,631,632],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":63,"searchDepth":64,"depth":64,"links":634},[635,636,637,643,644,645],{"id":13,"depth":64,"text":14},{"id":256,"depth":64,"text":257},{"id":294,"depth":64,"text":295,"children":638},[639,640,641,642],{"id":306,"depth":344,"text":307},{"id":486,"depth":344,"text":487},{"id":505,"depth":344,"text":506},{"id":520,"depth":344,"text":521},{"id":592,"depth":64,"text":593},{"id":47,"depth":64,"text":48},{"id":54,"depth":64,"text":55},"Jul 2023","Ported the DROID-SLAM visual-odometry framework to UCT's HPC environment and built a full Go CLI for executing, managing, and extracting inference and training jobs over SSH.","memory","\u002Fassets\u002Fproj-hpc.jpg",{},"\u002Fprojects\u002Fdroid-slam-hpc-port",{"title":239,"description":647},"projects\u002Fdroid-slam-hpc-port",[83,655,656,657,658,659,660,512,661],"Docker","SLAM","Linux","Go","HPC","SLURM","SSH","55JWuPR6RHe4i6KSYWk5r0hielir1iTSCO67WfHnngE",{"id":664,"title":665,"body":666,"date":735,"description":736,"extension":73,"icon":737,"image":738,"live":76,"meta":739,"navigation":78,"path":740,"repo":76,"seo":741,"stem":742,"tags":743,"__hash__":749},"projects\u002Fprojects\u002Fhat-for-stm32.md","HAT for the STM32",{"type":8,"value":667,"toc":731},[668,670,673,677,682,693,698,709,714,725],[11,669,14],{"id":13},[16,671,672],{},"This project centred around building an extension kit for the STM32 Discovery Board. The goal was to add more sensors and features to the board with a plug-and-play approach.",[11,674,676],{"id":675},"my-responsibility","My Responsibility",[16,678,679],{},[20,680,681],{},"Sensor Selection",[116,683,684,687,690],{},[119,685,686],{},"Evaluated and selected appropriate sensors for the desired functionality",[119,688,689],{},"Chosen sensors: temperature, humidity, and proximity\u002Fdistance sensors",[119,691,692],{},"Considered specifications, compatibility, and integration complexity",[16,694,695],{},[20,696,697],{},"PCB Design",[116,699,700,703,706],{},[119,701,702],{},"Designed the sensor subsystem on the PCB using KiCad",[119,704,705],{},"Laid out sensor connections and support circuitry for reliability",[119,707,708],{},"Calculated the power budget to ensure the subsystem could be sustainably powered",[16,710,711],{},[20,712,713],{},"Firmware Integration",[116,715,716,719,722],{},[119,717,718],{},"Wrote code to integrate the sensors with the rest of the system",[119,720,721],{},"Built clean interfaces that made it easy for others to adopt the sensors in their work",[119,723,724],{},"Ensured the plug-and-play experience worked seamlessly with the STM32",[16,726,727],{},[102,728],{"alt":729,"src":730},"HAT PCB Board","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1610878785620-3ab2d3a2ae7b?q=80&w=1120&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",{"title":63,"searchDepth":64,"depth":64,"links":732},[733,734],{"id":13,"depth":64,"text":14},{"id":675,"depth":64,"text":676},"2022","An extension kit for the STM32 Discovery Board designed to add sensors and features with a plug-and-play approach.","developer_board","\u002Fassets\u002Fproj-pcb.jpg",{},"\u002Fprojects\u002Fhat-for-stm32",{"title":665,"description":736},"projects\u002Fhat-for-stm32",[744,745,746,697,747,748],"STM32","C","Embedded","KiCad","Sensor Integration","iebr4ya5NoJcVDunlR4gBSBJPv287USJICzbU2XDeNg",{"id":751,"title":752,"body":753,"date":844,"description":845,"extension":73,"icon":846,"image":847,"live":76,"meta":848,"navigation":78,"path":849,"repo":76,"seo":850,"stem":851,"tags":852,"__hash__":856},"projects\u002Fprojects\u002Fmaze-solving-robot.md","Maze Solving Robot",{"type":8,"value":754,"toc":836},[755,757,764,768,771,775,782,793,797,800,826,828,831,833],[11,756,14],{"id":13},[16,758,759,760,763],{},"Built as part of a university design challenge, this robot had to autonomously navigate a physical maze featuring curved paths, right-angle turns, dead ends, and long straight runs. Working as a 3-person team, we placed ",[20,761,762],{},"1st out of 20 competing teams"," — finishing the maze faster than every other group in the class.",[11,765,767],{"id":766},"hardware","Hardware",[16,769,770],{},"The robot was assembled from pre-built components — motors, sensor arrays, a microcontroller unit, and a motor driver — and integrated onto a chassis. Rather than building hardware from scratch, the challenge was in characterising and modelling these components accurately enough to build a reliable control system on top of them.",[11,772,774],{"id":773},"modelling-control-system-design","Modelling & Control System Design",[16,776,777,778,781],{},"The centrepiece of the project was developing a mathematically grounded control system using ",[20,779,780],{},"MATLAB Simulink",". We first characterised the physical parameters of the robot — wheel radius, wheelbase, motor response — and used these to derive a dynamic model of how the platform behaves.",[16,783,784,785,788,789,792],{},"From that model, we designed controllers that translated high-level commands into precise physical motion. The result was a system where commanding the robot to move ",[20,786,787],{},"1 metre forward"," would produce exactly 1 metre of travel, and commanding a ",[20,790,791],{},"20-degree turn"," would yield exactly 20 degrees of rotation — repeatably and without manual tuning guesswork. This model-based approach gave us a level of precision and predictability that was difficult to achieve through empirical calibration alone.",[11,794,796],{"id":795},"algorithm","Algorithm",[16,798,799],{},"The maze-solving logic was implemented in MATLAB Simulink and deployed to the embedded target. The robot used IR sensors to follow the line and had to correctly handle every topology the maze could present:",[116,801,802,808,814,820],{},[119,803,804,807],{},[20,805,806],{},"Curved paths",": Continuous sensor feedback with differential speed adjustment to keep the robot on the line through bends",[119,809,810,813],{},[20,811,812],{},"Right-angle turns",": Detected by the sensor pattern crossing a junction, executing a precise 90° rotation using the calibrated control system",[119,815,816,819],{},[20,817,818],{},"3-way and 4-way decision points",": Junction detection logic to classify the intersection type and select the correct path",[119,821,822,825],{},[20,823,824],{},"Dead ends",": Detected when the line disappears entirely, triggering a 180° turn to backtrack",[11,827,48],{"id":47},[16,829,830],{},"The robot consistently completed the full maze in under 30 seconds during competition runs. Precise motor calibration and aggressive PID tuning were the key factors that differentiated the team's performance from other competitors.",[11,832,55],{"id":54},[16,834,835],{},"The biggest takeaway was the value of model-based design. Rather than tuning behaviour empirically, grounding the system in measured physical parameters made our controller reliable from the start. It also deepened my understanding of how mathematics translates into real-world behaviour — and how small inaccuracies in a model (motor variance, surface friction) can compound in a physical system.",{"title":63,"searchDepth":64,"depth":64,"links":837},[838,839,840,841,842,843],{"id":13,"depth":64,"text":14},{"id":766,"depth":64,"text":767},{"id":773,"depth":64,"text":774},{"id":795,"depth":64,"text":796},{"id":47,"depth":64,"text":48},{"id":54,"depth":64,"text":55},"Nov 2022","A line-following robot programmed to navigate and solve a maze featuring curved paths, right angles, 4-way and 3-way decision points, and dead ends — the fastest of 20 competing teams.","smart_toy","\u002Fassets\u002Fproj-robot.jpg",{},"\u002Fprojects\u002Fmaze-solving-robot",{"title":752,"description":845},"projects\u002Fmaze-solving-robot",[853,746,767,854,855],"MATLAB","Robotics","Control Systems","DhJufZWJJfDNcajf7It4gswWHx1lkqnNngb-xZil15A",{"id":858,"title":859,"body":860,"date":982,"description":983,"extension":73,"icon":984,"image":985,"live":76,"meta":986,"navigation":78,"path":987,"repo":76,"seo":988,"stem":989,"tags":990,"__hash__":994},"projects\u002Fprojects\u002Fnest-observation-kit.md","Red-winged Starling Nest Camera",{"type":8,"value":861,"toc":975},[862,864,867,869,929,933,936,947,951,954,968,972],[11,863,14],{"id":13},[16,865,866],{},"Built as a university project, this system passively monitored red-winged starling nests in remote areas of the University of Cape Town campus. The goal was to provide ornithological researchers with a non-invasive, automated way to track nesting activity across multiple simultaneously deployed units.",[11,868,767],{"id":766},[870,871,872,885],"table",{},[873,874,875],"thead",{},[876,877,878,882],"tr",{},[879,880,881],"th",{},"Component",[879,883,884],{},"Purpose",[886,887,888,897,905,913,921],"tbody",{},[876,889,890,894],{},[891,892,893],"td",{},"Raspberry Pi Zero W",[891,895,896],{},"Main compute and wireless connectivity",[876,898,899,902],{},[891,900,901],{},"Miniature camera module",[891,903,904],{},"Capture images and short video clips",[876,906,907,910],{},[891,908,909],{},"Temperature & humidity sensor",[891,911,912],{},"Log microclimate conditions around the nest",[876,914,915,918],{},[891,916,917],{},"PIR motion sensor",[891,919,920],{},"Trigger captures on nest activity",[876,922,923,926],{},[891,924,925],{},"Weatherproof enclosure",[891,927,928],{},"Outdoor deployment protection",[11,930,932],{"id":931},"software","Software",[16,934,935],{},"A Python daemon ran on each Pi Zero, listening for motion events. On trigger it would:",[530,937,938,941,944],{},[119,939,940],{},"Capture a timestamped image and a short video clip",[119,942,943],{},"Record temperature, humidity, and motion timestamps to a local SQLite database",[119,945,946],{},"Sync data to a central server over SSH when within campus Wi-Fi range",[11,948,950],{"id":949},"flutter-companion-app","Flutter Companion App",[16,952,953],{},"A Flutter app gave researchers a mobile interface to:",[116,955,956,959,962,965],{},[119,957,958],{},"View a live feed of all deployed units across campus",[119,960,961],{},"Browse the timestamped image archive per unit",[119,963,964],{},"Export data logs as CSV for analysis",[119,966,967],{},"Trigger remote configuration changes (e.g., capture sensitivity)",[11,969,971],{"id":970},"challenges-learnings","Challenges & Learnings",[16,973,974],{},"Coordinating multiple independent units introduced distributed systems challenges: each Pi had to handle connectivity drops gracefully, queue unsynced data, and resume uploads without duplicating records. Managing the data pipeline from embedded device to mobile app was the most complex aspect of the project.",{"title":63,"searchDepth":64,"depth":64,"links":976},[977,978,979,980,981],{"id":13,"depth":64,"text":14},{"id":766,"depth":64,"text":767},{"id":931,"depth":64,"text":932},{"id":949,"depth":64,"text":950},{"id":970,"depth":64,"text":971},"May 2023","A multi-unit surveillance system built to monitor red-winged starling nests across UCT's campus, using Raspberry Pi Zero, environmental sensors, and a Flutter companion app.","nest_cam_floodlight","\u002Fassets\u002Fproj-bird.jpg",{},"\u002Fprojects\u002Fnest-observation-kit",{"title":859,"description":983},"projects\u002Fnest-observation-kit",[991,83,992,993,746,657],"Raspberry Pi","Flutter","IoT","3JetpC2hnPhm98UNJtjMJpIG-1pAS6jzHyRIjfnrrC0",{"id":996,"title":997,"body":998,"date":1206,"description":1207,"extension":73,"icon":1208,"image":1209,"live":76,"meta":1210,"navigation":78,"path":1211,"repo":76,"seo":1212,"stem":1213,"tags":1214,"__hash__":1217},"projects\u002Fprojects\u002Fphasor-measurement-unit.md","Low-Cost Phasor Measurement Unit",{"type":8,"value":999,"toc":1196},[1000,1002,1005,1015,1019,1022,1042,1046,1049,1054,1057,1061,1064,1075,1078,1082,1085,1119,1142,1146,1153,1168,1175,1179,1186,1188,1191,1194],[11,1001,14],{"id":13},[16,1003,1004],{},"A Phasor Measurement Unit (PMU) is a device that takes precise, time-stamped electrical measurements of a power grid — capturing voltage, frequency, and phase angle — synchronised to a common time reference. That synchronisation is what makes PMUs powerful: it lets grid operators compare measurements taken at different locations at the exact same instant, giving a real-time picture of what's happening across the whole network.",[16,1006,1007,1008,1011,1012,589],{},"Commercial PMUs are expensive and bulky, limiting how widely they can be deployed. This final year project repurposed existing power measurement hardware and built a fully functional, low-cost PMU firmware in ",[20,1009,1010],{},"Rust"," on a Raspberry Pi — targeting real grid monitoring accuracy and full compliance with the ",[20,1013,1014],{},"IEEE C37.118 synchrophasor standard",[11,1016,1018],{"id":1017},"hardware-platform","Hardware Platform",[16,1020,1021],{},"The firmware runs on a custom board providing:",[116,1023,1024,1030,1036],{},[119,1025,1026,1029],{},[20,1027,1028],{},"RC voltage divider networks"," — step the three-phase grid voltage (±325V) safely down to ±0.5V for the ADC",[119,1031,1032,1035],{},[20,1033,1034],{},"MCP3914 ADC"," — an 8-channel, 24-bit delta-sigma converter, synchronously sampling all channels over a 20MHz SPI interface at ~9,875 Hz",[119,1037,1038,1041],{},[20,1039,1040],{},"u-blox MAX-M10S GNSS module"," — provides UTC time accurate to 60 nanoseconds from the satellite network, connected over I2C",[11,1043,1045],{"id":1044},"pipeline-architecture","Pipeline Architecture",[16,1047,1048],{},"The firmware is structured as a four-stage pipeline, each running in its own thread and passing data forward via channels:",[16,1050,1051],{},[20,1052,1053],{},"Data Acquisition → Data Processing → Data Storage → Data Transmission",[16,1055,1056],{},"This separation keeps each concern isolated and makes the system easy to test — both the ADC and time provider are defined as Rust traits, meaning fake hardware implementations can be swapped in for unit and integration tests without needing physical devices.",[11,1058,1060],{"id":1059},"time-synchronisation","Time Synchronisation",[16,1062,1063],{},"Measurements are only meaningful with precise timestamps. The GNSS module delivers an accurate UTC second, but measurements happen thousands of times per second — sub-second resolution is essential.",[16,1065,1066,1067,1070,1071,1074],{},"The solution was the Raspberry Pi's ",[20,1068,1069],{},"GPU timer"," — a free-running 64-bit counter driven by a 1MHz clock, independent of the CPU and OS scheduler. Accessing it requires memory-mapping the hardware System Timer register directly from ",[270,1072,1073],{},"\u002Fdev\u002Fmem",", giving 1 microsecond resolution with zero OS overhead. Anchoring this counter to the GNSS second pulse produces a precise full UTC timestamp for every single ADC sample.",[16,1076,1077],{},"The GNSS parser runs on a dedicated background thread, continuously reading NMEA messages over I2C and updating a shared, mutex-protected timestamp — keeping the acquisition loop free to focus entirely on sampling speed.",[11,1079,1081],{"id":1080},"signal-processing","Signal Processing",[16,1083,1084],{},"Once samples arrive from acquisition, the processing stage computes:",[116,1086,1087,1101,1107,1113],{},[119,1088,1089,1092,1093,1096,1097,1100],{},[20,1090,1091],{},"Synchrophasor"," — the magnitude and phase angle of each voltage waveform, calculated via ",[20,1094,1095],{},"FFT"," using the ",[270,1098,1099],{},"rustfft"," crate",[119,1102,1103,1106],{},[20,1104,1105],{},"Frequency"," — derived from the FFT output",[119,1108,1109,1112],{},[20,1110,1111],{},"ROCOF"," — Rate of Change of Frequency, used for detecting grid instability events",[119,1114,1115,1118],{},[20,1116,1117],{},"RMS voltage"," — effective magnitude per phase",[16,1120,1121,1122,1125,1126,1129,1130,1133,1134,1137,1138,1141],{},"Before the FFT, each signal passes through a ",[20,1123,1124],{},"2531-coefficient FIR filter"," to remove noise and out-of-band interference. Spline ",[20,1127,1128],{},"interpolation"," enforces a consistent effective sampling rate and corrects for any timing gaps. A ",[20,1131,1132],{},"ring buffer"," (",[270,1135,1136],{},"BoundedVecDeque",") keeps the most recent samples in memory for continuous windowed analysis. Parallel processing via ",[270,1139,1140],{},"rayon"," keeps throughput high across all channels.",[11,1143,1145],{"id":1144},"ieee-c37118-compliance","IEEE C37.118 Compliance",[16,1147,1148,1149,1152],{},"PMU output is transmitted using the ",[20,1150,1151],{},"IEEE C37.118.2 protocol"," — the international standard for synchrophasor data transfer — over TCP\u002FIP. The implementation covers:",[116,1154,1155,1158,1165],{},[119,1156,1157],{},"Data frames, configuration frames (CFG-2), header frames, and command frames",[119,1159,1160,1161,1164],{},"Binary encoding with ",[20,1162,1163],{},"CRC-CCITT"," validation on every frame",[119,1166,1167],{},"Multi-client support with connection and command frame handling",[16,1169,1170,1171,1174],{},"A separate ",[20,1172,1173],{},"WebSocket server"," streams live measurements to a web dashboard for real-time waveform and phasor visualisation.",[11,1176,1178],{"id":1177},"storage","Storage",[16,1180,1181,1182,1185],{},"All measurements are persisted to a local ",[20,1183,1184],{},"SQLite database"," in JSON format, supporting over two days of continuous storage. Historical data can be queried, exported as CSV or JSON, or replayed with timing accuracy for post-hoc analysis.",[11,1187,48],{"id":47},[16,1189,1190],{},"The system met the majority of its requirements: processing latency under 100 microseconds, microsecond-level timestamps, and a complete pipeline from raw ADC samples to IEEE C37.118-formatted output. The trait-based design proved its value during development — the fake ADC and fake time provider let the processing and transmission stages be tested fully on a laptop before any hardware was involved.",[16,1192,1193],{},"Formal phasor accuracy verification against a reference-grade PMU was not possible within the project scope, leaving that validation as future work.",[60,1195],{},{"title":63,"searchDepth":64,"depth":64,"links":1197},[1198,1199,1200,1201,1202,1203,1204,1205],{"id":13,"depth":64,"text":14},{"id":1017,"depth":64,"text":1018},{"id":1044,"depth":64,"text":1045},{"id":1059,"depth":64,"text":1060},{"id":1080,"depth":64,"text":1081},{"id":1144,"depth":64,"text":1145},{"id":1177,"depth":64,"text":1178},{"id":47,"depth":64,"text":48},"Oct 2024","A real-time Phasor Measurement Unit built in Rust on a Raspberry Pi, sampling three-phase grid voltages with GPS-synchronised timestamps and computing phasors, frequency, and ROCOF — fully compliant with the IEEE C37.118 standard.","bolt","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1754734387891-36fcbb96f830?q=80&w=1170&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",{},"\u002Fprojects\u002Fphasor-measurement-unit",{"title":997,"description":1207},"projects\u002Fphasor-measurement-unit",[1010,746,991,1215,1216,1081,657],"Power Systems","GNSS","1ArS91TECRpCZtlwmaoofAV8Ki9ZDhAnINwhJwaQ2BA",1780176250230]