1
0
mirror of synced 2024-12-22 00:53:08 +01:00

Added support for analog outputs (basic PWM) to Arduino Sketch

Added support for binding digital output actions to analog outputs
Fixed a few NullReferenceExceptions
Fixed Serial "Connecting..." state
Fixed update issues when multiple VoiceMeeter output actions are configured
Fixed VoiceMeeter Remote not logging out
This commit is contained in:
Mark van Renswoude 2021-03-08 20:18:47 +01:00
parent 0e27fec1e9
commit 543b55f3ba
21 changed files with 513 additions and 82 deletions

View File

@ -5,13 +5,15 @@
* *
*/ */
// Set this to the number of potentiometers you have connected // 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 // 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? // Set this to the number of PWM outputs you have connected
const byte AnalogOutputCount = 0; // 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 // Set this to the number of digital outputs you have connected
const byte DigitalOutputCount = 0; const byte DigitalOutputCount = 0;
@ -20,14 +22,27 @@ const byte DigitalOutputCount = 0;
// For each potentiometer, specify the pin // For each potentiometer, specify the pin
const byte AnalogInputPin[AnalogInputCount] = { const byte AnalogInputPin[AnalogInputCount] = {
A0, A0,
A1, A1
A2
}; };
// For each button, specify the pin. Assumes pull-up. // For each button, specify the pin. Assumes pull-up.
const byte DigitalInputPin[DigitalInputCount] = { 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 // For each digital output, specify the pin
const byte DigitalOutputPin[DigitalOutputCount] = { const byte DigitalOutputPin[DigitalOutputCount] = {
}; };
@ -107,7 +122,7 @@ struct DigitalInputStatus
struct AnalogInputStatus analogInputStatus[AnalogInputCount]; struct AnalogInputStatus analogInputStatus[AnalogInputCount];
struct DigitalInputStatus digitalInputStatus[AnalogInputCount]; struct DigitalInputStatus digitalInputStatus[DigitalInputCount];
void setup() void setup()
@ -143,6 +158,13 @@ void setup()
analogInputStatus[i].LastChange = millis(); 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 // Set up digital inputs and outputs
for (byte i = 0; i < DigitalInputCount; i++) for (byte i = 0; i < DigitalInputCount; i++)
@ -181,6 +203,27 @@ unsigned long focusOutputTime;
#define IsDigitalInputFocus(i) ((focusType == FocusInputType.DigitalInput) && (focusInputIndex == i)) #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() void loop()
{ {
#ifndef DebugOutputPlotter #ifndef DebugOutputPlotter
@ -291,6 +334,8 @@ void loop()
} }
Serial.println(); Serial.println();
lastOutput = millis();
} }
#endif #endif
} }
@ -306,7 +351,7 @@ void min_application_handler(uint8_t min_id, uint8_t *min_payload, uint8_t len_p
break; break;
case FrameIDAnalogOutput: case FrameIDAnalogOutput:
//processAnalogOutputMessage(); processAnalogOutputMessage(min_payload, len_payload);
break; break;
case FrameIDDigitalOutput: 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() void processQuitMessage()
{ {
active = false; active = false;

View File

@ -82,7 +82,7 @@ namespace MassiveKnob.Plugin.CoreAudio.Base
.Select(PlaybackDeviceViewModel.FromDevice) .Select(PlaybackDeviceViewModel.FromDevice)
.ToList(); .ToList();
Application.Current.Dispatcher.Invoke(() => Application.Current?.Dispatcher.Invoke(() =>
{ {
PlaybackDevices = deviceViewModels; PlaybackDevices = deviceViewModels;
SelectedDevice = deviceViewModels.SingleOrDefault(d => d.Id == settings.DeviceId); SelectedDevice = deviceViewModels.SingleOrDefault(d => d.Id == settings.DeviceId);

View File

@ -44,15 +44,17 @@ namespace MassiveKnob.Plugin.CoreAudio.GetDefault
private void ApplySettings() private void ApplySettings()
{ {
if (playbackDevice != null && playbackDevice.Id == settings.DeviceId) if (playbackDevice == null || playbackDevice.Id != settings.DeviceId)
return; {
var coreAudioController = CoreAudioControllerInstance.Acquire();
var coreAudioController = CoreAudioControllerInstance.Acquire(); playbackDevice = settings.DeviceId.HasValue
playbackDevice = settings.DeviceId.HasValue ? coreAudioController.GetDevice(settings.DeviceId.Value) : null; ? coreAudioController.GetDevice(settings.DeviceId.Value)
: null;
deviceChanged?.Dispose();
deviceChanged = playbackDevice?.PropertyChanged.Subscribe(PropertyChanged);
}
deviceChanged?.Dispose();
deviceChanged = playbackDevice?.PropertyChanged.Subscribe(PropertyChanged);
CheckActive(); CheckActive();
} }

View File

@ -44,15 +44,17 @@ namespace MassiveKnob.Plugin.CoreAudio.GetMuted
private void ApplySettings() private void ApplySettings()
{ {
if (playbackDevice != null && playbackDevice.Id == settings.DeviceId) if (playbackDevice == null || playbackDevice.Id != settings.DeviceId)
return; {
var coreAudioController = CoreAudioControllerInstance.Acquire();
var coreAudioController = CoreAudioControllerInstance.Acquire(); playbackDevice = settings.DeviceId.HasValue
playbackDevice = settings.DeviceId.HasValue ? coreAudioController.GetDevice(settings.DeviceId.Value) : null; ? coreAudioController.GetDevice(settings.DeviceId.Value)
: null;
deviceChanged?.Dispose();
deviceChanged = playbackDevice?.MuteChanged.Subscribe(MuteChanged);
}
deviceChanged?.Dispose();
deviceChanged = playbackDevice?.MuteChanged.Subscribe(MuteChanged);
if (playbackDevice != null) if (playbackDevice != null)
actionContext.SetDigitalOutput(settings.Inverted ? !playbackDevice.IsMuted : playbackDevice.IsMuted); actionContext.SetDigitalOutput(settings.Inverted ? !playbackDevice.IsMuted : playbackDevice.IsMuted);
} }

View File

@ -44,15 +44,17 @@ namespace MassiveKnob.Plugin.CoreAudio.GetVolume
private void ApplySettings() private void ApplySettings()
{ {
if (playbackDevice != null && playbackDevice.Id == settings.DeviceId) if (playbackDevice == null || playbackDevice.Id != settings.DeviceId)
return; {
var coreAudioController = CoreAudioControllerInstance.Acquire();
var coreAudioController = CoreAudioControllerInstance.Acquire(); playbackDevice = settings.DeviceId.HasValue
playbackDevice = settings.DeviceId.HasValue ? coreAudioController.GetDevice(settings.DeviceId.Value) : null; ? coreAudioController.GetDevice(settings.DeviceId.Value)
: null;
deviceChanged?.Dispose();
deviceChanged = playbackDevice?.VolumeChanged.Subscribe(VolumeChanged);
}
deviceChanged?.Dispose();
deviceChanged = playbackDevice?.VolumeChanged.Subscribe(VolumeChanged);
if (playbackDevice != null) if (playbackDevice != null)
actionContext.SetAnalogOutput((byte)playbackDevice.Volume); actionContext.SetAnalogOutput((byte)playbackDevice.Volume);
} }

View File

@ -15,7 +15,7 @@ namespace MassiveKnob.Plugin.CoreAudio.OSD
public static void Show(IDevice device) public static void Show(IDevice device)
{ {
Application.Current.Dispatcher.Invoke(() => Application.Current?.Dispatcher.Invoke(() =>
{ {
if (window == null) if (window == null)
{ {
@ -41,9 +41,11 @@ namespace MassiveKnob.Plugin.CoreAudio.OSD
{ {
hideTimer?.Dispose(); hideTimer?.Dispose();
hideTimer = null; hideTimer = null;
window = null;
} }
private static void Hide() private static void Hide()
{ {
Application.Current?.Dispatcher.Invoke(() => Application.Current?.Dispatcher.Invoke(() =>

View File

@ -49,8 +49,6 @@ namespace MassiveKnob.Plugin.SerialDevice.Worker
public void Connect(string portName, int baudRate, bool dtrEnable) public void Connect(string portName, int baudRate, bool dtrEnable)
{ {
context.Connecting();
lock (minProtocolLock) lock (minProtocolLock)
{ {
if (portName == lastPortName && baudRate == lastBaudRate && dtrEnable == lastDtrEnable) if (portName == lastPortName && baudRate == lastBaudRate && dtrEnable == lastDtrEnable)
@ -61,11 +59,12 @@ namespace MassiveKnob.Plugin.SerialDevice.Worker
lastDtrEnable = dtrEnable; lastDtrEnable = dtrEnable;
Disconnect(); Disconnect();
context.Connecting();
if (string.IsNullOrEmpty(portName) || baudRate == 0) if (string.IsNullOrEmpty(portName) || baudRate == 0)
return; return;
minProtocol?.Dispose(); minProtocol?.Dispose();
minProtocol = new MINProtocol(new MINSerialTransport(portName, baudRate, dtrEnable: dtrEnable), logger); minProtocol = new MINProtocol(new MINSerialTransport(portName, baudRate, dtrEnable: dtrEnable), logger);
minProtocol.OnConnected += MinProtocolOnOnConnected; minProtocol.OnConnected += MinProtocolOnOnConnected;

View File

@ -26,7 +26,6 @@ namespace MassiveKnob.Plugin.VoiceMeeter.GetParameter
private IMassiveKnobActionContext actionContext; private IMassiveKnobActionContext actionContext;
private VoiceMeeterGetParameterActionSettings settings; private VoiceMeeterGetParameterActionSettings settings;
private VoiceMeeterGetParameterActionSettingsViewModel viewModel; private VoiceMeeterGetParameterActionSettingsViewModel viewModel;
private Parameters parameters;
private IDisposable parameterChanged; private IDisposable parameterChanged;
@ -44,7 +43,6 @@ namespace MassiveKnob.Plugin.VoiceMeeter.GetParameter
{ {
InstanceRegister.Unregister(this); InstanceRegister.Unregister(this);
parameterChanged?.Dispose(); parameterChanged?.Dispose();
parameters?.Dispose();
} }
@ -54,17 +52,11 @@ namespace MassiveKnob.Plugin.VoiceMeeter.GetParameter
{ {
parameterChanged?.Dispose(); parameterChanged?.Dispose();
parameterChanged = null; parameterChanged = null;
parameters?.Dispose();
parameters = null;
return; return;
} }
if (parameters == null)
parameters = new Parameters();
if (parameterChanged == null) if (parameterChanged == null)
parameterChanged = parameters.Subscribe(x => ParametersChanged()); parameterChanged = InstanceRegister.SubscribeToParameterChanges(ParametersChanged);
ParametersChanged(); ParametersChanged();
} }

View File

@ -11,6 +11,13 @@ namespace MassiveKnob.Plugin.VoiceMeeter
private static readonly HashSet<IVoiceMeeterAction> Instances = new HashSet<IVoiceMeeterAction>(); private static readonly HashSet<IVoiceMeeterAction> Instances = new HashSet<IVoiceMeeterAction>();
private static Task initializeTask; private static Task initializeTask;
private static readonly object SubscribersLock = new object();
private static Parameters parameters;
private static IDisposable parametersSubscriber;
private static readonly List<Action> ParameterSubscriberActions = new List<Action>();
private static IDisposable voicemeeterClient;
// The VoiceMeeter Remote only connects to one instance, so all actions need to be in sync // The VoiceMeeter Remote only connects to one instance, so all actions need to be in sync
private static RunVoicemeeterParam version; private static RunVoicemeeterParam version;
@ -27,7 +34,8 @@ namespace MassiveKnob.Plugin.VoiceMeeter
initializeTask = Task.Run(async () => 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; 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) public static void Register(IVoiceMeeterAction instance)
@ -52,7 +109,12 @@ namespace MassiveKnob.Plugin.VoiceMeeter
{ {
lock (InstancesLock) 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); action(instance);
} }
} }
private class ParametersSubscriber : IDisposable
{
private readonly Action action;
public ParametersSubscriber(Action action)
{
this.action = action;
}
public void Dispose()
{
InstanceRegister.RemoveParameterSubscriber(action);
}
}
} }
} }

View File

@ -70,7 +70,7 @@ namespace MassiveKnob.Plugin.VoiceMeeter {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Get parameter. /// Looks up a localized string similar to VoiceMeeter: Get parameter.
/// </summary> /// </summary>
public static string GetParameterName { public static string GetParameterName {
get { get {
@ -106,7 +106,7 @@ namespace MassiveKnob.Plugin.VoiceMeeter {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Run macro. /// Looks up a localized string similar to VoiceMeeter: Run macro.
/// </summary> /// </summary>
public static string RunMacroName { public static string RunMacroName {
get { get {

View File

@ -121,7 +121,7 @@
<value>Turns the output on if the specified parameter equals the specified value.</value> <value>Turns the output on if the specified parameter equals the specified value.</value>
</data> </data>
<data name="GetParameterName" xml:space="preserve"> <data name="GetParameterName" xml:space="preserve">
<value>Get parameter</value> <value>VoiceMeeter: Get parameter</value>
</data> </data>
<data name="PluginDescription" xml:space="preserve"> <data name="PluginDescription" xml:space="preserve">
<value>Provides actions to run VoiceMeeter macros or check the current state.</value> <value>Provides actions to run VoiceMeeter macros or check the current state.</value>
@ -133,7 +133,7 @@
<value>Runs a VoiceMeeter macro when the input turns on.</value> <value>Runs a VoiceMeeter macro when the input turns on.</value>
</data> </data>
<data name="RunMacroName" xml:space="preserve"> <data name="RunMacroName" xml:space="preserve">
<value>Run macro</value> <value>VoiceMeeter: Run macro</value>
</data> </data>
<data name="SettingGetParameterInverted" xml:space="preserve"> <data name="SettingGetParameterInverted" xml:space="preserve">
<value>Inverted (on if the parameter does not equal the value)</value> <value>Inverted (on if the parameter does not equal the value)</value>

View File

@ -18,6 +18,9 @@ namespace MassiveKnob.Core
MassiveKnobActionInfo GetAction(MassiveKnobActionType actionType, int index); MassiveKnobActionInfo GetAction(MassiveKnobActionType actionType, int index);
MassiveKnobActionInfo SetAction(MassiveKnobActionType actionType, int index, IMassiveKnobAction action); MassiveKnobActionInfo SetAction(MassiveKnobActionType actionType, int index, IMassiveKnobAction action);
MassiveKnobSettings.DigitalToAnalogSettings GetDigitalToAnalogSettings(int analogOutputIndex);
void UpdateDigitalToAnalogSettings(int analogOutputIndex, Action<MassiveKnobSettings.DigitalToAnalogSettings> applyChanges);
MassiveKnobSettings GetSettings(); MassiveKnobSettings GetSettings();
void UpdateSettings(Action<MassiveKnobSettings> applyChanges); void UpdateSettings(Action<MassiveKnobSettings> applyChanges);
} }

View File

@ -32,6 +32,7 @@ namespace MassiveKnob.Core
private readonly Dictionary<int, byte> analogOutputValues = new Dictionary<int, byte>(); private readonly Dictionary<int, byte> analogOutputValues = new Dictionary<int, byte>();
private readonly Dictionary<int, bool> digitalOutputValues = new Dictionary<int, bool>(); private readonly Dictionary<int, bool> digitalOutputValues = new Dictionary<int, bool>();
private readonly Dictionary<int, bool> digitalToAnalogOutputValues = new Dictionary<int, bool>();
public MassiveKnobDeviceInfo ActiveDevice public MassiveKnobDeviceInfo ActiveDevice
@ -150,7 +151,7 @@ namespace MassiveKnob.Core
Action initializeAfterRegistration = null; Action initializeAfterRegistration = null;
var mapping = CreateActionMapping(action, index, (actionInstance, actionContext) => var mapping = CreateActionMapping(actionType, action, index, (actionInstance, actionContext) =>
{ {
initializeAfterRegistration = () => actionInstance.Initialize(actionContext); initializeAfterRegistration = () => actionInstance.Initialize(actionContext);
}); });
@ -165,7 +166,44 @@ namespace MassiveKnob.Core
return mapping?.ActionInfo; 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<MassiveKnobSettings.DigitalToAnalogSettings> 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() public MassiveKnobSettings GetSettings()
{ {
lock (settingsLock) lock (settingsLock)
@ -214,7 +252,6 @@ namespace MassiveKnob.Core
ActiveDevice?.Instance.Dispose(); ActiveDevice?.Instance.Dispose();
SetDeviceStatus(null, MassiveKnobDeviceStatus.Disconnected); SetDeviceStatus(null, MassiveKnobDeviceStatus.Disconnected);
// TODO (must have) move initialization to separate Task, to prevent issues at startup
// TODO (must have) exception handling! // TODO (must have) exception handling!
if (device != null) 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; IMassiveKnobDeviceInstance deviceInstance;
lock (settingsLock) lock (settingsLock)
{ {
if (analogOutputValues.TryGetValue(index, out var currentValue) && currentValue == value) if (!force && analogOutputValues.TryGetValue(index, out var currentValue) && currentValue == value)
return; return;
analogOutputValues[index] = value; 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; IMassiveKnobDeviceInstance deviceInstance;
lock (settingsLock) lock (settingsLock)
{ {
if (digitalOutputValues.TryGetValue(index, out var currentValue) && currentValue == on) if (!force && digitalOutputValues.TryGetValue(index, out var currentValue) && currentValue == on)
return; return;
digitalOutputValues[index] = on; digitalOutputValues[index] = on;
@ -426,7 +499,7 @@ namespace MassiveKnob.Core
deviceInstance = activeDevice.Instance; deviceInstance = activeDevice.Instance;
} }
deviceInstance.SetDigitalOutput(index, on); deviceInstance.SetDigitalOutput(index, on);
} }
@ -507,10 +580,10 @@ namespace MassiveKnob.Core
lock (settingsLock) lock (settingsLock)
{ {
UpdateMapping(analogInputs, specs.AnalogInputCount, settings.AnalogInput, DelayedInitialize); UpdateMapping(analogInputs, specs.AnalogInputCount, MassiveKnobActionType.InputAnalog, settings.AnalogInput, DelayedInitialize);
UpdateMapping(digitalInputs, specs.DigitalInputCount, settings.DigitalInput, DelayedInitialize); UpdateMapping(digitalInputs, specs.DigitalInputCount, MassiveKnobActionType.InputDigital, settings.DigitalInput, DelayedInitialize);
UpdateMapping(analogOutputs, specs.AnalogOutputCount, settings.AnalogOutput, DelayedInitialize); UpdateMapping(analogOutputs, specs.AnalogOutputCount, MassiveKnobActionType.OutputAnalog, settings.AnalogOutput, DelayedInitialize);
UpdateMapping(digitalOutputs, specs.DigitalOutputCount, settings.DigitalOutput, DelayedInitialize); UpdateMapping(digitalOutputs, specs.DigitalOutputCount, MassiveKnobActionType.OutputDigital, settings.DigitalOutput, DelayedInitialize);
} }
foreach (var delayedInitializeAction in delayedInitializeActions) foreach (var delayedInitializeAction in delayedInitializeActions)
@ -525,14 +598,17 @@ namespace MassiveKnob.Core
// Send out all cached values to initialize the device's outputs // Send out all cached values to initialize the device's outputs
foreach (var pair in analogOutputValues.Where(pair => pair.Key < specs.AnalogOutputCount)) 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)) 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<ActionMapping> mapping, int newCount, List<MassiveKnobSettings.ActionSettings> actionSettings, Action<IMassiveKnobActionInstance, IMassiveKnobActionContext> initializeOutsideLock) private void UpdateMapping(List<ActionMapping> mapping, int newCount, MassiveKnobActionType assignedActionType, List<MassiveKnobSettings.ActionSettings> actionSettings, Action<IMassiveKnobActionInstance, IMassiveKnobActionContext> initializeOutsideLock)
{ {
if (mapping.Count > newCount) if (mapping.Count > newCount)
{ {
@ -555,7 +631,7 @@ namespace MassiveKnob.Core
if (actionIndex < actionSettings.Count && actionSettings[actionIndex] != null) if (actionIndex < actionSettings.Count && actionSettings[actionIndex] != null)
{ {
var action = allActions.FirstOrDefault(d => d.ActionId == actionSettings[actionIndex].ActionId); var action = allActions.FirstOrDefault(d => d.ActionId == actionSettings[actionIndex].ActionId);
mapping.Add(CreateActionMapping(action, actionIndex, initializeOutsideLock)); mapping.Add(CreateActionMapping(assignedActionType, action, actionIndex, initializeOutsideLock));
} }
else else
mapping.Add(null); mapping.Add(null);
@ -564,7 +640,7 @@ namespace MassiveKnob.Core
} }
private ActionMapping CreateActionMapping(IMassiveKnobAction action, int index, Action<IMassiveKnobActionInstance, IMassiveKnobActionContext> initialize) private ActionMapping CreateActionMapping(MassiveKnobActionType assignedActionType, IMassiveKnobAction action, int index, Action<IMassiveKnobActionInstance, IMassiveKnobActionContext> initialize)
{ {
if (action == null) if (action == null)
return null; return null;
@ -573,12 +649,12 @@ namespace MassiveKnob.Core
new new
{ {
Action = action.ActionId, Action = action.ActionId,
action.ActionType, ActionType = assignedActionType,
Index = index Index = index
}); });
var instance = action.Create(new SerilogLoggerProvider(actionLogger).CreateLogger(null)); 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); var mapping = new ActionMapping(new MassiveKnobActionInfo(action, instance), context);
initialize(instance, context); initialize(instance, context);
@ -663,13 +739,14 @@ namespace MassiveKnob.Core
private readonly MassiveKnobOrchestrator owner; private readonly MassiveKnobOrchestrator owner;
private readonly IMassiveKnobAction action; private readonly IMassiveKnobAction action;
private readonly int index; private readonly int index;
private readonly MassiveKnobActionType assignedActionType;
public ActionContext(MassiveKnobOrchestrator owner, IMassiveKnobAction action, int index, MassiveKnobActionType assignedActionType)
public ActionContext(MassiveKnobOrchestrator owner, IMassiveKnobAction action, int index)
{ {
this.owner = owner; this.owner = owner;
this.action = action; this.action = action;
this.index = index; this.index = index;
this.assignedActionType = assignedActionType;
} }
@ -687,13 +764,16 @@ namespace MassiveKnob.Core
public void SetAnalogOutput(byte value) public void SetAnalogOutput(byte value)
{ {
owner.SetAnalogOutput(this, index, value); owner.SetAnalogOutput(this, index, value, false);
} }
public void SetDigitalOutput(bool on) 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);
} }
} }
} }

View File

@ -23,6 +23,7 @@ namespace MassiveKnob
loggingSwitch.SetLogging(settings.Log.Enabled, settings.Log.Level); loggingSwitch.SetLogging(settings.Log.Enabled, settings.Log.Level);
var logger = new LoggerConfiguration() var logger = new LoggerConfiguration()
.MinimumLevel.Verbose()
.Filter.ByIncludingOnly(loggingSwitch.IsIncluded) .Filter.ByIncludingOnly(loggingSwitch.IsIncluded)
.Enrich.FromLogContext() .Enrich.FromLogContext()
.WriteTo.File( .WriteTo.File(

View File

@ -89,6 +89,7 @@ namespace MassiveKnob.Settings
{ {
public Guid ActionId { get; set; } public Guid ActionId { get; set; }
public JObject Settings { get; set; } public JObject Settings { get; set; }
public DigitalToAnalogSettings DigitalToAnalog { get; set; }
public ActionSettings Clone() public ActionSettings Clone()
{ {
@ -97,7 +98,25 @@ namespace MassiveKnob.Settings
ActionId = ActionId, ActionId = ActionId,
// This is safe, as the JObject itself is never manipulated, only replaced // 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
}; };
} }
} }

View File

@ -96,6 +96,33 @@ namespace MassiveKnob {
} }
} }
/// <summary>
/// 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..
/// </summary>
public static string DigitalToAnalogDescription {
get {
return ResourceManager.GetString("DigitalToAnalogDescription", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Off.
/// </summary>
public static string DigitalToAnalogOff {
get {
return ResourceManager.GetString("DigitalToAnalogOff", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to On.
/// </summary>
public static string DigitalToAnalogOn {
get {
return ResourceManager.GetString("DigitalToAnalogOn", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Input #{0}. /// Looks up a localized string similar to Input #{0}.
/// </summary> /// </summary>

View File

@ -129,6 +129,15 @@
<data name="DeviceStatusDisconnected" xml:space="preserve"> <data name="DeviceStatusDisconnected" xml:space="preserve">
<value>Disconnected</value> <value>Disconnected</value>
</data> </data>
<data name="DigitalToAnalogDescription" xml:space="preserve">
<value>You are assigning a digital action to an analog output. Please specify how you want to represent the on and off values.</value>
</data>
<data name="DigitalToAnalogOff" xml:space="preserve">
<value>Off</value>
</data>
<data name="DigitalToAnalogOn" xml:space="preserve">
<value>On</value>
</data>
<data name="InputHeader" xml:space="preserve"> <data name="InputHeader" xml:space="preserve">
<value>Input #{0}</value> <value>Input #{0}</value>
</data> </data>

View File

@ -5,9 +5,10 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helpers="clr-namespace:MassiveKnob.Helpers" xmlns:helpers="clr-namespace:MassiveKnob.Helpers"
xmlns:viewModel="clr-namespace:MassiveKnob.ViewModel" xmlns:viewModel="clr-namespace:MassiveKnob.ViewModel"
xmlns:massiveKnob="clr-namespace:MassiveKnob"
mc:Ignorable="d" mc:Ignorable="d"
d:DesignHeight="100" d:DesignWidth="800" d:DesignHeight="300" d:DesignWidth="600"
d:DataContext="{d:DesignInstance viewModel:InputOutputViewModel}"> d:DataContext="{d:DesignInstance viewModel:InputOutputViewModelDesignTime, IsDesignTimeCreatable=True}">
<UserControl.Resources> <UserControl.Resources>
<ResourceDictionary> <ResourceDictionary>
<ResourceDictionary.MergedDictionaries> <ResourceDictionary.MergedDictionaries>
@ -39,6 +40,26 @@
DropdownItemsTemplate={StaticResource ActionDropdownItem}}" /> DropdownItemsTemplate={StaticResource ActionDropdownItem}}" />
<ContentControl Focusable="False" Content="{Binding ActionSettingsControl}" Style="{StaticResource SettingsControl}" /> <ContentControl Focusable="False" Content="{Binding ActionSettingsControl}" Style="{StaticResource SettingsControl}" />
<Grid Margin="0,24,0,0" Visibility="{Binding DigitalToAnalogVisibility}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" Text="{x:Static massiveKnob:Strings.DigitalToAnalogDescription}" TextWrapping="Wrap" />
<TextBlock Grid.Row="1" Grid.Column="0" Margin="0,8,8,8" VerticalAlignment="Center" Text="{x:Static massiveKnob:Strings.DigitalToAnalogOn}" />
<Slider Grid.Row="1" Grid.Column="1" VerticalAlignment="Center" Value="{Binding DigitalToAnalogOn}" Minimum="0" Maximum="100" />
<TextBlock Grid.Row="2" Grid.Column="0" Margin="0,8,8,8" VerticalAlignment="Center" Text="{x:Static massiveKnob:Strings.DigitalToAnalogOff}" />
<Slider Grid.Row="2" Grid.Column="1" VerticalAlignment="Center" Value="{Binding DigitalToAnalogOff}" Minimum="0" Maximum="100" />
</Grid>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
</UserControl> </UserControl>

View File

@ -2,7 +2,10 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Linq; using System.Linq;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using MassiveKnob.Core; using MassiveKnob.Core;
using MassiveKnob.Plugin; using MassiveKnob.Plugin;
@ -41,6 +44,7 @@ namespace MassiveKnob.ViewModel
var actionInfo = orchestrator.SetAction(actionType, index, selectedAction?.Action); var actionInfo = orchestrator.SetAction(actionType, index, selectedAction?.Action);
OnPropertyChanged(); OnPropertyChanged();
OnDependantPropertyChanged(nameof(DigitalToAnalogVisibility));
ActionSettingsControl = actionInfo?.Instance.CreateSettingsControl(); ActionSettingsControl = actionInfo?.Instance.CreateSettingsControl();
} }
@ -61,6 +65,61 @@ namespace MassiveKnob.ViewModel
OnPropertyChanged(); 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<bool> throttledDigitalToAnalogChanged = new Subject<bool>();
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 // ReSharper restore UnusedMember.Global
@ -73,10 +132,27 @@ namespace MassiveKnob.ViewModel
// For design-time support // For design-time support
if (orchestrator == null) if (orchestrator == null)
{
DigitalToAnalogOn = 100;
return; 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); var actionInfo = orchestrator.GetAction(actionType, index);
@ -85,13 +161,35 @@ namespace MassiveKnob.ViewModel
: Actions.Single(a => a.RepresentsNull); : Actions.Single(a => a.RepresentsNull);
actionSettingsControl = actionInfo?.Instance.CreateSettingsControl(); 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() public void Dispose()
{ {
if (ActionSettingsControl is IDisposable disposable) if (ActionSettingsControl is IDisposable disposable)
disposable.Dispose(); disposable.Dispose();
digitalToAnalogChangedSubscription?.Dispose();
throttledDigitalToAnalogChanged.Dispose();
} }
@ -101,5 +199,18 @@ namespace MassiveKnob.ViewModel
{ {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 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)
{
}
} }
} }

View File

@ -348,7 +348,13 @@ namespace MassiveKnob.ViewModel
SelectedMenuItem = activeMenuItem; 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 => deviceStatusSubscription = orchestrator.DeviceStatusSubject.Subscribe(status =>
{ {
OnDependantPropertyChanged(nameof(ConnectionStatusColor)); OnDependantPropertyChanged(nameof(ConnectionStatusColor));

@ -1 +1 @@
Subproject commit 65c76b3f214522dd5f1da3704b83375bf238daba Subproject commit 35b664d1fe5a03cfc112683e07dbf307dfe3d164