Smart collars for dogs and cats tell owners how many minutes their pet rested, walked and played today. Vets use this data too: a dog that suddenly plays much less may be in pain. Inside the collar there is no magic, just a motion sensor and a small learning program. In this lesson you build a prototype of that program. The MPU-6050 measures movement, and every 2 seconds the UNO calculates two features: how strongly it moved and how rhythmically. You record examples of the three activities, and the UNO learns the typical values of each with a nearest-centroid classifier. Three LEDs show what it thinks is happening right now, and every minute it reports the totals.
What you'll learn
- How to split a sensor stream into windows and calculate features for each window
- How a nearest-centroid classifier learns the "typical" example of each class
- Why features need to be scaled before you compare them
- How activity trackers work, and what their limits are
What you need
- UNO R3 board and USB cable (UNO R3 Starter Kit)
- GY-521 MPU-6050 module (45-in-1 Sensor Kit)
- Green, yellow and red LEDs and 3 resistors, 220 Ω (UNO R3 Starter Kit)
- 1 push button (UNO R3 Starter Kit)
- Long jumper wires, and a soft toy or your own ankle to test with
- Breadboard and jumper wires (UNO R3 Starter Kit)
This is a prototype on a breadboard, not a collar. Don't attach wires or a breadboard to a real animal. Test it by holding the sensor, tying it to your own ankle, or moving a soft toy. A real collar needs a tiny, safe, battery-powered board in a closed case.
Wiring

