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:
|
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**
|
**Must have**
|
||||||
1. Control multiple audio devices, one set of physical controls per device
|
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)
|
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.
|
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
|
a. by changing the Windows default output device
|
||||||
b. by running a VoiceMeeter macro
|
b. by running a VoiceMeeter macro
|
||||||
2. Corresponding LEDs to indicate the currently active device
|
2. Corresponding LEDs to indicate the currently active device
|
||||||
|
3. OSD
|
||||||
|
4. API / plugins to use extra knobs and buttons for other purposes
|
||||||
|
|
||||||
## Developing
|
## Developing
|
||||||
The hardware side uses an Arduino sketch to communicate the hardware state over the serial port.
|
The hardware side uses an Arduino sketch to communicate the hardware state over the serial port.
|
||||||
|
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.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
|
||||||
this.Text = "Massive Knob - Settings";
|
this.Text = "Massive Knob - Settings";
|
||||||
this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.SettingsForm_FormClosing);
|
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.NotifyIconMenu.ResumeLayout(false);
|
||||||
this.CommunicationGroupbox.ResumeLayout(false);
|
this.CommunicationGroupbox.ResumeLayout(false);
|
||||||
this.CommunicationGroupbox.PerformLayout();
|
this.CommunicationGroupbox.PerformLayout();
|
||||||
|
@ -4,6 +4,8 @@ using System.IO.Ports;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
|
using Dapplo.Windows.Devices;
|
||||||
|
using Dapplo.Windows.Devices.Enums;
|
||||||
using MassiveKnob.Hardware;
|
using MassiveKnob.Hardware;
|
||||||
using MassiveKnob.Settings;
|
using MassiveKnob.Settings;
|
||||||
using MassiveKnob.UserControls;
|
using MassiveKnob.UserControls;
|
||||||
@ -11,13 +13,15 @@ using Nito.AsyncEx;
|
|||||||
|
|
||||||
namespace MassiveKnob.Forms
|
namespace MassiveKnob.Forms
|
||||||
{
|
{
|
||||||
public partial class SettingsForm : Form, IMassiveKnobHardwareObserver
|
public partial class SettingsForm : Form, IMassiveKnobHardwareObserver, IObserver<DeviceNotificationEvent>
|
||||||
{
|
{
|
||||||
private readonly IAudioDeviceManager audioDeviceManager;
|
private readonly IAudioDeviceManager audioDeviceManager;
|
||||||
private readonly IMassiveKnobHardwareFactory massiveKnobHardwareFactory;
|
private readonly IMassiveKnobHardwareFactory massiveKnobHardwareFactory;
|
||||||
private readonly List<KnobDeviceControl> knobDeviceControls = new List<KnobDeviceControl>();
|
private readonly List<KnobDeviceControl> knobDeviceControls = new List<KnobDeviceControl>();
|
||||||
|
|
||||||
private bool loading = true;
|
private bool loading = true;
|
||||||
|
private string lastConnectedPort = null;
|
||||||
|
private IDisposable deviceSubscription;
|
||||||
private IMassiveKnobHardware hardware;
|
private IMassiveKnobHardware hardware;
|
||||||
private IAudioDevice[] devices;
|
private IAudioDevice[] devices;
|
||||||
private Settings.Settings settings;
|
private Settings.Settings settings;
|
||||||
@ -35,42 +39,43 @@ namespace MassiveKnob.Forms
|
|||||||
this.massiveKnobHardwareFactory = massiveKnobHardwareFactory;
|
this.massiveKnobHardwareFactory = massiveKnobHardwareFactory;
|
||||||
|
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
SerialPortStatusLabel.Text = Strings.StatusNotConnected;
|
SerialPortStatusLabel.Text = Strings.StatusNotConnected;
|
||||||
|
|
||||||
Task.Run(async () =>
|
// Due to the form not being visible initially (see SetVisibleCore), we can't use the Load event
|
||||||
{
|
AsyncLoad();
|
||||||
await LoadSettings();
|
|
||||||
|
|
||||||
await Task.WhenAll(
|
|
||||||
LoadSerialPorts(),
|
|
||||||
LoadAudioDevices()
|
|
||||||
);
|
|
||||||
|
|
||||||
loading = false;
|
|
||||||
await Connect();
|
|
||||||
}).ContinueWith(t =>
|
|
||||||
{
|
|
||||||
if (t.IsFaulted && t.Exception != null)
|
|
||||||
SafeCall(() => throw t.Exception);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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)
|
if (InvokeRequired)
|
||||||
Invoke(action);
|
Invoke(action);
|
||||||
else
|
else
|
||||||
action();
|
action();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private Task LoadSerialPorts()
|
private Task LoadSerialPorts()
|
||||||
{
|
{
|
||||||
var portNames = SerialPort.GetPortNames();
|
var portNames = SerialPort.GetPortNames();
|
||||||
|
|
||||||
SafeCall(() =>
|
RunInUIContext(() =>
|
||||||
{
|
{
|
||||||
SerialPortCombobox.BeginUpdate();
|
SerialPortCombobox.BeginUpdate();
|
||||||
try
|
try
|
||||||
@ -97,22 +102,19 @@ namespace MassiveKnob.Forms
|
|||||||
private async Task LoadSettings()
|
private async Task LoadSettings()
|
||||||
{
|
{
|
||||||
var newSettings = await SettingsJsonSerializer.Deserialize();
|
var newSettings = await SettingsJsonSerializer.Deserialize();
|
||||||
SafeCall(() => SetSettings(newSettings));
|
RunInUIContext(() => SetSettings(newSettings));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void SaveSettings()
|
private async Task SaveSettings()
|
||||||
{
|
{
|
||||||
if (settings == null)
|
if (settings == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Task.Run(async () =>
|
using (await saveSettingsLock.LockAsync())
|
||||||
{
|
{
|
||||||
using (await saveSettingsLock.LockAsync())
|
await SettingsJsonSerializer.Serialize(settings);
|
||||||
{
|
}
|
||||||
await SettingsJsonSerializer.Serialize(settings);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -120,9 +122,8 @@ namespace MassiveKnob.Forms
|
|||||||
{
|
{
|
||||||
string serialPort = null;
|
string serialPort = null;
|
||||||
|
|
||||||
SafeCall(() =>
|
RunInUIContext(() =>
|
||||||
{
|
{
|
||||||
SerialPortStatusLabel.Text = Strings.StatusConnecting;
|
|
||||||
serialPort = (string)SerialPortCombobox.SelectedItem;
|
serialPort = (string)SerialPortCombobox.SelectedItem;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -145,7 +146,7 @@ namespace MassiveKnob.Forms
|
|||||||
private async Task LoadAudioDevices()
|
private async Task LoadAudioDevices()
|
||||||
{
|
{
|
||||||
var newDevices = await audioDeviceManager.GetDevices();
|
var newDevices = await audioDeviceManager.GetDevices();
|
||||||
SafeCall(() => SetDevices(newDevices));
|
RunInUIContext(() => SetDevices(newDevices));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -158,10 +159,12 @@ namespace MassiveKnob.Forms
|
|||||||
SerialPortCombobox.SelectedItem = value.SerialPort;
|
SerialPortCombobox.SelectedItem = value.SerialPort;
|
||||||
|
|
||||||
// No need to update the knob device user controls, as they are not loaded yet
|
// No need to update the knob device user controls, as they are not loaded yet
|
||||||
|
// (guaranteed by the order in AsyncLoad)
|
||||||
|
|
||||||
settings = value;
|
settings = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void SetDevices(IEnumerable<IAudioDevice> value)
|
private void SetDevices(IEnumerable<IAudioDevice> value)
|
||||||
{
|
{
|
||||||
devices = value.ToArray();
|
devices = value.ToArray();
|
||||||
@ -206,13 +209,13 @@ namespace MassiveKnob.Forms
|
|||||||
if (i < settings.Knobs.Count)
|
if (i < settings.Knobs.Count)
|
||||||
knobDeviceControl.SetDeviceId(settings.Knobs[i].DeviceId);
|
knobDeviceControl.SetDeviceId(settings.Knobs[i].DeviceId);
|
||||||
|
|
||||||
knobDeviceControl.OnDeviceChanged += (sender, args) =>
|
knobDeviceControl.OnDeviceChanged += async (sender, args) =>
|
||||||
{
|
{
|
||||||
while (settings.Knobs.Count - 1 < args.KnobIndex)
|
while (settings.Knobs.Count - 1 < args.KnobIndex)
|
||||||
settings.Knobs.Add(new Settings.Settings.KnobSettings());
|
settings.Knobs.Add(new Settings.Settings.KnobSettings());
|
||||||
|
|
||||||
settings.Knobs[args.KnobIndex].DeviceId = args.DeviceId;
|
settings.Knobs[args.KnobIndex].DeviceId = args.DeviceId;
|
||||||
SaveSettings();
|
await SaveSettings();
|
||||||
};
|
};
|
||||||
|
|
||||||
knobDeviceControls.Add(knobDeviceControl);
|
knobDeviceControls.Add(knobDeviceControl);
|
||||||
@ -241,7 +244,13 @@ namespace MassiveKnob.Forms
|
|||||||
{
|
{
|
||||||
// Prevent the form from showing at startup
|
// Prevent the form from showing at startup
|
||||||
if (!startupVisibleCalled)
|
if (!startupVisibleCalled)
|
||||||
|
{
|
||||||
startupVisibleCalled = true;
|
startupVisibleCalled = true;
|
||||||
|
|
||||||
|
// Make sure the underlying window is still created, otherwise Close won't work
|
||||||
|
if (!IsHandleCreated)
|
||||||
|
CreateHandle();
|
||||||
|
}
|
||||||
else
|
else
|
||||||
base.SetVisibleCore(value);
|
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;
|
closing = true;
|
||||||
Close();
|
Close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void Connecting()
|
||||||
|
{
|
||||||
|
RunInUIContext(() =>
|
||||||
|
{
|
||||||
|
SerialPortStatusLabel.Text = Strings.StatusConnecting;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public void Connected(int knobCount)
|
public void Connected(int knobCount)
|
||||||
{
|
{
|
||||||
SafeCall(() =>
|
RunInUIContext(() =>
|
||||||
{
|
{
|
||||||
SerialPortStatusLabel.Text = Strings.StatusConnected;
|
SerialPortStatusLabel.Text = Strings.StatusConnected;
|
||||||
SetKnobCount(knobCount);
|
SetKnobCount(knobCount);
|
||||||
@ -272,14 +308,15 @@ namespace MassiveKnob.Forms
|
|||||||
|
|
||||||
public void Disconnected()
|
public void Disconnected()
|
||||||
{
|
{
|
||||||
SafeCall(() =>
|
RunInUIContext(() =>
|
||||||
{
|
{
|
||||||
SerialPortStatusLabel.Text = Strings.StatusNotConnected;
|
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)
|
if (knob >= settings.Knobs.Count)
|
||||||
return;
|
return;
|
||||||
@ -289,15 +326,12 @@ namespace MassiveKnob.Forms
|
|||||||
|
|
||||||
var deviceId = settings.Knobs[knob].DeviceId.Value;
|
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)
|
||||||
var device = await audioDeviceManager.GetDeviceById(deviceId);
|
await device.SetVolume(volume);
|
||||||
if (device != null)
|
}
|
||||||
await device.SetVolume(volume);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -310,20 +344,6 @@ namespace MassiveKnob.Forms
|
|||||||
e.Cancel = true;
|
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)
|
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;
|
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);
|
observers.Remove(observer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public void Connecting()
|
||||||
|
{
|
||||||
|
foreach (var observer in observers)
|
||||||
|
observer.Connecting();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public void Connected(int knobCount)
|
public void Connected(int knobCount)
|
||||||
{
|
{
|
||||||
foreach (var observer in observers)
|
foreach (var observer in observers)
|
||||||
|
@ -4,6 +4,7 @@ namespace MassiveKnob.Hardware
|
|||||||
{
|
{
|
||||||
public interface IMassiveKnobHardwareObserver
|
public interface IMassiveKnobHardwareObserver
|
||||||
{
|
{
|
||||||
|
void Connecting();
|
||||||
void Connected(int knobCount);
|
void Connected(int knobCount);
|
||||||
void Disconnected();
|
void Disconnected();
|
||||||
|
|
||||||
@ -25,6 +26,6 @@ namespace MassiveKnob.Hardware
|
|||||||
|
|
||||||
public interface IMassiveKnobHardwareFactory
|
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
|
public class MockMassiveKnobHardwareFactory : IMassiveKnobHardwareFactory
|
||||||
{
|
{
|
||||||
private readonly int knobCount;
|
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);
|
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\IAudioDeviceManager.cs" />
|
||||||
<Compile Include="Hardware\IMassiveKnobHardware.cs" />
|
<Compile Include="Hardware\IMassiveKnobHardware.cs" />
|
||||||
<Compile Include="Hardware\MockMassiveKnobHardware.cs" />
|
<Compile Include="Hardware\MockMassiveKnobHardware.cs" />
|
||||||
|
<Compile Include="Hardware\SerialMassiveKnobHardware.cs" />
|
||||||
<Compile Include="Program.cs" />
|
<Compile Include="Program.cs" />
|
||||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
<Compile Include="Settings\Settings.cs" />
|
<Compile Include="Settings\Settings.cs" />
|
||||||
@ -116,6 +117,12 @@
|
|||||||
<PackageReference Include="AudioSwitcher.AudioApi.CoreAudio">
|
<PackageReference Include="AudioSwitcher.AudioApi.CoreAudio">
|
||||||
<Version>4.0.0-alpha5</Version>
|
<Version>4.0.0-alpha5</Version>
|
||||||
</PackageReference>
|
</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">
|
<PackageReference Include="Newtonsoft.Json">
|
||||||
<Version>12.0.3</Version>
|
<Version>12.0.3</Version>
|
||||||
</PackageReference>
|
</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>();
|
container.Register<IAudioDeviceManagerFactory, CoreAudioDeviceManagerFactory>();
|
||||||
|
|
||||||
// For testing without the hardware:
|
// 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;
|
return container;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user