Week 14

Interface & Application Programming

Krypto

ASSIGNMENT

Write an application for the embedded board that interfaces a user with an input and/or output device. Document the design process, tools used, and the final working interface.

TOOLS

VS Code · HTML · CSS · JavaScript · ESP32 · MPU-6050 · GitLab Pages

Personal and Group Assignment

This week's personal assignment was to Write an application for the embedded board that you made. that interfaces a user with an input and/or output device(s)

Group Assignment

Here's the link to our group assignment of this week.

The Concept

Krypto Support -- A web app for IVDD monitoring

For this week's assignment I built the interface for my final project: a web application called Krypto Support that monitors the movement and activity of my dog Krypto, who has been diagnosed with Intervertebral Disc Disease (IVDD). The app connects wirelessly via WiFi to an ESP32 microcontroller with an MPU-6050 IMU sensor, reads real-time motion data, and presents it in a dashboard designed for both the dog's owner and the veterinarian treating the condition.

The input device is the MPU-6050 IMU sensor -- it captures acceleration and orientation data from Krypto's body. The embedded board is an ESP32 (and ultimately the XIAO ESP32-C6 in the final wearable), which processes the sensor data and serves it over WiFi. The application is the web dashboard that visualizes the data, detects impacts, tracks activity vs rest time, and generates a daily medical report that can be shared with the vet.

MPU-6050 -> ESP32 reads & processes -> Serves data over WiFi -> Web dashboard visualizes -> PDF report for vet

Why a web app?

A web app built with plain HTML, CSS and JavaScript requires no installation, runs on any device -- iPhone, Android, laptop -- and can be published instantly through GitLab Pages. For a wearable that needs to be accessed by both the dog's owner and a veterinarian in different locations, this is the most practical choice.

The ESP32 acts as a web server: it connects to the local WiFi network and exposes two endpoints -- /datos for the live sensor JSON and /reset to clear the daily counters. The dashboard polls these endpoints every second using the browser's Fetch API, with no backend or cloud service required.

My Process

00. Visual Design -- Canva Template

Before writing any code, I designed the visual identity of the app in Canva. This included defining the color palette, the logo concept, the layout of the main screens, and which metrics I wanted to show. Having a clear reference from the start made the coding process much faster -- I wasn't making design decisions in code, I was translating decisions already made visually.

The identity is built around Krypto's personality: the yellow ring references his characteristic collar, the green background his energy, and the blue is the primary brand color carried through all interactive elements. The font is bold and uppercase to give the app a strong, confident character.

Brand identity and color palette

Brand identity & color palette

The four brand colors defined in Canva: yellow #ffde59, blue #0d31e2, cream #f6f3e2, and green #89c518. Each color has a specific role across the interface.

Screen layout template

Screen layout template

The splash screen and dashboard layout sketched in Canva, showing the logo placement, metric cards, and the LOG IN button pinned to the bottom of the screen.

Color Hex Role in the interface
Yellow #ffde59 Logo ring, impact markers in the chart, accent on buttons
Blue #0d31e2 Primary brand color -- header bar, pitch bar, LOG IN button, section titles
Cream #f6f3e2 App background -- warm, non-clinical feel for a pet health app
Green #89c518 Active state indicator, roll bar, download PDF button

01. Embedded Code -- ESP32 Firmware

The ESP32 firmware handles three tasks: reading the MPU-6050 IMU sensor over I2C, processing the raw data with a complementary filter to compute pitch and roll angles, and serving the processed data as JSON over WiFi. The board exposes two HTTP endpoints: /datos returns the live sensor JSON, and /reset clears the daily counters.

I2C Scanner -- verify MPU-6050 is detected

Upload this sketch first to confirm the sensor is wired correctly. Open Serial Monitor at 115200 baud and press the EN/RST button -- you should see Dispositivo encontrado en 0x68.

// I2C Scanner -- C++
#include <Wire.h>
 
