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:
parent
0e27fec1e9
commit
543b55f3ba
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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(() =>
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
27
Windows/MassiveKnob/Strings.Designer.cs
generated
27
Windows/MassiveKnob/Strings.Designer.cs
generated
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
Loading…
Reference in New Issue
Block a user