Working proof-of-concept for refactoring
Implemented orchestrator Implemented mock device Implemented serial device Implemented volume action Removed old implementation
This commit is contained in:
parent
dc0e761244
commit
ff1e1ca74c
@ -9,8 +9,7 @@ const byte KnobCount = 1;
|
|||||||
|
|
||||||
// For each potentiometer, specify the port
|
// For each potentiometer, specify the port
|
||||||
const byte KnobPin[KnobCount] = {
|
const byte KnobPin[KnobCount] = {
|
||||||
// A0,
|
A2
|
||||||
A1
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Minimum time between reporting changing values, reduces serial traffic
|
// Minimum time between reporting changing values, reduces serial traffic
|
||||||
@ -46,6 +45,7 @@ float emaValue[KnobCount];
|
|||||||
unsigned long currentTime;
|
unsigned long currentTime;
|
||||||
unsigned long lastPlot;
|
unsigned long lastPlot;
|
||||||
|
|
||||||
|
|
||||||
void setup()
|
void setup()
|
||||||
{
|
{
|
||||||
Serial.begin(115200);
|
Serial.begin(115200);
|
||||||
@ -56,7 +56,10 @@ void setup()
|
|||||||
|
|
||||||
// Seed the moving average
|
// Seed the moving average
|
||||||
for (byte knobIndex = 0; knobIndex < KnobCount; knobIndex++)
|
for (byte knobIndex = 0; knobIndex < KnobCount; knobIndex++)
|
||||||
|
{
|
||||||
|
pinMode(KnobPin[knobIndex], INPUT);
|
||||||
emaValue[knobIndex] = analogRead(KnobPin[knobIndex]);
|
emaValue[knobIndex] = analogRead(KnobPin[knobIndex]);
|
||||||
|
}
|
||||||
|
|
||||||
for (byte seed = 1; seed < EMASeedCount - 1; seed++)
|
for (byte seed = 1; seed < EMASeedCount - 1; seed++)
|
||||||
for (byte knobIndex = 0; knobIndex < KnobCount; knobIndex++)
|
for (byte knobIndex = 0; knobIndex < KnobCount; knobIndex++)
|
||||||
@ -119,18 +122,28 @@ void processMessage(byte message)
|
|||||||
case 'Q': // Quit
|
case 'Q': // Quit
|
||||||
processQuitMessage();
|
processQuitMessage();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
outputError("Unknown message: " + (char)message);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void processHandshakeMessage()
|
void processHandshakeMessage()
|
||||||
{
|
{
|
||||||
byte buffer[2];
|
byte buffer[3];
|
||||||
if (Serial.readBytes(buffer, 3) < 3)
|
if (Serial.readBytes(buffer, 3) < 3)
|
||||||
|
{
|
||||||
|
outputError("Invalid handshake length");
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (buffer[0] != 'M' || buffer[1] != 'K')
|
if (buffer[0] != 'M' || buffer[1] != 'K')
|
||||||
|
{
|
||||||
|
outputError("Invalid handshake: " + String((char)buffer[0]) + String((char)buffer[1]) + String((char)buffer[2]));
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
switch (buffer[2])
|
switch (buffer[2])
|
||||||
{
|
{
|
||||||
@ -147,6 +160,8 @@ void processHandshakeMessage()
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
outputMode = PlainText;
|
||||||
|
outputError("Unknown output mode: " + String((char)buffer[2]));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -185,6 +200,11 @@ void processQuitMessage()
|
|||||||
|
|
||||||
byte getVolume(byte knobIndex)
|
byte getVolume(byte knobIndex)
|
||||||
{
|
{
|
||||||
|
analogRead(KnobPin[knobIndex]);
|
||||||
|
|
||||||
|
// Give the ADC some time to stabilize
|
||||||
|
delay(10);
|
||||||
|
|
||||||
analogReadValue[knobIndex] = analogRead(KnobPin[knobIndex]);
|
analogReadValue[knobIndex] = analogRead(KnobPin[knobIndex]);
|
||||||
emaValue[knobIndex] = (EMAAlpha * analogReadValue[knobIndex]) + ((1 - EMAAlpha) * emaValue[knobIndex]);
|
emaValue[knobIndex] = (EMAAlpha * analogReadValue[knobIndex]) + ((1 - EMAAlpha) * emaValue[knobIndex]);
|
||||||
|
|
||||||
@ -228,3 +248,21 @@ void outputPlotter()
|
|||||||
|
|
||||||
Serial.println();
|
Serial.println();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void outputError(String message)
|
||||||
|
{
|
||||||
|
switch (outputMode)
|
||||||
|
{
|
||||||
|
case Binary:
|
||||||
|
Serial.write('E');
|
||||||
|
Serial.write((byte)message.length());
|
||||||
|
Serial.print(message);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PlainText:
|
||||||
|
Serial.print("Error: ");
|
||||||
|
Serial.println(message);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
|
using AudioSwitcher.AudioApi;
|
||||||
|
using MassiveKnob.Plugin.CoreAudio.Settings;
|
||||||
|
|
||||||
namespace MassiveKnob.Plugin.CoreAudio.Actions
|
namespace MassiveKnob.Plugin.CoreAudio.Actions
|
||||||
{
|
{
|
||||||
@ -12,45 +14,62 @@ namespace MassiveKnob.Plugin.CoreAudio.Actions
|
|||||||
public string Description { get; } = "Sets the volume for the selected device, regardless of the current default device.";
|
public string Description { get; } = "Sets the volume for the selected device, regardless of the current default device.";
|
||||||
|
|
||||||
|
|
||||||
public IMassiveKnobActionInstance Create(IMassiveKnobActionContext context)
|
public IMassiveKnobActionInstance Create()
|
||||||
{
|
{
|
||||||
return new Instance(context);
|
return new Instance();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private class Instance : IMassiveKnobAnalogAction
|
private class Instance : IMassiveKnobAnalogAction
|
||||||
{
|
{
|
||||||
private readonly Settings settings;
|
private IMassiveKnobActionContext actionContext;
|
||||||
|
private DeviceVolumeActionSettings settings;
|
||||||
|
private IDevice playbackDevice;
|
||||||
|
|
||||||
|
|
||||||
public Instance(IMassiveKnobContext context)
|
public void Initialize(IMassiveKnobActionContext context)
|
||||||
{
|
{
|
||||||
settings = context.GetSettings<Settings>();
|
actionContext = context;
|
||||||
|
settings = context.GetSettings<DeviceVolumeActionSettings>();
|
||||||
|
ApplySettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void ApplySettings()
|
||||||
|
{
|
||||||
|
var coreAudioController = CoreAudioControllerInstance.Acquire();
|
||||||
|
playbackDevice = settings.DeviceId.HasValue ? coreAudioController.GetDevice(settings.DeviceId.Value) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public UserControl CreateSettingsControl()
|
public UserControl CreateSettingsControl()
|
||||||
{
|
{
|
||||||
return null;
|
var viewModel = new DeviceVolumeActionSettingsViewModel(settings);
|
||||||
|
viewModel.PropertyChanged += (sender, args) =>
|
||||||
|
{
|
||||||
|
if (!viewModel.IsSettingsProperty(args.PropertyName))
|
||||||
|
return;
|
||||||
|
|
||||||
|
actionContext.SetSettings(settings);
|
||||||
|
ApplySettings();
|
||||||
|
};
|
||||||
|
|
||||||
|
return new DeviceVolumeActionSettingsView(viewModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public ValueTask AnalogChanged(byte value)
|
public async ValueTask AnalogChanged(byte value)
|
||||||
{
|
{
|
||||||
// TODO set volume
|
if (playbackDevice == null)
|
||||||
return default;
|
return;
|
||||||
|
|
||||||
|
await playbackDevice.SetVolumeAsync(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private class Settings
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,16 @@
|
|||||||
|
using System;
|
||||||
|
using AudioSwitcher.AudioApi.CoreAudio;
|
||||||
|
|
||||||
|
namespace MassiveKnob.Plugin.CoreAudio
|
||||||
|
{
|
||||||
|
public static class CoreAudioControllerInstance
|
||||||
|
{
|
||||||
|
private static readonly Lazy<CoreAudioController> Instance = new Lazy<CoreAudioController>();
|
||||||
|
|
||||||
|
|
||||||
|
public static CoreAudioController Acquire()
|
||||||
|
{
|
||||||
|
return Instance.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -46,10 +46,20 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="Actions\DeviceVolumeAction.cs" />
|
<Compile Include="Actions\DeviceVolumeAction.cs" />
|
||||||
|
<Compile Include="CoreAudioControllerInstance.cs" />
|
||||||
<Compile Include="MassiveKnobCoreAudioPlugin.cs" />
|
<Compile Include="MassiveKnobCoreAudioPlugin.cs" />
|
||||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
<Compile Include="Settings\DeviceVolumeActionSettings.xaml.cs">
|
<Compile Include="Settings\DeviceVolumeActionSettings.cs" />
|
||||||
<DependentUpon>DeviceVolumeActionSettings.xaml</DependentUpon>
|
<Compile Include="Settings\DeviceVolumeActionSettingsView.xaml.cs">
|
||||||
|
<DependentUpon>DeviceVolumeActionSettingsView.xaml</DependentUpon>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="Settings\BaseDeviceSettings.cs" />
|
||||||
|
<Compile Include="Settings\BaseDeviceSettingsViewModel.cs" />
|
||||||
|
<Compile Include="Settings\DeviceVolumeActionSettingsViewModel.cs" />
|
||||||
|
<Compile Include="Strings.Designer.cs">
|
||||||
|
<DependentUpon>Strings.resx</DependentUpon>
|
||||||
|
<AutoGen>True</AutoGen>
|
||||||
|
<DesignTime>True</DesignTime>
|
||||||
</Compile>
|
</Compile>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@ -59,13 +69,21 @@
|
|||||||
</ProjectReference>
|
</ProjectReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="AudioSwitcher.AudioApi.CoreAudio">
|
||||||
|
<Version>4.0.0-alpha5</Version>
|
||||||
|
</PackageReference>
|
||||||
<PackageReference Include="System.Threading.Tasks.Extensions">
|
<PackageReference Include="System.Threading.Tasks.Extensions">
|
||||||
<Version>4.5.4</Version>
|
<Version>4.5.4</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup />
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Page Include="Settings\DeviceVolumeActionSettings.xaml">
|
<EmbeddedResource Include="Strings.resx">
|
||||||
|
<Generator>ResXFileCodeGenerator</Generator>
|
||||||
|
<LastGenOutput>Strings.Designer.cs</LastGenOutput>
|
||||||
|
</EmbeddedResource>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Page Include="Settings\DeviceVolumeActionSettingsView.xaml">
|
||||||
<SubType>Designer</SubType>
|
<SubType>Designer</SubType>
|
||||||
<Generator>MSBuild:Compile</Generator>
|
<Generator>MSBuild:Compile</Generator>
|
||||||
</Page>
|
</Page>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using MassiveKnob.Plugin.CoreAudio.Actions;
|
using MassiveKnob.Plugin.CoreAudio.Actions;
|
||||||
|
|
||||||
namespace MassiveKnob.Plugin.CoreAudio
|
namespace MassiveKnob.Plugin.CoreAudio
|
||||||
@ -17,5 +18,17 @@ namespace MassiveKnob.Plugin.CoreAudio
|
|||||||
{
|
{
|
||||||
new DeviceVolumeAction()
|
new DeviceVolumeAction()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
public MassiveKnobCoreAudioPlugin()
|
||||||
|
{
|
||||||
|
// My system suffers from this issue: https://github.com/xenolightning/AudioSwitcher/issues/40
|
||||||
|
// ...which causes the first call to the CoreAudioController to take up to 10 seconds,
|
||||||
|
// so initialise it as soon as possible. Bit of a workaround, but eh.
|
||||||
|
Task.Run(() =>
|
||||||
|
{
|
||||||
|
CoreAudioControllerInstance.Acquire();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
// General Information about an assembly is controlled through the following
|
// General Information about an assembly is controlled through the following
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace MassiveKnob.Plugin.CoreAudio.Settings
|
||||||
|
{
|
||||||
|
public class BaseDeviceSettings
|
||||||
|
{
|
||||||
|
public Guid? DeviceId { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,110 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows;
|
||||||
|
using AudioSwitcher.AudioApi;
|
||||||
|
|
||||||
|
namespace MassiveKnob.Plugin.CoreAudio.Settings
|
||||||
|
{
|
||||||
|
public class BaseDeviceSettingsViewModel : INotifyPropertyChanged
|
||||||
|
{
|
||||||
|
private readonly BaseDeviceSettings settings;
|
||||||
|
public event PropertyChangedEventHandler PropertyChanged;
|
||||||
|
|
||||||
|
private IList<PlaybackDeviceViewModel> playbackDevices;
|
||||||
|
private PlaybackDeviceViewModel selectedDevice;
|
||||||
|
|
||||||
|
// ReSharper disable UnusedMember.Global - used by WPF Binding
|
||||||
|
public IList<PlaybackDeviceViewModel> PlaybackDevices
|
||||||
|
{
|
||||||
|
get => playbackDevices;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
playbackDevices = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlaybackDeviceViewModel SelectedDevice
|
||||||
|
{
|
||||||
|
get => selectedDevice;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == selectedDevice)
|
||||||
|
return;
|
||||||
|
|
||||||
|
selectedDevice = value;
|
||||||
|
settings.DeviceId = selectedDevice?.Id;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ReSharper restore UnusedMember.Global
|
||||||
|
|
||||||
|
|
||||||
|
public BaseDeviceSettingsViewModel(BaseDeviceSettings settings)
|
||||||
|
{
|
||||||
|
this.settings = settings;
|
||||||
|
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
var coreAudioController = CoreAudioControllerInstance.Acquire();
|
||||||
|
var devices = await coreAudioController.GetPlaybackDevicesAsync();
|
||||||
|
var deviceViewModels = devices
|
||||||
|
.OrderBy(d => d.FullName)
|
||||||
|
.Select(PlaybackDeviceViewModel.FromDevice)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
Application.Current.Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
PlaybackDevices = deviceViewModels;
|
||||||
|
SelectedDevice = deviceViewModels.SingleOrDefault(d => d.Id == settings.DeviceId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public virtual bool IsSettingsProperty(string propertyName)
|
||||||
|
{
|
||||||
|
return propertyName != nameof(PlaybackDevices);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
|
||||||
|
{
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public class PlaybackDeviceViewModel
|
||||||
|
{
|
||||||
|
public Guid Id { get; private set; }
|
||||||
|
public string DisplayName { get; private set; }
|
||||||
|
|
||||||
|
|
||||||
|
public static PlaybackDeviceViewModel FromDevice(IDevice device)
|
||||||
|
{
|
||||||
|
string displayFormat;
|
||||||
|
|
||||||
|
if ((device.State & DeviceState.Disabled) != 0)
|
||||||
|
displayFormat = Strings.DeviceDisplayNameDisabled;
|
||||||
|
else if ((device.State & DeviceState.Unplugged) != 0)
|
||||||
|
displayFormat = Strings.DeviceDisplayNameUnplugged;
|
||||||
|
else if ((device.State & DeviceState.NotPresent) != 0)
|
||||||
|
displayFormat = Strings.DeviceDisplayNameNotPresent;
|
||||||
|
else if ((device.State & DeviceState.Active) == 0)
|
||||||
|
displayFormat = Strings.DeviceDisplayNameInactive;
|
||||||
|
else
|
||||||
|
displayFormat = Strings.DeviceDisplayNameActive;
|
||||||
|
|
||||||
|
return new PlaybackDeviceViewModel
|
||||||
|
{
|
||||||
|
Id = device.Id,
|
||||||
|
DisplayName = string.Format(displayFormat, device.FullName)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
namespace MassiveKnob.Plugin.CoreAudio.Settings
|
||||||
|
{
|
||||||
|
public class DeviceVolumeActionSettings : BaseDeviceSettings
|
||||||
|
{
|
||||||
|
// TODO OSD
|
||||||
|
}
|
||||||
|
}
|
@ -1,12 +0,0 @@
|
|||||||
<UserControl x:Class="MassiveKnob.Plugin.CoreAudio.Settings.DeviceVolumeActionSettings"
|
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
|
||||||
xmlns:local="clr-namespace:MassiveKnob.Plugin.CoreAudio.Settings"
|
|
||||||
mc:Ignorable="d"
|
|
||||||
d:DesignHeight="450" d:DesignWidth="800">
|
|
||||||
<Grid>
|
|
||||||
|
|
||||||
</Grid>
|
|
||||||
</UserControl>
|
|
@ -1,28 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows;
|
|
||||||
using System.Windows.Controls;
|
|
||||||
using System.Windows.Data;
|
|
||||||
using System.Windows.Documents;
|
|
||||||
using System.Windows.Input;
|
|
||||||
using System.Windows.Media;
|
|
||||||
using System.Windows.Media.Imaging;
|
|
||||||
using System.Windows.Navigation;
|
|
||||||
using System.Windows.Shapes;
|
|
||||||
|
|
||||||
namespace MassiveKnob.Plugin.CoreAudio.Settings
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Interaction logic for DeviceVolumeActionSettings.xaml
|
|
||||||
/// </summary>
|
|
||||||
public partial class DeviceVolumeActionSettings : UserControl
|
|
||||||
{
|
|
||||||
public DeviceVolumeActionSettings()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,14 @@
|
|||||||
|
<UserControl x:Class="MassiveKnob.Plugin.CoreAudio.Settings.DeviceVolumeActionSettingsView"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:settings="clr-namespace:MassiveKnob.Plugin.CoreAudio.Settings"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DesignHeight="200" d:DesignWidth="800"
|
||||||
|
d:DataContext="{d:DesignInstance settings:DeviceVolumeActionSettingsViewModel}">
|
||||||
|
<StackPanel Orientation="Vertical">
|
||||||
|
<TextBlock>Playback device</TextBlock>
|
||||||
|
<ComboBox ItemsSource="{Binding PlaybackDevices}" SelectedItem="{Binding SelectedDevice}" DisplayMemberPath="DisplayName" />
|
||||||
|
</StackPanel>
|
||||||
|
</UserControl>
|
@ -0,0 +1,14 @@
|
|||||||
|
namespace MassiveKnob.Plugin.CoreAudio.Settings
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Interaction logic for DeviceVolumeActionSettingsView.xaml
|
||||||
|
/// </summary>
|
||||||
|
public partial class DeviceVolumeActionSettingsView
|
||||||
|
{
|
||||||
|
public DeviceVolumeActionSettingsView(DeviceVolumeActionSettingsViewModel viewModel)
|
||||||
|
{
|
||||||
|
DataContext = viewModel;
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
namespace MassiveKnob.Plugin.CoreAudio.Settings
|
||||||
|
{
|
||||||
|
public class DeviceVolumeActionSettingsViewModel : BaseDeviceSettingsViewModel
|
||||||
|
{
|
||||||
|
// ReSharper disable once SuggestBaseTypeForParameter - by design
|
||||||
|
public DeviceVolumeActionSettingsViewModel(DeviceVolumeActionSettings settings) : base(settings)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
108
Windows/MassiveKnob.Plugin.CoreAudio/Strings.Designer.cs
generated
Normal file
108
Windows/MassiveKnob.Plugin.CoreAudio/Strings.Designer.cs
generated
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// <auto-generated>
|
||||||
|
// This code was generated by a tool.
|
||||||
|
// Runtime Version:4.0.30319.42000
|
||||||
|
//
|
||||||
|
// Changes to this file may cause incorrect behavior and will be lost if
|
||||||
|
// the code is regenerated.
|
||||||
|
// </auto-generated>
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
namespace MassiveKnob.Plugin.CoreAudio {
|
||||||
|
using System;
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A strongly-typed resource class, for looking up localized strings, etc.
|
||||||
|
/// </summary>
|
||||||
|
// This class was auto-generated by the StronglyTypedResourceBuilder
|
||||||
|
// class via a tool like ResGen or Visual Studio.
|
||||||
|
// To add or remove a member, edit your .ResX file then rerun ResGen
|
||||||
|
// with the /str option, or rebuild your VS project.
|
||||||
|
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
|
||||||
|
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||||
|
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||||
|
internal class Strings {
|
||||||
|
|
||||||
|
private static global::System.Resources.ResourceManager resourceMan;
|
||||||
|
|
||||||
|
private static global::System.Globalization.CultureInfo resourceCulture;
|
||||||
|
|
||||||
|
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
||||||
|
internal Strings() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the cached ResourceManager instance used by this class.
|
||||||
|
/// </summary>
|
||||||
|
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||||
|
internal static global::System.Resources.ResourceManager ResourceManager {
|
||||||
|
get {
|
||||||
|
if (object.ReferenceEquals(resourceMan, null)) {
|
||||||
|
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("MassiveKnob.Plugin.CoreAudio.Strings", typeof(Strings).Assembly);
|
||||||
|
resourceMan = temp;
|
||||||
|
}
|
||||||
|
return resourceMan;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Overrides the current thread's CurrentUICulture property for all
|
||||||
|
/// resource lookups using this strongly typed resource class.
|
||||||
|
/// </summary>
|
||||||
|
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||||
|
internal static global::System.Globalization.CultureInfo Culture {
|
||||||
|
get {
|
||||||
|
return resourceCulture;
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
resourceCulture = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to {0}.
|
||||||
|
/// </summary>
|
||||||
|
internal static string DeviceDisplayNameActive {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("DeviceDisplayNameActive", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to {0} (Disabled).
|
||||||
|
/// </summary>
|
||||||
|
internal static string DeviceDisplayNameDisabled {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("DeviceDisplayNameDisabled", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to {0} (Inactive).
|
||||||
|
/// </summary>
|
||||||
|
internal static string DeviceDisplayNameInactive {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("DeviceDisplayNameInactive", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to {0} (Not present).
|
||||||
|
/// </summary>
|
||||||
|
internal static string DeviceDisplayNameNotPresent {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("DeviceDisplayNameNotPresent", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to {0} (Unplugged).
|
||||||
|
/// </summary>
|
||||||
|
internal static string DeviceDisplayNameUnplugged {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("DeviceDisplayNameUnplugged", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
135
Windows/MassiveKnob.Plugin.CoreAudio/Strings.resx
Normal file
135
Windows/MassiveKnob.Plugin.CoreAudio/Strings.resx
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<root>
|
||||||
|
<!--
|
||||||
|
Microsoft ResX Schema
|
||||||
|
|
||||||
|
Version 2.0
|
||||||
|
|
||||||
|
The primary goals of this format is to allow a simple XML format
|
||||||
|
that is mostly human readable. The generation and parsing of the
|
||||||
|
various data types are done through the TypeConverter classes
|
||||||
|
associated with the data types.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
... ado.net/XML headers & schema ...
|
||||||
|
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||||
|
<resheader name="version">2.0</resheader>
|
||||||
|
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||||
|
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||||
|
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||||
|
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||||
|
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||||
|
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||||
|
</data>
|
||||||
|
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||||
|
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||||
|
<comment>This is a comment</comment>
|
||||||
|
</data>
|
||||||
|
|
||||||
|
There are any number of "resheader" rows that contain simple
|
||||||
|
name/value pairs.
|
||||||
|
|
||||||
|
Each data row contains a name, and value. The row also contains a
|
||||||
|
type or mimetype. Type corresponds to a .NET class that support
|
||||||
|
text/value conversion through the TypeConverter architecture.
|
||||||
|
Classes that don't support this are serialized and stored with the
|
||||||
|
mimetype set.
|
||||||
|
|
||||||
|
The mimetype is used for serialized objects, and tells the
|
||||||
|
ResXResourceReader how to depersist the object. This is currently not
|
||||||
|
extensible. For a given mimetype the value must be set accordingly:
|
||||||
|
|
||||||
|
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||||
|
that the ResXResourceWriter will generate, however the reader can
|
||||||
|
read any of the formats listed below.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.binary.base64
|
||||||
|
value : The object must be serialized with
|
||||||
|
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.soap.base64
|
||||||
|
value : The object must be serialized with
|
||||||
|
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||||
|
value : The object must be serialized into a byte array
|
||||||
|
: using a System.ComponentModel.TypeConverter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
-->
|
||||||
|
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||||
|
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||||
|
<xsd:element name="root" msdata:IsDataSet="true">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:choice maxOccurs="unbounded">
|
||||||
|
<xsd:element name="metadata">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="assembly">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:attribute name="alias" type="xsd:string" />
|
||||||
|
<xsd:attribute name="name" type="xsd:string" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="data">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="resheader">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:choice>
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:schema>
|
||||||
|
<resheader name="resmimetype">
|
||||||
|
<value>text/microsoft-resx</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="version">
|
||||||
|
<value>2.0</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="reader">
|
||||||
|
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="writer">
|
||||||
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<data name="DeviceDisplayNameActive" xml:space="preserve">
|
||||||
|
<value>{0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeviceDisplayNameDisabled" xml:space="preserve">
|
||||||
|
<value>{0} (Disabled)</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeviceDisplayNameInactive" xml:space="preserve">
|
||||||
|
<value>{0} (Inactive)</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeviceDisplayNameNotPresent" xml:space="preserve">
|
||||||
|
<value>{0} (Not present)</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeviceDisplayNameUnplugged" xml:space="preserve">
|
||||||
|
<value>{0} (Unplugged)</value>
|
||||||
|
</data>
|
||||||
|
</root>
|
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Threading;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
using MassiveKnob.Plugin.MockDevice.Settings;
|
using MassiveKnob.Plugin.MockDevice.Settings;
|
||||||
|
|
||||||
@ -9,37 +10,86 @@ namespace MassiveKnob.Plugin.MockDevice.Devices
|
|||||||
public Guid DeviceId { get; } = new Guid("e1a4977a-abf4-4c75-a17d-fd8d3a8451ff");
|
public Guid DeviceId { get; } = new Guid("e1a4977a-abf4-4c75-a17d-fd8d3a8451ff");
|
||||||
public string Name { get; } = "Mock device";
|
public string Name { get; } = "Mock device";
|
||||||
public string Description { get; } = "Emulates the actual device but does not communicate with anything.";
|
public string Description { get; } = "Emulates the actual device but does not communicate with anything.";
|
||||||
|
|
||||||
public IMassiveKnobDeviceInstance Create(IMassiveKnobContext context)
|
public IMassiveKnobDeviceInstance Create()
|
||||||
{
|
{
|
||||||
return new Instance(context);
|
return new Instance();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private class Instance : IMassiveKnobDeviceInstance
|
private class Instance : IMassiveKnobDeviceInstance
|
||||||
{
|
{
|
||||||
public Instance(IMassiveKnobContext context)
|
private IMassiveKnobDeviceContext deviceContext;
|
||||||
|
private MockDeviceSettings settings;
|
||||||
|
private Timer inputChangeTimer;
|
||||||
|
|
||||||
|
private int reportedAnalogInputCount;
|
||||||
|
private int reportedDigitalInputCount;
|
||||||
|
private readonly Random random = new Random();
|
||||||
|
|
||||||
|
|
||||||
|
public void Initialize(IMassiveKnobDeviceContext context)
|
||||||
{
|
{
|
||||||
// TODO read settings
|
deviceContext = context;
|
||||||
|
settings = deviceContext.GetSettings<MockDeviceSettings>();
|
||||||
|
|
||||||
|
ApplySettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
|
inputChangeTimer?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void ApplySettings()
|
||||||
|
{
|
||||||
|
if (settings.AnalogCount != reportedAnalogInputCount ||
|
||||||
|
settings.DigitalCount != reportedDigitalInputCount)
|
||||||
|
{
|
||||||
|
deviceContext.Connected(new DeviceSpecs(settings.AnalogCount, settings.DigitalCount, 0, 0));
|
||||||
|
|
||||||
|
reportedAnalogInputCount = settings.AnalogCount;
|
||||||
|
reportedDigitalInputCount = settings.DigitalCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var interval = TimeSpan.FromSeconds(Math.Max(settings.Interval, 1));
|
||||||
|
|
||||||
|
if (inputChangeTimer == null)
|
||||||
|
inputChangeTimer = new Timer(Tick, null, interval, interval);
|
||||||
|
else
|
||||||
|
inputChangeTimer.Change(interval, interval);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public UserControl CreateSettingsControl()
|
public UserControl CreateSettingsControl()
|
||||||
{
|
{
|
||||||
// TODO pass context
|
var viewModel = new MockDeviceSettingsViewModel(settings);
|
||||||
return new MockDeviceSettings();
|
viewModel.PropertyChanged += (sender, args) =>
|
||||||
|
{
|
||||||
|
deviceContext.SetSettings(settings);
|
||||||
|
ApplySettings();
|
||||||
|
};
|
||||||
|
|
||||||
|
return new MockDeviceSettingsView(viewModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void Tick(object state)
|
||||||
|
{
|
||||||
|
var totalInputCount = reportedAnalogInputCount + reportedDigitalInputCount;
|
||||||
|
if (totalInputCount == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var changeInput = random.Next(0, totalInputCount);
|
||||||
|
|
||||||
|
if (changeInput < reportedAnalogInputCount)
|
||||||
|
deviceContext.AnalogChanged(changeInput, (byte)random.Next(0, 101));
|
||||||
|
else
|
||||||
|
deviceContext.DigitalChanged(changeInput - reportedAnalogInputCount, random.Next(0, 2) == 1);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private class Settings
|
|
||||||
{
|
|
||||||
// TODO interval, etc.
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -48,9 +48,11 @@
|
|||||||
<Compile Include="Devices\MockDevice.cs" />
|
<Compile Include="Devices\MockDevice.cs" />
|
||||||
<Compile Include="MassiveKnobMockDevicePlugin.cs" />
|
<Compile Include="MassiveKnobMockDevicePlugin.cs" />
|
||||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
<Compile Include="Settings\MockDeviceSettings.xaml.cs">
|
<Compile Include="Settings\MockDeviceSettingsView.xaml.cs">
|
||||||
<DependentUpon>MockDeviceSettings.xaml</DependentUpon>
|
<DependentUpon>MockDeviceSettingsView.xaml</DependentUpon>
|
||||||
</Compile>
|
</Compile>
|
||||||
|
<Compile Include="Settings\MockDeviceSettings.cs" />
|
||||||
|
<Compile Include="Settings\MockDeviceSettingsViewModel.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\MassiveKnob.Plugin\MassiveKnob.Plugin.csproj">
|
<ProjectReference Include="..\MassiveKnob.Plugin\MassiveKnob.Plugin.csproj">
|
||||||
@ -60,7 +62,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup />
|
<ItemGroup />
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Page Include="Settings\MockDeviceSettings.xaml">
|
<Page Include="Settings\MockDeviceSettingsView.xaml">
|
||||||
<SubType>Designer</SubType>
|
<SubType>Designer</SubType>
|
||||||
<Generator>MSBuild:Compile</Generator>
|
<Generator>MSBuild:Compile</Generator>
|
||||||
</Page>
|
</Page>
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
namespace MassiveKnob.Plugin.MockDevice.Settings
|
||||||
|
{
|
||||||
|
public class MockDeviceSettings
|
||||||
|
{
|
||||||
|
public int AnalogCount { get; set; } = 3;
|
||||||
|
public int DigitalCount { get; set; } = 1;
|
||||||
|
public int Interval { get; set; } = 5;
|
||||||
|
}
|
||||||
|
}
|
@ -1,24 +0,0 @@
|
|||||||
<UserControl x:Class="MassiveKnob.Plugin.MockDevice.Settings.MockDeviceSettings"
|
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
|
||||||
xmlns:local="clr-namespace:MassiveKnob.Plugin.MockDevice.Settings"
|
|
||||||
mc:Ignorable="d" d:DesignWidth="800">
|
|
||||||
<Grid>
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<TextBlock Grid.Row="0" Grid.Column="0" Margin="4">Number of knobs</TextBlock>
|
|
||||||
<TextBox Grid.Row="0" Grid.Column="1" Margin="4" Width="150" HorizontalAlignment="Left"></TextBox>
|
|
||||||
|
|
||||||
<CheckBox Grid.Row="1" Grid.Column="1" Margin="4">Randomly change the volume</CheckBox>
|
|
||||||
</Grid>
|
|
||||||
</UserControl>
|
|
@ -1,28 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows;
|
|
||||||
using System.Windows.Controls;
|
|
||||||
using System.Windows.Data;
|
|
||||||
using System.Windows.Documents;
|
|
||||||
using System.Windows.Input;
|
|
||||||
using System.Windows.Media;
|
|
||||||
using System.Windows.Media.Imaging;
|
|
||||||
using System.Windows.Navigation;
|
|
||||||
using System.Windows.Shapes;
|
|
||||||
|
|
||||||
namespace MassiveKnob.Plugin.MockDevice.Settings
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Interaction logic for MockDeviceSettings.xaml
|
|
||||||
/// </summary>
|
|
||||||
public partial class MockDeviceSettings : UserControl
|
|
||||||
{
|
|
||||||
public MockDeviceSettings()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,30 @@
|
|||||||
|
<UserControl x:Class="MassiveKnob.Plugin.MockDevice.Settings.MockDeviceSettingsView"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:settings="clr-namespace:MassiveKnob.Plugin.MockDevice.Settings"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800"
|
||||||
|
d:DataContext="{d:DesignInstance Type=settings:MockDeviceSettingsViewModel}">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="0" Margin="4">Number of analog inputs</TextBlock>
|
||||||
|
<TextBox Grid.Row="0" Grid.Column="1" Margin="4" Width="150" HorizontalAlignment="Left" Text="{Binding AnalogCount}" />
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="1" Grid.Column="0" Margin="4">Number of digital inputs</TextBlock>
|
||||||
|
<TextBox Grid.Row="1" Grid.Column="1" Margin="4" Width="150" HorizontalAlignment="Left" Text="{Binding DigitalCount}" />
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="2" Grid.Column="0" Margin="4">Random change interval (seconds)</TextBlock>
|
||||||
|
<TextBox Grid.Row="2" Grid.Column="1" Margin="4" Width="150" HorizontalAlignment="Left" Text="{Binding Interval}" />
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
@ -0,0 +1,15 @@
|
|||||||
|
namespace MassiveKnob.Plugin.MockDevice.Settings
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Interaction logic for MockDeviceSettingsView.xaml
|
||||||
|
/// </summary>
|
||||||
|
public partial class MockDeviceSettingsView
|
||||||
|
{
|
||||||
|
public MockDeviceSettingsView(MockDeviceSettingsViewModel settingsViewModel)
|
||||||
|
{
|
||||||
|
DataContext = settingsViewModel;
|
||||||
|
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,67 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace MassiveKnob.Plugin.MockDevice.Settings
|
||||||
|
{
|
||||||
|
public class MockDeviceSettingsViewModel : INotifyPropertyChanged
|
||||||
|
{
|
||||||
|
private readonly MockDeviceSettings settings;
|
||||||
|
public event PropertyChangedEventHandler PropertyChanged;
|
||||||
|
|
||||||
|
|
||||||
|
// ReSharper disable UnusedMember.Global - used by WPF Binding
|
||||||
|
public int AnalogCount
|
||||||
|
{
|
||||||
|
get => settings.AnalogCount;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == settings.AnalogCount)
|
||||||
|
return;
|
||||||
|
|
||||||
|
settings.AnalogCount = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public int DigitalCount
|
||||||
|
{
|
||||||
|
get => settings.DigitalCount;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == settings.DigitalCount)
|
||||||
|
return;
|
||||||
|
|
||||||
|
settings.DigitalCount = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public int Interval
|
||||||
|
{
|
||||||
|
get => settings.Interval;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == settings.Interval)
|
||||||
|
return;
|
||||||
|
|
||||||
|
settings.Interval = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ReSharper restore UnusedMember.Global
|
||||||
|
|
||||||
|
|
||||||
|
public MockDeviceSettingsViewModel(MockDeviceSettings settings)
|
||||||
|
{
|
||||||
|
this.settings = settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
|
||||||
|
{
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,64 @@
|
|||||||
|
using System;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using MassiveKnob.Plugin.SerialDevice.Settings;
|
||||||
|
using MassiveKnob.Plugin.SerialDevice.Worker;
|
||||||
|
|
||||||
|
namespace MassiveKnob.Plugin.SerialDevice.Devices
|
||||||
|
{
|
||||||
|
public class SerialDevice : IMassiveKnobDevice
|
||||||
|
{
|
||||||
|
public Guid DeviceId { get; } = new Guid("65255f25-d8f6-426b-8f12-cf03c44a1bf5");
|
||||||
|
public string Name { get; } = "Serial device";
|
||||||
|
public string Description { get; } = "A Serial (USB) device which implements the Massive Knob Protocol.";
|
||||||
|
|
||||||
|
public IMassiveKnobDeviceInstance Create()
|
||||||
|
{
|
||||||
|
return new Instance();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private class Instance : IMassiveKnobDeviceInstance
|
||||||
|
{
|
||||||
|
private IMassiveKnobDeviceContext deviceContext;
|
||||||
|
private SerialDeviceSettings settings;
|
||||||
|
private SerialWorker worker;
|
||||||
|
|
||||||
|
public void Initialize(IMassiveKnobDeviceContext context)
|
||||||
|
{
|
||||||
|
deviceContext = context;
|
||||||
|
settings = deviceContext.GetSettings<SerialDeviceSettings>();
|
||||||
|
|
||||||
|
worker = new SerialWorker(context);
|
||||||
|
ApplySettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
worker.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void ApplySettings()
|
||||||
|
{
|
||||||
|
worker.Connect(settings.PortName, settings.BaudRate);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public UserControl CreateSettingsControl()
|
||||||
|
{
|
||||||
|
var viewModel = new SerialDeviceSettingsViewModel(settings);
|
||||||
|
viewModel.PropertyChanged += (sender, args) =>
|
||||||
|
{
|
||||||
|
if (!viewModel.IsSettingsProperty(args.PropertyName))
|
||||||
|
return;
|
||||||
|
|
||||||
|
deviceContext.SetSettings(settings);
|
||||||
|
ApplySettings();
|
||||||
|
};
|
||||||
|
|
||||||
|
return new SerialDeviceSettingsView(viewModel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,71 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||||
|
<PropertyGroup>
|
||||||
|
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||||
|
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||||
|
<ProjectGuid>{FC0D22D8-5F1B-4D85-8269-FA4837CDE3A2}</ProjectGuid>
|
||||||
|
<OutputType>Library</OutputType>
|
||||||
|
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||||
|
<RootNamespace>MassiveKnob.Plugin.SerialDevice</RootNamespace>
|
||||||
|
<AssemblyName>MassiveKnob.Plugin.SerialDevice</AssemblyName>
|
||||||
|
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
|
||||||
|
<FileAlignment>512</FileAlignment>
|
||||||
|
<Deterministic>true</Deterministic>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||||
|
<DebugSymbols>true</DebugSymbols>
|
||||||
|
<DebugType>full</DebugType>
|
||||||
|
<Optimize>false</Optimize>
|
||||||
|
<OutputPath>$(localappdata)\MassiveKnob\Plugins\</OutputPath>
|
||||||
|
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||||
|
<ErrorReport>prompt</ErrorReport>
|
||||||
|
<WarningLevel>4</WarningLevel>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||||
|
<DebugType>pdbonly</DebugType>
|
||||||
|
<Optimize>true</Optimize>
|
||||||
|
<OutputPath>bin\Release\</OutputPath>
|
||||||
|
<DefineConstants>TRACE</DefineConstants>
|
||||||
|
<ErrorReport>prompt</ErrorReport>
|
||||||
|
<WarningLevel>4</WarningLevel>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="PresentationCore" />
|
||||||
|
<Reference Include="PresentationFramework" />
|
||||||
|
<Reference Include="System" />
|
||||||
|
<Reference Include="System.Core" />
|
||||||
|
<Reference Include="System.Xaml" />
|
||||||
|
<Reference Include="System.Xml.Linq" />
|
||||||
|
<Reference Include="System.Data.DataSetExtensions" />
|
||||||
|
<Reference Include="Microsoft.CSharp" />
|
||||||
|
<Reference Include="System.Data" />
|
||||||
|
<Reference Include="System.Net.Http" />
|
||||||
|
<Reference Include="System.Xml" />
|
||||||
|
<Reference Include="WindowsBase" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="Devices\SerialDevice.cs" />
|
||||||
|
<Compile Include="MassiveKnobSerialDevicePlugin.cs" />
|
||||||
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
|
<Compile Include="Settings\SerialDeviceSettingsView.xaml.cs">
|
||||||
|
<DependentUpon>SerialDeviceSettingsView.xaml</DependentUpon>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="Settings\SerialDeviceSettingsViewModel.cs" />
|
||||||
|
<Compile Include="Settings\SerialDeviceSettings.cs" />
|
||||||
|
<Compile Include="Worker\SerialWorker.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\MassiveKnob.Plugin\MassiveKnob.Plugin.csproj">
|
||||||
|
<Project>{A1298BE4-1D23-416C-8C56-FC9264487A95}</Project>
|
||||||
|
<Name>MassiveKnob.Plugin</Name>
|
||||||
|
</ProjectReference>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Page Include="Settings\SerialDeviceSettingsView.xaml">
|
||||||
|
<SubType>Designer</SubType>
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
</Page>
|
||||||
|
</ItemGroup>
|
||||||
|
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||||
|
</Project>
|
@ -0,0 +1,20 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace MassiveKnob.Plugin.SerialDevice
|
||||||
|
{
|
||||||
|
[MassiveKnobPlugin]
|
||||||
|
public class MassiveKnobSerialDevicePlugin : IMassiveKnobDevicePlugin
|
||||||
|
{
|
||||||
|
public Guid PluginId { get; } = new Guid("276475e6-5ff0-420f-82dc-8aff5e8631d5");
|
||||||
|
public string Name { get; } = "Serial Device";
|
||||||
|
public string Description { get; } = "A Serial (USB) device which implements the Massive Knob Protocol.";
|
||||||
|
public string Author { get; } = "Mark van Renswoude <mark@x2software.net>";
|
||||||
|
public string Url { get; } = "https://www.github.com/MvRens/MassiveKnob/";
|
||||||
|
|
||||||
|
public IEnumerable<IMassiveKnobDevice> Devices { get; } = new IMassiveKnobDevice[]
|
||||||
|
{
|
||||||
|
new Devices.SerialDevice()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
// General Information about an assembly is controlled through the following
|
||||||
|
// set of attributes. Change these attribute values to modify the information
|
||||||
|
// associated with an assembly.
|
||||||
|
[assembly: AssemblyTitle("MassiveKnob.Plugin.SerialDevice")]
|
||||||
|
[assembly: AssemblyDescription("")]
|
||||||
|
[assembly: AssemblyConfiguration("")]
|
||||||
|
[assembly: AssemblyCompany("")]
|
||||||
|
[assembly: AssemblyProduct("MassiveKnob.Plugin.SerialDevice")]
|
||||||
|
[assembly: AssemblyCopyright("Copyright © 2021")]
|
||||||
|
[assembly: AssemblyTrademark("")]
|
||||||
|
[assembly: AssemblyCulture("")]
|
||||||
|
|
||||||
|
// Setting ComVisible to false makes the types in this assembly not visible
|
||||||
|
// to COM components. If you need to access a type in this assembly from
|
||||||
|
// COM, set the ComVisible attribute to true on that type.
|
||||||
|
[assembly: ComVisible(false)]
|
||||||
|
|
||||||
|
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||||
|
[assembly: Guid("fc0d22d8-5f1b-4d85-8269-fa4837cde3a2")]
|
||||||
|
|
||||||
|
// Version information for an assembly consists of the following four values:
|
||||||
|
//
|
||||||
|
// Major Version
|
||||||
|
// Minor Version
|
||||||
|
// Build Number
|
||||||
|
// Revision
|
||||||
|
//
|
||||||
|
// You can specify all the values or you can default the Build and Revision Numbers
|
||||||
|
// by using the '*' as shown below:
|
||||||
|
// [assembly: AssemblyVersion("1.0.*")]
|
||||||
|
[assembly: AssemblyVersion("1.0.0.0")]
|
||||||
|
[assembly: AssemblyFileVersion("1.0.0.0")]
|
@ -0,0 +1,8 @@
|
|||||||
|
namespace MassiveKnob.Plugin.SerialDevice.Settings
|
||||||
|
{
|
||||||
|
public class SerialDeviceSettings
|
||||||
|
{
|
||||||
|
public string PortName { get; set; } = null;
|
||||||
|
public int BaudRate { get; set; } = 115200;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
<UserControl x:Class="MassiveKnob.Plugin.SerialDevice.Settings.SerialDeviceSettingsView"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:settings="clr-namespace:MassiveKnob.Plugin.SerialDevice.Settings"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DesignHeight="100" d:DesignWidth="800"
|
||||||
|
d:DataContext="{d:DesignInstance settings:SerialDeviceSettingsViewModel}">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="0" Margin="4">Serial port</TextBlock>
|
||||||
|
<ComboBox Grid.Row="0" Grid.Column="1" Margin="4" Width="150" HorizontalAlignment="Left" ItemsSource="{Binding SerialPorts}" SelectedItem="{Binding PortName}" />
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="1" Grid.Column="0" Margin="4">Baud rate</TextBlock>
|
||||||
|
<TextBox Grid.Row="1" Grid.Column="1" Margin="4" Width="150" HorizontalAlignment="Left" Text="{Binding BaudRate}" />
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
@ -0,0 +1,14 @@
|
|||||||
|
namespace MassiveKnob.Plugin.SerialDevice.Settings
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Interaction logic for SerialDeviceSettingsView.xaml
|
||||||
|
/// </summary>
|
||||||
|
public partial class SerialDeviceSettingsView
|
||||||
|
{
|
||||||
|
public SerialDeviceSettingsView(SerialDeviceSettingsViewModel viewModel)
|
||||||
|
{
|
||||||
|
DataContext = viewModel;
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,77 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.IO.Ports;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace MassiveKnob.Plugin.SerialDevice.Settings
|
||||||
|
{
|
||||||
|
public class SerialDeviceSettingsViewModel : INotifyPropertyChanged
|
||||||
|
{
|
||||||
|
private readonly SerialDeviceSettings settings;
|
||||||
|
private IEnumerable<string> serialPorts;
|
||||||
|
public event PropertyChangedEventHandler PropertyChanged;
|
||||||
|
|
||||||
|
|
||||||
|
// ReSharper disable UnusedMember.Global - used by WPF Binding
|
||||||
|
public IEnumerable<string> SerialPorts
|
||||||
|
{
|
||||||
|
get => serialPorts;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
serialPorts = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public string PortName
|
||||||
|
{
|
||||||
|
get => settings.PortName;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == settings.PortName)
|
||||||
|
return;
|
||||||
|
|
||||||
|
settings.PortName = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public int BaudRate
|
||||||
|
{
|
||||||
|
get => settings.BaudRate;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == settings.BaudRate)
|
||||||
|
return;
|
||||||
|
|
||||||
|
settings.BaudRate = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ReSharper restore UnusedMember.Global
|
||||||
|
|
||||||
|
|
||||||
|
public SerialDeviceSettingsViewModel(SerialDeviceSettings settings)
|
||||||
|
{
|
||||||
|
this.settings = settings;
|
||||||
|
|
||||||
|
serialPorts = SerialPort.GetPortNames();
|
||||||
|
|
||||||
|
// TODO subscribe to device notification to refresh list
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public bool IsSettingsProperty(string propertyName)
|
||||||
|
{
|
||||||
|
return propertyName != nameof(SerialPorts);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
|
||||||
|
{
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
233
Windows/MassiveKnob.Plugin.SerialDevice/Worker/SerialWorker.cs
Normal file
233
Windows/MassiveKnob.Plugin.SerialDevice/Worker/SerialWorker.cs
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO.Ports;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace MassiveKnob.Plugin.SerialDevice.Worker
|
||||||
|
{
|
||||||
|
public class SerialWorker : IDisposable
|
||||||
|
{
|
||||||
|
private readonly IMassiveKnobDeviceContext context;
|
||||||
|
|
||||||
|
private readonly object workerLock = new object();
|
||||||
|
private string lastPortName;
|
||||||
|
private int lastBaudRate;
|
||||||
|
private Thread workerThread;
|
||||||
|
private CancellationTokenSource workerThreadCancellation = new CancellationTokenSource();
|
||||||
|
|
||||||
|
public SerialWorker(IMassiveKnobDeviceContext context)
|
||||||
|
{
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void Connect(string portName, int baudRate)
|
||||||
|
{
|
||||||
|
lock (workerLock)
|
||||||
|
{
|
||||||
|
if (portName == lastPortName && baudRate == lastBaudRate)
|
||||||
|
return;
|
||||||
|
|
||||||
|
lastPortName = portName;
|
||||||
|
lastBaudRate = baudRate;
|
||||||
|
|
||||||
|
Disconnect();
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(portName) || baudRate == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
|
||||||
|
workerThreadCancellation = new CancellationTokenSource();
|
||||||
|
workerThread = new Thread(() => RunWorker(workerThreadCancellation.Token, portName, baudRate))
|
||||||
|
{
|
||||||
|
Name = "MassiveKnobSerialDevice Worker"
|
||||||
|
};
|
||||||
|
workerThread.Start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void Disconnect()
|
||||||
|
{
|
||||||
|
lock (workerLock)
|
||||||
|
{
|
||||||
|
workerThreadCancellation?.Cancel();
|
||||||
|
|
||||||
|
workerThreadCancellation = null;
|
||||||
|
workerThread = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private void RunWorker(CancellationToken cancellationToken, string portName, int baudRate)
|
||||||
|
{
|
||||||
|
context.Connecting();
|
||||||
|
while (!cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
SerialPort serialPort = null;
|
||||||
|
DeviceSpecs specs = default;
|
||||||
|
|
||||||
|
void SafeCloseSerialPort()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
serialPort?.Dispose();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
|
||||||
|
serialPort = null;
|
||||||
|
context.Connecting();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
while (serialPort == null && !cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
if (!TryConnect(ref serialPort, portName, baudRate, out specs))
|
||||||
|
{
|
||||||
|
SafeCloseSerialPort();
|
||||||
|
Thread.Sleep(500);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
SafeCloseSerialPort();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var processingMessage = false;
|
||||||
|
|
||||||
|
Debug.Assert(serialPort != null, nameof(serialPort) + " != null");
|
||||||
|
serialPort.DataReceived += (sender, args) =>
|
||||||
|
{
|
||||||
|
if (args.EventType != SerialData.Chars || processingMessage)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var senderPort = (SerialPort)sender;
|
||||||
|
processingMessage = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var message = (char)senderPort.ReadByte();
|
||||||
|
ProcessMessage(senderPort, message);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
processingMessage = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
context.Connected(specs);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// This is where sending data to the hardware would be implemented
|
||||||
|
while (serialPort.IsOpen && !cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
Thread.Sleep(10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
|
||||||
|
context.Disconnected();
|
||||||
|
SafeCloseSerialPort();
|
||||||
|
|
||||||
|
if (!cancellationToken.IsCancellationRequested)
|
||||||
|
Thread.Sleep(500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static bool TryConnect(ref SerialPort serialPort, string portName, int baudRate, out DeviceSpecs specs)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
serialPort = new SerialPort(portName, baudRate)
|
||||||
|
{
|
||||||
|
Encoding = Encoding.ASCII,
|
||||||
|
ReadTimeout = 1000,
|
||||||
|
WriteTimeout = 1000,
|
||||||
|
DtrEnable = true // TODO make setting
|
||||||
|
};
|
||||||
|
serialPort.Open();
|
||||||
|
|
||||||
|
// Send handshake
|
||||||
|
serialPort.Write(new[] { 'H', 'M', 'K', 'B' }, 0, 4);
|
||||||
|
|
||||||
|
// Wait for reply
|
||||||
|
var response = serialPort.ReadByte();
|
||||||
|
|
||||||
|
if ((char) response == 'H')
|
||||||
|
{
|
||||||
|
var knobCount = serialPort.ReadByte();
|
||||||
|
if (knobCount > -1)
|
||||||
|
{
|
||||||
|
specs = new DeviceSpecs(knobCount, 0, 0, 0);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
CheckForError(serialPort, (char)response);
|
||||||
|
|
||||||
|
specs = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
specs = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void ProcessMessage(SerialPort serialPort, char message)
|
||||||
|
{
|
||||||
|
switch (message)
|
||||||
|
{
|
||||||
|
case 'V':
|
||||||
|
var knobIndex = (byte)serialPort.ReadByte();
|
||||||
|
var volume = (byte)serialPort.ReadByte();
|
||||||
|
|
||||||
|
if (knobIndex < 255 && volume <= 100)
|
||||||
|
context.AnalogChanged(knobIndex, volume);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static void CheckForError(SerialPort serialPort, char message)
|
||||||
|
{
|
||||||
|
if (message != 'E')
|
||||||
|
return;
|
||||||
|
|
||||||
|
var length = serialPort.ReadByte();
|
||||||
|
if (length <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var buffer = new byte[length];
|
||||||
|
var bytesRead = 0;
|
||||||
|
|
||||||
|
while (bytesRead < length)
|
||||||
|
bytesRead += serialPort.Read(buffer, bytesRead, length - bytesRead);
|
||||||
|
|
||||||
|
var errorMessage = Encoding.ASCII.GetString(buffer);
|
||||||
|
Debug.Print(errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -19,12 +19,17 @@ namespace MassiveKnob.Plugin
|
|||||||
InputDigital = 1 << 1,
|
InputDigital = 1 << 1,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Can be assigned to an output, like an LED or relay.
|
/// Can be assigned to an analog output.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
OutputSignal = 1 << 2
|
OutputAnalog = 1 << 2,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Can be assigned to a digital output, like an LED or relay.
|
||||||
|
/// </summary>
|
||||||
|
OutputDigital = 1 << 3
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides information about an action which can be assigned to a knob or button.
|
/// Provides information about an action which can be assigned to a knob or button.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -53,7 +58,6 @@ namespace MassiveKnob.Plugin
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Called when an action is bound to a knob or button to create an instance of the action.
|
/// Called when an action is bound to a knob or button to create an instance of the action.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="context">Provides an interface to the Massive Knob settings and device. Can be stored until the action instance is disposed.</param>
|
IMassiveKnobActionInstance Create();
|
||||||
IMassiveKnobActionInstance Create(IMassiveKnobActionContext context);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,9 +4,15 @@
|
|||||||
public interface IMassiveKnobActionContext : IMassiveKnobContext
|
public interface IMassiveKnobActionContext : IMassiveKnobContext
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets the state of the signal. Only valid for OutputSignal action types, will raise an exception otherwise.
|
/// Sets the state of the analog output. Only valid for OutputAnalog action types, will raise an exception otherwise.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The analog value in the range of 0 to 100.</param>
|
||||||
|
void SetAnalogOutput(byte value);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the state of the digital output. Only valid for OutputDigital action types, will raise an exception otherwise.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="on">Whether the signal is on or off.</param>
|
/// <param name="on">Whether the signal is on or off.</param>
|
||||||
void SetSignal(bool on);
|
void SetDigitalOutput(bool on);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,12 @@ namespace MassiveKnob.Plugin
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IMassiveKnobActionInstance : IDisposable
|
public interface IMassiveKnobActionInstance : IDisposable
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Called right after this instance is created.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">Provides an interface to the Massive Knob settings and device. Can be stored until the action instance is disposed.</param>
|
||||||
|
void Initialize(IMassiveKnobActionContext context);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Called when an action should display it's settings. Assume the width is variable, height is
|
/// Called when an action should display it's settings. Assume the width is variable, height is
|
||||||
/// determined by the UserControl. Return null to indicate there are no settings for this action.
|
/// determined by the UserControl. Return null to indicate there are no settings for this action.
|
||||||
|
@ -8,7 +8,7 @@ namespace MassiveKnob.Plugin
|
|||||||
public interface IMassiveKnobAnalogAction : IMassiveKnobActionInstance
|
public interface IMassiveKnobAnalogAction : IMassiveKnobActionInstance
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Called when a knob's position changes.
|
/// Called when an analog input's value changes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="value">The new value. Range is 0 to 100.</param>
|
/// <param name="value">The new value. Range is 0 to 100.</param>
|
||||||
ValueTask AnalogChanged(byte value);
|
ValueTask AnalogChanged(byte value);
|
||||||
|
@ -25,7 +25,6 @@ namespace MassiveKnob.Plugin
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Called when the device is selected.
|
/// Called when the device is selected.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="context">Provides an interface to the Massive Knob settings and device. Can be stored until the device instance is disposed.</param>
|
IMassiveKnobDeviceInstance Create();
|
||||||
IMassiveKnobDeviceInstance Create(IMassiveKnobContext context);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
80
Windows/MassiveKnob.Plugin/IMassiveKnobDeviceContext.cs
Normal file
80
Windows/MassiveKnob.Plugin/IMassiveKnobDeviceContext.cs
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
namespace MassiveKnob.Plugin
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public interface IMassiveKnobDeviceContext : IMassiveKnobContext
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Call when an attempt is being made to connect to the device. If the connection is always ready
|
||||||
|
/// this call can be skipped.
|
||||||
|
/// </summary>
|
||||||
|
void Connecting();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Call when the device is connected. This method must be called before AnalogChanged or DigitalChanged.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="specs">The specs as reported by the device.</param>
|
||||||
|
void Connected(DeviceSpecs specs);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Call when the connection to the device has been lost.
|
||||||
|
/// </summary>
|
||||||
|
void Disconnected();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Call when an analog input's value has changed.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="analogInputIndex">The index of the analog input. Must be within bounds of the value reported in Connected.</param>
|
||||||
|
/// <param name="value">The new value in the range from 0 to 100.</param>
|
||||||
|
void AnalogChanged(int analogInputIndex, byte value);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Call when a digital input's state has changed.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="digitalInputIndex">The index of the digital input. Must be within bounds of the value reported in Connected.</param>
|
||||||
|
/// <param name="on">Whether the input is considered on or off.</param>
|
||||||
|
void DigitalChanged(int digitalInputIndex, bool on);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Defines the specs as reported by the device.
|
||||||
|
/// </summary>
|
||||||
|
public readonly struct DeviceSpecs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The number of analog input controls supported by the device.
|
||||||
|
/// </summary>
|
||||||
|
public readonly int AnalogInputCount;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The number of digital input controls supported by the device.
|
||||||
|
/// </summary>
|
||||||
|
public readonly int DigitalInputCount;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The number of analog output controls supported by the device.
|
||||||
|
/// </summary>
|
||||||
|
public readonly int AnalogOutputCount;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The number of digital output controls supported by the device.
|
||||||
|
/// </summary>
|
||||||
|
public readonly int DigitalOutputCount;
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Defines the specs as reported by the device.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="analogInputCount">The number of analog input controls supported by the device.</param>
|
||||||
|
/// <param name="digitalInputCount">The number of digital input controls supported by the device.</param>
|
||||||
|
/// <param name="analogOutputCount">The number of analog output controls supported by the device.</param>
|
||||||
|
/// <param name="digitalOutputCount">The number of digital output controls supported by the device.</param>
|
||||||
|
public DeviceSpecs(int analogInputCount, int digitalInputCount, int analogOutputCount, int digitalOutputCount)
|
||||||
|
{
|
||||||
|
AnalogInputCount = analogInputCount;
|
||||||
|
DigitalInputCount = digitalInputCount;
|
||||||
|
AnalogOutputCount = analogOutputCount;
|
||||||
|
DigitalOutputCount = digitalOutputCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -8,6 +8,12 @@ namespace MassiveKnob.Plugin
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IMassiveKnobDeviceInstance : IDisposable
|
public interface IMassiveKnobDeviceInstance : IDisposable
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Called right after this instance is created.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">Provides an interface to the Massive Knob settings and device. Can be stored until the device instance is disposed.</param>
|
||||||
|
void Initialize(IMassiveKnobDeviceContext context);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Called when a device should display it's settings. Assume the width is variable, height is
|
/// Called when a device should display it's settings. Assume the width is variable, height is
|
||||||
/// determined by the UserControl. Return null to indicate there are no settings for this device.
|
/// determined by the UserControl. Return null to indicate there are no settings for this device.
|
||||||
|
@ -1,9 +1,16 @@
|
|||||||
namespace MassiveKnob.Plugin
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace MassiveKnob.Plugin
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Required to be implemented for Action type InputDigital. Receives an update when a knob's position changes.
|
/// Required to be implemented for Action type InputDigital. Receives an update when a knob's position changes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IMassiveKnobDigitalAction : IMassiveKnobActionInstance
|
public interface IMassiveKnobDigitalAction : IMassiveKnobActionInstance
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Called when a digital input's value changes.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="on">The new value.</param>
|
||||||
|
ValueTask DigitalChanged(bool on);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -44,6 +44,7 @@
|
|||||||
<Reference Include="System.Xml" />
|
<Reference Include="System.Xml" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Compile Include="IMassiveKnobDeviceContext.cs" />
|
||||||
<Compile Include="IMassiveKnobActionContext.cs" />
|
<Compile Include="IMassiveKnobActionContext.cs" />
|
||||||
<Compile Include="IMassiveKnobActionInstance.cs" />
|
<Compile Include="IMassiveKnobActionInstance.cs" />
|
||||||
<Compile Include="IMassiveKnobActionPlugin.cs" />
|
<Compile Include="IMassiveKnobActionPlugin.cs" />
|
||||||
@ -52,7 +53,7 @@
|
|||||||
<Compile Include="IMassiveKnobAnalogAction.cs" />
|
<Compile Include="IMassiveKnobAnalogAction.cs" />
|
||||||
<Compile Include="IMassiveKnobContext.cs" />
|
<Compile Include="IMassiveKnobContext.cs" />
|
||||||
<Compile Include="IMassiveKnobDevice.cs" />
|
<Compile Include="IMassiveKnobDevice.cs" />
|
||||||
<Compile Include="IMassiveKnobHardwarePlugin.cs" />
|
<Compile Include="IMassiveKnobDevicePlugin.cs" />
|
||||||
<Compile Include="MassiveKnobPluginAttribute.cs" />
|
<Compile Include="MassiveKnobPluginAttribute.cs" />
|
||||||
<Compile Include="IMassiveKnobPlugin.cs" />
|
<Compile Include="IMassiveKnobPlugin.cs" />
|
||||||
<Compile Include="IMassiveKnobAction.cs" />
|
<Compile Include="IMassiveKnobAction.cs" />
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
// General Information about an assembly is controlled through the following
|
// General Information about an assembly is controlled through the following
|
||||||
|
@ -11,6 +11,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MassiveKnob.Plugin.CoreAudi
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MassiveKnob.Plugin.MockDevice", "MassiveKnob.Plugin.MockDevice\MassiveKnob.Plugin.MockDevice.csproj", "{674DE974-B134-4DB5-BFAF-7BC3D05E16DE}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MassiveKnob.Plugin.MockDevice", "MassiveKnob.Plugin.MockDevice\MassiveKnob.Plugin.MockDevice.csproj", "{674DE974-B134-4DB5-BFAF-7BC3D05E16DE}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MassiveKnob.Plugin.SerialDevice", "MassiveKnob.Plugin.SerialDevice\MassiveKnob.Plugin.SerialDevice.csproj", "{FC0D22D8-5F1B-4D85-8269-FA4837CDE3A2}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@ -33,6 +35,10 @@ Global
|
|||||||
{674DE974-B134-4DB5-BFAF-7BC3D05E16DE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{674DE974-B134-4DB5-BFAF-7BC3D05E16DE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{674DE974-B134-4DB5-BFAF-7BC3D05E16DE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{674DE974-B134-4DB5-BFAF-7BC3D05E16DE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{674DE974-B134-4DB5-BFAF-7BC3D05E16DE}.Release|Any CPU.Build.0 = Release|Any CPU
|
{674DE974-B134-4DB5-BFAF-7BC3D05E16DE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{FC0D22D8-5F1B-4D85-8269-FA4837CDE3A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{FC0D22D8-5F1B-4D85-8269-FA4837CDE3A2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{FC0D22D8-5F1B-4D85-8269-FA4837CDE3A2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{FC0D22D8-5F1B-4D85-8269-FA4837CDE3A2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
@ -1,69 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace MassiveKnob.Hardware
|
|
||||||
{
|
|
||||||
public abstract class AbstractMassiveKnobHardware : IMassiveKnobHardware
|
|
||||||
{
|
|
||||||
protected ObserverProxy Observers = new ObserverProxy();
|
|
||||||
|
|
||||||
public void AttachObserver(IMassiveKnobHardwareObserver observer)
|
|
||||||
{
|
|
||||||
Observers.AttachObserver(observer);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DetachObserver(IMassiveKnobHardwareObserver observer)
|
|
||||||
{
|
|
||||||
Observers.DetachObserver(observer);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public abstract Task TryConnect();
|
|
||||||
public abstract Task Disconnect();
|
|
||||||
|
|
||||||
|
|
||||||
public class ObserverProxy : IMassiveKnobHardwareObserver
|
|
||||||
{
|
|
||||||
private readonly List<IMassiveKnobHardwareObserver> observers = new List<IMassiveKnobHardwareObserver>();
|
|
||||||
|
|
||||||
|
|
||||||
public void AttachObserver(IMassiveKnobHardwareObserver observer)
|
|
||||||
{
|
|
||||||
observers.Add(observer);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DetachObserver(IMassiveKnobHardwareObserver observer)
|
|
||||||
{
|
|
||||||
observers.Remove(observer);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void Connecting()
|
|
||||||
{
|
|
||||||
foreach (var observer in observers)
|
|
||||||
observer.Connecting();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void Connected(int knobCount)
|
|
||||||
{
|
|
||||||
foreach (var observer in observers)
|
|
||||||
observer.Connected(knobCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void Disconnected()
|
|
||||||
{
|
|
||||||
foreach (var observer in observers)
|
|
||||||
observer.Disconnected();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void VolumeChanged(int knob, int volume)
|
|
||||||
{
|
|
||||||
foreach (var observer in observers)
|
|
||||||
observer.VolumeChanged(knob, volume);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,82 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using AudioSwitcher.AudioApi;
|
|
||||||
using AudioSwitcher.AudioApi.CoreAudio;
|
|
||||||
|
|
||||||
namespace MassiveKnob.Hardware
|
|
||||||
{
|
|
||||||
public class CoreAudioDeviceManager : IAudioDeviceManager
|
|
||||||
{
|
|
||||||
private readonly Lazy<CoreAudioController> audioController = new Lazy<CoreAudioController>();
|
|
||||||
private List<IAudioDevice> devices;
|
|
||||||
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
if (audioController.IsValueCreated)
|
|
||||||
audioController.Value.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public async Task<IEnumerable<IAudioDevice>> GetDevices()
|
|
||||||
{
|
|
||||||
return devices ?? (devices = (await audioController.Value.GetPlaybackDevicesAsync())
|
|
||||||
.Select(device => new AudioDevice(device) as IAudioDevice)
|
|
||||||
.ToList());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public Task<IAudioDevice> GetDeviceById(Guid deviceId)
|
|
||||||
{
|
|
||||||
return Task.FromResult(devices?.FirstOrDefault(device => device.Id == deviceId));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private class AudioDevice : IAudioDevice
|
|
||||||
{
|
|
||||||
private readonly IDevice device;
|
|
||||||
|
|
||||||
public Guid Id { get; }
|
|
||||||
public string DisplayName { get; }
|
|
||||||
|
|
||||||
|
|
||||||
public AudioDevice(IDevice device)
|
|
||||||
{
|
|
||||||
this.device = device;
|
|
||||||
Id = device.Id;
|
|
||||||
|
|
||||||
string displayFormat;
|
|
||||||
|
|
||||||
if ((device.State & DeviceState.Disabled) != 0)
|
|
||||||
displayFormat = Strings.DeviceDisplayNameDisabled;
|
|
||||||
else if ((device.State & DeviceState.Unplugged) != 0)
|
|
||||||
displayFormat = Strings.DeviceDisplayNameUnplugged;
|
|
||||||
else if ((device.State & DeviceState.NotPresent) != 0)
|
|
||||||
displayFormat = Strings.DeviceDisplayNameNotPresent;
|
|
||||||
else if ((device.State & DeviceState.Active) == 0)
|
|
||||||
displayFormat = Strings.DeviceDisplayNameInactive;
|
|
||||||
else
|
|
||||||
displayFormat = Strings.DeviceDisplayNameActive;
|
|
||||||
|
|
||||||
DisplayName = string.Format(displayFormat, device.FullName);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public Task SetVolume(int volume)
|
|
||||||
{
|
|
||||||
return device.SetVolumeAsync(volume);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public class CoreAudioDeviceManagerFactory : IAudioDeviceManagerFactory
|
|
||||||
{
|
|
||||||
public IAudioDeviceManager Create()
|
|
||||||
{
|
|
||||||
return new CoreAudioDeviceManager();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace MassiveKnob.Hardware
|
|
||||||
{
|
|
||||||
public interface IAudioDevice
|
|
||||||
{
|
|
||||||
Guid Id { get; }
|
|
||||||
string DisplayName { get; }
|
|
||||||
|
|
||||||
Task SetVolume(int volume);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public interface IAudioDeviceManager : IDisposable
|
|
||||||
{
|
|
||||||
Task<IEnumerable<IAudioDevice>> GetDevices();
|
|
||||||
Task<IAudioDevice> GetDeviceById(Guid deviceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public interface IAudioDeviceManagerFactory
|
|
||||||
{
|
|
||||||
IAudioDeviceManager Create();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace MassiveKnob.Hardware
|
|
||||||
{
|
|
||||||
public interface IMassiveKnobHardwareObserver
|
|
||||||
{
|
|
||||||
void Connecting();
|
|
||||||
void Connected(int knobCount);
|
|
||||||
void Disconnected();
|
|
||||||
|
|
||||||
void VolumeChanged(int knob, int volume);
|
|
||||||
// void ButtonPress(int index); -- for switching the active device, TBD
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public interface IMassiveKnobHardware
|
|
||||||
{
|
|
||||||
void AttachObserver(IMassiveKnobHardwareObserver observer);
|
|
||||||
void DetachObserver(IMassiveKnobHardwareObserver observer);
|
|
||||||
|
|
||||||
Task TryConnect();
|
|
||||||
Task Disconnect();
|
|
||||||
// Task SetActiveKnob(int knob); -- for providing LED feedback when switching the active device, TBD
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public interface IMassiveKnobHardwareFactory
|
|
||||||
{
|
|
||||||
IMassiveKnobHardware Create(string portName);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,71 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace MassiveKnob.Hardware
|
|
||||||
{
|
|
||||||
public class MockMassiveKnobHardware : AbstractMassiveKnobHardware
|
|
||||||
{
|
|
||||||
private readonly int knobCount;
|
|
||||||
private readonly TimeSpan volumeChangeInterval;
|
|
||||||
private readonly int maxVolume;
|
|
||||||
private Timer changeVolumeTimer;
|
|
||||||
private readonly Random random = new Random();
|
|
||||||
|
|
||||||
|
|
||||||
public MockMassiveKnobHardware(int knobCount, TimeSpan volumeChangeInterval, int maxVolume)
|
|
||||||
{
|
|
||||||
this.knobCount = knobCount;
|
|
||||||
this.volumeChangeInterval = volumeChangeInterval;
|
|
||||||
this.maxVolume = maxVolume;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public override async Task TryConnect()
|
|
||||||
{
|
|
||||||
if (changeVolumeTimer != null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
await Task.Delay(2000);
|
|
||||||
|
|
||||||
Observers.Connected(knobCount);
|
|
||||||
changeVolumeTimer = new Timer(
|
|
||||||
state =>
|
|
||||||
{
|
|
||||||
Observers.VolumeChanged(random.Next(0, knobCount), random.Next(0, maxVolume));
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
volumeChangeInterval,
|
|
||||||
volumeChangeInterval);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public override Task Disconnect()
|
|
||||||
{
|
|
||||||
changeVolumeTimer?.Dispose();
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// ReSharper disable once UnusedMember.Global - for testing purposes only
|
|
||||||
public class MockMassiveKnobHardwareFactory : IMassiveKnobHardwareFactory
|
|
||||||
{
|
|
||||||
private readonly int knobCount;
|
|
||||||
private readonly TimeSpan volumeChangeInterval;
|
|
||||||
private readonly int maxVolume;
|
|
||||||
|
|
||||||
public MockMassiveKnobHardwareFactory(int knobCount, TimeSpan volumeChangeInterval, int maxVolume)
|
|
||||||
{
|
|
||||||
this.knobCount = knobCount;
|
|
||||||
this.volumeChangeInterval = volumeChangeInterval;
|
|
||||||
this.maxVolume = maxVolume;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public IMassiveKnobHardware Create(string portName)
|
|
||||||
{
|
|
||||||
return new MockMassiveKnobHardware(knobCount, volumeChangeInterval, maxVolume);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,180 +0,0 @@
|
|||||||
using System.Diagnostics;
|
|
||||||
using System.IO.Ports;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace MassiveKnob.Hardware
|
|
||||||
{
|
|
||||||
public class SerialMassiveKnobHardware : AbstractMassiveKnobHardware
|
|
||||||
{
|
|
||||||
private readonly string portName;
|
|
||||||
private Thread workerThread;
|
|
||||||
|
|
||||||
private readonly CancellationTokenSource workerThreadCancellation = new CancellationTokenSource();
|
|
||||||
private readonly TaskCompletionSource<bool> workerThreadCompleted = new TaskCompletionSource<bool>();
|
|
||||||
|
|
||||||
|
|
||||||
public SerialMassiveKnobHardware(string portName)
|
|
||||||
{
|
|
||||||
this.portName = portName;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public override async Task TryConnect()
|
|
||||||
{
|
|
||||||
if (workerThread != null)
|
|
||||||
await Disconnect();
|
|
||||||
|
|
||||||
workerThread = new Thread(RunWorker)
|
|
||||||
{
|
|
||||||
Name = "SerialMassiveKnobHardware Worker"
|
|
||||||
};
|
|
||||||
workerThread.Start();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public override async Task Disconnect()
|
|
||||||
{
|
|
||||||
workerThreadCancellation.Cancel();
|
|
||||||
await workerThreadCompleted.Task;
|
|
||||||
|
|
||||||
workerThread = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void RunWorker()
|
|
||||||
{
|
|
||||||
Observers.Connecting();
|
|
||||||
|
|
||||||
while (!workerThreadCancellation.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
SerialPort serialPort = null;
|
|
||||||
|
|
||||||
void SafeCloseSerialPort()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
serialPort?.Dispose();
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// ignroed
|
|
||||||
}
|
|
||||||
|
|
||||||
serialPort = null;
|
|
||||||
Observers.Disconnected();
|
|
||||||
Observers.Connecting();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
var knobCount = 0;
|
|
||||||
|
|
||||||
while (serialPort == null && !workerThreadCancellation.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
serialPort = new SerialPort(portName, 115200);
|
|
||||||
serialPort.Open();
|
|
||||||
|
|
||||||
// Send handshake
|
|
||||||
serialPort.Write(new[] { 'H', 'M', 'K', 'B' }, 0, 4);
|
|
||||||
|
|
||||||
// Wait for reply
|
|
||||||
serialPort.ReadTimeout = 1000;
|
|
||||||
var response = serialPort.ReadByte();
|
|
||||||
|
|
||||||
if ((char) response == 'H')
|
|
||||||
{
|
|
||||||
knobCount = serialPort.ReadByte();
|
|
||||||
if (knobCount > -1)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
SafeCloseSerialPort();
|
|
||||||
Thread.Sleep(500);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
SafeCloseSerialPort();
|
|
||||||
Thread.Sleep(500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (workerThreadCancellation.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
SafeCloseSerialPort();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
var processingMessage = false;
|
|
||||||
|
|
||||||
Debug.Assert(serialPort != null, nameof(serialPort) + " != null");
|
|
||||||
serialPort.DataReceived += (sender, args) =>
|
|
||||||
{
|
|
||||||
if (args.EventType != SerialData.Chars || processingMessage)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var senderPort = (SerialPort) sender;
|
|
||||||
processingMessage = true;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var message = (char) senderPort.ReadByte();
|
|
||||||
ProcessMessage(senderPort, message);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
processingMessage = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
Observers.Connected(knobCount);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// This is where sending data to the hardware would be implemented
|
|
||||||
while (serialPort.IsOpen && !workerThreadCancellation.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
Thread.Sleep(10);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// ignored
|
|
||||||
}
|
|
||||||
|
|
||||||
Observers.Disconnected();
|
|
||||||
SafeCloseSerialPort();
|
|
||||||
|
|
||||||
if (!workerThreadCancellation.IsCancellationRequested)
|
|
||||||
Thread.Sleep(500);
|
|
||||||
}
|
|
||||||
|
|
||||||
workerThreadCompleted.TrySetResult(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void ProcessMessage(SerialPort serialPort, char message)
|
|
||||||
{
|
|
||||||
switch (message)
|
|
||||||
{
|
|
||||||
case 'V':
|
|
||||||
var knobIndex = (byte)serialPort.ReadByte();
|
|
||||||
var volume = (byte)serialPort.ReadByte();
|
|
||||||
|
|
||||||
if (knobIndex < 255 && volume <= 100)
|
|
||||||
Observers.VolumeChanged(knobIndex, volume);
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public class SerialMassiveKnobHardwareFactory : IMassiveKnobHardwareFactory
|
|
||||||
{
|
|
||||||
public IMassiveKnobHardware Create(string portName)
|
|
||||||
{
|
|
||||||
return new SerialMassiveKnobHardware(portName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -24,7 +24,7 @@ namespace MassiveKnob.Helpers
|
|||||||
itemToCheck = VisualTreeHelper.GetParent(itemToCheck);
|
itemToCheck = VisualTreeHelper.GetParent(itemToCheck);
|
||||||
|
|
||||||
// If you stopped at a ComboBoxItem, you're in the dropdown
|
// If you stopped at a ComboBoxItem, you're in the dropdown
|
||||||
var inDropDown = (itemToCheck is ComboBoxItem);
|
var inDropDown = itemToCheck is ComboBoxItem;
|
||||||
|
|
||||||
return inDropDown
|
return inDropDown
|
||||||
? DropdownItemsTemplate ?? DropdownItemsTemplateSelector?.SelectTemplate(item, container)
|
? DropdownItemsTemplate ?? DropdownItemsTemplateSelector?.SelectTemplate(item, container)
|
||||||
@ -33,6 +33,7 @@ namespace MassiveKnob.Helpers
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ReSharper disable once UnusedMember.Global - used in XAML
|
||||||
public class ComboBoxTemplateSelectorExtension : MarkupExtension
|
public class ComboBoxTemplateSelectorExtension : MarkupExtension
|
||||||
{
|
{
|
||||||
public DataTemplate SelectedItemTemplate { get; set; }
|
public DataTemplate SelectedItemTemplate { get; set; }
|
||||||
@ -42,7 +43,7 @@ namespace MassiveKnob.Helpers
|
|||||||
|
|
||||||
public override object ProvideValue(IServiceProvider serviceProvider)
|
public override object ProvideValue(IServiceProvider serviceProvider)
|
||||||
{
|
{
|
||||||
return new ComboBoxTemplateSelector()
|
return new ComboBoxTemplateSelector
|
||||||
{
|
{
|
||||||
SelectedItemTemplate = SelectedItemTemplate,
|
SelectedItemTemplate = SelectedItemTemplate,
|
||||||
SelectedItemTemplateSelector = SelectedItemTemplateSelector,
|
SelectedItemTemplateSelector = SelectedItemTemplateSelector,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System;
|
/*
|
||||||
|
using System;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
|
|
||||||
namespace MassiveKnob.Helpers
|
namespace MassiveKnob.Helpers
|
||||||
@ -78,3 +79,4 @@ namespace MassiveKnob.Helpers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
65
Windows/MassiveKnob/Helpers/SerialQueue.cs
Normal file
65
Windows/MassiveKnob/Helpers/SerialQueue.cs
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
// Original source: https://github.com/Gentlee/SerialQueue
|
||||||
|
// ReSharper disable UnusedMember.Global - public API
|
||||||
|
|
||||||
|
namespace MassiveKnob.Helpers
|
||||||
|
{
|
||||||
|
public class SerialQueue
|
||||||
|
{
|
||||||
|
private readonly object locker = new object();
|
||||||
|
private readonly WeakReference<Task> lastTaskWeakRef = new WeakReference<Task>(null);
|
||||||
|
|
||||||
|
public Task Enqueue(Action action)
|
||||||
|
{
|
||||||
|
return Enqueue(() =>
|
||||||
|
{
|
||||||
|
action();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<T> Enqueue<T>(Func<T> function)
|
||||||
|
{
|
||||||
|
lock (locker)
|
||||||
|
{
|
||||||
|
var resultTask = lastTaskWeakRef.TryGetTarget(out var lastTask)
|
||||||
|
? lastTask.ContinueWith(_ => function(), TaskContinuationOptions.ExecuteSynchronously)
|
||||||
|
: Task.Run(function);
|
||||||
|
|
||||||
|
lastTaskWeakRef.SetTarget(resultTask);
|
||||||
|
|
||||||
|
return resultTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Enqueue(Func<Task> asyncAction)
|
||||||
|
{
|
||||||
|
lock (locker)
|
||||||
|
{
|
||||||
|
var resultTask = lastTaskWeakRef.TryGetTarget(out var lastTask)
|
||||||
|
? lastTask.ContinueWith(_ => asyncAction(), TaskContinuationOptions.ExecuteSynchronously).Unwrap()
|
||||||
|
: Task.Run(asyncAction);
|
||||||
|
|
||||||
|
lastTaskWeakRef.SetTarget(resultTask);
|
||||||
|
|
||||||
|
return resultTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<T> Enqueue<T>(Func<Task<T>> asyncFunction)
|
||||||
|
{
|
||||||
|
lock (locker)
|
||||||
|
{
|
||||||
|
var resultTask = lastTaskWeakRef.TryGetTarget(out var lastTask)
|
||||||
|
? lastTask.ContinueWith(_ => asyncFunction(), TaskContinuationOptions.ExecuteSynchronously).Unwrap()
|
||||||
|
: Task.Run(asyncFunction);
|
||||||
|
|
||||||
|
lastTaskWeakRef.SetTarget(resultTask);
|
||||||
|
|
||||||
|
return resultTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -58,19 +58,20 @@
|
|||||||
<Reference Include="WindowsBase" />
|
<Reference Include="WindowsBase" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="Hardware\AbstractMassiveKnobHardware.cs" />
|
|
||||||
<Compile Include="Hardware\CoreAudioDeviceManager.cs" />
|
|
||||||
<Compile Include="Hardware\IAudioDeviceManager.cs" />
|
|
||||||
<Compile Include="Hardware\IMassiveKnobHardware.cs" />
|
|
||||||
<Compile Include="Hardware\MockMassiveKnobHardware.cs" />
|
|
||||||
<Compile Include="Hardware\SerialMassiveKnobHardware.cs" />
|
|
||||||
<Compile Include="Helpers\ComboBoxTemplateSelector.cs" />
|
<Compile Include="Helpers\ComboBoxTemplateSelector.cs" />
|
||||||
<Compile Include="Helpers\DelegateCommand.cs" />
|
<Compile Include="Helpers\DelegateCommand.cs" />
|
||||||
|
<Compile Include="Helpers\SerialQueue.cs" />
|
||||||
<Compile Include="Model\IMassiveKnobOrchestrator.cs" />
|
<Compile Include="Model\IMassiveKnobOrchestrator.cs" />
|
||||||
<Compile Include="Model\IPluginManager.cs" />
|
<Compile Include="Model\IPluginManager.cs" />
|
||||||
<Compile Include="Model\MassiveKnobOrchestrator.cs" />
|
<Compile Include="Model\MassiveKnobOrchestrator.cs" />
|
||||||
<Compile Include="Model\PluginManager.cs" />
|
<Compile Include="Model\PluginManager.cs" />
|
||||||
|
<Compile Include="ViewModel\ActionViewModel.cs" />
|
||||||
|
<Compile Include="ViewModel\DeviceViewModel.cs" />
|
||||||
|
<Compile Include="ViewModel\InputOutputViewModel.cs" />
|
||||||
<Compile Include="ViewModel\SettingsViewModel.cs" />
|
<Compile Include="ViewModel\SettingsViewModel.cs" />
|
||||||
|
<Compile Include="View\InputOutputView.xaml.cs">
|
||||||
|
<DependentUpon>InputOutputView.xaml</DependentUpon>
|
||||||
|
</Compile>
|
||||||
<Compile Include="View\SettingsWindow.xaml.cs">
|
<Compile Include="View\SettingsWindow.xaml.cs">
|
||||||
<DependentUpon>SettingsWindow.xaml</DependentUpon>
|
<DependentUpon>SettingsWindow.xaml</DependentUpon>
|
||||||
</Compile>
|
</Compile>
|
||||||
@ -116,6 +117,9 @@
|
|||||||
<PackageReference Include="SimpleInjector">
|
<PackageReference Include="SimpleInjector">
|
||||||
<Version>5.2.1</Version>
|
<Version>5.2.1</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
<PackageReference Include="System.Reactive">
|
||||||
|
<Version>5.0.0</Version>
|
||||||
|
</PackageReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Content Include="MainIcon.ico" />
|
<Content Include="MainIcon.ico" />
|
||||||
@ -136,6 +140,10 @@
|
|||||||
<SubType>Designer</SubType>
|
<SubType>Designer</SubType>
|
||||||
<Generator>MSBuild:Compile</Generator>
|
<Generator>MSBuild:Compile</Generator>
|
||||||
</Page>
|
</Page>
|
||||||
|
<Page Include="View\InputOutputView.xaml">
|
||||||
|
<SubType>Designer</SubType>
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
</Page>
|
||||||
<Page Include="View\SettingsWindow.xaml">
|
<Page Include="View\SettingsWindow.xaml">
|
||||||
<SubType>Designer</SubType>
|
<SubType>Designer</SubType>
|
||||||
<Generator>MSBuild:Compile</Generator>
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
@ -1,11 +1,44 @@
|
|||||||
using MassiveKnob.Plugin;
|
using System;
|
||||||
|
using MassiveKnob.Plugin;
|
||||||
|
|
||||||
namespace MassiveKnob.Model
|
namespace MassiveKnob.Model
|
||||||
{
|
{
|
||||||
public interface IMassiveKnobOrchestrator
|
public interface IMassiveKnobOrchestrator : IDisposable
|
||||||
{
|
{
|
||||||
IMassiveKnobDeviceInstance ActiveDeviceInstance { get; }
|
MassiveKnobDeviceInfo ActiveDevice { get; }
|
||||||
|
IObservable<MassiveKnobDeviceInfo> ActiveDeviceSubject { get; }
|
||||||
|
|
||||||
IMassiveKnobDeviceInstance SetActiveDevice(IMassiveKnobDevice device);
|
MassiveKnobDeviceInfo SetActiveDevice(IMassiveKnobDevice device);
|
||||||
|
|
||||||
|
MassiveKnobActionInfo GetAction(MassiveKnobActionType actionType, int index);
|
||||||
|
MassiveKnobActionInfo SetAction(MassiveKnobActionType actionType, int index, IMassiveKnobAction action);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public class MassiveKnobDeviceInfo
|
||||||
|
{
|
||||||
|
public IMassiveKnobDevice Info { get; }
|
||||||
|
public IMassiveKnobDeviceInstance Instance { get; }
|
||||||
|
public DeviceSpecs? Specs { get; }
|
||||||
|
|
||||||
|
public MassiveKnobDeviceInfo(IMassiveKnobDevice info, IMassiveKnobDeviceInstance instance, DeviceSpecs? specs)
|
||||||
|
{
|
||||||
|
Info = info;
|
||||||
|
Instance = instance;
|
||||||
|
Specs = specs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public class MassiveKnobActionInfo
|
||||||
|
{
|
||||||
|
public IMassiveKnobAction Info { get; }
|
||||||
|
public IMassiveKnobActionInstance Instance { get; }
|
||||||
|
|
||||||
|
public MassiveKnobActionInfo(IMassiveKnobAction info, IMassiveKnobActionInstance instance)
|
||||||
|
{
|
||||||
|
Info = info;
|
||||||
|
Instance = instance;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,8 +5,7 @@ namespace MassiveKnob.Model
|
|||||||
{
|
{
|
||||||
public interface IPluginManager
|
public interface IPluginManager
|
||||||
{
|
{
|
||||||
IEnumerable<IMassiveKnobPlugin> Plugins { get; }
|
|
||||||
|
|
||||||
IEnumerable<IMassiveKnobDevicePlugin> GetDevicePlugins();
|
IEnumerable<IMassiveKnobDevicePlugin> GetDevicePlugins();
|
||||||
|
IEnumerable<IMassiveKnobActionPlugin> GetActionPlugins();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,51 +1,548 @@
|
|||||||
using MassiveKnob.Plugin;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reactive.Subjects;
|
||||||
|
using MassiveKnob.Helpers;
|
||||||
|
using MassiveKnob.Plugin;
|
||||||
|
using MassiveKnob.Settings;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace MassiveKnob.Model
|
namespace MassiveKnob.Model
|
||||||
{
|
{
|
||||||
public class MassiveKnobOrchestrator : IMassiveKnobOrchestrator
|
public class MassiveKnobOrchestrator : IMassiveKnobOrchestrator
|
||||||
{
|
{
|
||||||
private readonly Settings.Settings settings;
|
private readonly IPluginManager pluginManager;
|
||||||
|
|
||||||
|
private readonly object settingsLock = new object();
|
||||||
|
private Settings.Settings settings;
|
||||||
|
private readonly SerialQueue flushSettingsQueue = new SerialQueue();
|
||||||
|
|
||||||
|
private MassiveKnobDeviceInfo activeDevice;
|
||||||
|
private readonly Subject<MassiveKnobDeviceInfo> activeDeviceInfoSubject = new Subject<MassiveKnobDeviceInfo>();
|
||||||
|
private IMassiveKnobDeviceContext activeDeviceContext;
|
||||||
|
|
||||||
|
private readonly List<ActionMapping> analogInputs = new List<ActionMapping>();
|
||||||
|
private readonly List<ActionMapping> digitalInputs = new List<ActionMapping>();
|
||||||
|
private readonly List<ActionMapping> analogOutputs = new List<ActionMapping>();
|
||||||
|
private readonly List<ActionMapping> digitalOutputs = new List<ActionMapping>();
|
||||||
|
|
||||||
|
|
||||||
public IMassiveKnobDeviceInstance ActiveDeviceInstance { get; private set; }
|
public MassiveKnobDeviceInfo ActiveDevice
|
||||||
|
|
||||||
|
|
||||||
public MassiveKnobOrchestrator(Settings.Settings settings)
|
|
||||||
{
|
{
|
||||||
this.settings = settings;
|
get => activeDevice;
|
||||||
}
|
private set
|
||||||
|
|
||||||
|
|
||||||
public IMassiveKnobDeviceInstance SetActiveDevice(IMassiveKnobDevice device)
|
|
||||||
{
|
|
||||||
ActiveDeviceInstance?.Dispose();
|
|
||||||
ActiveDeviceInstance = device?.Create(new Context(settings));
|
|
||||||
|
|
||||||
return ActiveDeviceInstance;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public class Context : IMassiveKnobContext
|
|
||||||
{
|
|
||||||
private readonly Settings.Settings settings;
|
|
||||||
|
|
||||||
|
|
||||||
public Context(Settings.Settings settings)
|
|
||||||
{
|
{
|
||||||
this.settings = settings;
|
if (value == activeDevice)
|
||||||
|
return;
|
||||||
|
|
||||||
|
activeDevice = value;
|
||||||
|
activeDeviceInfoSubject.OnNext(activeDevice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IObservable<MassiveKnobDeviceInfo> ActiveDeviceSubject => activeDeviceInfoSubject;
|
||||||
|
|
||||||
|
|
||||||
|
public MassiveKnobOrchestrator(IPluginManager pluginManager)
|
||||||
|
{
|
||||||
|
this.pluginManager = pluginManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
activeDevice?.Instance?.Dispose();
|
||||||
|
|
||||||
|
void DisposeMappings(IEnumerable<ActionMapping> mappings)
|
||||||
|
{
|
||||||
|
foreach (var mapping in mappings)
|
||||||
|
mapping?.ActionInfo.Instance?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
DisposeMappings(analogInputs);
|
||||||
|
DisposeMappings(digitalInputs);
|
||||||
|
DisposeMappings(analogOutputs);
|
||||||
|
DisposeMappings(digitalOutputs);
|
||||||
|
|
||||||
|
activeDeviceInfoSubject?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void Load()
|
||||||
|
{
|
||||||
|
lock (settingsLock)
|
||||||
|
{
|
||||||
|
settings = SettingsJsonSerializer.Deserialize();
|
||||||
|
|
||||||
|
if (settings.Device == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var allDevices = pluginManager.GetDevicePlugins().SelectMany(dp => dp.Devices);
|
||||||
|
var device = allDevices.FirstOrDefault(d => d.DeviceId == settings.Device.DeviceId);
|
||||||
|
|
||||||
|
InternalSetActiveDevice(device, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
MassiveKnobDeviceInfo IMassiveKnobOrchestrator.ActiveDevice => activeDevice;
|
||||||
|
|
||||||
|
public MassiveKnobDeviceInfo SetActiveDevice(IMassiveKnobDevice device)
|
||||||
|
{
|
||||||
|
return InternalSetActiveDevice(device, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public MassiveKnobActionInfo GetAction(MassiveKnobActionType actionType, int index)
|
||||||
|
{
|
||||||
|
var list = GetActionMappingList(actionType);
|
||||||
|
return index >= list.Count ? null : list[index].ActionInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public MassiveKnobActionInfo SetAction(MassiveKnobActionType actionType, int index, IMassiveKnobAction action)
|
||||||
|
{
|
||||||
|
var list = GetActionMappingList(actionType);
|
||||||
|
if (index >= list.Count)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (list[index]?.ActionInfo.Info == action)
|
||||||
|
return list[index].ActionInfo;
|
||||||
|
|
||||||
|
list[index]?.ActionInfo.Instance?.Dispose();
|
||||||
|
|
||||||
|
lock (settingsLock)
|
||||||
|
{
|
||||||
|
var settingsList = GetActionSettingsList(actionType);
|
||||||
|
while (index >= settingsList.Count)
|
||||||
|
settingsList.Add(null);
|
||||||
|
|
||||||
|
settingsList[index] = action == null ? null : new Settings.Settings.ActionSettings
|
||||||
|
{
|
||||||
|
ActionId = action.ActionId,
|
||||||
|
Settings = null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
FlushSettings();
|
||||||
|
|
||||||
|
|
||||||
|
Action initializeAfterRegistration = null;
|
||||||
|
var mapping = CreateActionMapping(action, index, (actionInstance, actionContext) =>
|
||||||
|
{
|
||||||
|
initializeAfterRegistration = () => actionInstance.Initialize(actionContext);
|
||||||
|
});
|
||||||
|
|
||||||
|
list[index] = mapping;
|
||||||
|
initializeAfterRegistration?.Invoke();
|
||||||
|
|
||||||
|
return mapping?.ActionInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private MassiveKnobDeviceInfo InternalSetActiveDevice(IMassiveKnobDevice device, bool resetSettings)
|
||||||
|
{
|
||||||
|
if (device == ActiveDevice?.Info)
|
||||||
|
return ActiveDevice;
|
||||||
|
|
||||||
|
|
||||||
|
if (resetSettings)
|
||||||
|
{
|
||||||
|
lock (settingsLock)
|
||||||
|
{
|
||||||
|
if (device == null)
|
||||||
|
settings.Device = null;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
settings.Device = new Settings.Settings.DeviceSettings
|
||||||
|
{
|
||||||
|
DeviceId = device.DeviceId,
|
||||||
|
Settings = null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FlushSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
ActiveDevice?.Instance.Dispose();
|
||||||
|
|
||||||
|
if (device != null)
|
||||||
|
{
|
||||||
|
var instance = device.Create();
|
||||||
|
ActiveDevice = new MassiveKnobDeviceInfo(device, instance, null);
|
||||||
|
|
||||||
|
activeDeviceContext = new DeviceContext(this, device);
|
||||||
|
instance.Initialize(activeDeviceContext);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ActiveDevice = null;
|
||||||
|
activeDeviceContext = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ActiveDevice;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected T GetDeviceSettings<T>(IMassiveKnobDeviceContext context) where T : class, new()
|
||||||
|
{
|
||||||
|
if (context != activeDeviceContext)
|
||||||
|
throw new InvalidOperationException("Caller must be the active device to retrieve the settings");
|
||||||
|
|
||||||
|
lock (settingsLock)
|
||||||
|
{
|
||||||
|
return settings.Device.Settings?.ToObject<T>() ?? new T();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected void SetDeviceSettings<T>(IMassiveKnobDeviceContext context, IMassiveKnobDevice device, T deviceSettings) where T : class, new()
|
||||||
|
{
|
||||||
|
if (context != activeDeviceContext)
|
||||||
|
throw new InvalidOperationException("Caller must be the active device to update the settings");
|
||||||
|
|
||||||
|
lock (settingsLock)
|
||||||
|
{
|
||||||
|
if (settings.Device == null)
|
||||||
|
settings.Device = new Settings.Settings.DeviceSettings
|
||||||
|
{
|
||||||
|
DeviceId = device.DeviceId
|
||||||
|
};
|
||||||
|
|
||||||
|
settings.Device.Settings = JObject.FromObject(deviceSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
FlushSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected T GetActionSettings<T>(IMassiveKnobActionContext context, IMassiveKnobAction action, int index) where T : class, new()
|
||||||
|
{
|
||||||
|
var list = GetActionMappingList(action.ActionType);
|
||||||
|
if (index >= list.Count)
|
||||||
|
return new T();
|
||||||
|
|
||||||
|
if (list[index]?.Context != context)
|
||||||
|
throw new InvalidOperationException("Caller must be the active action to retrieve the settings");
|
||||||
|
|
||||||
|
lock (settingsLock)
|
||||||
|
{
|
||||||
|
var settingsList = GetActionSettingsList(action.ActionType);
|
||||||
|
if (index >= settingsList.Count)
|
||||||
|
return new T();
|
||||||
|
|
||||||
|
return settingsList[index].Settings?.ToObject<T>() ?? new T();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected void SetActionSettings<T>(IMassiveKnobActionContext context, IMassiveKnobAction action, int index, T actionSettings) where T : class, new()
|
||||||
|
{
|
||||||
|
var list = GetActionMappingList(action.ActionType);
|
||||||
|
if (index >= list.Count)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (list[index].Context != context)
|
||||||
|
throw new InvalidOperationException("Caller must be the active action to retrieve the settings");
|
||||||
|
|
||||||
|
lock (settingsLock)
|
||||||
|
{
|
||||||
|
var settingsList = GetActionSettingsList(action.ActionType);
|
||||||
|
|
||||||
|
while (index >= settingsList.Count)
|
||||||
|
settingsList.Add(null);
|
||||||
|
|
||||||
|
if (settingsList[index] == null)
|
||||||
|
settingsList[index] = new Settings.Settings.ActionSettings
|
||||||
|
{
|
||||||
|
ActionId = action.ActionId
|
||||||
|
};
|
||||||
|
|
||||||
|
settingsList[index].Settings = JObject.FromObject(actionSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
FlushSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected void AnalogChanged(IMassiveKnobDeviceContext context, int analogInputIndex, byte value)
|
||||||
|
{
|
||||||
|
if (context != activeDeviceContext)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var mapping = GetActionMappingList(MassiveKnobActionType.InputAnalog);
|
||||||
|
if (mapping == null || analogInputIndex >= mapping.Count)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (mapping[analogInputIndex].ActionInfo.Instance is IMassiveKnobAnalogAction analogAction)
|
||||||
|
analogAction.AnalogChanged(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected void DigitalChanged(IMassiveKnobDeviceContext context, int digitalInputIndex, bool on)
|
||||||
|
{
|
||||||
|
if (context != activeDeviceContext)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var mapping = GetActionMappingList(MassiveKnobActionType.InputAnalog);
|
||||||
|
if (mapping == null || digitalInputIndex >= mapping.Count)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (mapping[digitalInputIndex].ActionInfo.Instance is IMassiveKnobDigitalAction digitalAction)
|
||||||
|
digitalAction.DigitalChanged(on);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private List<ActionMapping> GetActionMappingList(MassiveKnobActionType actionType)
|
||||||
|
{
|
||||||
|
switch (actionType)
|
||||||
|
{
|
||||||
|
case MassiveKnobActionType.InputAnalog:
|
||||||
|
return analogInputs;
|
||||||
|
|
||||||
|
case MassiveKnobActionType.InputDigital:
|
||||||
|
return digitalInputs;
|
||||||
|
|
||||||
|
case MassiveKnobActionType.OutputAnalog:
|
||||||
|
return analogOutputs;
|
||||||
|
|
||||||
|
case MassiveKnobActionType.OutputDigital:
|
||||||
|
return digitalOutputs;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(actionType), actionType, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private List<Settings.Settings.ActionSettings> GetActionSettingsList(MassiveKnobActionType actionType)
|
||||||
|
{
|
||||||
|
switch (actionType)
|
||||||
|
{
|
||||||
|
case MassiveKnobActionType.InputAnalog:
|
||||||
|
return settings.AnalogInput;
|
||||||
|
|
||||||
|
case MassiveKnobActionType.InputDigital:
|
||||||
|
return settings.DigitalInput;
|
||||||
|
|
||||||
|
case MassiveKnobActionType.OutputAnalog:
|
||||||
|
return settings.AnalogOutput;
|
||||||
|
|
||||||
|
case MassiveKnobActionType.OutputDigital:
|
||||||
|
return settings.DigitalOutput;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(actionType), actionType, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FlushSettings()
|
||||||
|
{
|
||||||
|
Settings.Settings settingsSnapshot;
|
||||||
|
|
||||||
|
lock (settingsLock)
|
||||||
|
{
|
||||||
|
settingsSnapshot = settings.Clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
flushSettingsQueue.Enqueue(async () =>
|
||||||
|
{
|
||||||
|
await SettingsJsonSerializer.Serialize(settingsSnapshot);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected void UpdateActiveDeviceSpecs(IMassiveKnobDeviceContext context, DeviceSpecs specs)
|
||||||
|
{
|
||||||
|
if (context != activeDeviceContext)
|
||||||
|
return;
|
||||||
|
|
||||||
|
|
||||||
|
var delayedInitializeActions = new List<Action>();
|
||||||
|
void DelayedInitialize(IMassiveKnobActionInstance instance, IMassiveKnobActionContext instanceContext)
|
||||||
|
{
|
||||||
|
delayedInitializeActions.Add(() =>
|
||||||
|
{
|
||||||
|
instance.Initialize(instanceContext);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var delayedInitializeAction in delayedInitializeActions)
|
||||||
|
delayedInitializeAction();
|
||||||
|
|
||||||
|
|
||||||
|
ActiveDevice = new MassiveKnobDeviceInfo(
|
||||||
|
ActiveDevice.Info,
|
||||||
|
ActiveDevice.Instance,
|
||||||
|
specs);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void UpdateMapping(List<ActionMapping> mapping, int newCount, List<Settings.Settings.ActionSettings> actionSettings, Action<IMassiveKnobActionInstance, IMassiveKnobActionContext> initializeOutsideLock)
|
||||||
|
{
|
||||||
|
if (mapping.Count > newCount)
|
||||||
|
{
|
||||||
|
for (var actionIndex = newCount; actionIndex < mapping.Count; actionIndex++)
|
||||||
|
mapping[actionIndex]?.ActionInfo.Instance?.Dispose();
|
||||||
|
|
||||||
|
mapping.RemoveRange(newCount, mapping.Count - newCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actionSettings.Count > newCount)
|
||||||
|
actionSettings.RemoveRange(newCount, actionSettings.Count - newCount);
|
||||||
|
|
||||||
|
|
||||||
|
if (mapping.Count >= newCount) return;
|
||||||
|
{
|
||||||
|
var allActions = pluginManager.GetActionPlugins().SelectMany(ap => ap.Actions).ToArray();
|
||||||
|
|
||||||
|
for (var actionIndex = mapping.Count; actionIndex < newCount; actionIndex++)
|
||||||
|
{
|
||||||
|
if (actionIndex < actionSettings.Count && actionSettings[actionIndex] != null)
|
||||||
|
{
|
||||||
|
var action = allActions.FirstOrDefault(d => d.ActionId == actionSettings[actionIndex].ActionId);
|
||||||
|
mapping.Add(CreateActionMapping(action, actionIndex, initializeOutsideLock));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
mapping.Add(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private ActionMapping CreateActionMapping(IMassiveKnobAction action, int index, Action<IMassiveKnobActionInstance, IMassiveKnobActionContext> initialize)
|
||||||
|
{
|
||||||
|
if (action == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var instance = action.Create();
|
||||||
|
var context = new ActionContext(this, action, index);
|
||||||
|
|
||||||
|
var mapping = new ActionMapping(new MassiveKnobActionInfo(action, instance), context);
|
||||||
|
initialize(instance, context);
|
||||||
|
|
||||||
|
return mapping;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private class ActionMapping
|
||||||
|
{
|
||||||
|
public MassiveKnobActionInfo ActionInfo { get; }
|
||||||
|
public IMassiveKnobActionContext Context { get; }
|
||||||
|
|
||||||
|
|
||||||
|
public ActionMapping(MassiveKnobActionInfo actionInfo, IMassiveKnobActionContext context)
|
||||||
|
{
|
||||||
|
ActionInfo = actionInfo;
|
||||||
|
Context = context;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private class DeviceContext : IMassiveKnobDeviceContext
|
||||||
|
{
|
||||||
|
private readonly MassiveKnobOrchestrator owner;
|
||||||
|
private readonly IMassiveKnobDevice device;
|
||||||
|
|
||||||
|
|
||||||
|
public DeviceContext(MassiveKnobOrchestrator owner, IMassiveKnobDevice device)
|
||||||
|
{
|
||||||
|
this.owner = owner;
|
||||||
|
this.device = device;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public T GetSettings<T>() where T : class, new()
|
public T GetSettings<T>() where T : class, new()
|
||||||
{
|
{
|
||||||
// TODO
|
return owner.GetDeviceSettings<T>(this);
|
||||||
return default;
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void SetSettings<T>(T settings) where T : class, new()
|
||||||
|
{
|
||||||
|
owner.SetDeviceSettings(this, device, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void Connecting()
|
||||||
|
{
|
||||||
|
// TODO update status ?
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void Connected(DeviceSpecs specs)
|
||||||
|
{
|
||||||
|
// TODO update status ?
|
||||||
|
|
||||||
|
owner.UpdateActiveDeviceSpecs(this, specs);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void Disconnected()
|
||||||
|
{
|
||||||
|
// TODO update status ?
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void AnalogChanged(int analogInputIndex, byte value)
|
||||||
|
{
|
||||||
|
owner.AnalogChanged(this, analogInputIndex, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void DigitalChanged(int digitalInputIndex, bool on)
|
||||||
|
{
|
||||||
|
owner.DigitalChanged(this, digitalInputIndex, on);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private class ActionContext : IMassiveKnobActionContext
|
||||||
|
{
|
||||||
|
private readonly MassiveKnobOrchestrator owner;
|
||||||
|
private readonly IMassiveKnobAction action;
|
||||||
|
private readonly int index;
|
||||||
|
|
||||||
|
|
||||||
|
public ActionContext(MassiveKnobOrchestrator owner, IMassiveKnobAction action, int index)
|
||||||
|
{
|
||||||
|
this.owner = owner;
|
||||||
|
this.action = action;
|
||||||
|
this.index = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public T GetSettings<T>() where T : class, new()
|
||||||
|
{
|
||||||
|
return owner.GetActionSettings<T>(this, action, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void SetSettings<T>(T settings) where T : class, new()
|
||||||
|
{
|
||||||
|
owner.SetActionSettings(this, action, index, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void SetDigitalOutput(bool on)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void SetSettings<T>(T settings) where T : class, new()
|
public void SetAnalogOutput(byte value)
|
||||||
{
|
{
|
||||||
// TODO
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,21 +7,46 @@ using MassiveKnob.Plugin;
|
|||||||
|
|
||||||
namespace MassiveKnob.Model
|
namespace MassiveKnob.Model
|
||||||
{
|
{
|
||||||
|
public class MassiveKnobPluginIdConflictException : Exception
|
||||||
|
{
|
||||||
|
public Guid ConflictingId { get; }
|
||||||
|
public string FirstAssemblyFilename { get; }
|
||||||
|
public string ConflictingAssemblyFilename { get; }
|
||||||
|
|
||||||
|
|
||||||
|
public MassiveKnobPluginIdConflictException(
|
||||||
|
Guid conflictingId,
|
||||||
|
string firstAssemblyFilename,
|
||||||
|
string conflictingAssemblyFilename)
|
||||||
|
: base($"Conflicting ID {conflictingId} was already registered by {firstAssemblyFilename}.")
|
||||||
|
{
|
||||||
|
ConflictingId = conflictingId;
|
||||||
|
FirstAssemblyFilename = firstAssemblyFilename;
|
||||||
|
ConflictingAssemblyFilename = conflictingAssemblyFilename;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public class PluginManager : IPluginManager
|
public class PluginManager : IPluginManager
|
||||||
{
|
{
|
||||||
private readonly List<IMassiveKnobPlugin> plugins = new List<IMassiveKnobPlugin>();
|
private readonly List<IMassiveKnobPlugin> plugins = new List<IMassiveKnobPlugin>();
|
||||||
|
|
||||||
|
|
||||||
public IEnumerable<IMassiveKnobPlugin> Plugins => plugins;
|
|
||||||
|
|
||||||
public IEnumerable<IMassiveKnobDevicePlugin> GetDevicePlugins()
|
public IEnumerable<IMassiveKnobDevicePlugin> GetDevicePlugins()
|
||||||
{
|
{
|
||||||
return plugins.Where(p => p is IMassiveKnobDevicePlugin).Cast<IMassiveKnobDevicePlugin>();
|
return plugins.Where(p => p is IMassiveKnobDevicePlugin).Cast<IMassiveKnobDevicePlugin>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IEnumerable<IMassiveKnobActionPlugin> GetActionPlugins()
|
||||||
public void Load()
|
|
||||||
{
|
{
|
||||||
|
return plugins.Where(p => p is IMassiveKnobActionPlugin).Cast<IMassiveKnobActionPlugin>();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void Load(Action<Exception, string> onException)
|
||||||
|
{
|
||||||
|
var registeredIds = new RegisteredIds();
|
||||||
|
|
||||||
var codeBase = Assembly.GetEntryAssembly()?.CodeBase;
|
var codeBase = Assembly.GetEntryAssembly()?.CodeBase;
|
||||||
if (!string.IsNullOrEmpty(codeBase))
|
if (!string.IsNullOrEmpty(codeBase))
|
||||||
{
|
{
|
||||||
@ -29,17 +54,17 @@ namespace MassiveKnob.Model
|
|||||||
if (!string.IsNullOrEmpty(localPath))
|
if (!string.IsNullOrEmpty(localPath))
|
||||||
{
|
{
|
||||||
var applicationPluginPath = Path.Combine(localPath, @"Plugins");
|
var applicationPluginPath = Path.Combine(localPath, @"Plugins");
|
||||||
LoadPlugins(applicationPluginPath);
|
LoadPlugins(applicationPluginPath, registeredIds, onException);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var localPluginPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"MassiveKnob", @"Plugins");
|
var localPluginPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"MassiveKnob", @"Plugins");
|
||||||
LoadPlugins(localPluginPath);
|
LoadPlugins(localPluginPath, registeredIds, onException);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void LoadPlugins(string path)
|
private void LoadPlugins(string path, RegisteredIds registeredIds, Action<Exception, string> onException)
|
||||||
{
|
{
|
||||||
if (!Directory.Exists(path))
|
if (!Directory.Exists(path))
|
||||||
return;
|
return;
|
||||||
@ -51,19 +76,17 @@ namespace MassiveKnob.Model
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var pluginAssembly = Assembly.LoadFrom(filename);
|
var pluginAssembly = Assembly.LoadFrom(filename);
|
||||||
RegisterPlugins(pluginAssembly);
|
RegisterPlugins(filename, pluginAssembly, registeredIds);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
// TODO report error
|
onException(e, filename);
|
||||||
// Console.WriteLine(e);
|
|
||||||
throw;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void RegisterPlugins(Assembly assembly)
|
private void RegisterPlugins(string filename, Assembly assembly, RegisteredIds registeredIds)
|
||||||
{
|
{
|
||||||
var pluginTypes = assembly.GetTypes().Where(t => t.GetCustomAttribute<MassiveKnobPluginAttribute>() != null);
|
var pluginTypes = assembly.GetTypes().Where(t => t.GetCustomAttribute<MassiveKnobPluginAttribute>() != null);
|
||||||
foreach (var pluginType in pluginTypes)
|
foreach (var pluginType in pluginTypes)
|
||||||
@ -71,9 +94,57 @@ namespace MassiveKnob.Model
|
|||||||
var pluginInstance = Activator.CreateInstance(pluginType);
|
var pluginInstance = Activator.CreateInstance(pluginType);
|
||||||
if (!(pluginInstance is IMassiveKnobPlugin))
|
if (!(pluginInstance is IMassiveKnobPlugin))
|
||||||
throw new InvalidCastException($"Type {pluginType.FullName} claims to be a MassiveKnobPlugin but does not implement IMassiveKnobPlugin");
|
throw new InvalidCastException($"Type {pluginType.FullName} claims to be a MassiveKnobPlugin but does not implement IMassiveKnobPlugin");
|
||||||
|
|
||||||
|
ValidateRegistration(filename, (IMassiveKnobPlugin)pluginInstance, registeredIds);
|
||||||
plugins.Add((IMassiveKnobPlugin)pluginInstance);
|
plugins.Add((IMassiveKnobPlugin)pluginInstance);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static void ValidateRegistration(string filename, IMassiveKnobPlugin plugin, RegisteredIds registeredIds)
|
||||||
|
{
|
||||||
|
// Make sure all GUIDs are actually unique and someone has not copy/pasted a plugin without
|
||||||
|
// modifying the values. This way we can safely make that assumption in other code.
|
||||||
|
if (registeredIds.PluginById.TryGetValue(plugin.PluginId, out var conflictingPluginFilename))
|
||||||
|
throw new MassiveKnobPluginIdConflictException(plugin.PluginId, conflictingPluginFilename, filename);
|
||||||
|
|
||||||
|
registeredIds.PluginById.Add(plugin.PluginId, filename);
|
||||||
|
|
||||||
|
|
||||||
|
// ReSharper disable once ConvertIfStatementToSwitchStatement - no, a plugin can implement both interfaces
|
||||||
|
if (plugin is IMassiveKnobDevicePlugin devicePlugin)
|
||||||
|
{
|
||||||
|
foreach (var device in devicePlugin.Devices)
|
||||||
|
{
|
||||||
|
if (registeredIds.DeviceById.TryGetValue(device.DeviceId, out var conflictingDeviceFilename))
|
||||||
|
throw new MassiveKnobPluginIdConflictException(device.DeviceId, conflictingDeviceFilename, filename);
|
||||||
|
|
||||||
|
registeredIds.DeviceById.Add(device.DeviceId, filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ReSharper disable once InvertIf
|
||||||
|
if (plugin is IMassiveKnobActionPlugin actionPlugin)
|
||||||
|
{
|
||||||
|
foreach (var action in actionPlugin.Actions)
|
||||||
|
{
|
||||||
|
if (registeredIds.ActionById.TryGetValue(action.ActionId, out var conflictingActionFilename))
|
||||||
|
throw new MassiveKnobPluginIdConflictException(action.ActionId, conflictingActionFilename, filename);
|
||||||
|
|
||||||
|
registeredIds.ActionById.Add(action.ActionId, filename);
|
||||||
|
|
||||||
|
// TODO check ActionType vs. implemented interfaces
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private class RegisteredIds
|
||||||
|
{
|
||||||
|
public readonly Dictionary<Guid, string> PluginById = new Dictionary<Guid, string>();
|
||||||
|
public readonly Dictionary<Guid, string> DeviceById = new Dictionary<Guid, string>();
|
||||||
|
public readonly Dictionary<Guid, string> ActionById = new Dictionary<Guid, string>();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Text;
|
||||||
|
using System.Windows;
|
||||||
using MassiveKnob.Model;
|
using MassiveKnob.Model;
|
||||||
using MassiveKnob.Settings;
|
|
||||||
using MassiveKnob.View;
|
using MassiveKnob.View;
|
||||||
using MassiveKnob.ViewModel;
|
using MassiveKnob.ViewModel;
|
||||||
using SimpleInjector;
|
using SimpleInjector;
|
||||||
@ -14,33 +14,42 @@ namespace MassiveKnob
|
|||||||
/// The main entry point for the application.
|
/// The main entry point for the application.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[STAThread]
|
[STAThread]
|
||||||
public static void Main()
|
public static int Main()
|
||||||
{
|
{
|
||||||
MainAsync().GetAwaiter().GetResult();
|
var pluginManager = new PluginManager();
|
||||||
}
|
|
||||||
|
var messages = new StringBuilder();
|
||||||
|
pluginManager.Load((exception, filename) =>
|
||||||
|
{
|
||||||
|
messages.AppendLine($"{filename}: {exception.Message}");
|
||||||
|
});
|
||||||
|
|
||||||
|
if (messages.Length > 0)
|
||||||
|
{
|
||||||
|
MessageBox.Show($"Error while loading plugins:\r\n\r\n{messages}", "Massive Knob", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var orchestrator = new MassiveKnobOrchestrator(pluginManager);
|
||||||
|
orchestrator.Load();
|
||||||
|
|
||||||
|
|
||||||
private static async Task MainAsync()
|
|
||||||
{
|
|
||||||
var container = new Container();
|
var container = new Container();
|
||||||
container.Options.EnableAutoVerification = false;
|
container.Options.EnableAutoVerification = false;
|
||||||
|
|
||||||
container.RegisterSingleton<IMassiveKnobOrchestrator, MassiveKnobOrchestrator>();
|
container.RegisterInstance<IPluginManager>(pluginManager);
|
||||||
|
container.RegisterInstance<IMassiveKnobOrchestrator>(orchestrator);
|
||||||
|
|
||||||
container.Register<App>();
|
container.Register<App>();
|
||||||
container.Register<SettingsWindow>();
|
container.Register<SettingsWindow>();
|
||||||
container.Register<SettingsViewModel>();
|
container.Register<SettingsViewModel>();
|
||||||
|
|
||||||
var settings = await SettingsJsonSerializer.Deserialize();
|
|
||||||
container.RegisterInstance(settings);
|
|
||||||
|
|
||||||
var pluginManager = new PluginManager();
|
|
||||||
pluginManager.Load();
|
|
||||||
container.RegisterInstance<IPluginManager>(pluginManager);
|
|
||||||
|
|
||||||
|
|
||||||
var app = container.GetInstance<App>();
|
var app = container.GetInstance<App>();
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|
||||||
|
orchestrator.Dispose();
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace MassiveKnob.Settings
|
namespace MassiveKnob.Settings
|
||||||
@ -7,32 +8,67 @@ namespace MassiveKnob.Settings
|
|||||||
public class Settings
|
public class Settings
|
||||||
{
|
{
|
||||||
public DeviceSettings Device { get; set; }
|
public DeviceSettings Device { get; set; }
|
||||||
public List<ActionSettings> Actions { get; set; }
|
public List<ActionSettings> AnalogInput { get; set; }
|
||||||
|
public List<ActionSettings> DigitalInput { get; set; }
|
||||||
|
public List<ActionSettings> AnalogOutput { get; set; }
|
||||||
|
public List<ActionSettings> DigitalOutput { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
public void Verify()
|
||||||
|
{
|
||||||
|
if (AnalogInput == null) AnalogInput = new List<ActionSettings>();
|
||||||
|
if (DigitalInput == null) DigitalInput = new List<ActionSettings>();
|
||||||
|
if (AnalogOutput == null) AnalogOutput = new List<ActionSettings>();
|
||||||
|
if (DigitalOutput == null) DigitalOutput = new List<ActionSettings>();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public static Settings Default()
|
public Settings Clone()
|
||||||
{
|
{
|
||||||
return new Settings
|
return new Settings
|
||||||
{
|
{
|
||||||
Device = null,
|
Device = Device?.Clone(),
|
||||||
Actions = new List<ActionSettings>()
|
AnalogInput = AnalogInput.Select(a => a?.Clone()).ToList(),
|
||||||
|
DigitalInput = DigitalInput.Select(a => a?.Clone()).ToList(),
|
||||||
|
AnalogOutput = AnalogOutput.Select(a => a?.Clone()).ToList(),
|
||||||
|
DigitalOutput = DigitalOutput.Select(a => a?.Clone()).ToList()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public class DeviceSettings
|
public class DeviceSettings
|
||||||
{
|
{
|
||||||
public Guid? PluginId { get; set; }
|
|
||||||
public Guid? DeviceId { get; set; }
|
public Guid? DeviceId { get; set; }
|
||||||
public JObject Settings { get; set; }
|
public JObject Settings { get; set; }
|
||||||
|
|
||||||
|
public DeviceSettings Clone()
|
||||||
|
{
|
||||||
|
return new DeviceSettings
|
||||||
|
{
|
||||||
|
DeviceId = DeviceId,
|
||||||
|
|
||||||
|
// This is safe, as the JObject itself is never manipulated, only replaced
|
||||||
|
Settings = Settings
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public class ActionSettings
|
public class ActionSettings
|
||||||
{
|
{
|
||||||
public Guid PluginId { get; set; }
|
|
||||||
public Guid ActionId { get; set; }
|
public Guid ActionId { get; set; }
|
||||||
public JObject Settings { get; set; }
|
public JObject Settings { get; set; }
|
||||||
|
|
||||||
|
public ActionSettings Clone()
|
||||||
|
{
|
||||||
|
return new ActionSettings
|
||||||
|
{
|
||||||
|
ActionId = ActionId,
|
||||||
|
|
||||||
|
// This is safe, as the JObject itself is never manipulated, only replaced
|
||||||
|
Settings = Settings
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,25 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Converters;
|
||||||
|
|
||||||
namespace MassiveKnob.Settings
|
namespace MassiveKnob.Settings
|
||||||
{
|
{
|
||||||
public static class SettingsJsonSerializer
|
public static class SettingsJsonSerializer
|
||||||
{
|
{
|
||||||
|
private static readonly JsonSerializerSettings DefaultSettings = new JsonSerializerSettings
|
||||||
|
{
|
||||||
|
Formatting = Formatting.Indented,
|
||||||
|
Converters = new List<JsonConverter>
|
||||||
|
{
|
||||||
|
new StringEnumConverter()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
public static string GetDefaultFilename()
|
public static string GetDefaultFilename()
|
||||||
{
|
{
|
||||||
var path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"MassiveKnob");
|
var path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"MassiveKnob");
|
||||||
@ -24,7 +36,7 @@ namespace MassiveKnob.Settings
|
|||||||
|
|
||||||
public static async Task Serialize(Settings settings, string filename)
|
public static async Task Serialize(Settings settings, string filename)
|
||||||
{
|
{
|
||||||
var json = JsonConvert.SerializeObject(settings);
|
var json = JsonConvert.SerializeObject(settings, DefaultSettings);
|
||||||
|
|
||||||
using (var stream = new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.Read, 4096, true))
|
using (var stream = new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.Read, 4096, true))
|
||||||
using (var streamWriter = new StreamWriter(stream, Encoding.UTF8))
|
using (var streamWriter = new StreamWriter(stream, Encoding.UTF8))
|
||||||
@ -35,28 +47,34 @@ namespace MassiveKnob.Settings
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static Task<Settings> Deserialize()
|
public static Settings Deserialize()
|
||||||
{
|
{
|
||||||
return Deserialize(GetDefaultFilename());
|
return Deserialize(GetDefaultFilename());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<Settings> Deserialize(string filename)
|
public static Settings Deserialize(string filename)
|
||||||
{
|
{
|
||||||
if (!File.Exists(filename))
|
Settings settings = null;
|
||||||
return Settings.Default();
|
|
||||||
|
|
||||||
string json;
|
if (File.Exists(filename))
|
||||||
|
|
||||||
using (var stream = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 4096, true))
|
|
||||||
using (var streamReader = new StreamReader(stream, Encoding.UTF8))
|
|
||||||
{
|
{
|
||||||
json = await streamReader.ReadToEndAsync();
|
string json;
|
||||||
|
|
||||||
|
using (var stream = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 4096, true))
|
||||||
|
using (var streamReader = new StreamReader(stream, Encoding.UTF8))
|
||||||
|
{
|
||||||
|
json = streamReader.ReadToEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(json))
|
||||||
|
settings = JsonConvert.DeserializeObject<Settings>(json, DefaultSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (settings == null)
|
||||||
|
settings = new Settings();
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(json))
|
settings.Verify();
|
||||||
return Settings.Default();
|
return settings;
|
||||||
|
|
||||||
return JsonConvert.DeserializeObject<Settings>(json);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
78
Windows/MassiveKnob/Strings.Designer.cs
generated
78
Windows/MassiveKnob/Strings.Designer.cs
generated
@ -61,83 +61,11 @@ namespace MassiveKnob {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to {0}.
|
/// Looks up a localized string similar to Not configured.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static string DeviceDisplayNameActive {
|
internal static string ActionNotConfigured {
|
||||||
get {
|
get {
|
||||||
return ResourceManager.GetString("DeviceDisplayNameActive", resourceCulture);
|
return ResourceManager.GetString("ActionNotConfigured", resourceCulture);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Looks up a localized string similar to {0} (Disabled).
|
|
||||||
/// </summary>
|
|
||||||
internal static string DeviceDisplayNameDisabled {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("DeviceDisplayNameDisabled", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Looks up a localized string similar to {0} (Inactive).
|
|
||||||
/// </summary>
|
|
||||||
internal static string DeviceDisplayNameInactive {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("DeviceDisplayNameInactive", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Looks up a localized string similar to {0} (Not present).
|
|
||||||
/// </summary>
|
|
||||||
internal static string DeviceDisplayNameNotPresent {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("DeviceDisplayNameNotPresent", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Looks up a localized string similar to {0} (Unplugged).
|
|
||||||
/// </summary>
|
|
||||||
internal static string DeviceDisplayNameUnplugged {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("DeviceDisplayNameUnplugged", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Looks up a localized string similar to Knob {0}.
|
|
||||||
/// </summary>
|
|
||||||
internal static string KnobIndex {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("KnobIndex", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Looks up a localized string similar to Connected.
|
|
||||||
/// </summary>
|
|
||||||
internal static string StatusConnected {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("StatusConnected", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Looks up a localized string similar to Connecting....
|
|
||||||
/// </summary>
|
|
||||||
internal static string StatusConnecting {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("StatusConnecting", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Looks up a localized string similar to Not connected.
|
|
||||||
/// </summary>
|
|
||||||
internal static string StatusNotConnected {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("StatusNotConnected", resourceCulture);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -117,31 +117,7 @@
|
|||||||
<resheader name="writer">
|
<resheader name="writer">
|
||||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
</resheader>
|
</resheader>
|
||||||
<data name="StatusConnected" xml:space="preserve">
|
<data name="ActionNotConfigured" xml:space="preserve">
|
||||||
<value>Connected</value>
|
<value>Not configured</value>
|
||||||
</data>
|
|
||||||
<data name="StatusConnecting" xml:space="preserve">
|
|
||||||
<value>Connecting...</value>
|
|
||||||
</data>
|
|
||||||
<data name="StatusNotConnected" xml:space="preserve">
|
|
||||||
<value>Not connected</value>
|
|
||||||
</data>
|
|
||||||
<data name="KnobIndex" xml:space="preserve">
|
|
||||||
<value>Knob {0}</value>
|
|
||||||
</data>
|
|
||||||
<data name="DeviceDisplayNameActive" xml:space="preserve">
|
|
||||||
<value>{0}</value>
|
|
||||||
</data>
|
|
||||||
<data name="DeviceDisplayNameDisabled" xml:space="preserve">
|
|
||||||
<value>{0} (Disabled)</value>
|
|
||||||
</data>
|
|
||||||
<data name="DeviceDisplayNameInactive" xml:space="preserve">
|
|
||||||
<value>{0} (Inactive)</value>
|
|
||||||
</data>
|
|
||||||
<data name="DeviceDisplayNameNotPresent" xml:space="preserve">
|
|
||||||
<value>{0} (Not present)</value>
|
|
||||||
</data>
|
|
||||||
<data name="DeviceDisplayNameUnplugged" xml:space="preserve">
|
|
||||||
<value>{0} (Unplugged)</value>
|
|
||||||
</data>
|
</data>
|
||||||
</root>
|
</root>
|
@ -1,5 +1,9 @@
|
|||||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
<Style TargetType="Window" x:Key="DefaultWindow">
|
||||||
|
<Setter Property="Background" Value="#f0f0f0" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
<Style TargetType="MenuItem" x:Key="DefaultMenuItem">
|
<Style TargetType="MenuItem" x:Key="DefaultMenuItem">
|
||||||
<Setter Property="FontWeight" Value="Bold" />
|
<Setter Property="FontWeight" Value="Bold" />
|
||||||
</Style>
|
</Style>
|
||||||
@ -12,10 +16,21 @@
|
|||||||
<Setter Property="Padding" Value="5" />
|
<Setter Property="Padding" Value="5" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
|
<Style TargetType="TextBlock" x:Key="SubHeader">
|
||||||
|
<Setter Property="Background" Value="LightSlateGray" />
|
||||||
|
<Setter Property="Foreground" Value="White" />
|
||||||
|
<Setter Property="FontSize" Value="14" />
|
||||||
|
<Setter Property="Padding" Value="5" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
<Style TargetType="StackPanel" x:Key="Content">
|
<Style TargetType="StackPanel" x:Key="Content">
|
||||||
<Setter Property="Margin" Value="8" />
|
<Setter Property="Margin" Value="8" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
|
<Style TargetType="ContentControl" x:Key="SettingsControl">
|
||||||
|
<Setter Property="Margin" Value="0,8,0,0"></Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
<Style TargetType="TextBlock" x:Key="ComboBoxDescription">
|
<Style TargetType="TextBlock" x:Key="ComboBoxDescription">
|
||||||
<Setter Property="Foreground" Value="{x:Static SystemColors.GrayTextBrush}" />
|
<Setter Property="Foreground" Value="{x:Static SystemColors.GrayTextBrush}" />
|
||||||
</Style>
|
</Style>
|
||||||
|
44
Windows/MassiveKnob/View/InputOutputView.xaml
Normal file
44
Windows/MassiveKnob/View/InputOutputView.xaml
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<UserControl x:Class="MassiveKnob.View.InputOutputView"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:helpers="clr-namespace:MassiveKnob.Helpers"
|
||||||
|
xmlns:viewModel="clr-namespace:MassiveKnob.ViewModel"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DesignHeight="100" d:DesignWidth="800"
|
||||||
|
d:DataContext="{d:DesignInstance viewModel:InputOutputViewModel}">
|
||||||
|
<UserControl.Resources>
|
||||||
|
<ResourceDictionary>
|
||||||
|
<ResourceDictionary.MergedDictionaries>
|
||||||
|
<ResourceDictionary Source="../Style.xaml"></ResourceDictionary>
|
||||||
|
</ResourceDictionary.MergedDictionaries>
|
||||||
|
|
||||||
|
<DataTemplate x:Key="ActionDropdownItem">
|
||||||
|
<StackPanel Orientation="Vertical" d:DataContext="{d:DesignInstance Type=viewModel:ActionViewModel}">
|
||||||
|
<TextBlock Text="{Binding Name}" />
|
||||||
|
<TextBlock Text="{Binding Description}" Style="{StaticResource ComboBoxDescription}" Visibility="{Binding DescriptionVisibility}" />
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
|
||||||
|
<DataTemplate x:Key="ActionSelectedItem">
|
||||||
|
<TextBlock Text="{Binding Name}" d:DataContext="{d:DesignInstance Type=viewModel:ActionViewModel}" />
|
||||||
|
</DataTemplate>
|
||||||
|
</ResourceDictionary>
|
||||||
|
</UserControl.Resources>
|
||||||
|
<StackPanel Orientation="Vertical">
|
||||||
|
<TextBlock Text="{Binding DisplayName}" Style="{StaticResource SubHeader}"></TextBlock>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Vertical" Style="{StaticResource Content}">
|
||||||
|
<ComboBox
|
||||||
|
ItemsSource="{Binding Actions}"
|
||||||
|
SelectedItem="{Binding SelectedAction}"
|
||||||
|
IsSynchronizedWithCurrentItem="False"
|
||||||
|
ItemTemplateSelector="{helpers:ComboBoxTemplateSelector
|
||||||
|
SelectedItemTemplate={StaticResource ActionSelectedItem},
|
||||||
|
DropdownItemsTemplate={StaticResource ActionDropdownItem}}" />
|
||||||
|
|
||||||
|
<ContentControl Focusable="False" Content="{Binding ActionSettingsControl}" Style="{StaticResource SettingsControl}" />
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</UserControl>
|
13
Windows/MassiveKnob/View/InputOutputView.xaml.cs
Normal file
13
Windows/MassiveKnob/View/InputOutputView.xaml.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
namespace MassiveKnob.View
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Interaction logic for InputOutputView.xaml
|
||||||
|
/// </summary>
|
||||||
|
public partial class InputOutputView
|
||||||
|
{
|
||||||
|
public InputOutputView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -4,9 +4,13 @@
|
|||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:helpers="clr-namespace:MassiveKnob.Helpers"
|
xmlns:helpers="clr-namespace:MassiveKnob.Helpers"
|
||||||
|
xmlns:viewModel="clr-namespace:MassiveKnob.ViewModel"
|
||||||
|
xmlns:view="clr-namespace:MassiveKnob.View"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
Title="Massive Knob - Settings" Height="555" Width="704.231"
|
Title="Massive Knob - Settings" Height="555" Width="704.231"
|
||||||
WindowStartupLocation="CenterScreen">
|
WindowStartupLocation="CenterScreen"
|
||||||
|
Style="{StaticResource DefaultWindow}"
|
||||||
|
d:DataContext="{d:DesignInstance Type=viewModel:SettingsViewModel}">
|
||||||
<Window.Resources>
|
<Window.Resources>
|
||||||
<ResourceDictionary>
|
<ResourceDictionary>
|
||||||
<ResourceDictionary.MergedDictionaries>
|
<ResourceDictionary.MergedDictionaries>
|
||||||
@ -14,32 +18,103 @@
|
|||||||
</ResourceDictionary.MergedDictionaries>
|
</ResourceDictionary.MergedDictionaries>
|
||||||
|
|
||||||
<DataTemplate x:Key="DeviceDropdownItem">
|
<DataTemplate x:Key="DeviceDropdownItem">
|
||||||
<StackPanel Orientation="Vertical">
|
<StackPanel Orientation="Vertical" d:DataContext="{d:DesignInstance Type=viewModel:DeviceViewModel}">
|
||||||
<TextBlock Text="{Binding Name}" />
|
<TextBlock Text="{Binding Name}" />
|
||||||
<TextBlock Text="{Binding Description}" Style="{StaticResource ComboBoxDescription}" />
|
<TextBlock Text="{Binding Description}" Style="{StaticResource ComboBoxDescription}" Visibility="{Binding DescriptionVisibility}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
|
|
||||||
<DataTemplate x:Key="DeviceSelectedItem">
|
<DataTemplate x:Key="DeviceSelectedItem">
|
||||||
<TextBlock Text="{Binding Name}" />
|
<TextBlock Text="{Binding Name}" d:DataContext="{d:DesignInstance Type=viewModel:DeviceViewModel}" />
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
</Window.Resources>
|
</Window.Resources>
|
||||||
|
|
||||||
<ScrollViewer>
|
<ScrollViewer>
|
||||||
<StackPanel Orientation="Vertical">
|
<StackPanel Orientation="Vertical">
|
||||||
|
<!--
|
||||||
|
|
||||||
|
Device
|
||||||
|
|
||||||
|
-->
|
||||||
<TextBlock Style="{StaticResource Header}">Device</TextBlock>
|
<TextBlock Style="{StaticResource Header}">Device</TextBlock>
|
||||||
|
|
||||||
<StackPanel Orientation="Vertical" Style="{StaticResource Content}">
|
<StackPanel Orientation="Vertical" Style="{StaticResource Content}">
|
||||||
<ComboBox SelectedItem="{Binding SelectedDevice}" ItemsSource="{Binding Devices}" ItemTemplateSelector="{helpers:ComboBoxTemplateSelector
|
<ComboBox
|
||||||
SelectedItemTemplate={StaticResource DeviceSelectedItem},
|
ItemsSource="{Binding Devices}"
|
||||||
DropdownItemsTemplate={StaticResource DeviceDropdownItem}}" />
|
SelectedItem="{Binding SelectedDevice}"
|
||||||
|
IsSynchronizedWithCurrentItem="False"
|
||||||
|
ItemTemplateSelector="{helpers:ComboBoxTemplateSelector
|
||||||
|
SelectedItemTemplate={StaticResource DeviceSelectedItem},
|
||||||
|
DropdownItemsTemplate={StaticResource DeviceDropdownItem}}" />
|
||||||
|
|
||||||
<ContentControl Focusable="False" Content="{Binding SettingsControl}" />
|
|
||||||
|
<ContentControl Focusable="False" Content="{Binding SettingsControl}" Style="{StaticResource SettingsControl}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
|
|
||||||
<TextBlock Style="{StaticResource Header}">Controls</TextBlock>
|
|
||||||
|
<!--
|
||||||
|
|
||||||
|
Analog inputs
|
||||||
|
|
||||||
|
-->
|
||||||
|
<TextBlock Style="{StaticResource Header}" Visibility="{Binding AnalogInputVisibility}">Analog inputs</TextBlock>
|
||||||
|
|
||||||
|
<ItemsControl ItemsSource="{Binding AnalogInputs}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<view:InputOutputView />
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
|
||||||
|
|
||||||
|
<!--
|
||||||
|
|
||||||
|
Digital inputs
|
||||||
|
|
||||||
|
-->
|
||||||
|
<TextBlock Style="{StaticResource Header}" Visibility="{Binding DigitalInputVisibility}">Digital inputs</TextBlock>
|
||||||
|
|
||||||
|
<ItemsControl ItemsSource="{Binding DigitalInputs}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<view:InputOutputView />
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
|
||||||
|
|
||||||
|
<!--
|
||||||
|
|
||||||
|
Analog outputs
|
||||||
|
|
||||||
|
-->
|
||||||
|
<TextBlock Style="{StaticResource Header}" Visibility="{Binding AnalogOutputVisibility}">Analog outputs</TextBlock>
|
||||||
|
|
||||||
|
<ItemsControl ItemsSource="{Binding AnalogOutputs}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<view:InputOutputView />
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
|
||||||
|
|
||||||
|
<!--
|
||||||
|
|
||||||
|
Digital outputs
|
||||||
|
|
||||||
|
-->
|
||||||
|
<TextBlock Style="{StaticResource Header}" Visibility="{Binding DigitalOutputVisibility}">Digital outputs</TextBlock>
|
||||||
|
|
||||||
|
<ItemsControl ItemsSource="{Binding DigitalOutputs}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<view:InputOutputView />
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</Window>
|
</Window>
|
||||||
|
29
Windows/MassiveKnob/ViewModel/ActionViewModel.cs
Normal file
29
Windows/MassiveKnob/ViewModel/ActionViewModel.cs
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using MassiveKnob.Plugin;
|
||||||
|
|
||||||
|
namespace MassiveKnob.ViewModel
|
||||||
|
{
|
||||||
|
public class ActionViewModel
|
||||||
|
{
|
||||||
|
// ReSharper disable UnusedMember.Global - used by WPF Binding
|
||||||
|
public string Name => RepresentsNull ? Strings.ActionNotConfigured : Action.Name;
|
||||||
|
public string Description => RepresentsNull ? null : Action.Description;
|
||||||
|
|
||||||
|
public Visibility DescriptionVisibility => string.IsNullOrEmpty(Description) ? Visibility.Collapsed : Visibility.Visible;
|
||||||
|
// ReSharper restore UnusedMember.Global
|
||||||
|
|
||||||
|
public IMassiveKnobActionPlugin Plugin { get; }
|
||||||
|
public IMassiveKnobAction Action { get; }
|
||||||
|
|
||||||
|
public bool RepresentsNull => Action == null;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public ActionViewModel(IMassiveKnobActionPlugin plugin, IMassiveKnobAction action)
|
||||||
|
{
|
||||||
|
Plugin = plugin;
|
||||||
|
Action = action;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
25
Windows/MassiveKnob/ViewModel/DeviceViewModel.cs
Normal file
25
Windows/MassiveKnob/ViewModel/DeviceViewModel.cs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using MassiveKnob.Plugin;
|
||||||
|
|
||||||
|
namespace MassiveKnob.ViewModel
|
||||||
|
{
|
||||||
|
public class DeviceViewModel
|
||||||
|
{
|
||||||
|
// ReSharper disable UnusedMember.Global - used by WPF Binding
|
||||||
|
public string Name => Device.Name;
|
||||||
|
public string Description => Device.Description;
|
||||||
|
|
||||||
|
public Visibility DescriptionVisibility => string.IsNullOrEmpty(Description) ? Visibility.Collapsed : Visibility.Visible;
|
||||||
|
// ReSharper restore UnusedMember.Global
|
||||||
|
|
||||||
|
public IMassiveKnobDevicePlugin Plugin { get; }
|
||||||
|
public IMassiveKnobDevice Device { get; }
|
||||||
|
|
||||||
|
|
||||||
|
public DeviceViewModel(IMassiveKnobDevicePlugin plugin, IMassiveKnobDevice device)
|
||||||
|
{
|
||||||
|
Plugin = plugin;
|
||||||
|
Device = device;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
86
Windows/MassiveKnob/ViewModel/InputOutputViewModel.cs
Normal file
86
Windows/MassiveKnob/ViewModel/InputOutputViewModel.cs
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using MassiveKnob.Model;
|
||||||
|
using MassiveKnob.Plugin;
|
||||||
|
|
||||||
|
namespace MassiveKnob.ViewModel
|
||||||
|
{
|
||||||
|
public class InputOutputViewModel : INotifyPropertyChanged
|
||||||
|
{
|
||||||
|
private readonly SettingsViewModel settingsViewModel;
|
||||||
|
private readonly IMassiveKnobOrchestrator orchestrator;
|
||||||
|
private readonly MassiveKnobActionType actionType;
|
||||||
|
private readonly int index;
|
||||||
|
|
||||||
|
private ActionViewModel selectedAction;
|
||||||
|
private UserControl actionSettingsControl;
|
||||||
|
|
||||||
|
|
||||||
|
// ReSharper disable UnusedMember.Global - used by WPF Binding
|
||||||
|
public string DisplayName => actionType == MassiveKnobActionType.OutputAnalog || actionType == MassiveKnobActionType.OutputDigital
|
||||||
|
? $"Output #{index + 1}"
|
||||||
|
: $"Input #{index + 1}";
|
||||||
|
|
||||||
|
public IList<ActionViewModel> Actions => settingsViewModel.Actions;
|
||||||
|
|
||||||
|
|
||||||
|
public ActionViewModel SelectedAction
|
||||||
|
{
|
||||||
|
get => selectedAction;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == selectedAction)
|
||||||
|
return;
|
||||||
|
|
||||||
|
selectedAction = value == null || value.RepresentsNull ? null : value;
|
||||||
|
var actionInfo = orchestrator.SetAction(actionType, index, selectedAction?.Action);
|
||||||
|
|
||||||
|
OnPropertyChanged();
|
||||||
|
|
||||||
|
ActionSettingsControl = actionInfo?.Instance.CreateSettingsControl();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserControl ActionSettingsControl
|
||||||
|
{
|
||||||
|
get => actionSettingsControl;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == actionSettingsControl)
|
||||||
|
return;
|
||||||
|
|
||||||
|
actionSettingsControl = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ReSharper restore UnusedMember.Global
|
||||||
|
|
||||||
|
|
||||||
|
public InputOutputViewModel(SettingsViewModel settingsViewModel, IMassiveKnobOrchestrator orchestrator, MassiveKnobActionType actionType, int index)
|
||||||
|
{
|
||||||
|
this.settingsViewModel = settingsViewModel;
|
||||||
|
this.orchestrator = orchestrator;
|
||||||
|
this.actionType = actionType;
|
||||||
|
this.index = index;
|
||||||
|
|
||||||
|
var actionInfo = orchestrator.GetAction(actionType, index);
|
||||||
|
|
||||||
|
selectedAction = actionInfo != null
|
||||||
|
? Actions.SingleOrDefault(a => !a.RepresentsNull && a.Action.ActionId == actionInfo.Info.ActionId)
|
||||||
|
: Actions.Single(a => a.RepresentsNull);
|
||||||
|
|
||||||
|
actionSettingsControl = actionInfo?.Instance.CreateSettingsControl();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public event PropertyChangedEventHandler PropertyChanged;
|
||||||
|
|
||||||
|
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
|
||||||
|
{
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Windows;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
using MassiveKnob.Model;
|
using MassiveKnob.Model;
|
||||||
using MassiveKnob.Plugin;
|
using MassiveKnob.Plugin;
|
||||||
@ -11,47 +12,42 @@ namespace MassiveKnob.ViewModel
|
|||||||
{
|
{
|
||||||
public class SettingsViewModel : INotifyPropertyChanged
|
public class SettingsViewModel : INotifyPropertyChanged
|
||||||
{
|
{
|
||||||
private readonly Settings.Settings settings;
|
|
||||||
private readonly IMassiveKnobOrchestrator orchestrator;
|
private readonly IMassiveKnobOrchestrator orchestrator;
|
||||||
private DeviceViewModel selectedDevice;
|
private DeviceViewModel selectedDevice;
|
||||||
private UserControl settingsControl;
|
private UserControl settingsControl;
|
||||||
|
|
||||||
|
private DeviceSpecs? specs;
|
||||||
|
private IEnumerable<InputOutputViewModel> analogInputs;
|
||||||
|
private IEnumerable<InputOutputViewModel> digitalInputs;
|
||||||
|
private IEnumerable<InputOutputViewModel> analogOutputs;
|
||||||
|
private IEnumerable<InputOutputViewModel> digitalOutputs;
|
||||||
|
|
||||||
|
|
||||||
|
// ReSharper disable UnusedMember.Global - used by WPF Binding
|
||||||
|
public IList<DeviceViewModel> Devices { get; }
|
||||||
|
public IList<ActionViewModel> Actions { get; }
|
||||||
|
|
||||||
|
|
||||||
public IEnumerable<DeviceViewModel> Devices { get; }
|
|
||||||
public DeviceViewModel SelectedDevice
|
public DeviceViewModel SelectedDevice
|
||||||
{
|
{
|
||||||
get => selectedDevice;
|
get => selectedDevice;
|
||||||
|
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
if (value == selectedDevice)
|
if (value == selectedDevice)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
selectedDevice = value;
|
selectedDevice = value;
|
||||||
var deviceInstance = orchestrator.SetActiveDevice(value?.Device);
|
var deviceInfo = orchestrator.SetActiveDevice(value?.Device);
|
||||||
|
|
||||||
if (value == null)
|
|
||||||
settings.Device = null;
|
|
||||||
else
|
|
||||||
{
|
|
||||||
settings.Device = new Settings.Settings.DeviceSettings
|
|
||||||
{
|
|
||||||
PluginId = value.Plugin.PluginId,
|
|
||||||
DeviceId = value.Device.DeviceId,
|
|
||||||
Settings = null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
OnPropertyChanged();
|
OnPropertyChanged();
|
||||||
|
|
||||||
SettingsControl = deviceInstance?.CreateSettingsControl();
|
SettingsControl = deviceInfo?.Instance.CreateSettingsControl();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public UserControl SettingsControl
|
public UserControl SettingsControl
|
||||||
{
|
{
|
||||||
get => settingsControl;
|
get => settingsControl;
|
||||||
|
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
if (value == settingsControl)
|
if (value == settingsControl)
|
||||||
@ -62,20 +58,113 @@ namespace MassiveKnob.ViewModel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public DeviceSpecs? Specs
|
||||||
|
{
|
||||||
|
get => specs;
|
||||||
public SettingsViewModel(IPluginManager pluginManager, Settings.Settings settings, IMassiveKnobOrchestrator orchestrator)
|
set
|
||||||
|
{
|
||||||
|
specs = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
OnOtherPropertyChanged("AnalogInputVisibility");
|
||||||
|
OnOtherPropertyChanged("DigitalInputVisibility");
|
||||||
|
|
||||||
|
AnalogInputs = Enumerable
|
||||||
|
.Range(0, specs?.AnalogInputCount ?? 0)
|
||||||
|
.Select(i => new InputOutputViewModel(this, orchestrator, MassiveKnobActionType.InputAnalog, i));
|
||||||
|
|
||||||
|
DigitalInputs = Enumerable
|
||||||
|
.Range(0, specs?.DigitalInputCount ?? 0)
|
||||||
|
.Select(i => new InputOutputViewModel(this, orchestrator, MassiveKnobActionType.InputDigital, i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Visibility AnalogInputVisibility => specs.HasValue && specs.Value.AnalogInputCount > 0
|
||||||
|
? Visibility.Visible
|
||||||
|
: Visibility.Collapsed;
|
||||||
|
|
||||||
|
public IEnumerable<InputOutputViewModel> AnalogInputs
|
||||||
|
{
|
||||||
|
get => analogInputs;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
analogInputs = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Visibility DigitalInputVisibility => specs.HasValue && specs.Value.DigitalInputCount > 0
|
||||||
|
? Visibility.Visible
|
||||||
|
: Visibility.Collapsed;
|
||||||
|
|
||||||
|
public IEnumerable<InputOutputViewModel> DigitalInputs
|
||||||
|
{
|
||||||
|
get => digitalInputs;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
digitalInputs = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Visibility AnalogOutputVisibility => specs.HasValue && specs.Value.AnalogOutputCount > 0
|
||||||
|
? Visibility.Visible
|
||||||
|
: Visibility.Collapsed;
|
||||||
|
|
||||||
|
public IEnumerable<InputOutputViewModel> AnalogOutputs
|
||||||
|
{
|
||||||
|
get => analogOutputs;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
analogOutputs = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Visibility DigitalOutputVisibility => specs.HasValue && specs.Value.DigitalOutputCount > 0
|
||||||
|
? Visibility.Visible
|
||||||
|
: Visibility.Collapsed;
|
||||||
|
|
||||||
|
public IEnumerable<InputOutputViewModel> DigitalOutputs
|
||||||
|
{
|
||||||
|
get => digitalOutputs;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
digitalOutputs = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ReSharper restore UnusedMember.Global
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public SettingsViewModel(IPluginManager pluginManager, IMassiveKnobOrchestrator orchestrator)
|
||||||
{
|
{
|
||||||
this.settings = settings;
|
|
||||||
this.orchestrator = orchestrator;
|
this.orchestrator = orchestrator;
|
||||||
|
|
||||||
Devices = pluginManager.GetDevicePlugins().SelectMany(dp => dp.Devices.Select(d => new DeviceViewModel(dp, d)));
|
orchestrator.ActiveDeviceSubject.Subscribe(info => { Specs = info.Specs; });
|
||||||
|
|
||||||
if (settings.Device != null)
|
|
||||||
SelectedDevice = Devices.FirstOrDefault(d =>
|
Devices = pluginManager.GetDevicePlugins()
|
||||||
d.Plugin.PluginId == settings.Device.PluginId &&
|
.SelectMany(dp => dp.Devices.Select(d => new DeviceViewModel(dp, d)))
|
||||||
d.Device.DeviceId == settings.Device.DeviceId);
|
.ToList();
|
||||||
|
|
||||||
|
var allActions = new List<ActionViewModel>
|
||||||
|
{
|
||||||
|
new ActionViewModel(null, null)
|
||||||
|
};
|
||||||
|
|
||||||
|
allActions.AddRange(
|
||||||
|
pluginManager.GetActionPlugins()
|
||||||
|
.SelectMany(ap => ap.Actions.Select(a => new ActionViewModel(ap, a))));
|
||||||
|
|
||||||
|
Actions = allActions;
|
||||||
|
|
||||||
|
if (orchestrator.ActiveDevice == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
selectedDevice = Devices.Single(d => d.Device.DeviceId == orchestrator.ActiveDevice.Info.DeviceId);
|
||||||
|
SettingsControl = orchestrator.ActiveDevice.Instance.CreateSettingsControl();
|
||||||
|
Specs = orchestrator.ActiveDevice.Specs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -86,24 +175,9 @@ namespace MassiveKnob.ViewModel
|
|||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected virtual void OnOtherPropertyChanged(string propertyName)
|
||||||
|
|
||||||
public class DeviceViewModel
|
|
||||||
{
|
{
|
||||||
// ReSharper disable UnusedMember.Global - used by WPF Binding
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
public string Name => Device.Name;
|
|
||||||
public string Description => Device.Description;
|
|
||||||
// ReSharper restore UnusedMember.Global
|
|
||||||
|
|
||||||
public IMassiveKnobDevicePlugin Plugin { get; }
|
|
||||||
public IMassiveKnobDevice Device { get; }
|
|
||||||
|
|
||||||
|
|
||||||
public DeviceViewModel(IMassiveKnobDevicePlugin plugin, IMassiveKnobDevice device)
|
|
||||||
{
|
|
||||||
Plugin = plugin;
|
|
||||||
Device = device;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user