# 🧠 Taekwondo Scoring System – Spinning Kick Detection: Corrected Logic

> **Version:** 1.1  
> **Last Updated:** July 2025  
> **Target Developer:** Embedded & Android Developers  
> **Purpose:** Fix the **spinning detection logic** — it must come from the **attacker**, not the defender

---

## 📌 Table of Contents

- [1. The Problem](#1-the-problem)
- [2. Correct Logic: Spinning is Detected on the Attacker](#2-correct-logic-spinning-is-detected-on-the-attacker)
- [3. Data Flow for Spinning Kick](#3-data-flow-for-spinning-kick)
- [4. Time Synchronization & Correlation](#4-time-synchronization--correlation)
- [5. Updated Impact & Spinning Messages](#5-updated-impact--spinning-messages)
- [6. Scoring Engine Logic (C#)](#6-scoring-engine-logic-c)
- [7. ESP32: MPU6050 on Hogu (Attacker Side)](#7-esp32-mpu6050-on-hogu-attacker-side)
- [8. Best Practices](#8-best-practices)

---

## 1. The Problem

In the **original design**, spinning detection was tied to the **impact sensor**:

```json
{"type":"impact","device":"head_blue","force":900,"spinning":true,"time":...}
```

But this is **physically incorrect**:
- When **Red kicks Blue**, **Blue’s headgear** detects the impact.
- But **Red’s body** is the one that spun.
- **Blue’s sensor cannot know** that **Red** was spinning.

> ❌ **You cannot detect the attacker’s motion on the defender’s device.**

---

## 2. Correct Logic: Spinning is Detected on the Attacker

✅ **Spinning must be detected on the attacker’s hogu (trunk protector)**, where the **MPU6050** is mounted.

The system must:
1. Detect **impact** on **defender’s sensor** (e.g., `head_blue`)
2. Detect **rotation > 180°** on **attacker’s sensor** (e.g., `hogu_red`)
3. Confirm both events occurred within **±200ms**
4. Award **4 or 5 points** only if both conditions are met

---

## 3. Data Flow for Spinning Kick

```
[Red Athlete]
  ↓
ESP32 in Hogu detects: "I am spinning!" → sends:
  {"type":"motion","device":"hogu_red","spinning":true,"time":T1}

[Red kicks Blue]
  ↓
ESP32 in Blue's headgear detects: "I was hit!" → sends:
  {"type":"impact","device":"head_blue","force":900,"time":T2}

[Score Control App]
  ↓
Checks:
  - Was there a spinning event near T2?
  - From same athlete (Red)?
  - Within ±200ms?
  → If yes: Award 5 points
```

---

## 4. Time Synchronization & Correlation

All devices use **UTC ticks** for precise timing.

### Correlation Window
```csharp
bool IsSpinningKick(long impactTime, long spinningTime)
{
    long window = 200 * TimeSpan.TicksPerMillisecond; // ±200ms
    return Math.Abs(impactTime - spinningTime) <= window;
}
```

### Determine Attacker from Device Name
- `hogu_red` + `head_blue` → **Red attacked Blue**
- So check if **Red was spinning** at impact time

---

## 5. Updated Impact & Spinning Messages

### 5.1 Impact (Defender’s Device)
```json
{
  "type": "impact",
  "device": "head_blue",
  "force": 900,
  "time": 1712345678901
}
```
> ❌ **No `spinning` field here** — irrelevant.

### 5.2 Motion (Attacker’s Device)
```json
{
  "type": "motion",
  "device": "hogu_red",
  "spinning": true,
  "angle": 270,
  "time": 1712345678850
}
```

> Sent only when **rotation > 180°** is detected on **attacker’s hogu**.

---

## 6. Scoring Engine Logic (C#)

```csharp
private List<MotionEvent> _motionEvents = new();

public void AddImpact(string device, int force, long time)
{
    if (force < GetThreshold(device)) return;

    string defender = device.Contains("_red") ? "red" : "blue";
    string attacker = defender == "red" ? "blue" : "red";

    // Find matching spinning event from attacker
    var spinningEvent = _motionEvents
        .Where(m => IsSameAthlete(m.Device, attacker) && IsInTimeWindow(m.Time, time))
        .LastOrDefault();

    bool isSpinning = spinningEvent != null;

    int points = CalculatePoints(device, isSpinning);

    if (points > 0)
    {
        AddPoints(attacker, points);
        BroadcastScoreUpdate();
        LogEvent($"✅ +{points} to {attacker} (Spinning: {isSpinning})");
    }
}

public void AddMotion(string device, bool spinning, long time)
{
    if (spinning)
    {
        _motionEvents.Add(new MotionEvent { Device = device, Time = time });
        // Keep only last 5 sec of events
        _motionEvents = _motionEvents.Where(m => m.Time > time - 50000000).ToList();
    }
}

private bool IsSameAthlete(string device, string athlete)
{
    return device.Contains($"_{athlete}");
}

private bool IsInTimeWindow(long spinningTime, long impactTime)
{
    long diff = Math.Abs(impactTime - spinningTime);
    return diff <= 200 * TimeSpan.TicksPerMillisecond; // ±200ms
}

private int CalculatePoints(string device, bool spinning)
{
    if (device.Contains("head"))
        return spinning ? 5 : 3;
    else
        return spinning ? 4 : 1;
}

// Data Class
public class MotionEvent { public string Device; public long Time; }
```

---

## 7. ESP32: MPU6050 on Hogu (Attacker Side)

### 📌 Mount MPU6050 on **Hogu**, not headgear

```cpp
#include <Adafruit_MPU6050.h>
Adafruit_MPU6050 mpu;

void checkSpinning() {
  sensors_event_t gyro;
  mpu.getGyroSensor(&gyro);
  
  float rotation = abs(gyro.gyro.z); // Z-axis = spin
  if (rotation > 1.0) { // Threshold (rad/s)
    long now = millis() * 10000; // Ticks
    static long lastSpin = 0;
    
    if (now - lastSpin > 20000000) { // 2 sec cooldown
      lastSpin = now;
      client.print("{\"type\":\"motion\",\"device\":\"hogu_red\",\"spinning\":true,\"time\":" + String(now) + "}\n");
    }
  }
}
```

> Only **hogu devices** send `motion` events.

---

## 8. Best Practices

| Practice | Why |
|--------|-----|
| Only hogu sends `motion` events | Headgear can't detect attacker spin |
| Correlate within ±200ms | Fast kicks, timing jitter |
| Use UTC ticks | No drift |
| Clear old motion events | Prevent false matches |
| Log spinning events | Debugging |
| Calibrate MPU6050 | Reduce false positives |