void setup() {
  Serial.begin(115200);
  delay(3000);
  Wire.begin(21, 22); // SDA=21, SCL=22 on ESP32 Dev Module
  Serial.println("Escaneando I2C...");
 
  for (byte i = 8; i < 120; i++) {
    Wire.beginTransmission(i);
    if (Wire.endTransmission() == 0) {
      Serial.print("Dispositivo encontrado en 0x");
      Serial.println(i, HEX);
    }
  }
  Serial.println("Listo.");
}
 
void loop() {}
              
Full firmware -- WiFi server + MPU-6050 + complementary filter

This is the complete firmware uploaded to the ESP32. It initializes the MPU-6050 over I2C, connects to WiFi, and serves sensor data as JSON. The complementary filter combines accelerometer and gyroscope readings for stable pitch/roll angles. After uploading, open Serial Monitor at 115200 baud -- the board prints its IP address once connected to WiFi.

// Full firmware -- ESP32 + MPU-6050 + WiFi server
// Libraries
#include <Wire.h>        // I2C communication with MPU-6050
#include <MPU6050.h>     // MPU-6050 sensor library
#include <WiFi.h>       // WiFi connection
#include <WebServer.h>  // HTTP server to serve JSON data
 
// WiFi credentials -- replace with your network
const char* ssid     = "YOUR_WIFI";
const char* password = "YOUR_PASSWORD";
 
// Sensor and server objects
MPU6050 mpu(0x68);       // MPU-6050 at I2C address 0x68
WebServer server(80);    // HTTP server on port 80
 
// ---- Processed sensor data ----
float pitch = 0, roll = 0;   // Filtered angles (degrees)
float accelMag = 0;           // Acceleration magnitude (g)
unsigned long tiempoAnterior = 0;
 
// ---- Daily tracking counters ----
int   impactos       = 0;    // Number of impacts above threshold
long  segundosActivo = 0;    // Active time (seconds)
long  segundosReposo = 0;    // Rest time (seconds)
float pitchPromedio  = 0;    // Running average posture angle
int   muestrasTotal  = 0;
bool  estaActivo     = false;
 
// ---- Thresholds ----
const float UMBRAL_IMPACTO = 2.5;  // Impact detection (g)
const float UMBRAL_ACTIVO  = 0.3;  // Movement detection (g)
 
void setup() {
  Serial.begin(115200);
  Wire.begin(21, 22);   // I2C pins: SDA=21, SCL=22
  mpu.initialize();       // Wake up the MPU-6050
 
  // Connect to WiFi
  WiFi.begin(ssid, password);
  Serial.print("Conectando WiFi");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500); Serial.print(".");
  }
  Serial.println("\nIP: " + WiFi.localIP().toString());
 
  // ---- Endpoint: /datos ----
  // Returns all sensor data as JSON
  server.on("/datos", HTTP_GET, []() {
    String json = "{";
    json += "\"pitch\":"      + String(pitch, 1)         + ",";
    json += "\"roll\":"       + String(roll, 1)          + ",";
    json += "\"accel\":"      + String(accelMag, 2)      + ",";
    json += "\"impactos\":"   + String(impactos)          + ",";
    json += "\"activo\":"     + String(segundosActivo)    + ",";
    json += "\"reposo\":"     + String(segundosReposo)    + ",";
    json += "\"posturaAvg\":" + String(pitchPromedio, 1)  + ",";
    json += "\"estaActivo\":" + String(estaActivo ? "true" : "false");
    json += "}";
    server.sendHeader("Access-Control-Allow-Origin", "*");
    server.send(200, "application/json", json);
  });
 
  // ---- Endpoint: /reset ----
  // Resets all daily counters to zero
  server.on("/reset", HTTP_GET, []() {
    impactos = segundosActivo = segundosReposo = muestrasTotal = 0;
    pitchPromedio = 0;
    server.sendHeader("Access-Control-Allow-Origin", "*");
    server.send(200, "text/plain", "OK");
  });
 
  server.begin();
  tiempoAnterior = millis();
}
 
