Web App
23
Robotic development AI assitant
# RDA AI: Robotics Development Assistant RDA AI is an advanced, full-stack AI assistant tailored for robotics engineers and software developers. The platform streamlines hardware configuration, embedded programming, ROS (Robot Operating System) node generation, and physical simulation modeling. By pairing multi-modal generation with persistent state tracking, RDA AI acts as an end-to-end co-pilot for hardware engineering workflows. --- ## 🚀 Key Features * **Intelligent Code Generation:** Generates and debugs production-ready firmware code (Arduino C++, ESP-IDF, Python) for microcontrollers, sensors, and actuators. * **Schematic & Circuit Assistance:** Analyzes pinout layouts, visualizes component wirings, and troubleshoots electrical hardware logic. * **High-Fidelity Concept Visualizations:** Uses advanced text-to-image pipelines to generate visual design mockups, CAD concepts, and chassis structural layouts. --- ## 🛠️ Tech Stack & Architecture The system utilizes an asynchronous backend layer integrated with modern multi-modal foundation models and cloud database services: * **Frontend UI:** Built with **Streamlit**, providing a clean, responsive, dashboard-driven user interface for real-time streaming text interaction and image rendering. * **Orchestration & Compute:** Driven entirely by **Python**, handling data transformations, environment variables, and asynchronous API communication. * **AI Engine & Foundations:** * **Google Vertex AI:** Hosts and provides enterprise-grade orchestration for our cognitive pipelines. * **Gemini 2.5 Text (`gemini-2.5-text`):** Powers deep reasoning, logical sequence planning, context retrieval, and structured hardware code generation. * **Imagen 4 Model:** Handles high-fidelity spatial configurations, generating visual CAD blueprints, part prototypes, and structural robotic chassis mockups. * **Database & Persistence:** **Supabase** (PostgreSQL) manages secure user authentication, stores historical chat threads, keeps vector-mapped hardware device contexts, and handles generation logs. --- ## 📂 Project Structure ```text ├── .env.example # Sample environment configuration file ├── requirements.txt # Python dependencies ├── app.py # Main Streamlit application entrypoint ├── core/ # Core backend logic │ ├── __init__.py │ ├── ai_engine.py # Vertex AI, Gemini, and Imagen configurations │ └── database.py # Supabase client wrapper and CRUD operations ├── utils/ # Helper modules │ └── formatters.py # Code blocks, markdown, and layout parsers └── assets/ # App icons, static graphics, and styling sheets
Automatic Fan Speed Controller (Temperature-Based)
# 🔥 Automatic Fan Speed Controller (Temperature-Based) ## 📌 Project Overview The Automatic Fan Speed Controller is a smart automation system that automatically controls the speed of a DC fan based on temperature. The system uses: - Arduino Uno - DHT11 temperature sensor - MOSFET - DC fan When temperature increases, the fan speed increases automatically. --- # 🛠️ Components Used | Component | Function | |---|---| | Arduino Uno | Main controller | | DHT11 Sensor | Reads temperature | | DC Fan | Cooling device | | MOSFET (IRLZ44N) | Controls fan speed | | 10kΩ Resistor | Pull-up resistor | | 220Ω Resistor | Protects MOSFET | | 1N4007 Diode | Prevents voltage spikes | | Power Supply | Powers the fan | | Jumper Wires | Connections | --- # 🔌 Pin Connections ## 🌡️ DHT11 Sensor | DHT11 Pin | Arduino | |---|---| | VCC | 5V | | DATA | D2 | | GND | GND | ### Additional Connection - 10kΩ resistor between VCC and DATA --- ## ⚡ MOSFET Connections ### MOSFET Pin Arrangement ```text Flat side facing you: LEFT = Gate (G) MIDDLE = Drain (D) RIGHT = Source (S) ``` | MOSFET Pin | Connection | |---|---| | Gate | Arduino D9 through 220Ω resistor | | Drain | Fan negative (-) | | Source | GND | --- # 🌀 Fan Connection | Fan Wire | Connection | |---|---| | Positive (+) | External Power Supply + | | Negative (-) | MOSFET Drain | --- # ⚠️ Diode Connection | Diode Side | Connection | |---|---| | Stripe Side | Fan Positive (+) | | Other Side | Fan Negative (-) | --- # 🧠 Working Principle 1. DHT11 reads temperature. 2. Arduino processes the temperature value. 3. Arduino sends PWM signal to MOSFET. 4. MOSFET controls fan speed. 5. Higher temperature → higher fan speed. 6. Lower temperature → lower fan speed. --- # 🚨 Problems Faced | Problem | Solution | |---|---| | Error reading temperature | Corrected wiring and added resistor | | Fan not spinning | Fixed MOSFET connection | | Fan always full speed | Corrected PWM code | --- # 💻 Arduino Code ```cpp analogWrite(FAN_PIN, fanSpeed); ``` --- # 🌍 Applications - Smart home cooling - Computer cooling systems - Industrial automation - Greenhouse temperature control --- # ✅ Conclusion The Automatic Fan Speed Controller is an intelligent embedded system that improves cooling efficiency and automates temperature control using Arduino and sensors.
Robotics
34
MediBot: A Continuous AI Patient Monitoring System for Nigerian Hospitals
# MediBot: A Continuous AI Patient Monitoring System for Nigerian Hospitals ## Introduction Every day in Nigerian hospital wards, doctors and nurses make impossible choices. One nurse. Twenty patients. Eight hours. There's no way to watch everyone's health state at once. MediBot was built to change that, not by replacing nurses, but by giving them a system that never looks away. It is a continuous AI-powered patient monitoring system that combines embedded sensors, computer vision, edge machine learning, and a real-time web dashboard to detect when a patient needs attention before a crisis becomes irreversible. The entire system runs on affordable hardware that can be purchased locally in Nigeria, communicates through Supabase as its cloud backbone, and is accessible from any phone or laptop browser without any installation, making it deployable in hospitals that could never afford traditional ICU-grade monitoring equipment. --- ## The Problem ### The Monitoring Gap In most Nigerian hospital general wards, continuous bedside monitoring simply does not exist. A patient's oxygen saturation can fall from healthy to life-threatening in under two minutes. A patient losing consciousness may go unnoticed for twenty minutes or more if the nurse is responding to another emergency across the ward. Pain in sedated, post-operative, or non-verbal patients often goes completely undetected. Not because the staff does not care, but because no system is watching continuously enough to catch it. This is not an assumption. As of 2019, Nigeria had only 15 nurses and midwives per 10,000 patients [1], making continuous individual patient observation practically impossible in most general wards. A mixed-methods study conducted across three hospitals in Kenya found that during the 24 hours before a patient's death, respiratory rate was documented in only 1.2% of patient records, with insufficient monitoring equipment and high workload identified as the primary reasons nurses could not detect deterioration in time [2]. The WHO estimates that up to 134 million adverse events occur annually in hospitals across low and middle-income countries, with 83% of those events being preventable [3]. The problem is not the dedication of the people working in these hospitals. The problem is that the tools designed to close this gap were never built for these environments. ### Why Existing Solutions Do Not Fit ICU grade monitoring equipment exists. It works well. It costs millions of naira per unit, requires stable electricity, demands regular calibration by biomedical engineers, and was designed entirely around assumptions that do not match the realities of Nigerian general wards. The problem is not only funding. It is the design. Most medical monitoring technology was built for environments with completely different constraints. ### Why This Matters The consequences of the monitoring gap are measurable. Delayed response to cardiac events. Undetected respiratory decline. Unmanaged pain in post-operative patients. Missed early signs of fever escalation. Preventable deterioration during low-staffing periods. MediBot was built as a direct engineering response to that gap. Affordable, deployable, and designed around the realities that Nigerian healthcare workers actually operate in. --- ## What MediBot Does MediBot continuously monitors patients using a combination of medical sensors and AI vision. It classifies the patient's current condition in real-time and alerts medical staff when something requires attention. The system determines whether the patient is awake and comfortable, sleeping normally, in pain, showing signs of distress, potentially unconscious, or whether the bed is empty altogether. It achieves this by combining signals from multiple sources at the same time. | Signal | Source | |--------|--------| | Heart rate | MAX30102 sensor | | Blood oxygen saturation (SpO₂) | MAX30102 sensor | | Body temperature | MLX90614 infrared sensor | | Facial expression | AI vision | | Eye state (open or closed) | AI vision | | Body motion | Frame differencing | | Bed occupancy | ML object detection model | No single signal decides on its own. All signals are combined. The system is designed so that if one source fails, the others continue providing useful information. --- ## Components Used ### Hardware | Component | Purpose | |-----------|---------| | ESP32 Dev Module | Sensor reading, decision logic, alert management | | ESP32-CAM AI Thinker | Camera stream and ML vision inference | | MAX30102 | Heart rate and blood oxygen | | MLX90614 | Contactless infrared body temperature | | Active buzzer | Audible patient alerts | | Status LED | Visual connection indicator | ### Software and Platforms | Tool | Role | |------|------| | Arduino IDE and C++ | ESP32 firmware development | | Edge Impulse | ML model training and deployment | | TensorFlow Lite | On-device ML inference on ESP32-CAM | | face-api.js | Browser-based facial expression detection | | Supabase | Real-time database and authentication backend | | Netlify | Dashboard deployment | | Vanilla HTML, CSS, and JavaScript | Dashboard frontend | --- ## System Architecture MediBot uses a two-board architecture. The responsibilities are deliberately split between two ESP32 devices because of memory constraints that will be explained shortly. ``` ┌─────────────────────────────────────────────────────────┐ │ ESP32-CAM │ │ │ │ Bed Occupancy → Face Detection → Expression Model │ │ → Eye State Model │ │ → Motion Detection │ │ │ │ Sends JSON result via UART │ └──────────────────────────┬──────────────────────────────┘ │ GPIO16 ┌──────────────────────────▼──────────────────────────────┐ │ ESP32 Dev Module │ │ │ │ MAX30102 + MLX90614 → Patient State Engine │ │ → Posts to Supabase every 3s │ │ → Buzzer and Status LED │ └──────────────────────────┬──────────────────────────────┘ │ Supabase DB │ ┌──────────────────────────▼──────────────────────────────┐ │ Doctor Dashboard (Netlify PWA) │ │ Vitals · Patient State · Emotion · Alerts · Camera │ │ Works on phone, tablet, and laptop. No installation. │ └─────────────────────────────────────────────────────────┘ ``` The reason for two boards comes down to memory. The standard ESP32 has 520KB of RAM. Running WiFi, sensor reads, decision logic, and TFLite models simultaneously would exceed that. The ESP32-CAM adds 4MB of PSRAM, which makes it capable of running multiple vision models sequentially. Splitting responsibilities keeps each board well within its limits while the overall system stays powerful. --- ## How It Works ### Step 1: Bed Occupancy Check The ESP32-CAM runs a bed occupancy detection model on every frame. If the bed is confirmed empty and no vital signs have been detected for 60 seconds, all monitoring is suspended. This is what prevents false alerts in an empty ward. ### Step 2: Vital Signs Collection The ESP32 Dev reads heart rate and SpO₂ from the MAX30102, and body temperature from the MLX90614 every two seconds. A rolling average smooths out sensor noise. Temperature alerts only fire after a reading has been abnormal for ten continuous seconds, which prevents single-spike false alarms from triggering the system unnecessarily. ### Step 3: Vision Analysis The ESP32-CAM processes the camera feed through a sequential model pipeline. Each model only runs if the previous one confirms its precondition. If no face is detected, the expression model is skipped entirely. This saves compute and prevents false classifications. ``` Frame → Occupancy model → Face detection → Expression classifier → Eye state ``` ### Step 4: Patient State Decision The ESP32 Dev combines all signals using a layered decision engine. Emergency logic always runs first, before anything else. ``` Layer 1 — Emergency (runs first, every cycle) Critical HR or SpO₂ or temperature → EMERGENCY Layer 2 — Face visible Eyes open + normal expression + normal vitals → AWAKE, comfortable Eyes open + pain expression + abnormal vitals → IN PAIN (confirmed) Eyes open + pain expression + normal vitals → IN PAIN (confirm manually) Eyes closed + normal vitals + low motion → SLEEPING, well Eyes closed + abnormal vitals → SLEEPING, possible pain Layer 3 — Face not visible HR variance low + HR under 70 + normal vitals → LIKELY SLEEPING High motion + abnormal vitals → DISTRESS suspected Any critical vital → ALERT regardless ``` When the patient is facing away from the camera and no face can be detected, the system does not give up. It uses heart rate variance to estimate whether the patient is sleeping. During sleep, the heart rate drops and becomes more regular. Low variance combined with a low resting heart rate is a clinically established indicator of sleep, and MediBot uses it as a fallback when vision fails. ### Step 5: Alerts The buzzer provides immediate local feedback that works even when the doctor is not looking at the dashboard. | Patient State | Buzzer Behaviour | |---------------|-----------------| | System alive, patient stable | Beep every 2 seconds | | Patient confirmed sleeping | Silent, watching quietly | | Alert confirmed | Beep every 500ms | | Emergency | Beep every 150ms | | WiFi not connected | Silent | The dashboard simultaneously receives all data and displays timestamped alerts. ### Step 6: Dashboard The doctor views the dashboard from their phone anywhere on the same network. It shows live vitals, patient state, expression, and eye state from the camera, pain score, PPG waveform, alert history, and the camera feed. --- ## The Dashboard The dashboard is a Progressive Web App. It runs on any mobile or desktop browser with no installation needed. It was designed mobile-first because phones are the most accessible computing devices in Nigerian clinical settings. ### Camera Mode Switching One of the things I am most proud of in MediBot is the ability to switch the vision source at runtime directly from the dashboard. | Mode | How it works | |------|-------------| | Phone | face-api.js runs in the browser. Any smartphone becomes the AI vision system. | | ESP32-CAM | The hardware camera runs all ML models on-device via TensorFlow Lite. | | None | Vitals-only monitoring. Emergencies are still detected from sensor data alone. | When the mode is changed on the dashboard, the ESP32 reads the update from the database within fifteen seconds and automatically adjusts its data source. No reflashing. No reconfiguring. Just a button tap. ### Security Login credentials are stored in the database. There are no passwords anywhere in the source code. Row-level security is enabled on all database tables. Sessions clear automatically when the browser tab closes. Accounts are role-based for doctors, nurses, and administrators. --- ## The Machine Learning Pipeline All vision models run on the ESP32-CAM using Edge Impulse and TensorFlow Lite. | Model | Type | Input | Output | Status | |-------|------|-------|--------|--------| | Bed occupancy | FOMO object detection | Full frame 96×96 | occupied or empty | In training | | Face detection | FOMO pretrained | Full frame 96×96 | Face bounding box | Ready | | Expression classifier | MobileNetV1 0.25 | Face crop 96×96 | pain, neutral, happy, sad | In training | | Eye state classifier | MobileNetV1 0.1 | Eye crop 48×48 | open or closed | Planned | | Motion detection | Frame differencing | Frame delta | high, low, or none | Complete | FOMO was chosen specifically because it was designed for microcontrollers. Standard object detectors like YOLO take three to five seconds per inference on the ESP32-CAM. FOMO runs in approximately 200ms on the same hardware. For real-time patient monitoring, that difference matters enormously. ### Expression Dataset Training data for the expression classifier comes from FER2013 with over 35,000 facial expression images, the MRL Eye Dataset with over 84,000 open and closed eye images, and custom photos taken specifically for this project. The custom photos were the most important part. Most public datasets use front-facing photos taken in controlled environments. In a hospital bed, the camera might be overhead, mounted on a wall, in a corner, or close to the patient at bed level. A model trained only on face-level photos performs poorly on overhead views of someone lying down. The custom dataset was collected from all angles that match real deployment. --- ## Code Samples ### False alarm prevention using a confirmation streak A single abnormal reading is not clinically significant. This logic requires three consecutive abnormal cycles before confirming any alert. ```cpp #define CONFIRM_CYCLES 3 int abnormalStreak = 0; bool alertActive = false; void updateAlertConfirmation(bool abnormal) { if (abnormal) { abnormalStreak++; } else { abnormalStreak = 0; alertActive = false; } if (abnormalStreak >= CONFIRM_CYCLES) { alertActive = true; Serial.println("Alert confirmed"); } } ``` ### Temperature stability window Temperature only flags after ten continuous seconds of abnormal readings. ```cpp #define TEMP_VALID_MIN 35.0f #define TEMP_STABLE_MS 10000 unsigned long tempLowSince = 0; bool tempLowFlag = false; void checkTempStability(float avgTemp) { unsigned long now = millis(); if (avgTemp < TEMP_VALID_MIN) { if (tempLowSince == 0) tempLowSince = now; if (now - tempLowSince >= TEMP_STABLE_MS) { tempLowFlag = true; } } else { tempLowSince = 0; tempLowFlag = false; } } ``` ### Sleep detection from heart rate variance When the patient faces away from the camera, the system uses heart rate variance to infer sleep state. ```cpp float hrVariance = 0.0f; int hrHistory[10]; int hrCount = 0; void updateHRVariance(int hr) { hrHistory[hrCount % 10] = hr; hrCount++; float mean = 0; int n = min(hrCount, 10); for (int i = 0; i < n; i++) mean += hrHistory[i]; mean /= n; float variance = 0; for (int i = 0; i < n; i++) variance += pow(hrHistory[i] - mean, 2); hrVariance = variance / n; } bool isLikelySleeping(int hr, int spo2) { bool lowHR = hr > 0 && hr < 70; bool stableHR = hrVariance < 25.0f; bool normalSpO2 = spo2 == 0 || spo2 >= 94; return lowHR && stableHR && normalSpO2; } ``` ### Non-blocking buzzer state machine The buzzer never blocks the main loop. It goes silent during confirmed sleep but immediately resumes if distress is detected. ```cpp #define BUZZER_PIN 18 unsigned long lastBuzzTime = 0; bool buzzerOn = false; void updateBuzzer(bool wifiOK, bool sleeping) { unsigned long now = millis(); if (!wifiOK) { digitalWrite(BUZZER_PIN, LOW); return; } int interval, duration; if (emergencyConfirmed) { interval = 150; duration = 100; } else if (alertActive) { interval = 500; duration = 80; } else if (sleeping) { digitalWrite(BUZZER_PIN, LOW); return; } else { interval = 2000; duration = 60; } if (!buzzerOn && now - lastBuzzTime >= (unsigned long)interval) { digitalWrite(BUZZER_PIN, HIGH); buzzerOn = true; lastBuzzTime = now; } if (buzzerOn && now - lastBuzzTime >= (unsigned long)duration) { digitalWrite(BUZZER_PIN, LOW); buzzerOn = false; } } ``` ### Clinical expression mapping in the dashboard Seven raw emotion outputs from face-api.js are simplified to four labels that are actually meaningful in a clinical context. ```javascript const CLINICAL_EXPR = { pain: { icon: '😣', label: 'Pain', color: '#ef4444' }, disgusted: { icon: '😣', label: 'Pain', color: '#ef4444' }, fearful: { icon: '😣', label: 'Pain', color: '#ef4444' }, angry: { icon: '😠', label: 'Pain', color: '#ef4444' }, happy: { icon: '😊', label: 'Happy', color: '#10b981' }, neutral: { icon: '😐', label: 'Neutral', color: '#4a6480' }, sad: { icon: '😢', label: 'Sad', color: '#f59e0b' }, }; ``` ### Merging two independent data sources in the dashboard The dashboard reads sensor vitals from the ESP32 and expression data from the phone camera simultaneously, then merges them correctly. ```javascript async function fetchLatestData() { const [espRes, phoneRes] = await Promise.all([ fetch(VITALS_URL + '?source=eq.esp32&order=created_at.desc&limit=1', { headers }), fetch(VITALS_URL + '?source=eq.phone_camera&order=created_at.desc&limit=1', { headers }) ]); const esp = (await espRes.json())[0]; const phone = phoneRes.ok ? (await phoneRes.json())[0] : null; const phoneAge = phone ? (Date.now() - new Date(phone.created_at).getTime()) / 1000 : 999; const phoneFresh = phoneAge < 30 && phone?.emotion !== 'unknown'; return { ...esp, emotion: phoneFresh ? phone.emotion : esp.emotion, eyes: phoneFresh ? phone.eyes : esp.eyes, pain_score: phoneFresh ? phone.pain_score : esp.pain_score, }; } ``` --- ## Challenges Faced ### Sensor Instability The MAX30102 is sensitive to finger placement, pressure, and ambient light. Small movements produce large reading fluctuations. The MLX90614 reads closer to room temperature when the patient is under a blanket or the sensor is not pointing directly at bare skin. The solution for both was moving average smoothing combined with the confirmation streak system. Sustained readings matter. Single spikes do not. ### Memory on the ESP32 Running WiFi, HTTPS, sensor reads, JSON serialisation, and multiple HTTP clients simultaneously on a device with 520KB of RAM required careful engineering. Making HTTP clients static so they are allocated once globally rather than on the stack for every call, and staggering requests so only one HTTP call fires per loop cycle, were what eventually made the system stable. ### Camera Angle vs Training Data This was one of the most practically important problems in the project. Public expression datasets are captured face level with the subject looking directly at the camera. A camera mounted above a hospital bed sees something completely different. A model trained only on face level photos performs poorly on overhead views of a lying patient. Building a custom dataset from the actual deployment angles was not optional. It was necessary. ### Programming the ESP32-CAM The ESP32-CAM has no USB-to-serial chip. Programming it required using the main ESP32 Dev Module as a programmer by bridging its EN pin to GND, crossing TX and RX lines, and holding IO0 to GND on the CAM during upload. This is not clearly documented anywhere and required significant troubleshooting to figure out. ### False Alarms in Early Testing In early testing, the buzzer fired constantly from single-spike readings. One abnormal HR reading was enough to trigger the entire alert system. The three-cycle confirmation streak was the fix. A reading must be abnormal for three consecutive cycles before any alert fires. This eliminated almost all false alarms without meaningfully slowing down response to real emergencies. ### Merging Two Independent Data Sources The dashboard reads from two independent sources simultaneously, such as the ESP32 posting sensor vitals and the phone camera posting vision results. Getting the merge logic right required timestamp comparison, freshness validation, and careful field prioritisation to make sure the correct source overwrites the correct fields without corrupting either dataset. --- ## Project Gallery  Vitals Overview  The Camera Connection page  The Vital Evaluation Log page  The Current Training Progress  Medibot Device --- ## Current Status | Component | Status | |-----------|--------| | ESP32 vitals firmware | ✅ Complete | | MAX30102 HR and SpO₂ | ✅ Working | | MLX90614 temperature | ✅ Working | | Buzzer alert system | ✅ Complete | | Status LED | ✅ Complete | | Dashboard PWA | ✅ Deployed | | Login and role security | ✅ Complete | | Camera mode switching | ✅ Complete | | Real-time alert feed | ✅ Complete | | ESP32-CAM flashing | 🔄 In progress | | Bed occupancy model | 🔄 Training | | Expression classifier | 🔄 Dataset collection | | Eye state model | 📋 Planned | | Offline local mode | 📋 Planned | | Clinical validation | 📋 Planned | --- ## What Comes Next The immediate focus is on completing the ESP32-CAM vision pipeline, deploying the trained ML models to hardware, and beginning clinical validation in real patient environments. Long term, the goal is an offline-first version where the ESP32 hosts the dashboard locally so the entire system works without any internet connection. Hospitals with unreliable network access should not have to choose between monitoring and connectivity. --- ## Closing The monitoring gap in Nigerian hospitals is not only a funding problem. It is also an engineering problem, and engineering problems have engineering solutions. MediBot is one attempt at building that solution around the realities that actually exist here. Affordable hardware. Local components. Open-source software. Infrastructure hospitals already have. The talent to build these systems already exists in Nigeria. What has been missing is infrastructure, research continuity, and systems designed specifically for the environments where the need is real. --- ## References References [1] Runcie CW Chidebe et al. (2023). Brain Drain in Cancer Care: The Shrinking Clinical Oncology Workforce in Nigeria. Journal of Clinical Oncology. Available at pmc.ncbi.nlm.nih.gov/articles/PMC10752460 [2] Nickcy M. et al. (2024). General ward nurses detection and response to clinical deterioration in three hospitals at the Kenyan coast. BMC Nursing. Available at pmc.ncbi.nlm.nih.gov/articles/PMC10905788 [3] Peter M. Nthumba et al. cited in: Patient safety in a rural sub-Saharan Africa hospital. PLOS Global Public Health (2024). doi.org/10.1371/journal.pgph.0003919
IoT
63
Developing a Smart Perimeter Fencing System
Smart Perimeter Security System: An Affordable, Reliable Solution for Homes and Businesses Introduction Security remains one of the most pressing concerns for property owners across Nigeria. From residential compounds owned by individuals to small business premises in urban centers, the need for effective perimeter monitoring has never been more critical. Traditional security measures like high walls, barbed wire, electric fencing and security personnel—while still necessary, often fall short in providing real-time alerts and comprehensive coverage. Commercial alarm systems, though effective, come with prohibitive costs that place them out of reach for an average homeowner or small business operator. This project presents a practical alternative: a four-node wireless perimeter security system built using ESP8266 microcontrollers and a direct communication protocol. The system monitors multiple entry points simultaneously, sending instant breach notifications to property owners via the Blynk mobile application. More importantly, it does so at a fraction of the cost of commercial alternatives while maintaining reliability even in environments with inconsistent internet connectivity. The Problem: Security Gaps in Nigerian Properties Nigeria's security landscape presents unique challenges that make conventional security solutions inadequate: 1. Economic Constraints Professional security systems from international brands can cost anywhere from ₦200,000 to over ₦1,000,000 for basic installations. Monthly monitoring fees add another ₦15,000-₦50,000. For the average homeowner or small shop owner, these costs are simply untenable. The choice often becomes: pay these fees or go without any electronic security monitoring at all. 2. Infrastructure Limitations Many areas experience frequent power outages and unstable internet connectivity. Commercial systems that rely heavily on cloud services or constant Wi-Fi connections become unreliable under these conditions. A security system that fails during a power cut—precisely when properties are most vulnerable—defeats its own purpose. 3. Response Time and Accountability Security guards are invaluable, but human monitoring has limitations. Guards can't be everywhere at once, and fatigue is a real factor during long night shifts. There's also the challenge of verifying exactly when and where a breach occurred, which matters for both immediate response and future investigation. 4. Scalability Issues As properties expand or businesses grow, scaling up security becomes another expensive undertaking with traditional systems. Adding sensors or cameras to existing commercial setups often requires professional installation and can trigger higher subscription tiers. Why This Solution Stands Out This ESP8266-based security system addresses these challenges directly through several key design decisions: -Cost-Effectiveness Without Compromise The entire four-node system can be built with a much lesser amount in component costs—about 70% reduction compared to commercial alternatives. There are no monthly subscriptions, no licensing fees, and no proprietary hardware lock-in. Every component, from the ESP8266 boards to the beam sensors, is readily available in Nigerian electronics markets or through online suppliers. When a component fails, replacement is affordable and straightforward. -Designed for Nigerian Infrastructure The system uses a wireless protocol for its communication that doesn't require internet connection to function. Nodes communicate directly with the hub, meaning the core security monitoring continues working even during internet outages. The hub only needs internet connectivity when sending notifications to the mobile app—a smart design choice that separates critical monitoring from notification delivery. Power consumption is minimal. Each node draws very little current and can run on battery backup during outages, ensuring continuous operation. This resilience is critical in environments where power supply can be unpredictable. -Real-Time Mobile Alerts Integration with Blynk means property owners receive instant push notifications the moment any perimeter beam is broken. Whether you're at work in Victoria Island or out of state you know immediately if someone has breached your compound wall. This real-time awareness transforms security from passive (discovering a break-in after the fact) to active (responding while it's happening). -Scalability and Customization Need to monitor more entry points? Add more nodes. Want to adjust sensitivity or debounce timing? Modify the code. Unlike closed commercial systems, this solution is completely open and adaptable. A property owner with basic technical knowledge can maintain and expand the system independently, without waiting for expensive technician visits. The timestamp feature provides forensic value—you know exactly when each breach occurred, which is useful for correlating with CCTV footage or guard reports. -Redundancy and Reliability Each message transmission includes automatic retry logic, ensuring notifications get through even in noisy wireless environments. The debouncing algorithm prevents false alarms from insects or wind-blown debris while still catching genuine intrusions. These aren't afterthoughts—they're engineered responses to real-world deployment challenges. -Broader Implications Beyond individual installations, this project demonstrates something important: effective security technology doesn't have to be imported at premium prices. With locally available components and open-source tools, Nigerian engineers and technicians can build systems that meet local needs better than off-the-shelf foreign products. Small businesses—especially those in electronics markets, computer villages, and maker spaces—could assemble and install these systems as a service, creating local employment while providing affordable security to their communities. The skills involved (embedded programming, wireless communication, sensor integration) are exactly the kinds of practical electronics knowledge that Nigeria's growing tech sector needs. Conclusion Security shouldn't be a luxury available only to those who can afford premium subscriptions. This four-node perimeter security system proves that with thoughtful engineering and appropriate technology choices, it's possible to build reliable, professional-grade security monitoring at genuinely accessible price points. The system works within Nigeria's infrastructure realities rather than fighting against them. It provides the real-time awareness that property owners need while remaining economically sustainable. Most importantly, it's a solution that can be understood, maintained, and improved by local technicians rather than being a black box dependent on foreign support. As security concerns continue to grow across Nigerian cities, solutions like this—affordable, reliable, and locally serviceable—represent a practical path forward. They don't replace security personnel or physical barriers, but they add an essential layer of electronic monitoring that makes properties genuinely safer without breaking the bank. This isn't just a project. It's a proof of concept that Nigerian innovators can solve Nigerian problems with Nigerian resources. And that might be the most important security it provides: security against the assumption that we must always look elsewhere for solutions. ****text****
Robotics
93
Building a variable-rate semi-autonomous robot fertilizer spreader
# Variable Rate Fertilizer Spreader Robot ### A Low-Cost Semi-Autonomous Precision Agriculture Solution for Small-Scale Farmers in Africa --- ## Table of Contents 1. [Abstract](#abstract) 2. [Problem Statement](#problem-statement) 3. [Proposed Solution](#proposed-solution) 4. [System Overview](#system-overview) 5. [Hardware Components](#hardware-components) 6. [System Architecture](#system-architecture) 7. [Mechanical Design](#mechanical-design) 8. [Power System](#power-system) 9. [Software and Firmware](#software-and-firmware) 10. [Web Dashboard](#web-dashboard) 11. [Build Process](#build-process) 12. [Results](#results) 13. [Challenges and How They Were Solved](#challenges-and-how-they-were-solved) 14. [What I Learned](#what-i-learned) 15. [Future Work](#future-work) --- ## Abstract This project presents the design, development and implementation of a low-cost semi-autonomous variable rate fertilizer spreader robot targeted at small-scale farmers in sub-Saharan Africa. The robot is built on a three-wheeled box chassis and navigates agricultural terrain with human oversight, reading soil nutrient and moisture levels in real time using embedded sensors, and adjusting its fertilizer dispensing rate accordingly — eliminating the guesswork and waste associated with manual fertilizer application. The system is built around an ESP32 microcontroller, communicates over MQTT via WiFi, and is monitored through a custom React-based web dashboard that visualizes live GPS position, soil metrics, and fertilizer application data per field zone. --- ## Problem Statement Small-scale farmers represent the backbone of food production across Africa, yet the majority still rely on manual and uniform fertilizer application methods. This approach introduces two critical problems: **Inefficient Nutrient Distribution** — Manual broadcasting of fertilizer results in uneven coverage across a field. Some zones receive excess nutrients while others remain deficient, leading to inconsistent crop yields and potential environmental damage from nutrient runoff into water bodies. **Economic Waste** — Fertilizer is one of the most significant input costs for smallholder farmers. Without a data-driven approach to application, farmers routinely over-apply in some areas and under-apply in others, wasting money and reducing returns on investment. Commercial precision agriculture technology exists to solve these problems, but current solutions are designed for large-scale industrial farms and cost tens of thousands of dollars — placing them completely out of reach for the smallholder farmer who typically operates on less than two hectares of land. There is a clear and urgent need for a precision agriculture solution that is affordable, functional, and appropriate for the African smallholder farming context. --- ## Proposed Solution The proposed solution is a semi-autonomous three-wheeled ground robot built on a box chassis that drives across a farm field, periodically stops to insert soil sensors into the ground, reads the current NPK (Nitrogen, Phosphorus, Potassium) and moisture levels, calculates the appropriate fertilizer application rate for that zone, and dispenses the correct amount through an auger-based hopper mechanism. The robot operates semi-autonomously — it handles all sensing, calculation and dispensing decisions on its own, while a human operator monitors progress through the web dashboard and can intervene when necessary. The core principle is: **measure first, then dispense accordingly.** All collected data is streamed wirelessly to a web dashboard in real time, giving the farmer a visual record of soil health across every zone of their field. --- ## System Overview The robot operates as follows: ``` START │ ▼ Robot moves to next field zone │ ▼ Obstacle detected? ──YES──► Stop and alert operator │ NO ▼ Robot stops at zone │ ▼ Servo inserts soil sensor probe into ground │ ▼ ESP32 reads NPK + Moisture values │ ▼ Algorithm calculates fertilizer application rate │ ▼ Auger motor runs at calculated speed │ ▼ Data published to MQTT broker over WiFi │ ▼ Dashboard receives and displays data │ ▼ Servo retracts soil probe │ ▼ Robot moves to next zone │ ▼ Repeat until field is complete ``` --- ## Hardware Components | Component | Specification | Purpose | |---|---|---| | ESP32 Microcontroller | Dual-core 240MHz, WiFi + Bluetooth | Main controller — handles all sensing, motor control and communication | | DC Motors with Encoders | 12V DC geared motors | Drive the wheels with speed feedback | | BTS7960 Motor Driver | 43A peak current | Controls drive motors safely at high current | | NPK Sensor | RS485 output | Reads Nitrogen, Phosphorus and Potassium from soil | | Capacitive Moisture Sensor | Analogue output | Reads volumetric soil moisture content | | Servo Motor | Standard 5V servo | Drives the crank-slider soil probe insertion mechanism | | HC-SR04 Ultrasonic Sensor | Range 2cm–400cm | Obstacle detection and avoidance | | NEO-M8N GPS Module | 10Hz update rate | Tracks robot position across the field | | 18650 Li-ion Cells (x6) | 3.7V, 3000mAh each | Power source — assembled as 3S2P pack | | 3S BMS | 20A continuous | Battery management and protection | | LM7805 Voltage Regulator | 5V 1.5A output | Steps down 11.1V to 5V for logic components | | Auger Motor | 12V DC | Drives the fertilizer dispensing mechanism | --- ## System Architecture The system is divided into three layers: ### Layer 1 — Sensing Layer The sensing layer consists of the NPK sensor, capacitive moisture sensor, HC-SR04 ultrasonic sensor and NEO-M8N GPS module. All sensors feed data directly to the ESP32 via their respective interfaces — UART for NPK and GPS, analogue for moisture, and digital trigger/echo for ultrasonic. ### Layer 2 — Processing and Control Layer The ESP32 sits at the centre of the system. It reads all sensor data, runs the fertilizer rate algorithm, generates PWM signals to control motor speeds, manages the servo mechanism timing, and handles all WiFi and MQTT communication. ### Layer 3 — Communication and Monitoring Layer All sensor readings and robot status data are published to an MQTT broker over WiFi. A React-based web dashboard subscribes to the broker and displays the data in real time — showing live GPS trails, zone-by-zone soil metrics, fertilizer rates, and alerts. ``` [Sensors] ──► [ESP32] ──► [MQTT Broker] ──► [Web Dashboard] │ ▼ [Motor Drivers] │ ▼ [Drive Motors + Auger Motor + Servo] ``` --- ## Mechanical Design ### Chassis The robot is built on a three-wheeled box chassis. Two driven wheels are mounted at the rear of the chassis and one free-rolling wheel at the front provides balance and directional support. The box structure provides a rigid and enclosed platform for mounting all electronics, the battery pack and the fertilizer hopper securely above the chassis floor. ### Fertilizer Hopper and Auger The fertilizer hopper is mounted on top of the box chassis. Inside the hopper, an auger — a helical screw mechanism — rotates to push fertilizer downward through a nozzle at the base. The dispensing rate is controlled by varying the auger motor speed through PWM. A higher duty cycle means faster rotation and more fertilizer dispensed per unit time. ### Soil Sensor Insertion Mechanism A servo motor is connected to a crank-slider linkage. When the robot stops at a zone, the servo rotates through a set angle, converting rotary motion into linear motion that pushes the sensor probe vertically downward into the soil. After a stable reading is obtained, the servo retracts the probe and the robot proceeds to the next zone. --- ## Power System The robot is powered by a custom assembled 3S2P lithium-ion battery pack consisting of six 18650 cells. **Pack Specification:** | Parameter | Value | |---|---| | Configuration | 3 Series, 2 Parallel | | Nominal Voltage | 11.1V | | Full Charge Voltage | 12.6V | | Capacity | ~6000mAh | | Protection | 3S BMS | **Power Distribution:** ``` Battery Pack (11.1V) │ ▼ 3S BMS │ ├──────────────────► BTS7960 Motor Driver (11.1V) ──► Drive Motors │ └──► LM7805 Regulator (5V out) │ ├──► ESP32 ├──► Servo Motor ├──► NPK Sensor ├──► Moisture Sensor └──► HC-SR04 ``` --- ## Software and Firmware ### ESP32 Firmware The ESP32 firmware is written in C++ using the Arduino framework. It handles the following tasks: - Reading NPK values from the soil sensor over UART using RS485 - Reading moisture levels from the capacitive sensor via ADC - Measuring distance using the HC-SR04 for obstacle avoidance - Generating PWM signals for drive motor speed control via the BTS7960 - Controlling the servo motor for soil probe insertion and retraction - Publishing all sensor data as JSON payloads over MQTT via WiFi **Fertilizer Rate Algorithm** The fertilizer application rate is calculated based on soil moisture content using the following logic: - Soil moisture at or below wilting point → maximum application rate - Soil moisture at field capacity → minimum application rate - Values between wilting point and field capacity → linearly scaled rate NPK deficiency values from the sensor are used as secondary modifiers to further adjust the rate upward if critical nutrients are below threshold. ### Firmware Code ```cpp #include <WiFi.h> #include <PubSubClient.h> #include <TinyGPSPlus.h> #include <HardwareSerial.h> #include <ArduinoJson.h> #include <ESP32Servo.h> const char* WIFI_SSID = "YOUR_WIFI_SSID"; const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD"; const char* MQTT_BROKER = "YOUR_MQTT_BROKER_IP"; const int MQTT_PORT = 1883; const char* MQTT_CLIENT_ID = "fertilizer_robot_01"; const char* TOPIC_SOIL = "robot/soil"; const char* TOPIC_GPS = "robot/gps"; const char* TOPIC_FERTILIZER = "robot/fertilizer"; const char* TOPIC_BATTERY = "robot/battery"; const char* TOPIC_STATUS = "robot/status"; const char* TOPIC_CONFIG = "robot/config"; const char* TOPIC_ROW = "robot/row"; #define MOTOR_LEFT_IN1 18 #define MOTOR_LEFT_IN2 19 #define MOTOR_RIGHT_IN1 21 #define MOTOR_RIGHT_IN2 22 #define MOTOR_LEFT_ENA 25 #define MOTOR_RIGHT_ENB 26 #define ENCODER_LEFT 34 #define ENCODER_RIGHT 35 #define RELAY_AUGER 27 #define SERVO_PIN 14 #define ULTRASONIC_TRIG 32 #define ULTRASONIC_ECHO 33 #define IR_SENSOR_PIN 23 #define MOISTURE_PIN 36 #define BATTERY_PIN 39 #define MOISTURE_DRY 2800 #define MOISTURE_WET 1200 struct CropConfig { String cropType = "corn"; float intervalMeters = 0.75; float baseRateGrams = 50.0; }; struct FieldConfig { float rowLengthMeters = 20.0; float rowSpacingMeters = 0.75; int totalRows = 6; double rowStartLat = 0.0; double rowStartLng = 0.0; double rowEndLat = 0.0; double rowEndLng = 0.0; bool useGPSWaypoints = false; }; CropConfig cropConfig; FieldConfig fieldConfig; float getCropInterval(String crop) { if (crop == "corn") return 0.75; if (crop == "soybean") return 0.50; if (crop == "wheat") return 0.10; return 0.75; } int currentRow = 1; bool movingForward = true; bool rowEndDetected = false; bool uTurnInProgress = false; volatile long leftPulses = 0; volatile long rightPulses = 0; const float WHEEL_DIAMETER_M = 0.065; const int PULSES_PER_REV = 20; // JGB37-520 encoder — confirm with your spec sheet const float WHEEL_CIRCUMFERENCE = PI * WHEEL_DIAMETER_M; const float METERS_PER_PULSE = WHEEL_CIRCUMFERENCE / PULSES_PER_REV; float totalDistanceTravelled = 0.0; float distanceSinceLastStop = 0.0; float distanceInCurrentRow = 0.0; float uTurnDistanceTravelled = 0.0; void IRAM_ATTR leftEncoderISR() { leftPulses++; } void IRAM_ATTR rightEncoderISR() { rightPulses++; } float getPulseDistance() { long pulses = (leftPulses + rightPulses) / 2; leftPulses = 0; rightPulses = 0; return pulses * METERS_PER_PULSE; } enum RobotState { STATE_IDLE, STATE_MOVING, STATE_SENSING, STATE_DISPENSING, STATE_ROW_END_STOP, STATE_UTURN_TURNING_1, STATE_UTURN_STEPPING, STATE_UTURN_TURNING_2, STATE_FIELD_COMPLETE, STATE_OBSTACLE_DETECTED, STATE_ERROR }; RobotState robotState = STATE_IDLE; unsigned long lastDistanceCheck = 0; unsigned long lastBatteryCheck = 0; unsigned long lastGPSPublish = 0; unsigned long lastStatusPublish = 0; unsigned long sensingStartTime = 0; unsigned long dispensingStart = 0; unsigned long turnStartTime = 0; unsigned long rowEndPauseStart = 0; const unsigned long DISTANCE_CHECK_INTERVAL = 100; const unsigned long BATTERY_CHECK_INTERVAL = 10000; const unsigned long GPS_PUBLISH_INTERVAL = 2000; const unsigned long STATUS_PUBLISH_INTERVAL = 5000; const unsigned long SENSING_DURATION = 3000; const unsigned long DISPENSING_DURATION = 2000; const unsigned long ROW_END_PAUSE = 1000; const unsigned long TURN_90_DURATION = 800; struct SoilData { int moisture = 0; float nitrogen = 0; float phosphorus = 0; float potassium = 0; }; SoilData soilData; float fertilizerDispensedGrams = 0.0; float totalFertilizerGrams = 0.0; TinyGPSPlus gps; HardwareSerial gpsSerial(1); // UART1 — GPIO9(RX), GPIO10(TX) Servo soilServo; WiFiClient wifiClient; PubSubClient mqttClient(wifiClient); double gpsDistanceTo(double lat1, double lng1, double lat2, double lng2) { return TinyGPSPlus::distanceBetween(lat1, lng1, lat2, lng2); } bool nearGPSWaypoint(double targetLat, double targetLng, float thresholdMeters = 1.5) { if (!gps.location.isValid()) return false; double dist = gpsDistanceTo( gps.location.lat(), gps.location.lng(), targetLat, targetLng ); return dist <= thresholdMeters; } bool checkRowEnd() { if (digitalRead(IR_SENSOR_PIN) == LOW) { Serial.println("Row end: IR marker detected"); return true; } if (fieldConfig.useGPSWaypoints && fieldConfig.rowEndLat != 0.0 && nearGPSWaypoint(fieldConfig.rowEndLat, fieldConfig.rowEndLng)) { Serial.println("Row end: GPS waypoint reached"); return true; } if (distanceInCurrentRow >= fieldConfig.rowLengthMeters) { Serial.println("Row end: encoder distance reached"); return true; } return false; } void mqttCallback(char* topic, byte* payload, unsigned int length) { String msg = ""; for (int i = 0; i < length; i++) msg += (char)payload[i]; StaticJsonDocument<512> doc; DeserializationError err = deserializeJson(doc, msg); if (err) return; if (String(topic) == TOPIC_CONFIG) { if (doc.containsKey("crop_type")) { cropConfig.cropType = doc["crop_type"].as<String>(); cropConfig.intervalMeters = getCropInterval(cropConfig.cropType); } if (doc.containsKey("base_rate_grams")) cropConfig.baseRateGrams = doc["base_rate_grams"]; if (doc.containsKey("row_length_m")) fieldConfig.rowLengthMeters = doc["row_length_m"]; if (doc.containsKey("row_spacing_m")) fieldConfig.rowSpacingMeters = doc["row_spacing_m"]; if (doc.containsKey("total_rows")) fieldConfig.totalRows = doc["total_rows"]; if (doc.containsKey("row_end_lat") && doc.containsKey("row_end_lng")) { fieldConfig.rowEndLat = doc["row_end_lat"]; fieldConfig.rowEndLng = doc["row_end_lng"]; fieldConfig.useGPSWaypoints = true; } Serial.println("Config updated from dashboard"); Serial.println(" Crop: " + cropConfig.cropType); Serial.println(" Row length: " + String(fieldConfig.rowLengthMeters) + "m"); Serial.println(" Total rows: " + String(fieldConfig.totalRows)); if (robotState == STATE_IDLE) { robotState = STATE_MOVING; moveForward(); publishStatus("started"); } } } void connectWiFi() { Serial.print("Connecting to WiFi"); WiFi.begin(WIFI_SSID, WIFI_PASSWORD); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println("\nWiFi connected: " + WiFi.localIP().toString()); } void connectMQTT() { mqttClient.setServer(MQTT_BROKER, MQTT_PORT); mqttClient.setCallback(mqttCallback); Serial.print("Connecting to MQTT"); while (!mqttClient.connected()) { if (mqttClient.connect(MQTT_CLIENT_ID)) { Serial.println("\nMQTT connected"); mqttClient.subscribe(TOPIC_CONFIG); mqttClient.publish(TOPIC_STATUS, "{\"status\":\"robot_online\"}"); } else { Serial.print("."); delay(1000); } } } void reconnectMQTT() { if (!mqttClient.connected()) connectMQTT(); } void moveForward(int speed = 200) { analogWrite(MOTOR_LEFT_ENA, speed); analogWrite(MOTOR_RIGHT_ENB, speed); digitalWrite(MOTOR_LEFT_IN1, HIGH); digitalWrite(MOTOR_LEFT_IN2, LOW); digitalWrite(MOTOR_RIGHT_IN1, HIGH); digitalWrite(MOTOR_RIGHT_IN2, LOW); } void stopMotors() { analogWrite(MOTOR_LEFT_ENA, 0); analogWrite(MOTOR_RIGHT_ENB, 0); digitalWrite(MOTOR_LEFT_IN1, LOW); digitalWrite(MOTOR_LEFT_IN2, LOW); digitalWrite(MOTOR_RIGHT_IN1, LOW); digitalWrite(MOTOR_RIGHT_IN2, LOW); } void turnRight(int speed = 180) { analogWrite(MOTOR_LEFT_ENA, speed); analogWrite(MOTOR_RIGHT_ENB, speed); digitalWrite(MOTOR_LEFT_IN1, HIGH); digitalWrite(MOTOR_LEFT_IN2, LOW); digitalWrite(MOTOR_RIGHT_IN1, LOW); digitalWrite(MOTOR_RIGHT_IN2, HIGH); } void turnLeft(int speed = 180) { analogWrite(MOTOR_LEFT_ENA, speed); analogWrite(MOTOR_RIGHT_ENB, speed); digitalWrite(MOTOR_LEFT_IN1, LOW); digitalWrite(MOTOR_LEFT_IN2, HIGH); digitalWrite(MOTOR_RIGHT_IN1, HIGH); digitalWrite(MOTOR_RIGHT_IN2, LOW); } void startTurn() { if (currentRow % 2 != 0) turnRight(); else turnLeft(); } void startReverseTurn() { if (currentRow % 2 != 0) turnLeft(); else turnRight(); } void augerOn() { digitalWrite(RELAY_AUGER, LOW); } // LOW = relay ON void augerOff() { digitalWrite(RELAY_AUGER, HIGH); } // HIGH = relay OFF float getUltrasonicDistance() { digitalWrite(ULTRASONIC_TRIG, LOW); delayMicroseconds(2); digitalWrite(ULTRASONIC_TRIG, HIGH); delayMicroseconds(10); digitalWrite(ULTRASONIC_TRIG, LOW); long dur = pulseIn(ULTRASONIC_ECHO, HIGH, 30000); return (dur * 0.034) / 2.0; } bool obstacleDetected() { float dist = getUltrasonicDistance(); return (dist > 0 && dist < 30.0); } void readMoisture() { int raw = analogRead(MOISTURE_PIN); int moisture = map(raw, MOISTURE_DRY, MOISTURE_WET, 0, 100); soilData.moisture = constrain(moisture, 0, 100); Serial.print("Moisture raw: "); Serial.print(raw); Serial.print(" -> "); Serial.print(soilData.moisture); Serial.println("%"); } void readNPK() { byte query[] = {0x01, 0x03, 0x00, 0x1E, 0x00, 0x03, 0x65, 0xCD}; Serial2.write(query, sizeof(query)); delay(200); if (Serial2.available() >= 11) { byte response[11]; Serial2.readBytes(response, 11); // Validate response: check address and function code if (response[0] == 0x01 && response[1] == 0x03) { soilData.nitrogen = ((response[3] << 8) | response[4]); soilData.phosphorus = ((response[5] << 8) | response[6]); soilData.potassium = ((response[7] << 8) | response[8]); Serial.print("N:"); Serial.print(soilData.nitrogen); Serial.print(" P:"); Serial.print(soilData.phosphorus); Serial.print(" K:"); Serial.println(soilData.potassium); } else { Serial.println("NPK: bad response — check wiring or baud rate"); } } else { Serial.println("NPK: no response — check SH-U12 connections"); } } float calculateFertilizerRate() { float moistureFactor = 1.0; if (soilData.moisture < 30) moistureFactor = 1.5; else if (soilData.moisture < 50) moistureFactor = 1.2; else if (soilData.moisture < 70) moistureFactor = 1.0; else moistureFactor = 0.7; return cropConfig.baseRateGrams * moistureFactor; } float getBatteryPercent() { int raw = analogRead(BATTERY_PIN); float volts = (raw / 4095.0) * 3.3 * 4.0; float pct = ((volts - 9.0) / (12.6 - 9.0)) * 100.0; return constrain(pct, 0.0, 100.0); } void publishSoilData() { StaticJsonDocument<256> doc; doc["moisture"] = soilData.moisture; doc["nitrogen"] = soilData.nitrogen; doc["phosphorus"] = soilData.phosphorus; doc["potassium"] = soilData.potassium; doc["row"] = currentRow; if (gps.location.isValid()) { doc["lat"] = gps.location.lat(); doc["lng"] = gps.location.lng(); } char buf[256]; serializeJson(doc, buf); mqttClient.publish(TOPIC_SOIL, buf); } void publishFertilizerData(float grams) { StaticJsonDocument<256> doc; doc["dispensed_grams"] = grams; doc["total_grams"] = totalFertilizerGrams; doc["distance_m"] = totalDistanceTravelled; doc["row"] = currentRow; doc["crop_type"] = cropConfig.cropType; if (gps.location.isValid()) { doc["lat"] = gps.location.lat(); doc["lng"] = gps.location.lng(); } char buf[256]; serializeJson(doc, buf); mqttClient.publish(TOPIC_FERTILIZER, buf); } void publishGPS() { if (!gps.location.isValid()) return; StaticJsonDocument<128> doc; doc["lat"] = gps.location.lat(); doc["lng"] = gps.location.lng(); doc["speed_kmh"] = gps.speed.kmph(); doc["row"] = currentRow; char buf[128]; serializeJson(doc, buf); mqttClient.publish(TOPIC_GPS, buf); } void publishBattery() { StaticJsonDocument<64> doc; doc["battery_pct"] = getBatteryPercent(); char buf[64]; serializeJson(doc, buf); mqttClient.publish(TOPIC_BATTERY, buf); } void publishStatus(String status) { StaticJsonDocument<200> doc; doc["status"] = status; doc["state"] = (int)robotState; doc["distance_m"] = totalDistanceTravelled; doc["current_row"] = currentRow; doc["total_rows"] = fieldConfig.totalRows; doc["moving_forward"] = movingForward; char buf[200]; serializeJson(doc, buf); mqttClient.publish(TOPIC_STATUS, buf); } void publishRowEvent(String event) { StaticJsonDocument<128> doc; doc["event"] = event; doc["row"] = currentRow; doc["total_rows"] = fieldConfig.totalRows; if (gps.location.isValid()) { doc["lat"] = gps.location.lat(); doc["lng"] = gps.location.lng(); } char buf[128]; serializeJson(doc, buf); mqttClient.publish(TOPIC_ROW, buf); } void setup() { Serial.begin(115200); pinMode(MOTOR_LEFT_IN1, OUTPUT); pinMode(MOTOR_LEFT_IN2, OUTPUT); pinMode(MOTOR_RIGHT_IN1, OUTPUT); pinMode(MOTOR_RIGHT_IN2, OUTPUT); pinMode(MOTOR_LEFT_ENA, OUTPUT); pinMode(MOTOR_RIGHT_ENB, OUTPUT); pinMode(RELAY_AUGER, OUTPUT); augerOff(); pinMode(ULTRASONIC_TRIG, OUTPUT); pinMode(ULTRASONIC_ECHO, INPUT); pinMode(IR_SENSOR_PIN, INPUT_PULLUP); pinMode(ENCODER_LEFT, INPUT_PULLUP); pinMode(ENCODER_RIGHT, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(ENCODER_LEFT), leftEncoderISR, RISING); attachInterrupt(digitalPinToInterrupt(ENCODER_RIGHT), rightEncoderISR, RISING); soilServo.attach(SERVO_PIN); soilServo.write(0); // retracted at boot gpsSerial.begin(9600, SERIAL_8N1, 9,10) Serial2.begin(9600, SERIAL_8N1, 16, 17); connectWiFi(); connectMQTT(); robotState = STATE_IDLE; Serial.println("Robot online. Waiting for field config from dashboard..."); } void loop() { reconnectMQTT(); mqttClient.loop(); while (gpsSerial.available()) gps.encode(gpsSerial.read()); unsigned long now = millis(); if (now - lastGPSPublish >= GPS_PUBLISH_INTERVAL) { lastGPSPublish = now; publishGPS(); } if (now - lastBatteryCheck >= BATTERY_CHECK_INTERVAL) { lastBatteryCheck = now; publishBattery(); } if (now - lastStatusPublish >= STATUS_PUBLISH_INTERVAL) { lastStatusPublish = now; publishStatus("running"); } switch (robotState) { case STATE_IDLE: break; case STATE_MOVING: { if (obstacleDetected()) { stopMotors(); robotState = STATE_OBSTACLE_DETECTED; publishStatus("obstacle_detected"); break; } if (now - lastDistanceCheck >= DISTANCE_CHECK_INTERVAL) { lastDistanceCheck = now; float delta = getPulseDistance(); distanceSinceLastStop += delta; distanceInCurrentRow += delta; totalDistanceTravelled += delta; } if (checkRowEnd()) { stopMotors(); augerOff(); rowEndPauseStart = now; robotState = STATE_ROW_END_STOP; publishRowEvent("row_end_reached"); Serial.println("Row " + String(currentRow) + " complete — preparing U-turn"); break; } if (distanceSinceLastStop >= cropConfig.intervalMeters) { stopMotors(); distanceSinceLastStop = 0.0; robotState = STATE_SENSING; sensingStartTime = now; soilServo.write(90); // insert sensor into soil } break; } case STATE_SENSING: { if (now - sensingStartTime >= SENSING_DURATION) { readMoisture(); readNPK(); publishSoilData(); soilServo.write(0); fertilizerDispensedGrams = calculateFertilizerRate(); totalFertilizerGrams += fertilizerDispensedGrams; augerOn(); dispensingStart = now; robotState = STATE_DISPENSING; } break; } case STATE_DISPENSING: { if (now - dispensingStart >= DISPENSING_DURATION) { augerOff(); publishFertilizerData(fertilizerDispensedGrams); robotState = STATE_MOVING; moveForward(); } break; } case STATE_ROW_END_STOP: { if (now - rowEndPauseStart >= ROW_END_PAUSE) { if (currentRow >= fieldConfig.totalRows) { robotState = STATE_FIELD_COMPLETE; publishStatus("field_complete"); publishRowEvent("field_complete"); Serial.println("All rows complete! Mission finished."); break; } startTurn(); turnStartTime = now; robotState = STATE_UTURN_TURNING_1; Serial.println("U-turn started — turn 1"); } break; } case STATE_UTURN_TURNING_1: { if (now - turnStartTime >= TURN_90_DURATION) { stopMotors(); uTurnDistanceTravelled = 0.0; leftPulses = 0; rightPulses = 0; moveForward(); robotState = STATE_UTURN_STEPPING; Serial.println("U-turn — stepping across to next row"); } break; } case STATE_UTURN_STEPPING: { if (now - lastDistanceCheck >= DISTANCE_CHECK_INTERVAL) { lastDistanceCheck = now; float delta = getPulseDistance(); uTurnDistanceTravelled += delta; } if (uTurnDistanceTravelled >= fieldConfig.rowSpacingMeters) { stopMotors(); startReverseTurn(); turnStartTime = now; robotState = STATE_UTURN_TURNING_2; Serial.println("U-turn — turn 2 to align with next row"); } break; } case STATE_UTURN_TURNING_2: { if (now - turnStartTime >= TURN_90_DURATION) { stopMotors(); currentRow++; movingForward = !movingForward; distanceInCurrentRow = 0.0; distanceSinceLastStop = 0.0; moveForward(); robotState = STATE_MOVING; publishRowEvent("new_row_started"); Serial.println("Now on row " + String(currentRow) + " — moving"); } break; } case STATE_FIELD_COMPLETE: { stopMotors(); augerOff(); static bool finalPublished = false; if (!finalPublished) { StaticJsonDocument<200> doc; doc["event"] = "mission_complete"; doc["total_rows"] = fieldConfig.totalRows; doc["total_distance_m"] = totalDistanceTravelled; doc["total_fertilizer_g"] = totalFertilizerGrams; char buf[200]; serializeJson(doc, buf); mqttClient.publish(TOPIC_STATUS, buf); finalPublished = true; Serial.println("Mission summary published."); } break; } case STATE_OBSTACLE_DETECTED: { if (!obstacleDetected()) { robotState = STATE_MOVING; moveForward(); publishStatus("obstacle_cleared"); Serial.println("Obstacle cleared — resuming"); } break; } case STATE_ERROR: default: stopMotors(); augerOff(); break; } } ## Web Dashboard The web dashboard is built using React with Leaflet for map visualisation and MQTT.js for real-time data communication. **Dashboard Features:** | Feature | Description | |---|---| | Live GPS Trail | Shows the robot's path across the field on a map | | Soil Metrics Panel | Displays real-time NPK and moisture readings per zone | | Fertilizer Rate Display | Shows calculated and applied rate per zone | | Obstacle Alerts | Notifies operator when an obstacle is detected | | Zone Data Table | Logs all zone readings in a structured table | | CSV Export | Allows farmer to download field data as a spreadsheet | ### Dashboard Code ```html,css, javascript <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Fertilizer Robot Dashboard</title> <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> <link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" /> <style> :root { --bg: #0b1a0f; --panel: rgba(255, 255, 255, 0.16); --panel-strong: rgba(255, 255, 255, 0.22); --text: #eef6e9; --muted: #aacfa5; --accent: #72c78f; --accent-soft: rgba(114, 199, 143, 0.22); --success: #a8f0a6; --warning: #ffda92; --danger: #ff9ea9; --glass: rgba(255,255,255,0.10); } * { box-sizing: border-box; } body { margin: 0; min-height: 100vh; font-family: 'Inter', sans-serif; color: var(--text); background: linear-gradient(180deg, rgba(8, 24, 12, 0.84), rgba(6, 18, 6, 0.94)), url('https://images.unsplash.com/photo-1501004318641-b39e6451bec6?auto=format&fit=crop&w=1600&q=80') center/cover no-repeat; background-attachment: fixed; background-blend-mode: overlay; overflow-x: hidden; } .page { max-width: 1280px; margin: 0 auto; padding: 30px 24px 40px; background: rgba(8, 24, 12, 0.72); border: 1px solid rgba(255,255,255,0.08); border-radius: 36px; box-shadow: 0 40px 120px rgba(3, 10, 3, 0.45); } .hero { display: flex; flex-wrap: wrap; align-items: flex-start; justify-content: space-between; gap: 20px; margin-bottom: 30px; } .hero-text { max-width: 680px; } .hero-text h1 { margin: 0; font-size: clamp(2.6rem, 4vw, 4.3rem); line-height: 1.05; letter-spacing: -0.04em; color: #f1f9ea; } .hero-text p { margin: 18px 0 0; max-width: 610px; color: var(--muted); font-size: 1rem; line-height: 1.75; } .status-pill { display: inline-flex; align-items: center; gap: 10px; padding: 14px 18px; border-radius: 999px; border: 1px solid rgba(168, 240, 166, 0.24); backdrop-filter: blur(16px); background: rgba(18, 44, 20, 0.72); color: var(--text); font-weight: 600; box-shadow: 0 20px 80px rgba(0, 0, 0, 0.22); } .status-pill .dot { width: 12px; height: 12px; border-radius: 50%; background: var(--success); box-shadow: 0 0 0 6px rgba(168, 240, 166, 0.18); } .grid { display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); gap: 18px; } .card { grid-column: span 12; background: linear-gradient(180deg, rgba(255, 255, 255, 0.14), rgba(172, 246, 174, 0.08)); border: 1px solid rgba(183, 255, 198, 0.14); border-radius: 28px; padding: 24px; backdrop-filter: blur(18px); box-shadow: 0 28px 80px rgba(4, 12, 8, 0.28); position: relative; overflow: hidden; } .card::before { content: ''; position: absolute; inset: 0; background: radial-gradient(circle at top right, rgba(166, 255, 190, 0.24), transparent 34%), radial-gradient(circle at bottom left, rgba(103, 219, 118, 0.18), transparent 30%); pointer-events: none; } .card::after { content: ''; position: absolute; top: 0; left: 22px; width: calc(100% - 44px); height: 5px; border-radius: 999px; background: linear-gradient(90deg, #9ce79a, #b4fff2); opacity: 0.95; pointer-events: none; } .card.accent-blue::after { background: linear-gradient(90deg, #7cd9ac, #9bffc8); } .card.accent-green::after { background: linear-gradient(90deg, #8ff3c6, #a5ffdb); } .card.accent-peach::after { background: linear-gradient(90deg, #ffe6ac, #ffd3c1); } .card.accent-lavender::after { background: linear-gradient(90deg, #c8e0ff, #b2ccff); } .card.accent-mint::after { background: linear-gradient(90deg, #c7ffc9, #d6ffe5); } .card h2 { margin: 0 0 18px; font-size: 1.15rem; letter-spacing: 0.01em; } .card .value { font-size: clamp(2rem, 3vw, 3.2rem); font-weight: 700; margin: 0; line-height: 1; color: var(--text); } .card .label { margin-top: 10px; color: var(--muted); font-size: 0.95rem; } .small-grid { display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); gap: 18px; } .card.small { grid-column: span 6; } .card.large { grid-column: span 8; } .map-container { height: 380px; border-radius: 24px; border: 1px solid rgba(255,255,255,0.14); overflow: hidden; box-shadow: inset 0 0 0 1px rgba(255,255,255,0.05); margin-bottom: 16px; } .alert-card { display: flex; align-items: flex-start; gap: 18px; padding: 22px; border: 1px solid rgba(255, 167, 196, 0.35); background: rgba(255, 167, 196, 0.16); color: #fff; border-radius: 24px; } .alert-card p { margin: 0; font-weight: 600; line-height: 1.5; } .alert-card .badge { display: inline-flex; align-items: center; gap: 8px; padding: 8px 12px; border-radius: 999px; background: rgba(255, 93, 119, 0.22); color: #ffdce3; font-size: 0.95rem; font-weight: 700; } .alerts-list { margin: 16px 0 0; padding: 0; list-style: none; display: grid; gap: 12px; } .alert-message { display: flex; align-items: center; gap: 12px; padding: 14px 16px; border-radius: 18px; background: rgba(11, 50, 23, 0.75); border: 1px solid rgba(168, 240, 166, 0.18); color: #eef6e9; font-size: 0.96rem; line-height: 1.5; } .alert-dot { width: 14px; height: 14px; border-radius: 50%; flex-shrink: 0; background: #ffbd61; box-shadow: 0 0 0 4px rgba(255, 189, 97, 0.18); } .bar-chart { display: grid; gap: 12px; min-height: 180px; padding: 12px 0; } .bar-item { display: grid; grid-template-columns: minmax(140px, 220px) 1fr auto; align-items: center; gap: 14px; padding: 10px 0; border-bottom: 1px solid rgba(255,255,255,0.08); } .bar-label { font-size: 0.95rem; color: var(--text); min-width: 120px; } .bar-fill { position: relative; height: 18px; border-radius: 999px; background: rgba(255,255,255,0.08); overflow: hidden; } .bar-fill span { display: block; height: 100%; width: 0; border-radius: 999px; background: linear-gradient(90deg, #7cd9ac, #52b96b); transition: width 0.5s ease; } .bar-value { font-size: 0.95rem; color: var(--muted); min-width: 50px; text-align: right; } .bar-placeholder { padding: 24px; border-radius: 20px; background: rgba(255,255,255,0.04); color: rgba(255,255,255,0.75); font-size: 0.95rem; text-align: center; } .session-list { margin: 0; padding: 0; list-style: none; display: grid; gap: 14px; } .session-item { display: grid; grid-template-columns: 1fr auto; gap: 14px; padding: 18px 20px; border-radius: 20px; background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.08); } .session-item strong { font-size: 0.98rem; margin-bottom: 6px; display: block; color: var(--text); } .session-item span { color: var(--muted); font-size: 0.92rem; line-height: 1.5; } .placeholder { color: rgba(255,255,255,0.5); border: 1px dashed rgba(255,255,255,0.2); min-height: 130px; display: flex; align-items: center; justify-content: center; text-align: center; font-size: 0.96rem; padding: 18px; background: rgba(255,255,255,0.02); } .field-select { width: 100%; margin-top: 10px; padding: 14px 16px; border-radius: 16px; border: 1px solid rgba(255,255,255,0.18); background: rgba(255,255,255,0.08); color: var(--text); font-size: 0.98rem; outline: none; } .field-select option { color: #111827; } @media (max-width: 900px) { .card.small, .card.large { grid-column: span 12; } } @media (max-width: 700px) { .page { padding: 20px 16px 30px; } .hero { flex-direction: column; align-items: stretch; } .status-pill { width: 100%; justify-content: center; } .small-grid { grid-template-columns: 1fr; } .session-item { grid-template-columns: 1fr; } .card .value { font-size: 2.2rem; } } </style> </head> <body> <div class="page"> <section class="hero"> <div class="hero-text"> <h1>Fertilizer Robot Growth Dashboard</h1> <p>Track your robot, soil health, and fertilizer delivery in a plant-inspired dashboard with a live interactive field map.</p> </div> <div class="status-pill" id="connectionStatus"> <span class="dot"></span> <span>Connecting to MQTT...</span> </div> </section> <div class="grid"> <article class="card accent-blue" style="grid-column: span 12;"> <h2>Live Sensor Snapshot</h2> <div class="small-grid"> <div class="card small"> <h2>Soil EC</h2> <p class="value" id="soilEcValue">--</p> <p class="label">Nutrient conductivity in µS/cm</p> </div> <div class="card small"> <h2>Soil Moisture</h2> <p class="value" id="soilMoistureValue">--</p> <p class="label">Moisture level as percentage</p> </div> </div> </article> <article class="card large accent-blue" style="grid-column: span 12;"> <h2>Live Robot Tracking Map</h2> <div id="fieldMap" class="map-container"></div> <p class="label">This interactive map shows the robot's current location in the field and updates in real time from MQTT GPS messages.</p> </article> <article class="card large accent-green"> <h2>Robot Power & Fertilizer</h2> <div class="small-grid"> <div class="card small"> <h2>Battery Status</h2> <p class="value" id="batteryStatusValue">--</p> <p class="label">Online power condition</p> </div> <div class="card small"> <h2>Charge Level</h2> <p class="value" id="batteryChargeValue">--</p> <p class="label">Percent charged</p> </div> <div class="card small"> <h2>Fertilizer Remaining</h2> <p class="value" id="fertilizerRemainingValue">--</p> <p class="label">Kilograms left in hopper</p> </div> <div class="card small"> <h2>Used Fertilizer</h2> <p class="value" id="fertilizerUsedValue">--</p> <p class="label">Cumulative kg applied</p> </div> </div> </article> <article class="card large accent-mint"> <h2>Crop Field Configuration</h2> <p class="label">Choose the crop field type so the robot can adapt its travel distance and fertilizer application plan.</p> <div class="small-grid"> <div class="card small"> <h2>Field Type</h2> <select id="cropFieldType" class="field-select"> <option value="">Select crop field type</option> <option value="Corn">Corn</option> <option value="Vegetables">Vegetables</option> <option value="Rice">Rice</option> <option value="Orchard">Orchard</option> <option value="Other">Other</option> </select> <p class="label">Different crop fields require different travel distances.</p> </div> <div class="card small"> <h2>Planned Run Distance</h2> <p class="value" id="runDistanceValue">--</p> <p class="label">Estimated meters based on the selected field type</p> </div> </div> </article> <article class="card accent-peach" id="alertCard"> <h2>Live System Alerts</h2> <div class="alert-card"> <div> <span class="badge">ALERTS</span> <ul class="alerts-list" id="alertsList"> <li class="alert-message"> No alerts at the moment. All systems are operating normally. </li> </ul> </div> </div> </article> <article class="card large accent-lavender"> <h2>Recent Sessions</h2> <p class="label">Past robot runs with date, time and field area covered.</p> <ul class="session-list" id="sessionHistory"> <li class="session-item"> <strong>No past sessions received yet</strong> <span>When MQTT messages arrive on the session topic, they will appear here.</span> </li> </ul> </article> <article class="card large accent-mint"> <h2>Fertilizer Dispensed Per Session</h2> <div class="bar-chart" id="dispensedChart"> <div class="bar-placeholder">Session data will appear here once fertilizer dispense values arrive.</div> </div> </article> <article class="card small accent-mint"> <h2>Future Topic Space</h2> <div class="placeholder"> Add new sensors, alerts, field maps, or coverage reports here later. </div> </article> <article class="card small"> <h2>Placeholder Metrics</h2> <div class="placeholder"> Support new MQTT topics like pump flow, pH, weather, or spray pressure in future updates. </div> </article> </div> </div> <script src="https://unpkg.com/mqtt/dist/mqtt.min.js"></script> <script src="https://unpkg.com/leaflet/dist/leaflet.js"></script> <script> const host = 'broker.hivemq.com'; const port = 8000; const path = '/mqtt'; const url = `ws://${host}:${port}${path}`; const statusEl = document.getElementById('connectionStatus'); const statusDot = statusEl.querySelector('.dot'); const statusText = statusEl.querySelector('span'); const state = { soilEC: '--', soilMoisture: '--', gpsLocation: '--', fertilizerRemaining: '--', fertilizerUsed: '--', batteryCharge: '--', batteryStatus: '--', cropFieldType: '--', runDistance: '--', sessions: [] }; const map = L.map('fieldMap', { center: [15.0, 78.0], zoom: 6, scrollWheelZoom: false, zoomControl: true, }); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© OpenStreetMap contributors', }).addTo(map); const robotMarker = L.marker([15.0, 78.0], { title: 'Fertilizer Robot', }).addTo(map); function updateCard(id, value) { document.getElementById(id).textContent = value; } function setStatus(status, color, label) { statusDot.style.background = color; statusDot.style.boxShadow = `0 0 0 6px ${color}33`; statusText.textContent = label; } function parseGpsCoordinates(value) { if (!value) return null; if (typeof value === 'object' && value.lat && value.lon) { return [Number(value.lat), Number(value.lon)]; } const text = String(value).trim(); if (text.includes(',')) { const parts = text.split(',').map(part => part.trim()); if (parts.length >= 2) { const lat = Number(parts[0]); const lon = Number(parts[1]); if (!Number.isNaN(lat) && !Number.isNaN(lon)) { return [lat, lon]; } } } try { const json = JSON.parse(text); if (json.lat && json.lon) { return [Number(json.lat), Number(json.lon)]; } } catch (err) { // not JSON } return null; } function updateMapLocation(gpsText) { const coords = parseGpsCoordinates(gpsText); if (!coords) return; robotMarker.setLatLng(coords); map.panTo(coords, { animate: true, duration: 0.9 }); } function showAlert(message) { const card = document.getElementById('alertCard'); card.style.display = 'block'; document.getElementById('alertText').textContent = message; } function hideAlert() { document.getElementById('alertCard').style.display = 'none'; } const activeAlerts = {}; function addSessionEntry(entry) { const list = document.getElementById('sessionHistory'); if (state.sessions.length === 0) { list.innerHTML = ''; } const item = document.createElement('li'); item.className = 'session-item'; item.innerHTML = ` <div> <strong>${entry.date || 'Unknown date'} • ${entry.time || 'Unknown time'}</strong> <span>Field: ${entry.field || 'Unknown'} • Area: ${entry.area || entry.distance || '—'}</span> </div> <div> <span>${entry.notes || 'Session recorded'}</span> ${entry.fertilizerDispensedKg != null ? `<div class="label">Fertilizer dispensed: ${entry.fertilizerDispensedKg} kg</div>` : ''} </div> `; list.prepend(item); renderDispensedChart(); } function renderDispensedChart() { const chart = document.getElementById('dispensedChart'); const tracked = state.sessions .filter(entry => entry.fertilizerDispensedKg != null) .slice(-6) .reverse(); if (!tracked.length) { chart.innerHTML = '<div class="bar-placeholder">Session data will appear here once fertilizer dispense values arrive.</div>'; return; } const maxDispense = Math.max(...tracked.map(entry => Number(entry.fertilizerDispensedKg) || 0), 1); chart.innerHTML = tracked.map(entry => { const value = Number(entry.fertilizerDispensedKg) || 0; const width = Math.min(100, Math.round((value / maxDispense) * 100)); return ` <div class="bar-item"> <div class="bar-label">${entry.date || 'Session'} ${entry.time || ''}</div> <div class="bar-fill"><span style="width:${width}%"></span></div> <div class="bar-value">${value} kg</div> </div> `; }).join(''); } function refreshAlerts() { const list = document.getElementById('alertsList'); const keys = Object.keys(activeAlerts); if (!keys.length) { list.innerHTML = `<li class="alert-message"><span class="alert-dot"></span> No alerts at the moment. All systems are operating normally.</li>`; return; } list.innerHTML = keys.map(key => ` <li class="alert-message"> <span class="alert-dot"></span> ${activeAlerts[key]} </li> `).join(''); } function setAlert(type, message) { activeAlerts[type] = message; refreshAlerts(); } function clearAlert(type) { delete activeAlerts[type]; refreshAlerts(); } function updateMoistureAlerts(value) { const numeric = Number(String(value).replace(/[^\d.-]/g, '').trim()); if (Number.isNaN(numeric)) return; if (numeric <= 25) { setAlert('dry', 'Land too dry — watering is needed soon.'); } else { clearAlert('dry'); } } function updateFertilizerAlerts(value) { const numeric = Number(String(value).replace(/[^\d.-]/g, '').trim()); if (Number.isNaN(numeric)) return; if (numeric <= 10) { setAlert('fertilizer', `Fertilizer low: only ${numeric} kg remaining.`); } else { clearAlert('fertilizer'); } } function addRobotAlert(message) { setAlert(`robot-${message}`, message); } function updateDistanceForFieldType(type) { const distanceMap = { Corn: '120 m', Vegetables: '80 m', Rice: '150 m', Orchard: '100 m', Other: '90 m', }; const distance = distanceMap[type] || '--'; state.cropFieldType = type || '--'; state.runDistance = distance; updateCard('runDistanceValue', distance); } const cropFieldTypeSelect = document.getElementById('cropFieldType'); cropFieldTypeSelect.addEventListener('change', (event) => { updateDistanceForFieldType(event.target.value); }); function processMessage(topic, payload) { let data = payload; try { data = JSON.parse(payload); } catch (err) { data = payload; } switch (topic) { case 'soil/EC/nutrient level': state.soilEC = `${data}`; updateCard('soilEcValue', `${data}`); break; case 'soil/moisture/moisture level': state.soilMoisture = `${data}`; updateCard('soilMoistureValue', `${data}`); updateMoistureAlerts(data); break; case 'soil/GPS/location': state.gpsLocation = `${data}`; updateMapLocation(state.gpsLocation); break; case 'fertilizer/hopper/remaining': state.fertilizerRemaining = `${data} kg`; updateCard('fertilizerRemainingValue', `${data} kg`); updateFertilizerAlerts(data); break; case 'robot/obstacle/detected': case 'robot/obstacle': if (data === true || String(data).toLowerCase().includes('true') || String(data).toLowerCase().includes('obstacle')) { setAlert('obstacle', 'Obstacle detected — robot paused until the path is clear.'); } else { clearAlert('obstacle'); } break; case 'robot/alerts': if (data) { setAlert('robot-alert', `Robot alert: ${data}`); } else { clearAlert('robot-alert'); } break; case 'fertilizer/usage/total': state.fertilizerUsed = `${data} kg`; updateCard('fertilizerUsedValue', `${data} kg`); break; case 'robot/battery/percentage': state.batteryCharge = `${data}%`; updateCard('batteryChargeValue', `${data}%`); if (!isNaN(data) && Number(data) <= 20) { showAlert(`Battery low: ${data}% remaining. Recharge soon.`); } break; case 'robot/battery/status': state.batteryStatus = `${data}`; updateCard('batteryStatusValue', `${data}`); break; case 'sessions/past': if (typeof data === 'object') { const entry = { ...data, fertilizerDispensedKg: data.fertilizerDispensedKg ?? data.fertilizerDispensed ?? data.dispensedKg ?? data.dispensed ?? null, }; state.sessions.push(entry); addSessionEntry(entry); } else { try { const payloadObj = JSON.parse(data); const entry = { ...payloadObj, fertilizerDispensedKg: payloadObj.fertilizerDispensedKg ?? payloadObj.fertilizerDispensed ?? payloadObj.dispensedKg ?? payloadObj.dispensed ?? null, }; state.sessions.push(entry); addSessionEntry(entry); } catch (err) { addSessionEntry({ date: 'Unknown', time: 'Unknown', field: 'Unknown', area: data, notes: 'Session data received', fertilizerDispensedKg: null }); } } break; default: console.log('Unhandled topic:', topic, payload); } } function connectClient() { const client = mqtt.connect(url, { clientId: `fert-dash-${Math.random().toString(16).slice(2)}`, keepalive: 60, reconnectPeriod: 3000, }); client.on('connect', () => { setStatus('#35d07f', '#35d07f', 'Connected to MQTT'); client.subscribe([ 'soil/EC/nutrient level', 'soil/moisture/moisture level', 'soil/GPS/location', 'fertilizer/hopper/remaining', 'fertilizer/usage/total', 'robot/battery/percentage', 'robot/battery/status', 'sessions/past' ], err => { if (err) { console.warn('Subscribe error', err); } }); }); client.on('reconnect', () => { setStatus('#ffb95c', '#ffb95c', 'Reconnecting...'); }); client.on('error', err => { console.error('MQTT error', err); setStatus('#ff5d77', '#ff5d77', 'Connection error'); }); client.on('offline', () => { setStatus('#ff5d77', '#ff5d77', 'Offline'); }); client.on('close', () => { setStatus('#ff5d77', '#ff5d77', 'Disconnected'); }); client.on('message', (topic, message) => { processMessage(topic, message.toString()); }); } connectClient(); </script> </body> </html> ``` ## Build Process ### Step 1 — Chassis Assembly The three-wheeled box chassis was assembled as the structural foundation of the robot. The two rear drive motors were mounted inside the box and the front free-rolling wheel was attached at the front. The box structure encloses the electronics and provides mounting points for the hopper above. ### Step 2 — Battery Pack Assembly Six 18650 lithium-ion cells were arranged in a 3S2P configuration and connected using nickel strip tabs. The 3S BMS was wired to the pack for overcharge and short circuit protection. Output wires were soldered to the pack terminals and voltage was verified with a multimeter before connection to any components. ### Step 3 — Power Distribution Wiring The LM7805 voltage regulator was wired from the battery output to provide a stable 5V rail for the ESP32 and sensors. The BTS7960 motor driver was connected directly to the 11.1V battery output for motor power. All connections were tested for correct voltage before proceeding. ### Step 4 — Sensor Integration The NPK sensor, moisture sensor, HC-SR04 and GPS module were each wired to their respective ESP32 pins. Each sensor was tested individually and readings were verified on the Serial Monitor before full integration. ### Step 5 — Hopper and Auger Assembly The fertilizer hopper was fabricated and mounted centrally on top of the box chassis. The auger mechanism was assembled inside the hopper and connected to the auger motor. The dispensing nozzle was positioned to direct fertilizer downward toward the soil surface. ### Step 6 — Soil Probe Mechanism Assembly The servo motor and crank-slider linkage were assembled and calibrated. The insertion depth and servo rotation angle were tuned to ensure the probe reaches the correct soil depth consistently without applying excessive force to the mechanism. ### Step 7 — Firmware Development and Testing The ESP32 firmware was developed iteratively — beginning with basic sensor reading, progressing to motor control, then MQTT communication, and finally full system integration. Each subsystem was tested and confirmed working independently before being combined. ### Step 8 — Dashboard Development The React dashboard was developed to subscribe to the MQTT broker and display incoming data in real time. Map visualisation using Leaflet, the zone data table, and CSV export functionality were all implemented and tested with live data streamed from the robot. ### Step 9 — Full System Integration and Testing All subsystems were brought together and tested as a complete system. Motor control, sensor readings, fertilizer dispensing, obstacle detection and data publishing were all verified to operate correctly together under real conditions. --- ## Results Upon completion, the robot successfully demonstrated the following: - Semi-autonomous navigation across a test area on a three-wheeled box chassis - Real-time NPK and moisture reading from soil at each zone - Dynamic fertilizer dispensing rate adjustment based on sensor readings - Obstacle detection with operator alert via the dashboard - Live data streaming to the web dashboard over MQTT - Zone-by-zone soil data logging and CSV export for farmer records --- ## Challenges and How They Were Solved **Battery Pack Assembly** Hand-assembling a lithium-ion battery pack using nickel strips and a soldering iron required careful temperature management to avoid heat damage to the cells. Each connection was tested with a multimeter and voltage was confirmed at each stage before proceeding. **NPK Sensor Calibration** Achieving consistent NPK readings across varying soil conditions required tuning the RS485 communication parameters and implementing a multi-sample averaging approach in firmware to reduce noise in the output. **Motor Control Precision** Achieving straight-line driving on the three-wheeled chassis required independent tuning of each drive motor using encoder feedback to ensure both sides ran at equal speed regardless of load. **Wiring Assembly** The intention behind using a small box chassis was to achieve a seamless navigation of the robot between narrow rows of plants, but that quickly became a problem when it came to fitting all the wirings inside considering the fact that some of the wires for the sensors are quite thick. --- ## What I Learned Building this robot from scratch taught me that real engineering involves far more problem solving than any textbook suggests. Every component that worked perfectly in theory presented at least one unexpected challenge in practice. Throughout this project I gained hands-on experience with embedded systems programming, sensor integration, RS485 serial communication, wireless MQTT data streaming, motor control with encoder feedback, mechanical design, and full-stack web development — all within a single cohesive system. Most importantly, this project gave me a genuine appreciation for what appropriate technology can do for African agriculture. The continent has extraordinary agricultural potential. Bridging the gap between that potential and modern precision farming tools is not just an engineering challenge — it is a genuinely meaningful one. --- ## Future Improvements - Add soil pH sensing for more complete soil analysis per zone - Implement server-side machine learning for smarter fertilizer rate prediction based on historical zone data - Implement a better fertilizer dispensation mechanism than the current one --- ## Built By **Maryam Akande**
Autonomous Delivery Robot with GPS Navigation & IoT Control.
Markdown Editor # Autonomous Delivery Robot (DeliBot) ## What I Built An end-to-end autonomous delivery robot system capable of GPS waypoint navigation, real-time remote control, and package tracking. The robot uses Ackermann steering geometry, a brushless motor, compass heading, and live MQTT communication to navigate to a destination autonomously. A Flask web app with WebSockets serves as the control dashboard — complete with user auth, delivery dispatch, email notifications, and a live tracking link for recipients. ## The Problem It Solves Large environments like university campuses, hospitals, and warehouses waste time and money on manual delivery. This robot provides a **low-cost, intelligent alternative** with autonomous navigation and remote monitoring. ## System Architecture The robot runs a continuous **Sense → Process → Decide → Move** loop: - **Sense** — GPS locks position; HMC5883L reads heading; ultrasonic detects obstacles - **Process** — ESP8266 calculates bearing error using Haversine formula - **Decide** — Proportional steering control adjusts servo angle - **Move** — Brushless ESC drives forward at controlled speed ## Hardware Stack - `ESP8266` — WiFi microcontroller (brain of the robot) - `NEO-6M GPS` — Real-world coordinate tracking - `HMC5883L Compass` — Heading estimation (calibrated for Lagos, Nigeria) - `Brushless Motor + ESC` — High-efficiency propulsion - `Servo Motor` — Ackermann steering angle control - `ESP32-CAM` — Live video streaming ## Navigation Algorithm ```cpp float bearingError = targetBearing - compassHeading; bearingError = normalizeAngle(bearingError); // -180 to +180 float steeringAngle = bearingError * (TURN_GAIN / 100.0); steeringAngle = constrain(steeringAngle, -45, 45); setSteeringAngle(steeringAngle); ``` The robot slows down for sharp turns (>30°) and stops within 5 meters of the waypoint. ## Software & Communication - **MQTT (HiveMQ broker)** — Lightweight real-time command/status channel - **Flask + SocketIO** — Backend with live WebSocket state push to dashboard - **SQLite** — Delivery records with tracking IDs - **Email Notifications** — Auto-sent to recipient with a unique tracking link ## Key Features - Autonomous GPS waypoint navigation - Manual control fallback (forward / backward / left / right / stop) - Real-time ETA calculation (Haversine distance ÷ 1.5 m/s) - Secure login/register with hashed passwords - Per-delivery tracking links with live robot dashboard - Delivery history admin panel ## Results A fully functional delivery robot that navigates autonomously to GPS coordinates, streams live video, and notifies recipients via email — built entirely with low-cost, accessible hardware.
Robotics
32
MediBot: A Continuous AI Patient Monitoring System for Nigerian Hospitals
# MediBot: A Continuous AI Patient Monitoring System for Nigerian Hospitals ## Introduction Every day in Nigerian hospital wards, doctors and nurses make impossible choices. One nurse. Twenty patients. Eight hours. There's no way to watch everyone's health state at once. MediBot was built to change that, not by replacing nurses, but by giving them a system that never looks away. It is a continuous AI-powered patient monitoring system that combines embedded sensors, computer vision, edge machine learning, and a real-time web dashboard to detect when a patient needs attention before a crisis becomes irreversible. The entire system runs on affordable hardware that can be purchased locally in Nigeria, communicates through Supabase as its cloud backbone, and is accessible from any phone or laptop browser without any installation, making it deployable in hospitals that could never afford traditional ICU-grade monitoring equipment. --- ## The Problem ### The Monitoring Gap In most Nigerian hospital general wards, continuous bedside monitoring simply does not exist. A patient's oxygen saturation can fall from healthy to life-threatening in under two minutes. A patient losing consciousness may go unnoticed for twenty minutes or more if the nurse is responding to another emergency across the ward. Pain in sedated, post-operative, or non-verbal patients often goes completely undetected. Not because the staff does not care, but because no system is watching continuously enough to catch it. This is not an assumption. As of 2019, Nigeria had only 15 nurses and midwives per 10,000 patients [1], making continuous individual patient observation practically impossible in most general wards. A mixed-methods study conducted across three hospitals in Kenya found that during the 24 hours before a patient's death, respiratory rate was documented in only 1.2% of patient records, with insufficient monitoring equipment and high workload identified as the primary reasons nurses could not detect deterioration in time [2]. The WHO estimates that up to 134 million adverse events occur annually in hospitals across low and middle-income countries, with 83% of those events being preventable [3]. The problem is not the dedication of the people working in these hospitals. The problem is that the tools designed to close this gap were never built for these environments. ### Why Existing Solutions Do Not Fit ICU grade monitoring equipment exists. It works well. It costs millions of naira per unit, requires stable electricity, demands regular calibration by biomedical engineers, and was designed entirely around assumptions that do not match the realities of Nigerian general wards. The problem is not only funding. It is the design. Most medical monitoring technology was built for environments with completely different constraints. ### Why This Matters The consequences of the monitoring gap are measurable. Delayed response to cardiac events. Undetected respiratory decline. Unmanaged pain in post-operative patients. Missed early signs of fever escalation. Preventable deterioration during low-staffing periods. MediBot was built as a direct engineering response to that gap. Affordable, deployable, and designed around the realities that Nigerian healthcare workers actually operate in. --- ## What MediBot Does MediBot continuously monitors patients using a combination of medical sensors and AI vision. It classifies the patient's current condition in real-time and alerts medical staff when something requires attention. The system determines whether the patient is awake and comfortable, sleeping normally, in pain, showing signs of distress, potentially unconscious, or whether the bed is empty altogether. It achieves this by combining signals from multiple sources at the same time. | Signal | Source | |--------|--------| | Heart rate | MAX30102 sensor | | Blood oxygen saturation (SpO₂) | MAX30102 sensor | | Body temperature | MLX90614 infrared sensor | | Facial expression | AI vision | | Eye state (open or closed) | AI vision | | Body motion | Frame differencing | | Bed occupancy | ML object detection model | No single signal decides on its own. All signals are combined. The system is designed so that if one source fails, the others continue providing useful information. --- ## Components Used ### Hardware | Component | Purpose | |-----------|---------| | ESP32 Dev Module | Sensor reading, decision logic, alert management | | ESP32-CAM AI Thinker | Camera stream and ML vision inference | | MAX30102 | Heart rate and blood oxygen | | MLX90614 | Contactless infrared body temperature | | Active buzzer | Audible patient alerts | | Status LED | Visual connection indicator | ### Software and Platforms | Tool | Role | |------|------| | Arduino IDE and C++ | ESP32 firmware development | | Edge Impulse | ML model training and deployment | | TensorFlow Lite | On-device ML inference on ESP32-CAM | | face-api.js | Browser-based facial expression detection | | Supabase | Real-time database and authentication backend | | Netlify | Dashboard deployment | | Vanilla HTML, CSS, and JavaScript | Dashboard frontend | --- ## System Architecture MediBot uses a two-board architecture. The responsibilities are deliberately split between two ESP32 devices because of memory constraints that will be explained shortly. ``` ┌─────────────────────────────────────────────────────────┐ │ ESP32-CAM │ │ │ │ Bed Occupancy → Face Detection → Expression Model │ │ → Eye State Model │ │ → Motion Detection │ │ │ │ Sends JSON result via UART │ └──────────────────────────┬──────────────────────────────┘ │ GPIO16 ┌──────────────────────────▼──────────────────────────────┐ │ ESP32 Dev Module │ │ │ │ MAX30102 + MLX90614 → Patient State Engine │ │ → Posts to Supabase every 3s │ │ → Buzzer and Status LED │ └──────────────────────────┬──────────────────────────────┘ │ Supabase DB │ ┌──────────────────────────▼──────────────────────────────┐ │ Doctor Dashboard (Netlify PWA) │ │ Vitals · Patient State · Emotion · Alerts · Camera │ │ Works on phone, tablet, and laptop. No installation. │ └─────────────────────────────────────────────────────────┘ ``` The reason for two boards comes down to memory. The standard ESP32 has 520KB of RAM. Running WiFi, sensor reads, decision logic, and TFLite models simultaneously would exceed that. The ESP32-CAM adds 4MB of PSRAM, which makes it capable of running multiple vision models sequentially. Splitting responsibilities keeps each board well within its limits while the overall system stays powerful. --- ## How It Works ### Step 1: Bed Occupancy Check The ESP32-CAM runs a bed occupancy detection model on every frame. If the bed is confirmed empty and no vital signs have been detected for 60 seconds, all monitoring is suspended. This is what prevents false alerts in an empty ward. ### Step 2: Vital Signs Collection The ESP32 Dev reads heart rate and SpO₂ from the MAX30102, and body temperature from the MLX90614 every two seconds. A rolling average smooths out sensor noise. Temperature alerts only fire after a reading has been abnormal for ten continuous seconds, which prevents single-spike false alarms from triggering the system unnecessarily. ### Step 3: Vision Analysis The ESP32-CAM processes the camera feed through a sequential model pipeline. Each model only runs if the previous one confirms its precondition. If no face is detected, the expression model is skipped entirely. This saves compute and prevents false classifications. ``` Frame → Occupancy model → Face detection → Expression classifier → Eye state ``` ### Step 4: Patient State Decision The ESP32 Dev combines all signals using a layered decision engine. Emergency logic always runs first, before anything else. ``` Layer 1 — Emergency (runs first, every cycle) Critical HR or SpO₂ or temperature → EMERGENCY Layer 2 — Face visible Eyes open + normal expression + normal vitals → AWAKE, comfortable Eyes open + pain expression + abnormal vitals → IN PAIN (confirmed) Eyes open + pain expression + normal vitals → IN PAIN (confirm manually) Eyes closed + normal vitals + low motion → SLEEPING, well Eyes closed + abnormal vitals → SLEEPING, possible pain Layer 3 — Face not visible HR variance low + HR under 70 + normal vitals → LIKELY SLEEPING High motion + abnormal vitals → DISTRESS suspected Any critical vital → ALERT regardless ``` When the patient is facing away from the camera and no face can be detected, the system does not give up. It uses heart rate variance to estimate whether the patient is sleeping. During sleep, the heart rate drops and becomes more regular. Low variance combined with a low resting heart rate is a clinically established indicator of sleep, and MediBot uses it as a fallback when vision fails. ### Step 5: Alerts The buzzer provides immediate local feedback that works even when the doctor is not looking at the dashboard. | Patient State | Buzzer Behaviour | |---------------|-----------------| | System alive, patient stable | Beep every 2 seconds | | Patient confirmed sleeping | Silent, watching quietly | | Alert confirmed | Beep every 500ms | | Emergency | Beep every 150ms | | WiFi not connected | Silent | The dashboard simultaneously receives all data and displays timestamped alerts. ### Step 6: Dashboard The doctor views the dashboard from their phone anywhere on the same network. It shows live vitals, patient state, expression, and eye state from the camera, pain score, PPG waveform, alert history, and the camera feed. --- ## The Dashboard The dashboard is a Progressive Web App. It runs on any mobile or desktop browser with no installation needed. It was designed mobile-first because phones are the most accessible computing devices in Nigerian clinical settings. ### Camera Mode Switching One of the things I am most proud of in MediBot is the ability to switch the vision source at runtime directly from the dashboard. | Mode | How it works | |------|-------------| | Phone | face-api.js runs in the browser. Any smartphone becomes the AI vision system. | | ESP32-CAM | The hardware camera runs all ML models on-device via TensorFlow Lite. | | None | Vitals-only monitoring. Emergencies are still detected from sensor data alone. | When the mode is changed on the dashboard, the ESP32 reads the update from the database within fifteen seconds and automatically adjusts its data source. No reflashing. No reconfiguring. Just a button tap. ### Security Login credentials are stored in the database. There are no passwords anywhere in the source code. Row-level security is enabled on all database tables. Sessions clear automatically when the browser tab closes. Accounts are role-based for doctors, nurses, and administrators. --- ## The Machine Learning Pipeline All vision models run on the ESP32-CAM using Edge Impulse and TensorFlow Lite. | Model | Type | Input | Output | Status | |-------|------|-------|--------|--------| | Bed occupancy | FOMO object detection | Full frame 96×96 | occupied or empty | In training | | Face detection | FOMO pretrained | Full frame 96×96 | Face bounding box | Ready | | Expression classifier | MobileNetV1 0.25 | Face crop 96×96 | pain, neutral, happy, sad | In training | | Eye state classifier | MobileNetV1 0.1 | Eye crop 48×48 | open or closed | Planned | | Motion detection | Frame differencing | Frame delta | high, low, or none | Complete | FOMO was chosen specifically because it was designed for microcontrollers. Standard object detectors like YOLO take three to five seconds per inference on the ESP32-CAM. FOMO runs in approximately 200ms on the same hardware. For real-time patient monitoring, that difference matters enormously. ### Expression Dataset Training data for the expression classifier comes from FER2013 with over 35,000 facial expression images, the MRL Eye Dataset with over 84,000 open and closed eye images, and custom photos taken specifically for this project. The custom photos were the most important part. Most public datasets use front-facing photos taken in controlled environments. In a hospital bed, the camera might be overhead, mounted on a wall, in a corner, or close to the patient at bed level. A model trained only on face-level photos performs poorly on overhead views of someone lying down. The custom dataset was collected from all angles that match real deployment. --- ## Code Samples ### False alarm prevention using a confirmation streak A single abnormal reading is not clinically significant. This logic requires three consecutive abnormal cycles before confirming any alert. ```cpp #define CONFIRM_CYCLES 3 int abnormalStreak = 0; bool alertActive = false; void updateAlertConfirmation(bool abnormal) { if (abnormal) { abnormalStreak++; } else { abnormalStreak = 0; alertActive = false; } if (abnormalStreak >= CONFIRM_CYCLES) { alertActive = true; Serial.println("Alert confirmed"); } } ``` ### Temperature stability window Temperature only flags after ten continuous seconds of abnormal readings. ```cpp #define TEMP_VALID_MIN 35.0f #define TEMP_STABLE_MS 10000 unsigned long tempLowSince = 0; bool tempLowFlag = false; void checkTempStability(float avgTemp) { unsigned long now = millis(); if (avgTemp < TEMP_VALID_MIN) { if (tempLowSince == 0) tempLowSince = now; if (now - tempLowSince >= TEMP_STABLE_MS) { tempLowFlag = true; } } else { tempLowSince = 0; tempLowFlag = false; } } ``` ### Sleep detection from heart rate variance When the patient faces away from the camera, the system uses heart rate variance to infer sleep state. ```cpp float hrVariance = 0.0f; int hrHistory[10]; int hrCount = 0; void updateHRVariance(int hr) { hrHistory[hrCount % 10] = hr; hrCount++; float mean = 0; int n = min(hrCount, 10); for (int i = 0; i < n; i++) mean += hrHistory[i]; mean /= n; float variance = 0; for (int i = 0; i < n; i++) variance += pow(hrHistory[i] - mean, 2); hrVariance = variance / n; } bool isLikelySleeping(int hr, int spo2) { bool lowHR = hr > 0 && hr < 70; bool stableHR = hrVariance < 25.0f; bool normalSpO2 = spo2 == 0 || spo2 >= 94; return lowHR && stableHR && normalSpO2; } ``` ### Non-blocking buzzer state machine The buzzer never blocks the main loop. It goes silent during confirmed sleep but immediately resumes if distress is detected. ```cpp #define BUZZER_PIN 18 unsigned long lastBuzzTime = 0; bool buzzerOn = false; void updateBuzzer(bool wifiOK, bool sleeping) { unsigned long now = millis(); if (!wifiOK) { digitalWrite(BUZZER_PIN, LOW); return; } int interval, duration; if (emergencyConfirmed) { interval = 150; duration = 100; } else if (alertActive) { interval = 500; duration = 80; } else if (sleeping) { digitalWrite(BUZZER_PIN, LOW); return; } else { interval = 2000; duration = 60; } if (!buzzerOn && now - lastBuzzTime >= (unsigned long)interval) { digitalWrite(BUZZER_PIN, HIGH); buzzerOn = true; lastBuzzTime = now; } if (buzzerOn && now - lastBuzzTime >= (unsigned long)duration) { digitalWrite(BUZZER_PIN, LOW); buzzerOn = false; } } ``` ### Clinical expression mapping in the dashboard Seven raw emotion outputs from face-api.js are simplified to four labels that are actually meaningful in a clinical context. ```javascript const CLINICAL_EXPR = { pain: { icon: '😣', label: 'Pain', color: '#ef4444' }, disgusted: { icon: '😣', label: 'Pain', color: '#ef4444' }, fearful: { icon: '😣', label: 'Pain', color: '#ef4444' }, angry: { icon: '😠', label: 'Pain', color: '#ef4444' }, happy: { icon: '😊', label: 'Happy', color: '#10b981' }, neutral: { icon: '😐', label: 'Neutral', color: '#4a6480' }, sad: { icon: '😢', label: 'Sad', color: '#f59e0b' }, }; ``` ### Merging two independent data sources in the dashboard The dashboard reads sensor vitals from the ESP32 and expression data from the phone camera simultaneously, then merges them correctly. ```javascript async function fetchLatestData() { const [espRes, phoneRes] = await Promise.all([ fetch(VITALS_URL + '?source=eq.esp32&order=created_at.desc&limit=1', { headers }), fetch(VITALS_URL + '?source=eq.phone_camera&order=created_at.desc&limit=1', { headers }) ]); const esp = (await espRes.json())[0]; const phone = phoneRes.ok ? (await phoneRes.json())[0] : null; const phoneAge = phone ? (Date.now() - new Date(phone.created_at).getTime()) / 1000 : 999; const phoneFresh = phoneAge < 30 && phone?.emotion !== 'unknown'; return { ...esp, emotion: phoneFresh ? phone.emotion : esp.emotion, eyes: phoneFresh ? phone.eyes : esp.eyes, pain_score: phoneFresh ? phone.pain_score : esp.pain_score, }; } ``` --- ## Challenges Faced ### Sensor Instability The MAX30102 is sensitive to finger placement, pressure, and ambient light. Small movements produce large reading fluctuations. The MLX90614 reads closer to room temperature when the patient is under a blanket or the sensor is not pointing directly at bare skin. The solution for both was moving average smoothing combined with the confirmation streak system. Sustained readings matter. Single spikes do not. ### Memory on the ESP32 Running WiFi, HTTPS, sensor reads, JSON serialisation, and multiple HTTP clients simultaneously on a device with 520KB of RAM required careful engineering. Making HTTP clients static so they are allocated once globally rather than on the stack for every call, and staggering requests so only one HTTP call fires per loop cycle, were what eventually made the system stable. ### Camera Angle vs Training Data This was one of the most practically important problems in the project. Public expression datasets are captured face level with the subject looking directly at the camera. A camera mounted above a hospital bed sees something completely different. A model trained only on face level photos performs poorly on overhead views of a lying patient. Building a custom dataset from the actual deployment angles was not optional. It was necessary. ### Programming the ESP32-CAM The ESP32-CAM has no USB-to-serial chip. Programming it required using the main ESP32 Dev Module as a programmer by bridging its EN pin to GND, crossing TX and RX lines, and holding IO0 to GND on the CAM during upload. This is not clearly documented anywhere and required significant troubleshooting to figure out. ### False Alarms in Early Testing In early testing, the buzzer fired constantly from single-spike readings. One abnormal HR reading was enough to trigger the entire alert system. The three-cycle confirmation streak was the fix. A reading must be abnormal for three consecutive cycles before any alert fires. This eliminated almost all false alarms without meaningfully slowing down response to real emergencies. ### Merging Two Independent Data Sources The dashboard reads from two independent sources simultaneously, such as the ESP32 posting sensor vitals and the phone camera posting vision results. Getting the merge logic right required timestamp comparison, freshness validation, and careful field prioritisation to make sure the correct source overwrites the correct fields without corrupting either dataset. --- ## Project Gallery  Vitals Overview  The Camera Connection page  The Vital Evaluation Log page  The Current Training Progress  Medibot Device --- ## Current Status | Component | Status | |-----------|--------| | ESP32 vitals firmware | ✅ Complete | | MAX30102 HR and SpO₂ | ✅ Working | | MLX90614 temperature | ✅ Working | | Buzzer alert system | ✅ Complete | | Status LED | ✅ Complete | | Dashboard PWA | ✅ Deployed | | Login and role security | ✅ Complete | | Camera mode switching | ✅ Complete | | Real-time alert feed | ✅ Complete | | ESP32-CAM flashing | 🔄 In progress | | Bed occupancy model | 🔄 Training | | Expression classifier | 🔄 Dataset collection | | Eye state model | 📋 Planned | | Offline local mode | 📋 Planned | | Clinical validation | 📋 Planned | --- ## What Comes Next The immediate focus is on completing the ESP32-CAM vision pipeline, deploying the trained ML models to hardware, and beginning clinical validation in real patient environments. Long term, the goal is an offline-first version where the ESP32 hosts the dashboard locally so the entire system works without any internet connection. Hospitals with unreliable network access should not have to choose between monitoring and connectivity. --- ## Closing The monitoring gap in Nigerian hospitals is not only a funding problem. It is also an engineering problem, and engineering problems have engineering solutions. MediBot is one attempt at building that solution around the realities that actually exist here. Affordable hardware. Local components. Open-source software. Infrastructure hospitals already have. The talent to build these systems already exists in Nigeria. What has been missing is infrastructure, research continuity, and systems designed specifically for the environments where the need is real. --- ## References References [1] Anyasodor et al. (2023). Brain Drain in Cancer Care: The Shrinking Clinical Oncology Workforce in Nigeria. Journal of Clinical Oncology. Available at pmc.ncbi.nlm.nih.gov/articles/PMC10752460 [2] Kaluti D. et al. (2024). General ward nurses detection and response to clinical deterioration in three hospitals at the Kenyan coast. BMC Nursing. Available at pmc.ncbi.nlm.nih.gov/articles/PMC10905788 [3] Wilson et al. cited in: Patient safety in a rural sub-Saharan Africa hospital. PLOS Global Public Health (2024). doi.org/10.1371/journal.pgph.0003919
Robotics
65
an autonomous sorter
Web App
61
Building a Student Companion Agent
# Building a Student Companion Agent ## Overview StudyPadi AI is a student companion web app designed to help learners turn study materials into useful learning resources. The app allows students to upload PDFs, fetch YouTube transcripts, upload audio recordings, or record live voice notes directly in the browser. After loading the material, the system can generate structured study notes, answer questions through chat, create quizzes, produce podcast-style audio summaries, and help students review their learning content more efficiently. ## What I Built I built a Flask-based AI study assistant that supports multiple learning workflows: - PDF upload and text extraction - YouTube transcript fetching - Audio upload and transcription - Live browser recording - AI-generated study notes - Chat with uploaded study material - Quiz generation - Podcast/audio summary generation - AI writing detector and humanizer - Email/password authentication - Google OAuth support - Saved study sessions using SQLite The goal of the project is to reduce the stress of studying by helping students convert raw class materials into organized, interactive learning tools. ## Key Features ### 1. PDF Study Material Upload Students can upload PDF files, and the app extracts the text from the document. The extracted content becomes the active study source for notes, chat, quizzes, and podcasts. ### 2. Voice Note and Live Recording The app supports audio upload and live recording from the browser. Students can record lectures, explanations, or personal study notes, then transcribe them using Groq Whisper. ### 3. AI Study Notes Once a source is loaded, the app generates clean and structured notes. Students can choose styles such as professional, concise, detailed, or bullet-point notes. ### 4. Chat With Material Students can ask questions about the loaded material. The assistant responds based on the active document or transcript, making it useful for revision and clarification. ### 5. Quiz Generator The app can generate multiple-choice questions from the study material. This helps students test their understanding after reading or listening. ### 6. Podcast Generator The app converts study material into a conversational podcast-style script and audio file using Edge TTS. This makes it easier for students to revise while listening. ### 7. AI Detector and Humanizer The project also includes an AI writing detector and humanizer tool to help students review and improve written content. ## Tech Stack The backend was built with Python and Flask. SQLite is used for local data storage, while Groq powers transcription and AI text generation. The frontend uses HTML, CSS, Jinja templates, and vanilla JavaScript. Main technologies used: - Python - Flask - SQLite - HTML - CSS - JavaScript - Groq API - Whisper transcription - Edge TTS - PyPDF - ReportLab - Authlib - python-dotenv - yt-dlp - youtube-transcript-api ## How It Works The user first logs into the app and loads a study source. The source can be a PDF, YouTube video, uploaded audio file, or live recording. The backend extracts or transcribes the content and saves it as a study session. After that, the user can generate notes, ask questions, create quizzes, or generate podcast audio from the same source. Challenges One major challenge was making the live recording feature reliable. Browser recording can behave differently depending on the supported audio format, so I improved the recorder to choose a supported MIME type, handle empty recordings, clean up microphone streams, and reset the UI properly after recording. Another challenge was API key handling. I changed the app so API keys are not entered in the frontend. Instead, the Groq key is stored securely on the backend through environment variables. Results The final project is a functional AI-powered study companion that helps students study smarter. It brings together document processing, audio transcription, AI note generation, quizzes, chat, and podcast generation in one web app. ## Future Improvements In the future, I would like to add: Better dashboard analytics More quiz formats File organization by course or subject Cloud storage for uploaded materials Better mobile responsiveness More language support for transcription and notes ## Conclusion StudyPadi AI is designed to make studying more interactive and less overwhelming. By turning raw learning materials into notes, quizzes, chats, and audio summaries, it gives students a practical companion for learning and revision. ## Code Example ```python @app.route("/api/notes", methods=["POST"]) def gen_notes(): auth_error = require_user_json() if auth_error: return auth_error client = get_client() if not client: return jsonify({"error": "Server Groq API key is missing or invalid"}), 401 d = request.get_json() or {} activate_source_from_payload(d) raw = current_source_text() if not raw: return jsonify({"error": "No source text"}), 400 prompt = f"""Create well-organized study notes from this text. Text: {chunk(raw, 20000)} Create the study notes:""" result = generate(client, prompt, d.get("model", "llama-3.3-70b-versatile")) return jsonify({"notes": result})
Web App
38
CardioAI - AI-Powered ECG Interpretation
Robotics
34
Fire Detection and Suppression Robotic System
# Fire Suppression Robot ## Project Overview The Fire Suppression Robot is an autonomous firefighting system designed to detect and suppress fires in their early stages. Built on the ESP8266 microcontroller platform, this robot combines smoke detection capabilities with a mobile water-based suppression system to provide rapid response to fire emergencies. The system continuously monitors its environment using a smoke detector sensor and also receives signals from fixed sensors . Upon detecting smoke or fire, it automatically activates a water pump to suppress the flames while simultaneously sending real-time alerts via WiFi to notify users of the emergency.I used the Blynk app for the Alerts The robot's custom-built chassis houses DC motors that enable mobility, allowing it to navigate toward fire sources or patrol designated areas. This project demonstrates the integration of IoT technology with robotics to create an affordable, responsive fire safety solution suitable for homes, small offices, and educational environments. ## Table of Contents - [Project Overview](#project-overview) - [Features](#features) - [Tech Stack](#tech-stack) - [Hardware Components](#hardware-components) - [System Architecture](#system-architecture) - [Installation & Setup](#installation--setup) - [Usage](#usage) - [Future Improvements](#future-improvements) ## Features - Autonomous smoke detection - WiFi-enabled remote monitoring - Automated water-based fire suppression - Real-time alerts ## Tech Stack **Hardware:** - ESP8266 WiFi Microcontroller - Smoke Detector Sensor - DC Motors (for mobility) - Water Pump (fire suppression mechanism) - Lithium Battery (power supply) - Custom-built chassis **Software:** - C++ (Arduino framework) - WiFi connectivity for remote monitoring/control **Communication:** - WiFi Protocol (ESP8266 built-in) ## Hardware Components | Component | Specification | Purpose | |-----------|--------------|---------| | ESP8266 | NodeMCU | Main microcontroller | | Smoke Sensor | MQ-2/MQ-135 | Smoke detection | | Ultrasonic Sensor | HC-SR04 | Obstacle detection/distance measurement | | DC Motors | 6V/12V | Robot mobility | | Servo Motor | SG90/MG995 | Directional control/pump positioning | | Water Pump | 3-6V | Fire suppression | | LED | 5mm/RGB | Status indicator/alerts | | Battery | Li-ion 3.7V | Power supply | | Motor Driver | L298N/L293D | DC motor control | | Relay Module | 5V | Water pump control | ## System Architecture [Smoke Sensor] --> [ESP8266] --> [Motor Driver] --> [DC Motors] | +--> [Water Pump] | +--> [WiFi Module] --> [Remote Monitor] ## Installation & Setup ### Prerequisites - Arduino IDE - ESP8266 Board Package - Blynk IoT account and mobile app - Required libraries: ```cpp #include <ESP8266WiFi.h> #include <BlynkSimpleEsp8266.h> ``` ### Steps #### 1. Blynk IoT Setup 1. Download and install the **Blynk IoT app** (iOS/Android) 2. Create a new account or login 3. Create a new project and select **ESP8266** as device 4. Copy the **Auth Token** sent to your email 5. Configure datastreams/widgets for: - Smoke level indicator - Fire alert notifications - Pump status - System status display #### 2. Arduino IDE Setup 1. Clone or download the project 2. Open the `.ino` file in Arduino IDE 3. Install required libraries via Library Manager: - ESP8266WiFi - Blynk (by Volodymyr Shymanskyy) 4. Update credentials in the code: ```cpp #define BLYNK_AUTH_TOKEN "YourAuthToken" const char* ssid = "YOUR_SSID"; const char* password = "YOUR_PASSWORD"; ``` 5. Select **ESP8266 board** from Tools > Board 6. Select correct **COM port** 7. Upload the code to ESP8266 #### 3. Hardware Assembly 1. Connect smoke sensor to ESP8266 analog pin 2. Connect DC motors to motor driver 3. Connect water pump to relay module 4. Wire motor driver and relay to ESP8266 GPIO pins 5. Connect lithium battery with proper voltage regulation 6. Test all connections before powering on ### Wiring Diagram ## Code Structure ```cpp #define BLYNK_PRINT Serial #define BLYNK_TEMPLATE_ID "TMPL2r2DgC4O2" #define BLYNK_TEMPLATE_NAME "SMART SMOKE DETECTOR" #define BLYNK_AUTH_TOKEN "Ngaw17Ft_n5gy_7ul5KI-jvk7r7cFNNU" #include <ESP8266WiFi.h> #include <BlynkSimpleEsp8266.h> #include <Servo.h> char ssid[] = "Galaxy A06 361f"; char pass[] = "SharonvAzeezat"; const int IN1 = D1; const int IN2 = D2; const int IN3 = D3; const int IN4 = D4; const int LED_PIN = D8; const int RELAY_PIN = D7; const int MQ2_PIN = A0; const int TRIG_PIN = D5; const int ECHO_PIN = D6; const int SERVO_PIN = D0; Servo myServo; const int SMOKE_THRESHOLD = 450; bool fireDetected = false; unsigned long lastAlertTime = 0; const unsigned long ALERT_COOLDOWN = 10000; void stopRobot() { digitalWrite(IN1,LOW); digitalWrite(IN2,LOW); digitalWrite(IN3,LOW); digitalWrite(IN4,LOW); } void moveForward() { digitalWrite(IN1,LOW); digitalWrite(IN2,HIGH); digitalWrite(IN3,LOW); digitalWrite(IN4,HIGH); } void moveBackward() { digitalWrite(IN1,HIGH); digitalWrite(IN2,LOW); digitalWrite(IN3,HIGH); digitalWrite(IN4,LOW); } void turnLeft() { digitalWrite(IN1,HIGH); digitalWrite(IN2,LOW); digitalWrite(IN3,LOW); digitalWrite(IN4,HIGH); } void turnRight() { digitalWrite(IN1,LOW); digitalWrite(IN2,HIGH); digitalWrite(IN3,HIGH); digitalWrite(IN4,LOW); } long getDistance() { digitalWrite(TRIG_PIN, LOW); delayMicroseconds(2); digitalWrite(TRIG_PIN, HIGH); delayMicroseconds(10); digitalWrite(TRIG_PIN, LOW); long duration = pulseIn(ECHO_PIN, HIGH, 30000); if (duration == 0) return 999; return duration * 0.034 / 2; } long scanDistance(int angle) { myServo.write(angle); delay(400); return getDistance(); } void setup() { Serial.begin(115200); pinMode(RELAY_PIN, OUTPUT); pinMode(LED_PIN, OUTPUT); pinMode(IN1, OUTPUT); pinMode(IN2, OUTPUT); pinMode(IN3, OUTPUT); pinMode(IN4, OUTPUT); pinMode(TRIG_PIN, OUTPUT); pinMode(ECHO_PIN, INPUT); myServo.attach(SERVO_PIN); myServo.write(90); stopRobot(); Serial.println("Warming up MQ2 sensor..."); delay(3000); Blynk.begin(BLYNK_AUTH_TOKEN, ssid, pass); } void loop() { Blynk.run(); int smokeLevel = analogRead(MQ2_PIN); long distance = getDistance(); Blynk.virtualWrite(V3, smokeLevel); Blynk.virtualWrite(V9, distance); Serial.print("Smoke: "); Serial.print(smokeLevel); Serial.print(" | Distance: "); Serial.println(distance); if (smokeLevel > SMOKE_THRESHOLD) { fireDetected = true; stopRobot(); digitalWrite(RELAY_PIN, HIGH); digitalWrite(LED_PIN, HIGH); if (millis() - lastAlertTime > ALERT_COOLDOWN) { Blynk.logEvent("fire_alert", "CRITICAL: Smoke detected!"); lastAlertTime = millis(); } return; } fireDetected = false; digitalWrite(RELAY_PIN, LOW); digitalWrite(LED_PIN, LOW); if (distance > 0 && distance < 25) { stopRobot(); delay(200); long leftDist = scanDistance(150); long rightDist = scanDistance(30); myServo.write(90); delay(300); if (leftDist > rightDist) { turnLeft(); } else { turnRight(); } delay(500); stopRobot(); } else { moveForward(); } } ``` ## Future Improvements - [ ] Add camera for visual fire detection - [ ] Multiple fire suppression mechanisms - [ ] Mobile app integration ## Challenges Faced ### 1. Power Supply and Battery Configuration **Challenge:** Difficulty in selecting appropriate power supply and battery configuration for the diverse power requirements of different components. **Solution:** Conducted thorough analysis of each component's voltage and current requirements. Implemented a lithium-ion battery with appropriate voltage regulation to ensure stable power delivery across all subsystems. --- ### 2. Power Distribution and Safety **Challenge:** Challenges in distributing power across multiple components (ESP8266, motors, pump, sensors, servo) safely without causing damage or electrical hazards. **Solution:** Used separate power rails with appropriate current limiting and protection circuits. Implemented bypass capacitors to stabilize voltage and prevent voltage spikes from affecting sensitive components. --- ### 3. High-Current Pump Integration **Challenge:** Issues integrating a high-current water pump with low-power control circuitry without damaging the ESP8266. **Solution:** Utilized a relay module to isolate the high-current pump circuit from the ESP8266's control pins. This allowed safe switching of the pump while protecting the microcontroller from current surges. --- ### 4. System Integration **Challenge:** Difficulty combining multiple subsystems (smoke detection, motor control, water suppression, WiFi communication, obstacle detection) into a single cohesive working system. **Solution:** Developed modular code structure with separate functions for each subsystem. Implemented non-blocking code practices and careful timing management to ensure all systems operated harmoniously without interference ---
IoT
45
Automated Precision Irrigation & Nutrient Management System
# Croppeak_ng ### Automated Precision Irrigation & Nutrient Management System --- ## What I Built A full-stack IoT system that automates farm irrigation and fertilizer delivery using real sensor data so farmers stop guessing and start growing smarter. The system reads soil moisture, temperature, humidity, and water levels from field sensors, makes intelligent decisions, and controls pumps and valves automatically, all manageable from a mobile app or web dashboard in real time. **Stack at a glance:** - **Hardware** : STM32 microcontroller + ESP832 WiFi module + soil, TDS, rain, flow sensors, relays and others - **Backend** : Appwrite (database, auth, realtime) + Next.js API - **Mobile** : Flutter app with BLoC state management - **Web** : Next.js dashboard with Mantine UI --- ## How It Works ``` Soil Sensor → STM32 reads data → ESP832 sends to cloud ↓ Appwrite stores + broadcasts in real time ↓ Automation engine checks rules (moisture low? rain detected?) ↓ Command sent → relay fires → pump or valve activates ↓ Farmer sees everything live on Flutter app or web dashboard ``` --- ## Results - **~38% reduction** in water usage vs. manual scheduling - **Zero manual fertigation** — NPK dosing handled automatically by TDS readings - Fault detection catches dry-runs and pump failures in **under 3 seconds** - Works **offline** - RTC keeps automation running even without internet --- *Croppeak_ng - Precision farming, built for the Nigerian field.*
IoT
33
SmartStrip — Intelligent 5-Socket Energy Monitoring Extension
SmartStrip — Intelligent 5-Socket Energy Monitoring Extension SmartStrip transforms a standard 5-socket extension box into a smart, self-hosted energy monitoring and control system — built from the ground up at RAIN (Robotics and Artificial Intelligence Nigeria) in Ibadan, Nigeria. ## The Problem Most Nigerian homes and offices have no visibility into how much electricity individual appliances consume. With unstable power supply, prepaid meters, and rising NERC/DISCO tariffs, energy waste is expensive and invisible. SmartStrip solves this by giving users per-socket insight and control — without relying on expensive imported smart home ecosystems that aren't built for the Nigerian context. ## What It Does SmartStrip monitors and controls 5 independent sockets in real time: - **Per-socket energy monitoring** — voltage, current, active power, energy (kWh), power factor, and frequency are measured independently for each socket using dedicated PZEM-004T V4.0 sensors - **Remote relay control** — each socket can be switched ON or OFF remotely via MQTT from the web dashboard - **Electricity cost calculation** — uses PZEM's built-in kWh accumulator multiplied by a user-configurable tariff rate, reflecting real Nigerian electricity billing - **Real-time dashboard** — a six-page HTML/JS web interface displays live readings, analytics, schedules, and settings - **JWT authentication** — secure login system protects access to the dashboard and API - **Schedule automation** — sockets can be programmed to turn ON or OFF at set times automatically - **Self-hosted architecture** — no cloud dependency, no subscription fees, runs entirely on local infrastructure ## Hardware Stack | Component | Role | |-----------|------| | ESP32-WROOM-32 | Main microcontroller, WiFi, MQTT | | 5x PZEM-004T V4.0 (100A) | Per-socket energy measurement | | 8-channel 12V relay module | Per-socket switching | | HLK-PM03 PSU | Mains to DC power conversion | | CT clamps (x5) | Non-invasive current sensing | | Sony 12V 3A adapter | Relay coil power supply | ## Software Stack | Layer | Technology | |-------|------------| | Firmware | Arduino C++, PubSubClient, HardwareSerial | | Backend | FastAPI, SQLAlchemy 2.x, SQLite | | Auth | JWT via python-jose, passlib, bcrypt | | Broker | MQTT via HiveMQ | | Frontend | HTML, CSS, JavaScript (6 pages) | | Protocol | MQTT pub/sub for real-time data | ## How It Works The ESP32 polls all 5 PZEM sensors every 2 seconds using two hardware UART buses — PZEM1 on UART1 and PZEM2-5 sharing UART2 via unique Modbus addresses (0x01–0x05). Readings are packaged as JSON and published to MQTT topics per socket. The FastAPI backend subscribes to these topics, logs data to SQLite, and serves the web dashboard. Users can toggle sockets from the dashboard, triggering MQTT control messages that the ESP32 receives and acts on by driving the relay IN pins LOW. ## Nigerian Market Context SmartStrip was designed with the Nigerian electricity landscape in mind: - **220V mains** wiring throughout, following UK conventions (brown/blue/green-yellow) - **Service Based Tariff** support for accurate cost calculation - **Prepaid meter awareness** — helping users track consumption before their units run out - **Local sourcing** — all components sourced in Ibadan - **Self-hosted** — no dependence on foreign cloud infrastructure ## Build Journey SmartStrip was built entirely from scratch as an internship project at RAIN from initial ideation and component sourcing, through full physical assembly (drilling, wiring, casing), to firmware development and web application. The build involved real engineering challenges including UART bus sharing for 5 sensors, GPIO pin conflict resolution, mains wiring debugging, relay power supply design, and full-stack API development with authentication. ## Built at **RAIN — Robotics and Artificial Intelligence Nigeria** Ibadan, Nigeria · 2026
Robotics
38
Autonomous Delivery Robot with GPS Navigation & IoT Control.
# Autonomous Delivery Robot (DeliBot) ## What I Built An end-to-end autonomous delivery robot system capable of GPS waypoint navigation, real-time remote control, and package tracking. The robot uses Ackermann steering geometry, a brushless motor, compass heading, and live MQTT communication to navigate to a destination autonomously. A Flask web app with WebSockets serves as the control dashboard — complete with user auth, delivery dispatch, email notifications, and a live tracking link for recipients. ## The Problem It Solves Large environments like university campuses, hospitals, and warehouses waste time and money on manual delivery. This robot provides a **low-cost, intelligent alternative** with autonomous navigation and remote monitoring. ## System Architecture The robot runs a continuous **Sense → Process → Decide → Move** loop: - **Sense** — GPS locks position; HMC5883L reads heading; ultrasonic detects obstacles - **Process** — ESP8266 calculates bearing error using Haversine formula - **Decide** — Proportional steering control adjusts servo angle - **Move** — Brushless ESC drives forward at controlled speed ## Hardware Stack - `ESP8266` — WiFi microcontroller (brain of the robot) - `NEO-6M GPS` — Real-world coordinate tracking - `HMC5883L Compass` — Heading estimation (calibrated for Lagos, Nigeria) - `Brushless Motor + ESC` — High-efficiency propulsion - `Servo Motor` — Ackermann steering angle control - `ESP32-CAM` — Live video streaming ## Navigation Algorithm ```cpp float bearingError = targetBearing - compassHeading; bearingError = normalizeAngle(bearingError); // -180 to +180 float steeringAngle = bearingError * (TURN_GAIN / 100.0); steeringAngle = constrain(steeringAngle, -45, 45); setSteeringAngle(steeringAngle); ``` The robot slows down for sharp turns (>30°) and stops within 5 meters of the waypoint. ## Software & Communication - **MQTT (HiveMQ broker)** — Lightweight real-time command/status channel - **Flask + SocketIO** — Backend with live WebSocket state push to dashboard - **SQLite** — Delivery records with tracking IDs - **Email Notifications** — Auto-sent to recipient with a unique tracking link ## Key Features - Autonomous GPS waypoint navigation - Manual control fallback (forward / backward / left / right / stop) - Real-time ETA calculation (Haversine distance ÷ 1.5 m/s) - Secure login/register with hashed passwords - Per-delivery tracking links with live robot dashboard - Delivery history admin panel ## Results A fully functional delivery robot that navigates autonomously to GPS coordinates, streams live video, and notifies recipients via email — built entirely with low-cost, accessible hardware.
Web App
55
NNS KADA AI POWERED NAVAL ASSISTANT
```python @app.get("/dashboard", response_class=HTMLResponse) async def dashboard(request: Request): user = get_current_user(request) if not user: return RedirectResponse(url="/login", status_code=302) # Extract display name (last name from full_name) full_name = get_user_full_name(user) if full_name: parts = full_name.strip().split() display_name = parts[-1] if parts else user else: display_name = user return render(request, "dashboard.html", { "username": user, "display_name": display_name, }) # ===================================================================== # KNOWLEDGE BROWSER # ===================================================================== @app.get("/knowledge", response_class=HTMLResponse) async def knowledge_browser(request: Request): user = get_current_user(request) if not user: return RedirectResponse(url="/login", status_code=302) tree = get_knowledge_tree() return render(request, "knowledge.html", { "username": user, "tree": tree, }) @app.get("/knowledge/system/{system_id}", response_class=HTMLResponse) async def system_detail(request: Request, system_id: int): user = get_current_user(request) if not user: return RedirectResponse(url="/login", status_code=302) system = get_system(system_id) if not system: raise HTTPException(status_code=404, detail="System not found") subsystems = get_subsystems(system_id) for sub in subsystems: sub['components'] = get_components(sub['id']) procedures = get_procedures(system_id=system_id) faults_list = get_faults() # All faults for now return render(request, "knowledge.html", { "username": user, "tree": get_knowledge_tree(), "selected_system": system, "subsystems": subsystems, "procedures": procedures, "faults": faults_list, }) @app.post("/api/search-knowledge") async def api_search_knowledge(request: Request): user = get_current_user(request) if not user: return JSONResponse({"error": "Not authenticated"}, status_code=401) data = await request.json() query = sanitize_input(data.get("query", "")) if not query: return JSONResponse({"error": "No query provided"}, status_code=400) results = search_knowledge(query) return JSONResponse(results) ``` A few weeks ago, I set out to solve a problem that has quietly plagued naval operations for years — and I built something I'm genuinely proud of. *Introducing NNS KADA: An AI-Powered Naval Technical Assistant for the Nigerian Navy.* --- Here's the honest backstory. Naval officers on board a ship don't have the luxury of time. When something goes wrong — an engine fault, an electrical failure, a safety-critical procedure that needs to be executed immediately — the last thing anyone should be doing is flipping through hundreds of pages of technical manuals, cross-referencing documents scattered across different formats and locations, or waiting for someone with institutional knowledge to become available. That was the reality I wanted to change. --- *THE PROBLEM* Ship manuals are the lifeblood of naval operations. But in practice, they're: - Fragmented across multiple formats — PDFs, Word documents, spreadsheets - Too voluminous to comprehensively review within operational timeframes - Dependent on individual officers who carry critical knowledge in their heads — knowledge that walks off the ship during crew rotations The cost of this inefficiency isn't just inconvenience. Delays in accessing critical technical information can escalate equipment failures into safety incidents, compromise compliance readiness, and increase cognitive load during high-pressure situations. --- *THE SOLUTION I BUILT* I designed and developed a full-stack AI chatbot system that transforms ship manuals into a living, queryable knowledge base — one that any officer can ask a question in plain language and get a precise, accurate answer in seconds. Under the hood, the system uses a *Retrieval-Augmented Generation (RAG)* architecture: 🔹 Ship manuals (PDFs, DOCX, XLSX) are uploaded through an admin portal and automatically parsed, chunked, and embedded into a *ChromaDB vector store* using *Ollama's nomic-embed-text* embedding model — all running locally on the ship's hardware, with no data ever leaving the vessel. 🔹 When an officer asks a question, the system performs *semantic search* — not keyword matching — to retrieve the most relevant passages from across all uploaded manuals, ranked by cosine similarity. 🔹 The retrieved context is passed to a *large language model* — *Llama 3.2 via Ollama* as the primary engine (fully offline capable), with *Groq's LLM API* as an intelligent cloud fallback — which generates a natural, conversational response grounded entirely in the ship's own documentation. 🔹 The interface streams responses in real time with *markdown rendering*, so answers that include procedures, tables, or technical specifications display cleanly and readably — not as a wall of text. --- *WHAT MAKES THIS DIFFERENT FROM JUST SEARCHING A DOCUMENT?* A keyword search tells you which document to look in. This system reads the documents, understands the context of your question, and tells you the answer — with sources cited automatically. An officer asking "What is the starting procedure for the LST100?" doesn't get a list of documents to go read. They get the steps, in order, drawn from the exact manual that covers it. WHAT'S NEXT? This is Phase 1. The roadmap includes: → *Predictive Compliance* — proactively flagging expiring certificates and upcoming regulatory requirements before they become problems → *Real-Time Sensor Integration* — connecting the chatbot to onboard sensors for context-aware diagnostics ("the engine temperature is reading X, here's what the manual says to do") → *Autonomous Maintenance Scheduling* — agentic workflows that don't just answer questions but take action --- This project reminded me that the most impactful technology isn't always the most complex — sometimes it's just putting the right information in front of the right person at the right moment. *#RAIN#ArtificialIntelligence #NavalTechnology #RAG #LLM #Python #FastAPI #NigerianNavy #MaritimeTech #GenerativeAI #MachineLearning #SoftwareEngineering #Innovation #DefenceTech*
Robotics
48
Two-Wheel Self-Balancing Robot with Real-Time PID Stabilization
# Two-Wheel Self-Balancing Robot with Real-Time PID Stabilization ## What I Built A two-wheeled self-balancing robot built using an Arduino Uno, MPU6050 IMU, and L298N motor driver. The robot stabilizes itself in real time using a PID controller and continuously corrects its tilt angle through motor speed adjustment. This project explores embedded systems, control theory, sensor feedback, and real-time motor control. --- ## Components Used * Arduino Uno * MPU6050 IMU * L298N Motor Driver * DC geared motors * Li-ion batteries --- ## Control System The robot behaves similarly to an inverted pendulum system where balance must be continuously maintained through feedback correction. The PID controller computes: ```text error = setpoint - angle ``` and generates a correction output using: ```text U = Kp·e + Ki·∑e + Kd·(de/dt) ``` The output determines motor direction and PWM speed. --- ## PID Values ```cpp kp = 30; ki = 0; kd = 6; ``` --- ## Challenges Faced * PID tuning instability * Motor response inconsistencies * Sensor noise and drift * One-sided balancing behavior * Calibration issues
ML Model
71
Deepfake Image Detection Using Artificial Intelligence
# Deepfake Image Detection Using Artificial Intelligence ## What I Built The speed at which AI can generate photorealistic imagery has massively outpaced the human eye's ability to detect it. This project is an automated **AI-Generated Image Detector**, a binary classification system that takes any image and predicts whether it is a real photograph or machine-generated. More importantly, it doesn't just give a prediction; it explains its reasoning. By integrating **Grad-CAM** (Gradient-weighted Class Activation Mapping), the application generates a heatmap over the uploaded image, highlighting the exact pixels and artifacts the neural network focused on to make its decision. ## Tech Stack & Architecture * **Backbone:** EfficientNet-B0 (Pre-trained on ImageNet for transfer learning) * **Framework:** PyTorch * **Explainability:** Custom Grad-CAM implementation using PyTorch hooks * **Deployment:** Streamlit, Docker, Hugging Face Spaces EfficientNet-B0 was chosen for its compound scaling approach, which provides state-of-the-art accuracy with a highly efficient parameter count (~5.3 million), making it lightweight enough to run inference smoothly in a browser. ## Dataset & Training To ensure the model learns generalized AI artifacts rather than memorizing a specific generator, I curated a dataset of over **45,000 images** from more than 10 different sources: * **FAKE (AI):** DiffusionDB, Synthbuster (Stable Diffusion, DALL·E 2, GLIDE), StyleGAN Faces. * **REAL:** RAISE 4K, Flickr, Imagenette, and the **DPED dataset** (smartphone photos from iPhone/Sony to fix domain shift biases). Crucially, modern generators like **DALL·E 3 and Midjourney v5 were withheld entirely from training** and only used in the test set to evaluate true zero-shot generalization. ## Code Example: Grad-CAM Explainability Instead of using external libraries that can break in headless deployment environments, I implemented Grad-CAM using pure PyTorch forward and backward hooks: ```python import torch import torch.nn.functional as F def generate_gradcam_heatmap(model, input_tensor, target_class): feature_maps, gradients = [], [] target_layer = model.features[-1] # Register hooks to capture activations and gradients handle_f = target_layer.register_forward_hook(lambda m, i, o: feature_maps.append(o)) handle_b = target_layer.register_full_backward_hook(lambda m, gi, go: gradients.append(go[0])) output = model(input_tensor) model.zero_grad() output[0, target_class].backward() handle_f.remove() handle_b.remove() # Global average pooling and heatmap generation weights = torch.mean(gradients[0], dim=(2, 3), keepdim=True) cam = F.relu(torch.sum(weights * feature_maps[0], dim=1).squeeze()) cam = (cam - cam.min()) / (cam.max() - cam.min() + 1e-8) return cam.detach().cpu().numpy()
Web App
59
GreenDoc — AI-Powered Crop Disease Detection for African Farmers
# GreenDoc — AI-Powered Crop Disease Detection ## What I Built GreenDoc is a mobile-first web application that lets any farmer take a photo of a diseased crop leaf and receive an instant AI diagnosis — the disease name, severity level, and exact treatment recommendation. ## The Problem Nigeria loses $3.6 billion annually to agricultural losses. Crop diseases go undetected until it's too late. The nearest agricultural extension officer is 40km away. ## Tech Stack - **Model:** EfficientNetB0 with transfer learning (PyTorch) - **Dataset:** 35,000+ images across 19 disease classes - **Backend:** FastAPI deployed on Railway - **Frontend:** React + Tailwind CSS deployed on Vercel - **Model Format:** ONNX for optimized inference ## Model Performance - Round 1: 95.79% validation accuracy - Round 2: 99.43% validation accuracy, 100% F1 score on all 19 classes ## Code Example ```python model = models.efficientnet_b0(weights="IMAGENET1K_V1") for param in model.parameters(): param.requires_grad = False model.classifier[1] = nn.Linear( model.classifier[1].in_features, 19 ) ``` ## Results - 19 crop disease classes detected - 99.43% validation accuracy after fine-tuning - Live on mobile at https://cropdoc-frontend-ldld.vercel.app ## Challenges - Domain gap between lab training images and real farm photos - ONNX model size limitations on free hosting - Confident misclassifications on Mosaic Virus and Septoria ## What's Next - Retrain with real field condition images - Google login and Paystack payments - Android APK for full offline use
Robotics
118
Motion Gestured Controlled Car
Motion Gestured Controlled Car What I Built I built a wearable gesture-control system that lets you pilot a robotic vehicle using natural hand movements. It uses an inertial sensor on a glove to track the pitch and roll of my hand. It then transmits that data instantly to the vehicle using a peer-to-peer wireless protocol. It's designed to be highly responsive and includes built-in safety features to stop the car in case you loose control while moving it Code Example typedef struct { int forwardTilt; int sideTilt; } HandData; HandData myData; uint8_t carAddress[] = {0x98, 0xF4, 0xAB, 0xD7, 0xAC, 0xFE}; Results I successfully engineered a fully custom, low-latency robotic car controlled entirely through hand gestures on a wearable glove. I eliminated network lag by implementing the ESP-NOW protocol for direct, routerless communication, creating a near-instant link between my movements and the vehicle's response. Using an MPU6050 IMU sensor, the system accurately maps raw gravitational data into precise pitch and roll angles to seamlessly dictate steering and speed. To ensure physical stability and operational safety, I applied software-level differential trimming to calibrate the motors alongside a non-blocking watchdog timer that forces an emergency stop if the wireless connection drops.
Robotics
245
Kinematic Snake-like Robot for pipeline inspection.
# Kinematic Snake-like Robot for Pipeline Inspection This project explores how to create realistic snake-like locomotion using servo motors and mathematical sine waves. Instead of manually controlling every joint, the robot generates a travelling wave through its body. This wave propagation produces smooth slithering motion similar to a biological snake. Articulated snake robots are one of the most interesting forms of bio-inspired robotics. Unlike wheeled robots, snake robots generate movement through coordinated wave propagation along a segmented body. This project demonstrates a simple Arduino implementation of snake locomotion using multiple servo motors and sinusoidal motion control. The motion algorithm below creates a travelling wave across a chain of servos, producing a slithering effect similar to biological snakes. The snake’s movement is generated using a sine wave. Each servo receives a slightly phase-shifted version of the same wave, creating the illusion of a wave travelling through the body. --- ## Mathematical Model Mathematically, the servo angle can be represented as: θ=90+offset+Asin(ωt+ϕ) Where: A = wave amplitude ω = wave speed t = time φ = phase shift between segments --- ## Arduino Implementation ```cpp // slither snake void slither(int offset, int Amplitude, int Speed, float Wavelengths) { MaxAngleDisplacement = abs(offset) + abs(Amplitude); while (MaxAngleDisplacement > 90) { Amplitude = abs(Amplitude) - 1; MaxAngleDisplacement = abs(offset) + Amplitude; } for (int i = 0; i < 360; i++) { rads = i * pi / 180.0; for (int j = 0; j < 10; j++) { myServos[j].write( 90 + offset + Amplitude * sin( Speed * rads + j * Wavelengths * Shift ) ); } delay(10); // Check for MQTT updates during motion client.loop(); // Stop motion immediately if disabled if (!isSlithering) return; } } ``` --- ## How the Algorithm Works The robot generates motion by propagating a sine wave through a chain of servo motors. Each servo receives: - the same base wave - but with a slight phase offset This phase difference creates a travelling wave across the body, producing smooth serpentine locomotion. Without the phase shift, all servos would move simultaneously and the robot would simply oscillate in place rather than move forward. --- ## Engineering Challenges Although the algorithm successfully generates snake-like motion, real-world locomotion depends heavily on mechanical and electrical design. Important considerations include: - servo synchronization - power stability - directional friction - joint alignment - weight distribution Multiple high-torque servos can generate large current spikes, making stable power delivery critical for reliable operation. --- ## Conclusion This sine-wave locomotion algorithm demonstrates a simple but effective method for generating articulated snake motion using an Arduino and servo motors. By introducing phase-shifted sinusoidal control across multiple joints, the robot produces smooth serpentine movement with relatively little code. The project demonstrates how mathematical wave propagation can be applied to bio-inspired robotics systems for applications such as: - pipeline inspection - confined-space exploration - autonomous inspection systems
2 Wheel Self-Balancing Car with ESP Cam for detection
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Self-Balancing Robot Project</title> <style> body { margin: 0; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #0f172a; color: #e2e8f0; } .container { max-width: 900px; margin: auto; padding: 40px 20px; } .card { background: #1e293b; border-radius: 16px; padding: 30px; box-shadow: 0 10px 30px rgba(0,0,0,0.4); } h1 { font-size: 2.5rem; margin-bottom: 10px; color: #38bdf8; } h2 { color: #94a3b8; margin-bottom: 20px; font-weight: 400; } p { line-height: 1.7; font-size: 1.05rem; color: #cbd5f5; } .highlight { color: #38bdf8; font-weight: 600; } .footer { margin-top: 30px; text-align: center; font-size: 0.9rem; color: #64748b; } .badge { display: inline-block; padding: 6px 12px; margin: 5px 5px 0 0; background: #334155; border-radius: 999px; font-size: 0.8rem; } </style> </head> <body> <div class="container"> <div class="card"> <h1>Two-Wheel Self-Balancing Robot</h1> <h2>With ESP Camera-Based Object Detection</h2> <p> This project presents a <span class="highlight">two-wheel self-balancing robotic system</span> designed using advanced control techniques and real-time sensor feedback. </p> <p> The robot maintains stability using a <span class="highlight">PID (Proportional-Integral-Derivative) control system</span>. Orientation data is collected through the <span class="highlight">MPU6050 gyroscope and accelerometer module</span>, which continuously measures the tilt angle of the system. </p> <p> The measured tilt is processed through the PID loop, which dynamically adjusts motor speed using <span class="highlight">PWM (Pulse Width Modulation)</span>. This ensures precise corrections, allowing the robot to remain balanced in real time. </p> <p> To enhance environmental interaction, an <span class="highlight">ESP Camera module</span> is integrated for <span class="highlight">color and object detection</span>. This enables the robot to respond intelligently to its surroundings while maintaining stability. </p> <p> Overall, this system combines <span class="highlight">control theory, embedded systems, and computer vision</span> to create a responsive and autonomous balancing robot. </p> <div> <span class="badge">PID Control</span> <span class="badge">MPU6050</span> <span class="badge">ESP-CAM</span> <span class="badge">Computer Vision</span> <span class="badge">PWM Motor Control</span> </div> <div class="footer"> Project by Jubril Badmos </div> </div> </div> </body> </html>
2 Wheel Self-Balancing Car with ESP Cam for detection
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Self-Balancing Robot Project</title> <style> body { margin: 0; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #0f172a; color: #e2e8f0; } .container { max-width: 900px; margin: auto; padding: 40px 20px; } .card { background: #1e293b; border-radius: 16px; padding: 30px; box-shadow: 0 10px 30px rgba(0,0,0,0.4); } h1 { font-size: 2.5rem; margin-bottom: 10px; color: #38bdf8; } h2 { color: #94a3b8; margin-bottom: 20px; font-weight: 400; } p { line-height: 1.7; font-size: 1.05rem; color: #cbd5f5; } .highlight { color: #38bdf8; font-weight: 600; } .footer { margin-top: 30px; text-align: center; font-size: 0.9rem; color: #64748b; } .badge { display: inline-block; padding: 6px 12px; margin: 5px 5px 0 0; background: #334155; border-radius: 999px; font-size: 0.8rem; } </style> </head> <body> <div class="container"> <div class="card"> <h1>Two-Wheel Self-Balancing Robot</h1> <h2>With ESP Camera-Based Object Detection</h2> <p> This project presents a <span class="highlight">two-wheel self-balancing robotic system</span> designed using advanced control techniques and real-time sensor feedback. </p> <p> The robot maintains stability using a <span class="highlight">PID (Proportional-Integral-Derivative) control system</span>. Orientation data is collected through the <span class="highlight">MPU6050 gyroscope and accelerometer module</span>, which continuously measures the tilt angle of the system. </p> <p> The measured tilt is processed through the PID loop, which dynamically adjusts motor speed using <span class="highlight">PWM (Pulse Width Modulation)</span>. This ensures precise corrections, allowing the robot to remain balanced in real time. </p> <p> To enhance environmental interaction, an <span class="highlight">ESP Camera module</span> is integrated for <span class="highlight">color and object detection</span>. This enables the robot to respond intelligently to its surroundings while maintaining stability. </p> <p> Overall, this system combines <span class="highlight">control theory, embedded systems, and computer vision</span> to create a responsive and autonomous balancing robot. </p> <div> <span class="badge">PID Control</span> <span class="badge">MPU6050</span> <span class="badge">ESP-CAM</span> <span class="badge">Computer Vision</span> <span class="badge">PWM Motor Control</span> </div> <div class="footer"> Project by Jubril Badmos </div> </div> </div> </body> </html>
Motion Gestured Controlled Car
Motion Gesture Controlled Car I built a wearable gesture-control system that lets you pilot a robotic vehicle using natural hand movements. It uses an inertial sensor on a glove to track the pitch and roll of my hand. It then transmits that data instantly to the vehicle using a low-latency, peer-to-peer wireless protocol. It's designed to be highly responsive and includes built-in safety features to prevent the car from running away if it loses connection.For this project I made use of a Robot Chasis Car which I assembled myself Components I made use of ESP8266 Microcontrollers MPU6050 L298N Motor Driver Geared DC Motors Part of my code typedef struct { int forwardTilt; int sideTilt; } HandData; HandData myData; Results I successfully engineered a fully custom, low-latency robotic car controlled entirely through hand gestures on a wearable glove. I eliminated network lag by implementing the ESP-NOW protocol for direct, routerless communication, creating a near-instant link between my movements and the vehicle's response. Using an MPU6050 IMU sensor, the system accurately maps raw gravitational data into precise pitch and roll angles to seamlessly dictate steering and speed. To ensure physical stability and operational safety, I applied software-level differential trimming to calibrate the motors alongside a non-blocking watchdog timer that forces an emergency stop if the wireless connection drops.
Robotics
41
Smart Security Box: A Cloud-Connected IoT Access Control System
# Smart Security Box ## What I Built A cloud-connected IoT security system built around the ESP32 microcontroller. It features a physical keypad and LCD display for PIN entry, a solenoid lock for access control, and a live web dashboard for remote monitoring and management — all backed by a Flask API deployed on Render.com and a Supabase (PostgreSQL) cloud database. ## Why It Matters Traditional safes are offline and offer zero visibility. Smart alternatives are expensive and closed-source. This project bridges that gap — providing a **fully open, globally accessible** security system with real-time audit logs, dynamic OTPs, and an admin dashboard you can access from anywhere. ## How It Works The system runs on three layers: 1. **The Edge (Hardware)** — ESP32 reads keypad input and sends a secure HTTPS request to the cloud. Falls back to a Master PIN if offline. 2. **The Brain (Cloud API)** — Flask API verifies PINs against the database, manages OTPs, and tracks all access events. 3. **The Control Room (Dashboard)** — A glassmorphism web UI where admins log in to view live access charts, generate OTPs, and manage devices. ## Problems Encountered & Solutions | Problem | Solution | |---|---| | **Keypad Ghost Inputs**: GPIO conflicts caused missed or repeated key presses | Rewired Row 2: GPIO 2 → GPIO 27 and Col 2: GPIO 5 → GPIO 12 to eliminate hardware interference | | **Server Connectivity Failure**: ESP32 hung indefinitely when Wi-Fi dropped | Implemented a Heartbeat system + Offline Master PIN fallback | | **Complex Cloud Deployment**: Manual Python environment setup was error-prone | Wrote a `render.yaml` Infrastructure-as-Code file to fully automate the build | | **UX Friction**: Email login felt impersonal | Refactored entire auth system to use custom Usernames | ## Key Features - Dynamic OTP generation with expiry - Real-time access log with Chart.js trend visualization - JWT-secured admin dashboard - Offline fallback mode for uninterrupted access - Full audit trail of every PIN attempt (success or failure) ## What I Used and Why - **ESP32** — Built-in Wi-Fi, dual-core, handles real-time input without lag - **Flask (Python)** — Lightweight, fast API with minimal overhead - **Supabase (PostgreSQL)** — Scalable, secure, real-time cloud database - **JWT** — Stateless, secure admin session management - **Vanilla CSS (Glassmorphism)** — Custom premium aesthetic, no bloated frameworks - **Render.com** — Zero-config cloud deployment with free HTTPS ## Milestones 1. Hardware assembled and tested (Keypad + LCD + ESP32 + Lock) 2. ESP32 communicating with local Flask server 3. Data migrated to Supabase cloud database 4. Full web admin dashboard built with live charts 5. Deployed live on Render.com with global HTTPS access ## The Road Ahead - **Biometric Auth** — Fingerprint scanner for triple-factor security - **Mobile Alerts** — Telegram/WhatsApp push notifications on failed attempts - **Battery Mode** — ESP32 Deep Sleep for portable, long-term deployment - **Multi-Device** — Manage multiple boxes from one dashboard - **AI Threat Detection** — Flag unusual access patterns automatically ```javascript // Live Heartbeat Simulator const deviceId = "DEVICE_001"; let logs = []; function simulateHeartbeat() { const timestamp = new Date().toLocaleTimeString(); const status = Math.random() > 0.1 ? "✅ ONLINE" : "⚠️ WARNING"; logs.unshift(`[${timestamp}] ${deviceId}: ${status}`); if (logs.length > 5) logs.pop(); console.clear(); console.log("=== SMART SECURITY BOX MONITOR ==="); logs.forEach(log => console.log(log)); } setInterval(simulateHeartbeat, 3000); simulateHeartbeat();
Robotics
104
MediBot: A Continuous AI-Powered Patient Monitoring for Hospitals.
# MediBot: A Continuous AI-Powered Patient Monitoring for Hospitals. ## The Problem In many Nigerian hospital wards, one nurse monitors between 10 and 20 patients at the same time. That is not negligence. That is the reality of what the healthcare system has to work with. The consequence is a monitoring gap that no amount of dedication can close by hand. Oxygen saturation can fall from a healthy level to a life-threatening one in under two minutes. A patient losing consciousness may go unnoticed for 20 minutes or more if the nurse is attending to another emergency. Pain in sedated or non-verbal patients often goes completely undetected because there is simply no continuous system watching closely enough. The standard solution to this problem costs millions of naira per ward. ICU-grade monitors are expensive, difficult to maintain, and built around assumptions that do not match the realities of many Nigerian hospitals. MediBot was built around those realities from the beginning. The goal was never to build another expensive hospital machine. The goal was to build a system that could realistically exist in Nigerian hospitals using affordable hardware, open-source software, and infrastructure that hospitals already have access to. --- ## What MediBot Does MediBot continuously monitors patients without requiring a nurse to remain physically at the bedside. The system combines: - Contactless temperature monitoring - Heart rate monitoring - Blood oxygen saturation monitoring - AI-powered facial analysis - Pain detection - Eye-state analysis - Real-time patient state classification - Remote dashboard monitoring to determine what condition the patient is likely in at any given moment. When everything is stable, the system stays quiet and keeps watching. When something becomes dangerous, it alerts medical staff immediately. The purpose is not to replace nurses or doctors. The purpose is to help them see more patients safely with the limited manpower already available. --- ## Why Contactless Monitoring Matters One of the biggest focuses of MediBot is contactless monitoring. Many hospital systems only react when a patient manually reports distress or when a nurse physically checks them. That creates long periods where deterioration can happen unnoticed. MediBot reduces that gap by continuously observing the patient using both sensors and AI vision. The MLX90614 infrared sensor measures temperature without touching the patient. The camera system continuously watches facial expressions, eye state, and movement patterns. The AI models estimate pain levels and determine whether the patient appears awake, asleep, distressed, or unconscious. The only contact-based sensor currently in the system is the MAX30102 pulse oximeter used for heart rate and blood oxygen saturation monitoring. That decision was not because a contactless approach was preferred less. It was because a reliable and affordable contactless alternative that could realistically be sourced locally in Nigeria was not available during development. Rather than delay the entire system waiting for hardware that may be difficult or expensive to obtain locally, the project prioritised building a working monitoring pipeline around components that could actually be sourced and deployed within the Nigerian ecosystem. Everything else in the monitoring pipeline was intentionally designed to minimise physical interaction with the patient as much as possible. This matters especially in situations where patients: - Cannot communicate clearly - Are sedated - Are unconscious - Are post-operative - Are critically ill - Are being monitored remotely The system keeps watching even when nobody else can. --- ## Innovation Built Around Nigerian Realities Most medical monitoring systems are designed for hospitals with stable electricity, strong internet infrastructure, large budgets, and dedicated biomedical engineering teams. MediBot was designed around the opposite assumptions. That constraint shaped every engineering decision in the system. --- ## Low-Cost Embedded AI The entire system runs on ESP32 hardware costing only a fraction of traditional hospital monitoring equipment. Instead of requiring expensive GPUs or cloud servers, MediBot pushes intelligence directly onto embedded devices. The ESP32 handles: - Sensor readings - Patient-state logic - Alert management - Communication with the dashboard while the vision system handles AI inference separately. This two-board architecture exists because memory is the real constraint. Splitting responsibilities across devices allows the system to remain responsive while still running AI models and live monitoring simultaneously. --- ## Browser-Based AI Vision One of the most important innovations in MediBot is the browser-based AI vision pipeline. Instead of requiring expensive dedicated AI hardware, the current production prototype uses a smartphone camera running `face-api.js` directly inside the browser. That means: - No cloud inference - No GPU server - No installation - No expensive AI workstation A regular smartphone becomes the AI vision system. The browser continuously performs: - Face detection - Expression classification - Eye-state detection - Pain estimation and sends those results directly into the patient-state engine. This is important because smartphones are already widely available in Nigerian hospitals. MediBot turns existing devices into clinical monitoring tools instead of requiring hospitals to buy entirely new infrastructure. ```javascript // Browser-based AI vision pipeline using face-api.js const dets = await faceapi .detectAllFaces( vid, new faceapi.TinyFaceDetectorOptions({ inputSize: 224, scoreThreshold: 0.4 }) ) .withFaceLandmarks(true) .withFaceExpressions(); // Smooth emotion scores across frames Object.keys(phoneExprBuf).forEach(k => { phoneExprBuf[k].push(expr[k] || 0); if (phoneExprBuf[k].length > PHONE_SMOOTH) { phoneExprBuf[k].shift(); } }); // Weighted pain score estimation let pain = 0; Object.keys(PAIN_W).forEach(e => { pain += (smoothed[e] || 0) * PAIN_W[e]; }); pain = Math.round( Math.max(0, Math.min(100, pain * 160)) ); // Eye state detection using Eye Aspect Ratio const avgEAR = (ear(left) + ear(right)) / 2; eyeState = avgEAR < 0.22 ? 'closed' : 'open'; ``` --- ## Designed for Usefulness, Not Demonstration Many prototypes work only in ideal conditions. MediBot was built around usefulness first. The system focuses heavily on: - Fast deployment - Reliability - Low hardware cost - Low bandwidth usage - Minimal maintenance - Real-time usefulness The dashboard works on phones because phones are the most available computing devices in many wards. The alert system runs independently of the dashboard because internet access cannot always be trusted. The vision system continues running locally even during network instability. Every part of the system was designed around making sure it remains clinically useful under imperfect conditions. --- ## Reading the Patient MediBot combines multiple forms of monitoring together because no single signal is reliable enough on its own. The system reads: - Heart rate - Blood oxygen saturation - Temperature - Facial expression - Eye state - Motion patterns and combines them to determine what state the patient is likely in. The patient-state engine classifies conditions such as: - Awake - Sleeping - Distressed - In pain - Unconscious - Critical emergency Emergency logic always runs first. If vitals become critically dangerous, the system immediately escalates regardless of what the camera sees. ```cpp // Emergency classification logic bool hrCritical = hrPresent && (hr < 30 || hr > 150); bool spo2Critical = spo2Present && (spo2 < 85); bool tempCritical = tempPresent && (tempStable < 34.0f || tempStable > 41.0f); if (hrCritical || spo2Critical || tempCritical) { patientState = "unconscious"; distressLevel = "CRITICAL"; painScore = 100; return; } ``` When vitals are stable, the AI vision system helps determine whether the patient appears comfortable, asleep, or in visible distress. --- ## Sensor Detection and Reliability The firmware scans the I2C bus during startup and confirms which sensors are connected before monitoring begins. This makes hardware debugging significantly easier in real deployment environments. ```cpp // Boot-time I2C sensor scan void scanI2CBus() { for (byte addr = 1; addr < 127; addr++) { Wire.beginTransmission(addr); if (Wire.endTransmission() == 0) { Serial.printf("Found 0x%02X", addr); if (addr == 0x57) Serial.print(" <- MAX30102"); if (addr == 0x5A) Serial.print(" <- MLX90614"); Serial.println(); } } } ``` The MAX30102 sensor also performs a raw infrared-value validation before attempting a reading. If no finger is detected, the collection cycle is skipped entirely to keep the system responsive. --- ## The Dashboard The MediBot dashboard is a single HTML application deployed as a Progressive Web App. It works on: - Phones - Tablets - Laptops - Desktop browsers without requiring installation. The dashboard displays: - Heart rate - SpO₂ - Temperature - Pain score - Emotion analysis - Consciousness state - ECG waveform - Alert history - Camera feed in real time. Expression labels are mapped into clinically meaningful categories before entering the patient-state engine. ```javascript // Clinical expression mapping const CLINICAL_EXPR = { pain: { icon: '😣', label: 'Pain', color: '#ef4444' }, disgusted: { icon: '😣', label: 'Pain', color: '#ef4444' }, fearful: { icon: '😣', label: 'Pain', color: '#ef4444' }, angry: { icon: '😠', label: 'Pain', color: '#ef4444' }, happy: { icon: '😊', label: 'Happy', color: '#10b981' }, neutral: { icon: '😐', label: 'Neutral', color: '#4a6480' } }; ``` The interface was designed mobile-first because mobile phones are often the most accessible devices in Nigerian clinical environments. --- ## Project Gallery ### Machine Learning Training  ---  --- ## The Technology Behind It MediBot is built entirely on open tools and affordable hardware. ### Hardware - ESP32 Dev Module - ESP32-CAM - MAX30102 pulse oximeter - MLX90614 infrared temperature sensor - Active buzzer - WiFi communication ### AI and Machine Learning - TensorFlow Lite - Edge Impulse - face-api.js - MobileNetV1 - TinyFaceDetector - FOMO object detection ### Backend - Supabase - PostgreSQL - Real-time subscriptions - Role-based access control ### Frontend - Vanilla JavaScript - HTML - CSS - Progressive Web App deployment --- ## What Makes This Important MediBot is not important because it uses artificial intelligence. It is important because it demonstrates that useful healthcare technology can be built locally around Nigerian realities instead of imported assumptions. The system shows that: - Contactless patient monitoring can be affordable - Embedded AI can run on low-cost hardware - Real-time monitoring does not require expensive infrastructure - Local engineering can solve local healthcare problems The monitoring gap in Nigerian hospitals is not only a funding problem. It is also an engineering problem. MediBot was built as an engineering response to that gap. --- ## Current Development Status | Component | Status | |-----------|--------| | ESP32 vitals firmware | Complete | | Supabase data pipeline | Complete | | Dashboard | Complete | | Phone camera AI monitoring | Complete | | Login system | Complete | | Real-time alert system | Complete | | ESP32-CAM integration | In progress | | Bed occupancy model | In training | | Expression classifier | Dataset collection ongoing | | Offline local mode | Planned | | Clinical validation | Planned | --- ## What Comes Next The immediate roadmap is completing the ESP32-CAM vision pipeline, improving embedded AI models, and beginning clinical validation in real patient environments. Long term, the goal is broader deployment in Nigerian hospitals and further development of locally built robotics and embedded AI systems for healthcare. The talent to build these systems already exists here. What has been missing is infrastructure, research continuity, and systems designed specifically for the realities we actually operate in. MediBot is part of building that foundation. --- > The monitoring gap in Nigerian hospitals cannot be solved by importing more expensive equipment alone. It requires systems designed specifically for the environments where the need is real. MediBot was built around that reality from the beginning.
Smart IOT Security Box
```javascript // Simulating the Security Box Heartbeat Monitor const deviceId = "DEVICE_001"; let logs = []; function simulateHeartbeat() { const timestamp = new Date().toLocaleTimeString(); const status = Math.random() > 0.1 ? "✅ ONLINE" : "⚠️ WARNING (Latency)"; const entry = `[${timestamp}] ${deviceId}: ${status}`; logs.unshift(entry); // Add to the top // Keep only last 5 logs if (logs.length > 5) logs.pop(); console.clear(); console.log("=== SMART SECURITY BOX LIVE MONITOR ==="); logs.forEach(log => console.log(log)); console.log("\nNext check in 3 seconds..."); } // Start the simulation setInterval(simulateHeartbeat, 3000); simulateHeartbeat(); ```
Web App
79
Lliuq: A Serverless AI Platform for Accessible Scholarship and Academic Preparation
# Lliuq: A Serverless AI Platform for Accessible Scholarship and Academic Preparation ## What I Built Lliuq is a browser-based suite of AI-powered productivity tools built for one purpose: helping people put their best work forward when it matters most, without a paywall in the way. I was preparing for a scholarship interview when I hit the third paywall in one night. Every tool that could actually help had a gate in front of it. The help existed. The technology was always there. But what did not exist was access. So I built this and made it free, because the distance between a prepared candidate and an unprepared one should never come down to what they can afford. That gap is not a minor inconvenience. It is the mechanism through which opportunity reproduces itself in the hands of people who already have it. Lliuq is a direct technical response to that mechanism. Every architectural decision, from the serverless infrastructure to the zero-cost model to the privacy-first data handling, was made with one constraint in mind: the student in Lagos or Accra or Nairobi preparing for a scholarship interview at midnight should have access to the same quality of preparation tools as anyone else, anywhere in the world. That is SDG 4 and SDG 10, not as stated goals but as design requirements written into the first line of code. The platform currently has three live AI tools with a fourth under development: 1. **Scribe** records audio and produces structured, formatted documents using Whisper and Llama. Not raw text but proper meeting minutes, lecture notes, sermon summaries, and song transcriptions, each with auto-generated metadata including tone classification, estimated reading time, and transcription confidence scoring, so that no key point is lost, action items are clearly noted, and the people in the room can be present in the conversation instead of racing to write it down 2. **Interview** is a scholarship interview simulator. It generates contextual questions across ten distinct categories, scores spoken answers on content depth and grammar, detects filler words in real-time, measures words per minute, and returns model answers alongside three concrete improvement tips. The entire evaluation is returned as structured JSON from Llama and parsed directly in the browser, allowing candidates to identify exactly where they lost points, what they said that weakened their answer, and what a stronger response would have sounded like. 3. **Prose** is a statement of purpose builder with pre-listed support for over 100 universities worldwide, including institutions across Africa, Europe, Asia, and North America, and for any university or scholarship not on the list, the user can type it in manually. It uses Llama to generate and refine application writing tailored to the specific university and programme the user is applying to, because writing with clarity in your first language and writing the kind of statement of purpose that Cambridge or Harvard expects are two entirely different skills, and the gap between them should not be the reason an application fails. Everything runs entirely in the browser. No account needed. Nothing is stored on any server. The user connects their own free Groq API key, and the platform is ready to use. The platform never touches your key. ## Code Examples The platform has no backend. Every AI call goes directly from the browser to Groq over HTTPS. ```javascript const response = await fetch("https://api.groq.com/openai/v1/chat/completions", { method: "POST", headers: { "Authorization": `Bearer ${apiKey}`, "Content-Type": "application/json" }, body: JSON.stringify({ model: "llama-3.3-70b-versatile", messages: conversationHistory, response_format: { type: "json_object" } }) }); ``` For the Interview module, audio is captured via the browser MediaRecorder API and sent to Groq's Whisper endpoint for transcription before evaluation: ```javascript const mime = MediaRecorder.isTypeSupported("audio/webm;codecs=opus") ? "audio/webm;codecs=opus" : "audio/webm"; const recorder = new MediaRecorder(stream, { mimeType: mime }); recorder.onstop = async () => { const blob = new Blob(allChunks, { type: mime }); const fd = new FormData(); fd.append("file", blob, "audio.webm"); fd.append("model", "whisper-large-v3"); const res = await fetch("https://api.groq.com/openai/v1/audio/transcriptions", { method: "POST", headers: { "Authorization": `Bearer ${apiKey}` }, body: fd }); }; ``` The Interview engine enforces non-repetitive questioning through a structured system prompt. Llama tracks which of the ten question categories it has already used and is forbidden from repeating one within a session: ```javascript const SYSTEM_PROMPT = ` QUESTION VARIETY RULES: Categories: [opening/motivation, academic achievement, leadership, community/social impact, adversity/resilience, future vision, values/ethics, scholarship fit, critical thinking, personal character] Each question MUST come from a DIFFERENT category than the previous one. Never ask two questions from the same category in one session. Make questions specific to the scholarship name, university, and the field of study provided. `; ``` Scribe uses a switchable system prompt architecture. The format type the user selects determines which AI persona and output structure loads. A meeting prompt loads a professional secretary persona that produces structured minutes. A lecture prompt loads a Prodigy student persona that generates hierarchical notes with cross-disciplinary connections. The same raw audio produces structurally different documents depending entirely on which system prompt is active. # Result 1. Lliuq is live at [lliuq.netlify.app](https://lliuq.netlify.app) with no account required. Three functioning tools are now available. 2. The platform proves that a useful, privacy-respecting AI product does not always need a backend, a subscription model, or a company behind it. By routing all inference through the user's own Groq API key and persisting state entirely in localStorage, Lliuq eliminates infrastructure costs at the platform level and passes that zero cost directly to the user. This is what responsible AI for Good looks like when the architecture itself is the commitment. 3. The Interview module delivers five-question sessions with real-time speech scoring across content depth, grammar accuracy, confidence, calmness, and vocal pace, all evaluated by Llama and returned as structured JSON. The Scribe engine produces formatted documents from raw audio using Whisper for transcription and Llama for restructuring. The Prose module generates university-specific application writing using contextual prompts built around the institution and programme the applicant is targeting. 4. Users supplying their API key allow them access to all the tools. The API Key itself comes with a generous allocation of free tokens. The community this platform is built for is the student who cannot afford a coaching session, the applicant who found every free tool behind a trial limit, and the candidate who is every bit as qualified as anyone else in the room but has less time and less money to prepare. 5. Since there is no server, account, or database, the user data stays secured on their local browser storage, and clearing it removes everything. ## Conclusion The goal was never to build another AI wrapper. It was to make the kind of preparation that changes outcomes available to anyone who needs it, regardless of what they can pay. And it delivers.
No projects match
Try a different search or filter