diff --git a/README.md b/README.md index 59823d48..7871fc69 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ - [Demo of Operation](#demo-of-operation) - [Installation](#installation) - [Downloads](#downloads) + - [Version Update](#version-update) - [Initial Configuration](#initial-configuration) - [Video Walkthrough](#video-walkthrough) - [Source code](#source-code) @@ -50,6 +51,10 @@ More detailed instructions can be found in: [Settings Help Documentation](Source - Also within [Releases](https://github.com/charlestytler/streamdeck-dcs-interface/releases) is an optional `icon_library.zip` you can download for use with Streamdeck Profiles. +### Version Update + +If you have a prior version already installed on your StreamDeck, you will have to uninstall it first before installing the latest version. To do this click the "More Actions..." button at the bottom-right of the StreamDeck GUI and click "Uninstall" next to the DCS Interface plugin. + ### Initial Configuration If you plan to only use DCS Interface for Streamdeck with the DCS-ExportScript and not [Ikarus](https://github.com/s-d-a/Ikarus), you can modify the file `DCS-ExportScript\Config.lua` to have the following settings (where `IkarusPort` is changed from `1625` to `1725` for DCS Interface) to get everything connected: diff --git a/Release/com.ctytler.dcs.streamDeckPlugin b/Release/com.ctytler.dcs.streamDeckPlugin index d9750dba..f052fb82 100644 Binary files a/Release/com.ctytler.dcs.streamDeckPlugin and b/Release/com.ctytler.dcs.streamDeckPlugin differ diff --git a/Sources/DcsInterface/StreamdeckContext.cpp b/Sources/DcsInterface/StreamdeckContext.cpp index ebdb0d37..8e1ae8f7 100644 --- a/Sources/DcsInterface/StreamdeckContext.cpp +++ b/Sources/DcsInterface/StreamdeckContext.cpp @@ -46,12 +46,23 @@ void StreamdeckContext::updateContextState(DcsInterface *dcs_interface, ESDConne current_title_ = updated_title; mConnectionManager->SetTitle(current_title_, context_, kESDSDKTarget_HardwareAndSoftware); } + + if (delay_for_force_send_state_) { + if (delay_for_force_send_state_.value()-- <= 0) { + mConnectionManager->SetState(static_cast(current_state_), context_); + delay_for_force_send_state_.reset(); + } + } } void StreamdeckContext::forceSendState(ESDConnectionManager *mConnectionManager) { mConnectionManager->SetState(static_cast(current_state_), context_); } +void StreamdeckContext::forceSendStateAfterDelay(const int delay_count) { + delay_for_force_send_state_.emplace(delay_count); +} + void StreamdeckContext::updateContextSettings(const json &settings) { // Read in settings. const std::string dcs_id_increment_monitor_raw = @@ -190,7 +201,7 @@ bool StreamdeckContext::determineSendValueForSwitch(const KeyEvent event, const ContextState state, const json &settings, std::string &value) { - if (event == KEY_DOWN) { + if (event == KEY_UP) { if (state == FIRST) { value = EPLJSONUtils::GetStringByName(settings, "send_when_first_state_value"); } else { diff --git a/Sources/DcsInterface/StreamdeckContext.h b/Sources/DcsInterface/StreamdeckContext.h index 8a018b18..efbda569 100644 --- a/Sources/DcsInterface/StreamdeckContext.h +++ b/Sources/DcsInterface/StreamdeckContext.h @@ -10,6 +10,7 @@ #include "../Common/ESDConnectionManager.h" #endif +#include #include using KeyEvent = enum { KEY_DOWN, KEY_UP }; @@ -29,13 +30,21 @@ class StreamdeckContext { void updateContextState(DcsInterface *dcs_interface, ESDConnectionManager *mConnectionManager); /** - * @brief Forces an update to the Streamdeck of the context's current state be sent. + * @brief Forces an update to the Streamdeck of the context's current state be sent with current static values. * (Normally an update is sent to the Streamdeck only on change of current state). * * @param mConnectionManager Interface to StreamDeck. */ void forceSendState(ESDConnectionManager *mConnectionManager); + /** + * @brief Forces an update to the Streamdeck of the context's current state be sent after a specified delay. + * (Normally an update is sent to the Streamdeck only on change of current state). + * + * @param delay_count Number of frames before a force send of the context state is sent. + */ + void forceSendStateAfterDelay(const int delay_count); + /** * @brief Updates settings from received json payload. * @@ -100,6 +109,10 @@ class StreamdeckContext { bool compare_monitor_is_set_ = false; // True if all DCS ID comparison monitor settings have been set. bool string_monitor_is_set_ = false; // True if all DCS ID string monitor settings have been set. + // Optional settings. + std::optional delay_for_force_send_state_; // When populated, requests a force send of state to Streamdeck + // after counting down the stored delay value. + // Context state. ContextState current_state_ = FIRST; // Stored state of the context. std::string current_title_ = ""; // Stored title of the context. diff --git a/Sources/MyStreamDeckPlugin.cpp b/Sources/MyStreamDeckPlugin.cpp index 3449f84a..9d7fa114 100644 --- a/Sources/MyStreamDeckPlugin.cpp +++ b/Sources/MyStreamDeckPlugin.cpp @@ -140,10 +140,6 @@ void MyStreamDeckPlugin::KeyDownForAction(const std::string &inAction, if (dcs_interface_ != nullptr) { mVisibleContextsMutex.lock(); mVisibleContexts[inContext].handleButtonEvent(dcs_interface_, KEY_DOWN, inAction, inPayload); - // The Streamdeck will by default change a context's state after a button action, so a force send of the - // current context's state will keep the button state in sync with the plugin. (Not performed for switches - // as generally the change in state is desired there). - mVisibleContexts[inContext].forceSendState(mConnectionManager); mVisibleContextsMutex.unlock(); } } @@ -155,10 +151,16 @@ void MyStreamDeckPlugin::KeyUpForAction(const std::string &inAction, if (dcs_interface_ != nullptr) { mVisibleContextsMutex.lock(); - mVisibleContexts[inContext].handleButtonEvent(dcs_interface_, KEY_UP, inAction, inPayload); - // The Streamdeck will by default change a context's state after a button action, so a force send of the current + // The Streamdeck will by default change a context's state after a KeyUp event, so a force send of the current // context's state will keep the button state in sync with the plugin. - mVisibleContexts[inContext].forceSendState(mConnectionManager); + if (inAction.find("switch") != std::string::npos) { + // For switches use a delay to avoid jittering and a race condition of Streamdeck and Plugin trying to + // change state. + mVisibleContexts[inContext].forceSendStateAfterDelay(3); + } else { + mVisibleContexts[inContext].forceSendState(mConnectionManager); + } + mVisibleContexts[inContext].handleButtonEvent(dcs_interface_, KEY_UP, inAction, inPayload); mVisibleContextsMutex.unlock(); } } @@ -173,7 +175,7 @@ void MyStreamDeckPlugin::WillAppearForAction(const std::string &inAction, EPLJSONUtils::GetObjectByName(inPayload, "settings", settings); mVisibleContexts[inContext] = StreamdeckContext(inContext, settings); if (dcs_interface_ != nullptr) { - mVisibleContexts[inContext].updateContextState(dcs_interface_, mConnectionManager); + mVisibleContexts[inContext].forceSendState(mConnectionManager); } mVisibleContextsMutex.unlock(); } diff --git a/Sources/Test/StreamdeckContextTest.cpp b/Sources/Test/StreamdeckContextTest.cpp index 1597dfc0..5602c283 100644 --- a/Sources/Test/StreamdeckContextTest.cpp +++ b/Sources/Test/StreamdeckContextTest.cpp @@ -373,6 +373,46 @@ TEST_F(StreamdeckContextTestFixture, force_send_state_update) { EXPECT_EQ(esd_connection_manager.state_, 0); } +TEST_F(StreamdeckContextTestFixture, force_send_state_update_with_zero_delay) { + // Test 1 -- With updateContextState and no detected state changes, no state is sent to connection manager. + fixture_context.updateContextState(&dcs_interface, &esd_connection_manager); + EXPECT_EQ(esd_connection_manager.context_, ""); + + // Test -- force send will send current state regardless of state change. + int delay_count = 0; + fixture_context.forceSendStateAfterDelay(delay_count); + fixture_context.updateContextState(&dcs_interface, &esd_connection_manager); + EXPECT_EQ(esd_connection_manager.context_, "abc123"); + EXPECT_EQ(esd_connection_manager.state_, 0); +} + +TEST_F(StreamdeckContextTestFixture, force_send_state_update_after_delay) { + // Test -- force send will send current state regardless of state change. + int delay_count = 3; + fixture_context.forceSendStateAfterDelay(delay_count); + while (delay_count > 0) { + fixture_context.updateContextState(&dcs_interface, &esd_connection_manager); + EXPECT_EQ(esd_connection_manager.context_, ""); + delay_count--; + } + fixture_context.updateContextState(&dcs_interface, &esd_connection_manager); + EXPECT_EQ(esd_connection_manager.context_, "abc123"); + EXPECT_EQ(esd_connection_manager.state_, 0); +} + +TEST_F(StreamdeckContextTestFixture, force_send_state_update_negative_delay) { + // Test 1 -- With updateContextState and no detected state changes, no state is sent to connection manager. + fixture_context.updateContextState(&dcs_interface, &esd_connection_manager); + EXPECT_EQ(esd_connection_manager.context_, ""); + + // Test -- force send will occur as delay is already less than zero. + int delay_count = -3; + fixture_context.forceSendStateAfterDelay(delay_count); + fixture_context.updateContextState(&dcs_interface, &esd_connection_manager); + EXPECT_EQ(esd_connection_manager.context_, "abc123"); + EXPECT_EQ(esd_connection_manager.state_, 0); +} + class StreamdeckContextKeyPressTestFixture : public StreamdeckContextTestFixture { public: StreamdeckContextKeyPressTestFixture() @@ -459,38 +499,38 @@ TEST_F(StreamdeckContextKeyPressTestFixture, handle_keydown_momentary_empty_valu EXPECT_EQ(expected_command, ss_received.str()); } -TEST_F(StreamdeckContextKeyPressTestFixture, handle_keydown_switch_in_first_state) { +TEST_F(StreamdeckContextKeyPressTestFixture, handle_keyup_switch_in_first_state) { const std::string action = "com.ctytler.dcs.switch.two-state"; - fixture_context.handleButtonEvent(&dcs_interface, KEY_DOWN, action, payload); + fixture_context.handleButtonEvent(&dcs_interface, KEY_UP, action, payload); const std::stringstream ss_received = mock_dcs.DcsReceive(); std::string expected_command = "C" + device_id + "," + std::to_string(button_id) + "," + send_when_first_state_value; EXPECT_EQ(expected_command, ss_received.str()); } -TEST_F(StreamdeckContextKeyPressTestFixture, handle_keydown_switch_in_second_state) { +TEST_F(StreamdeckContextKeyPressTestFixture, handle_keyup_switch_in_second_state) { payload["state"] = 1; const std::string action = "com.ctytler.dcs.switch.two-state"; - fixture_context.handleButtonEvent(&dcs_interface, KEY_DOWN, action, payload); + fixture_context.handleButtonEvent(&dcs_interface, KEY_UP, action, payload); const std::stringstream ss_received = mock_dcs.DcsReceive(); std::string expected_command = "C" + device_id + "," + std::to_string(button_id) + "," + send_when_second_state_value; EXPECT_EQ(expected_command, ss_received.str()); } -TEST_F(StreamdeckContextKeyPressTestFixture, handle_keyup_switch) { +TEST_F(StreamdeckContextKeyPressTestFixture, handle_keydown_switch) { const std::string action = "com.ctytler.dcs.switch.two-state"; - fixture_context.handleButtonEvent(&dcs_interface, KEY_UP, action, payload); + fixture_context.handleButtonEvent(&dcs_interface, KEY_DOWN, action, payload); const std::stringstream ss_received = mock_dcs.DcsReceive(); // Expect no command sent (empty string is due to mock socket functionality). std::string expected_command = ""; EXPECT_EQ(expected_command, ss_received.str()); } -TEST_F(StreamdeckContextKeyPressTestFixture, handle_keydown_switch_empty_value) { +TEST_F(StreamdeckContextKeyPressTestFixture, handle_keyup_switch_empty_value) { payload["settings"]["send_when_first_state_value"] = ""; const std::string action = "com.ctytler.dcs.switch.two-state"; - fixture_context.handleButtonEvent(&dcs_interface, KEY_DOWN, action, payload); + fixture_context.handleButtonEvent(&dcs_interface, KEY_UP, action, payload); const std::stringstream ss_received = mock_dcs.DcsReceive(); std::string expected_command = ""; EXPECT_EQ(expected_command, ss_received.str()); diff --git a/Sources/com.ctytler.dcs.sdPlugin/dcs_interface.exe b/Sources/com.ctytler.dcs.sdPlugin/dcs_interface.exe index bb41ebfe..f5440358 100644 Binary files a/Sources/com.ctytler.dcs.sdPlugin/dcs_interface.exe and b/Sources/com.ctytler.dcs.sdPlugin/dcs_interface.exe differ diff --git a/Sources/com.ctytler.dcs.sdPlugin/helpDocs/helpContents.md b/Sources/com.ctytler.dcs.sdPlugin/helpDocs/helpContents.md index 021d08af..77c9b3de 100644 --- a/Sources/com.ctytler.dcs.sdPlugin/helpDocs/helpContents.md +++ b/Sources/com.ctytler.dcs.sdPlugin/helpDocs/helpContents.md @@ -51,6 +51,8 @@ The momentary buttons are used to send commands to clickabledata items of BTN ty _Note: Repetition of sent value while button is held pressed is not supported at this time._ +**Use with Axis (LEV) Type** -- Some of the radio frequency and comms channel selectors are designed to accept axis input, and they expect a single value to increment their value. For example a volume knob with limits 0,1 can be rotated with small rotations by setting "Send Value while Pressed" to `0.01` and disabling the release value (as the release value will just interfere). To rotate a greater amount per press, increase the send value. + ### Increment Settings Increment buttons are used for clickabledata items of TUMB type, which are rotary dials or other items that have multiple values you want to iterate through. @@ -167,7 +169,7 @@ _Click Value_ -- The value sent for a left/right click of the mouse. May have di - BTN: Click value is generally the "pressed" button value (usually 1). - TUMB (Rotary Knobs): This is the value the knob will be incremented within the range (usually +/-0.1). - TUMB (Switches): This is the value the switch will be incremented within the range on click (usually +/-1, but sometimes +/-0.1 also). -- LEV: Click value is the increment value within the range, however is often listed as 0 in the table. For these you will need to manually enter a Send value in Command settings that provides a desirable response. +- LEV: Click value is the increment value within the range, however is often listed as 0 in the table. For these you will need to manually enter a Send value in Command settings that provides a desirable response. (Note: depending on the item you will need either an increment type to vary the values sent, or a momentary button with the "Send on Release" disabled to provide a constant increase/decrease value). _Limit Min_ -- The minimum value the item can be commanded to. _Limit Max_ -- The maximum value the item can be commanded to. diff --git a/Sources/com.ctytler.dcs.sdPlugin/helpDocs/helpWindow.html b/Sources/com.ctytler.dcs.sdPlugin/helpDocs/helpWindow.html index 3fdb1a38..0c9441dd 100644 --- a/Sources/com.ctytler.dcs.sdPlugin/helpDocs/helpWindow.html +++ b/Sources/com.ctytler.dcs.sdPlugin/helpDocs/helpWindow.html @@ -84,7 +84,8 @@

DCS Command (on Button Press) Sett

The remaining settings differ according to what type of DCS Interface button is used and can be categorized into Momentary, Increment, and Switch.

Momentary Button settings

-

The momentary buttons are used to send commands to clickabledata items of BTN type.

+

The momentary buttons are used to send commands to clickabledata items of BTN type, or LEV type in special cases, + see note below.

Send Value while Pressed -- This is the value that is sent immediately upon pressing the Streamdeck button. By default most buttons will use 1 for a pressed value. Sometimes pairs of buttons (such as up/down arrow buttons on a panel) will have 1 as the pressed value for the up @@ -96,6 +97,11 @@

Momentary Button settings

Disable (Send Release) Check -- The checkbox next to the "Send Value while Released" will disable any command being sent when releasing a button.

Note: Repetition of sent value while button is held pressed is not supported at this time.

+

Use with Axis (LEV) Type -- Some of the radio frequency and comms channel selectors are designed + to accept axis input, and they expect a single value to increment their value. For example a volume knob with + limits 0,1 can be rotated with small rotations by setting "Send Value while Pressed" to `0.01` and disabling the + release value (as the release value will just interfere). To rotate a greater amount per press, increase the + send value.

Increment Settings

Increment buttons are used for clickabledata items of TUMB type, which are rotary dials or other items that have multiple values you want to iterate through.

@@ -238,6 +244,8 @@

Aircraft Module Clickabledata

but sometimes +/-0.1 also).
  • LEV: Click value is the increment value within the range, however is often listed as 0 in the table. For these you will need to manually enter a Send value in Command settings that provides a desirable response. + (Note: depending on the item you will need either an increment type to vary the values sent, or a momentary + button with the "Send on Release" disabled to provide a constant increase/decrease value).
  • Limit Min -- The minimum value the item can be commanded to.
    diff --git a/Sources/com.ctytler.dcs.sdPlugin/propertyinspector/id_lookup_window.html b/Sources/com.ctytler.dcs.sdPlugin/propertyinspector/id_lookup_window.html index 1370ce35..ccdae66a 100644 --- a/Sources/com.ctytler.dcs.sdPlugin/propertyinspector/id_lookup_window.html +++ b/Sources/com.ctytler.dcs.sdPlugin/propertyinspector/id_lookup_window.html @@ -157,7 +157,8 @@ TUMB: Rotary/Switch - use increment to advance within range, or switch to select two values
    LEV: Axis - use increment to advance within range, increment value can be adjusted - for sensitivity + for sensitivity. Some LEV items will need to use the momentary button with the "Send on + Release" disabled.