Improved stability of analog signals in the Arduino sketch

Reimplemented automatic refresh of the serial port list
Improved serial device connection retry
Moved plugins to their own subfolder for better separation, added metadata on the entry assembly to load
Added logging settings
Added Run at startup setting
Many visual enhancements
This commit is contained in:
Mark van Renswoude 2021-03-05 11:47:12 +01:00
parent cd1ab91f23
commit 197aef531a
59 changed files with 4128 additions and 2872 deletions

View File

@ -5,31 +5,31 @@
* *
*/ */
// Set this to the number of potentiometers you have connected // Set this to the number of potentiometers you have connected
const byte AnalogInputCount = 1; const byte AnalogInputCount = 3;
// Set this to the number of buttons you have connected // Set this to the number of buttons you have connected
const byte DigitalInputCount = 1; const byte DigitalInputCount = 0;
// Not supported yet - maybe PWM and/or other means of analog output? // Not supported yet - maybe PWM and/or other means of analog output?
const byte AnalogOutputCount = 0; const byte AnalogOutputCount = 0;
// Set this to the number of digital outputs you have connected // Set this to the number of digital outputs you have connected
const byte DigitalOutputCount = 1; const byte DigitalOutputCount = 0;
// For each potentiometer, specify the pin // For each potentiometer, specify the pin
const byte AnalogInputPin[AnalogInputCount] = { const byte AnalogInputPin[AnalogInputCount] = {
A0 A0,
A1,
A2
}; };
// For each button, specify the pin. Assumes pull-up. // For each button, specify the pin. Assumes pull-up.
const byte DigitalInputPin[DigitalInputCount] = { const byte DigitalInputPin[DigitalInputCount] = {
3
}; };
// For each digital output, specify the pin // For each digital output, specify the pin
const byte DigitalOutputPin[DigitalOutputCount] = { const byte DigitalOutputPin[DigitalOutputCount] = {
2
}; };
@ -42,7 +42,12 @@ const float EMAAlpha = 0.6;
// How many measurements to take at boot time for analog inputs to seed the EMA // How many measurements to take at boot time for analog inputs to seed the EMA
const byte EMASeedCount = 5; const byte EMASeedCount = 5;
// Minimum treshold for reporting changes in analog values, reduces noise left over from the EMA. Note that once an analog value
// changes beyond the treshold, that input will report all changes until the FocusTimeout has expired to avoid losing accuracy.
const byte AnalogTreshold = 2;
// How long to ignore other inputs after an input changes. Reduces noise due voltage drops.
const unsigned long FocusTimeout = 100;
/* /*
@ -51,6 +56,12 @@ const byte EMASeedCount = 5;
* Here be dragons. * Here be dragons.
* *
*/ */
// If defined, only outputs will be sent to the serial port as Arduino Plotter compatible data
//#define DebugOutputPlotter
#ifndef DebugOutputPlotter
#include "./min.h" #include "./min.h"
#include "./min.c" #include "./min.c"
@ -77,6 +88,7 @@ const uint8_t FrameIDAnalogOutput = 3;
const uint8_t FrameIDDigitalOutput = 4; const uint8_t FrameIDDigitalOutput = 4;
const uint8_t FrameIDQuit = 62; const uint8_t FrameIDQuit = 62;
const uint8_t FrameIDError = 63; const uint8_t FrameIDError = 63;
#endif
struct AnalogInputStatus struct AnalogInputStatus
@ -94,7 +106,6 @@ struct DigitalInputStatus
}; };
bool active = false;
struct AnalogInputStatus analogInputStatus[AnalogInputCount]; struct AnalogInputStatus analogInputStatus[AnalogInputCount];
struct DigitalInputStatus digitalInputStatus[AnalogInputCount]; struct DigitalInputStatus digitalInputStatus[AnalogInputCount];
@ -107,8 +118,10 @@ void setup()
while (!Serial) {} while (!Serial) {}
#ifndef DebugOutputPlotter
// Set up the MIN protocol (http://github.com/min-protocol/min) // Set up the MIN protocol (http://github.com/min-protocol/min)
min_init_context(&minContext, 0); min_init_context(&minContext, 0);
#endif
// Seed the moving average for analog inputs // Seed the moving average for analog inputs
@ -147,12 +160,39 @@ void setup()
} }
#ifdef DebugOutputPlotter
unsigned long lastOutput = 0;
#endif
enum FocusType
{
FocusTypeNone = 0,
FocusTypeAnalogInput = 1,
FocusTypeDigitalInput = 2,
FocusTypeOutput = 3
};
bool active = false;
FocusType focusType = FocusTypeNone;
byte focusInputIndex;
unsigned long focusOutputTime;
#define IsAnalogInputFocus(i) ((focusType == FocusInputType.AnalogInput) && (focusInputIndex == i))
#define IsDigitalInputFocus(i) ((focusType == FocusInputType.DigitalInput) && (focusInputIndex == i))
void loop() void loop()
{ {
#ifndef DebugOutputPlotter
char readBuffer[32]; char readBuffer[32];
size_t readBufferSize = Serial.available() > 0 ? Serial.readBytes(readBuffer, 32U) : 0; size_t readBufferSize = Serial.available() > 0 ? Serial.readBytes(readBuffer, 32U) : 0;
min_poll(&minContext, (uint8_t*)readBuffer, (uint8_t)readBufferSize); min_poll(&minContext, (uint8_t*)readBuffer, (uint8_t)readBufferSize);
#endif
if (focusType == FocusTypeOutput && millis() - focusOutputTime >= FocusTimeout)
focusType = FocusTypeNone;
// Check analog inputs // Check analog inputs
@ -160,8 +200,32 @@ void loop()
for (byte i = 0; i < AnalogInputCount; i++) for (byte i = 0; i < AnalogInputCount; i++)
{ {
newAnalogValue = getAnalogValue(i); newAnalogValue = getAnalogValue(i);
bool changed;
if (newAnalogValue != analogInputStatus[i].Value && (millis() - analogInputStatus[i].LastChange >= MinimumInterval)) switch (focusType)
{
case FocusTypeAnalogInput:
if (focusInputIndex != i)
continue;
if (millis() - analogInputStatus[i].LastChange < FocusTimeout)
{
changed = newAnalogValue != analogInputStatus[i].Value;
break;
}
else
focusType = FocusTypeNone;
// fall-through
case FocusTypeNone:
changed = abs(analogInputStatus[i].Value - newAnalogValue) >= AnalogTreshold;
break;
default:
continue;
}
if (changed && (millis() - analogInputStatus[i].LastChange >= MinimumInterval))
{ {
if (active) if (active)
// Send out new value // Send out new value
@ -179,6 +243,23 @@ void loop()
{ {
newDigitalValue = getDigitalValue(i); newDigitalValue = getDigitalValue(i);
switch (focusType)
{
case FocusTypeAnalogInput:
case FocusTypeOutput:
continue;
case FocusTypeDigitalInput:
if (focusInputIndex != i)
continue;
if (millis() - digitalInputStatus[i].LastChange >= FocusTimeout)
focusType = FocusTypeNone;
break;
}
if (newDigitalValue != digitalInputStatus[i].Value && (millis() - digitalInputStatus[i].LastChange >= MinimumInterval)) if (newDigitalValue != digitalInputStatus[i].Value && (millis() - digitalInputStatus[i].LastChange >= MinimumInterval))
{ {
if (active) if (active)
@ -189,9 +270,33 @@ void loop()
digitalInputStatus[i].LastChange = millis(); digitalInputStatus[i].LastChange = millis();
} }
} }
#ifdef DebugOutputPlotter
if (millis() - lastOutput >= 100)
{
for (byte i = 0; i < AnalogInputCount; i++)
{
if (i > 0)
Serial.print("\t");
Serial.print(analogInputStatus[i].Value);
}
for (byte i = 0; i < DigitalInputCount; i++)
{
if (i > 0 || AnalogInputCount > 0)
Serial.print("\t");
Serial.print(digitalInputStatus[i].Value ? 100 : 0);
}
Serial.println();
}
#endif
} }
#ifndef DebugOutputPlotter
void min_application_handler(uint8_t min_id, uint8_t *min_payload, uint8_t len_payload, uint8_t port) void min_application_handler(uint8_t min_id, uint8_t *min_payload, uint8_t len_payload, uint8_t port)
{ {
switch (min_id) switch (min_id)
@ -248,7 +353,12 @@ void processDigitalOutputMessage(uint8_t *min_payload, uint8_t len_payload)
byte outputIndex = min_payload[0]; byte outputIndex = min_payload[0];
if (outputIndex < DigitalOutputCount) if (outputIndex < DigitalOutputCount)
{
digitalWrite(DigitalOutputPin[min_payload[0]], min_payload[1] == 0 ? LOW : HIGH); digitalWrite(DigitalOutputPin[min_payload[0]], min_payload[1] == 0 ? LOW : HIGH);
focusType = FocusTypeOutput;
focusOutputTime = millis();
}
else else
outputError("Invalid digital output index: " + String(outputIndex)); outputError("Invalid digital output index: " + String(outputIndex));
} }
@ -258,6 +368,7 @@ void processQuitMessage()
{ {
active = false; active = false;
} }
#endif
byte getAnalogValue(byte analogInputIndex) byte getAnalogValue(byte analogInputIndex)
@ -285,19 +396,25 @@ bool getDigitalValue(byte digitalInputIndex)
void outputAnalogValue(byte analogInputIndex, byte newValue) void outputAnalogValue(byte analogInputIndex, byte newValue)
{ {
#ifndef DebugOutputPlotter
byte payload[2] = { analogInputIndex, newValue }; byte payload[2] = { analogInputIndex, newValue };
min_send_frame(&minContext, FrameIDAnalogInput, (uint8_t *)payload, 2); min_send_frame(&minContext, FrameIDAnalogInput, (uint8_t *)payload, 2);
#endif
} }
void outputDigitalValue(byte digitalInputIndex, bool newValue) void outputDigitalValue(byte digitalInputIndex, bool newValue)
{ {
#ifndef DebugOutputPlotter
byte payload[2] = { digitalInputIndex, newValue ? 1 : 0 }; byte payload[2] = { digitalInputIndex, newValue ? 1 : 0 };
min_send_frame(&minContext, FrameIDDigitalInput, (uint8_t *)payload, 2); min_send_frame(&minContext, FrameIDDigitalInput, (uint8_t *)payload, 2);
#endif
} }
void outputError(String message) void outputError(String message)
{ {
#ifndef DebugOutputPlotter
min_send_frame(&minContext, FrameIDError, (uint8_t *)message.c_str(), message.length()); min_send_frame(&minContext, FrameIDError, (uint8_t *)message.c_str(), message.length());
#endif
} }

View File

@ -22,4 +22,7 @@ Because of the second requirement, a simple media keys HID device does not suffi
## Developing ## Developing
The hardware side uses an Arduino sketch to communicate the hardware state over the serial port. The hardware side uses an Arduino sketch to communicate the hardware state over the serial port.
The Windows software is written in C# using .NET Framework 4.7.2 and Visual Studio 2019. The Windows software is written in C# using .NET Framework 4.7.2 and Visual Studio 2019.
Some icons courtesy of https://feathericons.com/

View File

@ -17,7 +17,7 @@
<DebugSymbols>true</DebugSymbols> <DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType> <DebugType>full</DebugType>
<Optimize>false</Optimize> <Optimize>false</Optimize>
<OutputPath>$(localappdata)\MassiveKnob\Plugins\</OutputPath> <OutputPath>$(localappdata)\MassiveKnob\Plugins\CoreAudio\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants> <DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport> <ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel> <WarningLevel>4</WarningLevel>
@ -145,6 +145,10 @@
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
</ItemGroup> </ItemGroup>
<ItemGroup /> <ItemGroup>
<None Include="MassiveKnobPlugin.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project> </Project>

View File

@ -0,0 +1,3 @@
{
"EntryAssembly": "MassiveKnob.Plugin.CoreAudio.dll"
}

View File

@ -8,8 +8,8 @@ namespace MassiveKnob.Plugin.EmulatorDevice.Devices
public class EmulatorDevice : IMassiveKnobDevice public class EmulatorDevice : IMassiveKnobDevice
{ {
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; } = "Emulator";
public string Description { get; } = "Emulates the actual device but does not communicate with anything."; public string Description { get; } = "Emulates an actual device but does not communicate with anything.";
public IMassiveKnobDeviceInstance Create(ILogger logger) public IMassiveKnobDeviceInstance Create(ILogger logger)
{ {

View File

@ -17,7 +17,7 @@
<DebugSymbols>true</DebugSymbols> <DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType> <DebugType>full</DebugType>
<Optimize>false</Optimize> <Optimize>false</Optimize>
<OutputPath>$(localappdata)\MassiveKnob\Plugins\</OutputPath> <OutputPath>$(localappdata)\MassiveKnob\Plugins\EmulatorDevice\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants> <DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport> <ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel> <WarningLevel>4</WarningLevel>
@ -79,5 +79,10 @@
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Include="MassiveKnobPlugin.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project> </Project>

View File

@ -0,0 +1,3 @@
{
"EntryAssembly": "MassiveKnob.Plugin.EmulatorDevice.dll"
}

View File

@ -17,7 +17,7 @@
<DebugSymbols>true</DebugSymbols> <DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType> <DebugType>full</DebugType>
<Optimize>false</Optimize> <Optimize>false</Optimize>
<OutputPath>$(localappdata)\MassiveKnob\Plugins\</OutputPath> <OutputPath>$(localappdata)\MassiveKnob\Plugins\SerialDevice\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants> <DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport> <ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel> <WarningLevel>4</WarningLevel>
@ -79,9 +79,17 @@
<PackageReference Include="Crc32.NET"> <PackageReference Include="Crc32.NET">
<Version>1.2.0</Version> <Version>1.2.0</Version>
</PackageReference> </PackageReference>
<PackageReference Include="Dapplo.Windows.Devices">
<Version>0.11.24</Version>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions"> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions">
<Version>5.0.0</Version> <Version>5.0.0</Version>
</PackageReference> </PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Include="MassiveKnobPlugin.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project> </Project>

View File

@ -0,0 +1,3 @@
{
"EntryAssembly": "MassiveKnob.Plugin.SerialDevice.dll"
}

View File

@ -1,14 +1,22 @@
namespace MassiveKnob.Plugin.SerialDevice.Settings using System;
namespace MassiveKnob.Plugin.SerialDevice.Settings
{ {
/// <summary> /// <summary>
/// Interaction logic for SerialDeviceSettingsView.xaml /// Interaction logic for SerialDeviceSettingsView.xaml
/// </summary> /// </summary>
public partial class SerialDeviceSettingsView public partial class SerialDeviceSettingsView : IDisposable
{ {
public SerialDeviceSettingsView(SerialDeviceSettingsViewModel viewModel) public SerialDeviceSettingsView(SerialDeviceSettingsViewModel viewModel)
{ {
DataContext = viewModel; DataContext = viewModel;
InitializeComponent(); InitializeComponent();
} }
public void Dispose()
{
(DataContext as SerialDeviceSettingsViewModel)?.Dispose();
}
} }
} }

View File

@ -1,19 +1,23 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.IO.Ports; using System.IO.Ports;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using Dapplo.Windows.Devices;
using Dapplo.Windows.Devices.Enums;
namespace MassiveKnob.Plugin.SerialDevice.Settings namespace MassiveKnob.Plugin.SerialDevice.Settings
{ {
public class SerialDeviceSettingsViewModel : INotifyPropertyChanged public class SerialDeviceSettingsViewModel : IDisposable, INotifyPropertyChanged, IObserver<DeviceNotificationEvent>
{ {
private readonly SerialDeviceSettings settings; private readonly SerialDeviceSettings settings;
private IEnumerable<string> serialPorts; private IList<string> serialPorts;
private readonly IDisposable deviceSubscription;
public event PropertyChangedEventHandler PropertyChanged; public event PropertyChangedEventHandler PropertyChanged;
// ReSharper disable UnusedMember.Global - used by WPF Binding // ReSharper disable UnusedMember.Global - used by WPF Binding
public IEnumerable<string> SerialPorts public IList<string> SerialPorts
{ {
get => serialPorts; get => serialPorts;
set set
@ -29,7 +33,7 @@ namespace MassiveKnob.Plugin.SerialDevice.Settings
get => settings.PortName; get => settings.PortName;
set set
{ {
if (value == settings.PortName) if (value == settings.PortName || value == null)
return; return;
settings.PortName = value; settings.PortName = value;
@ -72,8 +76,13 @@ namespace MassiveKnob.Plugin.SerialDevice.Settings
this.settings = settings; this.settings = settings;
serialPorts = SerialPort.GetPortNames(); serialPorts = SerialPort.GetPortNames();
deviceSubscription = DeviceNotification.OnNotification.Subscribe(this);
// TODO (must have - port from old source) subscribe to device notification to refresh list }
public void Dispose()
{
deviceSubscription.Dispose();
} }
@ -87,5 +96,31 @@ namespace MassiveKnob.Plugin.SerialDevice.Settings
{ {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
} }
protected virtual void OnOtherPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public void OnNext(DeviceNotificationEvent value)
{
if ((value.EventType == DeviceChangeEvent.DeviceArrival ||
value.EventType == DeviceChangeEvent.DeviceRemoveComplete) &&
value.Is(DeviceBroadcastDeviceType.DeviceInterface))
{
SerialPorts = SerialPort.GetPortNames();
}
}
public void OnError(Exception error)
{
}
public void OnCompleted()
{
}
} }
} }

View File

@ -49,6 +49,8 @@ namespace MassiveKnob.Plugin.SerialDevice.Worker
public void Connect(string portName, int baudRate, bool dtrEnable) public void Connect(string portName, int baudRate, bool dtrEnable)
{ {
context.Connecting();
lock (minProtocolLock) lock (minProtocolLock)
{ {
if (portName == lastPortName && baudRate == lastBaudRate && dtrEnable == lastDtrEnable) if (portName == lastPortName && baudRate == lastBaudRate && dtrEnable == lastDtrEnable)
@ -75,14 +77,28 @@ namespace MassiveKnob.Plugin.SerialDevice.Worker
public void SetAnalogOutput(int analogOutputIndex, byte value) public void SetAnalogOutput(int analogOutputIndex, byte value)
{ {
minProtocol?.QueueFrame( IMINProtocol instance;
lock (minProtocolLock)
{
instance = minProtocol;
}
instance?.QueueFrame(
(byte)MassiveKnobFrameID.AnalogOutput, (byte)MassiveKnobFrameID.AnalogOutput,
new [] { (byte)analogOutputIndex, value }); new [] { (byte)analogOutputIndex, value });
} }
public void SetDigitalOutput(int digitalOutputIndex, bool on) public void SetDigitalOutput(int digitalOutputIndex, bool on)
{ {
minProtocol?.QueueFrame( IMINProtocol instance;
lock (minProtocolLock)
{
instance = minProtocol;
}
instance?.QueueFrame(
(byte)MassiveKnobFrameID.DigitalOutput, (byte)MassiveKnobFrameID.DigitalOutput,
new [] { (byte)digitalOutputIndex, on ? (byte)1 : (byte)0 }); new [] { (byte)digitalOutputIndex, on ? (byte)1 : (byte)0 });
} }
@ -90,16 +106,42 @@ namespace MassiveKnob.Plugin.SerialDevice.Worker
private void MinProtocolOnOnConnected(object sender, EventArgs e) private void MinProtocolOnOnConnected(object sender, EventArgs e)
{ {
IMINProtocol instance;
lock (minProtocolLock)
{
if (minProtocol != sender as IMINProtocol)
return;
instance = minProtocol;
}
if (instance == null)
return;
Task.Run(async () => Task.Run(async () =>
{ {
await minProtocol.Reset(); await instance.Reset();
await minProtocol.QueueFrame((byte)MassiveKnobFrameID.Handshake, new[] { (byte)'M', (byte)'K' }); await instance.QueueFrame((byte)MassiveKnobFrameID.Handshake, new[] { (byte)'M', (byte)'K' });
}); });
} }
private void MinProtocolOnOnFrame(object sender, MINFrameEventArgs e) private void MinProtocolOnOnFrame(object sender, MINFrameEventArgs e)
{ {
IMINProtocol instance;
lock (minProtocolLock)
{
if (minProtocol != sender as IMINProtocol)
return;
instance = minProtocol;
}
if (instance == null)
return;
// ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault - by design // ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault - by design
switch ((MassiveKnobFrameID)e.Id) switch ((MassiveKnobFrameID)e.Id)
{ {
@ -108,6 +150,8 @@ namespace MassiveKnob.Plugin.SerialDevice.Worker
{ {
logger.LogError("Invalid handshake response length, expected 4, got {length}: {payload}", logger.LogError("Invalid handshake response length, expected 4, got {length}: {payload}",
e.Payload.Length, BitConverter.ToString(e.Payload)); e.Payload.Length, BitConverter.ToString(e.Payload));
Disconnect();
return; return;
} }
@ -153,7 +197,10 @@ namespace MassiveKnob.Plugin.SerialDevice.Worker
lock (minProtocolLock) lock (minProtocolLock)
{ {
minProtocol?.Dispose(); minProtocol?.Dispose();
minProtocol = null;
} }
context.Disconnected();
} }

View File

@ -51,7 +51,10 @@ namespace MassiveKnob
settingsWindow.Show(); settingsWindow.Show();
} }
else else
{
settingsWindow.WindowState = WindowState.Normal;
settingsWindow.Activate(); settingsWindow.Activate();
}
} }

View File

@ -1,7 +1,8 @@
using System; using System;
using MassiveKnob.Plugin; using MassiveKnob.Plugin;
using MassiveKnob.Settings;
namespace MassiveKnob.Model namespace MassiveKnob.Core
{ {
public interface IMassiveKnobOrchestrator : IDisposable public interface IMassiveKnobOrchestrator : IDisposable
{ {
@ -12,6 +13,9 @@ namespace MassiveKnob.Model
MassiveKnobActionInfo GetAction(MassiveKnobActionType actionType, int index); MassiveKnobActionInfo GetAction(MassiveKnobActionType actionType, int index);
MassiveKnobActionInfo SetAction(MassiveKnobActionType actionType, int index, IMassiveKnobAction action); MassiveKnobActionInfo SetAction(MassiveKnobActionType actionType, int index, IMassiveKnobAction action);
MassiveKnobSettings GetSettings();
void UpdateSettings(Action<MassiveKnobSettings> applyChanges);
} }

View File

@ -1,7 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using MassiveKnob.Plugin; using MassiveKnob.Plugin;
namespace MassiveKnob.Model namespace MassiveKnob.Core
{ {
public interface IPluginManager public interface IPluginManager
{ {

View File

@ -9,7 +9,7 @@ using Newtonsoft.Json.Linq;
using Serilog.Extensions.Logging; using Serilog.Extensions.Logging;
using ILogger = Serilog.ILogger; using ILogger = Serilog.ILogger;
namespace MassiveKnob.Model namespace MassiveKnob.Core
{ {
public class MassiveKnobOrchestrator : IMassiveKnobOrchestrator public class MassiveKnobOrchestrator : IMassiveKnobOrchestrator
{ {
@ -17,7 +17,7 @@ namespace MassiveKnob.Model
private readonly ILogger logger; private readonly ILogger logger;
private readonly object settingsLock = new object(); private readonly object settingsLock = new object();
private Settings.Settings settings; private MassiveKnobSettings massiveKnobSettings;
private readonly SerialQueue flushSettingsQueue = new SerialQueue(); private readonly SerialQueue flushSettingsQueue = new SerialQueue();
private MassiveKnobDeviceInfo activeDevice; private MassiveKnobDeviceInfo activeDevice;
@ -49,15 +49,17 @@ namespace MassiveKnob.Model
public IObservable<MassiveKnobDeviceInfo> ActiveDeviceSubject => activeDeviceInfoSubject; public IObservable<MassiveKnobDeviceInfo> ActiveDeviceSubject => activeDeviceInfoSubject;
public MassiveKnobOrchestrator(IPluginManager pluginManager, ILogger logger) public MassiveKnobOrchestrator(IPluginManager pluginManager, ILogger logger, MassiveKnobSettings massiveKnobSettings)
{ {
this.pluginManager = pluginManager; this.pluginManager = pluginManager;
this.logger = logger; this.logger = logger;
this.massiveKnobSettings = massiveKnobSettings;
} }
public void Dispose() public void Dispose()
{ {
activeDeviceContext = null;
activeDevice?.Instance?.Dispose(); activeDevice?.Instance?.Dispose();
void DisposeMappings(ICollection<ActionMapping> mappings) void DisposeMappings(ICollection<ActionMapping> mappings)
@ -85,13 +87,11 @@ namespace MassiveKnob.Model
{ {
lock (settingsLock) lock (settingsLock)
{ {
settings = SettingsJsonSerializer.Deserialize(); if (massiveKnobSettings.Device == null)
if (settings.Device == null)
return; return;
var allDevices = pluginManager.GetDevicePlugins().SelectMany(dp => dp.Devices); var allDevices = pluginManager.GetDevicePlugins().SelectMany(dp => dp.Devices);
var device = allDevices.FirstOrDefault(d => d.DeviceId == settings.Device.DeviceId); var device = allDevices.FirstOrDefault(d => d.DeviceId == massiveKnobSettings.Device.DeviceId);
InternalSetActiveDevice(device, false); InternalSetActiveDevice(device, false);
} }
@ -135,7 +135,7 @@ namespace MassiveKnob.Model
while (index >= settingsList.Count) while (index >= settingsList.Count)
settingsList.Add(null); settingsList.Add(null);
settingsList[index] = action == null ? null : new Settings.Settings.ActionSettings settingsList[index] = action == null ? null : new MassiveKnobSettings.ActionSettings
{ {
ActionId = action.ActionId, ActionId = action.ActionId,
Settings = null Settings = null
@ -161,6 +161,26 @@ namespace MassiveKnob.Model
return mapping?.ActionInfo; return mapping?.ActionInfo;
} }
public MassiveKnobSettings GetSettings()
{
lock (settingsLock)
{
return massiveKnobSettings.Clone();
}
}
public void UpdateSettings(Action<MassiveKnobSettings> applyChanges)
{
lock (settingsLock)
{
applyChanges(massiveKnobSettings);
}
FlushSettings();
}
private MassiveKnobDeviceInfo InternalSetActiveDevice(IMassiveKnobDevice device, bool resetSettings) private MassiveKnobDeviceInfo InternalSetActiveDevice(IMassiveKnobDevice device, bool resetSettings)
{ {
@ -173,10 +193,10 @@ namespace MassiveKnob.Model
lock (settingsLock) lock (settingsLock)
{ {
if (device == null) if (device == null)
settings.Device = null; massiveKnobSettings.Device = null;
else else
{ {
settings.Device = new Settings.Settings.DeviceSettings massiveKnobSettings.Device = new MassiveKnobSettings.DeviceSettings
{ {
DeviceId = device.DeviceId, DeviceId = device.DeviceId,
Settings = null Settings = null
@ -191,7 +211,7 @@ namespace MassiveKnob.Model
if (device != null) if (device != null)
{ {
var instance = device.Create(new SerilogLoggerProvider(logger.ForContext("Device", device.DeviceId)).CreateLogger(null)); var instance = device.Create(new SerilogLoggerProvider(logger.ForContext("Context", new { Device = device.DeviceId })).CreateLogger(null));
ActiveDevice = new MassiveKnobDeviceInfo(device, instance, null); ActiveDevice = new MassiveKnobDeviceInfo(device, instance, null);
activeDeviceContext = new DeviceContext(this, device); activeDeviceContext = new DeviceContext(this, device);
@ -210,11 +230,11 @@ namespace MassiveKnob.Model
protected T GetDeviceSettings<T>(IMassiveKnobDeviceContext context) where T : class, new() protected T GetDeviceSettings<T>(IMassiveKnobDeviceContext context) where T : class, new()
{ {
if (context != activeDeviceContext) if (context != activeDeviceContext)
throw new InvalidOperationException("Caller must be the active device to retrieve the settings"); throw new InvalidOperationException("Caller must be the active device to retrieve the massiveKnobSettings");
lock (settingsLock) lock (settingsLock)
{ {
return settings.Device.Settings?.ToObject<T>() ?? new T(); return massiveKnobSettings.Device.Settings?.ToObject<T>() ?? new T();
} }
} }
@ -222,17 +242,17 @@ namespace MassiveKnob.Model
protected void SetDeviceSettings<T>(IMassiveKnobDeviceContext context, IMassiveKnobDevice device, T deviceSettings) where T : class, new() protected void SetDeviceSettings<T>(IMassiveKnobDeviceContext context, IMassiveKnobDevice device, T deviceSettings) where T : class, new()
{ {
if (context != activeDeviceContext) if (context != activeDeviceContext)
throw new InvalidOperationException("Caller must be the active device to update the settings"); throw new InvalidOperationException("Caller must be the active device to update the massiveKnobSettings");
lock (settingsLock) lock (settingsLock)
{ {
if (settings.Device == null) if (massiveKnobSettings.Device == null)
settings.Device = new Settings.Settings.DeviceSettings massiveKnobSettings.Device = new MassiveKnobSettings.DeviceSettings
{ {
DeviceId = device.DeviceId DeviceId = device.DeviceId
}; };
settings.Device.Settings = JObject.FromObject(deviceSettings); massiveKnobSettings.Device.Settings = JObject.FromObject(deviceSettings);
} }
FlushSettings(); FlushSettings();
@ -248,7 +268,7 @@ namespace MassiveKnob.Model
return new T(); return new T();
if (list[index]?.Context != context) if (list[index]?.Context != context)
throw new InvalidOperationException("Caller must be the active action to retrieve the settings"); throw new InvalidOperationException("Caller must be the active action to retrieve the massiveKnobSettings");
var settingsList = GetActionSettingsList(action.ActionType); var settingsList = GetActionSettingsList(action.ActionType);
if (index >= settingsList.Count) if (index >= settingsList.Count)
@ -268,7 +288,7 @@ namespace MassiveKnob.Model
return; return;
if (list[index]?.Context != context) if (list[index]?.Context != context)
throw new InvalidOperationException("Caller must be the active action to retrieve the settings"); throw new InvalidOperationException("Caller must be the active action to retrieve the massiveKnobSettings");
var settingsList = GetActionSettingsList(action.ActionType); var settingsList = GetActionSettingsList(action.ActionType);
@ -276,7 +296,7 @@ namespace MassiveKnob.Model
settingsList.Add(null); settingsList.Add(null);
if (settingsList[index] == null) if (settingsList[index] == null)
settingsList[index] = new Settings.Settings.ActionSettings settingsList[index] = new MassiveKnobSettings.ActionSettings
{ {
ActionId = action.ActionId ActionId = action.ActionId
}; };
@ -408,21 +428,21 @@ namespace MassiveKnob.Model
} }
private List<Settings.Settings.ActionSettings> GetActionSettingsList(MassiveKnobActionType actionType) private List<MassiveKnobSettings.ActionSettings> GetActionSettingsList(MassiveKnobActionType actionType)
{ {
switch (actionType) switch (actionType)
{ {
case MassiveKnobActionType.InputAnalog: case MassiveKnobActionType.InputAnalog:
return settings.AnalogInput; return massiveKnobSettings.AnalogInput;
case MassiveKnobActionType.InputDigital: case MassiveKnobActionType.InputDigital:
return settings.DigitalInput; return massiveKnobSettings.DigitalInput;
case MassiveKnobActionType.OutputAnalog: case MassiveKnobActionType.OutputAnalog:
return settings.AnalogOutput; return massiveKnobSettings.AnalogOutput;
case MassiveKnobActionType.OutputDigital: case MassiveKnobActionType.OutputDigital:
return settings.DigitalOutput; return massiveKnobSettings.DigitalOutput;
default: default:
throw new ArgumentOutOfRangeException(nameof(actionType), actionType, null); throw new ArgumentOutOfRangeException(nameof(actionType), actionType, null);
@ -431,16 +451,16 @@ namespace MassiveKnob.Model
private void FlushSettings() private void FlushSettings()
{ {
Settings.Settings settingsSnapshot; MassiveKnobSettings massiveKnobSettingsSnapshot;
lock (settingsLock) lock (settingsLock)
{ {
settingsSnapshot = settings.Clone(); massiveKnobSettingsSnapshot = massiveKnobSettings.Clone();
} }
flushSettingsQueue.Enqueue(async () => flushSettingsQueue.Enqueue(async () =>
{ {
await SettingsJsonSerializer.Serialize(settingsSnapshot); await MassiveKnobSettingsJsonSerializer.Serialize(massiveKnobSettingsSnapshot);
}); });
} }
@ -462,10 +482,10 @@ namespace MassiveKnob.Model
lock (settingsLock) lock (settingsLock)
{ {
UpdateMapping(analogInputs, specs.AnalogInputCount, settings.AnalogInput, DelayedInitialize); UpdateMapping(analogInputs, specs.AnalogInputCount, massiveKnobSettings.AnalogInput, DelayedInitialize);
UpdateMapping(digitalInputs, specs.DigitalInputCount, settings.DigitalInput, DelayedInitialize); UpdateMapping(digitalInputs, specs.DigitalInputCount, massiveKnobSettings.DigitalInput, DelayedInitialize);
UpdateMapping(analogOutputs, specs.AnalogOutputCount, settings.AnalogOutput, DelayedInitialize); UpdateMapping(analogOutputs, specs.AnalogOutputCount, massiveKnobSettings.AnalogOutput, DelayedInitialize);
UpdateMapping(digitalOutputs, specs.DigitalOutputCount, settings.DigitalOutput, DelayedInitialize); UpdateMapping(digitalOutputs, specs.DigitalOutputCount, massiveKnobSettings.DigitalOutput, DelayedInitialize);
} }
foreach (var delayedInitializeAction in delayedInitializeActions) foreach (var delayedInitializeAction in delayedInitializeActions)
@ -487,7 +507,7 @@ namespace MassiveKnob.Model
} }
private void UpdateMapping(List<ActionMapping> mapping, int newCount, List<Settings.Settings.ActionSettings> actionSettings, Action<IMassiveKnobActionInstance, IMassiveKnobActionContext> initializeOutsideLock) private void UpdateMapping(List<ActionMapping> mapping, int newCount, List<MassiveKnobSettings.ActionSettings> actionSettings, Action<IMassiveKnobActionInstance, IMassiveKnobActionContext> initializeOutsideLock)
{ {
if (mapping.Count > newCount) if (mapping.Count > newCount)
{ {
@ -524,10 +544,13 @@ namespace MassiveKnob.Model
if (action == null) if (action == null)
return null; return null;
var actionLogger = logger var actionLogger = logger.ForContext("Context",
.ForContext("Action", action.ActionId) new
.ForContext("ActionType", action.ActionType) {
.ForContext("Index", index); Action = action.ActionId,
action.ActionType,
Index = index
});
var instance = action.Create(new SerilogLoggerProvider(actionLogger).CreateLogger(null)); var instance = action.Create(new SerilogLoggerProvider(actionLogger).CreateLogger(null));
var context = new ActionContext(this, action, index); var context = new ActionContext(this, action, index);

View File

@ -3,11 +3,13 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Text;
using MassiveKnob.Plugin; using MassiveKnob.Plugin;
using Newtonsoft.Json;
using Serilog; using Serilog;
using Serilog.Extensions.Logging; using Serilog.Extensions.Logging;
namespace MassiveKnob.Model namespace MassiveKnob.Core
{ {
public class MassiveKnobPluginIdConflictException : Exception public class MassiveKnobPluginIdConflictException : Exception
{ {
@ -75,26 +77,70 @@ namespace MassiveKnob.Model
private void LoadPlugins(string path, RegisteredIds registeredIds, Action<Exception, string> onException) private void LoadPlugins(string path, RegisteredIds registeredIds, Action<Exception, string> onException)
{ {
logger.Information("Checking {path} for plugins...", path);
if (!Directory.Exists(path)) if (!Directory.Exists(path))
return; return;
var filenames = Directory.GetFiles(path, "*.dll");
var metadataFilenames = Directory.GetFiles(path, "MassiveKnobPlugin.json", SearchOption.AllDirectories);
foreach (var filename in filenames)
foreach (var metadataFilename in metadataFilenames)
{ {
var pluginPath = Path.GetDirectoryName(metadataFilename);
if (string.IsNullOrEmpty(pluginPath))
continue;
PluginMetadata pluginMetadata;
try try
{ {
var pluginAssembly = Assembly.LoadFrom(filename); pluginMetadata = LoadMetadata(metadataFilename);
RegisterPlugins(filename, pluginAssembly, registeredIds);
} }
catch (Exception e) catch (Exception e)
{ {
onException(e, filename); logger.Warning("Could not load plugin metadata from {metadataFilename}: {message}", metadataFilename, e.Message);
continue;
}
var entryAssemblyFilename = Path.Combine(pluginPath, pluginMetadata.EntryAssembly);
if (!File.Exists(entryAssemblyFilename))
{
logger.Warning("Entry assembly specified in {metadataFilename} does not exist: {entryAssemblyFilename}", entryAssemblyFilename);
continue;
}
try
{
logger.Information("Plugin found in {pluginPath}", pluginPath);
var pluginAssembly = Assembly.LoadFrom(entryAssemblyFilename);
RegisterPlugins(entryAssemblyFilename, pluginAssembly, registeredIds);
}
catch (Exception e)
{
logger.Warning("Error while loading plugin {entryAssemblyFilename}: {message}", entryAssemblyFilename, e.Message);
onException(e, entryAssemblyFilename);
} }
} }
} }
private static PluginMetadata LoadMetadata(string filename)
{
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))
throw new IOException("Metadata file is empty");
return JsonConvert.DeserializeObject<PluginMetadata>(json);
}
private void RegisterPlugins(string filename, Assembly assembly, RegisteredIds registeredIds) 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);
@ -104,7 +150,10 @@ namespace MassiveKnob.Model
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); var plugin = (IMassiveKnobPlugin) pluginInstance;
logger.Information("Found plugin with Id {pluginId}: {name}", plugin.PluginId, plugin.Name);
ValidateRegistration(filename, plugin, registeredIds);
plugins.Add((IMassiveKnobPlugin)pluginInstance); plugins.Add((IMassiveKnobPlugin)pluginInstance);
} }
} }
@ -125,6 +174,8 @@ namespace MassiveKnob.Model
{ {
foreach (var device in devicePlugin.Devices) foreach (var device in devicePlugin.Devices)
{ {
logger.Information("- Device {deviceId}: {name}", device.DeviceId, device.Name);
if (registeredIds.DeviceById.TryGetValue(device.DeviceId, out var conflictingDeviceFilename)) if (registeredIds.DeviceById.TryGetValue(device.DeviceId, out var conflictingDeviceFilename))
throw new MassiveKnobPluginIdConflictException(device.DeviceId, conflictingDeviceFilename, filename); throw new MassiveKnobPluginIdConflictException(device.DeviceId, conflictingDeviceFilename, filename);
@ -138,6 +189,8 @@ namespace MassiveKnob.Model
{ {
foreach (var action in actionPlugin.Actions) foreach (var action in actionPlugin.Actions)
{ {
logger.Information("- Action {actionId}: {name}", action.ActionId, action.Name);
if (registeredIds.ActionById.TryGetValue(action.ActionId, out var conflictingActionFilename)) if (registeredIds.ActionById.TryGetValue(action.ActionId, out var conflictingActionFilename))
throw new MassiveKnobPluginIdConflictException(action.ActionId, conflictingActionFilename, filename); throw new MassiveKnobPluginIdConflictException(action.ActionId, conflictingActionFilename, filename);
@ -184,5 +237,11 @@ namespace MassiveKnob.Model
public readonly Dictionary<Guid, string> DeviceById = new Dictionary<Guid, string>(); public readonly Dictionary<Guid, string> DeviceById = new Dictionary<Guid, string>();
public readonly Dictionary<Guid, string> ActionById = new Dictionary<Guid, string>(); public readonly Dictionary<Guid, string> ActionById = new Dictionary<Guid, string>();
} }
private class PluginMetadata
{
public string EntryAssembly { get; set; }
}
} }
} }

View File

@ -0,0 +1,18 @@
using System;
using System.Windows.Data;
namespace MassiveKnob.Helpers
{
public class ComparisonConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return value?.Equals(parameter);
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return value?.Equals(true) == true ? parameter : Binding.DoNothing;
}
}
}

View File

@ -1,82 +0,0 @@
/*
using System;
using System.Windows.Input;
namespace MassiveKnob.Helpers
{
public class DelegateCommand : ICommand
{
private readonly Action execute;
private readonly Func<bool> canExecute;
public DelegateCommand(Action execute) : this(execute, null)
{
}
public DelegateCommand(Action execute, Func<bool> canExecute)
{
this.execute = execute ?? throw new ArgumentNullException(nameof(execute));
this.canExecute = canExecute;
}
public bool CanExecute(object parameter)
{
return canExecute?.Invoke() ?? true;
}
public void Execute(object parameter)
{
execute();
}
public event EventHandler CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
}
public class DelegateCommand<T> : ICommand
{
private readonly Action<T> execute;
private readonly Predicate<T> canExecute;
public DelegateCommand(Action<T> execute) : this(execute, null)
{
}
public DelegateCommand(Action<T> execute, Predicate<T> canExecute)
{
this.execute = execute ?? throw new ArgumentNullException(nameof(execute));
this.canExecute = canExecute;
}
public bool CanExecute(object parameter)
{
return canExecute?.Invoke((T)parameter) ?? true;
}
public void Execute(object parameter)
{
execute((T)parameter);
}
public event EventHandler CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
}
}
*/

Binary file not shown.

Before

Width:  |  Height:  |  Size: 169 KiB

View File

@ -36,7 +36,7 @@
<Prefer32Bit>false</Prefer32Bit> <Prefer32Bit>false</Prefer32Bit>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<ApplicationIcon>MainIcon.ico</ApplicationIcon> <ApplicationIcon>Resources\MainIcon.ico</ApplicationIcon>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<StartupObject>MassiveKnob.Program</StartupObject> <StartupObject>MassiveKnob.Program</StartupObject>
@ -59,15 +59,19 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Helpers\ComboBoxTemplateSelector.cs" /> <Compile Include="Helpers\ComboBoxTemplateSelector.cs" />
<Compile Include="Helpers\DelegateCommand.cs" /> <Compile Include="Helpers\ComparisonConverter.cs" />
<Compile Include="Helpers\SerialQueue.cs" /> <Compile Include="Helpers\SerialQueue.cs" />
<Compile Include="Model\IMassiveKnobOrchestrator.cs" /> <Compile Include="Core\IMassiveKnobOrchestrator.cs" />
<Compile Include="Model\IPluginManager.cs" /> <Compile Include="Core\IPluginManager.cs" />
<Compile Include="Model\MassiveKnobOrchestrator.cs" /> <Compile Include="Core\MassiveKnobOrchestrator.cs" />
<Compile Include="Model\PluginManager.cs" /> <Compile Include="Core\PluginManager.cs" />
<Compile Include="Settings\ILoggingSwitch.cs" />
<Compile Include="Settings\LoggingSwitch.cs" />
<Compile Include="ViewModel\LoggingLevelViewModel.cs" />
<Compile Include="ViewModel\ActionViewModel.cs" /> <Compile Include="ViewModel\ActionViewModel.cs" />
<Compile Include="ViewModel\DeviceViewModel.cs" /> <Compile Include="ViewModel\DeviceViewModel.cs" />
<Compile Include="ViewModel\InputOutputViewModel.cs" /> <Compile Include="ViewModel\InputOutputViewModel.cs" />
<Compile Include="ViewModel\MenuItemProperties.cs" />
<Compile Include="ViewModel\SettingsViewModel.cs" /> <Compile Include="ViewModel\SettingsViewModel.cs" />
<Compile Include="View\InputOutputView.xaml.cs"> <Compile Include="View\InputOutputView.xaml.cs">
<DependentUpon>InputOutputView.xaml</DependentUpon> <DependentUpon>InputOutputView.xaml</DependentUpon>
@ -77,16 +81,38 @@
</Compile> </Compile>
<Compile Include="Program.cs" /> <Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Settings\Settings.cs" /> <Compile Include="Settings\MassiveKnobSettings.cs" />
<Compile Include="Settings\SettingsJsonSerializer.cs" /> <Compile Include="Settings\MassiveKnobSettingsJsonSerializer.cs" />
<Compile Include="Strings.Designer.cs"> <Compile Include="Strings.Designer.cs">
<AutoGen>True</AutoGen> <AutoGen>True</AutoGen>
<DesignTime>True</DesignTime> <DesignTime>True</DesignTime>
<DependentUpon>Strings.resx</DependentUpon> <DependentUpon>Strings.resx</DependentUpon>
</Compile> </Compile>
<Compile Include="View\Settings\AnalogInputsView.xaml.cs">
<DependentUpon>AnalogInputsView.xaml</DependentUpon>
</Compile>
<Compile Include="View\Settings\AnalogOutputsView.xaml.cs">
<DependentUpon>AnalogOutputsView.xaml</DependentUpon>
</Compile>
<Compile Include="View\Settings\StartupView.xaml.cs">
<DependentUpon>StartupView.xaml</DependentUpon>
</Compile>
<Compile Include="View\Settings\LoggingView.xaml.cs">
<DependentUpon>LoggingView.xaml</DependentUpon>
</Compile>
<Compile Include="View\Settings\DigitalInputsView.xaml.cs">
<DependentUpon>DigitalInputsView.xaml</DependentUpon>
</Compile>
<Compile Include="View\Settings\DigitalOutputsView.xaml.cs">
<DependentUpon>DigitalOutputsView.xaml</DependentUpon>
</Compile>
<Compile Include="View\Settings\DeviceView.xaml.cs">
<DependentUpon>DeviceView.xaml</DependentUpon>
</Compile>
<EmbeddedResource Include="Strings.resx"> <EmbeddedResource Include="Strings.resx">
<Generator>ResXFileCodeGenerator</Generator> <Generator>PublicResXFileCodeGenerator</Generator>
<LastGenOutput>Strings.Designer.cs</LastGenOutput> <LastGenOutput>Strings.Designer.cs</LastGenOutput>
<SubType>Designer</SubType>
</EmbeddedResource> </EmbeddedResource>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -134,7 +160,7 @@
</PackageReference> </PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Content Include="MainIcon.ico" /> <Resource Include="Resources\MainIcon.ico" />
<Resource Include="Resources\NotifyIcon.ico" /> <Resource Include="Resources\NotifyIcon.ico" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -146,6 +172,58 @@
<DependentUpon>App.xaml</DependentUpon> <DependentUpon>App.xaml</DependentUpon>
<SubType>Code</SubType> <SubType>Code</SubType>
</Compile> </Compile>
<Page Include="Resources\Analog.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Resources\Device.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Resources\Digital.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Resources\IconStyle.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Resources\Logging.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Resources\Startup.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="View\Settings\AnalogInputsView.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="View\Settings\AnalogOutputsView.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="View\Settings\StartupView.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="View\Settings\LoggingView.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="View\Settings\DigitalInputsView.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="View\Settings\DigitalOutputsView.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="View\Settings\DeviceView.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Page Include="Style.xaml"> <Page Include="Style.xaml">

View File

@ -2,39 +2,36 @@
using System.IO; using System.IO;
using System.Text; using System.Text;
using System.Windows; using System.Windows;
using MassiveKnob.Model; using MassiveKnob.Core;
using MassiveKnob.Settings;
using MassiveKnob.View; using MassiveKnob.View;
using MassiveKnob.ViewModel; using MassiveKnob.ViewModel;
using Serilog; using Serilog;
using Serilog.Core;
using Serilog.Events;
using SimpleInjector; using SimpleInjector;
namespace MassiveKnob namespace MassiveKnob
{ {
public static class Program public static class Program
{ {
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread] [STAThread]
public static int Main() public static int Main()
{ {
// TODO (should have) make configurable var settings = MassiveKnobSettingsJsonSerializer.Deserialize();
var loggingLevelSwitch = new LoggingLevelSwitch();
//var loggingLevelSwitch = new LoggingLevelSwitch(LogEventLevel.Verbose); var loggingSwitch = new LoggingSwitch();
loggingSwitch.SetLogging(settings.Log.Enabled, settings.Log.Level);
var logger = new LoggerConfiguration() var logger = new LoggerConfiguration()
//.MinimumLevel.Verbose() .Filter.ByIncludingOnly(loggingSwitch.IsIncluded)
.MinimumLevel.ControlledBy(loggingLevelSwitch)
.Enrich.FromLogContext() .Enrich.FromLogContext()
.WriteTo.File( .WriteTo.File(
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"MassiveKnob", @"Logs", @".log"), Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"MassiveKnob", @"Logs", @".log"),
LogEventLevel.Verbose, rollingInterval: RollingInterval.Day) rollingInterval: RollingInterval.Day,
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{@Context}{NewLine}{Exception}")
.CreateLogger(); .CreateLogger();
logger.Information("MassiveKnob starting");
var pluginManager = new PluginManager(logger); var pluginManager = new PluginManager(logger);
var messages = new StringBuilder(); var messages = new StringBuilder();
@ -49,7 +46,7 @@ namespace MassiveKnob
return 1; return 1;
} }
var orchestrator = new MassiveKnobOrchestrator(pluginManager, logger); var orchestrator = new MassiveKnobOrchestrator(pluginManager, logger, settings);
orchestrator.Load(); orchestrator.Load();
@ -57,7 +54,7 @@ namespace MassiveKnob
container.Options.EnableAutoVerification = false; container.Options.EnableAutoVerification = false;
container.RegisterInstance(logger); container.RegisterInstance(logger);
container.RegisterInstance(loggingLevelSwitch); container.RegisterInstance<ILoggingSwitch>(loggingSwitch);
container.RegisterInstance<IPluginManager>(pluginManager); container.RegisterInstance<IPluginManager>(pluginManager);
container.RegisterInstance<IMassiveKnobOrchestrator>(orchestrator); container.RegisterInstance<IMassiveKnobOrchestrator>(orchestrator);
@ -69,6 +66,7 @@ namespace MassiveKnob
var app = container.GetInstance<App>(); var app = container.GetInstance<App>();
app.Run(); app.Run();
logger.Information("MassiveKnob shutting down");
orchestrator.Dispose(); orchestrator.Dispose();
return 0; return 0;
} }

View File

@ -0,0 +1,15 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Viewbox Stretch="Uniform" x:Key="Analog">
<Canvas Width="24" Height="24">
<Canvas.Resources>
<ResourceDictionary Source="IconStyle.xaml" />
</Canvas.Resources>
<Polyline Points="22 12 18 12 15 21 9 3 6 12 2 12" FillRule="NonZero" Style="{StaticResource IconStroke}"/>
</Canvas>
</Viewbox>
</ResourceDictionary>

View File

@ -0,0 +1,24 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Viewbox Stretch="Uniform" x:Key="Device">
<Canvas Width="24" Height="24">
<Canvas.Resources>
<ResourceDictionary Source="IconStyle.xaml" />
</Canvas.Resources>
<Rectangle Canvas.Left="4" Canvas.Top="4" Width="16" Height="16" RadiusX="2" RadiusY="2" Style="{StaticResource IconStroke}" />
<Rectangle Canvas.Left="9" Canvas.Top="9" Width="6" Height="6" Style="{StaticResource IconStroke}" />
<Line X1="9" Y1="1" X2="9" Y2="4" Style="{StaticResource IconStroke}" />
<Line X1="15" Y1="1" X2="15" Y2="4" Style="{StaticResource IconStroke}" />
<Line X1="9" Y1="20" X2="9" Y2="23" Style="{StaticResource IconStroke}" />
<Line X1="15" Y1="20" X2="15" Y2="23" Style="{StaticResource IconStroke}" />
<Line X1="20" Y1="9" X2="23" Y2="9" Style="{StaticResource IconStroke}" />
<Line X1="20" Y1="14" X2="23" Y2="14" Style="{StaticResource IconStroke}" />
<Line X1="1" Y1="9" X2="4" Y2="9" Style="{StaticResource IconStroke}" />
<Line X1="1" Y1="14" X2="4" Y2="14" Style="{StaticResource IconStroke}" />
</Canvas>
</Viewbox>
</ResourceDictionary>

View File

@ -0,0 +1,16 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Viewbox Stretch="Uniform" x:Key="Digital">
<Canvas Width="24" Height="24">
<Canvas.Resources>
<ResourceDictionary Source="IconStyle.xaml" />
</Canvas.Resources>
<Rectangle Canvas.Left="1" Canvas.Top="5" Width="22" Height="14" RadiusX="7" RadiusY="7" Style="{StaticResource IconStroke}" />
<Ellipse Canvas.Left="13" Canvas.Top="9" Width="6" Height="6" Style="{StaticResource IconStroke}" />
</Canvas>
</Viewbox>
</ResourceDictionary>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,10 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style x:Key="IconStroke">
<Setter Property="Shape.StrokeThickness" Value="2" />
<Setter Property="Shape.Stroke" Value="Black" />
<Setter Property="Line.StrokeLineJoin" Value="Round" />
<Setter Property="Line.StrokeStartLineCap" Value="Round" />
<Setter Property="Line.StrokeEndLineCap" Value="Round" />
</Style>
</ResourceDictionary>

View File

@ -0,0 +1,20 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Viewbox Stretch="Uniform" x:Key="Logging">
<Canvas Width="24" Height="24">
<Path Name="path2" StrokeThickness="2" Stroke="Black" StrokeLineJoin="Round" StrokeStartLineCap="Round" StrokeEndLineCap="Round">
<Path.Data>
<PathGeometry Figures="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" FillRule="NonZero"/>
</Path.Data>
</Path>
<Polyline Points="14 2 14 8 20 8" Name="polyline4" FillRule="NonZero" StrokeThickness="2" Stroke="Black" StrokeLineJoin="Round" StrokeStartLineCap="Round" StrokeEndLineCap="Round"/>
<Line X1="16" Y1="13" X2="8" Y2="13" Name="line6" StrokeThickness="2" Stroke="Black" StrokeLineJoin="Round" StrokeStartLineCap="Round" StrokeEndLineCap="Round"/>
<Line X1="16" Y1="17" X2="8" Y2="17" Name="line8" StrokeThickness="2" Stroke="Black" StrokeLineJoin="Round" StrokeStartLineCap="Round" StrokeEndLineCap="Round"/>
<Polyline Points="10 9 9 9 8 9" Name="polyline10" FillRule="NonZero" StrokeThickness="2" Stroke="Black" StrokeLineJoin="Round" StrokeStartLineCap="Round" StrokeEndLineCap="Round"/>
</Canvas>
</Viewbox>
</ResourceDictionary>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 169 KiB

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 KiB

After

Width:  |  Height:  |  Size: 165 KiB

View File

@ -0,0 +1,15 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Viewbox Stretch="Uniform" x:Key="Startup">
<Canvas Width="24" Height="24">
<Canvas.Resources>
<ResourceDictionary Source="IconStyle.xaml" />
</Canvas.Resources>
<Polygon Points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" FillRule="NonZero" Style="{StaticResource IconStroke}"/>
</Canvas>
</Viewbox>
</ResourceDictionary>

View File

@ -0,0 +1,9 @@
using Serilog.Events;
namespace MassiveKnob.Settings
{
public interface ILoggingSwitch
{
void SetLogging(bool enabled, LogEventLevel minimumLevel);
}
}

View File

@ -0,0 +1,26 @@
using System;
using Serilog.Core;
using Serilog.Events;
namespace MassiveKnob.Settings
{
public class LoggingSwitch : LoggingLevelSwitch, ILoggingSwitch
{
private bool enabled;
private LogEventLevel minimumLevel;
public bool IsIncluded(LogEvent logEvent)
{
return enabled && logEvent.Level >= minimumLevel;
}
// ReSharper disable ParameterHidesMember
public void SetLogging(bool enabled, LogEventLevel minimumLevel)
{
this.enabled = enabled;
this.minimumLevel = minimumLevel;
}
}
}

View File

@ -2,10 +2,25 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Serilog.Events;
namespace MassiveKnob.Settings namespace MassiveKnob.Settings
{ {
public class Settings public enum SettingsMenuItem
{
None,
Device,
AnalogInputs,
DigitalInputs,
AnalogOutputs,
DigitalOutputs,
Logging,
Startup
}
public class MassiveKnobSettings
{ {
public DeviceSettings Device { get; set; } public DeviceSettings Device { get; set; }
public List<ActionSettings> AnalogInput { get; set; } public List<ActionSettings> AnalogInput { get; set; }
@ -13,6 +28,20 @@ namespace MassiveKnob.Settings
public List<ActionSettings> AnalogOutput { get; set; } public List<ActionSettings> AnalogOutput { get; set; }
public List<ActionSettings> DigitalOutput { get; set; } public List<ActionSettings> DigitalOutput { get; set; }
private UISettings ui;
public UISettings UI
{
get => ui ?? (ui = new UISettings());
set => ui = value ?? new UISettings();
}
private LogSettings log;
public LogSettings Log
{
get => log ?? (log = new LogSettings());
set => log = value ?? new LogSettings();
}
public void Verify() public void Verify()
{ {
@ -23,15 +52,17 @@ namespace MassiveKnob.Settings
} }
public Settings Clone() public MassiveKnobSettings Clone()
{ {
return new Settings return new MassiveKnobSettings
{ {
Device = Device?.Clone(), Device = Device?.Clone(),
AnalogInput = AnalogInput.Select(a => a?.Clone()).ToList(), AnalogInput = AnalogInput.Select(a => a?.Clone()).ToList(),
DigitalInput = DigitalInput.Select(a => a?.Clone()).ToList(), DigitalInput = DigitalInput.Select(a => a?.Clone()).ToList(),
AnalogOutput = AnalogOutput.Select(a => a?.Clone()).ToList(), AnalogOutput = AnalogOutput.Select(a => a?.Clone()).ToList(),
DigitalOutput = DigitalOutput.Select(a => a?.Clone()).ToList() DigitalOutput = DigitalOutput.Select(a => a?.Clone()).ToList(),
UI = UI.Clone(),
Log = Log.Clone()
}; };
} }
@ -70,5 +101,35 @@ namespace MassiveKnob.Settings
}; };
} }
} }
public class UISettings
{
public SettingsMenuItem ActiveMenuItem { get; set; } = SettingsMenuItem.None;
public UISettings Clone()
{
return new UISettings
{
ActiveMenuItem = ActiveMenuItem
};
}
}
public class LogSettings
{
public bool Enabled { get; set; } = true;
public LogEventLevel Level { get; set; } = LogEventLevel.Information;
public LogSettings Clone()
{
return new LogSettings
{
Enabled = Enabled,
Level = Level
};
}
}
} }
} }

View File

@ -8,7 +8,7 @@ using Newtonsoft.Json.Converters;
namespace MassiveKnob.Settings namespace MassiveKnob.Settings
{ {
public static class SettingsJsonSerializer public static class MassiveKnobSettingsJsonSerializer
{ {
private static readonly JsonSerializerSettings DefaultSettings = new JsonSerializerSettings private static readonly JsonSerializerSettings DefaultSettings = new JsonSerializerSettings
{ {
@ -29,12 +29,12 @@ namespace MassiveKnob.Settings
} }
public static Task Serialize(Settings settings) public static Task Serialize(MassiveKnobSettings settings)
{ {
return Serialize(settings, GetDefaultFilename()); return Serialize(settings, GetDefaultFilename());
} }
public static async Task Serialize(Settings settings, string filename) public static async Task Serialize(MassiveKnobSettings settings, string filename)
{ {
var json = JsonConvert.SerializeObject(settings, DefaultSettings); var json = JsonConvert.SerializeObject(settings, DefaultSettings);
@ -47,14 +47,14 @@ namespace MassiveKnob.Settings
} }
public static Settings Deserialize() public static MassiveKnobSettings Deserialize()
{ {
return Deserialize(GetDefaultFilename()); return Deserialize(GetDefaultFilename());
} }
public static Settings Deserialize(string filename) public static MassiveKnobSettings Deserialize(string filename)
{ {
Settings settings = null; MassiveKnobSettings settings = null;
if (File.Exists(filename)) if (File.Exists(filename))
{ {
@ -67,11 +67,11 @@ namespace MassiveKnob.Settings
} }
if (!string.IsNullOrEmpty(json)) if (!string.IsNullOrEmpty(json))
settings = JsonConvert.DeserializeObject<Settings>(json, DefaultSettings); settings = JsonConvert.DeserializeObject<MassiveKnobSettings>(json, DefaultSettings);
} }
if (settings == null) if (settings == null)
settings = new Settings(); settings = new MassiveKnobSettings();
settings.Verify(); settings.Verify();
return settings; return settings;

View File

@ -22,7 +22,7 @@ namespace MassiveKnob {
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Strings { public class Strings {
private static global::System.Resources.ResourceManager resourceMan; private static global::System.Resources.ResourceManager resourceMan;
@ -36,7 +36,7 @@ namespace MassiveKnob {
/// Returns the cached ResourceManager instance used by this class. /// Returns the cached ResourceManager instance used by this class.
/// </summary> /// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager { public static global::System.Resources.ResourceManager ResourceManager {
get { get {
if (object.ReferenceEquals(resourceMan, null)) { if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("MassiveKnob.Strings", typeof(Strings).Assembly); global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("MassiveKnob.Strings", typeof(Strings).Assembly);
@ -51,7 +51,7 @@ namespace MassiveKnob {
/// resource lookups using this strongly typed resource class. /// resource lookups using this strongly typed resource class.
/// </summary> /// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture { public static global::System.Globalization.CultureInfo Culture {
get { get {
return resourceCulture; return resourceCulture;
} }
@ -63,10 +63,226 @@ namespace MassiveKnob {
/// <summary> /// <summary>
/// Looks up a localized string similar to Not configured. /// Looks up a localized string similar to Not configured.
/// </summary> /// </summary>
internal static string ActionNotConfigured { public static string ActionNotConfigured {
get { get {
return ResourceManager.GetString("ActionNotConfigured", resourceCulture); return ResourceManager.GetString("ActionNotConfigured", resourceCulture);
} }
} }
/// <summary>
/// Looks up a localized string similar to Input #{0}.
/// </summary>
public static string InputHeader {
get {
return ResourceManager.GetString("InputHeader", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Enabled.
/// </summary>
public static string LoggingEnabled {
get {
return ResourceManager.GetString("LoggingEnabled", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Logging level.
/// </summary>
public static string LoggingLevel {
get {
return ResourceManager.GetString("LoggingLevel", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Error.
/// </summary>
public static string LoggingLevelError {
get {
return ResourceManager.GetString("LoggingLevelError", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Only serious errors are logged..
/// </summary>
public static string LoggingLevelErrorDescription {
get {
return ResourceManager.GetString("LoggingLevelErrorDescription", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Information.
/// </summary>
public static string LoggingLevelInformation {
get {
return ResourceManager.GetString("LoggingLevelInformation", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Information, warnings and errors are logged. This is the recommended level..
/// </summary>
public static string LoggingLevelInformationDescription {
get {
return ResourceManager.GetString("LoggingLevelInformationDescription", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Verbose.
/// </summary>
public static string LoggingLevelVerbose {
get {
return ResourceManager.GetString("LoggingLevelVerbose", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to I like big logs and I can not lie!.
/// </summary>
public static string LoggingLevelVerboseDescription {
get {
return ResourceManager.GetString("LoggingLevelVerboseDescription", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Warning.
/// </summary>
public static string LoggingLevelWarning {
get {
return ResourceManager.GetString("LoggingLevelWarning", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Warnings and errors are logged. This includes issues with loading plugins..
/// </summary>
public static string LoggingLevelWarningDescription {
get {
return ResourceManager.GetString("LoggingLevelWarningDescription", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Logs are saved to {0}.
/// </summary>
public static string LoggingOutputPath {
get {
return ResourceManager.GetString("LoggingOutputPath", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Device.
/// </summary>
public static string MenuGroupDevice {
get {
return ResourceManager.GetString("MenuGroupDevice", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Settings.
/// </summary>
public static string MenuGroupSettings {
get {
return ResourceManager.GetString("MenuGroupSettings", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Analog inputs.
/// </summary>
public static string MenuItemAnalogInputs {
get {
return ResourceManager.GetString("MenuItemAnalogInputs", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Analog outputs.
/// </summary>
public static string MenuItemAnalogOutputs {
get {
return ResourceManager.GetString("MenuItemAnalogOutputs", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Device.
/// </summary>
public static string MenuItemDevice {
get {
return ResourceManager.GetString("MenuItemDevice", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Digital inputs.
/// </summary>
public static string MenuItemDigitalInputs {
get {
return ResourceManager.GetString("MenuItemDigitalInputs", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Digital outputs.
/// </summary>
public static string MenuItemDigitalOutputs {
get {
return ResourceManager.GetString("MenuItemDigitalOutputs", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Logging.
/// </summary>
public static string MenuItemLogging {
get {
return ResourceManager.GetString("MenuItemLogging", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Startup.
/// </summary>
public static string MenuItemStartup {
get {
return ResourceManager.GetString("MenuItemStartup", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Output #{0}.
/// </summary>
public static string OutputHeader {
get {
return ResourceManager.GetString("OutputHeader", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Run MassiveKnob at startup.
/// </summary>
public static string RunAtStartup {
get {
return ResourceManager.GetString("RunAtStartup", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Massive Knob - Settings.
/// </summary>
public static string SettingsWindowTitle {
get {
return ResourceManager.GetString("SettingsWindowTitle", resourceCulture);
}
}
} }
} }

View File

@ -120,4 +120,76 @@
<data name="ActionNotConfigured" xml:space="preserve"> <data name="ActionNotConfigured" xml:space="preserve">
<value>Not configured</value> <value>Not configured</value>
</data> </data>
<data name="InputHeader" xml:space="preserve">
<value>Input #{0}</value>
</data>
<data name="LoggingEnabled" xml:space="preserve">
<value>Enabled</value>
</data>
<data name="LoggingLevel" xml:space="preserve">
<value>Logging level</value>
</data>
<data name="LoggingLevelError" xml:space="preserve">
<value>Error</value>
</data>
<data name="LoggingLevelErrorDescription" xml:space="preserve">
<value>Only serious errors are logged.</value>
</data>
<data name="LoggingLevelInformation" xml:space="preserve">
<value>Information</value>
</data>
<data name="LoggingLevelInformationDescription" xml:space="preserve">
<value>Information, warnings and errors are logged. This is the recommended level.</value>
</data>
<data name="LoggingLevelVerbose" xml:space="preserve">
<value>Verbose</value>
</data>
<data name="LoggingLevelVerboseDescription" xml:space="preserve">
<value>I like big logs and I can not lie!</value>
</data>
<data name="LoggingLevelWarning" xml:space="preserve">
<value>Warning</value>
</data>
<data name="LoggingLevelWarningDescription" xml:space="preserve">
<value>Warnings and errors are logged. This includes issues with loading plugins.</value>
</data>
<data name="LoggingOutputPath" xml:space="preserve">
<value>Logs are saved to {0}</value>
</data>
<data name="MenuGroupDevice" xml:space="preserve">
<value>Device</value>
</data>
<data name="MenuGroupSettings" xml:space="preserve">
<value>Settings</value>
</data>
<data name="MenuItemAnalogInputs" xml:space="preserve">
<value>Analog inputs</value>
</data>
<data name="MenuItemAnalogOutputs" xml:space="preserve">
<value>Analog outputs</value>
</data>
<data name="MenuItemDevice" xml:space="preserve">
<value>Device</value>
</data>
<data name="MenuItemDigitalInputs" xml:space="preserve">
<value>Digital inputs</value>
</data>
<data name="MenuItemDigitalOutputs" xml:space="preserve">
<value>Digital outputs</value>
</data>
<data name="MenuItemLogging" xml:space="preserve">
<value>Logging</value>
</data>
<data name="MenuItemStartup" xml:space="preserve">
<value>Startup</value>
</data>
<data name="OutputHeader" xml:space="preserve">
<value>Output #{0}</value>
</data>
<data name="RunAtStartup" xml:space="preserve">
<value>Run MassiveKnob at startup</value>
</data>
<data name="SettingsWindowTitle" xml:space="preserve">
<value>Massive Knob - Settings</value>
</data>
</root> </root>

View File

@ -1,7 +1,7 @@
<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"> <Style TargetType="Window" x:Key="DefaultWindow">
<Setter Property="Background" Value="#f0f0f0" /> <Setter Property="Background" Value="White" />
</Style> </Style>
<Style TargetType="MenuItem" x:Key="DefaultMenuItem"> <Style TargetType="MenuItem" x:Key="DefaultMenuItem">
@ -31,6 +31,14 @@
</Style> </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="#808080" />
</Style>
<Style TargetType="TextBlock" x:Key="Label">
<Setter Property="Margin" Value="0,0,0,4" />
</Style>
<Style TargetType="TextBlock" x:Key="SubLabel">
<Setter Property="Foreground" Value="#808080" />
</Style> </Style>
</ResourceDictionary> </ResourceDictionary>

View File

@ -0,0 +1,29 @@
<UserControl x:Class="MassiveKnob.View.Settings.AnalogInputsView"
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:viewModel="clr-namespace:MassiveKnob.ViewModel"
xmlns:view="clr-namespace:MassiveKnob.View"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="800"
d:DataContext="{d:DesignInstance Type=viewModel:SettingsViewModelDesignTime, IsDesignTimeCreatable=True}">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../../Style.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<StackPanel Orientation="Vertical" SnapsToDevicePixels="True" UseLayoutRounding="True" TextOptions.TextFormattingMode="Display">
<ItemsControl ItemsSource="{Binding AnalogInputs}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<view:InputOutputView />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</UserControl>

View File

@ -0,0 +1,13 @@
namespace MassiveKnob.View.Settings
{
/// <summary>
/// Interaction logic for AnalogInputsView.xaml
/// </summary>
public partial class AnalogInputsView
{
public AnalogInputsView()
{
InitializeComponent();
}
}
}

View File

@ -0,0 +1,28 @@
<UserControl x:Class="MassiveKnob.View.Settings.AnalogOutputsView"
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:viewModel="clr-namespace:MassiveKnob.ViewModel"
xmlns:view="clr-namespace:MassiveKnob.View"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="800"
d:DataContext="{d:DesignInstance Type=viewModel:SettingsViewModelDesignTime, IsDesignTimeCreatable=True}">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../../Style.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<StackPanel Orientation="Vertical" SnapsToDevicePixels="True" UseLayoutRounding="True" TextOptions.TextFormattingMode="Display">
<ItemsControl ItemsSource="{Binding AnalogOutputs}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<view:InputOutputView />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</UserControl>

View File

@ -0,0 +1,13 @@
namespace MassiveKnob.View.Settings
{
/// <summary>
/// Interaction logic for AnalogOutputsView.xaml
/// </summary>
public partial class AnalogOutputsView
{
public AnalogOutputsView()
{
InitializeComponent();
}
}
}

View File

@ -0,0 +1,44 @@
<UserControl x:Class="MassiveKnob.View.Settings.DeviceView"
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:viewModel="clr-namespace:MassiveKnob.ViewModel"
xmlns:helpers="clr-namespace:MassiveKnob.Helpers"
mc:Ignorable="d"
d:DesignHeight="200" d:DesignWidth="800"
d:DataContext="{d:DesignInstance Type=viewModel:SettingsViewModelDesignTime, IsDesignTimeCreatable=True}">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../../Style.xaml" />
</ResourceDictionary.MergedDictionaries>
<DataTemplate x:Key="DeviceDropdownItem">
<StackPanel Orientation="Vertical" d:DataContext="{d:DesignInstance Type=viewModel:DeviceViewModel}">
<TextBlock Text="{Binding Name}" />
<TextBlock Text="{Binding Description}" Style="{StaticResource ComboBoxDescription}" Visibility="{Binding DescriptionVisibility}" />
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="DeviceSelectedItem">
<TextBlock Text="{Binding Name}" d:DataContext="{d:DesignInstance Type=viewModel:DeviceViewModel}" />
</DataTemplate>
</ResourceDictionary>
</UserControl.Resources>
<StackPanel Orientation="Vertical" SnapsToDevicePixels="True" UseLayoutRounding="True" TextOptions.TextFormattingMode="Display">
<StackPanel Orientation="Vertical" Style="{StaticResource Content}">
<ComboBox
ItemsSource="{Binding Devices}"
SelectedItem="{Binding SelectedDevice}"
IsSynchronizedWithCurrentItem="False"
ItemTemplateSelector="{helpers:ComboBoxTemplateSelector
SelectedItemTemplate={StaticResource DeviceSelectedItem},
DropdownItemsTemplate={StaticResource DeviceDropdownItem}}" />
<ContentControl Focusable="False" Content="{Binding SettingsControl}" Style="{StaticResource SettingsControl}" />
</StackPanel>
</StackPanel>
</UserControl>

View File

@ -0,0 +1,13 @@
namespace MassiveKnob.View.Settings
{
/// <summary>
/// Interaction logic for DeviceView.xaml
/// </summary>
public partial class DeviceView
{
public DeviceView()
{
InitializeComponent();
}
}
}

View File

@ -0,0 +1,28 @@
<UserControl x:Class="MassiveKnob.View.Settings.DigitalInputsView"
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:viewModel="clr-namespace:MassiveKnob.ViewModel"
xmlns:view="clr-namespace:MassiveKnob.View"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="800"
d:DataContext="{d:DesignInstance Type=viewModel:SettingsViewModelDesignTime, IsDesignTimeCreatable=True}">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../../Style.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<StackPanel Orientation="Vertical" SnapsToDevicePixels="True" UseLayoutRounding="True" TextOptions.TextFormattingMode="Display">
<ItemsControl ItemsSource="{Binding DigitalInputs}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<view:InputOutputView />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</UserControl>

View File

@ -0,0 +1,13 @@
namespace MassiveKnob.View.Settings
{
/// <summary>
/// Interaction logic for DigitalInputsView.xaml
/// </summary>
public partial class DigitalInputsView
{
public DigitalInputsView()
{
InitializeComponent();
}
}
}

View File

@ -0,0 +1,28 @@
<UserControl x:Class="MassiveKnob.View.Settings.DigitalOutputsView"
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:viewModel="clr-namespace:MassiveKnob.ViewModel"
xmlns:view="clr-namespace:MassiveKnob.View"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="800"
d:DataContext="{d:DesignInstance Type=viewModel:SettingsViewModelDesignTime, IsDesignTimeCreatable=True}">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../../Style.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<StackPanel Orientation="Vertical" SnapsToDevicePixels="True" UseLayoutRounding="True" TextOptions.TextFormattingMode="Display">
<ItemsControl ItemsSource="{Binding DigitalOutputs}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<view:InputOutputView />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</UserControl>

View File

@ -0,0 +1,13 @@
namespace MassiveKnob.View.Settings
{
/// <summary>
/// Interaction logic for DigitalOutputsView.xaml
/// </summary>
public partial class DigitalOutputsView
{
public DigitalOutputsView()
{
InitializeComponent();
}
}
}

View File

@ -0,0 +1,50 @@
<UserControl x:Class="MassiveKnob.View.Settings.LoggingView"
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:viewModel="clr-namespace:MassiveKnob.ViewModel"
xmlns:helpers="clr-namespace:MassiveKnob.Helpers"
xmlns:massiveKnob="clr-namespace:MassiveKnob"
mc:Ignorable="d"
d:DesignHeight="200" d:DesignWidth="800"
d:DataContext="{d:DesignInstance Type=viewModel:SettingsViewModelDesignTime, IsDesignTimeCreatable=True}">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../../Style.xaml" />
</ResourceDictionary.MergedDictionaries>
<DataTemplate x:Key="LoggingLevelDropdownItem">
<StackPanel Orientation="Vertical" d:DataContext="{d:DesignInstance Type=viewModel:LoggingLevelViewModel}">
<TextBlock Text="{Binding Name}" />
<TextBlock Text="{Binding Description}" Style="{StaticResource ComboBoxDescription}" />
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="LoggingLevelSelectedItem">
<TextBlock Text="{Binding Name}" d:DataContext="{d:DesignInstance Type=viewModel:LoggingLevelViewModel}" />
</DataTemplate>
</ResourceDictionary>
</UserControl.Resources>
<StackPanel Orientation="Vertical" SnapsToDevicePixels="True" UseLayoutRounding="True" TextOptions.TextFormattingMode="Display">
<StackPanel Orientation="Vertical" Style="{StaticResource Content}">
<CheckBox Margin="0,0,0,8" IsChecked="{Binding LoggingEnabled}">
<TextBlock Text="{x:Static massiveKnob:Strings.LoggingEnabled}" />
</CheckBox>
<TextBlock Text="{x:Static massiveKnob:Strings.LoggingLevel}" Style="{StaticResource Label}" />
<ComboBox
Margin="0,0,0,8"
ItemsSource="{Binding LoggingLevels}"
SelectedItem="{Binding SelectedLoggingLevel}"
IsSynchronizedWithCurrentItem="False"
ItemTemplateSelector="{helpers:ComboBoxTemplateSelector
SelectedItemTemplate={StaticResource LoggingLevelSelectedItem},
DropdownItemsTemplate={StaticResource LoggingLevelDropdownItem}}" />
<TextBlock Text="{Binding LoggingOutputPath}" Style="{StaticResource SubLabel}" />
</StackPanel>
</StackPanel>
</UserControl>

View File

@ -0,0 +1,13 @@
namespace MassiveKnob.View.Settings
{
/// <summary>
/// Interaction logic for LoggingView.xaml
/// </summary>
public partial class LoggingView
{
public LoggingView()
{
InitializeComponent();
}
}
}

View File

@ -0,0 +1,26 @@
<UserControl x:Class="MassiveKnob.View.Settings.StartupView"
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:viewModel="clr-namespace:MassiveKnob.ViewModel"
xmlns:massiveKnob="clr-namespace:MassiveKnob"
mc:Ignorable="d"
d:DesignHeight="200" d:DesignWidth="800"
d:DataContext="{d:DesignInstance Type=viewModel:SettingsViewModelDesignTime, IsDesignTimeCreatable=True}">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../../Style.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<StackPanel Orientation="Vertical" SnapsToDevicePixels="True" UseLayoutRounding="True" TextOptions.TextFormattingMode="Display">
<StackPanel Orientation="Vertical" Style="{StaticResource Content}">
<CheckBox Margin="0,0,0,8" IsChecked="{Binding RunAtStartup}">
<TextBlock Text="{x:Static massiveKnob:Strings.RunAtStartup}" />
</CheckBox>
</StackPanel>
</StackPanel>
</UserControl>

View File

@ -0,0 +1,13 @@
namespace MassiveKnob.View.Settings
{
/// <summary>
/// Interaction logic for StartupView.xaml
/// </summary>
public partial class StartupView
{
public StartupView()
{
InitializeComponent();
}
}
}

View File

@ -3,118 +3,89 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
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:viewModel="clr-namespace:MassiveKnob.ViewModel" xmlns:viewModel="clr-namespace:MassiveKnob.ViewModel"
xmlns:view="clr-namespace:MassiveKnob.View" xmlns:massiveKnob="clr-namespace:MassiveKnob"
xmlns:helpers="clr-namespace:MassiveKnob.Helpers"
xmlns:settings="clr-namespace:MassiveKnob.Settings"
mc:Ignorable="d" mc:Ignorable="d"
Title="Massive Knob - Settings" Height="555" Width="704.231" Title="{x:Static massiveKnob:Strings.SettingsWindowTitle}" Height="555" Width="800"
WindowStartupLocation="CenterScreen" WindowStartupLocation="CenterScreen"
ResizeMode="CanMinimize"
Style="{StaticResource DefaultWindow}" Style="{StaticResource DefaultWindow}"
d:DataContext="{d:DesignInstance Type=viewModel:SettingsViewModel}"> d:DataContext="{d:DesignInstance Type=viewModel:SettingsViewModelDesignTime, IsDesignTimeCreatable=True}">
<Window.Resources> <Window.Resources>
<ResourceDictionary> <ResourceDictionary>
<ResourceDictionary.MergedDictionaries> <ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../Style.xaml" /> <ResourceDictionary Source="../Style.xaml" />
<ResourceDictionary Source="../Resources/Analog.xaml" />
<ResourceDictionary Source="../Resources/Device.xaml" />
<ResourceDictionary Source="../Resources/Digital.xaml" />
<ResourceDictionary Source="../Resources/Logging.xaml" />
<ResourceDictionary Source="../Resources/Device.xaml" />
<ResourceDictionary Source="../Resources/Startup.xaml" />
</ResourceDictionary.MergedDictionaries> </ResourceDictionary.MergedDictionaries>
<DataTemplate x:Key="DeviceDropdownItem"> <helpers:ComparisonConverter x:Key="ComparisonConverter" />
<StackPanel Orientation="Vertical" d:DataContext="{d:DesignInstance Type=viewModel:DeviceViewModel}">
<TextBlock Text="{Binding Name}" />
<TextBlock Text="{Binding Description}" Style="{StaticResource ComboBoxDescription}" Visibility="{Binding DescriptionVisibility}" />
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="DeviceSelectedItem"> <Style TargetType="StackPanel" x:Key="Menu">
<TextBlock Text="{Binding Name}" d:DataContext="{d:DesignInstance Type=viewModel:DeviceViewModel}" /> <Setter Property="Background" Value="#f0f0f0" />
</DataTemplate> </Style>
<Style TargetType="TextBlock" x:Key="MenuGroup">
<Setter Property="Background" Value="#e0e0e0" />
<Setter Property="Padding" Value="8,4,8,4" />
<Setter Property="FontWeight" Value="SemiBold" />
</Style>
<Style TargetType="RadioButton" x:Key="MenuItem">
<Setter Property="OverridesDefaultStyle" Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="RadioButton">
<DockPanel x:Name="Root">
<ContentControl DockPanel.Dock="Left" Content="{TemplateBinding viewModel:MenuItemProperties.Icon}" Height="16" Margin="8,0,0,0" />
<TextBlock Padding="8,4,8,4" FontSize="14" Text="{TemplateBinding viewModel:MenuItemProperties.Text}" />
</DockPanel>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#4081a9f1" TargetName="Root" />
</Trigger>
<Trigger Property="IsChecked" Value="True">
<Setter Property="Background" Value="#8081a9f1" TargetName="Root" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary> </ResourceDictionary>
</Window.Resources> </Window.Resources>
<ScrollViewer VerticalScrollBarVisibility="Auto"> <Grid>
<StackPanel Orientation="Vertical"> <Grid.ColumnDefinitions>
<!-- <ColumnDefinition Width="250" />
<ColumnDefinition Width="*"/>
Device </Grid.ColumnDefinitions>
-->
<TextBlock Style="{StaticResource Header}">Device</TextBlock>
<StackPanel Orientation="Vertical" Style="{StaticResource Content}">
<ComboBox
ItemsSource="{Binding Devices}"
SelectedItem="{Binding SelectedDevice}"
IsSynchronizedWithCurrentItem="False"
ItemTemplateSelector="{helpers:ComboBoxTemplateSelector
SelectedItemTemplate={StaticResource DeviceSelectedItem},
DropdownItemsTemplate={StaticResource DeviceDropdownItem}}" />
<ScrollViewer Grid.Row="0" Grid.Column="0" VerticalScrollBarVisibility="Auto">
<StackPanel Style="{StaticResource Menu}" Orientation="Vertical" SnapsToDevicePixels="True" UseLayoutRounding="True" TextOptions.TextFormattingMode="Display">
<TextBlock Style="{StaticResource MenuGroup}" Text="{x:Static massiveKnob:Strings.MenuGroupDevice}" />
<RadioButton Style="{StaticResource MenuItem}" viewModel:MenuItemProperties.Text="{x:Static massiveKnob:Strings.MenuItemDevice}" viewModel:MenuItemProperties.Icon="{StaticResource Device}" IsChecked="{Binding Path=SelectedMenuItem, Converter={StaticResource ComparisonConverter}, ConverterParameter={x:Static settings:SettingsMenuItem.Device}}"/>
<RadioButton Style="{StaticResource MenuItem}" viewModel:MenuItemProperties.Text="{x:Static massiveKnob:Strings.MenuItemAnalogInputs}" viewModel:MenuItemProperties.Icon="{StaticResource Analog}" Visibility="{Binding AnalogInputVisibility}" IsChecked="{Binding Path=SelectedMenuItem, Converter={StaticResource ComparisonConverter}, ConverterParameter={x:Static settings:SettingsMenuItem.AnalogInputs}}"/>
<RadioButton Style="{StaticResource MenuItem}" viewModel:MenuItemProperties.Text="{x:Static massiveKnob:Strings.MenuItemDigitalInputs}" viewModel:MenuItemProperties.Icon="{StaticResource Digital}" Visibility="{Binding DigitalInputVisibility}" IsChecked="{Binding Path=SelectedMenuItem, Converter={StaticResource ComparisonConverter}, ConverterParameter={x:Static settings:SettingsMenuItem.DigitalInputs}}"/>
<RadioButton Style="{StaticResource MenuItem}" viewModel:MenuItemProperties.Text="{x:Static massiveKnob:Strings.MenuItemAnalogOutputs}" viewModel:MenuItemProperties.Icon="{StaticResource Analog}" Visibility="{Binding AnalogOutputVisibility}" IsChecked="{Binding Path=SelectedMenuItem, Converter={StaticResource ComparisonConverter}, ConverterParameter={x:Static settings:SettingsMenuItem.AnalogOutputs}}"/>
<RadioButton Style="{StaticResource MenuItem}" viewModel:MenuItemProperties.Text="{x:Static massiveKnob:Strings.MenuItemDigitalOutputs}" viewModel:MenuItemProperties.Icon="{StaticResource Digital}" Visibility="{Binding DigitalOutputVisibility}" IsChecked="{Binding Path=SelectedMenuItem, Converter={StaticResource ComparisonConverter}, ConverterParameter={x:Static settings:SettingsMenuItem.DigitalOutputs}}"/>
<ContentControl Focusable="False" Content="{Binding SettingsControl}" Style="{StaticResource SettingsControl}" /> <TextBlock Style="{StaticResource MenuGroup}" Text="{x:Static massiveKnob:Strings.MenuGroupSettings}" />
<RadioButton Style="{StaticResource MenuItem}" viewModel:MenuItemProperties.Text="{x:Static massiveKnob:Strings.MenuItemLogging}" viewModel:MenuItemProperties.Icon="{StaticResource Logging}" IsChecked="{Binding Path=SelectedMenuItem, Converter={StaticResource ComparisonConverter}, ConverterParameter={x:Static settings:SettingsMenuItem.Logging}}"/>
<RadioButton Style="{StaticResource MenuItem}" viewModel:MenuItemProperties.Text="{x:Static massiveKnob:Strings.MenuItemStartup}" viewModel:MenuItemProperties.Icon="{StaticResource Startup}" IsChecked="{Binding Path=SelectedMenuItem, Converter={StaticResource ComparisonConverter}, ConverterParameter={x:Static settings:SettingsMenuItem.Startup}}"/>
</StackPanel> </StackPanel>
</ScrollViewer>
<ScrollViewer Grid.Row="0" Grid.Column="1" VerticalScrollBarVisibility="Auto">
<!-- <ContentControl Content="{Binding SelectedView}" />
</ScrollViewer>
Analog inputs </Grid>
-->
<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>
</ScrollViewer>
</Window> </Window>

View File

@ -7,10 +7,16 @@ namespace MassiveKnob.View
/// </summary> /// </summary>
public partial class SettingsWindow public partial class SettingsWindow
{ {
// ReSharper disable once SuggestBaseTypeForParameter - for clarity
public SettingsWindow(SettingsViewModel settingsViewModel) public SettingsWindow(SettingsViewModel settingsViewModel)
{ {
DataContext = settingsViewModel; DataContext = settingsViewModel;
InitializeComponent(); InitializeComponent();
Closed += (sender, args) =>
{
settingsViewModel.Dispose();
};
} }
} }
} }

View File

@ -1,14 +1,15 @@
using System.Collections.Generic; using System;
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.Controls; using System.Windows.Controls;
using MassiveKnob.Model; using MassiveKnob.Core;
using MassiveKnob.Plugin; using MassiveKnob.Plugin;
namespace MassiveKnob.ViewModel namespace MassiveKnob.ViewModel
{ {
public class InputOutputViewModel : INotifyPropertyChanged public class InputOutputViewModel : IDisposable, INotifyPropertyChanged
{ {
private readonly IMassiveKnobOrchestrator orchestrator; private readonly IMassiveKnobOrchestrator orchestrator;
private readonly MassiveKnobActionType actionType; private readonly MassiveKnobActionType actionType;
@ -19,9 +20,11 @@ namespace MassiveKnob.ViewModel
// ReSharper disable UnusedMember.Global - used by WPF Binding // ReSharper disable UnusedMember.Global - used by WPF Binding
public string DisplayName => actionType == MassiveKnobActionType.OutputAnalog || actionType == MassiveKnobActionType.OutputDigital public string DisplayName => string.Format(
? $"Output #{index + 1}" actionType == MassiveKnobActionType.OutputAnalog || actionType == MassiveKnobActionType.OutputDigital
: $"Input #{index + 1}"; ? Strings.OutputHeader
: Strings.InputHeader,
index + 1);
public IList<ActionViewModel> Actions { get; } public IList<ActionViewModel> Actions { get; }
@ -51,6 +54,9 @@ namespace MassiveKnob.ViewModel
if (value == actionSettingsControl) if (value == actionSettingsControl)
return; return;
if (actionSettingsControl is IDisposable disposable)
disposable.Dispose();
actionSettingsControl = value; actionSettingsControl = value;
OnPropertyChanged(); OnPropertyChanged();
} }
@ -63,6 +69,11 @@ namespace MassiveKnob.ViewModel
this.orchestrator = orchestrator; this.orchestrator = orchestrator;
this.actionType = actionType; this.actionType = actionType;
this.index = index; this.index = index;
// For design-time support
if (orchestrator == null)
return;
Actions = settingsViewModel.Actions.Where(a => a.RepresentsNull || a.Action.ActionType == actionType).ToList(); Actions = settingsViewModel.Actions.Where(a => a.RepresentsNull || a.Action.ActionType == actionType).ToList();
@ -76,6 +87,13 @@ namespace MassiveKnob.ViewModel
actionSettingsControl = actionInfo?.Instance.CreateSettingsControl(); actionSettingsControl = actionInfo?.Instance.CreateSettingsControl();
} }
public void Dispose()
{
if (ActionSettingsControl is IDisposable disposable)
disposable.Dispose();
}
public event PropertyChangedEventHandler PropertyChanged; public event PropertyChangedEventHandler PropertyChanged;

View File

@ -0,0 +1,20 @@
using Serilog.Events;
namespace MassiveKnob.ViewModel
{
public class LoggingLevelViewModel
{
public LogEventLevel Level { get; }
public string Name { get; }
public string Description { get; }
public LoggingLevelViewModel(LogEventLevel level, string name, string description)
{
Level = level;
Name = name;
Description = description;
}
}
}

View File

@ -0,0 +1,21 @@
using System.Windows;
using System.Windows.Controls;
namespace MassiveKnob.ViewModel
{
public static class MenuItemProperties
{
public static string GetText(DependencyObject obj) { return (string) obj.GetValue(TextProperty); }
public static void SetText(DependencyObject obj, string value) { obj.SetValue(TextProperty, value); }
public static readonly DependencyProperty TextProperty =
DependencyProperty.RegisterAttached("Text", typeof(string), typeof(MenuItemProperties), new FrameworkPropertyMetadata("Menu item"));
public static Viewbox GetIcon(DependencyObject obj) { return (Viewbox)obj.GetValue(IconProperty); }
public static void SetIcon(DependencyObject obj, Viewbox value) { obj.SetValue(IconProperty, value); }
public static readonly DependencyProperty IconProperty =
DependencyProperty.RegisterAttached("Icon", typeof(Viewbox), typeof(MenuItemProperties), new FrameworkPropertyMetadata(null));
}
}

View File

@ -1,20 +1,42 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq; using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using MassiveKnob.Model; using MassiveKnob.Core;
using MassiveKnob.Plugin; using MassiveKnob.Plugin;
using MassiveKnob.Settings;
using MassiveKnob.View.Settings;
using Microsoft.Win32;
using Serilog.Events;
namespace MassiveKnob.ViewModel namespace MassiveKnob.ViewModel
{ {
// TODO (nice to have) better design-time version public class SettingsViewModel : IDisposable, INotifyPropertyChanged
public class SettingsViewModel : INotifyPropertyChanged
{ {
private readonly Dictionary<SettingsMenuItem, Type> menuItemControls = new Dictionary<SettingsMenuItem, Type>
{
{ SettingsMenuItem.Device, typeof(DeviceView) },
{ SettingsMenuItem.AnalogInputs, typeof(AnalogInputsView) },
{ SettingsMenuItem.DigitalInputs, typeof(DigitalInputsView) },
{ SettingsMenuItem.AnalogOutputs, typeof(AnalogOutputsView) },
{ SettingsMenuItem.DigitalOutputs, typeof(DigitalOutputsView) },
{ SettingsMenuItem.Logging, typeof(LoggingView) },
{ SettingsMenuItem.Startup, typeof(StartupView) }
};
private readonly IMassiveKnobOrchestrator orchestrator; private readonly IMassiveKnobOrchestrator orchestrator;
private readonly ILoggingSwitch loggingSwitch;
private DeviceViewModel selectedDevice; private DeviceViewModel selectedDevice;
private UserControl selectedView;
private SettingsMenuItem selectedMenuItem;
private UserControl settingsControl; private UserControl settingsControl;
private DeviceSpecs? specs; private DeviceSpecs? specs;
@ -25,10 +47,46 @@ namespace MassiveKnob.ViewModel
// ReSharper disable UnusedMember.Global - used by WPF Binding // ReSharper disable UnusedMember.Global - used by WPF Binding
public SettingsMenuItem SelectedMenuItem
{
get => selectedMenuItem;
set
{
if (value == selectedMenuItem)
return;
selectedMenuItem = value;
OnPropertyChanged();
if (menuItemControls.TryGetValue(selectedMenuItem, out var viewType))
SelectedView = (UserControl) Activator.CreateInstance(viewType);
orchestrator?.UpdateSettings(settings =>
{
settings.UI.ActiveMenuItem = selectedMenuItem;
});
}
}
public UserControl SelectedView
{
get => selectedView;
set
{
if (value == selectedView)
return;
selectedView = value;
OnPropertyChanged();
}
}
public IList<DeviceViewModel> Devices { get; } public IList<DeviceViewModel> Devices { get; }
public IList<ActionViewModel> Actions { get; } public IList<ActionViewModel> Actions { get; }
public DeviceViewModel SelectedDevice public DeviceViewModel SelectedDevice
{ {
get => selectedDevice; get => selectedDevice;
@ -38,7 +96,7 @@ namespace MassiveKnob.ViewModel
return; return;
selectedDevice = value; selectedDevice = value;
var deviceInfo = orchestrator.SetActiveDevice(value?.Device); var deviceInfo = orchestrator?.SetActiveDevice(value?.Device);
OnPropertyChanged(); OnPropertyChanged();
@ -54,6 +112,9 @@ namespace MassiveKnob.ViewModel
if (value == settingsControl) if (value == settingsControl)
return; return;
if (settingsControl is IDisposable disposable)
disposable.Dispose();
settingsControl = value; settingsControl = value;
OnPropertyChanged(); OnPropertyChanged();
} }
@ -71,6 +132,11 @@ namespace MassiveKnob.ViewModel
OnDependantPropertyChanged("AnalogOutputVisibility"); OnDependantPropertyChanged("AnalogOutputVisibility");
OnDependantPropertyChanged("DigitalOutputVisibility"); OnDependantPropertyChanged("DigitalOutputVisibility");
DisposeInputOutputViewModels(AnalogInputs);
DisposeInputOutputViewModels(DigitalInputs);
DisposeInputOutputViewModels(AnalogOutputs);
DisposeInputOutputViewModels(DigitalOutputs);
AnalogInputs = Enumerable AnalogInputs = Enumerable
.Range(0, specs?.AnalogInputCount ?? 0) .Range(0, specs?.AnalogInputCount ?? 0)
.Select(i => new InputOutputViewModel(this, orchestrator, MassiveKnobActionType.InputAnalog, i)); .Select(i => new InputOutputViewModel(this, orchestrator, MassiveKnobActionType.InputAnalog, i));
@ -89,6 +155,7 @@ namespace MassiveKnob.ViewModel
} }
} }
public Visibility AnalogInputVisibility => specs.HasValue && specs.Value.AnalogInputCount > 0 public Visibility AnalogInputVisibility => specs.HasValue && specs.Value.AnalogInputCount > 0
? Visibility.Visible ? Visibility.Visible
: Visibility.Collapsed; : Visibility.Collapsed;
@ -144,13 +211,80 @@ namespace MassiveKnob.ViewModel
OnPropertyChanged(); OnPropertyChanged();
} }
} }
public IList<LoggingLevelViewModel> LoggingLevels { get; }
private LoggingLevelViewModel selectedLoggingLevel;
public LoggingLevelViewModel SelectedLoggingLevel
{
get => selectedLoggingLevel;
set
{
if (value == selectedLoggingLevel)
return;
selectedLoggingLevel = value;
OnPropertyChanged();
ApplyLoggingSettings();
}
}
private bool loggingEnabled;
public bool LoggingEnabled
{
get => loggingEnabled;
set
{
if (value == loggingEnabled)
return;
loggingEnabled = value;
OnPropertyChanged();
ApplyLoggingSettings();
}
}
// TODO (code quality) do not hardcode path here
public string LoggingOutputPath { get; } = string.Format(Strings.LoggingOutputPath, Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"MassiveKnob", @"Logs"));
private bool runAtStartup;
public bool RunAtStartup
{
get => runAtStartup;
set
{
if (value == runAtStartup)
return;
runAtStartup = value;
OnPropertyChanged();
ApplyRunAtStartup();
}
}
// ReSharper restore UnusedMember.Global // ReSharper restore UnusedMember.Global
public SettingsViewModel(IPluginManager pluginManager, IMassiveKnobOrchestrator orchestrator, ILoggingSwitch loggingSwitch)
public SettingsViewModel(IPluginManager pluginManager, IMassiveKnobOrchestrator orchestrator)
{ {
this.orchestrator = orchestrator; this.orchestrator = orchestrator;
this.loggingSwitch = loggingSwitch;
// For design-time support
if (orchestrator == null)
return;
var activeMenuItem = orchestrator.GetSettings().UI.ActiveMenuItem;
if (activeMenuItem == SettingsMenuItem.None)
activeMenuItem = SettingsMenuItem.Device;
SelectedMenuItem = activeMenuItem;
orchestrator.ActiveDeviceSubject.Subscribe(info => { Specs = info.Specs; }); orchestrator.ActiveDeviceSubject.Subscribe(info => { Specs = info.Specs; });
@ -170,12 +304,83 @@ namespace MassiveKnob.ViewModel
Actions = allActions; Actions = allActions;
if (orchestrator.ActiveDevice == null) 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;
}
selectedDevice = Devices.Single(d => d.Device.DeviceId == orchestrator.ActiveDevice.Info.DeviceId);
SettingsControl = orchestrator.ActiveDevice.Instance.CreateSettingsControl(); var logSettings = orchestrator.GetSettings().Log;
Specs = orchestrator.ActiveDevice.Specs; LoggingLevels = new List<LoggingLevelViewModel>
{
new LoggingLevelViewModel(LogEventLevel.Error, Strings.LoggingLevelError, Strings.LoggingLevelErrorDescription),
new LoggingLevelViewModel(LogEventLevel.Warning, Strings.LoggingLevelWarning, Strings.LoggingLevelWarningDescription),
new LoggingLevelViewModel(LogEventLevel.Information, Strings.LoggingLevelInformation, Strings.LoggingLevelInformationDescription),
new LoggingLevelViewModel(LogEventLevel.Verbose, Strings.LoggingLevelVerbose, Strings.LoggingLevelVerboseDescription),
};
selectedLoggingLevel = LoggingLevels.SingleOrDefault(l => l.Level == logSettings.Level)
?? LoggingLevels.Single(l => l.Level == LogEventLevel.Information);
loggingEnabled = logSettings.Enabled;
var runKey = Registry.CurrentUser.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", false);
runAtStartup = runKey?.GetValue("MassiveKnob") != null;
}
public void Dispose()
{
if (SettingsControl is IDisposable disposable)
disposable.Dispose();
DisposeInputOutputViewModels(AnalogInputs);
DisposeInputOutputViewModels(DigitalInputs);
DisposeInputOutputViewModels(AnalogOutputs);
DisposeInputOutputViewModels(DigitalOutputs);
}
private void ApplyLoggingSettings()
{
orchestrator?.UpdateSettings(settings =>
{
settings.Log.Enabled = LoggingEnabled;
settings.Log.Level = SelectedLoggingLevel.Level;
});
loggingSwitch?.SetLogging(LoggingEnabled, selectedLoggingLevel.Level);
}
private void ApplyRunAtStartup()
{
var runKey = Registry.CurrentUser.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", true);
Debug.Assert(runKey != null, nameof(runKey) + " != null");
if (RunAtStartup)
{
var entryAssembly = Assembly.GetEntryAssembly();
Debug.Assert(entryAssembly != null, nameof(entryAssembly) + " != null");
runKey.SetValue("MassiveKnob", new Uri(entryAssembly.CodeBase).LocalPath);
}
else
{
runKey.DeleteValue("MassiveKnob", false);
}
}
private static void DisposeInputOutputViewModels(IEnumerable<InputOutputViewModel> viewModels)
{
if (viewModels == null)
return;
foreach (var viewModel in viewModels)
viewModel.Dispose();
} }
@ -191,4 +396,13 @@ namespace MassiveKnob.ViewModel
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
} }
} }
public class SettingsViewModelDesignTime : SettingsViewModel
{
public SettingsViewModelDesignTime() : base(null, null, null)
{
Specs = new DeviceSpecs(2, 2, 2, 2);
}
}
} }

@ -1 +1 @@
Subproject commit 6db7da6234713a50a2c278c00bcd710249738e5e Subproject commit 65c76b3f214522dd5f1da3704b83375bf238daba