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
const byte AnalogInputCount = 1;
const byte AnalogInputCount = 3;
// 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?
const byte AnalogOutputCount = 0;
// 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
const byte AnalogInputPin[AnalogInputCount] = {
A0
A0,
A1,
A2
};
// For each button, specify the pin. Assumes pull-up.
const byte DigitalInputPin[DigitalInputCount] = {
3
};
// For each digital output, specify the pin
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
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.
*
*/
// 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.c"
@ -77,6 +88,7 @@ const uint8_t FrameIDAnalogOutput = 3;
const uint8_t FrameIDDigitalOutput = 4;
const uint8_t FrameIDQuit = 62;
const uint8_t FrameIDError = 63;
#endif
struct AnalogInputStatus
@ -94,7 +106,6 @@ struct DigitalInputStatus
};
bool active = false;
struct AnalogInputStatus analogInputStatus[AnalogInputCount];
struct DigitalInputStatus digitalInputStatus[AnalogInputCount];
@ -107,8 +118,10 @@ void setup()
while (!Serial) {}
#ifndef DebugOutputPlotter
// Set up the MIN protocol (http://github.com/min-protocol/min)
min_init_context(&minContext, 0);
#endif
// 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()
{
#ifndef DebugOutputPlotter
char readBuffer[32];
size_t readBufferSize = Serial.available() > 0 ? Serial.readBytes(readBuffer, 32U) : 0;
min_poll(&minContext, (uint8_t*)readBuffer, (uint8_t)readBufferSize);
#endif
if (focusType == FocusTypeOutput && millis() - focusOutputTime >= FocusTimeout)
focusType = FocusTypeNone;
// Check analog inputs
@ -160,8 +200,32 @@ void loop()
for (byte i = 0; i < AnalogInputCount; 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)
// Send out new value
@ -179,6 +243,23 @@ void loop()
{
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 (active)
@ -189,9 +270,33 @@ void loop()
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)
{
switch (min_id)
@ -248,7 +353,12 @@ void processDigitalOutputMessage(uint8_t *min_payload, uint8_t len_payload)
byte outputIndex = min_payload[0];
if (outputIndex < DigitalOutputCount)
{
digitalWrite(DigitalOutputPin[min_payload[0]], min_payload[1] == 0 ? LOW : HIGH);
focusType = FocusTypeOutput;
focusOutputTime = millis();
}
else
outputError("Invalid digital output index: " + String(outputIndex));
}
@ -258,6 +368,7 @@ void processQuitMessage()
{
active = false;
}
#endif
byte getAnalogValue(byte analogInputIndex)
@ -285,19 +396,25 @@ bool getDigitalValue(byte digitalInputIndex)
void outputAnalogValue(byte analogInputIndex, byte newValue)
{
#ifndef DebugOutputPlotter
byte payload[2] = { analogInputIndex, newValue };
min_send_frame(&minContext, FrameIDAnalogInput, (uint8_t *)payload, 2);
#endif
}
void outputDigitalValue(byte digitalInputIndex, bool newValue)
{
#ifndef DebugOutputPlotter
byte payload[2] = { digitalInputIndex, newValue ? 1 : 0 };
min_send_frame(&minContext, FrameIDDigitalInput, (uint8_t *)payload, 2);
#endif
}
void outputError(String message)
{
#ifndef DebugOutputPlotter
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
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>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>$(localappdata)\MassiveKnob\Plugins\</OutputPath>
<OutputPath>$(localappdata)\MassiveKnob\Plugins\CoreAudio\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
@ -145,6 +145,10 @@
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup />
<ItemGroup>
<None Include="MassiveKnobPlugin.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</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 Guid DeviceId { get; } = new Guid("e1a4977a-abf4-4c75-a17d-fd8d3a8451ff");
public string Name { get; } = "Mock device";
public string Description { get; } = "Emulates the actual device but does not communicate with anything.";
public string Name { get; } = "Emulator";
public string Description { get; } = "Emulates an actual device but does not communicate with anything.";
public IMassiveKnobDeviceInstance Create(ILogger logger)
{

View File

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

View File

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

View File

@ -17,7 +17,7 @@
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>$(localappdata)\MassiveKnob\Plugins\</OutputPath>
<OutputPath>$(localappdata)\MassiveKnob\Plugins\SerialDevice\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
@ -79,9 +79,17 @@
<PackageReference Include="Crc32.NET">
<Version>1.2.0</Version>
</PackageReference>
<PackageReference Include="Dapplo.Windows.Devices">
<Version>0.11.24</Version>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions">
<Version>5.0.0</Version>
</PackageReference>
</ItemGroup>
<ItemGroup>
<None Include="MassiveKnobPlugin.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</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>
/// Interaction logic for SerialDeviceSettingsView.xaml
/// </summary>
public partial class SerialDeviceSettingsView
public partial class SerialDeviceSettingsView : IDisposable
{
public SerialDeviceSettingsView(SerialDeviceSettingsViewModel viewModel)
{
DataContext = viewModel;
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.IO.Ports;
using System.Runtime.CompilerServices;
using Dapplo.Windows.Devices;
using Dapplo.Windows.Devices.Enums;
namespace MassiveKnob.Plugin.SerialDevice.Settings
{
public class SerialDeviceSettingsViewModel : INotifyPropertyChanged
public class SerialDeviceSettingsViewModel : IDisposable, INotifyPropertyChanged, IObserver<DeviceNotificationEvent>
{
private readonly SerialDeviceSettings settings;
private IEnumerable<string> serialPorts;
private IList<string> serialPorts;
private readonly IDisposable deviceSubscription;
public event PropertyChangedEventHandler PropertyChanged;
// ReSharper disable UnusedMember.Global - used by WPF Binding
public IEnumerable<string> SerialPorts
public IList<string> SerialPorts
{
get => serialPorts;
set
@ -29,7 +33,7 @@ namespace MassiveKnob.Plugin.SerialDevice.Settings
get => settings.PortName;
set
{
if (value == settings.PortName)
if (value == settings.PortName || value == null)
return;
settings.PortName = value;
@ -72,8 +76,13 @@ namespace MassiveKnob.Plugin.SerialDevice.Settings
this.settings = settings;
serialPorts = SerialPort.GetPortNames();
// TODO (must have - port from old source) subscribe to device notification to refresh list
deviceSubscription = DeviceNotification.OnNotification.Subscribe(this);
}
public void Dispose()
{
deviceSubscription.Dispose();
}
@ -87,5 +96,31 @@ namespace MassiveKnob.Plugin.SerialDevice.Settings
{
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)
{
context.Connecting();
lock (minProtocolLock)
{
if (portName == lastPortName && baudRate == lastBaudRate && dtrEnable == lastDtrEnable)
@ -75,14 +77,28 @@ namespace MassiveKnob.Plugin.SerialDevice.Worker
public void SetAnalogOutput(int analogOutputIndex, byte value)
{
minProtocol?.QueueFrame(
IMINProtocol instance;
lock (minProtocolLock)
{
instance = minProtocol;
}
instance?.QueueFrame(
(byte)MassiveKnobFrameID.AnalogOutput,
new [] { (byte)analogOutputIndex, value });
}
public void SetDigitalOutput(int digitalOutputIndex, bool on)
{
minProtocol?.QueueFrame(
IMINProtocol instance;
lock (minProtocolLock)
{
instance = minProtocol;
}
instance?.QueueFrame(
(byte)MassiveKnobFrameID.DigitalOutput,
new [] { (byte)digitalOutputIndex, on ? (byte)1 : (byte)0 });
}
@ -90,16 +106,42 @@ namespace MassiveKnob.Plugin.SerialDevice.Worker
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 () =>
{
await minProtocol.Reset();
await minProtocol.QueueFrame((byte)MassiveKnobFrameID.Handshake, new[] { (byte)'M', (byte)'K' });
await instance.Reset();
await instance.QueueFrame((byte)MassiveKnobFrameID.Handshake, new[] { (byte)'M', (byte)'K' });
});
}
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
switch ((MassiveKnobFrameID)e.Id)
{
@ -108,6 +150,8 @@ namespace MassiveKnob.Plugin.SerialDevice.Worker
{
logger.LogError("Invalid handshake response length, expected 4, got {length}: {payload}",
e.Payload.Length, BitConverter.ToString(e.Payload));
Disconnect();
return;
}
@ -153,7 +197,10 @@ namespace MassiveKnob.Plugin.SerialDevice.Worker
lock (minProtocolLock)
{
minProtocol?.Dispose();
minProtocol = null;
}
context.Disconnected();
}

View File

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

View File

@ -1,7 +1,8 @@
using System;
using MassiveKnob.Plugin;
using MassiveKnob.Settings;
namespace MassiveKnob.Model
namespace MassiveKnob.Core
{
public interface IMassiveKnobOrchestrator : IDisposable
{
@ -12,6 +13,9 @@ namespace MassiveKnob.Model
MassiveKnobActionInfo GetAction(MassiveKnobActionType actionType, int index);
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 MassiveKnob.Plugin;
namespace MassiveKnob.Model
namespace MassiveKnob.Core
{
public interface IPluginManager
{

View File

@ -9,7 +9,7 @@ using Newtonsoft.Json.Linq;
using Serilog.Extensions.Logging;
using ILogger = Serilog.ILogger;
namespace MassiveKnob.Model
namespace MassiveKnob.Core
{
public class MassiveKnobOrchestrator : IMassiveKnobOrchestrator
{
@ -17,7 +17,7 @@ namespace MassiveKnob.Model
private readonly ILogger logger;
private readonly object settingsLock = new object();
private Settings.Settings settings;
private MassiveKnobSettings massiveKnobSettings;
private readonly SerialQueue flushSettingsQueue = new SerialQueue();
private MassiveKnobDeviceInfo activeDevice;
@ -49,15 +49,17 @@ namespace MassiveKnob.Model
public IObservable<MassiveKnobDeviceInfo> ActiveDeviceSubject => activeDeviceInfoSubject;
public MassiveKnobOrchestrator(IPluginManager pluginManager, ILogger logger)
public MassiveKnobOrchestrator(IPluginManager pluginManager, ILogger logger, MassiveKnobSettings massiveKnobSettings)
{
this.pluginManager = pluginManager;
this.logger = logger;
this.massiveKnobSettings = massiveKnobSettings;
}
public void Dispose()
{
activeDeviceContext = null;
activeDevice?.Instance?.Dispose();
void DisposeMappings(ICollection<ActionMapping> mappings)
@ -85,13 +87,11 @@ namespace MassiveKnob.Model
{
lock (settingsLock)
{
settings = SettingsJsonSerializer.Deserialize();
if (settings.Device == null)
if (massiveKnobSettings.Device == null)
return;
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);
}
@ -135,7 +135,7 @@ namespace MassiveKnob.Model
while (index >= settingsList.Count)
settingsList.Add(null);
settingsList[index] = action == null ? null : new Settings.Settings.ActionSettings
settingsList[index] = action == null ? null : new MassiveKnobSettings.ActionSettings
{
ActionId = action.ActionId,
Settings = null
@ -161,6 +161,26 @@ namespace MassiveKnob.Model
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)
{
@ -173,10 +193,10 @@ namespace MassiveKnob.Model
lock (settingsLock)
{
if (device == null)
settings.Device = null;
massiveKnobSettings.Device = null;
else
{
settings.Device = new Settings.Settings.DeviceSettings
massiveKnobSettings.Device = new MassiveKnobSettings.DeviceSettings
{
DeviceId = device.DeviceId,
Settings = null
@ -191,7 +211,7 @@ namespace MassiveKnob.Model
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);
activeDeviceContext = new DeviceContext(this, device);
@ -210,11 +230,11 @@ namespace MassiveKnob.Model
protected T GetDeviceSettings<T>(IMassiveKnobDeviceContext context) where T : class, new()
{
if (context != activeDeviceContext)
throw new InvalidOperationException("Caller must be the active device to retrieve the settings");
throw new InvalidOperationException("Caller must be the active device to retrieve the massiveKnobSettings");
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()
{
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)
{
if (settings.Device == null)
settings.Device = new Settings.Settings.DeviceSettings
if (massiveKnobSettings.Device == null)
massiveKnobSettings.Device = new MassiveKnobSettings.DeviceSettings
{
DeviceId = device.DeviceId
};
settings.Device.Settings = JObject.FromObject(deviceSettings);
massiveKnobSettings.Device.Settings = JObject.FromObject(deviceSettings);
}
FlushSettings();
@ -248,7 +268,7 @@ namespace MassiveKnob.Model
return new T();
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);
if (index >= settingsList.Count)
@ -268,7 +288,7 @@ namespace MassiveKnob.Model
return;
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);
@ -276,7 +296,7 @@ namespace MassiveKnob.Model
settingsList.Add(null);
if (settingsList[index] == null)
settingsList[index] = new Settings.Settings.ActionSettings
settingsList[index] = new MassiveKnobSettings.ActionSettings
{
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)
{
case MassiveKnobActionType.InputAnalog:
return settings.AnalogInput;
return massiveKnobSettings.AnalogInput;
case MassiveKnobActionType.InputDigital:
return settings.DigitalInput;
return massiveKnobSettings.DigitalInput;
case MassiveKnobActionType.OutputAnalog:
return settings.AnalogOutput;
return massiveKnobSettings.AnalogOutput;
case MassiveKnobActionType.OutputDigital:
return settings.DigitalOutput;
return massiveKnobSettings.DigitalOutput;
default:
throw new ArgumentOutOfRangeException(nameof(actionType), actionType, null);
@ -431,16 +451,16 @@ namespace MassiveKnob.Model
private void FlushSettings()
{
Settings.Settings settingsSnapshot;
MassiveKnobSettings massiveKnobSettingsSnapshot;
lock (settingsLock)
{
settingsSnapshot = settings.Clone();
massiveKnobSettingsSnapshot = massiveKnobSettings.Clone();
}
flushSettingsQueue.Enqueue(async () =>
{
await SettingsJsonSerializer.Serialize(settingsSnapshot);
await MassiveKnobSettingsJsonSerializer.Serialize(massiveKnobSettingsSnapshot);
});
}
@ -462,10 +482,10 @@ namespace MassiveKnob.Model
lock (settingsLock)
{
UpdateMapping(analogInputs, specs.AnalogInputCount, settings.AnalogInput, DelayedInitialize);
UpdateMapping(digitalInputs, specs.DigitalInputCount, settings.DigitalInput, DelayedInitialize);
UpdateMapping(analogOutputs, specs.AnalogOutputCount, settings.AnalogOutput, DelayedInitialize);
UpdateMapping(digitalOutputs, specs.DigitalOutputCount, settings.DigitalOutput, DelayedInitialize);
UpdateMapping(analogInputs, specs.AnalogInputCount, massiveKnobSettings.AnalogInput, DelayedInitialize);
UpdateMapping(digitalInputs, specs.DigitalInputCount, massiveKnobSettings.DigitalInput, DelayedInitialize);
UpdateMapping(analogOutputs, specs.AnalogOutputCount, massiveKnobSettings.AnalogOutput, DelayedInitialize);
UpdateMapping(digitalOutputs, specs.DigitalOutputCount, massiveKnobSettings.DigitalOutput, DelayedInitialize);
}
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)
{
@ -524,10 +544,13 @@ namespace MassiveKnob.Model
if (action == null)
return null;
var actionLogger = logger
.ForContext("Action", action.ActionId)
.ForContext("ActionType", action.ActionType)
.ForContext("Index", index);
var actionLogger = logger.ForContext("Context",
new
{
Action = action.ActionId,
action.ActionType,
Index = index
});
var instance = action.Create(new SerilogLoggerProvider(actionLogger).CreateLogger(null));
var context = new ActionContext(this, action, index);

View File

@ -3,11 +3,13 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using MassiveKnob.Plugin;
using Newtonsoft.Json;
using Serilog;
using Serilog.Extensions.Logging;
namespace MassiveKnob.Model
namespace MassiveKnob.Core
{
public class MassiveKnobPluginIdConflictException : Exception
{
@ -75,26 +77,70 @@ namespace MassiveKnob.Model
private void LoadPlugins(string path, RegisteredIds registeredIds, Action<Exception, string> onException)
{
logger.Information("Checking {path} for plugins...", path);
if (!Directory.Exists(path))
return;
var filenames = Directory.GetFiles(path, "*.dll");
foreach (var filename in filenames)
var metadataFilenames = Directory.GetFiles(path, "MassiveKnobPlugin.json", SearchOption.AllDirectories);
foreach (var metadataFilename in metadataFilenames)
{
var pluginPath = Path.GetDirectoryName(metadataFilename);
if (string.IsNullOrEmpty(pluginPath))
continue;
PluginMetadata pluginMetadata;
try
{
var pluginAssembly = Assembly.LoadFrom(filename);
RegisterPlugins(filename, pluginAssembly, registeredIds);
pluginMetadata = LoadMetadata(metadataFilename);
}
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)
{
var pluginTypes = assembly.GetTypes().Where(t => t.GetCustomAttribute<MassiveKnobPluginAttribute>() != null);
@ -104,7 +150,10 @@ namespace MassiveKnob.Model
if (!(pluginInstance is 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);
}
}
@ -125,6 +174,8 @@ namespace MassiveKnob.Model
{
foreach (var device in devicePlugin.Devices)
{
logger.Information("- Device {deviceId}: {name}", device.DeviceId, device.Name);
if (registeredIds.DeviceById.TryGetValue(device.DeviceId, out var conflictingDeviceFilename))
throw new MassiveKnobPluginIdConflictException(device.DeviceId, conflictingDeviceFilename, filename);
@ -138,6 +189,8 @@ namespace MassiveKnob.Model
{
foreach (var action in actionPlugin.Actions)
{
logger.Information("- Action {actionId}: {name}", action.ActionId, action.Name);
if (registeredIds.ActionById.TryGetValue(action.ActionId, out var conflictingActionFilename))
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> 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>
</PropertyGroup>
<PropertyGroup>
<ApplicationIcon>MainIcon.ico</ApplicationIcon>
<ApplicationIcon>Resources\MainIcon.ico</ApplicationIcon>
</PropertyGroup>
<PropertyGroup>
<StartupObject>MassiveKnob.Program</StartupObject>
@ -59,15 +59,19 @@
</ItemGroup>
<ItemGroup>
<Compile Include="Helpers\ComboBoxTemplateSelector.cs" />
<Compile Include="Helpers\DelegateCommand.cs" />
<Compile Include="Helpers\ComparisonConverter.cs" />
<Compile Include="Helpers\SerialQueue.cs" />
<Compile Include="Model\IMassiveKnobOrchestrator.cs" />
<Compile Include="Model\IPluginManager.cs" />
<Compile Include="Model\MassiveKnobOrchestrator.cs" />
<Compile Include="Model\PluginManager.cs" />
<Compile Include="Core\IMassiveKnobOrchestrator.cs" />
<Compile Include="Core\IPluginManager.cs" />
<Compile Include="Core\MassiveKnobOrchestrator.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\DeviceViewModel.cs" />
<Compile Include="ViewModel\InputOutputViewModel.cs" />
<Compile Include="ViewModel\MenuItemProperties.cs" />
<Compile Include="ViewModel\SettingsViewModel.cs" />
<Compile Include="View\InputOutputView.xaml.cs">
<DependentUpon>InputOutputView.xaml</DependentUpon>
@ -77,16 +81,38 @@
</Compile>
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Settings\Settings.cs" />
<Compile Include="Settings\SettingsJsonSerializer.cs" />
<Compile Include="Settings\MassiveKnobSettings.cs" />
<Compile Include="Settings\MassiveKnobSettingsJsonSerializer.cs" />
<Compile Include="Strings.Designer.cs">
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
<DependentUpon>Strings.resx</DependentUpon>
</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">
<Generator>ResXFileCodeGenerator</Generator>
<Generator>PublicResXFileCodeGenerator</Generator>
<LastGenOutput>Strings.Designer.cs</LastGenOutput>
<SubType>Designer</SubType>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
@ -134,7 +160,7 @@
</PackageReference>
</ItemGroup>
<ItemGroup>
<Content Include="MainIcon.ico" />
<Resource Include="Resources\MainIcon.ico" />
<Resource Include="Resources\NotifyIcon.ico" />
</ItemGroup>
<ItemGroup>
@ -146,6 +172,58 @@
<DependentUpon>App.xaml</DependentUpon>
<SubType>Code</SubType>
</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>
<Page Include="Style.xaml">

View File

@ -2,39 +2,36 @@
using System.IO;
using System.Text;
using System.Windows;
using MassiveKnob.Model;
using MassiveKnob.Core;
using MassiveKnob.Settings;
using MassiveKnob.View;
using MassiveKnob.ViewModel;
using Serilog;
using Serilog.Core;
using Serilog.Events;
using SimpleInjector;
namespace MassiveKnob
{
public static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
public static int Main()
{
// TODO (should have) make configurable
var loggingLevelSwitch = new LoggingLevelSwitch();
//var loggingLevelSwitch = new LoggingLevelSwitch(LogEventLevel.Verbose);
var settings = MassiveKnobSettingsJsonSerializer.Deserialize();
var loggingSwitch = new LoggingSwitch();
loggingSwitch.SetLogging(settings.Log.Enabled, settings.Log.Level);
var logger = new LoggerConfiguration()
//.MinimumLevel.Verbose()
.MinimumLevel.ControlledBy(loggingLevelSwitch)
.Filter.ByIncludingOnly(loggingSwitch.IsIncluded)
.Enrich.FromLogContext()
.WriteTo.File(
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();
logger.Information("MassiveKnob starting");
var pluginManager = new PluginManager(logger);
var messages = new StringBuilder();
@ -49,7 +46,7 @@ namespace MassiveKnob
return 1;
}
var orchestrator = new MassiveKnobOrchestrator(pluginManager, logger);
var orchestrator = new MassiveKnobOrchestrator(pluginManager, logger, settings);
orchestrator.Load();
@ -57,7 +54,7 @@ namespace MassiveKnob
container.Options.EnableAutoVerification = false;
container.RegisterInstance(logger);
container.RegisterInstance(loggingLevelSwitch);
container.RegisterInstance<ILoggingSwitch>(loggingSwitch);
container.RegisterInstance<IPluginManager>(pluginManager);
container.RegisterInstance<IMassiveKnobOrchestrator>(orchestrator);
@ -69,6 +66,7 @@ namespace MassiveKnob
var app = container.GetInstance<App>();
app.Run();
logger.Information("MassiveKnob shutting down");
orchestrator.Dispose();
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.Linq;
using Newtonsoft.Json.Linq;
using Serilog.Events;
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 List<ActionSettings> AnalogInput { get; set; }
@ -13,6 +28,20 @@ namespace MassiveKnob.Settings
public List<ActionSettings> AnalogOutput { 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()
{
@ -23,15 +52,17 @@ namespace MassiveKnob.Settings
}
public Settings Clone()
public MassiveKnobSettings Clone()
{
return new Settings
return new MassiveKnobSettings
{
Device = Device?.Clone(),
AnalogInput = AnalogInput.Select(a => a?.Clone()).ToList(),
DigitalInput = DigitalInput.Select(a => a?.Clone()).ToList(),
AnalogOutput = AnalogOutput.Select(a => a?.Clone()).ToList(),
DigitalOutput = DigitalOutput.Select(a => a?.Clone()).ToList()
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
{
public static class SettingsJsonSerializer
public static class MassiveKnobSettingsJsonSerializer
{
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());
}
public static async Task Serialize(Settings settings, string filename)
public static async Task Serialize(MassiveKnobSettings settings, string filename)
{
var json = JsonConvert.SerializeObject(settings, DefaultSettings);
@ -47,14 +47,14 @@ namespace MassiveKnob.Settings
}
public static Settings Deserialize()
public static MassiveKnobSettings Deserialize()
{
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))
{
@ -67,11 +67,11 @@ namespace MassiveKnob.Settings
}
if (!string.IsNullOrEmpty(json))
settings = JsonConvert.DeserializeObject<Settings>(json, DefaultSettings);
settings = JsonConvert.DeserializeObject<MassiveKnobSettings>(json, DefaultSettings);
}
if (settings == null)
settings = new Settings();
settings = new MassiveKnobSettings();
settings.Verify();
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.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Strings {
public class Strings {
private static global::System.Resources.ResourceManager resourceMan;
@ -36,7 +36,7 @@ namespace MassiveKnob {
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
public static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
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.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
public static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
@ -63,10 +63,226 @@ namespace MassiveKnob {
/// <summary>
/// Looks up a localized string similar to Not configured.
/// </summary>
internal static string ActionNotConfigured {
public static string ActionNotConfigured {
get {
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">
<value>Not configured</value>
</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>

View File

@ -1,7 +1,7 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style TargetType="Window" x:Key="DefaultWindow">
<Setter Property="Background" Value="#f0f0f0" />
<Setter Property="Background" Value="White" />
</Style>
<Style TargetType="MenuItem" x:Key="DefaultMenuItem">
@ -31,6 +31,14 @@
</Style>
<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>
</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:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:helpers="clr-namespace:MassiveKnob.Helpers"
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"
Title="Massive Knob - Settings" Height="555" Width="704.231"
Title="{x:Static massiveKnob:Strings.SettingsWindowTitle}" Height="555" Width="800"
WindowStartupLocation="CenterScreen"
ResizeMode="CanMinimize"
Style="{StaticResource DefaultWindow}"
d:DataContext="{d:DesignInstance Type=viewModel:SettingsViewModel}">
d:DataContext="{d:DesignInstance Type=viewModel:SettingsViewModelDesignTime, IsDesignTimeCreatable=True}">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<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>
<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>
<helpers:ComparisonConverter x:Key="ComparisonConverter" />
<DataTemplate x:Key="DeviceSelectedItem">
<TextBlock Text="{Binding Name}" d:DataContext="{d:DesignInstance Type=viewModel:DeviceViewModel}" />
</DataTemplate>
<Style TargetType="StackPanel" x:Key="Menu">
<Setter Property="Background" Value="#f0f0f0" />
</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>
</Window.Resources>
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Orientation="Vertical">
<!--
Device
-->
<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}}" />
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="250" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<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>
</ScrollViewer>
<!--
Analog inputs
-->
<TextBlock Style="{StaticResource Header}" Visibility="{Binding AnalogInputVisibility}">Analog inputs</TextBlock>
<ItemsControl ItemsSource="{Binding AnalogInputs}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<view:InputOutputView />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!--
Digital inputs
-->
<TextBlock Style="{StaticResource Header}" Visibility="{Binding DigitalInputVisibility}">Digital inputs</TextBlock>
<ItemsControl ItemsSource="{Binding DigitalInputs}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<view:InputOutputView />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!--
Analog outputs
-->
<TextBlock Style="{StaticResource Header}" Visibility="{Binding AnalogOutputVisibility}">Analog outputs</TextBlock>
<ItemsControl ItemsSource="{Binding AnalogOutputs}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<view:InputOutputView />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!--
Digital outputs
-->
<TextBlock Style="{StaticResource Header}" Visibility="{Binding DigitalOutputVisibility}">Digital outputs</TextBlock>
<ItemsControl ItemsSource="{Binding DigitalOutputs}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<view:InputOutputView />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</ScrollViewer>
<ScrollViewer Grid.Row="0" Grid.Column="1" VerticalScrollBarVisibility="Auto">
<ContentControl Content="{Binding SelectedView}" />
</ScrollViewer>
</Grid>
</Window>

View File

@ -7,10 +7,16 @@ namespace MassiveKnob.View
/// </summary>
public partial class SettingsWindow
{
// ReSharper disable once SuggestBaseTypeForParameter - for clarity
public SettingsWindow(SettingsViewModel settingsViewModel)
{
DataContext = settingsViewModel;
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.Linq;
using System.Runtime.CompilerServices;
using System.Windows.Controls;
using MassiveKnob.Model;
using MassiveKnob.Core;
using MassiveKnob.Plugin;
namespace MassiveKnob.ViewModel
{
public class InputOutputViewModel : INotifyPropertyChanged
public class InputOutputViewModel : IDisposable, INotifyPropertyChanged
{
private readonly IMassiveKnobOrchestrator orchestrator;
private readonly MassiveKnobActionType actionType;
@ -19,9 +20,11 @@ namespace MassiveKnob.ViewModel
// ReSharper disable UnusedMember.Global - used by WPF Binding
public string DisplayName => actionType == MassiveKnobActionType.OutputAnalog || actionType == MassiveKnobActionType.OutputDigital
? $"Output #{index + 1}"
: $"Input #{index + 1}";
public string DisplayName => string.Format(
actionType == MassiveKnobActionType.OutputAnalog || actionType == MassiveKnobActionType.OutputDigital
? Strings.OutputHeader
: Strings.InputHeader,
index + 1);
public IList<ActionViewModel> Actions { get; }
@ -51,6 +54,9 @@ namespace MassiveKnob.ViewModel
if (value == actionSettingsControl)
return;
if (actionSettingsControl is IDisposable disposable)
disposable.Dispose();
actionSettingsControl = value;
OnPropertyChanged();
}
@ -63,6 +69,11 @@ namespace MassiveKnob.ViewModel
this.orchestrator = orchestrator;
this.actionType = actionType;
this.index = index;
// For design-time support
if (orchestrator == null)
return;
Actions = settingsViewModel.Actions.Where(a => a.RepresentsNull || a.Action.ActionType == actionType).ToList();
@ -76,6 +87,13 @@ namespace MassiveKnob.ViewModel
actionSettingsControl = actionInfo?.Instance.CreateSettingsControl();
}
public void Dispose()
{
if (ActionSettingsControl is IDisposable disposable)
disposable.Dispose();
}
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.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Controls;
using MassiveKnob.Model;
using MassiveKnob.Core;
using MassiveKnob.Plugin;
using MassiveKnob.Settings;
using MassiveKnob.View.Settings;
using Microsoft.Win32;
using Serilog.Events;
namespace MassiveKnob.ViewModel
{
// TODO (nice to have) better design-time version
public class SettingsViewModel : INotifyPropertyChanged
public class SettingsViewModel : IDisposable, 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 ILoggingSwitch loggingSwitch;
private DeviceViewModel selectedDevice;
private UserControl selectedView;
private SettingsMenuItem selectedMenuItem;
private UserControl settingsControl;
private DeviceSpecs? specs;
@ -25,10 +47,46 @@ namespace MassiveKnob.ViewModel
// 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<ActionViewModel> Actions { get; }
public DeviceViewModel SelectedDevice
{
get => selectedDevice;
@ -38,7 +96,7 @@ namespace MassiveKnob.ViewModel
return;
selectedDevice = value;
var deviceInfo = orchestrator.SetActiveDevice(value?.Device);
var deviceInfo = orchestrator?.SetActiveDevice(value?.Device);
OnPropertyChanged();
@ -54,6 +112,9 @@ namespace MassiveKnob.ViewModel
if (value == settingsControl)
return;
if (settingsControl is IDisposable disposable)
disposable.Dispose();
settingsControl = value;
OnPropertyChanged();
}
@ -71,6 +132,11 @@ namespace MassiveKnob.ViewModel
OnDependantPropertyChanged("AnalogOutputVisibility");
OnDependantPropertyChanged("DigitalOutputVisibility");
DisposeInputOutputViewModels(AnalogInputs);
DisposeInputOutputViewModels(DigitalInputs);
DisposeInputOutputViewModels(AnalogOutputs);
DisposeInputOutputViewModels(DigitalOutputs);
AnalogInputs = Enumerable
.Range(0, specs?.AnalogInputCount ?? 0)
.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
? Visibility.Visible
: Visibility.Collapsed;
@ -144,13 +211,80 @@ namespace MassiveKnob.ViewModel
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
public SettingsViewModel(IPluginManager pluginManager, IMassiveKnobOrchestrator orchestrator)
public SettingsViewModel(IPluginManager pluginManager, IMassiveKnobOrchestrator orchestrator, ILoggingSwitch loggingSwitch)
{
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; });
@ -170,12 +304,83 @@ namespace MassiveKnob.ViewModel
Actions = allActions;
if (orchestrator.ActiveDevice == null)
return;
if (orchestrator.ActiveDevice != null)
{
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();
Specs = orchestrator.ActiveDevice.Specs;
var logSettings = orchestrator.GetSettings().Log;
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));
}
}
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