void loop() {
  server.handleClient();
 
  // ---- Read raw sensor data ----
  int16_t ax, ay, az, gx, gy, gz;
  mpu.getMotion6(&ax, &ay, &az, &gx, &gy, &gz);
 
  unsigned long ahora = millis();
  float dt = (ahora - tiempoAnterior) / 1000.0;
  tiempoAnterior = ahora;
 
  // ---- Complementary filter ----
  // Combines accelerometer (stable, noisy) with
  // gyroscope (smooth, drifts) for accurate angles
  float ap = atan2(ay, az) * 180.0 / PI;
  float ar = atan2(-ax, az) * 180.0 / PI;
  pitch = 0.98 * (pitch + (gx / 131.0) * dt) + 0.02 * ap;
  roll  = 0.98 * (roll  + (gy / 131.0) * dt) + 0.02 * ar;
 
  // ---- Acceleration magnitude in g ----
  accelMag = sqrt(
    pow(ax / 16384.0, 2) +
    pow(ay / 16384.0, 2) +
    pow(az / 16384.0, 2)
  );
 
  // ---- Impact detection ----
  if (accelMag > UMBRAL_IMPACTO) impactos++;
 
  // ---- Activity tracking ----
  float mov = abs(accelMag - 1.0);
  estaActivo = mov > UMBRAL_ACTIVO;
  if (estaActivo) segundosActivo++;
  else            segundosReposo++;
 
  // ---- Running average posture ----
  muestrasTotal++;
  pitchPromedio += (pitch - pitchPromedio) / muestrasTotal;
 
  delay(100);
}
              
# Serial Monitor output after uploading the I2C scanner:
-> Escaneando I2C...
-> Dispositivo encontrado en 0x68
-> Listo.
# Serial Monitor output after uploading the full firmware:
-> Conectando WiFi......
-> IP: 192.168.1.45

02. Web App -- Dashboard Code

The complete web application lives in a single index.html file containing HTML, CSS and JavaScript. I built it in VS Code with the Live Server extension for real-time preview. The app has four screens managed by a show/hide system, and the Krypto logo is embedded as base64 so the file is fully self-contained.

Splash screen
Login / Register
Live dashboard
PDF report generator
localStorage session
PWA installable
VS Code with Live Server

VS Code with Live Server

The index.html open in VS Code with the Live Server preview running in Chrome. Changes to the code reflect immediately in the browser without manual refresh.

Splash screen

Splash screen

The opening screen with the Krypto logo, app name in bold uppercase blue, and the LOG IN button anchored to the bottom of the screen -- matching the Canva template exactly.

HTML structure -- screen navigation

The app is organized into three screens. Only one is visible at a time. The goTo(id) function handles all transitions.

// HTML
<!-- Screen 1: Splash -->
<div id="splash" class="screen active">
  <img class="logo-img" src="data:image/png;base64,..."/>
  <div class="app-name">Krypto<br>Support</div>
  <button onclick="goTo('loginreg')">LOG IN</button>
</div>
 
<!-- Screen 2: Login / Register -->
<div id="loginreg" class="screen">
  <!-- Two tabs: login + registration -->
  <!-- Registration collects: name, email, password -->
  <!-- Pet profile: name, breed, age, weight, photo -->
</div>
 
<!-- Screen 3: Dashboard -->
<div id="dash" class="screen">
  <!-- Blue header with dog name and connection status -->
  <!-- IP input field to connect to ESP32 -->
  <!-- 4 metric cards: impacts, active, rest, posture -->
  <!-- Orientation bars: pitch, roll, acceleration -->
  <!-- 60s acceleration chart (Canvas API) -->
  <!-- Actions: reset day + generate PDF report -->
</div>
JavaScript -- fetching ESP32 data and updating UI

The dashboard polls the ESP32 every second using the Fetch API and updates all UI elements -- metric cards, orientation bars, and the acceleration chart -- without reloading the page.

