1
0
mirror of synced 2024-06-29 02:47:39 +00:00
MassiveKnob/Windows/MassiveKnob/Core/MassiveKnobOrchestrator.cs
Mark van Renswoude 197aef531a 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
2021-03-05 11:47:12 +01:00

677 lines
22 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Subjects;
using MassiveKnob.Helpers;
using MassiveKnob.Plugin;
using MassiveKnob.Settings;
using Newtonsoft.Json.Linq;
using Serilog.Extensions.Logging;
using ILogger = Serilog.ILogger;
namespace MassiveKnob.Core
{
public class MassiveKnobOrchestrator : IMassiveKnobOrchestrator
{
private readonly IPluginManager pluginManager;
private readonly ILogger logger;
private readonly object settingsLock = new object();
private MassiveKnobSettings massiveKnobSettings;
private readonly SerialQueue flushSettingsQueue = new SerialQueue();
private MassiveKnobDeviceInfo activeDevice;
private readonly Subject<MassiveKnobDeviceInfo> activeDeviceInfoSubject = new Subject<MassiveKnobDeviceInfo>();
private IMassiveKnobDeviceContext activeDeviceContext;
private readonly List<ActionMapping> analogInputs = new List<ActionMapping>();
private readonly List<ActionMapping> digitalInputs = new List<ActionMapping>();
private readonly List<ActionMapping> analogOutputs = new List<ActionMapping>();
private readonly List<ActionMapping> digitalOutputs = new List<ActionMapping>();
private readonly Dictionary<int, byte> analogOutputValues = new Dictionary<int, byte>();
private readonly Dictionary<int, bool> digitalOutputValues = new Dictionary<int, bool>();
public MassiveKnobDeviceInfo ActiveDevice
{
get => activeDevice;
private set
{
if (value == activeDevice)
return;
activeDevice = value;
activeDeviceInfoSubject.OnNext(activeDevice);
}
}
public IObservable<MassiveKnobDeviceInfo> ActiveDeviceSubject => activeDeviceInfoSubject;
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)
{
foreach (var mapping in mappings)
mapping?.ActionInfo.Instance?.Dispose();
mappings.Clear();
}
lock (settingsLock)
{
DisposeMappings(analogInputs);
DisposeMappings(digitalInputs);
DisposeMappings(analogOutputs);
DisposeMappings(digitalOutputs);
}
activeDeviceInfoSubject?.Dispose();
}
public void Load()
{
lock (settingsLock)
{
if (massiveKnobSettings.Device == null)
return;
var allDevices = pluginManager.GetDevicePlugins().SelectMany(dp => dp.Devices);
var device = allDevices.FirstOrDefault(d => d.DeviceId == massiveKnobSettings.Device.DeviceId);
InternalSetActiveDevice(device, false);
}
}
MassiveKnobDeviceInfo IMassiveKnobOrchestrator.ActiveDevice => activeDevice;
public MassiveKnobDeviceInfo SetActiveDevice(IMassiveKnobDevice device)
{
return InternalSetActiveDevice(device, true);
}
public MassiveKnobActionInfo GetAction(MassiveKnobActionType actionType, int index)
{
lock (settingsLock)
{
var list = GetActionMappingList(actionType);
return index >= list.Count ? null : list[index]?.ActionInfo;
}
}
public MassiveKnobActionInfo SetAction(MassiveKnobActionType actionType, int index, IMassiveKnobAction action)
{
List<ActionMapping> list;
lock (settingsLock)
{
list = GetActionMappingList(actionType);
if (index >= list.Count)
return null;
if (list[index]?.ActionInfo.Info == action)
return list[index].ActionInfo;
list[index]?.ActionInfo.Instance?.Dispose();
var settingsList = GetActionSettingsList(actionType);
while (index >= settingsList.Count)
settingsList.Add(null);
settingsList[index] = action == null ? null : new MassiveKnobSettings.ActionSettings
{
ActionId = action.ActionId,
Settings = null
};
}
FlushSettings();
Action initializeAfterRegistration = null;
var mapping = CreateActionMapping(action, index, (actionInstance, actionContext) =>
{
initializeAfterRegistration = () => actionInstance.Initialize(actionContext);
});
lock (settingsLock)
{
list[index] = mapping;
}
initializeAfterRegistration?.Invoke();
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)
{
if (device == ActiveDevice?.Info)
return ActiveDevice;
if (resetSettings)
{
lock (settingsLock)
{
if (device == null)
massiveKnobSettings.Device = null;
else
{
massiveKnobSettings.Device = new MassiveKnobSettings.DeviceSettings
{
DeviceId = device.DeviceId,
Settings = null
};
}
}
FlushSettings();
}
ActiveDevice?.Instance.Dispose();
if (device != 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);
instance.Initialize(activeDeviceContext);
}
else
{
ActiveDevice = null;
activeDeviceContext = null;
}
return ActiveDevice;
}
protected T GetDeviceSettings<T>(IMassiveKnobDeviceContext context) where T : class, new()
{
if (context != activeDeviceContext)
throw new InvalidOperationException("Caller must be the active device to retrieve the massiveKnobSettings");
lock (settingsLock)
{
return massiveKnobSettings.Device.Settings?.ToObject<T>() ?? new T();
}
}
protected void SetDeviceSettings<T>(IMassiveKnobDeviceContext context, IMassiveKnobDevice device, T deviceSettings) where T : class, new()
{
if (context != activeDeviceContext)
throw new InvalidOperationException("Caller must be the active device to update the massiveKnobSettings");
lock (settingsLock)
{
if (massiveKnobSettings.Device == null)
massiveKnobSettings.Device = new MassiveKnobSettings.DeviceSettings
{
DeviceId = device.DeviceId
};
massiveKnobSettings.Device.Settings = JObject.FromObject(deviceSettings);
}
FlushSettings();
}
protected T GetActionSettings<T>(IMassiveKnobActionContext context, IMassiveKnobAction action, int index) where T : class, new()
{
lock (settingsLock)
{
var list = GetActionMappingList(action.ActionType);
if (index >= list.Count)
return new T();
if (list[index]?.Context != context)
throw new InvalidOperationException("Caller must be the active action to retrieve the massiveKnobSettings");
var settingsList = GetActionSettingsList(action.ActionType);
if (index >= settingsList.Count)
return new T();
return settingsList[index].Settings?.ToObject<T>() ?? new T();
}
}
protected void SetActionSettings<T>(IMassiveKnobActionContext context, IMassiveKnobAction action, int index, T actionSettings) where T : class, new()
{
lock (settingsLock)
{
var list = GetActionMappingList(action.ActionType);
if (index >= list.Count)
return;
if (list[index]?.Context != context)
throw new InvalidOperationException("Caller must be the active action to retrieve the massiveKnobSettings");
var settingsList = GetActionSettingsList(action.ActionType);
while (index >= settingsList.Count)
settingsList.Add(null);
if (settingsList[index] == null)
settingsList[index] = new MassiveKnobSettings.ActionSettings
{
ActionId = action.ActionId
};
settingsList[index].Settings = JObject.FromObject(actionSettings);
}
FlushSettings();
}
protected void AnalogChanged(IMassiveKnobDeviceContext context, int analogInputIndex, byte value)
{
if (context != activeDeviceContext)
return;
IMassiveKnobAnalogAction analogAction;
lock (settingsLock)
{
if (analogOutputValues.TryGetValue(analogInputIndex, out var currentValue) && currentValue == value)
return;
analogOutputValues[analogInputIndex] = value;
var mapping = GetActionMappingList(MassiveKnobActionType.InputAnalog);
if (mapping == null || analogInputIndex >= mapping.Count || mapping[analogInputIndex] == null)
return;
analogAction = mapping[analogInputIndex].ActionInfo.Instance as IMassiveKnobAnalogAction;
}
analogAction?.AnalogChanged(value);
}
protected void DigitalChanged(IMassiveKnobDeviceContext context, int digitalInputIndex, bool on)
{
if (context != activeDeviceContext)
return;
IMassiveKnobDigitalAction digitalAction;
lock (settingsLock)
{
if (digitalOutputValues.TryGetValue(digitalInputIndex, out var currentValue) && currentValue == on)
return;
digitalOutputValues[digitalInputIndex] = on;
var mapping = GetActionMappingList(MassiveKnobActionType.InputDigital);
if (mapping == null || digitalInputIndex >= mapping.Count || mapping[digitalInputIndex] == null)
return;
digitalAction = mapping[digitalInputIndex].ActionInfo.Instance as IMassiveKnobDigitalAction;
}
digitalAction?.DigitalChanged(on);
}
public void SetAnalogOutput(IMassiveKnobActionContext context, int index, byte value)
{
if (activeDevice == null)
return;
IMassiveKnobDeviceInstance deviceInstance;
lock (settingsLock)
{
var list = GetActionMappingList(MassiveKnobActionType.OutputAnalog);
if (index >= list.Count)
return;
if (context != null && list[index]?.Context != context)
return;
deviceInstance = activeDevice.Instance;
}
deviceInstance.SetAnalogOutput(index, value);
}
public void SetDigitalOutput(IMassiveKnobActionContext context, int index, bool on)
{
if (activeDevice == null)
return;
IMassiveKnobDeviceInstance deviceInstance;
lock (settingsLock)
{
var list = GetActionMappingList(MassiveKnobActionType.OutputDigital);
if (index >= list.Count)
return;
if (context != null && list[index]?.Context != context)
return;
deviceInstance = activeDevice.Instance;
}
deviceInstance.SetDigitalOutput(index, on);
}
private List<ActionMapping> GetActionMappingList(MassiveKnobActionType actionType)
{
switch (actionType)
{
case MassiveKnobActionType.InputAnalog:
return analogInputs;
case MassiveKnobActionType.InputDigital:
return digitalInputs;
case MassiveKnobActionType.OutputAnalog:
return analogOutputs;
case MassiveKnobActionType.OutputDigital:
return digitalOutputs;
default:
throw new ArgumentOutOfRangeException(nameof(actionType), actionType, null);
}
}
private List<MassiveKnobSettings.ActionSettings> GetActionSettingsList(MassiveKnobActionType actionType)
{
switch (actionType)
{
case MassiveKnobActionType.InputAnalog:
return massiveKnobSettings.AnalogInput;
case MassiveKnobActionType.InputDigital:
return massiveKnobSettings.DigitalInput;
case MassiveKnobActionType.OutputAnalog:
return massiveKnobSettings.AnalogOutput;
case MassiveKnobActionType.OutputDigital:
return massiveKnobSettings.DigitalOutput;
default:
throw new ArgumentOutOfRangeException(nameof(actionType), actionType, null);
}
}
private void FlushSettings()
{
MassiveKnobSettings massiveKnobSettingsSnapshot;
lock (settingsLock)
{
massiveKnobSettingsSnapshot = massiveKnobSettings.Clone();
}
flushSettingsQueue.Enqueue(async () =>
{
await MassiveKnobSettingsJsonSerializer.Serialize(massiveKnobSettingsSnapshot);
});
}
protected void UpdateActiveDeviceSpecs(IMassiveKnobDeviceContext context, DeviceSpecs specs)
{
if (context != activeDeviceContext)
return;
var delayedInitializeActions = new List<Action>();
void DelayedInitialize(IMassiveKnobActionInstance instance, IMassiveKnobActionContext instanceContext)
{
delayedInitializeActions.Add(() =>
{
instance.Initialize(instanceContext);
});
}
lock (settingsLock)
{
UpdateMapping(analogInputs, specs.AnalogInputCount, 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)
delayedInitializeAction();
ActiveDevice = new MassiveKnobDeviceInfo(
ActiveDevice.Info,
ActiveDevice.Instance,
specs);
// Send out all cached values to initialize the device's outputs
foreach (var pair in analogOutputValues.Where(pair => pair.Key < specs.AnalogOutputCount))
SetAnalogOutput(null, pair.Key, pair.Value);
foreach (var pair in digitalOutputValues.Where(pair => pair.Key < specs.DigitalOutputCount))
SetDigitalOutput(null, pair.Key, pair.Value);
}
private void UpdateMapping(List<ActionMapping> mapping, int newCount, List<MassiveKnobSettings.ActionSettings> actionSettings, Action<IMassiveKnobActionInstance, IMassiveKnobActionContext> initializeOutsideLock)
{
if (mapping.Count > newCount)
{
for (var actionIndex = newCount; actionIndex < mapping.Count; actionIndex++)
mapping[actionIndex]?.ActionInfo.Instance?.Dispose();
mapping.RemoveRange(newCount, mapping.Count - newCount);
}
if (actionSettings.Count > newCount)
actionSettings.RemoveRange(newCount, actionSettings.Count - newCount);
if (mapping.Count >= newCount) return;
{
var allActions = pluginManager.GetActionPlugins().SelectMany(ap => ap.Actions).ToArray();
for (var actionIndex = mapping.Count; actionIndex < newCount; actionIndex++)
{
if (actionIndex < actionSettings.Count && actionSettings[actionIndex] != null)
{
var action = allActions.FirstOrDefault(d => d.ActionId == actionSettings[actionIndex].ActionId);
mapping.Add(CreateActionMapping(action, actionIndex, initializeOutsideLock));
}
else
mapping.Add(null);
}
}
}
private ActionMapping CreateActionMapping(IMassiveKnobAction action, int index, Action<IMassiveKnobActionInstance, IMassiveKnobActionContext> initialize)
{
if (action == null)
return null;
var 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);
var mapping = new ActionMapping(new MassiveKnobActionInfo(action, instance), context);
initialize(instance, context);
return mapping;
}
private class ActionMapping
{
public MassiveKnobActionInfo ActionInfo { get; }
public IMassiveKnobActionContext Context { get; }
public ActionMapping(MassiveKnobActionInfo actionInfo, IMassiveKnobActionContext context)
{
ActionInfo = actionInfo;
Context = context;
}
}
private class DeviceContext : IMassiveKnobDeviceContext
{
private readonly MassiveKnobOrchestrator owner;
private readonly IMassiveKnobDevice device;
public DeviceContext(MassiveKnobOrchestrator owner, IMassiveKnobDevice device)
{
this.owner = owner;
this.device = device;
}
public T GetSettings<T>() where T : class, new()
{
return owner.GetDeviceSettings<T>(this);
}
public void SetSettings<T>(T settings) where T : class, new()
{
owner.SetDeviceSettings(this, device, settings);
}
public void Connecting()
{
// TODO (should have) update status ?
}
public void Connected(DeviceSpecs specs)
{
// TODO (should have) update status ?
owner.UpdateActiveDeviceSpecs(this, specs);
}
public void Disconnected()
{
// TODO (should have) update status ?
}
public void AnalogChanged(int analogInputIndex, byte value)
{
owner.AnalogChanged(this, analogInputIndex, value);
}
public void DigitalChanged(int digitalInputIndex, bool on)
{
owner.DigitalChanged(this, digitalInputIndex, on);
}
}
private class ActionContext : IMassiveKnobActionContext
{
private readonly MassiveKnobOrchestrator owner;
private readonly IMassiveKnobAction action;
private readonly int index;
public ActionContext(MassiveKnobOrchestrator owner, IMassiveKnobAction action, int index)
{
this.owner = owner;
this.action = action;
this.index = index;
}
public T GetSettings<T>() where T : class, new()
{
return owner.GetActionSettings<T>(this, action, index);
}
public void SetSettings<T>(T settings) where T : class, new()
{
owner.SetActionSettings(this, action, index, settings);
}
public void SetAnalogOutput(byte value)
{
owner.SetAnalogOutput(this, index, value);
}
public void SetDigitalOutput(bool on)
{
owner.SetDigitalOutput(this, index, on);
}
}
}
}