diff --git a/arduino/Krabicka_hexagon.3mf b/arduino/Krabicka_hexagon.3mf new file mode 100644 index 0000000..c2ee7c1 Binary files /dev/null and b/arduino/Krabicka_hexagon.3mf differ diff --git a/arduino/Krabicka_hexagon.FCStd b/arduino/Krabicka_hexagon.FCStd new file mode 100644 index 0000000..032f341 Binary files /dev/null and b/arduino/Krabicka_hexagon.FCStd differ diff --git a/arduino/Krabicka_hexagon.png b/arduino/Krabicka_hexagon.png new file mode 100644 index 0000000..965f717 Binary files /dev/null and b/arduino/Krabicka_hexagon.png differ diff --git a/arduino/README.md b/arduino/README.md new file mode 100644 index 0000000..0cc3dd2 --- /dev/null +++ b/arduino/README.md @@ -0,0 +1,50 @@ +# Nexus Game Controller + +A game controller using a Seeed Studio XIAO nRF52840 Sense board that emulates a Bluetooth keyboard. + +## Hardware +- [Seeed Studio XIAO nRF52840 Sense](#https://www.seeedstudio.com/Seeed-XIAO-BLE-Sense-nRF52840-p-5253.html) +- 9 [push buttons](https://rpishop.cz/komponenty/6128-pimoroni-cerne-arkadove-tlacitko.html) connected to pins D1-D6 and D8-D10. + +## Features +- **Bluetooth LE Keyboard:** Connects to any computer or mobile device as a standard keyboard. +- **Multi-Event Buttons:** Each of the 9 buttons supports three types of interactions: + - Single Click + - Double Click + - Long Press +- **Character Mapping:** Each button event sends a unique character. +- **Serial Debugging:** Outputs button events and Bluetooth connection status to the serial monitor. + +## Button Mappings + +| Button | Single Click | Double Click | Long Press | +|--------|--------------|--------------|------------| +| D1 | `a` | `b` | `c` | +| D2 | `d` | `e` | `f` | +| D3 | `g` | `h` | `i` | +| D4 | `j` | `k` | `l` | +| D5 | `m` | `n` | `o` | +| D6 | `p` | `q` | `r` | +| D8 | `s` | `t` | `u` | +| D9 | `v` | `w` | `x` | +| D10 | `y` | `z` | `1` | + +## How to Use + +1. **Setup Arduino IDE:** + - Install the "Seeed nRF52 Boards" package. + - Select "Seeed XIAO BLE Sense - nRF52840" as the board. + - Install the "Adafruit Bluefruit nRF52" library. +2. **Upload:** Compile and upload the [sketch](/arduino/sketch.ino) to your XIAO board. +3. **Connect:** Scan for Bluetooth devices on your computer or mobile device and connect to "Nexus Game Controller". +4. **Play:** Press the buttons to send keystrokes. Open the Serial Monitor at 115200 baud to see debug information. + +## 3D Print + +![Hexagon](/arduino/Krabicka_hexagon.png) + +You can import the [3mf file](/arduino/Krabicka_hexagon.3mf) to your slicer for printing. + +## Customize your design + +Open the [3D model](/arduino/Krabicka_hexagon.FCStd) in FreeCAD to model your changes. \ No newline at end of file diff --git a/arduino/sketch.ino b/arduino/sketch.ino new file mode 100644 index 0000000..4683596 --- /dev/null +++ b/arduino/sketch.ino @@ -0,0 +1,178 @@ +#include +#include + +// Pin definitions for the 9 buttons +const int buttonPins[] = {1, 2, 3, 4, 5, 6, 8, 9, 10}; +const int numButtons = sizeof(buttonPins) / sizeof(buttonPins[0]); + +// Button state tracking +struct Button { + int pin; + bool lastState; + unsigned long lastDebounceTime; + unsigned long pressTime; + int clickCount; + bool isPressed; + bool longPressHandled; +}; + +Button buttons[numButtons]; + +// Debounce and timing constants +const unsigned long debounceDelay = 50; +const unsigned long doubleClickDelay = 400; +const unsigned long longPressDelay = 1000; + +// BLE Keyboard object +BLEDis bledis; +BLEHidAdafruit blehid; + +// Character mapping for each event +// 9 buttons * 3 events/button = 27 characters +const char eventChars[numButtons][3] = { + {'a', 'b', 'c'}, // Button D1: single, double, long + {'d', 'e', 'f'}, // Button D2: single, double, long + {'g', 'h', 'i'}, // Button D3: single, double, long + {'j', 'k', 'l'}, // Button D4: single, double, long + {'m', 'n', 'o'}, // Button D5: single, double, long + {'p', 'q', 'r'}, // Button D6: single, double, long + {'s', 't', 'u'}, // Button D8: single, double, long + {'v', 'w', 'x'}, // Button D9: single, double, long + {'y', 'z', '1'} // Button D10: single, double, long +}; + +void setup() { + Serial.begin(115200); + //while ( !Serial ) delay(10); // Wait for serial port to connect. + + Serial.println("Nexus Game Controller Starting..."); + + // Initialize buttons + for (int i = 0; i < numButtons; i++) { + buttons[i].pin = buttonPins[i]; + pinMode(buttons[i].pin, INPUT_PULLUP); + buttons[i].lastState = HIGH; + buttons[i].lastDebounceTime = 0; + buttons[i].pressTime = 0; + buttons[i].clickCount = 0; + buttons[i].isPressed = false; + buttons[i].longPressHandled = false; + } + + // Setup Bluetooth + Bluefruit.begin(); + Bluefruit.setName("Nexus Game Controller"); + Bluefruit.setTxPower(4); + + // Configure and start BLE services + bledis.setManufacturer("Seeed Studio"); + bledis.setModel("XIAO nRF52840 Sense"); + bledis.begin(); + + blehid.begin(); + + // Start advertising + Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE); + Bluefruit.Advertising.addTxPower(); + Bluefruit.Advertising.addAppearance(BLE_APPEARANCE_HID_KEYBOARD); + Bluefruit.Advertising.addService(blehid); + Bluefruit.Advertising.addName(); + + Bluefruit.Advertising.restartOnDisconnect(true); + Bluefruit.Advertising.setInterval(32, 244); // in unit of 0.625 ms + Bluefruit.Advertising.setFastTimeout(30); // number of seconds in fast advertising mode + Bluefruit.Advertising.start(0); // 0 = Don't stop advertising after n seconds + + Serial.println("Advertising..."); + + // Set up connection/disconnection callbacks + Bluefruit.Periph.setConnectCallback(connect_callback); + Bluefruit.Periph.setDisconnectCallback(disconnect_callback); +} + +void connect_callback(uint16_t conn_handle) { + (void) conn_handle; + Serial.println("Bluetooth connected"); +} + +void disconnect_callback(uint16_t conn_handle, uint8_t reason) { + (void) conn_handle; + (void) reason; + Serial.println("Bluetooth disconnected"); +} + + +void loop() { + for (int i = 0; i < numButtons; i++) { + handleButton(&buttons[i], i); + } +} + +void handleButton(Button* b, int buttonIndex) { + bool reading = digitalRead(b->pin); + + // Debounce logic + if (reading != b->lastState) { + b->lastDebounceTime = millis(); + } + + if ((millis() - b->lastDebounceTime) > debounceDelay) { + // If the button state has been stable + if (reading == LOW && !b->isPressed) { // Button just pressed + b->isPressed = true; + b->pressTime = millis(); + b->clickCount++; + b->longPressHandled = false; + } else if (reading == HIGH && b->isPressed) { // Button just released + b->isPressed = false; + if (!b->longPressHandled) { + // It's a click, but we need to wait for a potential double click + } else { + // This was a long press, so we do nothing on release + b->longPressHandled = false; // Reset for next time + } + } + } + + // Check for long press + if (b->isPressed && !b->longPressHandled && (millis() - b->pressTime > longPressDelay)) { + triggerEvent(buttonIndex, 2); // Long Press + b->longPressHandled = true; + b->clickCount = 0; // Reset click count after a long press + } + + // Check for single/double click timeout + if (b->clickCount > 0 && !b->isPressed && (millis() - b->pressTime > doubleClickDelay)) { + if (!b->longPressHandled) { + if (b->clickCount == 1) { + triggerEvent(buttonIndex, 0); // Single Click + } else if (b->clickCount == 2) { + triggerEvent(buttonIndex, 1); // Double Click + } + } + b->clickCount = 0; + } + + b->lastState = reading; +} + +void triggerEvent(int buttonIndex, int eventType) { + // eventType: 0 = single, 1 = double, 2 = long + char key = eventChars[buttonIndex][eventType]; + + const char* eventStr[] = {"single click", "double click", "long press"}; + + Serial.print("Button D"); + Serial.print(buttonPins[buttonIndex]); + Serial.print(" triggered "); + Serial.print(eventStr[eventType]); + Serial.print(" event. Character '"); + Serial.print(key); + Serial.println("' has been sent."); + + if (Bluefruit.connected()) { + blehid.keyPress(key); + delay(10); // a small delay to prevent flooding + blehid.keyRelease(); + } +} diff --git a/docs/remote-control.md b/docs/remote-control.md index 27075ec..a67e4c8 100644 --- a/docs/remote-control.md +++ b/docs/remote-control.md @@ -4,8 +4,8 @@ 1. [HID Smart Buttons](#hid-smart-buttons) 2. [MQTT Remote Control](#mqtt-remote-control) - [Mosquitto Installation Guide](#mosquitto-installation-guide) - - [Mosquitto MQTT Broker on Android 16's Native Linux VM](#mosquitto-mqtt-broker-on-android-16s-native-linux-vm) - - [Mosquitto MQTT Broker on older versions of Android then 16](#mosquitto-mqtt-broker-on-older-versions-of-android-then-16) + - [Mosquitto MQTT Broker using `Termux` app](#mosquitto-mqtt-broker-using-termux-app) + - [Mosquitto MQTT Broker with VPN on Android 16's Native Linux VM](#mosquitto-mqtt-broker-with-vpn-on-android-16s-native-linux-vm) - [Android Shortcut Setup](#android-shortcut-setup) - [Configure `Quick Tap` gesture to trigger the shortcut](#configure-quick-tap-gesture-to-trigger-the-shortcut) - [Testing with `mosquitto_pub` (via Termux)](#testing-with-mosquitto_pub-via-termux) @@ -15,7 +15,7 @@ ## HID Smart Buttons For an enhanced tactile experience, Nexus Timer supports Smart Buttons based on Bluetooth-connected microcontroller (e.g., XIAO nRF52840) implementing HID (Human Interface Device) protocol emulating a keyboard. -* **Buttons:** Connect 3 physical buttons, potentially extended (e.g., via 1.5m wires) for easy player access. +* **Buttons:** Connect 3 physical buttons, potentially extended e.g., via 1.5m wires for easy player access. * **Configuration:** * **Player 1's Button:** Single Click: Emulates a key press (e.g., 'a'). Configure this as Player 1's "Pass Turn / My Pause" hotkey in the app. * **Player 2's Button:** Single Click: Emulates a key press (e.g., 'b'). Configure as Player 2's "Pass Turn / My Pause" hotkey. @@ -23,15 +23,50 @@ For an enhanced tactile experience, Nexus Timer supports Smart Buttons based on * **If Player 3 is Game Admin:** * **Player 3's Button:** Long Press: Emulates a key press (e.g., 's'). Configure as the "Global Stop/Pause All" hotkey in the app. * **Player 3's Button:** Double Click: Emulates a key press (e.g., 'x'). Configure as the "Global Run All Timers" hotkey in the app. + +The code for the XIAO nRF52840 module with a 3D printing files can be found in the [arduino](/arduino) subdirectory. ## MQTT Remote Control -Players can use their smartphones to send commands to Nexus Timer via MQTT. This requires an MQTT broker (like Mosquitto) on the same network. +Alternatively to a dedicated smart buttons, players can use their smartphones to send commands to Nexus Timer via MQTT. This requires an MQTT broker (like Mosquitto) on the same network. ### Mosquitto Installation Guide -#### Mosquitto MQTT Broker on Android 16's Native Linux VM +#### Mosquitto MQTT Broker using `Termux` app -This guide details how to install and configure the Mosquitto MQTT broker within the native `Linux Virtual Machine environment` introduced in Android 16. For older versions proceed to [Mosquitto MQTT Broker on older versions of Android then 16](#mosquitto-mqtt-broker-on-older-versions-of-android-then-16). +1. **Install Termux** from the [Play Store](https://play.google.com/store/apps/details?id=com.termux) and run. +2. **Update packages and install Mosquitto in Termux:** + ```bash + pkg update && pkg upgrade + pkg install mosquitto + ``` +3. **Configure the MQTT Broker:** + ```bash + nano $PREFIX/etc/mosquitto/mosquitto.conf + ``` + Add the following configuration, then save and exit: + ```ini + # MQTT listener on port 1883 + # MQTT connection from the HTTP Shortcut app + listener 1883 0.0.0.0 + protocol mqtt + + # WebSocket listener on port 9001 + # MQTT over WebSocket connection from the PWA (Web App) + listener 9001 0.0.0.0 + protocol websockets + + # Allow clients to connect without username/password + allow_anonymous true + ``` +4. **Run Mosquitto with the configuration:** + ```bash + mosquitto -c $PREFIX/etc/mosquitto/mosquitto.conf + ``` +--- + +#### Mosquitto MQTT Broker with VPN on Android 16's Native Linux VM + +This guide details how to install and configure the Mosquitto MQTT broker within the native `Linux Virtual Machine environment` introduced in Android 16. Note that ports exposed by the Mosquitto cannot be reached from LAN. The workarround is using a VPN (wireguard). 1. **Enable the Linux Development Environment** @@ -113,39 +148,6 @@ Your Mosquitto MQTT broker is now successfully configured and running on your An --- -#### Mosquitto MQTT Broker on older versions of Android then 16 - -1. **Install Termux** from the [Play Store](https://play.google.com/store/apps/details?id=com.termux) and run. -2. **Update packages and install Mosquitto in Termux:** - ```bash - pkg update && pkg upgrade - pkg install mosquitto - ``` -3. **Configure the MQTT Broker:** - ```bash - nano $PREFIX/etc/mosquitto/mosquitto.conf - ``` - Add the following configuration, then save and exit: - ```ini - # MQTT listener on port 1883 - # MQTT connection from the HTTP Shortcut app - listener 1883 0.0.0.0 - protocol mqtt - - # WebSocket listener on port 9001 - # MQTT over WebSocket connection from the PWA (Web App) - listener 9001 0.0.0.0 - protocol websockets - - # Allow clients to connect without username/password - allow_anonymous true - ``` -4. **Run Mosquitto with the configuration:** - ```bash - mosquitto -c $PREFIX/etc/mosquitto/mosquitto.conf - ``` ---- - ### Android Shortcut Setup * Install the `HTTP Shortcuts` app from the [Play Store](https://play.google.com/store/apps/details?id=ch.rmy.android.http_shortcuts). * Create a new shortcut.