// JS
// Poll ESP32 every second and update the UI
async function obtener() {
  try {
    const r = await fetch(`http://${IP}/datos`);
    const d = await r.json();
 
    // Update metric cards
    document.getElementById('impactos').textContent = d.impactos;
    document.getElementById('activo').textContent   = Math.floor(d.activo / 60);
    document.getElementById('reposo').textContent   = Math.floor(d.reposo / 60);
    document.getElementById('postura').textContent  = parseFloat(d.posturaAvg).toFixed(1);
 
    // Update orientation bars (pitch, roll, accel)
    barra('bPitch', 'vPitch', d.pitch, -90, 90, 'deg');
    barra('bRoll',  'vRoll',  d.roll,  -90, 90, 'deg');
    barra('bAccel', 'vAccel', d.accel,   0,  4, ' g');
 
    // Push to acceleration history chart
    hist.push(parseFloat(d.accel));
    if (hist.length > MAX) hist.shift();
    dibujar();
  } catch(e) {
    document.getElementById('connStatus').textContent = 'No connection';
  }
}
setInterval(obtener, 1000);
JavaScript -- acceleration chart with Canvas API

The acceleration history chart is drawn frame by frame using the HTML Canvas API. Impact events above 2.5g are highlighted as yellow dots with a blue outline.

// JS
function dibujar() {
  if (!canvas) return;
  canvas.width = canvas.offsetWidth;
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  const w = canvas.width / MAX;
 
  // Reference line at 1g (stationary)
  const y1 = canvas.height - (1 / 4) * canvas.height;
  ctx.strokeStyle = '#e0dcc8'; ctx.setLineDash([4, 4]);
  ctx.beginPath(); ctx.moveTo(0, y1); ctx.lineTo(canvas.width, y1); ctx.stroke();
  ctx.setLineDash([]);
 
  // Draw acceleration line (blue)
  ctx.strokeStyle = '#0d31e2'; ctx.lineWidth = 2;
  ctx.beginPath();
  hist.forEach((v, i) => {
    const x = i * w;
    const y = canvas.height - (Math.min(v, 4) / 4) * canvas.height;
    i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
  });
  ctx.stroke();
 
  // Impact dots -- yellow fill, blue outline
  hist.forEach((v, i) => {
    if (v > 2.5) {
      ctx.fillStyle = '#ffde59';
      ctx.beginPath();
      ctx.arc(i * w, canvas.height - (Math.min(v, 4) / 4) * canvas.height, 4, 0, Math.PI * 2);
      ctx.fill();
      ctx.strokeStyle = '#0d31e2'; ctx.stroke();
    }
  });
}
JavaScript -- PDF report with jsPDF

The PDF is generated entirely client-side using the jsPDF library. It includes the pet profile pulled from localStorage, the day's metrics, and a color-coded clinical analysis section.

// JS
function descargarPDF() {
  const { jsPDF } = window.jspdf;
  const doc = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' });
  const user = JSON.parse(localStorage.getItem('ks-user') || '{}');
 
  // Blue header
  doc.setFillColor(13, 49, 226);
  doc.rect(0, 0, 210, 40, 'F');
  doc.setTextColor(255, 255, 255);
  doc.setFontSize(22); doc.setFont('helvetica', 'bold');
  doc.text('KRYPTO SUPPORT', 18, 17);
 
  // Pet profile card
  doc.setFillColor(246, 243, 226);
  doc.roundedRect(18, 48, 174, 24, 3, 3, 'F');
  doc.setTextColor(13, 49, 226);
  doc.text(user.dogname || 'Krypto', 22, 62);
 
  // Save as dated file
  doc.save('Krypto_Reporte_' + new Date().toISOString().slice(0, 10) + '.pdf');
}

App features

The finished app has four screens: a splash screen, a login/registration screen where the owner enters their details and the pet's profile (name, breed, age, weight, and photo), the main dashboard with live metrics, and a PDF report generator. User data is saved in localStorage so the app remembers the session and skips directly to the dashboard on subsequent visits.

