diff --git a/Arduino/MassiveKnob/MassiveKnob.ino b/Arduino/MassiveKnob/MassiveKnob.ino index 291bc8e..9829040 100644 --- a/Arduino/MassiveKnob/MassiveKnob.ino +++ b/Arduino/MassiveKnob/MassiveKnob.ino @@ -5,13 +5,15 @@ * */ // Set this to the number of potentiometers you have connected -const byte AnalogInputCount = 3; +const byte AnalogInputCount = 2; // Set this to the number of buttons you have connected -const byte DigitalInputCount = 0; +const byte DigitalInputCount = 3; -// Not supported yet - maybe PWM and/or other means of analog output? -const byte AnalogOutputCount = 0; +// Set this to the number of PWM outputs you have connected +// Note that this version of the sketch only does a simple analogWrite with the full range, +// which is not compatible with servos. Modify as required. +const byte AnalogOutputCount = 3; // Set this to the number of digital outputs you have connected const byte DigitalOutputCount = 0; @@ -20,14 +22,27 @@ const byte DigitalOutputCount = 0; // For each potentiometer, specify the pin const byte AnalogInputPin[AnalogInputCount] = { A0, - A1, - A2 + A1 }; // For each button, specify the pin. Assumes pull-up. const byte DigitalInputPin[DigitalInputCount] = { + 7, + 8, + 9 }; +// For each analog output, specify the PWM capable pin +const byte AnalogOutputPin[AnalogOutputCount] = { + 3, + 5, + 6 +}; + +// Define this constant to apply a standard LED brightness curve to (all) analog outputs +#define AnalogOutputGammaCorrection + + // For each digital output, specify the pin const byte DigitalOutputPin[DigitalOutputCount] = { }; @@ -107,7 +122,7 @@ struct DigitalInputStatus struct AnalogInputStatus analogInputStatus[AnalogInputCount]; -struct DigitalInputStatus digitalInputStatus[AnalogInputCount]; +struct DigitalInputStatus digitalInputStatus[DigitalInputCount]; void setup() @@ -143,6 +158,13 @@ void setup() analogInputStatus[i].LastChange = millis(); } + // Set up analog outputs + for (byte i = 0; i < AnalogOutputCount; i++) + { + pinMode(AnalogOutputPin[i], OUTPUT); + analogWrite(AnalogOutputPin[i], 0); + } + // Set up digital inputs and outputs for (byte i = 0; i < DigitalInputCount; i++) @@ -181,6 +203,27 @@ unsigned long focusOutputTime; #define IsDigitalInputFocus(i) ((focusType == FocusInputType.DigitalInput) && (focusInputIndex == i)) +#ifdef AnalogOutputGammaCorrection +const uint8_t PROGMEM gamma8[] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, + 2, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 5, 5, 5, + 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, + 10, 10, 11, 11, 11, 12, 12, 13, 13, 13, 14, 14, 15, 15, 16, 16, + 17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22, 23, 24, 24, 25, + 25, 26, 27, 27, 28, 29, 29, 30, 31, 32, 32, 33, 34, 35, 35, 36, + 37, 38, 39, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 50, + 51, 52, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 66, 67, 68, + 69, 70, 72, 73, 74, 75, 77, 78, 79, 81, 82, 83, 85, 86, 87, 89, + 90, 92, 93, 95, 96, 98, 99,101,102,104,105,107,109,110,112,114, + 115,117,119,120,122,124,126,127,129,131,133,135,137,138,140,142, + 144,146,148,150,152,154,156,158,160,162,164,167,169,171,173,175, + 177,180,182,184,186,189,191,193,196,198,200,203,205,208,210,213, + 215,218,220,223,225,228,231,233,236,239,241,244,247,249,252,255 }; +#endif + + void loop() { #ifndef DebugOutputPlotter @@ -291,6 +334,8 @@ void loop() } Serial.println(); + + lastOutput = millis(); } #endif } @@ -306,7 +351,7 @@ void min_application_handler(uint8_t min_id, uint8_t *min_payload, uint8_t len_p break; case FrameIDAnalogOutput: - //processAnalogOutputMessage(); + processAnalogOutputMessage(min_payload, len_payload); break; case FrameIDDigitalOutput: @@ -364,6 +409,36 @@ void processDigitalOutputMessage(uint8_t *min_payload, uint8_t len_payload) } +void processAnalogOutputMessage(uint8_t *min_payload, uint8_t len_payload) +{ + if (len_payload < 2) + { + outputError("Invalid analog output payload length"); + return; + } + + byte outputIndex = min_payload[0]; + if (outputIndex < AnalogOutputCount) + { + byte value = min_payload[1]; + if (value > 100) + value = 100; + + value = map(value, 0, 100, 0, 255); + + #ifdef AnalogOutputGammaCorrection + value = pgm_read_byte(&gamma8[value]); + #endif + + analogWrite(AnalogOutputPin[min_payload[0]], value); + + focusType = FocusTypeOutput; + focusOutputTime = millis(); + } + else + outputError("Invalid analog output index: " + String(outputIndex)); +} + void processQuitMessage() { active = false; diff --git a/Windows/MassiveKnob.Plugin.CoreAudio/Base/BaseDeviceSettingsViewModel.cs b/Windows/MassiveKnob.Plugin.CoreAudio/Base/BaseDeviceSettingsViewModel.cs index 9f89426..c560580 100644 --- a/Windows/MassiveKnob.Plugin.CoreAudio/Base/BaseDeviceSettingsViewModel.cs +++ b/Windows/MassiveKnob.Plugin.CoreAudio/Base/BaseDeviceSettingsViewModel.cs @@ -82,7 +82,7 @@ namespace MassiveKnob.Plugin.CoreAudio.Base .Select(PlaybackDeviceViewModel.FromDevice) .ToList(); - Application.Current.Dispatcher.Invoke(() => + Application.Current?.Dispatcher.Invoke(() => { PlaybackDevices = deviceViewModels; SelectedDevice = deviceViewModels.SingleOrDefault(d => d.Id == settings.DeviceId); diff --git a/Windows/MassiveKnob.Plugin.CoreAudio/GetDefault/DeviceGetDefaultAction.cs b/Windows/MassiveKnob.Plugin.CoreAudio/GetDefault/DeviceGetDefaultAction.cs index 17c184f..4ee16ea 100644 --- a/Windows/MassiveKnob.Plugin.CoreAudio/GetDefault/DeviceGetDefaultAction.cs +++ b/Windows/MassiveKnob.Plugin.CoreAudio/GetDefault/DeviceGetDefaultAction.cs @@ -44,15 +44,17 @@ namespace MassiveKnob.Plugin.CoreAudio.GetDefault private void ApplySettings() { - if (playbackDevice != null && playbackDevice.Id == settings.DeviceId) - return; - - var coreAudioController = CoreAudioControllerInstance.Acquire(); - playbackDevice = settings.DeviceId.HasValue ? coreAudioController.GetDevice(settings.DeviceId.Value) : null; + if (playbackDevice == null || playbackDevice.Id != settings.DeviceId) + { + var coreAudioController = CoreAudioControllerInstance.Acquire(); + playbackDevice = settings.DeviceId.HasValue + ? coreAudioController.GetDevice(settings.DeviceId.Value) + : null; + + deviceChanged?.Dispose(); + deviceChanged = playbackDevice?.PropertyChanged.Subscribe(PropertyChanged); + } - deviceChanged?.Dispose(); - deviceChanged = playbackDevice?.PropertyChanged.Subscribe(PropertyChanged); - CheckActive(); } diff --git a/Windows/MassiveKnob.Plugin.CoreAudio/GetMuted/DeviceGetMutedAction.cs b/Windows/MassiveKnob.Plugin.CoreAudio/GetMuted/DeviceGetMutedAction.cs index 01b8476..a6c6035 100644 --- a/Windows/MassiveKnob.Plugin.CoreAudio/GetMuted/DeviceGetMutedAction.cs +++ b/Windows/MassiveKnob.Plugin.CoreAudio/GetMuted/DeviceGetMutedAction.cs @@ -44,15 +44,17 @@ namespace MassiveKnob.Plugin.CoreAudio.GetMuted private void ApplySettings() { - if (playbackDevice != null && playbackDevice.Id == settings.DeviceId) - return; - - var coreAudioController = CoreAudioControllerInstance.Acquire(); - playbackDevice = settings.DeviceId.HasValue ? coreAudioController.GetDevice(settings.DeviceId.Value) : null; + if (playbackDevice == null || playbackDevice.Id != settings.DeviceId) + { + var coreAudioController = CoreAudioControllerInstance.Acquire(); + playbackDevice = settings.DeviceId.HasValue + ? coreAudioController.GetDevice(settings.DeviceId.Value) + : null; + + deviceChanged?.Dispose(); + deviceChanged = playbackDevice?.MuteChanged.Subscribe(MuteChanged); + } - deviceChanged?.Dispose(); - deviceChanged = playbackDevice?.MuteChanged.Subscribe(MuteChanged); - if (playbackDevice != null) actionContext.SetDigitalOutput(settings.Inverted ? !playbackDevice.IsMuted : playbackDevice.IsMuted); } diff --git a/Windows/MassiveKnob.Plugin.CoreAudio/GetVolume/DeviceGetVolumeAction.cs b/Windows/MassiveKnob.Plugin.CoreAudio/GetVolume/DeviceGetVolumeAction.cs index 1de4061..1fef7d8 100644 --- a/Windows/MassiveKnob.Plugin.CoreAudio/GetVolume/DeviceGetVolumeAction.cs +++ b/Windows/MassiveKnob.Plugin.CoreAudio/GetVolume/DeviceGetVolumeAction.cs @@ -44,15 +44,17 @@ namespace MassiveKnob.Plugin.CoreAudio.GetVolume private void ApplySettings() { - if (playbackDevice != null && playbackDevice.Id == settings.DeviceId) - return; - - var coreAudioController = CoreAudioControllerInstance.Acquire(); - playbackDevice = settings.DeviceId.HasValue ? coreAudioController.GetDevice(settings.DeviceId.Value) : null; + if (playbackDevice == null || playbackDevice.Id != settings.DeviceId) + { + var coreAudioController = CoreAudioControllerInstance.Acquire(); + playbackDevice = settings.DeviceId.HasValue + ? coreAudioController.GetDevice(settings.DeviceId.Value) + : null; + + deviceChanged?.Dispose(); + deviceChanged = playbackDevice?.VolumeChanged.Subscribe(VolumeChanged); + } - deviceChanged?.Dispose(); - deviceChanged = playbackDevice?.VolumeChanged.Subscribe(VolumeChanged); - if (playbackDevice != null) actionContext.SetAnalogOutput((byte)playbackDevice.Volume); } diff --git a/Windows/MassiveKnob.Plugin.CoreAudio/OSD/OSDManager.cs b/Windows/MassiveKnob.Plugin.CoreAudio/OSD/OSDManager.cs index 77ffd61..ad134ce 100644 --- a/Windows/MassiveKnob.Plugin.CoreAudio/OSD/OSDManager.cs +++ b/Windows/MassiveKnob.Plugin.CoreAudio/OSD/OSDManager.cs @@ -15,7 +15,7 @@ namespace MassiveKnob.Plugin.CoreAudio.OSD public static void Show(IDevice device) { - Application.Current.Dispatcher.Invoke(() => + Application.Current?.Dispatcher.Invoke(() => { if (window == null) { @@ -41,9 +41,11 @@ namespace MassiveKnob.Plugin.CoreAudio.OSD { hideTimer?.Dispose(); hideTimer = null; + + window = null; } - + private static void Hide() { Application.Current?.Dispatcher.Invoke(() => diff --git a/Windows/MassiveKnob.Plugin.SerialDevice/Worker/SerialWorker.cs b/Windows/MassiveKnob.Plugin.SerialDevice/Worker/SerialWorker.cs index 1449597..c55a2cc 100644 --- a/Windows/MassiveKnob.Plugin.SerialDevice/Worker/SerialWorker.cs +++ b/Windows/MassiveKnob.Plugin.SerialDevice/Worker/SerialWorker.cs @@ -49,8 +49,6 @@ namespace MassiveKnob.Plugin.SerialDevice.Worker public void Connect(string portName, int baudRate, bool dtrEnable) { - context.Connecting(); - lock (minProtocolLock) { if (portName == lastPortName && baudRate == lastBaudRate && dtrEnable == lastDtrEnable) @@ -61,11 +59,12 @@ namespace MassiveKnob.Plugin.SerialDevice.Worker lastDtrEnable = dtrEnable; Disconnect(); + context.Connecting(); if (string.IsNullOrEmpty(portName) || baudRate == 0) return; - + minProtocol?.Dispose(); minProtocol = new MINProtocol(new MINSerialTransport(portName, baudRate, dtrEnable: dtrEnable), logger); minProtocol.OnConnected += MinProtocolOnOnConnected; diff --git a/Windows/MassiveKnob.Plugin.VoiceMeeter/GetParameter/VoiceMeeterGetParameterAction.cs b/Windows/MassiveKnob.Plugin.VoiceMeeter/GetParameter/VoiceMeeterGetParameterAction.cs index ac76b15..bce5bd2 100644 --- a/Windows/MassiveKnob.Plugin.VoiceMeeter/GetParameter/VoiceMeeterGetParameterAction.cs +++ b/Windows/MassiveKnob.Plugin.VoiceMeeter/GetParameter/VoiceMeeterGetParameterAction.cs @@ -26,7 +26,6 @@ namespace MassiveKnob.Plugin.VoiceMeeter.GetParameter private IMassiveKnobActionContext actionContext; private VoiceMeeterGetParameterActionSettings settings; private VoiceMeeterGetParameterActionSettingsViewModel viewModel; - private Parameters parameters; private IDisposable parameterChanged; @@ -44,7 +43,6 @@ namespace MassiveKnob.Plugin.VoiceMeeter.GetParameter { InstanceRegister.Unregister(this); parameterChanged?.Dispose(); - parameters?.Dispose(); } @@ -54,17 +52,11 @@ namespace MassiveKnob.Plugin.VoiceMeeter.GetParameter { parameterChanged?.Dispose(); parameterChanged = null; - - parameters?.Dispose(); - parameters = null; return; } - if (parameters == null) - parameters = new Parameters(); - if (parameterChanged == null) - parameterChanged = parameters.Subscribe(x => ParametersChanged()); + parameterChanged = InstanceRegister.SubscribeToParameterChanges(ParametersChanged); ParametersChanged(); } diff --git a/Windows/MassiveKnob.Plugin.VoiceMeeter/InstanceRegister.cs b/Windows/MassiveKnob.Plugin.VoiceMeeter/InstanceRegister.cs index e04e945..f61fa4b 100644 --- a/Windows/MassiveKnob.Plugin.VoiceMeeter/InstanceRegister.cs +++ b/Windows/MassiveKnob.Plugin.VoiceMeeter/InstanceRegister.cs @@ -11,6 +11,13 @@ namespace MassiveKnob.Plugin.VoiceMeeter private static readonly HashSet Instances = new HashSet(); private static Task initializeTask; + private static readonly object SubscribersLock = new object(); + private static Parameters parameters; + private static IDisposable parametersSubscriber; + private static readonly List ParameterSubscriberActions = new List(); + + private static IDisposable voicemeeterClient; + // The VoiceMeeter Remote only connects to one instance, so all actions need to be in sync private static RunVoicemeeterParam version; @@ -27,7 +34,8 @@ namespace MassiveKnob.Plugin.VoiceMeeter initializeTask = Task.Run(async () => { - await global::VoiceMeeter.Remote.Initialize(version); + voicemeeterClient?.Dispose(); + voicemeeterClient = await global::VoiceMeeter.Remote.Initialize(version); }); } } @@ -37,6 +45,55 @@ namespace MassiveKnob.Plugin.VoiceMeeter { return initializeTask ?? Task.CompletedTask; } + + + // For the same reason, we can only subscribe to the parameters once, as they will not be "dirty" + // for other subscribers otherwise + public static IDisposable SubscribeToParameterChanges(Action action) + { + lock (SubscribersLock) + { + if (parameters == null) + { + parameters = new Parameters(); + parametersSubscriber = parameters.Subscribe(x => NotifyParameterSubscribers()); + } + + ParameterSubscriberActions.Add(action); + return new ParametersSubscriber(action); + } + } + + + private static void NotifyParameterSubscribers() + { + Action[] subscribers; + + lock (SubscribersLock) + { + subscribers = ParameterSubscriberActions.ToArray(); + } + + foreach (var subscriber in subscribers) + subscriber(); + } + + + private static void RemoveParameterSubscriber(Action action) + { + lock (SubscribersLock) + { + // ReSharper disable once InvertIf + if (ParameterSubscriberActions.Remove(action) && ParameterSubscriberActions.Count == 0) + { + parametersSubscriber.Dispose(); + parametersSubscriber = null; + + parameters.Dispose(); + parameters = null; + } + } + } public static void Register(IVoiceMeeterAction instance) @@ -52,7 +109,12 @@ namespace MassiveKnob.Plugin.VoiceMeeter { lock (InstancesLock) { - Instances.Remove(instance); + // ReSharper disable once InvertIf + if (Instances.Remove(instance) && Instances.Count == 0) + { + voicemeeterClient?.Dispose(); + voicemeeterClient = null; + } } } @@ -65,5 +127,23 @@ namespace MassiveKnob.Plugin.VoiceMeeter action(instance); } } + + + private class ParametersSubscriber : IDisposable + { + private readonly Action action; + + + public ParametersSubscriber(Action action) + { + this.action = action; + } + + + public void Dispose() + { + InstanceRegister.RemoveParameterSubscriber(action); + } + } } } diff --git a/Windows/MassiveKnob.Plugin.VoiceMeeter/Strings.Designer.cs b/Windows/MassiveKnob.Plugin.VoiceMeeter/Strings.Designer.cs index 2cbb168..7460a8c 100644 --- a/Windows/MassiveKnob.Plugin.VoiceMeeter/Strings.Designer.cs +++ b/Windows/MassiveKnob.Plugin.VoiceMeeter/Strings.Designer.cs @@ -70,7 +70,7 @@ namespace MassiveKnob.Plugin.VoiceMeeter { } /// - /// Looks up a localized string similar to Get parameter. + /// Looks up a localized string similar to VoiceMeeter: Get parameter. /// public static string GetParameterName { get { @@ -106,7 +106,7 @@ namespace MassiveKnob.Plugin.VoiceMeeter { } /// - /// Looks up a localized string similar to Run macro. + /// Looks up a localized string similar to VoiceMeeter: Run macro. /// public static string RunMacroName { get { diff --git a/Windows/MassiveKnob.Plugin.VoiceMeeter/Strings.resx b/Windows/MassiveKnob.Plugin.VoiceMeeter/Strings.resx index f688f92..2b62915 100644 --- a/Windows/MassiveKnob.Plugin.VoiceMeeter/Strings.resx +++ b/Windows/MassiveKnob.Plugin.VoiceMeeter/Strings.resx @@ -121,7 +121,7 @@ Turns the output on if the specified parameter equals the specified value. - Get parameter + VoiceMeeter: Get parameter Provides actions to run VoiceMeeter macros or check the current state. @@ -133,7 +133,7 @@ Runs a VoiceMeeter macro when the input turns on. - Run macro + VoiceMeeter: Run macro Inverted (on if the parameter does not equal the value) diff --git a/Windows/MassiveKnob/Core/IMassiveKnobOrchestrator.cs b/Windows/MassiveKnob/Core/IMassiveKnobOrchestrator.cs index 45ef9c2..670bd57 100644 --- a/Windows/MassiveKnob/Core/IMassiveKnobOrchestrator.cs +++ b/Windows/MassiveKnob/Core/IMassiveKnobOrchestrator.cs @@ -18,6 +18,9 @@ namespace MassiveKnob.Core MassiveKnobActionInfo GetAction(MassiveKnobActionType actionType, int index); MassiveKnobActionInfo SetAction(MassiveKnobActionType actionType, int index, IMassiveKnobAction action); + MassiveKnobSettings.DigitalToAnalogSettings GetDigitalToAnalogSettings(int analogOutputIndex); + void UpdateDigitalToAnalogSettings(int analogOutputIndex, Action applyChanges); + MassiveKnobSettings GetSettings(); void UpdateSettings(Action applyChanges); } diff --git a/Windows/MassiveKnob/Core/MassiveKnobOrchestrator.cs b/Windows/MassiveKnob/Core/MassiveKnobOrchestrator.cs index 5c5fac7..75e1d35 100644 --- a/Windows/MassiveKnob/Core/MassiveKnobOrchestrator.cs +++ b/Windows/MassiveKnob/Core/MassiveKnobOrchestrator.cs @@ -32,6 +32,7 @@ namespace MassiveKnob.Core private readonly Dictionary analogOutputValues = new Dictionary(); private readonly Dictionary digitalOutputValues = new Dictionary(); + private readonly Dictionary digitalToAnalogOutputValues = new Dictionary(); public MassiveKnobDeviceInfo ActiveDevice @@ -150,7 +151,7 @@ namespace MassiveKnob.Core Action initializeAfterRegistration = null; - var mapping = CreateActionMapping(action, index, (actionInstance, actionContext) => + var mapping = CreateActionMapping(actionType, action, index, (actionInstance, actionContext) => { initializeAfterRegistration = () => actionInstance.Initialize(actionContext); }); @@ -165,7 +166,44 @@ namespace MassiveKnob.Core return mapping?.ActionInfo; } + + public MassiveKnobSettings.DigitalToAnalogSettings GetDigitalToAnalogSettings(int analogOutputIndex) + { + lock (settingsLock) + { + var settingsList = GetActionSettingsList(MassiveKnobActionType.OutputAnalog); + if (analogOutputIndex >= settingsList.Count) + return new MassiveKnobSettings.DigitalToAnalogSettings(); + + return settingsList[analogOutputIndex].DigitalToAnalog?.Clone() ?? new MassiveKnobSettings.DigitalToAnalogSettings(); + } + } + + public void UpdateDigitalToAnalogSettings(int analogOutputIndex, Action applyChanges) + { + lock (settingsLock) + { + var settingsList = GetActionSettingsList(MassiveKnobActionType.OutputAnalog); + while (analogOutputIndex >= settingsList.Count) + settingsList.Add(null); + + if (settingsList[analogOutputIndex] == null) + settingsList[analogOutputIndex] = new MassiveKnobSettings.ActionSettings(); + + if (settingsList[analogOutputIndex].DigitalToAnalog == null) + settingsList[analogOutputIndex].DigitalToAnalog = new MassiveKnobSettings.DigitalToAnalogSettings(); + + applyChanges(settingsList[analogOutputIndex].DigitalToAnalog); + } + + FlushSettings(); + + if (digitalToAnalogOutputValues.TryGetValue(analogOutputIndex, out var on)) + SetDigitalToAnalogOutput(null, analogOutputIndex, on, true); + } + + public MassiveKnobSettings GetSettings() { lock (settingsLock) @@ -214,7 +252,6 @@ namespace MassiveKnob.Core ActiveDevice?.Instance.Dispose(); SetDeviceStatus(null, MassiveKnobDeviceStatus.Disconnected); - // TODO (must have) move initialization to separate Task, to prevent issues at startup // TODO (must have) exception handling! if (device != null) { @@ -373,13 +410,13 @@ namespace MassiveKnob.Core } - public void SetAnalogOutput(IMassiveKnobActionContext context, int index, byte value) + public void SetAnalogOutput(IMassiveKnobActionContext context, int index, byte value, bool force) { IMassiveKnobDeviceInstance deviceInstance; lock (settingsLock) { - if (analogOutputValues.TryGetValue(index, out var currentValue) && currentValue == value) + if (!force && analogOutputValues.TryGetValue(index, out var currentValue) && currentValue == value) return; analogOutputValues[index] = value; @@ -402,13 +439,49 @@ namespace MassiveKnob.Core } - public void SetDigitalOutput(IMassiveKnobActionContext context, int index, bool on) + public void SetDigitalToAnalogOutput(IMassiveKnobActionContext context, int index, bool on, bool force) + { + IMassiveKnobDeviceInstance deviceInstance; + MassiveKnobSettings.DigitalToAnalogSettings digitalToAnalogSettings = null; + + lock (settingsLock) + { + if (!force && digitalToAnalogOutputValues.TryGetValue(index, out var currentValue) && currentValue == on) + return; + + digitalToAnalogOutputValues[index] = on; + + + if (activeDevice == null) + return; + + var list = GetActionMappingList(MassiveKnobActionType.OutputAnalog); + if (index >= list.Count) + return; + + if (context != null && list[index]?.Context != context) + return; + + var settingsList = GetActionSettingsList(MassiveKnobActionType.OutputAnalog); + if (index < settingsList.Count) + digitalToAnalogSettings = settingsList[index].DigitalToAnalog; + + deviceInstance = activeDevice.Instance; + } + + deviceInstance.SetAnalogOutput(index, on + ? digitalToAnalogSettings?.OnValue ?? 100 + : digitalToAnalogSettings?.OffValue ?? 0); + } + + + public void SetDigitalOutput(IMassiveKnobActionContext context, int index, bool on, bool force) { IMassiveKnobDeviceInstance deviceInstance; lock (settingsLock) { - if (digitalOutputValues.TryGetValue(index, out var currentValue) && currentValue == on) + if (!force && digitalOutputValues.TryGetValue(index, out var currentValue) && currentValue == on) return; digitalOutputValues[index] = on; @@ -426,7 +499,7 @@ namespace MassiveKnob.Core deviceInstance = activeDevice.Instance; } - + deviceInstance.SetDigitalOutput(index, on); } @@ -507,10 +580,10 @@ namespace MassiveKnob.Core lock (settingsLock) { - UpdateMapping(analogInputs, specs.AnalogInputCount, settings.AnalogInput, DelayedInitialize); - UpdateMapping(digitalInputs, specs.DigitalInputCount, settings.DigitalInput, DelayedInitialize); - UpdateMapping(analogOutputs, specs.AnalogOutputCount, settings.AnalogOutput, DelayedInitialize); - UpdateMapping(digitalOutputs, specs.DigitalOutputCount, settings.DigitalOutput, DelayedInitialize); + UpdateMapping(analogInputs, specs.AnalogInputCount, MassiveKnobActionType.InputAnalog, settings.AnalogInput, DelayedInitialize); + UpdateMapping(digitalInputs, specs.DigitalInputCount, MassiveKnobActionType.InputDigital, settings.DigitalInput, DelayedInitialize); + UpdateMapping(analogOutputs, specs.AnalogOutputCount, MassiveKnobActionType.OutputAnalog, settings.AnalogOutput, DelayedInitialize); + UpdateMapping(digitalOutputs, specs.DigitalOutputCount, MassiveKnobActionType.OutputDigital, settings.DigitalOutput, DelayedInitialize); } foreach (var delayedInitializeAction in delayedInitializeActions) @@ -525,14 +598,17 @@ namespace MassiveKnob.Core // Send out all cached values to initialize the device's outputs foreach (var pair in analogOutputValues.Where(pair => pair.Key < specs.AnalogOutputCount)) - SetAnalogOutput(null, pair.Key, pair.Value); + SetAnalogOutput(null, pair.Key, pair.Value, true); foreach (var pair in digitalOutputValues.Where(pair => pair.Key < specs.DigitalOutputCount)) - SetDigitalOutput(null, pair.Key, pair.Value); + SetDigitalOutput(null, pair.Key, pair.Value, true); + + foreach (var pair in digitalToAnalogOutputValues.Where(pair => pair.Key < specs.AnalogOutputCount)) + SetDigitalToAnalogOutput(null, pair.Key, pair.Value, true); } - private void UpdateMapping(List mapping, int newCount, List actionSettings, Action initializeOutsideLock) + private void UpdateMapping(List mapping, int newCount, MassiveKnobActionType assignedActionType, List actionSettings, Action initializeOutsideLock) { if (mapping.Count > newCount) { @@ -555,7 +631,7 @@ namespace MassiveKnob.Core if (actionIndex < actionSettings.Count && actionSettings[actionIndex] != null) { var action = allActions.FirstOrDefault(d => d.ActionId == actionSettings[actionIndex].ActionId); - mapping.Add(CreateActionMapping(action, actionIndex, initializeOutsideLock)); + mapping.Add(CreateActionMapping(assignedActionType, action, actionIndex, initializeOutsideLock)); } else mapping.Add(null); @@ -564,7 +640,7 @@ namespace MassiveKnob.Core } - private ActionMapping CreateActionMapping(IMassiveKnobAction action, int index, Action initialize) + private ActionMapping CreateActionMapping(MassiveKnobActionType assignedActionType, IMassiveKnobAction action, int index, Action initialize) { if (action == null) return null; @@ -573,12 +649,12 @@ namespace MassiveKnob.Core new { Action = action.ActionId, - action.ActionType, + ActionType = assignedActionType, Index = index }); var instance = action.Create(new SerilogLoggerProvider(actionLogger).CreateLogger(null)); - var context = new ActionContext(this, action, index); + var context = new ActionContext(this, action, index, assignedActionType); var mapping = new ActionMapping(new MassiveKnobActionInfo(action, instance), context); initialize(instance, context); @@ -663,13 +739,14 @@ namespace MassiveKnob.Core private readonly MassiveKnobOrchestrator owner; private readonly IMassiveKnobAction action; private readonly int index; + private readonly MassiveKnobActionType assignedActionType; - - public ActionContext(MassiveKnobOrchestrator owner, IMassiveKnobAction action, int index) + public ActionContext(MassiveKnobOrchestrator owner, IMassiveKnobAction action, int index, MassiveKnobActionType assignedActionType) { this.owner = owner; this.action = action; this.index = index; + this.assignedActionType = assignedActionType; } @@ -687,13 +764,16 @@ namespace MassiveKnob.Core public void SetAnalogOutput(byte value) { - owner.SetAnalogOutput(this, index, value); + owner.SetAnalogOutput(this, index, value, false); } public void SetDigitalOutput(bool on) { - owner.SetDigitalOutput(this, index, on); + if (assignedActionType == MassiveKnobActionType.OutputAnalog) + owner.SetDigitalToAnalogOutput(this, index, on, false); + else + owner.SetDigitalOutput(this, index, on, false); } } } diff --git a/Windows/MassiveKnob/Program.cs b/Windows/MassiveKnob/Program.cs index d6d9064..0e64f6f 100644 --- a/Windows/MassiveKnob/Program.cs +++ b/Windows/MassiveKnob/Program.cs @@ -23,6 +23,7 @@ namespace MassiveKnob loggingSwitch.SetLogging(settings.Log.Enabled, settings.Log.Level); var logger = new LoggerConfiguration() + .MinimumLevel.Verbose() .Filter.ByIncludingOnly(loggingSwitch.IsIncluded) .Enrich.FromLogContext() .WriteTo.File( diff --git a/Windows/MassiveKnob/Settings/MassiveKnobSettings.cs b/Windows/MassiveKnob/Settings/MassiveKnobSettings.cs index 33a194a..b56c4f1 100644 --- a/Windows/MassiveKnob/Settings/MassiveKnobSettings.cs +++ b/Windows/MassiveKnob/Settings/MassiveKnobSettings.cs @@ -89,6 +89,7 @@ namespace MassiveKnob.Settings { public Guid ActionId { get; set; } public JObject Settings { get; set; } + public DigitalToAnalogSettings DigitalToAnalog { get; set; } public ActionSettings Clone() { @@ -97,7 +98,25 @@ namespace MassiveKnob.Settings ActionId = ActionId, // This is safe, as the JObject itself is never manipulated, only replaced - Settings = Settings + Settings = Settings, + + DigitalToAnalog = DigitalToAnalog?.Clone() + }; + } + } + + + public class DigitalToAnalogSettings + { + public byte OffValue { get; set; } + public byte OnValue { get; set; } = 100; + + public DigitalToAnalogSettings Clone() + { + return new DigitalToAnalogSettings + { + OffValue = OffValue, + OnValue = OnValue }; } } diff --git a/Windows/MassiveKnob/Strings.Designer.cs b/Windows/MassiveKnob/Strings.Designer.cs index 60fd47e..b34191f 100644 --- a/Windows/MassiveKnob/Strings.Designer.cs +++ b/Windows/MassiveKnob/Strings.Designer.cs @@ -96,6 +96,33 @@ namespace MassiveKnob { } } + /// + /// Looks up a localized string similar to You are assigning a digital action to an analog output. Please specify how you want to represent the on and off values.. + /// + public static string DigitalToAnalogDescription { + get { + return ResourceManager.GetString("DigitalToAnalogDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Off. + /// + public static string DigitalToAnalogOff { + get { + return ResourceManager.GetString("DigitalToAnalogOff", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to On. + /// + public static string DigitalToAnalogOn { + get { + return ResourceManager.GetString("DigitalToAnalogOn", resourceCulture); + } + } + /// /// Looks up a localized string similar to Input #{0}. /// diff --git a/Windows/MassiveKnob/Strings.resx b/Windows/MassiveKnob/Strings.resx index 4924581..58fdfc1 100644 --- a/Windows/MassiveKnob/Strings.resx +++ b/Windows/MassiveKnob/Strings.resx @@ -129,6 +129,15 @@ Disconnected + + You are assigning a digital action to an analog output. Please specify how you want to represent the on and off values. + + + Off + + + On + Input #{0} diff --git a/Windows/MassiveKnob/View/InputOutputView.xaml b/Windows/MassiveKnob/View/InputOutputView.xaml index 5e0a3fc..99fc97f 100644 --- a/Windows/MassiveKnob/View/InputOutputView.xaml +++ b/Windows/MassiveKnob/View/InputOutputView.xaml @@ -5,9 +5,10 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:helpers="clr-namespace:MassiveKnob.Helpers" xmlns:viewModel="clr-namespace:MassiveKnob.ViewModel" + xmlns:massiveKnob="clr-namespace:MassiveKnob" mc:Ignorable="d" - d:DesignHeight="100" d:DesignWidth="800" - d:DataContext="{d:DesignInstance viewModel:InputOutputViewModel}"> + d:DesignHeight="300" d:DesignWidth="600" + d:DataContext="{d:DesignInstance viewModel:InputOutputViewModelDesignTime, IsDesignTimeCreatable=True}"> @@ -39,6 +40,26 @@ DropdownItemsTemplate={StaticResource ActionDropdownItem}}" /> + + + + + + + + + + + + + + + + + + + + diff --git a/Windows/MassiveKnob/ViewModel/InputOutputViewModel.cs b/Windows/MassiveKnob/ViewModel/InputOutputViewModel.cs index bd81402..4bdb10b 100644 --- a/Windows/MassiveKnob/ViewModel/InputOutputViewModel.cs +++ b/Windows/MassiveKnob/ViewModel/InputOutputViewModel.cs @@ -2,7 +2,10 @@ using System.Collections.Generic; using System.ComponentModel; using System.Linq; +using System.Reactive.Linq; +using System.Reactive.Subjects; using System.Runtime.CompilerServices; +using System.Windows; using System.Windows.Controls; using MassiveKnob.Core; using MassiveKnob.Plugin; @@ -41,6 +44,7 @@ namespace MassiveKnob.ViewModel var actionInfo = orchestrator.SetAction(actionType, index, selectedAction?.Action); OnPropertyChanged(); + OnDependantPropertyChanged(nameof(DigitalToAnalogVisibility)); ActionSettingsControl = actionInfo?.Instance.CreateSettingsControl(); } @@ -61,6 +65,61 @@ namespace MassiveKnob.ViewModel OnPropertyChanged(); } } + + public Visibility DigitalToAnalogVisibility + { + get + { + // Design-time support + if (orchestrator == null) + return Visibility.Visible; + + if (actionType != MassiveKnobActionType.OutputAnalog) + return Visibility.Collapsed; + + if (SelectedAction == null || SelectedAction.RepresentsNull) + return Visibility.Collapsed; + + return SelectedAction.Action.ActionType == MassiveKnobActionType.OutputDigital + ? Visibility.Visible + : Visibility.Collapsed; + } + } + + + private readonly Subject throttledDigitalToAnalogChanged = new Subject(); + private readonly IDisposable digitalToAnalogChangedSubscription; + + private byte digitalToAnalogOn; + public byte DigitalToAnalogOn + { + get => digitalToAnalogOn; + set + { + if (actionType != MassiveKnobActionType.OutputAnalog || value == digitalToAnalogOn) + return; + + digitalToAnalogOn = value; + OnPropertyChanged(); + throttledDigitalToAnalogChanged.OnNext(true); + } + } + + + private byte digitalToAnalogOff; + public byte DigitalToAnalogOff + { + get => digitalToAnalogOff; + set + { + if (actionType != MassiveKnobActionType.OutputAnalog || value == digitalToAnalogOff) + return; + + digitalToAnalogOff = value; + OnPropertyChanged(); + throttledDigitalToAnalogChanged.OnNext(true); + } + } // ReSharper restore UnusedMember.Global @@ -73,10 +132,27 @@ namespace MassiveKnob.ViewModel // For design-time support if (orchestrator == null) + { + DigitalToAnalogOn = 100; return; + } - Actions = settingsViewModel.Actions.Where(a => a.RepresentsNull || a.Action.ActionType == actionType).ToList(); + bool AllowAction(ActionViewModel actionViewModel) + { + if (actionViewModel.RepresentsNull) + return true; + + if (actionViewModel.Action.ActionType == actionType) + return true; + + // Allow digital actions to be assigned to analog outputs, extra conversion settings will be shown + return actionType == MassiveKnobActionType.OutputAnalog && + actionViewModel.Action.ActionType == MassiveKnobActionType.OutputDigital; + } + + + Actions = settingsViewModel.Actions.Where(AllowAction).ToList(); var actionInfo = orchestrator.GetAction(actionType, index); @@ -85,13 +161,35 @@ namespace MassiveKnob.ViewModel : Actions.Single(a => a.RepresentsNull); actionSettingsControl = actionInfo?.Instance.CreateSettingsControl(); + + + if (actionType != MassiveKnobActionType.OutputAnalog) + return; + + var digitalToAnalogSettings = orchestrator.GetDigitalToAnalogSettings(index); + digitalToAnalogOn = digitalToAnalogSettings.OnValue; + digitalToAnalogOff = digitalToAnalogSettings.OffValue; + + digitalToAnalogChangedSubscription = throttledDigitalToAnalogChanged + .Throttle(TimeSpan.FromMilliseconds(250)) + .Subscribe(b => + { + orchestrator?.UpdateDigitalToAnalogSettings(index, settings => + { + settings.OnValue = digitalToAnalogOn; + settings.OffValue = digitalToAnalogOff; + }); + }); } - + public void Dispose() { if (ActionSettingsControl is IDisposable disposable) disposable.Dispose(); + + digitalToAnalogChangedSubscription?.Dispose(); + throttledDigitalToAnalogChanged.Dispose(); } @@ -101,5 +199,18 @@ namespace MassiveKnob.ViewModel { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } + + protected virtual void OnDependantPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } + + + public class InputOutputViewModelDesignTime : InputOutputViewModel + { + public InputOutputViewModelDesignTime() : base(null, null, MassiveKnobActionType.OutputAnalog, 0) + { + } } } diff --git a/Windows/MassiveKnob/ViewModel/SettingsViewModel.cs b/Windows/MassiveKnob/ViewModel/SettingsViewModel.cs index f4b4703..f104af3 100644 --- a/Windows/MassiveKnob/ViewModel/SettingsViewModel.cs +++ b/Windows/MassiveKnob/ViewModel/SettingsViewModel.cs @@ -348,7 +348,13 @@ namespace MassiveKnob.ViewModel SelectedMenuItem = activeMenuItem; - activeDeviceSubscription = orchestrator.ActiveDeviceSubject.Subscribe(info => { Specs = info.Specs; }); + activeDeviceSubscription = orchestrator.ActiveDeviceSubject.Subscribe(info => + { + Application.Current?.Dispatcher.Invoke(() => + { + Specs = info.Specs; + }); + }); deviceStatusSubscription = orchestrator.DeviceStatusSubject.Subscribe(status => { OnDependantPropertyChanged(nameof(ConnectionStatusColor)); diff --git a/Windows/min.NET b/Windows/min.NET index 65c76b3..35b664d 160000 --- a/Windows/min.NET +++ b/Windows/min.NET @@ -1 +1 @@ -Subproject commit 65c76b3f214522dd5f1da3704b83375bf238daba +Subproject commit 35b664d1fe5a03cfc112683e07dbf307dfe3d164