385 lines
10 KiB
Markdown
385 lines
10 KiB
Markdown
# 🌐 Taekwondo Scoring System – Wi-Fi Connectivity Guide
|
||
|
||
> **Version:** 1.0
|
||
> **Last Updated:** July 2025
|
||
> **Target Developer:** Embedded & Android developers
|
||
> **Purpose:** Detailed guide on **Wi-Fi connectivity** for single and multi-court setups with **auto-discovery and conflict prevention**
|
||
|
||
---
|
||
|
||
## 📌 Table of Contents
|
||
|
||
- [1. Overview](#1-overview)
|
||
- [2. Mode 1: Wi-Fi Hotspot-Based (Single Court)](#2-mode-1-wi-fi-hotspot-based-single-court)
|
||
- [3. Mode 2: Wi-Fi Non-Hotspot (Tournament Mode)](#3-mode-2-wi-fi-non-hotspot-tournament-mode)
|
||
- [4. Auto-Discovery Mechanism](#4-auto-discovery-mechanism)
|
||
- [5. Multi-Court Safety & Conflict Prevention](#5-multi-court-safety--conflict-prevention)
|
||
- [6. ESP32 Code Examples](#6-esp32-code-examples)
|
||
- [7. Android .NET Server Code](#7-android-net-server-code)
|
||
- [8. Best Practices](#8-best-practices)
|
||
|
||
---
|
||
|
||
## 1. Overview
|
||
|
||
This guide details **two Wi-Fi connection modes** for the **Taekwondo Scoring System**:
|
||
|
||
| Mode | Use Case | Network Control |
|
||
|------|---------|----------------|
|
||
| **1. Hotspot-Based** | Clubs, single court | Tablet creates Wi-Fi |
|
||
| **2. Non-Hotspot (Client)** | Tournaments, multi-court | Tablet joins existing Wi-Fi |
|
||
|
||
Both modes use **auto-discovery** so devices (sensors, joysticks, TVs) can **find the server without IP input**.
|
||
|
||
Key goals:
|
||
- ✅ Zero-config for non-technical users
|
||
- ✅ Reliable auto-discovery
|
||
- ✅ Prevent cross-talk between courts
|
||
- ✅ Secure and scalable
|
||
|
||
---
|
||
|
||
## 2. Mode 1: Wi-Fi Hotspot-Based (Single Court)
|
||
|
||
Ideal for **training clubs** or **local events** with **one court**.
|
||
|
||
### 📶 Network Setup
|
||
- **Score Control Tablet** creates a Wi-Fi hotspot:
|
||
- **SSID**: `TKD_SCORING_COURT1`
|
||
- **Password**: `taekwondo123`
|
||
- **IP**: `192.168.4.1` (default hotspot IP)
|
||
- All devices connect to this network.
|
||
|
||
### ✅ Advantages
|
||
- No dependency on venue Wi-Fi
|
||
- Fixed server IP: `192.168.4.1`
|
||
- Fast, stable, low-latency
|
||
- Simple for staff: "Connect to TKD_SCORING_COURT1"
|
||
|
||
### 🔄 Auto-Discovery Flow
|
||
```
|
||
[ESP32 Device] → Connects to TKD_SCORING_COURT1
|
||
↓
|
||
Sends UDP broadcast: TKD_DISCOVER
|
||
↓
|
||
[Tablet] ← Responds: SERVER_IP:192.168.4.1
|
||
↓
|
||
Device connects to 192.168.4.1:5000
|
||
```
|
||
|
||
> Since the tablet is the **only router**, it's always the **first IP** (`192.168.4.1`).
|
||
|
||
---
|
||
|
||
## 3. Mode 2: Wi-Fi Non-Hotspot (Tournament Mode)
|
||
|
||
Used in **multi-court tournaments** where multiple score systems run **on the same venue Wi-Fi**.
|
||
|
||
### 📶 Network Setup
|
||
- **Score Control Tablet** connects to **existing Wi-Fi** (e.g., `Venue_Guest`)
|
||
- Gets IP via DHCP (e.g., `192.168.1.100`)
|
||
- Starts TCP server on Port `5000`
|
||
- Devices must **discover** the server IP
|
||
|
||
### ⚠️ Challenge
|
||
- Server IP is **not fixed**
|
||
- Devices must **find the correct server** among many
|
||
|
||
### ✅ Solution: **UDP Broadcast Discovery**
|
||
|
||
Devices send a broadcast to find any active **Taekwondo scoring server**.
|
||
|
||
---
|
||
|
||
## 4. Auto-Discovery Mechanism
|
||
|
||
To enable auto-discovery in **both modes**, we use **UDP broadcast**.
|
||
|
||
### 📡 Discovery Protocol
|
||
|
||
| Step | Message |
|
||
|------|--------|
|
||
| 1 | Device sends: `TKD_DISCOVER` |
|
||
| 2 | Server responds: `SERVER_IP:<ip>:<port>:<court_id>` |
|
||
| 3 | Device connects to `ip:port` |
|
||
|
||
#### Example Response
|
||
```
|
||
SERVER_IP:192.168.1.100:5000:COURT1
|
||
```
|
||
|
||
This allows devices to:
|
||
- Find the server
|
||
- Know which court they belong to
|
||
|
||
---
|
||
|
||
### 🔄 Discovery Flow (Non-Hotspot Mode)
|
||
|
||
```
|
||
[ESP32 Device] → Joins venue Wi-Fi
|
||
↓
|
||
Sends UDP broadcast: TKD_DISCOVER
|
||
↓
|
||
[Court 1 Server] ← Responds: SERVER_IP:192.168.1.100:5000:COURT1
|
||
[Court 2 Server] ← Responds: SERVER_IP:192.168.1.101:5000:COURT2
|
||
↓
|
||
[Device] → Uses SSID or config to choose COURT1 → connects
|
||
```
|
||
|
||
> Devices only connect to the server matching their **court ID**.
|
||
|
||
---
|
||
|
||
## 5. Multi-Court Safety & Conflict Prevention
|
||
|
||
To prevent **cross-talk** between courts:
|
||
|
||
### ✅ 1. Unique SSID per Court (Recommended)
|
||
- **Court 1**: `TKD_COURT1`
|
||
- **Court 2**: `TKD_COURT2`
|
||
- Devices are **pre-configured** to connect only to their court’s SSID
|
||
|
||
> ✅ **Best for tournaments** — no discovery conflicts
|
||
|
||
### ✅ 2. Court ID in Discovery Response
|
||
- Server includes `COURT1`, `COURT2` in response
|
||
- Device validates: only connect if `court_id` matches
|
||
|
||
### ✅ 3. MAC Address Whitelisting
|
||
- Each server has a **pre-loaded list** of allowed device MACs
|
||
- Blocks devices from other courts
|
||
|
||
### ✅ 4. Separate UDP Ports (Optional)
|
||
- Court 1 listens on UDP `5002`
|
||
- Court 2 listens on UDP `5003`
|
||
- Prevents broadcast overlap
|
||
|
||
---
|
||
|
||
### 🛡️ Safety Summary
|
||
|
||
| Risk | Solution |
|
||
|------|----------|
|
||
| Device connects to wrong court | Unique SSID or Court ID check |
|
||
| Two tablets respond to discovery | Use Court ID filtering |
|
||
| Unauthorized device connects | MAC whitelisting |
|
||
| IP conflict | Let DHCP handle it |
|
||
|
||
---
|
||
|
||
## 6. ESP32 Code Examples
|
||
|
||
### 📡 ESP32: Auto-Discovery & Connect (Arduino C++)
|
||
|
||
```cpp
|
||
#include <WiFi.h>
|
||
#include <WiFiUdp.h>
|
||
|
||
const char* ssid = "TKD_SCORING_COURT1"; // Change per court
|
||
const char* password = "taekwondo123";
|
||
const int udpPort = 5002;
|
||
const char* discoverMsg = "TKD_DISCOVER";
|
||
const char* expectedCourt = "COURT1";
|
||
|
||
WiFiUDP udp;
|
||
|
||
String getServerIP() {
|
||
udp.beginPacket("255.255.255.255", udpPort);
|
||
udp.print(discoverMsg);
|
||
udp.endPacket();
|
||
|
||
unsigned long timeout = millis() + 3000;
|
||
while (millis() < timeout) {
|
||
int len = udp.parsePacket();
|
||
if (len > 0) {
|
||
char response[64];
|
||
udp.read(response, len);
|
||
response[len] = '\0';
|
||
|
||
// Parse: SERVER_IP:192.168.1.100:5000:COURT1
|
||
String resp = String(response);
|
||
int ipStart = resp.indexOf("SERVER_IP:") + 10;
|
||
int courtStart = resp.lastIndexOf(":") + 1;
|
||
String courtId = resp.substring(courtStart);
|
||
|
||
if (courtId == expectedCourt) {
|
||
int portEnd = resp.lastIndexOf(":");
|
||
int ipEnd = resp.lastIndexOf(":", portEnd - 1);
|
||
return resp.substring(ipStart, ipEnd);
|
||
}
|
||
}
|
||
delay(100);
|
||
}
|
||
return "";
|
||
}
|
||
|
||
void setup() {
|
||
Serial.begin(115200);
|
||
WiFi.begin(ssid, password);
|
||
|
||
while (WiFi.status() != WL_CONNECTED) {
|
||
delay(500);
|
||
Serial.print(".");
|
||
}
|
||
|
||
udp.begin(udpPort + 1); // Use different port for listening
|
||
|
||
String serverIP = getServerIP();
|
||
if (serverIP != "") {
|
||
Serial.println("Found server: " + serverIP);
|
||
// Now connect to TCP server
|
||
WiFiClient client;
|
||
if (client.connect(serverIP.c_str(), 5000)) {
|
||
client.println("DEVICE:NAME=HOGU_RED,TYPE=SENSOR,MAC=" + WiFi.macAddress());
|
||
// Send data...
|
||
}
|
||
} else {
|
||
Serial.println("No server found!");
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 7. Android .NET Server Code
|
||
|
||
### 🖥️ Android: UDP Discovery Server (C# .NET MAUI)
|
||
|
||
```csharp
|
||
using System.Net;
|
||
using System.Net.Sockets;
|
||
using System.Text;
|
||
|
||
public class UdpDiscoveryServer
|
||
{
|
||
private UdpClient _udp;
|
||
private string _serverIp;
|
||
private string _courtId;
|
||
private int _tcpPort = 5000;
|
||
private int _udpPort = 5002;
|
||
|
||
public UdpDiscoveryServer(string courtId)
|
||
{
|
||
_courtId = courtId;
|
||
_serverIp = GetLocalIPAddress(); // e.g., 192.168.1.100
|
||
}
|
||
|
||
public void Start()
|
||
{
|
||
_udp = new UdpClient(_udpPort);
|
||
Task.Run(async () => {
|
||
while (true)
|
||
{
|
||
var remote = new IPEndPoint(IPAddress.Any, 0);
|
||
try
|
||
{
|
||
byte[] data = _udp.Receive(ref remote);
|
||
string request = Encoding.UTF8.GetString(data);
|
||
if (request.Trim() == "TKD_DISCOVER")
|
||
{
|
||
string response = $"SERVER_IP:{_serverIp}:{_tcpPort}:{_courtId}";
|
||
byte[] responseData = Encoding.UTF8.GetBytes(response);
|
||
_udp.Send(responseData, responseData.Length, remote);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
// Handle error
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
private string GetLocalIPAddress()
|
||
{
|
||
// Get Wi-Fi IP (simplified)
|
||
// In practice, use ConnectivityManager or WifiManager
|
||
return "192.168.4.1"; // Or get dynamically
|
||
}
|
||
}
|
||
```
|
||
|
||
### 🖥️ Android: TCP Server (Score Control App)
|
||
|
||
```csharp
|
||
using System.Net;
|
||
using System.Net.Sockets;
|
||
|
||
public class TcpServer
|
||
{
|
||
private TcpListener _listener;
|
||
private Dictionary<string, TcpClient> _clients = new();
|
||
|
||
public async Task Start(int port)
|
||
{
|
||
_listener = new TcpListener(IPAddress.Any, port);
|
||
_listener.Start();
|
||
|
||
while (true)
|
||
{
|
||
TcpClient client = await _listener.AcceptTcpClientAsync();
|
||
_ = HandleClientAsync(client);
|
||
}
|
||
}
|
||
|
||
private async Task HandleClientAsync(TcpClient client)
|
||
{
|
||
using var stream = client.GetStream();
|
||
using var reader = new StreamReader(stream);
|
||
|
||
string identity = await reader.ReadLineAsync();
|
||
if (identity?.StartsWith("DEVICE:") == true)
|
||
{
|
||
var parts = ParseIdentity(identity);
|
||
string mac = parts["MAC"];
|
||
string name = parts["NAME"];
|
||
|
||
// Validate MAC against whitelist
|
||
if (IsAllowed(mac))
|
||
{
|
||
_clients[mac] = client;
|
||
// Start reading data...
|
||
}
|
||
else
|
||
{
|
||
// Send DENIED and close
|
||
var writer = new StreamWriter(stream);
|
||
await writer.WriteLineAsync("DENIED");
|
||
await writer.FlushAsync();
|
||
client.Close();
|
||
}
|
||
}
|
||
}
|
||
|
||
private Dictionary<string, string> ParseIdentity(string line)
|
||
{
|
||
var result = new Dictionary<string, string>();
|
||
if (!line.StartsWith("DEVICE:")) return result;
|
||
var parts = line.Substring(7).Split(',');
|
||
foreach (var part in parts)
|
||
{
|
||
var kv = part.Split('=', 2);
|
||
if (kv.Length == 2) result[kv[0]] = kv[1];
|
||
}
|
||
return result;
|
||
}
|
||
|
||
private bool IsAllowed(string mac) => AllowedDevices.Contains(mac);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 8. Best Practices
|
||
|
||
| Practice | Why |
|
||
|--------|-----|
|
||
| **Use unique SSID per court** | Prevents discovery conflicts |
|
||
| **Pre-configure device Wi-Fi** | No user setup needed |
|
||
| **Include Court ID in discovery** | Safe multi-court operation |
|
||
| **Whitelist device MACs** | Prevents spoofing |
|
||
| **Use UDP broadcast on 5002+** | Avoids port conflicts |
|
||
| **Fixed hotspot IP (192.168.4.1)** | Simplifies client logic |
|
||
| **Reconnect logic on ESP32** | Handles Wi-Fi drops |
|
||
| **Log connection events** | Debugging and auditing | |