The PDF report is generated client-side using jsPDF -- no server needed. It includes the pet profile, daily metrics, a clinical analysis section with color-coded alerts, and a footer with the Fab Academy project credit.

03. The Interface

The dashboard is designed for mobile-first use -- the dog's owner checks it from their phone while Krypto is wearing the sensor. The layout follows the brand identity defined in Canva: cream background, blue header bar, yellow accents, and bold uppercase typography.

Login and registration screen

Login / Registration

Two-tab screen: existing users log in with email and password, new users register their profile and their pet's details including a photo.

Live dashboard

Live dashboard

Four metric cards showing impacts, active time, rest time, and average posture angle. Orientation bars update in real time from the IMU data.

PDF report

PDF report

One-tap report generation with the pet profile, daily metrics, and a clinical analysis section with automatic alerts for elevated impacts or abnormal posture.

Metric Source Clinical relevance for IVDD
Impacts (>2.5g) Accelerometer magnitude spike High-impact events stress the intervertebral discs directly
Active time Movement threshold on accel mag Tracks whether the dog is respecting prescribed rest periods
Rest time Near-zero acceleration Confirms recovery time after episodes or surgery
Posture angle Pitch average (complementary filter) Sustained abnormal angles may indicate postural compensation due to pain

04. Publishing -- GitLab Pages

Once the interface was complete and tested locally with Live Server, I published it through GitLab Pages so it can be accessed from any device -- not just the development computer. The repository already existed as part of the Fab Academy GitLab instance, so publishing only required adding a pipeline configuration file.

  • 1Pushed index.html to the krypto-support repository on GitLab
  • 2Added a .gitlab-ci.yml pipeline file via the Pipeline Editor
  • 3The pipeline ran automatically, copying the file into a public/ folder and deploying it
  • 4Opened the published URL on iPhone Safari and tested the connection to the ESP32
  • 5Added to iPhone home screen as a PWA -- it installs like a native app with no App Store
// YAML
# .gitlab-ci.yml -- GitLab Pages deploy
pages:
  stage: deploy
  script:
    - mkdir public
    - cp index.html public/index.html
  artifacts:
    paths:
      - public
  only:
    - main
GitLab pipeline passed

GitLab pipeline -- green

The CI/CD pipeline completed successfully. The green checkmark confirms the site was deployed and is live at the GitLab Pages URL.

App connected on iPhone

Connected on iPhone

The app open in Safari on iPhone, connected to the ESP32 on the same WiFi network. The dashboard shows live data from the MPU-6050 sensor.

PWA -- installing as a native app

The app includes the necessary meta tags for iOS to treat it as a Progressive Web App. From Safari, tapping Share then Add to Home Screen installs it with the Krypto logo as the icon and opens it without the browser chrome -- it looks and feels like a native app. No App Store, no Xcode, no developer account required.

The one constraint is that the ESP32 connection only works when the phone is on the same WiFi network as the sensor. The login, profile, and PDF report features work from anywhere since they run entirely in the browser using localStorage.

05. Demo -- App in action

The following video shows the full workflow: the ESP32 connected to the MPU-6050, the dashboard loading on iPhone, the live metrics updating as the sensor moves, and the PDF report being generated and downloaded.

ESP32 IP address on Serial Monitor

ESP32 sending data

Serial Monitor showing the IP address assigned to the ESP32 after connecting to WiFi. This IP is entered in the dashboard to start the live feed.

Live data on iPhone

Live data on external device

The dashboard open on iPhone Safari, connected to the ESP32. The metric cards and orientation bars update every second as the sensor moves.

06. Board Interaction

This video shows the physical ESP32 board connected to the MPU-6050 sensor responding in real time. As the sensor is tilted, shaken and moved, the dashboard on the phone updates the pitch, roll, acceleration values and impact counter simultaneously.

07. Files

Source files and links for this week's work.


↓ index.html ↗ GitLab repository ↗ Live app