894 lines
27 KiB
Plaintext
894 lines
27 KiB
Plaintext
Perfect! Let's update the project to **TAKEONE Reflex** and add advanced color management for different game scenarios.
|
|
|
|
## ✅ Updated Code with Advanced Color Features
|
|
|
|
```cpp
|
|
#include <BLEDevice.h>
|
|
#include <BLEServer.h>
|
|
#include <BLEUtils.h>
|
|
#include <BLE2902.h>
|
|
#include <Adafruit_NeoPixel.h>
|
|
#include <Preferences.h>
|
|
|
|
// ==========================================
|
|
// ⚙️ USER CONFIGURATION (MODULAR)
|
|
// ==========================================
|
|
#define DEFAULT_DEVICE_NAME "TAKEONE_Reflex"
|
|
#define PROJECT_NAME "TAKEONE Reflex"
|
|
#define FIRMWARE_VERSION "1.0.0"
|
|
|
|
// --- Feature Toggles (Comment to disable) ---
|
|
#define FEATURE_PIEZO
|
|
#define FEATURE_BUTTON
|
|
#define FEATURE_ACCEL
|
|
|
|
// --- Pin Assignments (ESP32-C3 Super Mini) ---
|
|
#define LED_PIN 3
|
|
|
|
#ifdef FEATURE_PIEZO
|
|
#define PIEZO_PIN 4
|
|
#define PIEZO_THRESHOLD 1500
|
|
#endif
|
|
|
|
#ifdef FEATURE_BUTTON
|
|
#define BUTTON_PIN 5
|
|
#define BUTTON_THRESHOLD 100
|
|
#endif
|
|
|
|
#ifdef FEATURE_ACCEL
|
|
#define ACCEL_X_PIN 6
|
|
#define ACCEL_Y_PIN 7
|
|
#define ACCEL_Z_PIN 0
|
|
#define ACCEL_THRESHOLD 150
|
|
#endif
|
|
|
|
#define NUM_PIXELS 12
|
|
#define BRIGHTNESS 50
|
|
#define BREATHE_MAX 40
|
|
#define MAX_PRESETS 8 // Number of color presets
|
|
#define MAX_SEQUENCE_STEPS 16 // Max colors in sequence
|
|
// ==========================================
|
|
|
|
// --- BLE UUIDs ---
|
|
#define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
|
|
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"
|
|
#define DEVICE_INFO_SERVICE_UUID "180A"
|
|
#define MODEL_NUMBER_CHAR_UUID "2A24"
|
|
#define SERIAL_NUMBER_CHAR_UUID "2A25"
|
|
#define FIRMWARE_REV_CHAR_UUID "2A26"
|
|
#define MANUFACTURER_CHAR_UUID "2A29"
|
|
|
|
// --- Pod States ---
|
|
enum PodState {
|
|
STATE_BOOT,
|
|
STATE_WAITING,
|
|
STATE_CONNECTED,
|
|
STATE_ACTIVE,
|
|
STATE_SUCCESS,
|
|
STATE_SEQUENCE // For playing color sequences
|
|
};
|
|
|
|
// --- Color Structure ---
|
|
struct LEDColor {
|
|
uint8_t r, g, b;
|
|
};
|
|
|
|
// --- Default Colors ---
|
|
LEDColor colorWait = {0, 0, 255}; // Blue (breathing)
|
|
LEDColor colorConn = {0, 255, 255}; // Cyan
|
|
LEDColor colorActive = {255, 0, 0}; // Red
|
|
LEDColor colorSuccess = {0, 255, 0}; // Green
|
|
|
|
// --- Color Presets (saved in flash) ---
|
|
LEDColor colorPresets[MAX_PRESETS];
|
|
|
|
// --- Color Sequence ---
|
|
LEDColor colorSequence[MAX_SEQUENCE_STEPS];
|
|
uint8_t sequenceLength = 0;
|
|
uint8_t currentStep = 0;
|
|
unsigned long lastStepTime = 0;
|
|
unsigned long stepDuration = 500; // ms per step
|
|
|
|
// --- Globals ---
|
|
Preferences preferences;
|
|
char deviceName[16] = DEFAULT_DEVICE_NAME;
|
|
|
|
Adafruit_NeoPixel strip(NUM_PIXELS, LED_PIN, NEO_GRB + NEO_KHZ800);
|
|
BLEServer *pServer = NULL;
|
|
BLECharacteristic *pCharacteristic = NULL;
|
|
bool deviceConnected = false;
|
|
bool oldDeviceConnected = false;
|
|
|
|
PodState currentState = STATE_BOOT;
|
|
unsigned long stateStartTime = 0;
|
|
unsigned long lastAnimTime = 0;
|
|
int animStep = 0;
|
|
|
|
unsigned long startTime = 0;
|
|
unsigned long reactionTime = 0;
|
|
bool gameActive = false;
|
|
bool isTapped = false;
|
|
|
|
int baseX = 0, baseY = 0, baseZ = 0;
|
|
bool accelCalibrated = false;
|
|
|
|
// --- Helper: Create Color ---
|
|
uint32_t makeColor(LEDColor c) {
|
|
return strip.Color(c.r, c.g, c.b);
|
|
}
|
|
|
|
// --- Helper: Set All LEDs ---
|
|
void fillStrip(uint32_t color) {
|
|
for (int i = 0; i < NUM_PIXELS; i++) {
|
|
strip.setPixelColor(i, color);
|
|
}
|
|
strip.show();
|
|
}
|
|
|
|
// --- Helper: Parse Color from String "R,G,B" ---
|
|
LEDColor parseColor(String str) {
|
|
LEDColor c = {0, 0, 0};
|
|
int comma1 = str.indexOf(',');
|
|
int comma2 = str.lastIndexOf(',');
|
|
if (comma1 > 0 && comma2 > comma1) {
|
|
c.r = str.substring(0, comma1).toInt();
|
|
c.g = str.substring(comma1 + 1, comma2).toInt();
|
|
c.b = str.substring(comma2 + 1).toInt();
|
|
}
|
|
return c;
|
|
}
|
|
|
|
// --- Helper: Format Color to String ---
|
|
String formatColor(LEDColor c) {
|
|
return String(c.r) + "," + String(c.g) + "," + String(c.b);
|
|
}
|
|
|
|
// --- Save/Load Functions ---
|
|
void saveDeviceName(const char* name) {
|
|
preferences.begin("takeone", false);
|
|
preferences.putString("deviceName", name);
|
|
preferences.end();
|
|
strncpy(deviceName, name, 15);
|
|
deviceName[15] = '\0';
|
|
}
|
|
|
|
void loadDeviceName() {
|
|
preferences.begin("takeone", false);
|
|
String savedName = preferences.getString("deviceName", DEFAULT_DEVICE_NAME);
|
|
preferences.end();
|
|
strncpy(deviceName, savedName.c_str(), 15);
|
|
deviceName[15] = '\0';
|
|
}
|
|
|
|
void saveColorPreset(uint8_t index, LEDColor color) {
|
|
preferences.begin("takeone", false);
|
|
String key = "preset" + String(index);
|
|
String value = formatColor(color);
|
|
preferences.putString(key.c_str(), value);
|
|
preferences.end();
|
|
colorPresets[index] = color;
|
|
}
|
|
|
|
void loadColorPresets() {
|
|
preferences.begin("takeone", false);
|
|
for (int i = 0; i < MAX_PRESETS; i++) {
|
|
String key = "preset" + String(i);
|
|
String saved = preferences.getString(key.c_str(), "");
|
|
if (saved.length() > 0) {
|
|
colorPresets[i] = parseColor(saved);
|
|
} else {
|
|
// Default presets
|
|
switch(i) {
|
|
case 0: colorPresets[i] = {255, 0, 0}; break; // Red
|
|
case 1: colorPresets[i] = {0, 255, 0}; break; // Green
|
|
case 2: colorPresets[i] = {0, 0, 255}; break; // Blue
|
|
case 3: colorPresets[i] = {255, 255, 0}; break; // Yellow
|
|
case 4: colorPresets[i] = {255, 0, 255}; break; // Magenta
|
|
case 5: colorPresets[i] = {0, 255, 255}; break; // Cyan
|
|
case 6: colorPresets[i] = {255, 128, 0}; break; // Orange
|
|
case 7: colorPresets[i] = {128, 0, 255}; break; // Purple
|
|
}
|
|
}
|
|
}
|
|
preferences.end();
|
|
}
|
|
|
|
void saveSequence() {
|
|
preferences.begin("takeone", false);
|
|
String seqStr = "";
|
|
for (int i = 0; i < sequenceLength; i++) {
|
|
if (i > 0) seqStr += ";";
|
|
seqStr += formatColor(colorSequence[i]);
|
|
}
|
|
preferences.putString("sequence", seqStr);
|
|
preferences.putUInt("seqDuration", stepDuration);
|
|
preferences.end();
|
|
}
|
|
|
|
void loadSequence() {
|
|
preferences.begin("takeone", false);
|
|
String saved = preferences.getString("sequence", "");
|
|
stepDuration = preferences.getUInt("seqDuration", 500);
|
|
|
|
sequenceLength = 0;
|
|
if (saved.length() > 0) {
|
|
int startIndex = 0;
|
|
while (startIndex < saved.length() && sequenceLength < MAX_SEQUENCE_STEPS) {
|
|
int endIndex = saved.indexOf(';', startIndex);
|
|
if (endIndex == -1) endIndex = saved.length();
|
|
|
|
String colorStr = saved.substring(startIndex, endIndex);
|
|
colorSequence[sequenceLength++] = parseColor(colorStr);
|
|
startIndex = endIndex + 1;
|
|
}
|
|
}
|
|
preferences.end();
|
|
}
|
|
|
|
// --- Play Next Sequence Step ---
|
|
void playNextSequenceStep() {
|
|
if (sequenceLength == 0) return;
|
|
|
|
fillStrip(makeColor(colorSequence[currentStep]));
|
|
currentStep = (currentStep + 1) % sequenceLength;
|
|
lastStepTime = millis();
|
|
}
|
|
|
|
// --- BLE Callbacks ---
|
|
class MyServerCallbacks: public BLEServerCallbacks {
|
|
void onConnect(BLEServer* pServer) {
|
|
deviceConnected = true;
|
|
currentState = STATE_CONNECTED;
|
|
stateStartTime = millis();
|
|
Serial.println("Device Connected");
|
|
}
|
|
void onDisconnect(BLEServer* pServer) {
|
|
deviceConnected = false;
|
|
currentState = STATE_WAITING;
|
|
stateStartTime = millis();
|
|
Serial.println("Device Disconnected");
|
|
BLEDevice::startAdvertising();
|
|
}
|
|
};
|
|
|
|
class MyCallbacks: public BLECharacteristicCallbacks {
|
|
void onWrite(BLECharacteristic *pCharacteristic) {
|
|
String value = pCharacteristic->getValue();
|
|
value.trim();
|
|
|
|
Serial.print("Received: [");
|
|
Serial.print(value);
|
|
Serial.println("]");
|
|
|
|
// --- Device Name Commands ---
|
|
if (value.startsWith("SET_DEVICE_NAME:")) {
|
|
String newName = value.substring(16);
|
|
newName.trim();
|
|
|
|
if (newName.length() > 0 && newName.length() <= 15) {
|
|
saveDeviceName(newName.c_str());
|
|
BLEDevice::deinit(true);
|
|
delay(100);
|
|
BLEDevice::init(deviceName);
|
|
|
|
BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
|
|
pAdvertising->addServiceUUID(SERVICE_UUID);
|
|
pAdvertising->setScanResponse(true);
|
|
BLEDevice::startAdvertising();
|
|
|
|
pCharacteristic->setValue("OK:DEVICE_NAME:" + String(deviceName));
|
|
pCharacteristic->notify();
|
|
} else {
|
|
pCharacteristic->setValue("ERROR:NAME_INVALID");
|
|
pCharacteristic->notify();
|
|
}
|
|
}
|
|
else if (value == "GET_DEVICE_NAME") {
|
|
pCharacteristic->setValue("DEVICE_NAME:" + String(deviceName));
|
|
pCharacteristic->notify();
|
|
}
|
|
// --- Color Preset Commands ---
|
|
else if (value.startsWith("SAVE_PRESET:")) {
|
|
// Format: SAVE_PRESET:n:R,G,B
|
|
int firstColon = value.indexOf(':');
|
|
int secondColon = value.indexOf(':', firstColon + 1);
|
|
if (firstColon > 0 && secondColon > firstColon) {
|
|
uint8_t index = value.substring(firstColon + 1, secondColon).toInt();
|
|
if (index < MAX_PRESETS) {
|
|
LEDColor c = parseColor(value.substring(secondColon + 1));
|
|
saveColorPreset(index, c);
|
|
pCharacteristic->setValue("OK:PRESET_SAVED:" + String(index));
|
|
pCharacteristic->notify();
|
|
} else {
|
|
pCharacteristic->setValue("ERROR:PRESET_INDEX");
|
|
pCharacteristic->notify();
|
|
}
|
|
}
|
|
}
|
|
else if (value.startsWith("LOAD_PRESET:")) {
|
|
// Format: LOAD_PRESET:n
|
|
int colon = value.indexOf(':');
|
|
if (colon > 0) {
|
|
uint8_t index = value.substring(colon + 1).toInt();
|
|
if (index < MAX_PRESETS) {
|
|
fillStrip(makeColor(colorPresets[index]));
|
|
pCharacteristic->setValue("OK:PRESET_LOADED:" + String(index));
|
|
pCharacteristic->notify();
|
|
} else {
|
|
pCharacteristic->setValue("ERROR:PRESET_INDEX");
|
|
pCharacteristic->notify();
|
|
}
|
|
}
|
|
}
|
|
else if (value == "GET_PRESETS") {
|
|
String response = "PRESETS|";
|
|
for (int i = 0; i < MAX_PRESETS; i++) {
|
|
if (i > 0) response += "|";
|
|
response += String(i) + ":" + formatColor(colorPresets[i]);
|
|
}
|
|
pCharacteristic->setValue(response);
|
|
pCharacteristic->notify();
|
|
}
|
|
// --- Color Sequence Commands ---
|
|
else if (value.startsWith("SET_SEQUENCE:")) {
|
|
// Format: SET_SEQUENCE:R1,G1,B1;R2,G2,B2;R3,G3,B3
|
|
String seqData = value.substring(13);
|
|
sequenceLength = 0;
|
|
|
|
int startIndex = 0;
|
|
while (startIndex < seqData.length() && sequenceLength < MAX_SEQUENCE_STEPS) {
|
|
int endIndex = seqData.indexOf(';', startIndex);
|
|
if (endIndex == -1) endIndex = seqData.length();
|
|
|
|
String colorStr = seqData.substring(startIndex, endIndex);
|
|
colorSequence[sequenceLength++] = parseColor(colorStr);
|
|
startIndex = endIndex + 1;
|
|
}
|
|
|
|
saveSequence();
|
|
pCharacteristic->setValue("OK:SEQUENCE_SET:" + String(sequenceLength));
|
|
pCharacteristic->notify();
|
|
}
|
|
else if (value.startsWith("SET_STEP_DURATION:")) {
|
|
// Format: SET_STEP_DURATION:500 (milliseconds)
|
|
int colon = value.indexOf(':');
|
|
if (colon > 0) {
|
|
stepDuration = value.substring(colon + 1).toInt();
|
|
saveSequence();
|
|
pCharacteristic->setValue("OK:DURATION:" + String(stepDuration));
|
|
pCharacteristic->notify();
|
|
}
|
|
}
|
|
else if (value == "PLAY_SEQUENCE") {
|
|
if (sequenceLength > 0) {
|
|
currentState = STATE_SEQUENCE;
|
|
currentStep = 0;
|
|
playNextSequenceStep();
|
|
pCharacteristic->setValue("OK:SEQUENCE_PLAYING");
|
|
pCharacteristic->notify();
|
|
} else {
|
|
pCharacteristic->setValue("ERROR:NO_SEQUENCE");
|
|
pCharacteristic->notify();
|
|
}
|
|
}
|
|
else if (value == "STOP_SEQUENCE") {
|
|
currentState = STATE_CONNECTED;
|
|
fillStrip(makeColor(colorConn));
|
|
pCharacteristic->setValue("OK:SEQUENCE_STOPPED");
|
|
pCharacteristic->notify();
|
|
}
|
|
// --- Standard Color Commands ---
|
|
else if (value.startsWith("COLOR_WAIT:")) {
|
|
colorWait = parseColor(value.substring(11));
|
|
pCharacteristic->setValue("OK:COLOR_WAIT:" + formatColor(colorWait));
|
|
pCharacteristic->notify();
|
|
}
|
|
else if (value.startsWith("COLOR_CONN:")) {
|
|
colorConn = parseColor(value.substring(11));
|
|
pCharacteristic->setValue("OK:COLOR_CONN:" + formatColor(colorConn));
|
|
pCharacteristic->notify();
|
|
}
|
|
else if (value.startsWith("COLOR_ACTIVE:")) {
|
|
colorActive = parseColor(value.substring(13));
|
|
pCharacteristic->setValue("OK:COLOR_ACTIVE:" + formatColor(colorActive));
|
|
pCharacteristic->notify();
|
|
}
|
|
else if (value.startsWith("COLOR_SUCCESS:")) {
|
|
colorSuccess = parseColor(value.substring(14));
|
|
pCharacteristic->setValue("OK:COLOR_SUCCESS:" + formatColor(colorSuccess));
|
|
pCharacteristic->notify();
|
|
}
|
|
else if (value == "GET_COLORS") {
|
|
String response = "COLORS|WAIT:" + formatColor(colorWait) +
|
|
"|CONN:" + formatColor(colorConn) +
|
|
"|ACTIVE:" + formatColor(colorActive) +
|
|
"|SUCCESS:" + formatColor(colorSuccess);
|
|
pCharacteristic->setValue(response);
|
|
pCharacteristic->notify();
|
|
}
|
|
else if (value == "RESET_COLORS") {
|
|
colorWait = {0, 0, 255};
|
|
colorConn = {0, 255, 255};
|
|
colorActive = {255, 0, 0};
|
|
colorSuccess = {0, 255, 0};
|
|
pCharacteristic->setValue("OK:COLORS_RESET");
|
|
pCharacteristic->notify();
|
|
}
|
|
// --- Game Commands ---
|
|
else if (value == "START") {
|
|
currentState = STATE_ACTIVE;
|
|
fillStrip(makeColor(colorActive));
|
|
startTime = micros();
|
|
gameActive = true;
|
|
isTapped = false;
|
|
accelCalibrated = false;
|
|
pCharacteristic->setValue("OK:STARTED");
|
|
pCharacteristic->notify();
|
|
Serial.println("Game Started");
|
|
}
|
|
else if (value == "RESET") {
|
|
currentState = STATE_CONNECTED;
|
|
fillStrip(makeColor(colorConn));
|
|
gameActive = false;
|
|
pCharacteristic->setValue("OK:RESET");
|
|
pCharacteristic->notify();
|
|
Serial.println("Game Reset");
|
|
}
|
|
// --- Device Info ---
|
|
else if (value == "GET_DEVICE_INFO") {
|
|
String response = "INFO|NAME:" + String(deviceName) +
|
|
"|MODEL:TAKEONE-Reflex" +
|
|
"|FW:" + String(FIRMWARE_VERSION) +
|
|
"|MANUFACTURER:DIY" +
|
|
"|SENSORS:" +
|
|
String(defined(FEATURE_PIEZO) ? "P" : "-") +
|
|
String(defined(FEATURE_BUTTON) ? "B" : "-") +
|
|
String(defined(FEATURE_ACCEL) ? "A" : "-");
|
|
pCharacteristic->setValue(response);
|
|
pCharacteristic->notify();
|
|
}
|
|
else if (value == "FACTORY_RESET") {
|
|
preferences.begin("takeone", false);
|
|
preferences.clear();
|
|
preferences.end();
|
|
strncpy(deviceName, DEFAULT_DEVICE_NAME, 15);
|
|
loadColorPresets();
|
|
|
|
BLEDevice::deinit(true);
|
|
delay(100);
|
|
BLEDevice::init(deviceName);
|
|
|
|
BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
|
|
pAdvertising->addServiceUUID(SERVICE_UUID);
|
|
pAdvertising->setScanResponse(true);
|
|
BLEDevice::startAdvertising();
|
|
|
|
pCharacteristic->setValue("OK:FACTORY_RESET");
|
|
pCharacteristic->notify();
|
|
Serial.println("Factory reset complete");
|
|
}
|
|
else {
|
|
pCharacteristic->setValue("ERROR:UNKNOWN_COMMAND");
|
|
pCharacteristic->notify();
|
|
}
|
|
}
|
|
};
|
|
|
|
// --- Calibrate Accelerometer ---
|
|
void calibrateAccel() {
|
|
#ifdef FEATURE_ACCEL
|
|
if (!accelCalibrated) {
|
|
baseX = analogRead(ACCEL_X_PIN);
|
|
baseY = analogRead(ACCEL_Y_PIN);
|
|
baseZ = analogRead(ACCEL_Z_PIN);
|
|
accelCalibrated = true;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
// --- Check All Enabled Triggers ---
|
|
void checkTriggers() {
|
|
if (!gameActive || !deviceConnected || isTapped) return;
|
|
|
|
bool triggered = false;
|
|
|
|
#ifdef FEATURE_PIEZO
|
|
int piezoValue = analogRead(PIEZO_PIN);
|
|
if (piezoValue > PIEZO_THRESHOLD) {
|
|
triggered = true;
|
|
}
|
|
#endif
|
|
|
|
#ifdef FEATURE_BUTTON
|
|
#if !defined(FEATURE_PIEZO) || (BUTTON_PIN != PIEZO_PIN)
|
|
if (digitalRead(BUTTON_PIN) == LOW) {
|
|
triggered = true;
|
|
}
|
|
#endif
|
|
#endif
|
|
|
|
#ifdef FEATURE_ACCEL
|
|
if (accelCalibrated && !triggered) {
|
|
int curX = analogRead(ACCEL_X_PIN);
|
|
int curY = analogRead(ACCEL_Y_PIN);
|
|
int curZ = analogRead(ACCEL_Z_PIN);
|
|
|
|
int diffX = abs(curX - baseX);
|
|
int diffY = abs(curY - baseY);
|
|
int diffZ = abs(curZ - baseZ);
|
|
|
|
if (diffX > ACCEL_THRESHOLD || diffY > ACCEL_THRESHOLD || diffZ > ACCEL_THRESHOLD) {
|
|
triggered = true;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
if (triggered) {
|
|
isTapped = true;
|
|
unsigned long endTime = micros();
|
|
reactionTime = (endTime - startTime) / 1000;
|
|
|
|
String result = "RESULT:" + String(reactionTime) + "ms";
|
|
pCharacteristic->setValue(result);
|
|
pCharacteristic->notify();
|
|
|
|
currentState = STATE_SUCCESS;
|
|
stateStartTime = millis();
|
|
fillStrip(makeColor(colorSuccess));
|
|
|
|
gameActive = false;
|
|
}
|
|
}
|
|
|
|
// --- Reset Trigger After Settle ---
|
|
void resetTrigger() {
|
|
if (isTapped) {
|
|
bool settled = false;
|
|
|
|
#ifdef FEATURE_PIEZO
|
|
int piezoValue = analogRead(PIEZO_PIN);
|
|
if (piezoValue < (PIEZO_THRESHOLD / 2)) settled = true;
|
|
#elif defined(FEATURE_BUTTON)
|
|
if (digitalRead(BUTTON_PIN) == HIGH) settled = true;
|
|
#else
|
|
if (millis() - stateStartTime > 2000) settled = true;
|
|
#endif
|
|
|
|
if (settled) {
|
|
isTapped = false;
|
|
accelCalibrated = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Smart Animation Engine ---
|
|
void handleAnimations() {
|
|
unsigned long now = millis();
|
|
|
|
if (currentState == STATE_BOOT) {
|
|
if (now - stateStartTime < 2000) {
|
|
if (now - lastAnimTime > 50) {
|
|
for (int i = 0; i < NUM_PIXELS; i++) {
|
|
uint32_t color = strip.ColorHSV(i * 255 / NUM_PIXELS + animStep * 10, 255, BRIGHTNESS);
|
|
strip.setPixelColor(i, color);
|
|
}
|
|
strip.show();
|
|
animStep++;
|
|
lastAnimTime = now;
|
|
}
|
|
} else {
|
|
currentState = STATE_WAITING;
|
|
stateStartTime = now;
|
|
fillStrip(0);
|
|
}
|
|
}
|
|
|
|
else if (currentState == STATE_WAITING) {
|
|
if (now - lastAnimTime > 10) {
|
|
float breath = sin(now / 800.0);
|
|
float normalized = (breath + 1.0) / 2.0;
|
|
float gammaCorrected = pow(normalized, 2.2);
|
|
int brightness = (int)(gammaCorrected * BREATHE_MAX);
|
|
|
|
uint32_t color = strip.Color(colorWait.r * brightness / BREATHE_MAX,
|
|
colorWait.g * brightness / BREATHE_MAX,
|
|
colorWait.b * brightness / BREATHE_MAX);
|
|
fillStrip(color);
|
|
lastAnimTime = now;
|
|
}
|
|
}
|
|
|
|
else if (currentState == STATE_CONNECTED) {
|
|
if (now - lastAnimTime > 500) {
|
|
fillStrip(makeColor(colorConn));
|
|
lastAnimTime = now;
|
|
}
|
|
}
|
|
|
|
else if (currentState == STATE_ACTIVE) {
|
|
if (now - lastAnimTime > 500) {
|
|
fillStrip(makeColor(colorActive));
|
|
lastAnimTime = now;
|
|
}
|
|
}
|
|
|
|
else if (currentState == STATE_SEQUENCE) {
|
|
if (now - lastStepTime >= stepDuration) {
|
|
playNextSequenceStep();
|
|
}
|
|
}
|
|
|
|
else if (currentState == STATE_SUCCESS) {
|
|
if (now - stateStartTime > 2000) {
|
|
currentState = STATE_CONNECTED;
|
|
fillStrip(makeColor(colorConn));
|
|
}
|
|
}
|
|
}
|
|
|
|
void setup() {
|
|
Serial.begin(115200);
|
|
while (!Serial);
|
|
|
|
loadDeviceName();
|
|
loadColorPresets();
|
|
loadSequence();
|
|
|
|
Serial.println("================================");
|
|
Serial.println(" " + String(PROJECT_NAME));
|
|
Serial.println(" Firmware v" + String(FIRMWARE_VERSION));
|
|
Serial.println("================================");
|
|
Serial.print("Device Name: ");
|
|
Serial.println(deviceName);
|
|
Serial.print("Piezo: "); Serial.println(defined(FEATURE_PIEZO) ? "ON" : "OFF");
|
|
Serial.print("Button: "); Serial.println(defined(FEATURE_BUTTON) ? "ON" : "OFF");
|
|
Serial.print("Accel: "); Serial.println(defined(FEATURE_ACCEL) ? "ON" : "OFF");
|
|
Serial.println("================================");
|
|
|
|
pinMode(LED_PIN, OUTPUT);
|
|
|
|
#ifdef FEATURE_PIEZO
|
|
pinMode(PIEZO_PIN, INPUT);
|
|
#endif
|
|
|
|
#ifdef FEATURE_BUTTON
|
|
pinMode(BUTTON_PIN, INPUT_PULLUP);
|
|
#endif
|
|
|
|
#ifdef FEATURE_ACCEL
|
|
pinMode(ACCEL_X_PIN, INPUT);
|
|
pinMode(ACCEL_Y_PIN, INPUT);
|
|
pinMode(ACCEL_Z_PIN, INPUT);
|
|
#endif
|
|
|
|
analogReadResolution(12);
|
|
|
|
strip.begin();
|
|
strip.setBrightness(BRIGHTNESS);
|
|
strip.show();
|
|
fillStrip(0);
|
|
|
|
currentState = STATE_BOOT;
|
|
stateStartTime = millis();
|
|
|
|
BLEDevice::init(deviceName);
|
|
pServer = BLEDevice::createServer();
|
|
pServer->setCallbacks(new MyServerCallbacks());
|
|
|
|
BLEService *pService = pServer->createService(SERVICE_UUID);
|
|
pCharacteristic = pService->createCharacteristic(
|
|
CHARACTERISTIC_UUID,
|
|
BLECharacteristic::PROPERTY_READ |
|
|
BLECharacteristic::PROPERTY_WRITE |
|
|
BLECharacteristic::PROPERTY_NOTIFY
|
|
);
|
|
|
|
pCharacteristic->setCallbacks(new MyCallbacks());
|
|
pCharacteristic->addDescriptor(new BLE2902());
|
|
pService->start();
|
|
|
|
BLEService *pDeviceInfoService = pServer->createService(DEVICE_INFO_SERVICE_UUID);
|
|
|
|
BLECharacteristic *pModelNumberChar = pDeviceInfoService->createCharacteristic(
|
|
MODEL_NUMBER_CHAR_UUID, BLECharacteristic::PROPERTY_READ);
|
|
pModelNumberChar->setValue("TAKEONE-Reflex");
|
|
|
|
BLECharacteristic *pSerialNumberChar = pDeviceInfoService->createCharacteristic(
|
|
SERIAL_NUMBER_CHAR_UUID, BLECharacteristic::PROPERTY_READ);
|
|
uint8_t baseMac[6];
|
|
esp_read_mac(baseMac, ESP_MAC_WIFI_STA);
|
|
char macStr[18];
|
|
snprintf(macStr, sizeof(macStr), "%02X:%02X:%02X:%02X:%02X:%02X",
|
|
baseMac[0], baseMac[1], baseMac[2], baseMac[3], baseMac[4], baseMac[5]);
|
|
pSerialNumberChar->setValue(macStr);
|
|
|
|
BLECharacteristic *pFirmwareRevChar = pDeviceInfoService->createCharacteristic(
|
|
FIRMWARE_REV_CHAR_UUID, BLECharacteristic::PROPERTY_READ);
|
|
pFirmwareRevChar->setValue(FIRMWARE_VERSION);
|
|
|
|
BLECharacteristic *pManufacturerChar = pDeviceInfoService->createCharacteristic(
|
|
MANUFACTURER_CHAR_UUID, BLECharacteristic::PROPERTY_READ);
|
|
pManufacturerChar->setValue("DIY Reflex Pod");
|
|
|
|
pDeviceInfoService->start();
|
|
|
|
BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
|
|
pAdvertising->addServiceUUID(SERVICE_UUID);
|
|
pAdvertising->addServiceUUID(DEVICE_INFO_SERVICE_UUID);
|
|
pAdvertising->setScanResponse(true);
|
|
pAdvertising->setMinPreferred(0x06);
|
|
pAdvertising->setMinPreferred(0x12);
|
|
BLEDevice::startAdvertising();
|
|
|
|
Serial.println("System Ready - Waiting for connection...");
|
|
Serial.print("Advertising as: ");
|
|
Serial.println(deviceName);
|
|
}
|
|
|
|
void loop() {
|
|
if (!deviceConnected && oldDeviceConnected) {
|
|
delay(500);
|
|
pServer->startAdvertising();
|
|
oldDeviceConnected = deviceConnected;
|
|
}
|
|
if (deviceConnected && !oldDeviceConnected) {
|
|
oldDeviceConnected = deviceConnected;
|
|
}
|
|
|
|
handleAnimations();
|
|
|
|
if (gameActive && !accelCalibrated) {
|
|
calibrateAccel();
|
|
}
|
|
|
|
checkTriggers();
|
|
resetTrigger();
|
|
|
|
delay(10);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 📱 Complete BLE Command Reference
|
|
|
|
### **Device Management**
|
|
| Command | Description | Response |
|
|
| :--- | :--- | :--- |
|
|
| `SET_DEVICE_NAME:NewName` | Change device name (max 15 chars) | `OK:DEVICE_NAME:NewName` |
|
|
| `GET_DEVICE_NAME` | Get current name | `DEVICE_NAME:TAKEONE_Reflex` |
|
|
| `GET_DEVICE_INFO` | Get full device info | `INFO\|NAME:...\|MODEL:...\|...` |
|
|
| `FACTORY_RESET` | Reset all settings | `OK:FACTORY_RESET` |
|
|
|
|
### **Standard Colors**
|
|
| Command | Description | Example |
|
|
| :--- | :--- | :--- |
|
|
| `COLOR_WAIT:R,G,B` | Waiting/breathing color | `COLOR_WAIT:0,0,255` |
|
|
| `COLOR_CONN:R,G,B` | Connected state color | `COLOR_CONN:0,255,255` |
|
|
| `COLOR_ACTIVE:R,G,B` | Game active color | `COLOR_ACTIVE:255,0,0` |
|
|
| `COLOR_SUCCESS:R,G,B` | Success/triggered color | `COLOR_SUCCESS:0,255,0` |
|
|
| `GET_COLORS` | Get all colors | `COLORS\|WAIT:...\|CONN:...\|...` |
|
|
| `RESET_COLORS` | Reset to defaults | `OK:COLORS_RESET` |
|
|
|
|
### **Color Presets (NEW!)**
|
|
Save and recall up to **8 color presets** for different game scenarios:
|
|
|
|
| Command | Description | Example |
|
|
| :--- | :--- | :--- |
|
|
| `SAVE_PRESET:n:R,G,B` | Save color to preset slot n (0-7) | `SAVE_PRESET:3:255,128,0` |
|
|
| `LOAD_PRESET:n` | Load and apply preset n | `LOAD_PRESET:3` |
|
|
| `GET_PRESETS` | Get all saved presets | `PRESETS\|0:255,0,0\|1:0,255,0\|...` |
|
|
|
|
**Use Case**:
|
|
- Preset 0: Red (stop/slap)
|
|
- Preset 1: Green (go/jump)
|
|
- Preset 2: Blue (squat)
|
|
- Preset 3: Yellow (push-up)
|
|
- etc.
|
|
|
|
### **Color Sequences (NEW!)**
|
|
Create automatic color patterns for reaction drills:
|
|
|
|
| Command | Description | Example |
|
|
| :--- | :--- | :--- |
|
|
| `SET_SEQUENCE:R1,G1,B1;R2,G2,B2;...` | Set sequence (up to 16 colors) | `SET_SEQUENCE:255,0,0;0,255,0;0,0,255` |
|
|
| `SET_STEP_DURATION:ms` | Set duration per step | `SET_STEP_DURATION:1000` |
|
|
| `PLAY_SEQUENCE` | Start playing sequence | `OK:SEQUENCE_PLAYING` |
|
|
| `STOP_SEQUENCE` | Stop sequence | `OK:SEQUENCE_STOPPED` |
|
|
|
|
**Use Case**:
|
|
- Red → Green → Blue cycle for "follow the light" drills
|
|
- Random color patterns for cognitive training
|
|
- Timed sequences for interval training
|
|
|
|
### **Game Control**
|
|
| Command | Description | Response |
|
|
| :--- | :--- | :--- |
|
|
| `START` | Start reaction test | `OK:STARTED` |
|
|
| `RESET` | Stop game | `OK:RESET` |
|
|
|
|
---
|
|
|
|
## 🎮 Example Training Scenarios
|
|
|
|
### **Scenario 1: Color-Coded Actions**
|
|
```
|
|
# Setup different colors for different actions
|
|
SAVE_PRESET:0:255,0,0 # Red = Slap
|
|
SAVE_PRESET:1:0,255,0 # Green = Jump
|
|
SAVE_PRESET:2:0,0,255 # Blue = Squat
|
|
SAVE_PRESET:3:255,255,0 # Yellow = Push-up
|
|
|
|
# During training, coach sends:
|
|
LOAD_PRESET:0 # Athlete sees RED → Slaps pod
|
|
LOAD_PRESET:1 # Athlete sees GREEN → Jumps
|
|
LOAD_PRESET:2 # Athlete sees BLUE → Squats
|
|
```
|
|
|
|
### **Scenario 2: Pattern Recognition**
|
|
```
|
|
# Create a sequence
|
|
SET_SEQUENCE:255,0,0;0,255,0;0,0,255;255,255,0
|
|
SET_STEP_DURATION:800
|
|
PLAY_SEQUENCE
|
|
|
|
# Pod cycles: Red → Green → Blue → Yellow → Red...
|
|
# Athlete must perform action matching each color
|
|
```
|
|
|
|
### **Scenario 3: Reaction Drill**
|
|
```
|
|
# Set custom active color (purple)
|
|
COLOR_ACTIVE:255,0,255
|
|
|
|
# Start game
|
|
START
|
|
# Pod turns PURPLE
|
|
# Athlete reacts
|
|
# Result: RESULT:245ms
|
|
```
|
|
|
|
---
|
|
|
|
## 📊 Default Color Presets
|
|
|
|
| Slot | Color | RGB Value | Use Case |
|
|
| :--- | :--- | :--- | :--- |
|
|
| 0 | Red | 255,0,0 | Stop/Slap |
|
|
| 1 | Green | 0,255,0 | Go/Jump |
|
|
| 2 | Blue | 0,0,255 | Squat/Hold |
|
|
| 3 | Yellow | 255,255,0 | Push-up |
|
|
| 4 | Magenta | 255,0,255 | Burpee |
|
|
| 5 | Cyan | 0,255,255 | Rest |
|
|
| 6 | Orange | 255,128,0 | Lunge |
|
|
| 7 | Purple | 128,0,255 | Plank |
|
|
|
|
---
|
|
|
|
## 🎯 Quick Start Guide
|
|
|
|
1. **Connect** to `TAKEONE_Reflex`
|
|
2. **Enable notifications** on characteristic
|
|
3. **Test basic function**:
|
|
```
|
|
START
|
|
[Press button]
|
|
→ RESULT:XXXms
|
|
```
|
|
4. **Set custom colors**:
|
|
```
|
|
SAVE_PRESET:0:255,0,0
|
|
SAVE_PRESET:1:0,255,0
|
|
LOAD_PRESET:0 → Red lights up
|
|
LOAD_PRESET:1 → Green lights up
|
|
```
|
|
5. **Create sequence**:
|
|
```
|
|
SET_SEQUENCE:255,0,0;0,255,0;0,0,255
|
|
SET_STEP_DURATION:1000
|
|
PLAY_SEQUENCE
|
|
```
|
|
|
|
---
|
|
|
|
This makes **TAKEONE Reflex** a professional-grade training tool with unlimited color combinations for different exercises! 🎨💪 |