diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..01c02eec --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,66 @@ +# Contributing to DCS Interface + +Thank you for your interest in contributing to this project! + +## Fork Information + +This fork adds Stream Deck Plus rotary encoder support to the original project. + +### Original Project + +- Original Repository: [charlestytler/streamdeck-dcs-interface](https://github.com/charlestytler/streamdeck-dcs-interface) +- License: GNU General Public License v3.0 + +## How to Contribute + +1. **Fork the repository** on GitHub +2. **Clone your fork** locally +3. **Create a feature branch** (`git checkout -b feature/my-new-feature`) +4. **Make your changes** and commit them with clear messages +5. **Test your changes** thoroughly +6. **Push to your fork** (`git push origin feature/my-new-feature`) +7. **Submit a Pull Request** to the original repository + +## Development Guidelines + +### Building from Source + +Before building, ensure you have: +- Microsoft Visual Studio 2022 (or Build Tools with Platform Toolset v145) +- MSBuild added to your PATH +- npm for Windows + +Run the build script: +```batch +cd Tools +.\build_plugin.bat +``` + +### Code Style + +- C++: Follow existing code conventions in the project +- JavaScript/HTML: Use consistent indentation and formatting +- Add comments for complex logic + +### Testing + +- Test all functionality with actual Stream Deck hardware +- Verify DCS integration with multiple aircraft modules +- Run unit tests before submitting (`Test.exe` generated during build) + +## Reporting Issues + +When reporting issues, please include: +- Stream Deck model and firmware version +- DCS World version +- Steps to reproduce the issue +- Expected vs actual behavior +- Any error messages or logs + +## License + +By contributing, you agree that your contributions will be licensed under the GNU General Public License v3.0, consistent with the original project. + +## Acknowledgments + +This project builds upon the excellent work of the original author and contributors. All modifications respect the original GPL-3.0 license terms. diff --git a/README.md b/README.md index bf37cb5d..34dbec41 100644 --- a/README.md +++ b/README.md @@ -28,12 +28,26 @@ There are currently three settings for each Streamdeck button you create: - DCS Command - Specify which button/switch you want to activate in game (allows setting of any clickable object in a cockpit). - Streamdeck buttons support push-button, switch, and increment (dials, levers, etc.) input types. + - **Stream Deck Plus rotary encoders** support rotation with separate CW/CCW increment values and encoder press for fixed values. - Image Change Settings - Specify a function within the DCS simulation to monitor and change the display of the Streamdeck image conditionally. - Examples: Lamps for Warnings/Modes, Switch states - Title Text Change Settings - Specify a function in the DCS simulation which will be monitored and its text is displayed as the Streamdeck button Title. - Examples: UFC text displays, scratchpads, radio displays -Can also support multiple physical Streamdecks at once. +Can also support multiple physical Streamdecks at once, including **Stream Deck Plus** with rotary encoder support. + +## Stream Deck Plus Encoder Features + +The plugin now supports **Stream Deck Plus rotary encoders** with the following capabilities: + +- **Rotation Control**: Separate clockwise (CW) and counter-clockwise (CCW) increment values per tick +- **Press Action**: Send a fixed value when pressing the encoder button +- **LCD Display**: Real-time display of DCS values on the encoder LCD screen +- **Value Mapping**: Map numeric DCS values to custom text (e.g., "0.2" → "OFF", "0.8" → "ARMED") +- **Automatic Gauge**: Visual indicator bar based on minimum/maximum value ranges +- **Live Updates**: Changes to mappings take effect immediately without restart + +Configure encoders using the dedicated Property Inspector with intuitive controls for all settings. ## Detailed Documentation @@ -125,4 +139,6 @@ Before running the .bat file you will need to: Running the batch script will build the Streamdeck plugin and run all unit tests, generating the plugin file at `Release/com.ctytler.dcs.streamDeckPlugin`. -Current version was built with Visual Studio Community 2022. +**Build Requirements:** +- Visual Studio 2022 (v17.11+) or Visual Studio 2025 with Platform Toolset v145 +- The encoder support features require Visual Studio 2025 or compatible toolset diff --git a/Sources/backend-cpp/ElgatoSD/ElgatoSD.vcxproj b/Sources/backend-cpp/ElgatoSD/ElgatoSD.vcxproj index ec11b55f..23ca0112 100644 --- a/Sources/backend-cpp/ElgatoSD/ElgatoSD.vcxproj +++ b/Sources/backend-cpp/ElgatoSD/ElgatoSD.vcxproj @@ -29,26 +29,26 @@ StaticLibrary true - v143 + v145 Unicode StaticLibrary false - v143 + v145 true Unicode StaticLibrary true - v143 + v145 Unicode StaticLibrary false - v143 + v145 true Unicode diff --git a/Sources/backend-cpp/SimulatorInterface/SimulatorInterface.vcxproj b/Sources/backend-cpp/SimulatorInterface/SimulatorInterface.vcxproj index 37336a92..a17961b0 100644 --- a/Sources/backend-cpp/SimulatorInterface/SimulatorInterface.vcxproj +++ b/Sources/backend-cpp/SimulatorInterface/SimulatorInterface.vcxproj @@ -29,26 +29,26 @@ StaticLibrary true - v143 + v145 Unicode StaticLibrary false - v143 + v145 true Unicode StaticLibrary true - v143 + v145 Unicode StaticLibrary false - v143 + v145 true Unicode @@ -174,6 +174,12 @@ + + + {329f64ac-6346-46d2-a671-9b5dd626b485} + false + + diff --git a/Sources/backend-cpp/StreamdeckContext/SendActions/IncrementAction.cpp b/Sources/backend-cpp/StreamdeckContext/SendActions/IncrementAction.cpp index 4a3e7c2a..136130fe 100644 --- a/Sources/backend-cpp/StreamdeckContext/SendActions/IncrementAction.cpp +++ b/Sources/backend-cpp/StreamdeckContext/SendActions/IncrementAction.cpp @@ -6,6 +6,9 @@ #include "ElgatoSD/EPLJSONUtils.h" +#include +#include + void IncrementAction::handleButtonPressedEvent(SimulatorInterface *simulator_interface, ESDConnectionManager *mConnectionManager, const json &inPayload) @@ -23,6 +26,124 @@ void IncrementAction::handleButtonPressedEvent(SimulatorInterface *simulator_int } } +std::string IncrementAction::getCurrentDisplayValue(SimulatorInterface *simulator_interface, const json &settings) +{ + const auto dcs_id_increment_monitor_str = EPLJSONUtils::GetStringByName(settings, "dcs_id_increment_monitor"); + + if (is_integer(dcs_id_increment_monitor_str)) { + const int dcs_id = std::stoi(dcs_id_increment_monitor_str); + const std::optional maybe_value = simulator_interface->get_value_at_addr(dcs_id); + + if (maybe_value.has_value()) { + const std::string raw_value = maybe_value.value().str(); + + // Check if there's a value-to-text mapping + const auto mapping_str = EPLJSONUtils::GetStringByName(settings, "encoder_value_text_mapping"); + + if (!mapping_str.empty()) { + // Parse the mapping format: "value1:text1;value2:text2;..." + std::istringstream mapping_stream(mapping_str); + std::string pair; + + while (std::getline(mapping_stream, pair, ';')) { + // Find the colon separator + size_t colon_pos = pair.find(':'); + if (colon_pos != std::string::npos) { + std::string map_value = pair.substr(0, colon_pos); + std::string map_text = pair.substr(colon_pos + 1); + + // Remove any formatting remnants (|color:|size:) from old format + size_t pipe_pos = map_text.find('|'); + if (pipe_pos != std::string::npos) { + map_text = map_text.substr(0, pipe_pos); + } + + // Trim whitespace + map_value.erase(0, map_value.find_first_not_of(" \t\n\r")); + map_value.erase(map_value.find_last_not_of(" \t\n\r") + 1); + map_text.erase(0, map_text.find_first_not_of(" \t\n\r")); + map_text.erase(map_text.find_last_not_of(" \t\n\r") + 1); + + // Compare values (with tolerance for floating point) + if (is_number(map_value)) { + Decimal map_decimal(map_value); + Decimal current_decimal(raw_value); + + // Check if values match (within a small tolerance) + Decimal diff = (map_decimal > current_decimal) ? (map_decimal - current_decimal) : (current_decimal - map_decimal); + if (std::stod(diff.str()) < 0.0001) { + return map_text; + } + } + } + } + } + + // No mapping found, return raw value + return raw_value; + } + } + + return ""; +} + +std::string IncrementAction::getCurrentImagePath(SimulatorInterface *simulator_interface, const json &settings) +{ + const auto dcs_id_increment_monitor_str = EPLJSONUtils::GetStringByName(settings, "dcs_id_increment_monitor"); + + if (is_integer(dcs_id_increment_monitor_str)) { + const int dcs_id = std::stoi(dcs_id_increment_monitor_str); + const std::optional maybe_value = simulator_interface->get_value_at_addr(dcs_id); + + if (maybe_value.has_value()) { + const std::string raw_value = maybe_value.value().str(); + + // Check if there's a value-to-image mapping + const auto mapping_str = EPLJSONUtils::GetStringByName(settings, "encoder_value_text_mapping"); + + if (!mapping_str.empty()) { + // Parse the mapping format: "value1:text1;value2:IMG:imagepath;..." + std::istringstream mapping_stream(mapping_str); + std::string pair; + + while (std::getline(mapping_stream, pair, ';')) { + // Find the colon separator + size_t colon_pos = pair.find(':'); + if (colon_pos != std::string::npos) { + std::string map_value = pair.substr(0, colon_pos); + std::string map_content = pair.substr(colon_pos + 1); + + // Trim whitespace + map_value.erase(0, map_value.find_first_not_of(" \t\n\r")); + map_value.erase(map_value.find_last_not_of(" \t\n\r") + 1); + map_content.erase(0, map_content.find_first_not_of(" \t\n\r")); + map_content.erase(map_content.find_last_not_of(" \t\n\r") + 1); + + // Check if this is an image mapping (format: IMG:path) + if (map_content.substr(0, 4) == "IMG:") { + std::string image_path = map_content.substr(4); + + // Compare values (with tolerance for floating point) + if (is_number(map_value)) { + Decimal map_decimal(map_value); + Decimal current_decimal(raw_value); + + // Check if values match (within a small tolerance) + Decimal diff = (map_decimal > current_decimal) ? (map_decimal - current_decimal) : (current_decimal - map_decimal); + if (std::stod(diff.str()) < 0.0001) { + return image_path; + } + } + } + } + } + } + } + } + + return ""; +} + void IncrementAction::handleButtonReleasedEvent(SimulatorInterface *simulator_interface, ESDConnectionManager *mConnectionManager, const json &inPayload) @@ -46,3 +167,97 @@ std::optional IncrementAction::determineSendValue(const json &setti } return std::nullopt; } + +void IncrementAction::handleEncoderRotation(SimulatorInterface *simulator_interface, + ESDConnectionManager *mConnectionManager, + const json &inPayload, + int ticks) +{ + const auto settings = inPayload["settings"]; + const auto send_address = EPLJSONUtils::GetStringByName(settings, "send_address"); + const auto increment_cw_str = EPLJSONUtils::GetStringByName(settings, "increment_cw"); + const auto increment_ccw_str = EPLJSONUtils::GetStringByName(settings, "increment_ccw"); + const auto increment_min_str = EPLJSONUtils::GetStringByName(settings, "increment_min"); + const auto increment_max_str = EPLJSONUtils::GetStringByName(settings, "increment_max"); + const bool cycling_is_allowed = EPLJSONUtils::GetBoolByName(settings, "increment_cycle_allowed_check"); + + // Debug logging + mConnectionManager->LogMessage("[Encoder Rotation] send_address: " + send_address); + mConnectionManager->LogMessage("[Encoder Rotation] increment_cw: " + increment_cw_str); + mConnectionManager->LogMessage("[Encoder Rotation] increment_ccw: " + increment_ccw_str); + mConnectionManager->LogMessage("[Encoder Rotation] min: " + increment_min_str + " max: " + increment_max_str); + mConnectionManager->LogMessage("[Encoder Rotation] ticks: " + std::to_string(ticks)); + + // Update increment monitor with current game state + increment_monitor_.update_settings(settings); + increment_monitor_.update(simulator_interface); + + // Choose the appropriate increment value based on rotation direction + std::string increment_value_str; + if (ticks > 0) { + // Clockwise rotation + increment_value_str = increment_cw_str; + mConnectionManager->LogMessage("[Encoder Rotation] Direction: CW, using increment_cw"); + } else if (ticks < 0) { + // Counter-clockwise rotation + increment_value_str = increment_ccw_str; + mConnectionManager->LogMessage("[Encoder Rotation] Direction: CCW, using increment_ccw"); + } else { + // No rotation + mConnectionManager->LogMessage("[Encoder Rotation] No rotation (ticks = 0), ignoring"); + return; + } + + if (is_number(increment_value_str) && is_number(increment_min_str) && is_number(increment_max_str)) { + const Decimal increment_value(increment_value_str); + + // Use absolute value of ticks since direction is already handled by choosing CW or CCW value + const Decimal delta_cmd = increment_value * Decimal(std::to_string(std::abs(ticks))); + + const auto value = increment_monitor_.get_increment_after_command(delta_cmd, + Decimal(increment_min_str), + Decimal(increment_max_str), + cycling_is_allowed); + mConnectionManager->LogMessage("[Encoder Rotation] Sending value: " + value.str() + " to address: " + send_address); + simulator_interface->send_command(send_address, value.str()); + } else { + mConnectionManager->LogMessage("[Encoder Rotation] Invalid settings - not all values are numbers"); + } +} + +void IncrementAction::handleEncoderPress(SimulatorInterface *simulator_interface, + ESDConnectionManager *mConnectionManager, + const json &inPayload) +{ + const auto settings = inPayload["settings"]; + const auto send_address = EPLJSONUtils::GetStringByName(settings, "send_address"); + + // Debug logging + mConnectionManager->LogMessage("[Encoder Press] send_address: " + send_address); + + // Verify send_address is not empty + if (send_address.empty()) { + mConnectionManager->LogMessage("[Encoder Press] send_address is empty - cannot send command"); + return; + } + + // Check if there's a fixed value setting for encoder press (e.g., "encoder_press_value") + const auto encoder_press_value_str = EPLJSONUtils::GetStringByName(settings, "encoder_press_value"); + + mConnectionManager->LogMessage("[Encoder Press] encoder_press_value: " + encoder_press_value_str); + + if (!encoder_press_value_str.empty()) { + // Send the fixed value configured for encoder press + mConnectionManager->LogMessage("[Encoder Press] Sending fixed value: " + encoder_press_value_str); + simulator_interface->send_command(send_address, encoder_press_value_str); + } else { + // Fallback: if no fixed value is set, use increment_min as default (reset to minimum) + const auto increment_min_str = EPLJSONUtils::GetStringByName(settings, "increment_min"); + if (!increment_min_str.empty()) { + mConnectionManager->LogMessage("[Encoder Press] Sending min value: " + increment_min_str); + simulator_interface->send_command(send_address, increment_min_str); + } else { + mConnectionManager->LogMessage("[Encoder Press] No value to send - both encoder_press_value and increment_min are empty"); + } + } +} diff --git a/Sources/backend-cpp/StreamdeckContext/SendActions/IncrementAction.h b/Sources/backend-cpp/StreamdeckContext/SendActions/IncrementAction.h index 81b6575f..73a19159 100644 --- a/Sources/backend-cpp/StreamdeckContext/SendActions/IncrementAction.h +++ b/Sources/backend-cpp/StreamdeckContext/SendActions/IncrementAction.h @@ -29,6 +29,47 @@ class IncrementAction : public SendActionInterface ESDConnectionManager *mConnectionManager, const json &inPayload); + /** + * @brief Handles encoder rotation event with direction (positive = clockwise, negative = counter-clockwise). + * + * @param simulator_interface Interface to simulator containing current game state. + * @param mConnectionManager Interface to StreamDeck. + * @param inPayload Json payload received with encoder rotation callback. + * @param ticks Number of rotation ticks (positive = clockwise, negative = counter-clockwise). + */ + void handleEncoderRotation(SimulatorInterface *simulator_interface, + ESDConnectionManager *mConnectionManager, + const json &inPayload, + int ticks); + + /** + * @brief Handles encoder press to set a fixed value. + * + * @param simulator_interface Interface to simulator containing current game state. + * @param mConnectionManager Interface to StreamDeck. + * @param inPayload Json payload received with encoder press callback. + */ + void handleEncoderPress(SimulatorInterface *simulator_interface, + ESDConnectionManager *mConnectionManager, + const json &inPayload); + /** + * @brief Returns the current display value for the encoder LCD. + * + * @param simulator_interface Interface to simulator containing current game state. + * @param settings Json settings from Streamdeck property inspector. + * @return Current value as string, or empty string if not available. + */ + std::string getCurrentDisplayValue(SimulatorInterface *simulator_interface, const json &settings); + + /** + * @brief Returns the current image path for the encoder background. + * + * @param simulator_interface Interface to simulator containing current game state. + * @param settings Json settings from Streamdeck property inspector. + * @return Image path as string, or empty string if not available. + */ + std::string getCurrentImagePath(SimulatorInterface *simulator_interface, const json &settings); + private: IncrementMonitor increment_monitor_{}; // Monitors DCS ID to determine the state of an incremental switch. diff --git a/Sources/backend-cpp/StreamdeckContext/SendActions/SendActionFactory.cpp b/Sources/backend-cpp/StreamdeckContext/SendActions/SendActionFactory.cpp index b534bdd8..56df2fb0 100644 --- a/Sources/backend-cpp/StreamdeckContext/SendActions/SendActionFactory.cpp +++ b/Sources/backend-cpp/StreamdeckContext/SendActions/SendActionFactory.cpp @@ -15,6 +15,8 @@ SendActionFactory::SendActionFactory() button_action_from_uuid_["com.ctytler.dcs.static.text.one-state"] = ButtonAction::MOMENTARY; button_action_from_uuid_["com.ctytler.dcs.increment.dial.two-state"] = ButtonAction::INCREMENT; button_action_from_uuid_["com.ctytler.dcs.increment.textdial.two-state"] = ButtonAction::INCREMENT; + button_action_from_uuid_["com.ctytler.dcs.encoder.rotary"] = ButtonAction::INCREMENT; + button_action_from_uuid_["com.ctytler.dcs.encoder.rotary.text"] = ButtonAction::INCREMENT; button_action_from_uuid_["com.ctytler.dcs.up-down.switch.two-state"] = ButtonAction::SWITCH; } diff --git a/Sources/backend-cpp/StreamdeckContext/StreamdeckContext.cpp b/Sources/backend-cpp/StreamdeckContext/StreamdeckContext.cpp index b528d79d..a6d87f8a 100644 --- a/Sources/backend-cpp/StreamdeckContext/StreamdeckContext.cpp +++ b/Sources/backend-cpp/StreamdeckContext/StreamdeckContext.cpp @@ -2,6 +2,7 @@ #include "StreamdeckContext.h" +#include "StreamdeckContext/SendActions/IncrementAction.h" #include "StreamdeckContext/SendActions/SendActionFactory.h" #include "ElgatoSD/EPLJSONUtils.h" @@ -33,6 +34,55 @@ void StreamdeckContext::updateContextState(SimulatorInterface *simulator_interfa mConnectionManager->SetTitle(current_title_, context_, kESDSDKTarget_HardwareAndSoftware); } + // Update encoder display with current increment value if this is an encoder action + if (send_action_) { + auto *increment_action = dynamic_cast(send_action_.get()); + if (increment_action) { + const std::string current_value = increment_action->getCurrentDisplayValue(simulator_interface, settings_); + + if (!current_value.empty()) { + json feedback; + feedback["value"] = current_value; + + // Calculate indicator (gauge) based on min/max/current + const auto min_str = EPLJSONUtils::GetStringByName(settings_, "increment_min"); + const auto max_str = EPLJSONUtils::GetStringByName(settings_, "increment_max"); + const auto dcs_id_str = EPLJSONUtils::GetStringByName(settings_, "dcs_id_increment_monitor"); + + if (!min_str.empty() && !max_str.empty() && is_integer(dcs_id_str)) { + try { + double min_val = std::stod(min_str); + double max_val = std::stod(max_str); + int dcs_id = std::stoi(dcs_id_str); + + auto maybe_current = simulator_interface->get_value_at_addr(dcs_id); + if (maybe_current.has_value()) { + double current = std::stod(maybe_current.value().str()); + + // Calculate percentage (0-100) + double range = max_val - min_val; + if (range > 0) { + double percentage = ((current - min_val) / range) * 100.0; + // Clamp to 0-100 + if (percentage < 0.0) percentage = 0.0; + if (percentage > 100.0) percentage = 100.0; + feedback["indicator"] = static_cast(percentage); + } + } + } catch (...) { + // Ignore errors in indicator calculation + } + } + + // Send feedback on every update, not just when value changes + if (current_value != last_encoder_display_value_) { + last_encoder_display_value_ = current_value; + } + mConnectionManager->SetFeedback(feedback, context_); + } + } + } + if (delay_for_force_send_state_) { if (delay_for_force_send_state_.value()-- <= 0) { mConnectionManager->SetState(current_state_, context_); @@ -53,6 +103,7 @@ void StreamdeckContext::forceSendStateAfterDelay(const int delay_count) void StreamdeckContext::updateContextSettings(const json &settings) { + settings_ = settings; comparison_monitor_.update_settings(settings); title_monitor_.update_settings(settings); } @@ -78,3 +129,37 @@ void StreamdeckContext::handleButtonReleasedEvent(SimulatorInterface *simulator_ forceSendState(mConnectionManager); } } + +void StreamdeckContext::handleEncoderRotation(SimulatorInterface *simulator_interface, + ESDConnectionManager *mConnectionManager, + const json &inPayload, + int ticks) +{ + // Cast to IncrementAction to access encoder-specific methods + auto *increment_action = dynamic_cast(send_action_.get()); + if (increment_action) { + // Create a new payload with stored settings + json payload_with_settings = inPayload; + payload_with_settings["settings"] = settings_; + + increment_action->handleEncoderRotation(simulator_interface, mConnectionManager, payload_with_settings, ticks); + } +} + +void StreamdeckContext::handleEncoderPress(SimulatorInterface *simulator_interface, + ESDConnectionManager *mConnectionManager, + const json &inPayload) +{ + // Cast to IncrementAction to access encoder-specific methods + auto *increment_action = dynamic_cast(send_action_.get()); + if (increment_action) { + // Create a new payload with stored settings + json payload_with_settings = inPayload; + payload_with_settings["settings"] = settings_; + + increment_action->handleEncoderPress(simulator_interface, mConnectionManager, payload_with_settings); + } + + // Force send state update after encoder press + forceSendState(mConnectionManager); +} diff --git a/Sources/backend-cpp/StreamdeckContext/StreamdeckContext.h b/Sources/backend-cpp/StreamdeckContext/StreamdeckContext.h index afcd3ee8..b23d764b 100644 --- a/Sources/backend-cpp/StreamdeckContext/StreamdeckContext.h +++ b/Sources/backend-cpp/StreamdeckContext/StreamdeckContext.h @@ -74,6 +74,30 @@ class StreamdeckContext ESDConnectionManager *mConnectionManager, const json &inPayload); + /** + * @brief Handles encoder rotation events. + * + * @param simulator_interface Interface to simulator containing current game state. + * @param mConnectionManager Interface to StreamDeck. + * @param payload Json payload received with encoder rotation callback. + * @param ticks Number of rotation ticks (positive = clockwise, negative = counter-clockwise). + */ + void handleEncoderRotation(SimulatorInterface *simulator_interface, + ESDConnectionManager *mConnectionManager, + const json &inPayload, + int ticks); + + /** + * @brief Handles encoder press events. + * + * @param simulator_interface Interface to simulator containing current game state. + * @param mConnectionManager Interface to StreamDeck. + * @param payload Json payload received with encoder press callback. + */ + void handleEncoderPress(SimulatorInterface *simulator_interface, + ESDConnectionManager *mConnectionManager, + const json &inPayload); + static const int NUM_FRAMES_DELAY_FORCED_STATE_UPDATE = 3; // Kept public for unit testing. private: @@ -83,6 +107,9 @@ class StreamdeckContext // Mutable context state. int current_state_ = 0; // Stored state of the context. std::string current_title_ = ""; // Stored title of the context. + std::string last_encoder_display_value_ = ""; // Last value displayed on encoder LCD. + std::string last_encoder_image_path_ = ""; // Last image path set for encoder background. + json settings_; // Stored settings for this context. // Monitors. ImageStateMonitor comparison_monitor_{}; // Monitors DCS ID to determine the image state of Streamdeck context. diff --git a/Sources/backend-cpp/StreamdeckContext/StreamdeckContext.vcxproj b/Sources/backend-cpp/StreamdeckContext/StreamdeckContext.vcxproj index 133c0f01..6b658b5b 100644 --- a/Sources/backend-cpp/StreamdeckContext/StreamdeckContext.vcxproj +++ b/Sources/backend-cpp/StreamdeckContext/StreamdeckContext.vcxproj @@ -29,26 +29,26 @@ StaticLibrary true - v143 + v145 Unicode StaticLibrary false - v143 + v145 true Unicode StaticLibrary true - v143 + v145 Unicode StaticLibrary false - v143 + v145 true Unicode @@ -178,6 +178,20 @@ + + + {f60ca831-72e9-4992-bcfe-12b3387f646b} + false + + + {2782d849-65f7-4b89-a856-964fd918269a} + false + + + {329f64ac-6346-46d2-a671-9b5dd626b485} + false + + diff --git a/Sources/backend-cpp/StreamdeckInterface/StreamdeckInterface.cpp b/Sources/backend-cpp/StreamdeckInterface/StreamdeckInterface.cpp index a144de6a..172c3b84 100644 --- a/Sources/backend-cpp/StreamdeckInterface/StreamdeckInterface.cpp +++ b/Sources/backend-cpp/StreamdeckInterface/StreamdeckInterface.cpp @@ -183,6 +183,78 @@ void StreamdeckInterface::KeyUpForAction(const std::string &inAction, mVisibleContextsMutex.unlock(); } +void StreamdeckInterface::DialRotateForAction(const std::string &inAction, + const std::string &inContext, + const json &inPayload, + const std::string &inDeviceID) +{ + const auto payload = backwardsCompatibilityHandler(inPayload); + + mVisibleContextsMutex.lock(); + if (mVisibleContexts.count(inContext) > 0) { + const auto protocol = mVisibleContexts[inContext].protocol(); + if (simConnectionManager_.is_connected(protocol)) { + // Get rotation ticks from payload (positive = clockwise, negative = counter-clockwise) + int ticks = 0; + if (payload.contains("ticks")) { + ticks = payload["ticks"].get(); + } + + // Call the encoder-specific rotation handler with direction + mVisibleContexts[inContext].handleEncoderRotation( + simConnectionManager_.get_interface(protocol), mConnectionManager, payload, ticks); + } + } + mVisibleContextsMutex.unlock(); +} + +void StreamdeckInterface::DialPressForAction(const std::string &inAction, + const std::string &inContext, + const json &inPayload, + const std::string &inDeviceID) +{ + const auto payload = backwardsCompatibilityHandler(inPayload); + + mConnectionManager->LogMessage("[DialPress] Event received for context: " + inContext); + + mVisibleContextsMutex.lock(); + if (mVisibleContexts.count(inContext) > 0) { + mConnectionManager->LogMessage("[DialPress] Context found in visible contexts"); + const auto protocol = mVisibleContexts[inContext].protocol(); + if (simConnectionManager_.is_connected(protocol)) { + mConnectionManager->LogMessage("[DialPress] Simulator connected - calling handleEncoderPress"); + mVisibleContexts[inContext].handleEncoderPress( + simConnectionManager_.get_interface(protocol), mConnectionManager, payload); + } else { + mConnectionManager->LogMessage("[DialPress] Simulator NOT connected"); + } + } else { + mConnectionManager->LogMessage("[DialPress] Context NOT found in visible contexts"); + } + mVisibleContextsMutex.unlock(); +} + +void StreamdeckInterface::TouchTapForAction(const std::string &inAction, + const std::string &inContext, + const json &inPayload, + const std::string &inDeviceID) +{ + const auto payload = backwardsCompatibilityHandler(inPayload); + + mVisibleContextsMutex.lock(); + if (mVisibleContexts.count(inContext) > 0) { + const auto protocol = mVisibleContexts[inContext].protocol(); + if (simConnectionManager_.is_connected(protocol)) { + // Treat touch tap as a momentary button press + mVisibleContexts[inContext].handleButtonPressedEvent( + simConnectionManager_.get_interface(protocol), mConnectionManager, payload); + mVisibleContexts[inContext].handleButtonReleasedEvent( + simConnectionManager_.get_interface(protocol), mConnectionManager, payload); + } + } + mVisibleContextsMutex.unlock(); +} + void StreamdeckInterface::WillAppearForAction(const std::string &inAction, const std::string &inContext, const json &inPayload, diff --git a/Sources/backend-cpp/StreamdeckInterface/StreamdeckInterface.h b/Sources/backend-cpp/StreamdeckInterface/StreamdeckInterface.h index 91573098..5e1da617 100644 --- a/Sources/backend-cpp/StreamdeckInterface/StreamdeckInterface.h +++ b/Sources/backend-cpp/StreamdeckInterface/StreamdeckInterface.h @@ -35,6 +35,21 @@ class StreamdeckInterface : public ESDBasePlugin const json &inPayload, const std::string &inDeviceID) override; + void DialRotateForAction(const std::string& inAction, + const std::string& inContext, + const json& inPayload, + const std::string& inDeviceID) override; + + void DialPressForAction(const std::string& inAction, + const std::string& inContext, + const json& inPayload, + const std::string& inDeviceID) override; + + void TouchTapForAction(const std::string& inAction, + const std::string& inContext, + const json& inPayload, + const std::string& inDeviceID) override; + /** * The 'willAppear' event is the first event a key will receive, right before it gets showed on your Stream Deck * and/or in Stream Deck software. diff --git a/Sources/backend-cpp/StreamdeckInterface/StreamdeckInterface.vcxproj b/Sources/backend-cpp/StreamdeckInterface/StreamdeckInterface.vcxproj index 9df4d45f..4d207a79 100644 --- a/Sources/backend-cpp/StreamdeckInterface/StreamdeckInterface.vcxproj +++ b/Sources/backend-cpp/StreamdeckInterface/StreamdeckInterface.vcxproj @@ -29,26 +29,26 @@ Application true - v143 + v145 Unicode Application false - v143 + v145 true Unicode Application true - v143 + v145 Unicode Application false - v143 + v145 true Unicode diff --git a/Sources/backend-cpp/Test/Test.vcxproj b/Sources/backend-cpp/Test/Test.vcxproj index e3dadaee..d04f843b 100644 --- a/Sources/backend-cpp/Test/Test.vcxproj +++ b/Sources/backend-cpp/Test/Test.vcxproj @@ -1,4 +1,4 @@ - + @@ -23,7 +23,7 @@ Win32Proj 10.0 Application - v143 + v145 Unicode diff --git a/Sources/backend-cpp/Utilities/Utilities.vcxproj b/Sources/backend-cpp/Utilities/Utilities.vcxproj index f79059d3..b1bcc9e2 100644 --- a/Sources/backend-cpp/Utilities/Utilities.vcxproj +++ b/Sources/backend-cpp/Utilities/Utilities.vcxproj @@ -29,26 +29,26 @@ StaticLibrary true - v143 + v145 Unicode StaticLibrary false - v143 + v145 true Unicode StaticLibrary true - v143 + v145 Unicode StaticLibrary false - v143 + v145 true Unicode diff --git a/Sources/com.ctytler.dcs.sdPlugin/manifest.json b/Sources/com.ctytler.dcs.sdPlugin/manifest.json index 5b1ba7a9..b67679be 100644 --- a/Sources/com.ctytler.dcs.sdPlugin/manifest.json +++ b/Sources/com.ctytler.dcs.sdPlugin/manifest.json @@ -21,7 +21,15 @@ "Tooltip": "Button which communicates with DCS-BIOS", "UUID": "com.ctytler.dcs.dcs-bios", "PropertyInspectorPath": "propertyinspector/dcs_bios_prop_inspector.html", - "ShowTitle": false + "ShowTitle": false, + "Encoder": { + "layout": "$A1", + "TriggerDescription": { + "Rotate": "Rotate to increment/decrement", + "Push": "Press to activate", + "Touch": "Touch to activate" + } + } }, { "Icon": "images/Chrome_Switch_Low", @@ -44,7 +52,15 @@ "Tooltip": "Button press depends on displayed state", "UUID": "com.ctytler.dcs.up-down.switch.two-state", "PropertyInspectorPath": "propertyinspector/index.html", - "ShowTitle": false + "ShowTitle": false, + "Encoder": { + "layout": "$A1", + "TriggerDescription": { + "Rotate": "Rotate to switch state", + "Push": "Press to toggle", + "Touch": "Touch to toggle" + } + } }, { "Icon": "images/button_light_on", @@ -122,7 +138,15 @@ "SupportedInMultiActions": false, "Tooltip": "Incremental input button can be used for dials/switches/multi-position", "UUID": "com.ctytler.dcs.increment.dial.two-state", - "PropertyInspectorPath": "propertyinspector/index.html" + "PropertyInspectorPath": "propertyinspector/index.html", + "Encoder": { + "layout": "$A1", + "TriggerDescription": { + "Rotate": "Rotate to increment/decrement", + "Push": "Press to activate", + "Touch": "Touch to activate" + } + } }, { "Icon": "images/Rotary_Switch_Half_0", @@ -144,7 +168,61 @@ "SupportedInMultiActions": false, "Tooltip": "Incremental input button with space for text above", "UUID": "com.ctytler.dcs.increment.textdial.two-state", - "PropertyInspectorPath": "propertyinspector/index.html" + "PropertyInspectorPath": "propertyinspector/index.html", + "Encoder": { + "layout": "$A1", + "TriggerDescription": { + "Rotate": "Rotate to increment/decrement", + "Push": "Press to activate", + "Touch": "Touch to activate" + } + } + }, + { + "Icon": "images/Rotary_Dial_0", + "Name": "DCS Rotary Encoder", + "States": [ + { + "Image": "images/Rotary_Dial_0" + } + ], + "Controllers": ["Encoder"], + "SupportedInMultiActions": false, + "Tooltip": "Rotary encoder for Stream Deck Plus - Incremental input control", + "UUID": "com.ctytler.dcs.encoder.rotary", + "PropertyInspectorPath": "propertyinspector/encoder_prop_inspector.html", + "Encoder": { + "layout": "$B2", + "background": "images/Rotary_Dial_0", + "TriggerDescription": { + "Rotate": "Rotate to increment/decrement value", + "Push": "Press encoder to send command", + "Touch": "Touch to activate" + } + } + }, + { + "Icon": "images/Rotary_Switch_Half_0", + "Name": "DCS Rotary Encoder (Text Display)", + "States": [ + { + "Image": "images/Rotary_Switch_Half_0" + } + ], + "Controllers": ["Encoder"], + "SupportedInMultiActions": false, + "Tooltip": "Rotary encoder with text display for Stream Deck Plus", + "UUID": "com.ctytler.dcs.encoder.rotary.text", + "PropertyInspectorPath": "propertyinspector/encoder_prop_inspector.html", + "Encoder": { + "layout": "$B2", + "background": "images/Rotary_Switch_Half_0", + "TriggerDescription": { + "Rotate": "Rotate to increment/decrement value", + "Push": "Press encoder to send command", + "Touch": "Touch to activate" + } + } } ], "SDKVersion": 2, diff --git a/Sources/com.ctytler.dcs.sdPlugin/propertyinspector/encoder_prop_inspector.html b/Sources/com.ctytler.dcs.sdPlugin/propertyinspector/encoder_prop_inspector.html new file mode 100644 index 00000000..37870ec2 --- /dev/null +++ b/Sources/com.ctytler.dcs.sdPlugin/propertyinspector/encoder_prop_inspector.html @@ -0,0 +1,314 @@ + + + + + + + + + DCS Rotary Encoder Property Inspector + + + + + + + Encoder Rotation Settings (Incremental Input) + + + Button ID + + Clear + + + + + Device ID + + + + + + DCS ID (Monitor) + + + + + Increment CW (Clockwise per tick) + + + + + Increment CCW (Counter-Clockwise per tick) + + + + + Increment range min/max + + + + + + Cycle Settings + + Allow cycling to beginning + + + + + + Encoder Press Settings (Fixed Value) + + + Value to Send on Press + + + + + + When you press the encoder button, this fixed value will be sent. + Leave empty to send the minimum value by default. + + + + + + DCS Display Settings + + + Value Mappings + + Add + + + + + + + + + + + + + Additional Help + + + + ID Lookup + + + Help + + + DCS Comms + + + + + + + + + + + + + + + + + + + + + +