# 🧠 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 _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 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 |