Implemented Arduino firmware
Implemented serial connection in Windows application
This commit is contained in:
parent
072e73df88
commit
f15491c541
230
Arduino/MassiveKnob/MassiveKnob.ino
Normal file
230
Arduino/MassiveKnob/MassiveKnob.ino
Normal file
@ -0,0 +1,230 @@
|
||||
/*
|
||||
*
|
||||
* Configuration
|
||||
* Modify these settings according to your hardware design
|
||||
*
|
||||
*/
|
||||
// Set this to the number of potentiometers you have connected
|
||||
const byte KnobCount = 1;
|
||||
|
||||
// For each potentiometer, specify the port
|
||||
const byte KnobPin[KnobCount] = {
|
||||
// A0,
|
||||
A1
|
||||
};
|
||||
|
||||
// Minimum time between reporting changing values, reduces serial traffic
|
||||
const unsigned long MinimumInterval = 50;
|
||||
|
||||
// Alpha value of the Exponential Moving Average (EMA) to reduce noise
|
||||
const float EMAAlpha = 0.6;
|
||||
|
||||
// How many measurements to take at boot time to seed the EMA
|
||||
const byte EMASeedCount = 5;
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
*
|
||||
* Le code
|
||||
* Here be dragons.
|
||||
*
|
||||
*/
|
||||
bool active = false;
|
||||
enum OutputMode {
|
||||
Binary, // Communication with the desktop application
|
||||
PlainText, // Plain text, useful for the Arduino Serial Monitor
|
||||
Plotter // Graph values, for the Arduino Serial Plotter
|
||||
};
|
||||
OutputMode outputMode = Binary;
|
||||
|
||||
byte volume[KnobCount];
|
||||
unsigned long lastChange[KnobCount];
|
||||
int analogReadValue[KnobCount];
|
||||
float emaValue[KnobCount];
|
||||
unsigned long currentTime;
|
||||
unsigned long lastPlot;
|
||||
|
||||
void setup()
|
||||
{
|
||||
Serial.begin(115200);
|
||||
|
||||
// Wait for the Serial port hardware to initialise
|
||||
while (!Serial) {}
|
||||
|
||||
|
||||
// Seed the moving average
|
||||
for (byte knobIndex = 0; knobIndex < KnobCount; knobIndex++)
|
||||
emaValue[knobIndex] = analogRead(KnobPin[knobIndex]);
|
||||
|
||||
for (byte seed = 1; seed < EMASeedCount - 1; seed++)
|
||||
for (byte knobIndex = 0; knobIndex < KnobCount; knobIndex++)
|
||||
getVolume(knobIndex);
|
||||
|
||||
|
||||
// Read the initial values
|
||||
currentTime = millis();
|
||||
for (byte knobIndex = 0; knobIndex < KnobCount; knobIndex++)
|
||||
{
|
||||
volume[knobIndex] = getVolume(knobIndex);
|
||||
lastChange[knobIndex] = currentTime;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void loop()
|
||||
{
|
||||
if (Serial.available())
|
||||
processMessage(Serial.read());
|
||||
|
||||
// Not that due to ADC checking and Serial communication, currentTime will not be
|
||||
// accurate throughout the loop. But since we don't need exact timing for the interval this
|
||||
// is acceptable and saves a few calls to millis.
|
||||
currentTime = millis();
|
||||
|
||||
// Check volume knobs
|
||||
byte newVolume;
|
||||
for (byte knobIndex = 0; knobIndex < KnobCount; knobIndex++)
|
||||
{
|
||||
newVolume = getVolume(knobIndex);
|
||||
|
||||
if (newVolume != volume[knobIndex] && (currentTime - lastChange[knobIndex] >= MinimumInterval))
|
||||
{
|
||||
if (active)
|
||||
// Send out new value
|
||||
outputVolume(knobIndex, newVolume);
|
||||
|
||||
volume[knobIndex] = newVolume;
|
||||
lastChange[knobIndex] = currentTime;
|
||||
}
|
||||
}
|
||||
|
||||
if (outputMode == Plotter && (currentTime - lastPlot) >= 50)
|
||||
{
|
||||
outputPlotter();
|
||||
lastPlot = currentTime;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void processMessage(byte message)
|
||||
{
|
||||
switch (message)
|
||||
{
|
||||
case 'H': // Handshake
|
||||
processHandshakeMessage();
|
||||
break;
|
||||
|
||||
case 'Q': // Quit
|
||||
processQuitMessage();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void processHandshakeMessage()
|
||||
{
|
||||
byte buffer[2];
|
||||
if (Serial.readBytes(buffer, 3) < 3)
|
||||
return;
|
||||
|
||||
if (buffer[0] != 'M' || buffer[1] != 'K')
|
||||
return;
|
||||
|
||||
switch (buffer[2])
|
||||
{
|
||||
case 'B':
|
||||
outputMode = Binary;
|
||||
break;
|
||||
|
||||
case 'P':
|
||||
outputMode = PlainText;
|
||||
break;
|
||||
|
||||
case 'G':
|
||||
outputMode = Plotter;
|
||||
break;
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
switch (outputMode)
|
||||
{
|
||||
case Binary:
|
||||
Serial.write('H');
|
||||
Serial.write(KnobCount);
|
||||
break;
|
||||
|
||||
case PlainText:
|
||||
Serial.print("Hello! I have ");
|
||||
Serial.print(KnobCount);
|
||||
Serial.println(" knobs.");
|
||||
break;
|
||||
}
|
||||
|
||||
active = true;
|
||||
}
|
||||
|
||||
|
||||
void processQuitMessage()
|
||||
{
|
||||
switch (outputMode)
|
||||
{
|
||||
case Binary:
|
||||
case PlainText:
|
||||
Serial.write('Q');
|
||||
break;
|
||||
}
|
||||
|
||||
active = false;
|
||||
}
|
||||
|
||||
|
||||
byte getVolume(byte knobIndex)
|
||||
{
|
||||
analogReadValue[knobIndex] = analogRead(KnobPin[knobIndex]);
|
||||
emaValue[knobIndex] = (EMAAlpha * analogReadValue[knobIndex]) + ((1 - EMAAlpha) * emaValue[knobIndex]);
|
||||
|
||||
return map(emaValue[knobIndex], 0, 1023, 0, 100);
|
||||
}
|
||||
|
||||
|
||||
void outputVolume(byte knobIndex, byte newVolume)
|
||||
{
|
||||
switch (outputMode)
|
||||
{
|
||||
case Binary:
|
||||
Serial.write('V');
|
||||
Serial.write(knobIndex);
|
||||
Serial.write(newVolume);
|
||||
break;
|
||||
|
||||
case PlainText:
|
||||
Serial.print("Volume #");
|
||||
Serial.print(knobIndex);
|
||||
Serial.print(" = ");
|
||||
Serial.println(newVolume);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void outputPlotter()
|
||||
{
|
||||
for (byte i = 0; i < KnobCount; i++)
|
||||
{
|
||||
if (i > 0)
|
||||
Serial.print('\t');
|
||||
|
||||
Serial.print(analogReadValue[i]);
|
||||
Serial.print('\t');
|
||||
Serial.print(emaValue[i]);
|
||||
Serial.print('\t');
|
||||
Serial.print(volume[i]);
|
||||
}
|
||||
|
||||
Serial.println();
|
||||
}
|
@ -6,8 +6,8 @@ Control audio devices using physical knobs.
|
||||
Inspired by [an article on Prusa's blog](https://blog.prusaprinters.org/3d-print-an-oversized-media-control-volume-knob-arduino-basics_30184/), this project has a slightly different set of goals:
|
||||
|
||||
**Must have**
|
||||
1. Control multiple audio devices, one set of physical controls per device
|
||||
2. Volume is set to an absolute value (potentiometer instead of a rotary encoder)
|
||||
1. ✔ Control multiple audio devices, one set of physical controls per device
|
||||
2. ✔ Volume is set to an absolute value (potentiometer instead of a rotary encoder)
|
||||
|
||||
Because of the second requirement, a simple media keys HID device does not suffice and extra software is required on the desktop.
|
||||
|
||||
@ -16,6 +16,8 @@ Because of the second requirement, a simple media keys HID device does not suffi
|
||||
a. by changing the Windows default output device
|
||||
b. by running a VoiceMeeter macro
|
||||
2. Corresponding LEDs to indicate the currently active device
|
||||
3. OSD
|
||||
4. API / plugins to use extra knobs and buttons for other purposes
|
||||
|
||||
## Developing
|
||||
The hardware side uses an Arduino sketch to communicate the hardware state over the serial port.
|
||||
|
1
Windows/Forms/SettingsForm.Designer.cs
generated
1
Windows/Forms/SettingsForm.Designer.cs
generated
@ -185,7 +185,6 @@ namespace MassiveKnob.Forms
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
|
||||
this.Text = "Massive Knob - Settings";
|
||||
this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.SettingsForm_FormClosing);
|
||||
this.FormClosed += new System.Windows.Forms.FormClosedEventHandler(this.SettingsForm_FormClosed);
|
||||
this.NotifyIconMenu.ResumeLayout(false);
|
||||
this.CommunicationGroupbox.ResumeLayout(false);
|
||||
this.CommunicationGroupbox.PerformLayout();
|
||||
|
@ -4,6 +4,8 @@ using System.IO.Ports;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using Dapplo.Windows.Devices;
|
||||
using Dapplo.Windows.Devices.Enums;
|
||||
using MassiveKnob.Hardware;
|
||||
using MassiveKnob.Settings;
|
||||
using MassiveKnob.UserControls;
|
||||
@ -11,13 +13,15 @@ using Nito.AsyncEx;
|
||||
|
||||
namespace MassiveKnob.Forms
|
||||
{
|
||||
public partial class SettingsForm : Form, IMassiveKnobHardwareObserver
|
||||
public partial class SettingsForm : Form, IMassiveKnobHardwareObserver, IObserver<DeviceNotificationEvent>
|
||||
{
|
||||
private readonly IAudioDeviceManager audioDeviceManager;
|
||||
private readonly IMassiveKnobHardwareFactory massiveKnobHardwareFactory;
|
||||
private readonly List<KnobDeviceControl> knobDeviceControls = new List<KnobDeviceControl>();
|
||||
|
||||
private bool loading = true;
|
||||
private string lastConnectedPort = null;
|
||||
private IDisposable deviceSubscription;
|
||||
private IMassiveKnobHardware hardware;
|
||||
private IAudioDevice[] devices;
|
||||
private Settings.Settings settings;
|
||||
@ -35,42 +39,43 @@ namespace MassiveKnob.Forms
|
||||
this.massiveKnobHardwareFactory = massiveKnobHardwareFactory;
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
SerialPortStatusLabel.Text = Strings.StatusNotConnected;
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
await LoadSettings();
|
||||
|
||||
await Task.WhenAll(
|
||||
LoadSerialPorts(),
|
||||
LoadAudioDevices()
|
||||
);
|
||||
|
||||
loading = false;
|
||||
await Connect();
|
||||
}).ContinueWith(t =>
|
||||
{
|
||||
if (t.IsFaulted && t.Exception != null)
|
||||
SafeCall(() => throw t.Exception);
|
||||
});
|
||||
// Due to the form not being visible initially (see SetVisibleCore), we can't use the Load event
|
||||
AsyncLoad();
|
||||
}
|
||||
|
||||
|
||||
private void SafeCall(Action action)
|
||||
|
||||
private async void AsyncLoad()
|
||||
{
|
||||
await LoadSettings();
|
||||
|
||||
await Task.WhenAll(
|
||||
LoadSerialPorts(),
|
||||
LoadAudioDevices()
|
||||
);
|
||||
|
||||
deviceSubscription = DeviceNotification.OnNotification.Subscribe(this);
|
||||
|
||||
loading = false;
|
||||
await Connect();
|
||||
}
|
||||
|
||||
|
||||
private void RunInUIContext(Action action)
|
||||
{
|
||||
if (InvokeRequired)
|
||||
Invoke(action);
|
||||
else
|
||||
action();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private Task LoadSerialPorts()
|
||||
{
|
||||
var portNames = SerialPort.GetPortNames();
|
||||
|
||||
SafeCall(() =>
|
||||
RunInUIContext(() =>
|
||||
{
|
||||
SerialPortCombobox.BeginUpdate();
|
||||
try
|
||||
@ -97,22 +102,19 @@ namespace MassiveKnob.Forms
|
||||
private async Task LoadSettings()
|
||||
{
|
||||
var newSettings = await SettingsJsonSerializer.Deserialize();
|
||||
SafeCall(() => SetSettings(newSettings));
|
||||
RunInUIContext(() => SetSettings(newSettings));
|
||||
}
|
||||
|
||||
|
||||
private void SaveSettings()
|
||||
private async Task SaveSettings()
|
||||
{
|
||||
if (settings == null)
|
||||
return;
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
using (await saveSettingsLock.LockAsync())
|
||||
{
|
||||
await SettingsJsonSerializer.Serialize(settings);
|
||||
}
|
||||
});
|
||||
|
||||
using (await saveSettingsLock.LockAsync())
|
||||
{
|
||||
await SettingsJsonSerializer.Serialize(settings);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -120,9 +122,8 @@ namespace MassiveKnob.Forms
|
||||
{
|
||||
string serialPort = null;
|
||||
|
||||
SafeCall(() =>
|
||||
RunInUIContext(() =>
|
||||
{
|
||||
SerialPortStatusLabel.Text = Strings.StatusConnecting;
|
||||
serialPort = (string)SerialPortCombobox.SelectedItem;
|
||||
});
|
||||
|
||||
@ -145,7 +146,7 @@ namespace MassiveKnob.Forms
|
||||
private async Task LoadAudioDevices()
|
||||
{
|
||||
var newDevices = await audioDeviceManager.GetDevices();
|
||||
SafeCall(() => SetDevices(newDevices));
|
||||
RunInUIContext(() => SetDevices(newDevices));
|
||||
}
|
||||
|
||||
|
||||
@ -158,10 +159,12 @@ namespace MassiveKnob.Forms
|
||||
SerialPortCombobox.SelectedItem = value.SerialPort;
|
||||
|
||||
// No need to update the knob device user controls, as they are not loaded yet
|
||||
// (guaranteed by the order in AsyncLoad)
|
||||
|
||||
settings = value;
|
||||
}
|
||||
|
||||
|
||||
private void SetDevices(IEnumerable<IAudioDevice> value)
|
||||
{
|
||||
devices = value.ToArray();
|
||||
@ -206,13 +209,13 @@ namespace MassiveKnob.Forms
|
||||
if (i < settings.Knobs.Count)
|
||||
knobDeviceControl.SetDeviceId(settings.Knobs[i].DeviceId);
|
||||
|
||||
knobDeviceControl.OnDeviceChanged += (sender, args) =>
|
||||
knobDeviceControl.OnDeviceChanged += async (sender, args) =>
|
||||
{
|
||||
while (settings.Knobs.Count - 1 < args.KnobIndex)
|
||||
settings.Knobs.Add(new Settings.Settings.KnobSettings());
|
||||
|
||||
settings.Knobs[args.KnobIndex].DeviceId = args.DeviceId;
|
||||
SaveSettings();
|
||||
await SaveSettings();
|
||||
};
|
||||
|
||||
knobDeviceControls.Add(knobDeviceControl);
|
||||
@ -241,7 +244,13 @@ namespace MassiveKnob.Forms
|
||||
{
|
||||
// Prevent the form from showing at startup
|
||||
if (!startupVisibleCalled)
|
||||
{
|
||||
startupVisibleCalled = true;
|
||||
|
||||
// Make sure the underlying window is still created, otherwise Close won't work
|
||||
if (!IsHandleCreated)
|
||||
CreateHandle();
|
||||
}
|
||||
else
|
||||
base.SetVisibleCore(value);
|
||||
}
|
||||
@ -253,16 +262,43 @@ namespace MassiveKnob.Forms
|
||||
}
|
||||
|
||||
|
||||
private void Quit()
|
||||
private async Task Quit()
|
||||
{
|
||||
Hide();
|
||||
|
||||
deviceSubscription?.Dispose();
|
||||
|
||||
foreach (var knobDeviceControl in knobDeviceControls)
|
||||
knobDeviceControl.Dispose();
|
||||
|
||||
knobDeviceControls.Clear();
|
||||
|
||||
|
||||
if (hardware != null)
|
||||
{
|
||||
hardware.DetachObserver(this);
|
||||
await hardware.Disconnect();
|
||||
}
|
||||
|
||||
audioDeviceManager?.Dispose();
|
||||
|
||||
closing = true;
|
||||
Close();
|
||||
}
|
||||
|
||||
|
||||
public void Connecting()
|
||||
{
|
||||
RunInUIContext(() =>
|
||||
{
|
||||
SerialPortStatusLabel.Text = Strings.StatusConnecting;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public void Connected(int knobCount)
|
||||
{
|
||||
SafeCall(() =>
|
||||
RunInUIContext(() =>
|
||||
{
|
||||
SerialPortStatusLabel.Text = Strings.StatusConnected;
|
||||
SetKnobCount(knobCount);
|
||||
@ -272,14 +308,15 @@ namespace MassiveKnob.Forms
|
||||
|
||||
public void Disconnected()
|
||||
{
|
||||
SafeCall(() =>
|
||||
RunInUIContext(() =>
|
||||
{
|
||||
SerialPortStatusLabel.Text = Strings.StatusNotConnected;
|
||||
lastConnectedPort = null;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public void VolumeChanged(int knob, int volume)
|
||||
public async void VolumeChanged(int knob, int volume)
|
||||
{
|
||||
if (knob >= settings.Knobs.Count)
|
||||
return;
|
||||
@ -289,15 +326,12 @@ namespace MassiveKnob.Forms
|
||||
|
||||
var deviceId = settings.Knobs[knob].DeviceId.Value;
|
||||
|
||||
Task.Run(async () =>
|
||||
using (await setVolumeLock.LockAsync())
|
||||
{
|
||||
using (await setVolumeLock.LockAsync())
|
||||
{
|
||||
var device = await audioDeviceManager.GetDeviceById(deviceId);
|
||||
if (device != null)
|
||||
await device.SetVolume(volume);
|
||||
}
|
||||
});
|
||||
var device = await audioDeviceManager.GetDeviceById(deviceId);
|
||||
if (device != null)
|
||||
await device.SetVolume(volume);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -310,20 +344,6 @@ namespace MassiveKnob.Forms
|
||||
e.Cancel = true;
|
||||
}
|
||||
|
||||
|
||||
private void SettingsForm_FormClosed(object sender, FormClosedEventArgs e)
|
||||
{
|
||||
foreach (var knobDeviceControl in knobDeviceControls)
|
||||
knobDeviceControl.Dispose();
|
||||
|
||||
knobDeviceControls.Clear();
|
||||
|
||||
|
||||
hardware?.DetachObserver(this);
|
||||
hardware?.Disconnect().GetAwaiter().GetResult();
|
||||
audioDeviceManager?.Dispose();
|
||||
}
|
||||
|
||||
|
||||
private void NotifyIcon_DoubleClick(object sender, EventArgs e)
|
||||
{
|
||||
@ -331,9 +351,9 @@ namespace MassiveKnob.Forms
|
||||
}
|
||||
|
||||
|
||||
private void QuitToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
private async void QuitToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
Quit();
|
||||
await Quit();
|
||||
}
|
||||
|
||||
|
||||
@ -349,15 +369,39 @@ namespace MassiveKnob.Forms
|
||||
}
|
||||
|
||||
|
||||
private void SerialPortCombobox_SelectedIndexChanged(object sender, EventArgs e)
|
||||
private async void SerialPortCombobox_SelectedIndexChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (loading || (string)SerialPortCombobox.SelectedItem == settings.SerialPort)
|
||||
var newPort = (string) SerialPortCombobox.SelectedItem;
|
||||
if (loading || newPort == lastConnectedPort)
|
||||
return;
|
||||
|
||||
settings.SerialPort = (string) SerialPortCombobox.SelectedItem;
|
||||
SaveSettings();
|
||||
|
||||
Task.Run(Connect);
|
||||
lastConnectedPort = newPort;
|
||||
if (settings.SerialPort != newPort)
|
||||
{
|
||||
settings.SerialPort = (string) SerialPortCombobox.SelectedItem;
|
||||
await SaveSettings();
|
||||
}
|
||||
|
||||
await Connect();
|
||||
}
|
||||
|
||||
|
||||
public async void OnNext(DeviceNotificationEvent value)
|
||||
{
|
||||
if ((value.EventType == DeviceChangeEvent.DeviceArrival ||
|
||||
value.EventType == DeviceChangeEvent.DeviceRemoveComplete) &&
|
||||
value.Is(DeviceBroadcastDeviceType.DeviceInterface))
|
||||
{
|
||||
await LoadSerialPorts();
|
||||
}
|
||||
}
|
||||
|
||||
public void OnError(Exception error)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnCompleted()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -36,8 +36,15 @@ namespace MassiveKnob.Hardware
|
||||
{
|
||||
observers.Remove(observer);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void Connecting()
|
||||
{
|
||||
foreach (var observer in observers)
|
||||
observer.Connecting();
|
||||
}
|
||||
|
||||
|
||||
public void Connected(int knobCount)
|
||||
{
|
||||
foreach (var observer in observers)
|
||||
|
@ -4,6 +4,7 @@ namespace MassiveKnob.Hardware
|
||||
{
|
||||
public interface IMassiveKnobHardwareObserver
|
||||
{
|
||||
void Connecting();
|
||||
void Connected(int knobCount);
|
||||
void Disconnected();
|
||||
|
||||
@ -25,6 +26,6 @@ namespace MassiveKnob.Hardware
|
||||
|
||||
public interface IMassiveKnobHardwareFactory
|
||||
{
|
||||
IMassiveKnobHardware Create(string serialPort);
|
||||
IMassiveKnobHardware Create(string portName);
|
||||
}
|
||||
}
|
||||
|
@ -48,6 +48,7 @@ namespace MassiveKnob.Hardware
|
||||
}
|
||||
|
||||
|
||||
// ReSharper disable once UnusedMember.Global - for testing purposes only
|
||||
public class MockMassiveKnobHardwareFactory : IMassiveKnobHardwareFactory
|
||||
{
|
||||
private readonly int knobCount;
|
||||
@ -62,7 +63,7 @@ namespace MassiveKnob.Hardware
|
||||
}
|
||||
|
||||
|
||||
public IMassiveKnobHardware Create(string serialPort)
|
||||
public IMassiveKnobHardware Create(string portName)
|
||||
{
|
||||
return new MockMassiveKnobHardware(knobCount, volumeChangeInterval, maxVolume);
|
||||
}
|
||||
|
180
Windows/Hardware/SerialMassiveKnobHardware.cs
Normal file
180
Windows/Hardware/SerialMassiveKnobHardware.cs
Normal file
@ -0,0 +1,180 @@
|
||||
using System.Diagnostics;
|
||||
using System.IO.Ports;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MassiveKnob.Hardware
|
||||
{
|
||||
public class SerialMassiveKnobHardware : AbstractMassiveKnobHardware
|
||||
{
|
||||
private readonly string portName;
|
||||
private Thread workerThread;
|
||||
|
||||
private readonly CancellationTokenSource workerThreadCancellation = new CancellationTokenSource();
|
||||
private readonly TaskCompletionSource<bool> workerThreadCompleted = new TaskCompletionSource<bool>();
|
||||
|
||||
|
||||
public SerialMassiveKnobHardware(string portName)
|
||||
{
|
||||
this.portName = portName;
|
||||
}
|
||||
|
||||
|
||||
public override async Task TryConnect()
|
||||
{
|
||||
if (workerThread != null)
|
||||
await Disconnect();
|
||||
|
||||
workerThread = new Thread(RunWorker)
|
||||
{
|
||||
Name = "SerialMassiveKnobHardware Worker"
|
||||
};
|
||||
workerThread.Start();
|
||||
}
|
||||
|
||||
|
||||
public override async Task Disconnect()
|
||||
{
|
||||
workerThreadCancellation.Cancel();
|
||||
await workerThreadCompleted.Task;
|
||||
|
||||
workerThread = null;
|
||||
}
|
||||
|
||||
|
||||
private void RunWorker()
|
||||
{
|
||||
Observers.Connecting();
|
||||
|
||||
while (!workerThreadCancellation.IsCancellationRequested)
|
||||
{
|
||||
SerialPort serialPort = null;
|
||||
|
||||
void SafeCloseSerialPort()
|
||||
{
|
||||
try
|
||||
{
|
||||
serialPort?.Dispose();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignroed
|
||||
}
|
||||
|
||||
serialPort = null;
|
||||
Observers.Disconnected();
|
||||
Observers.Connecting();
|
||||
}
|
||||
|
||||
|
||||
var knobCount = 0;
|
||||
|
||||
while (serialPort == null && !workerThreadCancellation.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
serialPort = new SerialPort(portName, 115200);
|
||||
serialPort.Open();
|
||||
|
||||
// Send handshake
|
||||
serialPort.Write(new[] { 'H', 'M', 'K', 'B' }, 0, 4);
|
||||
|
||||
// Wait for reply
|
||||
serialPort.ReadTimeout = 1000;
|
||||
var response = serialPort.ReadByte();
|
||||
|
||||
if ((char) response == 'H')
|
||||
{
|
||||
knobCount = serialPort.ReadByte();
|
||||
if (knobCount > -1)
|
||||
break;
|
||||
}
|
||||
|
||||
SafeCloseSerialPort();
|
||||
Thread.Sleep(500);
|
||||
}
|
||||
catch
|
||||
{
|
||||
SafeCloseSerialPort();
|
||||
Thread.Sleep(500);
|
||||
}
|
||||
}
|
||||
|
||||
if (workerThreadCancellation.IsCancellationRequested)
|
||||
{
|
||||
SafeCloseSerialPort();
|
||||
break;
|
||||
}
|
||||
|
||||
var processingMessage = false;
|
||||
|
||||
Debug.Assert(serialPort != null, nameof(serialPort) + " != null");
|
||||
serialPort.DataReceived += (sender, args) =>
|
||||
{
|
||||
if (args.EventType != SerialData.Chars || processingMessage)
|
||||
return;
|
||||
|
||||
var senderPort = (SerialPort) sender;
|
||||
processingMessage = true;
|
||||
try
|
||||
{
|
||||
var message = (char) senderPort.ReadByte();
|
||||
ProcessMessage(senderPort, message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
processingMessage = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Observers.Connected(knobCount);
|
||||
try
|
||||
{
|
||||
// This is where sending data to the hardware would be implemented
|
||||
while (serialPort.IsOpen && !workerThreadCancellation.IsCancellationRequested)
|
||||
{
|
||||
Thread.Sleep(10);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
|
||||
Observers.Disconnected();
|
||||
SafeCloseSerialPort();
|
||||
|
||||
if (!workerThreadCancellation.IsCancellationRequested)
|
||||
Thread.Sleep(500);
|
||||
}
|
||||
|
||||
workerThreadCompleted.TrySetResult(true);
|
||||
}
|
||||
|
||||
|
||||
private void ProcessMessage(SerialPort serialPort, char message)
|
||||
{
|
||||
switch (message)
|
||||
{
|
||||
case 'V':
|
||||
var knobIndex = (byte)serialPort.ReadByte();
|
||||
var volume = (byte)serialPort.ReadByte();
|
||||
|
||||
if (knobIndex < 255 && volume <= 100)
|
||||
Observers.VolumeChanged(knobIndex, volume);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class SerialMassiveKnobHardwareFactory : IMassiveKnobHardwareFactory
|
||||
{
|
||||
public IMassiveKnobHardware Create(string portName)
|
||||
{
|
||||
return new SerialMassiveKnobHardware(portName);
|
||||
}
|
||||
}
|
||||
}
|
@ -65,6 +65,7 @@
|
||||
<Compile Include="Hardware\IAudioDeviceManager.cs" />
|
||||
<Compile Include="Hardware\IMassiveKnobHardware.cs" />
|
||||
<Compile Include="Hardware\MockMassiveKnobHardware.cs" />
|
||||
<Compile Include="Hardware\SerialMassiveKnobHardware.cs" />
|
||||
<Compile Include="Program.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Settings\Settings.cs" />
|
||||
@ -116,6 +117,12 @@
|
||||
<PackageReference Include="AudioSwitcher.AudioApi.CoreAudio">
|
||||
<Version>4.0.0-alpha5</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Dapplo.Windows.Devices">
|
||||
<Version>0.11.24</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Dapplo.Windows.Messages">
|
||||
<Version>0.11.24</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Newtonsoft.Json">
|
||||
<Version>12.0.3</Version>
|
||||
</PackageReference>
|
||||
|
2
Windows/MassiveKnob.sln.DotSettings
Normal file
2
Windows/MassiveKnob.sln.DotSettings
Normal file
@ -0,0 +1,2 @@
|
||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=UI/@EntryIndexedValue">UI</s:String></wpf:ResourceDictionary>
|
@ -35,7 +35,8 @@ namespace MassiveKnob
|
||||
container.Register<IAudioDeviceManagerFactory, CoreAudioDeviceManagerFactory>();
|
||||
|
||||
// For testing without the hardware:
|
||||
container.Register<IMassiveKnobHardwareFactory>(() => new MockMassiveKnobHardwareFactory(3, TimeSpan.FromSeconds(1), 25));
|
||||
container.Register<IMassiveKnobHardwareFactory, SerialMassiveKnobHardwareFactory>();
|
||||
//container.Register<IMassiveKnobHardwareFactory>(() => new MockMassiveKnobHardwareFactory(3, TimeSpan.FromSeconds(1), 25));
|
||||
|
||||
return container;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user