| From | To |
|---|---|
| MPU-6050 VCC | 5V |
| MPU-6050 GND | GND |
| MPU-6050 SCL | UNO A5 |
| MPU-6050 SDA | UNO A4 |
| UNO pin 5 | 220 Ω resistor, then the green LED (resting) |
| UNO pin 6 | 220 Ω resistor, then the yellow LED (walking) |
| UNO pin 7 | 220 Ω resistor, then the red LED (playing) |
| All LEDs' short legs | GND |
| Push button, one leg | UNO pin 2 |
| Push button, the diagonally opposite leg | GND |
The sketch
Wire and EEPROM come with the Arduino IDE.
#include <Wire.h>
#include <EEPROM.h>
const int MPU = 0x68;
void wakeMpu() {
Wire.begin();
Wire.beginTransmission(MPU);
Wire.write(0x6B); // power management: wake up
Wire.write(0);
Wire.endTransmission();
}
void readAccel(float& ax, float& ay, float& az) { // in g
Wire.beginTransmission(MPU);
Wire.write(0x3B);
Wire.endTransmission(false);
Wire.requestFrom(MPU, 6);
int16_t v[3];
for (int i = 0; i < 3; i++) {
v[i] = Wire.read() << 8;
v[i] |= Wire.read();
}
ax = v[0] / 16384.0;
ay = v[1] / 16384.0;
az = v[2] / 16384.0;
}
const int LED_PINS[3] = {5, 6, 7};
const int BUTTON_PIN = 2;
const char* NAMES[3] = {"resting", "walking", "playing"};
const int WINDOW = 50; // 50 samples at 25 per second = 2 seconds
const int EXAMPLES = 8; // windows recorded per training round
struct Model {
byte marker; // 0x41 = trained
float sum[3][2]; // sum of each feature, per activity
float sumSq[2]; // sum of squares of each feature, all activities
int count[3]; // windows recorded per activity
};
Model model;
int trainClass = -1, trainLeft = 0;
float minutes[3] = {0, 0, 0};
unsigned long lastReport = 0;
// One 2-second window: feature 0 = movement strength, feature 1 = rhythm (peaks per second)
void window(float f[2]) {
float mag[WINDOW], avg = 0;
for (int i = 0; i < WINDOW; i++) {
float ax, ay, az;
readAccel(ax, ay, az);
mag[i] = sqrt(ax * ax + ay * ay + az * az); // total acceleration, about 1 g when still
avg += mag[i];
delay(40);
}
avg /= WINDOW;
float var = 0;
int peaks = 0;
for (int i = 0; i < WINDOW; i++) {
var += (mag[i] - avg) * (mag[i] - avg);
if (i > 0 && i < WINDOW - 1 && mag[i] > avg + 0.08 && mag[i] > mag[i - 1] && mag[i] >= mag[i + 1]) peaks++;
}
f[0] = sqrt(var / WINDOW); // standard deviation in g
f[1] = peaks / 2.0; // peaks per second
}
float centroid(int c, int k) { // the average example of an activity
return model.sum[c][k] / model.count[c];
}
float scaleOf(int k) { // typical spread of a feature, over all examples
int n = model.count[0] + model.count[1] + model.count[2];
float all = model.sum[0][k] + model.sum[1][k] + model.sum[2][k];
float m = all / n;
return max(sqrt(max(model.sumSq[k] / n - m * m, 0.0f)), 0.05f);
}
int classify(const float f[2]) {
int best = -1;
float bestD = 1e9;
for (int c = 0; c < 3; c++) {
if (model.count[c] == 0) continue;
float d = 0;
for (int k = 0; k < 2; k++) {
float diff = (f[k] - centroid(c, k)) / scaleOf(k); // scaled difference
d += diff * diff;
}
if (d < bestD) { bestD = d; best = c; }
}
return best;
}
void learn(int c, const float f[2]) {
for (int k = 0; k < 2; k++) {
model.sum[c][k] += f[k];
model.sumSq[k] += f[k] * f[k];
}
model.count[c]++;
}
void setup() {
Serial.begin(115200);
pinMode(BUTTON_PIN, INPUT_PULLUP);
for (int i = 0; i < 3; i++) pinMode(LED_PINS[i], OUTPUT);
wakeMpu();
EEPROM.get(0, model);
if (model.marker != 0x41) memset(&model, 0, sizeof(model));
Serial.println("Type r, w or p, then press the button to record 8 windows of resting, walking or playing.");
Serial.println("Type x to forget everything.");
}
void loop() {
if (Serial.available()) {
char c = Serial.read();
int which = c == 'r' ? 0 : c == 'w' ? 1 : c == 'p' ? 2 : -1;
if (which >= 0) {
trainClass = which;
Serial.print("Ready to record ");
Serial.print(NAMES[which]);
Serial.println(". Press the button and start moving.");
}
if (c == 'x') {
memset(&model, 0, sizeof(model));
EEPROM.put(0, model);
Serial.println("Forgotten.");
}
}
if (trainClass >= 0 && digitalRead(BUTTON_PIN) == LOW) trainLeft = EXAMPLES;
float f[2];
window(f); // takes 2 seconds
if (trainLeft > 0) {
learn(trainClass, f);
trainLeft--;
for (int i = 0; i < 3; i++) digitalWrite(LED_PINS[i], i == trainClass && trainLeft % 2);
Serial.print("Recorded ");
Serial.print(NAMES[trainClass]);
Serial.print(": strength ");
Serial.print(f[0], 3);
Serial.print(" g, rhythm ");
Serial.print(f[1], 1);
Serial.println(" /s");
if (trainLeft == 0) {
model.marker = 0x41;
EEPROM.put(0, model);
trainClass = -1;
Serial.println("Done and saved.");
}
return;
}
int c = classify(f);
for (int i = 0; i < 3; i++) digitalWrite(LED_PINS[i], i == c);
if (c < 0) return; // not trained yet
minutes[c] += 2.0 / 60;
Serial.print("Now: ");
Serial.print(NAMES[c]);
Serial.print(" (strength ");
Serial.print(f[0], 3);
Serial.print(", rhythm ");
Serial.print(f[1], 1);
Serial.println(")");
if (millis() - lastReport > 60000) {
lastReport = millis();
Serial.print("Today: resting ");
Serial.print(minutes[0], 1);
Serial.print(" min, walking ");
Serial.print(minutes[1], 1);
Serial.print(" min, playing ");
Serial.print(minutes[2], 1);
Serial.println(" min");
}
}
Train and test it
- Upload the sketch and open the Serial Monitor at 115200 baud, "No line ending".
- Type
r, press Enter, then press the button and hold the sensor still for 16 seconds: 8 windows of resting. - Type
w, press the button and walk around the room holding the sensor at your side, or with it tied to your ankle. - Type
p, press the button and "play": jump, shake, run on the spot. - Now just watch. Every 2 seconds the LEDs and the Serial Monitor show what the UNO thinks you are doing, and once a minute you get the totals. Training is saved, so it survives unplugging. Type
xto start again.
How it works
Sensor data arrives as an endless stream, so the sketch cuts it into windows of 2 seconds and describes each window with two features. Strength is the standard deviation of the total acceleration: almost 0 when resting, and much bigger when playing. Rhythm counts the peaks per second. Walking creates a steady beat of steps, about 2 per second for a person and more for a small dog, while play is wild and irregular.
A nearest-centroid classifier learns one "typical example" per activity: the centroid, which is simply the average of its training windows. A new window gets the activity whose centroid is closest. Compared to k-nearest neighbours it needs very little memory: the sketch only keeps running totals (sums and counts) and calculates the averages from them, so the whole model is 11 numbers, however much you train.
There's one catch: the two features have completely different sizes. Strength is around 0.01 to 0.5 g, rhythm is 0 to 5 peaks per second. Without scaling, the rhythm would decide almost everything. So each difference is divided by the typical spread of that feature (scaleOf()) before the distances are added up. This feature scaling is one of the most common steps in machine learning. Real activity trackers use more features and more training data, from many animals, and they still sometimes mistake scratching for play!
Try this next
- Add a fourth activity: "sleeping" versus "resting". Can two features tell them apart, or do you need a new feature?
- Add a third feature, such as the average tilt angle (lying down versus standing).
- Send the totals to your phone: rebuild it on the ESP32 with a web page like the pet activity chart.
- Ask a dog owner how their dog moves when it plays, and how often it walks. Does your model match?
Troubleshooting
- It always says the same activity: record more varied examples, and make the activities clearly different while recording.
- Walking and playing are mixed up: walk more steadily when you record walking, and play much harder when you record playing.
- Nothing happens after r, w or p: press the button once after typing the letter; recording starts with the next 2-second window.
- All values are 0: check SDA on A4 and SCL on A5.