#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>
#include <MPU6050_light.h>
MPU6050 mpu(Wire);
// --- WiFi ---
const char* SSID = "Wokwi-GUEST";
const char* PASSWORD = "";
// --- WebServer ---
WebServer server(80);
// --- Піни ---
const int LED_PIN = 15;
const int BUZZER_PIN = 18;
// --- Класи рухів ---
const char* CLASSES[] = {"snap", "fall", "lift_rotate", "idle"};
const int NUM_CLASSES = 4;
const int INPUT_SIZE = 7;
// --- Параметри перцептрона ---
float W[NUM_CLASSES][INPUT_SIZE];
float b[NUM_CLASSES];
float lr = 0.01;
int epochs = 10;
int samplesPerClass = 200;
float epochLoss[50];
// --- Стани ---
int repCount = 0;
unsigned long lastRepTime = 0;
String lastPred = "idle";
// --- Генератор псевдоданих (для тренування) ---
void generateSample(int label, float* x) {
if (label == 2) { // lift_rotate
x[0] = random(-50, 50) / 100.0;
x[1] = 1.0 + random(-50, 50) / 100.0;
x[2] = random(-50, 50) / 100.0;
x[3] = 5.0 + random(-200, 200) / 100.0;
x[4] = 5.0 + random(-200, 200) / 100.0;
x[5] = 5.0 + random(-200, 200) / 100.0;
x[6] = random(0, 20) / 100.0;
} else if (label == 0) { // snap
x[0] = random(-20, 20) / 100.0;
x[1] = random(-20, 20) / 100.0;
x[2] = random(-20, 20) / 100.0;
x[3] = random(-50, 50) / 100.0;
x[4] = random(-50, 50) / 100.0;
x[5] = random(-50, 50) / 100.0;
x[6] = 0.7 + random(0, 30) / 100.0;
} else if (label == 1) { // fall
x[0] = random(-200, 200) / 100.0;
x[1] = -9.8 + random(-200, 200) / 100.0;
x[2] = random(-200, 200) / 100.0;
x[3] = random(-500, 500) / 100.0;
x[4] = random(-500, 500) / 100.0;
x[5] = random(-500, 500) / 100.0;
x[6] = 0.2 + random(0, 30) / 100.0;
} else { // idle
x[0] = random(-5, 5) / 100.0;
x[1] = random(-5, 5) / 100.0;
x[2] = random(-5, 5) / 100.0;
x[3] = random(-5, 5) / 100.0;
x[4] = random(-5, 5) / 100.0;
x[5] = random(-5, 5) / 100.0;
x[6] = random(0, 5) / 100.0;
}
}
// --- Softmax ---
float softmax(float* z, int i) {
float sum = 0;
for (int j = 0; j < NUM_CLASSES; j++) sum += exp(z[j]);
return exp(z[i]) / sum;
}
// --- Передбачення ---
int predict(float* x) {
float z[NUM_CLASSES];
for (int i = 0; i < NUM_CLASSES; i++) {
z[i] = b[i];
for (int j = 0; j < INPUT_SIZE; j++) {
z[i] += W[i][j] * x[j];
}
}
int best = 0;
float bestVal = -1e9;
for (int i = 0; i < NUM_CLASSES; i++) {
float p = softmax(z, i);
if (p > bestVal) {
bestVal = p;
best = i;
}
}
return best;
}
// --- Крок навчання ---
float trainStep(float* x, int yTrue) {
float z[NUM_CLASSES];
float p[NUM_CLASSES];
for (int i = 0; i < NUM_CLASSES; i++) {
z[i] = b[i];
for (int j = 0; j < INPUT_SIZE; j++) {
z[i] += W[i][j] * x[j];
}
}
for (int i = 0; i < NUM_CLASSES; i++) p[i] = softmax(z, i);
float loss = 0.0;
for (int i = 0; i < NUM_CLASSES; i++) {
float target = (i == yTrue ? 1.0 : 0.0);
loss += -target * log(max(p[i], (float)1e-6));
float grad = p[i] - target;
for (int j = 0; j < INPUT_SIZE; j++) W[i][j] -= lr * grad * x[j];
b[i] -= lr * grad;
}
return loss;
}
// --- HTML сторінка ---
const char MAIN_page[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<title>Smart Gym Dashboard</title>
<meta charset="utf-8">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
body {
margin: 0;
padding: 0;
font-family: Arial, sans-serif;
background: #f5f5f7;
color: #111;
}
header{
background:#111;
color:white;
padding:18px 25px;
font-size:26px;
font-weight:bold;
letter-spacing:.5px;
}
.container{
width:90%;
max-width:700px;
margin:auto;
padding:20px;
text-align:center;
}
.infoBox{
background:white;
border-radius:14px;
padding:18px;
margin-top:15px;
box-shadow:0 4px 15px rgba(0,0,0,0.08);
font-size:22px;
font-weight:600;
transition:.3s;
}
.infoBox span{
color:#d43333;
font-weight:700;
}
#lossChart{
margin-top:30px;
background:white;
padding:10px;
border-radius:14px;
box-shadow:0 4px 15px rgba(0,0,0,0.07);
}
</style>
</head>
<body>
<header>🏋 Smart Gym Monitor</header>
<div class="container">
<div class="infoBox">
Motion: <span id="motion">-</span>
</div>
<div class="infoBox">
Reps: <span id="reps">0</span>
</div>
<canvas id="lossChart" width="380" height="200"></canvas>
</div>
<script>
async function refresh(){
const res = await fetch('/state');
const data = await res.json();
document.getElementById('motion').textContent=data.motion;
document.getElementById('reps').textContent=data.reps;
}
async function drawLoss(){
const res=await fetch('/data');
const data=await res.json();
new Chart(document.getElementById('lossChart'),{
type:'line',
data:{
labels:data.epochs,
datasets:[{
label:'Training Loss',
data:data.loss,
borderColor:'#d43333',
borderWidth:2,
tension:0.3
}]
}
});
}
drawLoss();
setInterval(refresh,1200);
</script>
</body>
</html>
)rawliteral";
// --- Маршрути ---
void handleRoot() { server.send(200, "text/html", MAIN_page); }
void handleData() {
String json = "{ \"epochs\": [";
for (int i = 0; i < epochs; i++) {
json += String(i+1);
if (i < epochs-1) json += ",";
}
json += "], \"loss\": [";
for (int i = 0; i < epochs; i++) {
json += String(epochLoss[i], 4);
if (i < epochs-1) json += ",";
}
json += "] }";
server.send(200, "application/json", json);
}
void handleState() {
String json = "{ \"motion\": \"" + lastPred + "\", \"reps\": " + String(repCount) + " }";
server.send(200, "application/json", json);
}
void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
pinMode(BUZZER_PIN, OUTPUT);
// WiFi
WiFi.begin(SSID, PASSWORD);
Serial.print("Connecting to WiFi...");
while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }
Serial.println();
Serial.print("Connected! IP: ");
Serial.println(WiFi.localIP());
server.on("/", handleRoot);
server.on("/data", handleData);
server.on("/state", handleState);
server.begin();
Wire.begin();
mpu.begin();
mpu.calcOffsets(true, true);
// Ініціалізація ваг
for (int i = 0; i < NUM_CLASSES; i++) {
b[i] = 0;
for (int j = 0; j < INPUT_SIZE; j++)
W[i][j] = ((float)random(-100, 100)) / 1000.0;
}
// Навчання на псевдоданих
Serial.println("Training...");
for (int e = 0; e < epochs; e++) {
float totalLoss = 0;
for (int c = 0; c < NUM_CLASSES; c++) {
for (int n = 0; n < samplesPerClass; n++) {
float x[INPUT_SIZE];
generateSample(c, x);
totalLoss += trainStep(x, c);
}
}
epochLoss[e] = totalLoss / (NUM_CLASSES * samplesPerClass);
Serial.print("Epoch "); Serial.print(e+1);
Serial.print(" Loss: "); Serial.println(epochLoss[e], 4);
}
Serial.println("Done!");
}
void loop() {
server.handleClient();
// Зчитування реальних даних MPU
mpu.update();
float x[INPUT_SIZE] = {
mpu.getAccX(), mpu.getAccY(), mpu.getAccZ(),
mpu.getGyroX(), mpu.getGyroY(), mpu.getGyroZ(),
random(0, 100) / 100.0f
};
int pred = predict(x);
lastPred = CLASSES[pred];
// Якщо рух — підняття ваги (rep)
if (lastPred == "lift_rotate") {
if (millis() - lastRepTime > 1000) { // захист від подвійного рахунку
repCount++;
lastRepTime = millis();
digitalWrite(LED_PIN, HIGH);
tone(BUZZER_PIN, 400, 150);
delay(150);
digitalWrite(LED_PIN, LOW);
}
}
delay(500);
}