Introduction: Guided Running Shoe (with/ Arduino IOT Cloud)

This Instructable documents a functional prototype for a guided running shoe that displays directional navigation using an LED matrix mounted on a piece of footwear. The shoe points toward a programmed destination using latitude and longitude, allowing a runner or walker to navigate without looking at a screen.

This prototype uses:

  1. Arduino Nano ESP32
  2. WiFi connection over mobile device
  3. Arduino IoT Cloud as the data link
  4. Phone sensors (GPS + accelerometer) for remote navigation intelligence

Ultimately, this project is part of my ongoing study into screenless wayfinding and wearable navigation systems.

Supplies

  1. Arduino Nano ESP32
  2. Adafruit DotStar / DotMatrix
  3. 1” × 1” thin acrylic sheet
  4. Velcro straps (3/4" width)
  5. Hot glue
  6. Construction paper
  7. Stranded wire
  8. Leather/fabric hole punch

Step 1: Wiring Up

I wired the Arduino Nano ESP32 directly to a 64-pixel Adafruit Dot Matrix using four short jumper wires. I deliberately kept the wires short (about 20–25mm), because the display and board will eventually sit almost back-to-back once mounted, and long wires create slack, stress, and failure points.

Here’s what the connections came down to:

  1. VBUS (5V) → 5V on the Dot Matrix
  2. This provides power to the pixels.
  3. GND → GND
  4. A shared reference ground is essential; without this, the entire display stays dark.
  5. Pin 4 → DIN
  6. DIN carries the actual pixel data from the board to the display.
  7. Pin 2 → CLK
  8. CLK provides the timing pulse so the matrix knows when to shift data.

There’s really not much more to it — but it wasn’t obvious until I had done it.

Before committing everything with hot glue and Velcro, I ran a quick test by uploading a simple sketch that lights all the pixels uniformly. This is the fast and dirty truth test: if every pixel comes on, the wiring is correct; if only some flicker or nothing happens, something is wrong and you fix it now, not after building the enclosure.

Step 2: Connecting the Phone and Arduino Through the IoT Cloud

Once your phone is added as a Device in the Arduino IoT Cloud, it will also automatically appear in the Things tab. Clicking into it reveals the live data coming from the phone sensors — accelerometer values, GPS coordinates, and anything else you’ve enabled. (If you have the paid plan, you’ll see extra phone variables available beyond the basics.)

The next move is to link those variables to the Arduino Nano.

This part is conceptually simple, but there are several traps that can leave you with a “compiling but not working” situation — which is exactly what happened to me.

Inside the Arduino’s Thing page, I used the Add Variable button to start syncing data. When the “Add Variable” window opens, you can choose Sync with Other Things, which lets you directly mirror any variable coming from the phone. For example, I linked accelerometer_linear from the phone to the same-named variable on the Nano. Keeping the names identical turns out to be surprisingly important — it helps avoid confusion later in troubleshooting.

The key piece — and the source of hours lost — is the permission mode. The variables must be set to Read & Write.

If you select Read Only, the values visually appear in the cloud dashboard but never actually propagate to the Arduino. And nothing will warn you about this.

Once I corrected that, I repeated the process for the remaining sensor variables.

At that point I had synced everything, but still saw no live data coming into the Nano. What finally fixed it was discovering that the Arduino IoT Cloud does not automatically regenerate its internal thingProperties.h file when variables change. So I deleted the entire sketch, reopened it fresh, and the cloud editor recreated a new thingProperties.h that actually matched the new variable setup.

Only then did the serial monitor begin reflecting real sensor data.

You know it’s working when:

  1. the values in both the phone Thing and the Arduino Thing refresh as you move the phone
  2. and those same values appear in your serial monitor when testing code
  3. and both devices show an Online status in IoT Cloud

It took longer than I wished, but it was a valuable discovery:

your code can succeed and still silently fail if your cloud configuration isn’t fully aligned



Step 3: The First “Light-Up” Test

Before attempting navigation logic, I wrote a simple test sketch that let me shake the phone to make the LED matrix respond.

This confirmed two very important things:

  1. The cloud sync was truly working in both directions
  2. The wiring between the Arduino and matrix was reliable

Seeing that first reaction — the LED flickering from a physical movement — is when the project felt “alive.”

Step 4: Making the Physical Device

The materials are very minimal: Velcro, construction paper, short wire leads, the Arduino, and a square of acrylic.

The dot matrix and Arduino are attached back-to-back, with paper hot-glued between them to prevent shorts. The top face of the Arduino also gets paper, mostly to avoid accidentally pressing the reset pin, which happened several times early on.

The Velcro wraps around the whole assembly like a small belt, allowing the board to clip to the tongue or laces of the shoe. A hole punch was used to make a clean opening for the USB-C port so the shoe can be re-programmed or charged without cutting anything apart.

The acrylic square is hot-glued to the front of the matrix, acting as a lightweight protective lens and giving a clearer visual surface during motion.

Step 5: Orientation & Calibration

Because the matrix can be rotated relative to the shoe, I wrote a north-arrow visualization so I could orient the pixel drawing patterns. I then rotated the shoe until the displayed “North” matched true north.

From there, every other arrow was calibrated relative to that north reference.

Step 6: Arrow Directions Explained

Step 7: Final Version Code

Writing the Final Code (and Surviving the Debugging)

Once the wiring and cloud syncing were stable, I shifted focus to the actual logic running the shoe. This meant writing the code that (1) interprets GPS data, (2) compares it to previous positions, (3) determines the desired heading, and (4) selects the correct arrow animation to display on the dot matrix.

A lot of this logic I wrote myself: mapping out how to compute direction, how to smooth the heading, how to convert the destination latitude/longitude into a directional arrow, and how often the GPS should be sampled to avoid noise. I also wrote all the arrow definitions for the dot matrix (North, East, South, West, and the diagonals), and laid out how the visuals should appear in relation to one another on the 8×8 grid.

The central idea was simple:

figure out which way the runner needs to turn, then display that arrow.

The implementation, however, required several subtle layers to make it robust. The logic computes bearing between the previous GPS point and the current one, to establish the movement direction of the runner, and separately computes bearing from the runner's current position to the destination. The difference between those two bearings determines the arrow.

Where ChatGPT Came In

While I established the structure, patterns, and algorithmic logic, ChatGPT served almost like a patient software assistant — helping turn those ideas into valid syntax, catching mismatched braces, and especially helping with compiling errors specific to the Arduino Cloud environment.

One repeated stumbling block was something the Arduino IoT Cloud requires:

Every variable synced from the Cloud must have a corresponding callback function in the sketch, even when the function doesn’t actually need to do anything.

Without these callbacks, the code compiles, uploads partway, and then fails at the linking stage — a frustrating behavior because the editor made it seem like the program was valid. After several rebuilds, reconfiguration attempts, and troubleshooting conversations, I realized that the callbacks had to be explicitly present, even if empty.

This was the most persistent error, and the final solution was to include the following necessary stub functions at the end of the sketch:


// =====================================
// REQUIRED CALLBACKS — DO NOTHING
// =====================================
void onAccelerometerLinearChange() {}
void onAccelerometerXChange() {}
void onAccelerometerYChange() {}
void onAccelerometerZChange() {}
void onGpsChange() {}

This solved a problem that didn’t feel like traditional debugging — it wasn’t an algorithm issue, but rather a structural requirement of the Arduino IoT Cloud system.

Once those were added, the code compiled cleanly and began behaving consistently.

With that in place, the system finally ran: the device compared live GPS position to prior positions, understood coarse heading, calculated the direction toward the destination, and displayed the appropriate arrow onto the shoe.

It didn’t just “work.”

It translated geography — thousands of coordinates in the real world — into a single point of light in front of you.

And that’s when I felt the meaning of the project click:

Instead of needing a screen, the wearer just looks down and moves toward the light.

Step 8: Adding Your Directions

Destinations can be added in the code by plugging your desired location into https://www.latlong.net/ and putting the outputted latitude, longitude coordinates in the code.

Step 9: Final Code

#include "thingProperties.h"
#include <Adafruit_DotStar.h>
#include <SPI.h>
#include <math.h>

// ======================================================
// DOTSTAR SETUP
// ======================================================
#define DATAPIN 4
#define CLOCKPIN 2
#define NUMPIXELS 64

// 0 = no rotation
// 1 = 90° clockwise
// 2 = 180°
// 3 = 90° counter-clockwise
#define ROTATION 0 // change this to match how the shoe is oriented

Adafruit_DotStar matrix(NUMPIXELS, DATAPIN, CLOCKPIN, DOTSTAR_BRG);

void clearMatrix() {
for (int i = 0; i < NUMPIXELS; i++) {
matrix.setPixelColor(i, 0);
}
}

// ======================================================
// DESTINATION (Gramercy)
// ======================================================
float destLat = 40.737011;
float destLon = -73.983360;

// ======================================================
// STATE FOR GPS & BEARINGS
// ======================================================
float lastLat = 0.0;
float lastLon = 0.0;
bool hasLastPos = false;

float movementBearing = 0.0; // direction you're walking
bool hasMovementBearing = false;

float relBearingSmoothed = 0.0;
bool hasRelBearing = false;

unsigned long lastGPSms = 0;

// How often we look at GPS
const unsigned long GPS_SAMPLE_MS = 700; // faster = more responsive

// Minimum movement (deg lat+lon) to treat as "you moved"
const float GPS_DELTA = 0.00001; // tweak if needed

// ======================================================
// MATH HELPERS
// ======================================================
float toRad(float d){ return d * 0.01745329252; }

float computeBearing(float lat1, float lon1, float lat2, float lon2) {
lat1 = toRad(lat1);
lat2 = toRad(lat2);
float dLon = toRad(lon2 - lon1);

float y = sin(dLon) * cos(lat2);
float x = cos(lat1) * sin(lat2) -
sin(lat1) * cos(lat2) * cos(dLon);

float bearing = atan2(y,x) * 57.295779513; // → degrees
bearing = fmod(bearing + 360.0, 360.0); // normalize 0–360
return bearing;
}

// Normalize angle difference to -180..180
float shortestAngleDiff(float target, float current) {
float diff = target - current;
while (diff > 180.0) diff -= 360.0;
while (diff < -180.0) diff += 360.0;
return diff;
}

// ======================================================
// COORDINATE MAPPING WITH GLOBAL ROTATION
// ======================================================
int xyRot(int x, int y) {
int xr, yr;

#if ROTATION == 0
xr = x; yr = y; // no rotation
#elif ROTATION == 1
// 90° clockwise
xr = 7 - y; yr = x;
#elif ROTATION == 2
// 180°
xr = 7 - x; yr = 7 - y;
#elif ROTATION == 3
// 90° counter-clockwise
xr = y; yr = 7 - x;
#endif

return yr * 8 + xr;
}

// ======================================================
// ARROW DEFINITIONS (YOUR ORIGINAL DESIGNS)
// ======================================================

// NORTH
const int NORTH[][2] = {
{3,1}, {4,1}, {5,1}, {2,2}, {4,2}, {6,2},
{1,3}, {4,3}, {7,3}, {4,4}, {4,5}, {4,6}, {4,7}
};

// SOUTH
const int SOUTH[][2] = {
{1,4}, {2,5}, {3,6}, {4,6}, {5,6}, {6,5}, {7,4},
{4,5}, {4,4}, {4,3}, {4,2}, {4,1}, {4,0}
};

// EAST
const int EAST[][2] = {
{6,2}, {6,3}, {6,4}, {5,1}, {5,3}, {5,5},
{4,1}, {4,3}, {4,6}, {3,3}, {2,3}, {1,3}, {0,3}
};

// WEST
const int WEST[][2] = {
{1,2}, {1,3}, {1,4}, {2,1}, {3,0}, {2,5}, {3,6},
{2,3}, {3,3}, {4,3}, {5,3}, {6,3}, {7,3}
};

// NORTHWEST
const int NORTHWEST[][2] = {
{0,0}, {1,0}, {2,0}, {3,0}, {4,0},
{0,1}, {0,2}, {0,3}, {0,4},
{1,1}, {2,2}, {3,3}, {4,4}, {5,5}, {6,6}
};

// NORTHEAST
const int NORTHEAST[][2] = {
{7,0}, {7,1}, {7,2}, {7,3}, {7,4},
{6,0}, {5,0}, {4,0}, {3,0},
{6,6}, {5,2}, {4,3}, {3,4}, {2,5}, {1,6}
};

// simple SE / SW to complete the 8 directions
const int SOUTHEAST[][2] = {
{4,6}, {5,5}, {6,4}, {5,3}, {4,2}
};

const int SOUTHWEST[][2] = {
{2,6}, {1,5}, {0,4}, {1,3}, {2,2}
};

// ======================================================
// DRAW HELPERS
// ======================================================
void drawPixels(const int arr[][2], int n) {
clearMatrix();
for (int i = 0; i < n; i++) {
int x = arr[i][0];
int y = arr[i][1];
if (x < 0 || x > 7 || y < 0 || y > 7) continue;
int idx = xyRot(x, y);
matrix.setPixelColor(idx, 255, 150, 25);
}
matrix.show();
}

void drawNorth() { drawPixels(NORTH, sizeof(NORTH)/sizeof(NORTH[0])); }
void drawSouth() { drawPixels(SOUTH, sizeof(SOUTH)/sizeof(SOUTH[0])); }
void drawEast() { drawPixels(EAST, sizeof(EAST)/sizeof(EAST[0])); }
void drawWest() { drawPixels(WEST, sizeof(WEST)/sizeof(WEST[0])); }
void drawNorthWest() { drawPixels(NORTHWEST, sizeof(NORTHWEST)/sizeof(NORTHWEST[0])); }
void drawNorthEast() { drawPixels(NORTHEAST, sizeof(NORTHEAST)/sizeof(NORTHEAST[0])); }
void drawSouthEast() { drawPixels(SOUTHEAST, sizeof(SOUTHEAST)/sizeof(SOUTHEAST[0])); }
void drawSouthWest() { drawPixels(SOUTHWEST, sizeof(SOUTHWEST)/sizeof(SOUTHWEST[0])); }

// Map 0–360° → 8 directions using your arrow art
// Here 0° means "straight ahead", 90° = "right", 180° = "behind", 270° = "left"
void drawDirection(float angleDeg) {
// normalize 0–360
angleDeg = fmod(angleDeg + 360.0, 360.0);

int dir = (int)((angleDeg + 22.5) / 45.0) % 8;

switch (dir) {
case 0: drawNorth(); break; // straight ahead
case 1: drawNorthEast(); break; // slight right
case 2: drawEast(); break; // right
case 3: drawSouthEast(); break; // back-right
case 4: drawSouth(); break; // back
case 5: drawSouthWest(); break; // back-left
case 6: drawWest(); break; // left
case 7: drawNorthWest(); break; // slight left
}
}

// ======================================================
// SETUP
// ======================================================
void setup() {
Serial.begin(115200);
delay(1500);

initProperties();
ArduinoCloud.begin(ArduinoIoTPreferredConnection);

matrix.begin();
matrix.setBrightness(40);
clearMatrix();
matrix.show();
}

// ======================================================
// LOOP — RELATIVE WAYFINDING
// ======================================================
void loop() {
ArduinoCloud.update();

Location loc = gps.getValue();
float lat = loc.lat;
float lon = loc.lon;

// Debug: see what GPS is doing
Serial.print("GPS: ");
Serial.print(lat, 6);
Serial.print(", ");
Serial.println(lon, 6);

// Wait for real GPS
if (lat == 0.0 && lon == 0.0) {
Serial.println("Waiting for valid GPS...");
return;
}

// Throttle GPS updates
unsigned long now = millis();
if (now - lastGPSms < GPS_SAMPLE_MS) return;
lastGPSms = now;

// --- 1. Update movement bearing if we have a previous point
if (hasLastPos) {
float d = fabs(lat - lastLat) + fabs(lon - lastLon);

if (d > GPS_DELTA) {
float newMove = computeBearing(lastLat, lastLon, lat, lon);

if (!hasMovementBearing) {
movementBearing = newMove;
hasMovementBearing = true;
} else {
float diff = shortestAngleDiff(newMove, movementBearing);
movementBearing += diff * 0.5; // smooth movement direction
}
}
}

lastLat = lat;
lastLon = lon;
hasLastPos = true;

// --- 2. Bearing from current position → destination
float destBearing = computeBearing(lat, lon, destLat, destLon);

// Debug
Serial.print("destBearing: ");
Serial.println(destBearing);

float toDisplayAngle;

if (hasMovementBearing) {
// Relative angle: where to turn relative to where you're walking
float rel = shortestAngleDiff(destBearing, movementBearing); // -180..180
float rel0to360 = fmod(rel + 360.0, 360.0);

if (!hasRelBearing) {
relBearingSmoothed = rel0to360;
hasRelBearing = true;
} else {
float diffRel = shortestAngleDiff(rel0to360, relBearingSmoothed);
relBearingSmoothed += diffRel * 0.4; // smooth but responsive
}

toDisplayAngle = relBearingSmoothed;

Serial.print("movementBearing: ");
Serial.println(movementBearing);
Serial.print("rel (0-360): ");
Serial.println(rel0to360);
Serial.print("relSmoothed: ");
Serial.println(relBearingSmoothed);
} else {
// No movement yet → just point to destination in world coords
toDisplayAngle = destBearing;
Serial.println("No movement bearing yet, using destBearing directly.");
}

drawDirection(toDisplayAngle);
}

// ======================================================
// REQUIRED IOT CALLBACKS (NO-OP)
// ======================================================
void onAccelerometerLinearChange() {}
void onAccelerometerXChange() {}
void onAccelerometerYChange() {}
void onAccelerometerZChange() {}
void onGpsChange() {}

Step 10: Takeaways

This document doesn’t present a finished consumer product. It presents a stepping stone — a working proof that navigation can be embodied, peripheral, and minimally distracting.

The project validated that the technology stack is feasible with simple consumer level electronics.


Things I'm particularly proud of in this project:

  1. Project perseverance
  2. problem solving & troubleshooting
  3. Connecting between devices

What I would work on next:

  1. Animations for when the wifi signal disconnects
  2. Adding gps and compass on the device for better responsiveness + accuracy
  3. 3D Printing a housing for these and a battery (if the form factor gets much bigger than perhaps integrating into the shoe)
  4. Creating more code for how the serial monitor displays data (for better understanding)
  5. A dedicated instructable and video walkthrough for phone to arduino IOT connection as this is not super easy to setup alone without sufficient know-how