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
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;

View File

@ -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);

View File

@ -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();
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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(() =>

View File

@ -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;

View File

@ -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();
}

View File

@ -11,6 +11,13 @@ namespace MassiveKnob.Plugin.VoiceMeeter
private static readonly HashSet<IVoiceMeeterAction> Instances = new HashSet<IVoiceMeeterAction>();
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
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);
}
}
}
}

View File

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

View File

@ -121,7 +121,7 @@
<value>Turns the output on if the specified parameter equals the specified value.</value>
</data>
<data name="GetParameterName" xml:space="preserve">
<value>Get parameter</value>
<value>VoiceMeeter: Get parameter</value>
</data>
<data name="PluginDescription" xml:space="preserve">
<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>
</data>
<data name="RunMacroName" xml:space="preserve">
<value>Run macro</value>
<value>VoiceMeeter: Run macro</value>
</data>
<data name="SettingGetParameterInverted" xml:space="preserve">
<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 SetAction(MassiveKnobActionType actionType, int index, IMassiveKnobAction action);
MassiveKnobSettings.DigitalToAnalogSettings GetDigitalToAnalogSettings(int analogOutputIndex);
void UpdateDigitalToAnalogSettings(int analogOutputIndex, Action<MassiveKnobSettings.DigitalToAnalogSettings> applyChanges);
MassiveKnobSettings GetSettings();
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, bool> digitalOutputValues = new Dictionary<int, bool>();
private readonly Dictionary<int, bool> digitalToAnalogOutputValues = new Dictionary<int, bool>();
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<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()
{
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<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)
{
@ -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<IMassiveKnobActionInstance, IMassiveKnobActionContext> initialize)
private ActionMapping CreateActionMapping(MassiveKnobActionType assignedActionType, IMassiveKnobAction action, int index, Action<IMassiveKnobActionInstance, IMassiveKnobActionContext> 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);
}
}
}

View File

@ -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(

View File

@ -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
};
}
}

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>
/// Looks up a localized string similar to Input #{0}.
/// </summary>

View File

@ -129,6 +129,15 @@
<data name="DeviceStatusDisconnected" xml:space="preserve">
<value>Disconnected</value>
</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">
<value>Input #{0}</value>
</data>

View File

@ -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}">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
@ -39,6 +40,26 @@
DropdownItemsTemplate={StaticResource ActionDropdownItem}}" />
<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>
</UserControl>

View File

@ -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<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
@ -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)
{
}
}
}

View File

@ -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));

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