基于C#与Digispark的自定义媒体控制设备开发问询
Great project goal—building a customizable Griffin PowerMate alternative with Digispark is totally achievable. Let’s break down the implementation into actionable steps, covering both the Arduino firmware overhaul and C# desktop app integration, plus a flexible macro system.
We’ll split the system into two key components:
- Digispark Firmware: Captures raw hardware events (button clicks, encoder rotations) and sends them to the PC via USB. It’ll also keep your existing media controls as a fallback if the C# app isn’t running.
- C# Desktop App: Listens for incoming events, matches them against user-configured mappings/macros, and executes the corresponding actions (media controls, system commands, app launches, etc.).
Your current code directly triggers HID media keys—we’ll adjust it to send event data to the PC instead, while retaining fallback functionality.
Key Changes:
- Add Serial Communication: Use Digispark’s SoftwareSerial (since it has limited hardware UART) to send lightweight event codes to the PC.
- Standardize Event Codes: Send simple identifiers to save memory:
C: Single button clickD: Double button clickH: Short button holdL: Long button hold+: Encoder clockwise (original volume up)-: Encoder counter-clockwise (original volume down)
- Fallback Logic: If no PC connection is detected after a short timeout, revert to your original media control behavior.
Modified Firmware Example:
#include "TrinketHidCombo.h" #include <SoftwareSerial.h> #define PIN_ENCODER_A 0 #define PIN_ENCODER_B 2 #define PIN_BUTTON 1 #define TRINKET_PINx PINB // SoftwareSerial setup (adjust pins to unused Digispark pins) SoftwareSerial mySerial(3, 4); // RX, TX static uint8_t enc_prev_pos = 0; static uint8_t enc_flags = 0; boolean pcConnected = false; unsigned long lastPcCheck = 0; const long pcCheckInterval = 2000; void setup() { // Pin configuration with pull-up resistors pinMode(PIN_ENCODER_A, INPUT_PULLUP); pinMode(PIN_ENCODER_B, INPUT_PULLUP); pinMode(PIN_BUTTON, INPUT_PULLUP); TrinketHidCombo.begin(); mySerial.begin(9600); // Capture initial encoder state if (digitalRead(PIN_ENCODER_A) == LOW) enc_prev_pos |= (1 << 0); if (digitalRead(PIN_ENCODER_B) == LOW) enc_prev_pos |= (1 << 1); } void loop() { // Check for PC connection handshake if (millis() - lastPcCheck > pcCheckInterval) { mySerial.println("PING"); lastPcCheck = millis(); } if (mySerial.available() > 0) { String response = mySerial.readStringUntil('\n'); if (response == "ACK") pcConnected = true; } int8_t enc_action = 0; uint8_t enc_cur_pos = 0; // Encoder reading logic (unchanged from your original code) if (bit_is_clear(TRINKET_PINx, PIN_ENCODER_A)) enc_cur_pos |= (1 << 0); if (bit_is_clear(TRINKET_PINx, PIN_ENCODER_B)) enc_cur_pos |= (1 << 1); if (enc_cur_pos != enc_prev_pos) { if (enc_prev_pos == 0x00) { if (enc_cur_pos == 0x01) enc_flags |= (1 << 0); else if (enc_cur_pos == 0x02) enc_flags |= (1 << 1); } if (enc_cur_pos == 0x03) { enc_flags |= (1 << 4); } else if (enc_cur_pos == 0x00) { if (enc_prev_pos == 0x02) enc_flags |= (1 << 2); else if (enc_prev_pos == 0x01) enc_flags |= (1 << 3); if ((bit_is_set(enc_flags, 0) && (bit_is_set(enc_flags, 2) || bit_is_set(enc_flags, 4))) || (bit_is_set(enc_flags, 2) && (bit_is_set(enc_flags, 0) || bit_is_set(enc_flags, 4)))) { enc_action = 1; } else if ((bit_is_set(enc_flags, 1) && (bit_is_set(enc_flags, 3) || bit_is_set(enc_flags, 4))) || (bit_is_set(enc_flags, 3) && (bit_is_set(enc_flags, 1) || bit_is_set(enc_flags, 4)))) { enc_action = -1; } enc_flags = 0; } } // Button event handling int b = checkButton(); if (b != 0) { if (pcConnected) { switch(b) { case 1: mySerial.println("C"); break; case 2: mySerial.println("D"); break; case 3: mySerial.println("H"); break; case 4: mySerial.println("L"); break; } } else { // Fallback to original media actions if (b == 1) clickEvent(); if (b == 2) doubleClickEvent(); if (b == 3) holdEvent(); if (b == 4) longHoldEvent(); } } // Encoder action handling if (enc_action != 0) { if (pcConnected) { mySerial.println(enc_action > 0 ? "+" : "-"); } else { if (enc_action > 0) TrinketHidCombo.pressMultimediaKey(MMKEY_VOL_UP); else TrinketHidCombo.pressMultimediaKey(MMKEY_VOL_DOWN); } } enc_prev_pos = enc_cur_pos; TrinketHidCombo.poll(); } // Original event functions (unchanged for fallback) void clickEvent() { TrinketHidCombo.pressMultimediaKey(MMKEY_PLAYPAUSE); } void doubleClickEvent() { TrinketHidCombo.pressMultimediaKey(MMKEY_MUTE); } void holdEvent() { TrinketHidCombo.pressMultimediaKey(MMKEY_SCAN_PREV_TRACK); } void longHoldEvent() { TrinketHidCombo.pressMultimediaKey(MMKEY_SCAN_NEXT_TRACK); } // Completed button debounce and event detection logic int debounce = 20; int DCgap = 500; int holdTime = 1000; int longHoldTime = 1500; boolean buttonVal = HIGH; boolean buttonLast = HIGH; boolean DCwaiting = false; boolean DConUp = false; boolean singleOK = true; long downTime = 0; long upTime = 0; boolean ignoreUp = false; int checkButton() { int event = 0; buttonVal = digitalRead(PIN_BUTTON); if (buttonVal == LOW && buttonLast == HIGH && (millis() - upTime) > debounce) { downTime = millis(); ignoreUp = false; singleOK = true; DConUp = false; DCwaiting = false; } else if (buttonVal == HIGH && buttonLast == LOW && (millis() - downTime) > debounce) { if (!ignoreUp) { upTime = millis(); if ((millis() - downTime) < holdTime) { if (DCwaiting) { event = 2; DConUp = false; DCwaiting = false; singleOK = false; } else if (singleOK) { DCwaiting = true; } } else if ((millis() - downTime) < longHoldTime) { event = 3; singleOK = false; DCwaiting = false; } else { event = 4; singleOK = false; DCwaiting = false; } } } else if (buttonVal == HIGH && (millis() - upTime) >= DCgap && DCwaiting) { event = 1; DConUp = false; DCwaiting = false; singleOK = true; } buttonLast = buttonVal; return event; }
The C# app will handle event listening, configuration management, and action execution. Here’s how to build it:
Core Components:
- Serial Communication: Use
System.IO.Ports.SerialPortto connect to the Digispark. Send anACKhandshake when you receive aPINGto confirm connection. - Event Queue: Maintain a timestamped queue of incoming events to track sequences for macros.
- Configuration System: Store user mappings and macros in a JSON file for persistence. Example structure:
{ "SingleMappings": { "C": "PlayPause", "+": "VolumeUp" }, "Macros": [ { "Sequence": ["D", "-"], "Timeout": 1000, "Action": "LockScreen" } ] } - Action Execution: Use Windows API calls (like
SendInput) to simulate keyboard/mouse actions, orProcess.Startto launch apps. For system commands (lock screen), calluser32.dll’sLockWorkStationfunction.
Macro Matching Logic:
- Add each incoming event to a queue with its timestamp.
- Periodically check the queue against all defined macros:
- Verify if the last N events (matching the macro’s sequence) occurred within the timeout window.
- If a match is found, execute the associated action and clear the queue to avoid re-triggering.
UI Suggestions:
Build a WinForms/WPF interface with:
- A dropdown to select the Digispark’s serial port.
- A panel for single-event mappings (select event, choose action).
- A macro editor to create/modify event sequences and bind actions.
- Save/load buttons for configurations.
To match Griffin PowerMate’s flexibility:
- Variable Timeouts: Let users adjust the maximum interval between events in a macro.
- Repeatable Actions: Allow macros to trigger repeated actions (e.g., hold encoder left to scroll down).
- Layered Configurations: Add profile switching (e.g., work mode vs gaming mode with different mappings).
内容的提问来源于stack exchange,提问作者DavidAnastasov

