Merge branch 'feature/pluggable' into develop
This commit is contained in:
commit
f518115bc7
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,5 +1,7 @@
|
|||||||
.vs/
|
.vs/
|
||||||
bin/
|
bin/
|
||||||
obj/
|
obj/
|
||||||
|
Windows/packages/
|
||||||
|
Windows/Release/
|
||||||
|
|
||||||
*.user
|
*.user
|
7
.gitmodules
vendored
Normal file
7
.gitmodules
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
[submodule "Windows/min.NET"]
|
||||||
|
path = Windows/min.NET
|
||||||
|
url = https://github.com/MvRens/min.NET
|
||||||
|
|
||||||
|
[submodule "Windows/VoicemeeterRemote"]
|
||||||
|
path = Windows/VoicemeeterRemote
|
||||||
|
url = https://github.com/MvRens/VoicemeeterRemote
|
@ -5,24 +5,49 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
// Set this to the number of potentiometers you have connected
|
// Set this to the number of potentiometers you have connected
|
||||||
const byte KnobCount = 1;
|
const byte AnalogInputCount = 3;
|
||||||
|
|
||||||
// For each potentiometer, specify the port
|
// Set this to the number of buttons you have connected
|
||||||
const byte KnobPin[KnobCount] = {
|
const byte DigitalInputCount = 0;
|
||||||
// A0,
|
|
||||||
A1
|
// 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 = 0;
|
||||||
|
|
||||||
|
|
||||||
|
// For each potentiometer, specify the pin
|
||||||
|
const byte AnalogInputPin[AnalogInputCount] = {
|
||||||
|
A0,
|
||||||
|
A1,
|
||||||
|
A2
|
||||||
};
|
};
|
||||||
|
|
||||||
// Minimum time between reporting changing values, reduces serial traffic
|
// For each button, specify the pin. Assumes pull-up.
|
||||||
|
const byte DigitalInputPin[DigitalInputCount] = {
|
||||||
|
};
|
||||||
|
|
||||||
|
// For each digital output, specify the pin
|
||||||
|
const byte DigitalOutputPin[DigitalOutputCount] = {
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// Minimum time between reporting changing values, reduces serial traffic and debounces digital inputs
|
||||||
const unsigned long MinimumInterval = 50;
|
const unsigned long MinimumInterval = 50;
|
||||||
|
|
||||||
// Alpha value of the Exponential Moving Average (EMA) to reduce noise
|
// Alpha value of the Exponential Moving Average (EMA) for analog inputs to reduce noise
|
||||||
const float EMAAlpha = 0.6;
|
const float EMAAlpha = 0.6;
|
||||||
|
|
||||||
// How many measurements to take at boot time to seed the EMA
|
// How many measurements to take at boot time for analog inputs to seed the EMA
|
||||||
const byte EMASeedCount = 5;
|
const byte EMASeedCount = 5;
|
||||||
|
|
||||||
|
// Minimum treshold for reporting changes in analog values, reduces noise left over from the EMA. Note that once an analog value
|
||||||
|
// changes beyond the treshold, that input will report all changes until the FocusTimeout has expired to avoid losing accuracy.
|
||||||
|
const byte AnalogTreshold = 2;
|
||||||
|
|
||||||
|
// How long to ignore other inputs after an input changes. Reduces noise due voltage drops.
|
||||||
|
const unsigned long FocusTimeout = 100;
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -31,20 +56,59 @@ const byte EMASeedCount = 5;
|
|||||||
* Here be dragons.
|
* 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];
|
// If defined, only outputs will be sent to the serial port as Arduino Plotter compatible data
|
||||||
unsigned long lastChange[KnobCount];
|
//#define DebugOutputPlotter
|
||||||
int analogReadValue[KnobCount];
|
|
||||||
float emaValue[KnobCount];
|
|
||||||
unsigned long currentTime;
|
#ifndef DebugOutputPlotter
|
||||||
unsigned long lastPlot;
|
#include "./min.h"
|
||||||
|
#include "./min.c"
|
||||||
|
|
||||||
|
|
||||||
|
// MIN protocol context and callbacks
|
||||||
|
struct min_context minContext;
|
||||||
|
|
||||||
|
uint16_t min_tx_space(uint8_t port) { return 512U; }
|
||||||
|
void min_tx_byte(uint8_t port, uint8_t byte)
|
||||||
|
{
|
||||||
|
while (Serial.write(&byte, 1U) == 0) { }
|
||||||
|
}
|
||||||
|
uint32_t min_time_ms(void) { return millis(); }
|
||||||
|
void min_application_handler(uint8_t min_id, uint8_t *min_payload, uint8_t len_payload, uint8_t port);
|
||||||
|
void min_tx_start(uint8_t port) {}
|
||||||
|
void min_tx_finished(uint8_t port) {}
|
||||||
|
|
||||||
|
|
||||||
|
const uint8_t FrameIDHandshake = 42;
|
||||||
|
const uint8_t FrameIDHandshakeResponse = 43;
|
||||||
|
const uint8_t FrameIDAnalogInput = 1;
|
||||||
|
const uint8_t FrameIDDigitalInput = 2;
|
||||||
|
const uint8_t FrameIDAnalogOutput = 3;
|
||||||
|
const uint8_t FrameIDDigitalOutput = 4;
|
||||||
|
const uint8_t FrameIDQuit = 62;
|
||||||
|
const uint8_t FrameIDError = 63;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
struct AnalogInputStatus
|
||||||
|
{
|
||||||
|
byte Value;
|
||||||
|
unsigned long LastChange;
|
||||||
|
int ReadValue;
|
||||||
|
float EMAValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DigitalInputStatus
|
||||||
|
{
|
||||||
|
bool Value;
|
||||||
|
unsigned long LastChange;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
struct AnalogInputStatus analogInputStatus[AnalogInputCount];
|
||||||
|
struct DigitalInputStatus digitalInputStatus[AnalogInputCount];
|
||||||
|
|
||||||
|
|
||||||
void setup()
|
void setup()
|
||||||
{
|
{
|
||||||
@ -54,177 +118,303 @@ void setup()
|
|||||||
while (!Serial) {}
|
while (!Serial) {}
|
||||||
|
|
||||||
|
|
||||||
// Seed the moving average
|
#ifndef DebugOutputPlotter
|
||||||
for (byte knobIndex = 0; knobIndex < KnobCount; knobIndex++)
|
// Set up the MIN protocol (http://github.com/min-protocol/min)
|
||||||
emaValue[knobIndex] = analogRead(KnobPin[knobIndex]);
|
min_init_context(&minContext, 0);
|
||||||
|
#endif
|
||||||
for (byte seed = 1; seed < EMASeedCount - 1; seed++)
|
|
||||||
for (byte knobIndex = 0; knobIndex < KnobCount; knobIndex++)
|
|
||||||
getVolume(knobIndex);
|
|
||||||
|
|
||||||
|
|
||||||
// Read the initial values
|
// Seed the moving average for analog inputs
|
||||||
currentTime = millis();
|
for (byte i = 0; i < AnalogInputCount; i++)
|
||||||
for (byte knobIndex = 0; knobIndex < KnobCount; knobIndex++)
|
|
||||||
{
|
{
|
||||||
volume[knobIndex] = getVolume(knobIndex);
|
pinMode(AnalogInputPin[i], INPUT);
|
||||||
lastChange[knobIndex] = currentTime;
|
analogInputStatus[i].EMAValue = analogRead(AnalogInputPin[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (byte i = 0; i < AnalogInputCount; i++)
|
||||||
|
for (byte seed = 1; seed < EMASeedCount - 1; seed++)
|
||||||
|
getAnalogValue(i);
|
||||||
|
|
||||||
|
|
||||||
|
// Read the initial stabilized values
|
||||||
|
for (byte i = 0; i < AnalogInputCount; i++)
|
||||||
|
{
|
||||||
|
analogInputStatus[i].Value = getAnalogValue(i);
|
||||||
|
analogInputStatus[i].LastChange = millis();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Set up digital inputs and outputs
|
||||||
|
for (byte i = 0; i < DigitalInputCount; i++)
|
||||||
|
{
|
||||||
|
pinMode(DigitalInputPin[i], INPUT_PULLUP);
|
||||||
|
digitalInputStatus[i].Value = getDigitalValue(i);
|
||||||
|
digitalInputStatus[i].LastChange = millis();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (byte i = 0; i < DigitalOutputCount; i++)
|
||||||
|
{
|
||||||
|
pinMode(DigitalOutputPin[i], OUTPUT);
|
||||||
|
digitalWrite(DigitalOutputPin[i], LOW);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#ifdef DebugOutputPlotter
|
||||||
|
unsigned long lastOutput = 0;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
enum FocusType
|
||||||
|
{
|
||||||
|
FocusTypeNone = 0,
|
||||||
|
FocusTypeAnalogInput = 1,
|
||||||
|
FocusTypeDigitalInput = 2,
|
||||||
|
FocusTypeOutput = 3
|
||||||
|
};
|
||||||
|
|
||||||
|
bool active = false;
|
||||||
|
FocusType focusType = FocusTypeNone;
|
||||||
|
byte focusInputIndex;
|
||||||
|
unsigned long focusOutputTime;
|
||||||
|
|
||||||
|
#define IsAnalogInputFocus(i) ((focusType == FocusInputType.AnalogInput) && (focusInputIndex == i))
|
||||||
|
#define IsDigitalInputFocus(i) ((focusType == FocusInputType.DigitalInput) && (focusInputIndex == i))
|
||||||
|
|
||||||
|
|
||||||
void loop()
|
void loop()
|
||||||
{
|
{
|
||||||
if (Serial.available())
|
#ifndef DebugOutputPlotter
|
||||||
processMessage(Serial.read());
|
char readBuffer[32];
|
||||||
|
size_t readBufferSize = Serial.available() > 0 ? Serial.readBytes(readBuffer, 32U) : 0;
|
||||||
|
|
||||||
// Not that due to ADC checking and Serial communication, currentTime will not be
|
min_poll(&minContext, (uint8_t*)readBuffer, (uint8_t)readBufferSize);
|
||||||
// accurate throughout the loop. But since we don't need exact timing for the interval this
|
#endif
|
||||||
// is acceptable and saves a few calls to millis.
|
|
||||||
currentTime = millis();
|
|
||||||
|
|
||||||
// Check volume knobs
|
|
||||||
byte newVolume;
|
if (focusType == FocusTypeOutput && millis() - focusOutputTime >= FocusTimeout)
|
||||||
for (byte knobIndex = 0; knobIndex < KnobCount; knobIndex++)
|
focusType = FocusTypeNone;
|
||||||
|
|
||||||
|
|
||||||
|
// Check analog inputs
|
||||||
|
byte newAnalogValue;
|
||||||
|
for (byte i = 0; i < AnalogInputCount; i++)
|
||||||
{
|
{
|
||||||
newVolume = getVolume(knobIndex);
|
newAnalogValue = getAnalogValue(i);
|
||||||
|
bool changed;
|
||||||
|
|
||||||
if (newVolume != volume[knobIndex] && (currentTime - lastChange[knobIndex] >= MinimumInterval))
|
switch (focusType)
|
||||||
{
|
{
|
||||||
if (active)
|
case FocusTypeAnalogInput:
|
||||||
// Send out new value
|
if (focusInputIndex != i)
|
||||||
outputVolume(knobIndex, newVolume);
|
continue;
|
||||||
|
|
||||||
volume[knobIndex] = newVolume;
|
if (millis() - analogInputStatus[i].LastChange < FocusTimeout)
|
||||||
lastChange[knobIndex] = currentTime;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (outputMode == Plotter && (currentTime - lastPlot) >= 50)
|
|
||||||
{
|
{
|
||||||
outputPlotter();
|
changed = newAnalogValue != analogInputStatus[i].Value;
|
||||||
lastPlot = currentTime;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void processMessage(byte message)
|
|
||||||
{
|
|
||||||
switch (message)
|
|
||||||
{
|
|
||||||
case 'H': // Handshake
|
|
||||||
processHandshakeMessage();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'Q': // Quit
|
|
||||||
processQuitMessage();
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
else
|
||||||
|
focusType = FocusTypeNone;
|
||||||
|
// fall-through
|
||||||
|
|
||||||
|
case FocusTypeNone:
|
||||||
void processHandshakeMessage()
|
changed = abs(analogInputStatus[i].Value - newAnalogValue) >= AnalogTreshold;
|
||||||
{
|
|
||||||
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;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed && (millis() - analogInputStatus[i].LastChange >= MinimumInterval))
|
||||||
|
{
|
||||||
|
if (active)
|
||||||
|
// Send out new value
|
||||||
|
outputAnalogValue(i, newAnalogValue);
|
||||||
|
|
||||||
|
analogInputStatus[i].Value = newAnalogValue;
|
||||||
|
analogInputStatus[i].LastChange = millis();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Check digital inputs
|
||||||
|
bool newDigitalValue;
|
||||||
|
for (byte i = 0; i < DigitalInputCount; i++)
|
||||||
|
{
|
||||||
|
newDigitalValue = getDigitalValue(i);
|
||||||
|
|
||||||
|
switch (focusType)
|
||||||
|
{
|
||||||
|
case FocusTypeAnalogInput:
|
||||||
|
case FocusTypeOutput:
|
||||||
|
continue;
|
||||||
|
|
||||||
|
case FocusTypeDigitalInput:
|
||||||
|
if (focusInputIndex != i)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (millis() - digitalInputStatus[i].LastChange >= FocusTimeout)
|
||||||
|
focusType = FocusTypeNone;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (newDigitalValue != digitalInputStatus[i].Value && (millis() - digitalInputStatus[i].LastChange >= MinimumInterval))
|
||||||
|
{
|
||||||
|
if (active)
|
||||||
|
// Send out new value
|
||||||
|
outputDigitalValue(i, newDigitalValue);
|
||||||
|
|
||||||
|
digitalInputStatus[i].Value = newDigitalValue;
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
case FrameIDHandshake:
|
||||||
|
processHandshakeMessage(min_payload, len_payload);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FrameIDAnalogOutput:
|
||||||
|
//processAnalogOutputMessage();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FrameIDDigitalOutput:
|
||||||
|
processDigitalOutputMessage(min_payload, len_payload);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FrameIDQuit:
|
||||||
|
processQuitMessage();
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
outputError("Unknown frame ID: " + String(min_id));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void processHandshakeMessage(uint8_t *min_payload, uint8_t len_payload)
|
||||||
|
{
|
||||||
|
if (len_payload < 2)
|
||||||
|
{
|
||||||
|
outputError("Invalid handshake length");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (min_payload[0] != 'M' || min_payload[1] != 'K')
|
||||||
switch (outputMode)
|
|
||||||
{
|
{
|
||||||
case Binary:
|
outputError("Invalid handshake: " + String((char)min_payload[0]) + String((char)min_payload[1]));
|
||||||
Serial.write('H');
|
return;
|
||||||
Serial.write(KnobCount);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case PlainText:
|
|
||||||
Serial.print("Hello! I have ");
|
|
||||||
Serial.print(KnobCount);
|
|
||||||
Serial.println(" knobs.");
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
byte payload[4] { AnalogInputCount, DigitalInputCount, AnalogOutputCount, DigitalOutputCount };
|
||||||
|
if (min_queue_frame(&minContext, FrameIDHandshakeResponse, (uint8_t *)payload, 4))
|
||||||
active = true;
|
active = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void processDigitalOutputMessage(uint8_t *min_payload, uint8_t len_payload)
|
||||||
|
{
|
||||||
|
if (len_payload < 2)
|
||||||
|
{
|
||||||
|
outputError("Invalid digital output payload length");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void processQuitMessage()
|
void processQuitMessage()
|
||||||
{
|
{
|
||||||
switch (outputMode)
|
|
||||||
{
|
|
||||||
case Binary:
|
|
||||||
case PlainText:
|
|
||||||
Serial.write('Q');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
active = false;
|
active = false;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
byte getVolume(byte knobIndex)
|
byte getAnalogValue(byte analogInputIndex)
|
||||||
{
|
{
|
||||||
analogReadValue[knobIndex] = analogRead(KnobPin[knobIndex]);
|
analogRead(AnalogInputPin[analogInputIndex]);
|
||||||
emaValue[knobIndex] = (EMAAlpha * analogReadValue[knobIndex]) + ((1 - EMAAlpha) * emaValue[knobIndex]);
|
|
||||||
|
|
||||||
return map(emaValue[knobIndex], 0, 1023, 0, 100);
|
// Give the ADC some time to stabilize
|
||||||
|
delay(10);
|
||||||
|
|
||||||
|
int readValue = analogRead(AnalogInputPin[analogInputIndex]);
|
||||||
|
analogInputStatus[analogInputIndex].ReadValue = readValue;
|
||||||
|
|
||||||
|
int newEMAValue = (EMAAlpha * readValue) + ((1 - EMAAlpha) * analogInputStatus[analogInputIndex].EMAValue);
|
||||||
|
analogInputStatus[analogInputIndex].EMAValue = newEMAValue;
|
||||||
|
|
||||||
|
return map(newEMAValue, 0, 1023, 0, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void outputVolume(byte knobIndex, byte newVolume)
|
bool getDigitalValue(byte digitalInputIndex)
|
||||||
{
|
{
|
||||||
switch (outputMode)
|
return digitalRead(DigitalInputPin[digitalInputIndex]) == LOW;
|
||||||
{
|
|
||||||
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()
|
void outputAnalogValue(byte analogInputIndex, byte newValue)
|
||||||
{
|
{
|
||||||
for (byte i = 0; i < KnobCount; i++)
|
#ifndef DebugOutputPlotter
|
||||||
{
|
byte payload[2] = { analogInputIndex, newValue };
|
||||||
if (i > 0)
|
min_send_frame(&minContext, FrameIDAnalogInput, (uint8_t *)payload, 2);
|
||||||
Serial.print('\t');
|
#endif
|
||||||
|
}
|
||||||
Serial.print(analogReadValue[i]);
|
|
||||||
Serial.print('\t');
|
|
||||||
Serial.print(emaValue[i]);
|
void outputDigitalValue(byte digitalInputIndex, bool newValue)
|
||||||
Serial.print('\t');
|
{
|
||||||
Serial.print(volume[i]);
|
#ifndef DebugOutputPlotter
|
||||||
}
|
byte payload[2] = { digitalInputIndex, newValue ? 1 : 0 };
|
||||||
|
min_send_frame(&minContext, FrameIDDigitalInput, (uint8_t *)payload, 2);
|
||||||
Serial.println();
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void outputError(String message)
|
||||||
|
{
|
||||||
|
#ifndef DebugOutputPlotter
|
||||||
|
min_send_frame(&minContext, FrameIDError, (uint8_t *)message.c_str(), message.length());
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
641
Arduino/MassiveKnob/min.c
Normal file
641
Arduino/MassiveKnob/min.c
Normal file
@ -0,0 +1,641 @@
|
|||||||
|
// Copyright (c) 2014-2017 JK Energy Ltd.
|
||||||
|
//
|
||||||
|
// Use authorized under the MIT license.
|
||||||
|
|
||||||
|
#include "min.h"
|
||||||
|
|
||||||
|
#define TRANSPORT_FIFO_SIZE_FRAMES_MASK ((uint8_t)((1U << TRANSPORT_FIFO_SIZE_FRAMES_BITS) - 1U))
|
||||||
|
#define TRANSPORT_FIFO_SIZE_FRAME_DATA_MASK ((uint16_t)((1U << TRANSPORT_FIFO_SIZE_FRAME_DATA_BITS) - 1U))
|
||||||
|
|
||||||
|
// Number of bytes needed for a frame with a given payload length, excluding stuff bytes
|
||||||
|
// 3 header bytes, ID/control byte, length byte, seq byte, 4 byte CRC, EOF byte
|
||||||
|
#define ON_WIRE_SIZE(p) ((p) + 11U)
|
||||||
|
|
||||||
|
// Special protocol bytes
|
||||||
|
enum {
|
||||||
|
HEADER_BYTE = 0xaaU,
|
||||||
|
STUFF_BYTE = 0x55U,
|
||||||
|
EOF_BYTE = 0x55U,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Receiving state machine
|
||||||
|
enum {
|
||||||
|
SEARCHING_FOR_SOF,
|
||||||
|
RECEIVING_ID_CONTROL,
|
||||||
|
RECEIVING_SEQ,
|
||||||
|
RECEIVING_LENGTH,
|
||||||
|
RECEIVING_PAYLOAD,
|
||||||
|
RECEIVING_CHECKSUM_3,
|
||||||
|
RECEIVING_CHECKSUM_2,
|
||||||
|
RECEIVING_CHECKSUM_1,
|
||||||
|
RECEIVING_CHECKSUM_0,
|
||||||
|
RECEIVING_EOF,
|
||||||
|
};
|
||||||
|
|
||||||
|
#ifdef TRANSPORT_PROTOCOL
|
||||||
|
|
||||||
|
#ifndef TRANSPORT_ACK_RETRANSMIT_TIMEOUT_MS
|
||||||
|
#define TRANSPORT_ACK_RETRANSMIT_TIMEOUT_MS (25U)
|
||||||
|
#endif
|
||||||
|
#ifndef TRANSPORT_FRAME_RETRANSMIT_TIMEOUT_MS
|
||||||
|
#define TRANSPORT_FRAME_RETRANSMIT_TIMEOUT_MS (50U) // Should be long enough for a whole window to be transmitted plus an ACK / NACK to get back
|
||||||
|
#endif
|
||||||
|
#ifndef TRANSPORT_MAX_WINDOW_SIZE
|
||||||
|
#define TRANSPORT_MAX_WINDOW_SIZE (16U)
|
||||||
|
#endif
|
||||||
|
#ifndef TRANSPORT_IDLE_TIMEOUT_MS
|
||||||
|
#define TRANSPORT_IDLE_TIMEOUT_MS (1000U)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
enum {
|
||||||
|
// Top bit must be set: these are for the transport protocol to use
|
||||||
|
// 0x7f and 0x7e are reserved MIN identifiers.
|
||||||
|
ACK = 0xffU,
|
||||||
|
RESET = 0xfeU,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Where the payload data of the frame FIFO is stored
|
||||||
|
uint8_t payloads_ring_buffer[TRANSPORT_FIFO_MAX_FRAME_DATA];
|
||||||
|
|
||||||
|
static uint32_t now;
|
||||||
|
static void send_reset(struct min_context *self);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static void crc32_init_context(struct crc32_context *context)
|
||||||
|
{
|
||||||
|
context->crc = 0xffffffffU;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void crc32_step(struct crc32_context *context, uint8_t byte)
|
||||||
|
{
|
||||||
|
context->crc ^= byte;
|
||||||
|
for(uint32_t j = 0; j < 8; j++) {
|
||||||
|
uint32_t mask = (uint32_t) -(context->crc & 1U);
|
||||||
|
context->crc = (context->crc >> 1) ^ (0xedb88320U & mask);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t crc32_finalize(struct crc32_context *context)
|
||||||
|
{
|
||||||
|
return ~context->crc;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void stuffed_tx_byte(struct min_context *self, uint8_t byte)
|
||||||
|
{
|
||||||
|
// Transmit the byte
|
||||||
|
min_tx_byte(self->port, byte);
|
||||||
|
crc32_step(&self->tx_checksum, byte);
|
||||||
|
|
||||||
|
// See if an additional stuff byte is needed
|
||||||
|
if(byte == HEADER_BYTE) {
|
||||||
|
if(--self->tx_header_byte_countdown == 0) {
|
||||||
|
min_tx_byte(self->port, STUFF_BYTE); // Stuff byte
|
||||||
|
self->tx_header_byte_countdown = 2U;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
self->tx_header_byte_countdown = 2U;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void on_wire_bytes(struct min_context *self, uint8_t id_control, uint8_t seq, uint8_t *payload_base, uint16_t payload_offset, uint16_t payload_mask, uint8_t payload_len)
|
||||||
|
{
|
||||||
|
uint8_t n, i;
|
||||||
|
uint32_t checksum;
|
||||||
|
|
||||||
|
self->tx_header_byte_countdown = 2U;
|
||||||
|
crc32_init_context(&self->tx_checksum);
|
||||||
|
|
||||||
|
min_tx_start(self->port);
|
||||||
|
|
||||||
|
// Header is 3 bytes; because unstuffed will reset receiver immediately
|
||||||
|
min_tx_byte(self->port, HEADER_BYTE);
|
||||||
|
min_tx_byte(self->port, HEADER_BYTE);
|
||||||
|
min_tx_byte(self->port, HEADER_BYTE);
|
||||||
|
|
||||||
|
stuffed_tx_byte(self, id_control);
|
||||||
|
if(id_control & 0x80U) {
|
||||||
|
// Send the sequence number if it is a transport frame
|
||||||
|
stuffed_tx_byte(self, seq);
|
||||||
|
}
|
||||||
|
|
||||||
|
stuffed_tx_byte(self, payload_len);
|
||||||
|
|
||||||
|
for(i = 0, n = payload_len; n > 0; n--, i++) {
|
||||||
|
stuffed_tx_byte(self, payload_base[payload_offset]);
|
||||||
|
payload_offset++;
|
||||||
|
payload_offset &= payload_mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
checksum = crc32_finalize(&self->tx_checksum);
|
||||||
|
|
||||||
|
// Network order is big-endian. A decent C compiler will spot that this
|
||||||
|
// is extracting bytes and will use efficient instructions.
|
||||||
|
stuffed_tx_byte(self, (uint8_t)((checksum >> 24) & 0xffU));
|
||||||
|
stuffed_tx_byte(self, (uint8_t)((checksum >> 16) & 0xffU));
|
||||||
|
stuffed_tx_byte(self, (uint8_t)((checksum >> 8) & 0xffU));
|
||||||
|
stuffed_tx_byte(self, (uint8_t)((checksum >> 0) & 0xffU));
|
||||||
|
|
||||||
|
// Ensure end-of-frame doesn't contain 0xaa and confuse search for start-of-frame
|
||||||
|
min_tx_byte(self->port, EOF_BYTE);
|
||||||
|
|
||||||
|
min_tx_finished(self->port);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef TRANSPORT_PROTOCOL
|
||||||
|
|
||||||
|
// Pops frame from front of queue, reclaims its ring buffer space
|
||||||
|
static void transport_fifo_pop(struct min_context *self)
|
||||||
|
{
|
||||||
|
#ifdef ASSERTION_CHECKING
|
||||||
|
assert(self->transport_fifo.n_frames != 0);
|
||||||
|
#endif
|
||||||
|
struct transport_frame *frame = &self->transport_fifo.frames[self->transport_fifo.head_idx];
|
||||||
|
min_debug_print("Popping frame id=%d seq=%d\n", frame->min_id, frame->seq);
|
||||||
|
|
||||||
|
#ifdef ASSERTION_CHECKING
|
||||||
|
assert(self->transport_fifo.n_ring_buffer_bytes >= frame->payload_len);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
self->transport_fifo.n_frames--;
|
||||||
|
self->transport_fifo.head_idx++;
|
||||||
|
self->transport_fifo.head_idx &= TRANSPORT_FIFO_SIZE_FRAMES_MASK;
|
||||||
|
self->transport_fifo.n_ring_buffer_bytes -= frame->payload_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Claim a buffer slot from the FIFO. Returns 0 if there is no space.
|
||||||
|
static struct transport_frame *transport_fifo_push(struct min_context *self, uint16_t data_size)
|
||||||
|
{
|
||||||
|
// A frame is only queued if there aren't too many frames in the FIFO and there is space in the
|
||||||
|
// data ring buffer.
|
||||||
|
struct transport_frame *ret = 0;
|
||||||
|
if (self->transport_fifo.n_frames < TRANSPORT_FIFO_MAX_FRAMES) {
|
||||||
|
// Is there space in the ring buffer for the frame payload?
|
||||||
|
if(self->transport_fifo.n_ring_buffer_bytes <= TRANSPORT_FIFO_MAX_FRAME_DATA - data_size) {
|
||||||
|
self->transport_fifo.n_frames++;
|
||||||
|
if (self->transport_fifo.n_frames > self->transport_fifo.n_frames_max) {
|
||||||
|
// High-water mark of FIFO (for diagnostic purposes)
|
||||||
|
self->transport_fifo.n_frames_max = self->transport_fifo.n_frames;
|
||||||
|
}
|
||||||
|
// Create FIFO entry
|
||||||
|
ret = &(self->transport_fifo.frames[self->transport_fifo.tail_idx]);
|
||||||
|
ret->payload_offset = self->transport_fifo.ring_buffer_tail_offset;
|
||||||
|
|
||||||
|
// Claim ring buffer space
|
||||||
|
self->transport_fifo.n_ring_buffer_bytes += data_size;
|
||||||
|
if(self->transport_fifo.n_ring_buffer_bytes > self->transport_fifo.n_ring_buffer_bytes_max) {
|
||||||
|
// High-water mark of ring buffer usage (for diagnostic purposes)
|
||||||
|
self->transport_fifo.n_ring_buffer_bytes_max = self->transport_fifo.n_ring_buffer_bytes;
|
||||||
|
}
|
||||||
|
self->transport_fifo.ring_buffer_tail_offset += data_size;
|
||||||
|
self->transport_fifo.ring_buffer_tail_offset &= TRANSPORT_FIFO_SIZE_FRAME_DATA_MASK;
|
||||||
|
|
||||||
|
// Claim FIFO space
|
||||||
|
self->transport_fifo.tail_idx++;
|
||||||
|
self->transport_fifo.tail_idx &= TRANSPORT_FIFO_SIZE_FRAMES_MASK;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
min_debug_print("No FIFO payload space: data_size=%d, n_ring_buffer_bytes=%d\n", data_size, self->transport_fifo.n_ring_buffer_bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
min_debug_print("No FIFO frame slots\n");
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the nth frame in the FIFO
|
||||||
|
static struct transport_frame *transport_fifo_get(struct min_context *self, uint8_t n)
|
||||||
|
{
|
||||||
|
uint8_t idx = self->transport_fifo.head_idx;
|
||||||
|
return &self->transport_fifo.frames[(idx + n) & TRANSPORT_FIFO_SIZE_FRAMES_MASK];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sends the given frame to the serial line
|
||||||
|
static void transport_fifo_send(struct min_context *self, struct transport_frame *frame)
|
||||||
|
{
|
||||||
|
min_debug_print("transport_fifo_send: min_id=%d, seq=%d, payload_len=%d\n", frame->min_id, frame->seq, frame->payload_len);
|
||||||
|
on_wire_bytes(self, frame->min_id | (uint8_t)0x80U, frame->seq, payloads_ring_buffer, frame->payload_offset, TRANSPORT_FIFO_SIZE_FRAME_DATA_MASK, frame->payload_len);
|
||||||
|
frame->last_sent_time_ms = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't queue an ACK frame - we send it straight away (if there's space to do so)
|
||||||
|
static void send_ack(struct min_context *self)
|
||||||
|
{
|
||||||
|
// In the embedded end we don't reassemble out-of-order frames and so never ask for retransmits. Payload is
|
||||||
|
// always the same as the sequence number.
|
||||||
|
min_debug_print("send ACK: seq=%d\n", self->transport_fifo.rn);
|
||||||
|
if(ON_WIRE_SIZE(0) <= min_tx_space(self->port)) {
|
||||||
|
on_wire_bytes(self, ACK, self->transport_fifo.rn, &self->transport_fifo.rn, 0, 0xffU, 1U);
|
||||||
|
self->transport_fifo.last_sent_ack_time_ms = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't queue an RESET frame - we send it straight away (if there's space to do so)
|
||||||
|
static void send_reset(struct min_context *self)
|
||||||
|
{
|
||||||
|
min_debug_print("send RESET\n");
|
||||||
|
if(ON_WIRE_SIZE(0) <= min_tx_space(self->port)) {
|
||||||
|
on_wire_bytes(self, RESET, 0, 0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void transport_fifo_reset(struct min_context *self)
|
||||||
|
{
|
||||||
|
// Clear down the transmission FIFO queue
|
||||||
|
self->transport_fifo.n_frames = 0;
|
||||||
|
self->transport_fifo.head_idx = 0;
|
||||||
|
self->transport_fifo.tail_idx = 0;
|
||||||
|
self->transport_fifo.n_ring_buffer_bytes = 0;
|
||||||
|
self->transport_fifo.ring_buffer_tail_offset = 0;
|
||||||
|
self->transport_fifo.sn_max = 0;
|
||||||
|
self->transport_fifo.sn_min = 0;
|
||||||
|
self->transport_fifo.rn = 0;
|
||||||
|
|
||||||
|
// Reset the timers
|
||||||
|
self->transport_fifo.last_received_anything_ms = now;
|
||||||
|
self->transport_fifo.last_sent_ack_time_ms = now;
|
||||||
|
self->transport_fifo.last_received_frame_ms = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void min_transport_reset(struct min_context *self, bool inform_other_side)
|
||||||
|
{
|
||||||
|
if (inform_other_side) {
|
||||||
|
// Tell the other end we have gone away
|
||||||
|
send_reset(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Throw our frames away
|
||||||
|
transport_fifo_reset(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Queues a MIN ID / payload frame into the outgoing FIFO
|
||||||
|
// API call.
|
||||||
|
// Returns true if the frame was queued OK.
|
||||||
|
bool min_queue_frame(struct min_context *self, uint8_t min_id, uint8_t *payload, uint8_t payload_len)
|
||||||
|
{
|
||||||
|
struct transport_frame *frame = transport_fifo_push(self, payload_len); // Claim a FIFO slot, reserve space for payload
|
||||||
|
|
||||||
|
// We are just queueing here: the poll() function puts the frame into the window and on to the wire
|
||||||
|
if(frame != 0) {
|
||||||
|
// Copy frame details into frame slot, copy payload into ring buffer
|
||||||
|
frame->min_id = min_id & (uint8_t)0x3fU;
|
||||||
|
frame->payload_len = payload_len;
|
||||||
|
|
||||||
|
uint16_t payload_offset = frame->payload_offset;
|
||||||
|
for(uint32_t i = 0; i < payload_len; i++) {
|
||||||
|
payloads_ring_buffer[payload_offset] = payload[i];
|
||||||
|
payload_offset++;
|
||||||
|
payload_offset &= TRANSPORT_FIFO_SIZE_FRAME_DATA_MASK;
|
||||||
|
}
|
||||||
|
min_debug_print("Queued ID=%d, len=%d\n", min_id, payload_len);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
self->transport_fifo.dropped_frames++;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool min_queue_has_space_for_frame(struct min_context *self, uint8_t payload_len) {
|
||||||
|
return self->transport_fifo.n_frames < TRANSPORT_FIFO_MAX_FRAMES &&
|
||||||
|
self->transport_fifo.n_ring_buffer_bytes <= TRANSPORT_FIFO_MAX_FRAME_DATA - payload_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finds the frame in the window that was sent least recently
|
||||||
|
static struct transport_frame *find_retransmit_frame(struct min_context *self)
|
||||||
|
{
|
||||||
|
uint8_t window_size = self->transport_fifo.sn_max - self->transport_fifo.sn_min;
|
||||||
|
|
||||||
|
#ifdef ASSERTION_CHECKS
|
||||||
|
assert(window_size > 0);
|
||||||
|
assert(window_size <= self->transport_fifo.nframes);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Start with the head of the queue and call this the oldest
|
||||||
|
struct transport_frame *oldest_frame = &self->transport_fifo.frames[self->transport_fifo.head_idx];
|
||||||
|
uint32_t oldest_elapsed_time = now - oldest_frame->last_sent_time_ms;
|
||||||
|
|
||||||
|
uint8_t idx = self->transport_fifo.head_idx;
|
||||||
|
for(uint8_t i = 0; i < window_size; i++) {
|
||||||
|
uint32_t elapsed = now - self->transport_fifo.frames[idx].last_sent_time_ms;
|
||||||
|
if(elapsed > oldest_elapsed_time) { // Strictly older only; otherwise the earlier frame is deemed the older
|
||||||
|
oldest_elapsed_time = elapsed;
|
||||||
|
oldest_frame = &self->transport_fifo.frames[idx];
|
||||||
|
}
|
||||||
|
idx++;
|
||||||
|
idx &= TRANSPORT_FIFO_SIZE_FRAMES_MASK;
|
||||||
|
}
|
||||||
|
|
||||||
|
return oldest_frame;
|
||||||
|
}
|
||||||
|
#endif // TRANSPORT_PROTOCOL
|
||||||
|
|
||||||
|
// This runs the receiving half of the transport protocol, acknowledging frames received, discarding
|
||||||
|
// duplicates received, and handling RESET requests.
|
||||||
|
static void valid_frame_received(struct min_context *self)
|
||||||
|
{
|
||||||
|
uint8_t id_control = self->rx_frame_id_control;
|
||||||
|
uint8_t *payload = self->rx_frame_payload_buf;
|
||||||
|
uint8_t payload_len = self->rx_control;
|
||||||
|
|
||||||
|
#ifdef TRANSPORT_PROTOCOL
|
||||||
|
uint8_t seq = self->rx_frame_seq;
|
||||||
|
uint8_t num_acked;
|
||||||
|
uint8_t num_nacked;
|
||||||
|
uint8_t num_in_window;
|
||||||
|
|
||||||
|
// When we receive anything we know the other end is still active and won't shut down
|
||||||
|
self->transport_fifo.last_received_anything_ms = now;
|
||||||
|
|
||||||
|
switch(id_control) {
|
||||||
|
case ACK:
|
||||||
|
// If we get an ACK then we remove all the acknowledged frames with seq < rn
|
||||||
|
// The payload byte specifies the number of NACKed frames: how many we want retransmitted because
|
||||||
|
// they have gone missing.
|
||||||
|
// But we need to make sure we don't accidentally ACK too many because of a stale ACK from an old session
|
||||||
|
num_acked = seq - self->transport_fifo.sn_min;
|
||||||
|
num_nacked = payload[0] - seq;
|
||||||
|
num_in_window = self->transport_fifo.sn_max - self->transport_fifo.sn_min;
|
||||||
|
|
||||||
|
if(num_acked <= num_in_window) {
|
||||||
|
self->transport_fifo.sn_min = seq;
|
||||||
|
#ifdef ASSERTION_CHECKING
|
||||||
|
assert(self->transport_fifo.n_frames >= num_in_window);
|
||||||
|
assert(num_in_window <= TRANSPORT_MAX_WINDOW_SIZE);
|
||||||
|
assert(num_nacked <= TRANSPORT_MAX_WINDOW_SIZE);
|
||||||
|
#endif
|
||||||
|
// Now pop off all the frames up to (but not including) rn
|
||||||
|
// The ACK contains Rn; all frames before Rn are ACKed and can be removed from the window
|
||||||
|
min_debug_print("Received ACK seq=%d, num_acked=%d, num_nacked=%d\n", seq, num_acked, num_nacked);
|
||||||
|
for(uint8_t i = 0; i < num_acked; i++) {
|
||||||
|
transport_fifo_pop(self);
|
||||||
|
}
|
||||||
|
uint8_t idx = self->transport_fifo.head_idx;
|
||||||
|
// Now retransmit the number of frames that were requested
|
||||||
|
for(uint8_t i = 0; i < num_nacked; i++) {
|
||||||
|
struct transport_frame *retransmit_frame = &self->transport_fifo.frames[idx];
|
||||||
|
transport_fifo_send(self, retransmit_frame);
|
||||||
|
idx++;
|
||||||
|
idx &= TRANSPORT_FIFO_SIZE_FRAMES_MASK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
min_debug_print("Received spurious ACK seq=%d\n", seq);
|
||||||
|
self->transport_fifo.spurious_acks++;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case RESET:
|
||||||
|
// If we get a RESET demand then we reset the transport protocol (empty the FIFO, reset the
|
||||||
|
// sequence numbers, etc.)
|
||||||
|
// We don't send anything, we just do it. The other end can send frames to see if this end is
|
||||||
|
// alive (pings, etc.) or just wait to get application frames.
|
||||||
|
self->transport_fifo.resets_received++;
|
||||||
|
transport_fifo_reset(self);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (id_control & 0x80U) {
|
||||||
|
// Incoming application frames
|
||||||
|
|
||||||
|
// Reset the activity time (an idle connection will be stalled)
|
||||||
|
self->transport_fifo.last_received_frame_ms = now;
|
||||||
|
|
||||||
|
if (seq == self->transport_fifo.rn) {
|
||||||
|
// Accept this frame as matching the sequence number we were looking for
|
||||||
|
|
||||||
|
// Now looking for the next one in the sequence
|
||||||
|
self->transport_fifo.rn++;
|
||||||
|
|
||||||
|
// Always send an ACK back for the frame we received
|
||||||
|
// ACKs are short (should be about 9 microseconds to send on the wire) and
|
||||||
|
// this will cut the latency down.
|
||||||
|
// We also periodically send an ACK in case the ACK was lost, and in any case
|
||||||
|
// frames are re-sent.
|
||||||
|
send_ack(self);
|
||||||
|
|
||||||
|
// Now ready to pass this up to the application handlers
|
||||||
|
|
||||||
|
// Pass frame up to application handler to deal with
|
||||||
|
min_debug_print("Incoming app frame seq=%d, id=%d, payload len=%d\n", seq, id_control & (uint8_t)0x3fU, payload_len);
|
||||||
|
min_application_handler(id_control & (uint8_t)0x3fU, payload, payload_len, self->port);
|
||||||
|
} else {
|
||||||
|
// Discard this frame because we aren't looking for it: it's either a dupe because it was
|
||||||
|
// retransmitted when our ACK didn't get through in time, or else it's further on in the
|
||||||
|
// sequence and others got dropped.
|
||||||
|
self->transport_fifo.sequence_mismatch_drop++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Not a transport frame
|
||||||
|
min_application_handler(id_control & (uint8_t)0x3fU, payload, payload_len, self->port);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
#else // TRANSPORT_PROTOCOL
|
||||||
|
min_application_handler(id_control & (uint8_t)0x3fU, payload, payload_len, self->port);
|
||||||
|
#endif // TRANSPORT_PROTOCOL
|
||||||
|
}
|
||||||
|
|
||||||
|
static void rx_byte(struct min_context *self, uint8_t byte)
|
||||||
|
{
|
||||||
|
// Regardless of state, three header bytes means "start of frame" and
|
||||||
|
// should reset the frame buffer and be ready to receive frame data
|
||||||
|
//
|
||||||
|
// Two in a row in over the frame means to expect a stuff byte.
|
||||||
|
uint32_t crc;
|
||||||
|
|
||||||
|
if(self->rx_header_bytes_seen == 2) {
|
||||||
|
self->rx_header_bytes_seen = 0;
|
||||||
|
if(byte == HEADER_BYTE) {
|
||||||
|
self->rx_frame_state = RECEIVING_ID_CONTROL;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(byte == STUFF_BYTE) {
|
||||||
|
/* Discard this byte; carry on receiving on the next character */
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
/* Something has gone wrong, give up on this frame and look for header again */
|
||||||
|
self->rx_frame_state = SEARCHING_FOR_SOF;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(byte == HEADER_BYTE) {
|
||||||
|
self->rx_header_bytes_seen++;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
self->rx_header_bytes_seen = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch(self->rx_frame_state) {
|
||||||
|
case SEARCHING_FOR_SOF:
|
||||||
|
break;
|
||||||
|
case RECEIVING_ID_CONTROL:
|
||||||
|
self->rx_frame_id_control = byte;
|
||||||
|
self->rx_frame_payload_bytes = 0;
|
||||||
|
crc32_init_context(&self->rx_checksum);
|
||||||
|
crc32_step(&self->rx_checksum, byte);
|
||||||
|
if(byte & 0x80U) {
|
||||||
|
#ifdef TRANSPORT_PROTOCOL
|
||||||
|
self->rx_frame_state = RECEIVING_SEQ;
|
||||||
|
#else
|
||||||
|
// If there is no transport support compiled in then all transport frames are ignored
|
||||||
|
self->rx_frame_state = SEARCHING_FOR_SOF;
|
||||||
|
#endif // TRANSPORT_PROTOCOL
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
self->rx_frame_seq = 0;
|
||||||
|
self->rx_frame_state = RECEIVING_LENGTH;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case RECEIVING_SEQ:
|
||||||
|
self->rx_frame_seq = byte;
|
||||||
|
crc32_step(&self->rx_checksum, byte);
|
||||||
|
self->rx_frame_state = RECEIVING_LENGTH;
|
||||||
|
break;
|
||||||
|
case RECEIVING_LENGTH:
|
||||||
|
self->rx_frame_length = byte;
|
||||||
|
self->rx_control = byte;
|
||||||
|
crc32_step(&self->rx_checksum, byte);
|
||||||
|
if(self->rx_frame_length > 0) {
|
||||||
|
// Can reduce the RAM size by compiling limits to frame sizes
|
||||||
|
if(self->rx_frame_length <= MAX_PAYLOAD) {
|
||||||
|
self->rx_frame_state = RECEIVING_PAYLOAD;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Frame dropped because it's longer than any frame we can buffer
|
||||||
|
self->rx_frame_state = SEARCHING_FOR_SOF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
self->rx_frame_state = RECEIVING_CHECKSUM_3;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case RECEIVING_PAYLOAD:
|
||||||
|
self->rx_frame_payload_buf[self->rx_frame_payload_bytes++] = byte;
|
||||||
|
crc32_step(&self->rx_checksum, byte);
|
||||||
|
if(--self->rx_frame_length == 0) {
|
||||||
|
self->rx_frame_state = RECEIVING_CHECKSUM_3;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case RECEIVING_CHECKSUM_3:
|
||||||
|
self->rx_frame_checksum = ((uint32_t)byte) << 24;
|
||||||
|
self->rx_frame_state = RECEIVING_CHECKSUM_2;
|
||||||
|
break;
|
||||||
|
case RECEIVING_CHECKSUM_2:
|
||||||
|
self->rx_frame_checksum |= ((uint32_t)byte) << 16;
|
||||||
|
self->rx_frame_state = RECEIVING_CHECKSUM_1;
|
||||||
|
break;
|
||||||
|
case RECEIVING_CHECKSUM_1:
|
||||||
|
self->rx_frame_checksum |= ((uint32_t)byte) << 8;
|
||||||
|
self->rx_frame_state = RECEIVING_CHECKSUM_0;
|
||||||
|
break;
|
||||||
|
case RECEIVING_CHECKSUM_0:
|
||||||
|
self->rx_frame_checksum |= byte;
|
||||||
|
crc = crc32_finalize(&self->rx_checksum);
|
||||||
|
if(self->rx_frame_checksum != crc) {
|
||||||
|
// Frame fails the checksum and so is dropped
|
||||||
|
self->rx_frame_state = SEARCHING_FOR_SOF;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Checksum passes, go on to check for the end-of-frame marker
|
||||||
|
self->rx_frame_state = RECEIVING_EOF;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case RECEIVING_EOF:
|
||||||
|
if(byte == 0x55u) {
|
||||||
|
// Frame received OK, pass up data to handler
|
||||||
|
valid_frame_received(self);
|
||||||
|
}
|
||||||
|
// else discard
|
||||||
|
// Look for next frame */
|
||||||
|
self->rx_frame_state = SEARCHING_FOR_SOF;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Should never get here but in case we do then reset to a safe state
|
||||||
|
self->rx_frame_state = SEARCHING_FOR_SOF;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// API call: sends received bytes into a MIN context and runs the transport timeouts
|
||||||
|
void min_poll(struct min_context *self, uint8_t *buf, uint32_t buf_len)
|
||||||
|
{
|
||||||
|
for(uint32_t i = 0; i < buf_len; i++) {
|
||||||
|
rx_byte(self, buf[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef TRANSPORT_PROTOCOL
|
||||||
|
uint8_t window_size;
|
||||||
|
|
||||||
|
now = min_time_ms();
|
||||||
|
|
||||||
|
bool remote_connected = (now - self->transport_fifo.last_received_anything_ms < TRANSPORT_IDLE_TIMEOUT_MS);
|
||||||
|
bool remote_active = (now - self->transport_fifo.last_received_frame_ms < TRANSPORT_IDLE_TIMEOUT_MS);
|
||||||
|
|
||||||
|
// This sends one new frame or resends one old frame
|
||||||
|
window_size = self->transport_fifo.sn_max - self->transport_fifo.sn_min; // Window size
|
||||||
|
if((window_size < TRANSPORT_MAX_WINDOW_SIZE) && (self->transport_fifo.n_frames > window_size)) {
|
||||||
|
// There are new frames we can send; but don't even bother if there's no buffer space for them
|
||||||
|
struct transport_frame *frame = transport_fifo_get(self, window_size);
|
||||||
|
if(ON_WIRE_SIZE(frame->payload_len) <= min_tx_space(self->port)) {
|
||||||
|
frame->seq = self->transport_fifo.sn_max;
|
||||||
|
transport_fifo_send(self, frame);
|
||||||
|
|
||||||
|
// Move window on
|
||||||
|
self->transport_fifo.sn_max++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Sender cannot send new frames so resend old ones (if there's anyone there)
|
||||||
|
if((window_size > 0) && remote_connected) {
|
||||||
|
// There are unacknowledged frames. Can re-send an old frame. Pick the least recently sent one.
|
||||||
|
struct transport_frame *oldest_frame = find_retransmit_frame(self);
|
||||||
|
if(now - oldest_frame->last_sent_time_ms >= TRANSPORT_FRAME_RETRANSMIT_TIMEOUT_MS) {
|
||||||
|
// Resending oldest frame if there's a chance there's enough space to send it
|
||||||
|
if(ON_WIRE_SIZE(oldest_frame->payload_len) <= min_tx_space(self->port)) {
|
||||||
|
transport_fifo_send(self, oldest_frame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifndef DISABLE_TRANSPORT_ACK_RETRANSMIT
|
||||||
|
// Periodically transmit the ACK with the rn value, unless the line has gone idle
|
||||||
|
if(now - self->transport_fifo.last_sent_ack_time_ms > TRANSPORT_ACK_RETRANSMIT_TIMEOUT_MS) {
|
||||||
|
if(remote_active) {
|
||||||
|
send_ack(self);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif // DISABLE_TRANSPORT_ACK_RETRANSMIT
|
||||||
|
#endif // TRANSPORT_PROTOCOL
|
||||||
|
}
|
||||||
|
|
||||||
|
void min_init_context(struct min_context *self, uint8_t port)
|
||||||
|
{
|
||||||
|
// Initialize context
|
||||||
|
self->rx_header_bytes_seen = 0;
|
||||||
|
self->rx_frame_state = SEARCHING_FOR_SOF;
|
||||||
|
self->port = port;
|
||||||
|
|
||||||
|
#ifdef TRANSPORT_PROTOCOL
|
||||||
|
// Counters for diagnosis purposes
|
||||||
|
self->transport_fifo.spurious_acks = 0;
|
||||||
|
self->transport_fifo.sequence_mismatch_drop = 0;
|
||||||
|
self->transport_fifo.dropped_frames = 0;
|
||||||
|
self->transport_fifo.resets_received = 0;
|
||||||
|
self->transport_fifo.n_ring_buffer_bytes_max = 0;
|
||||||
|
self->transport_fifo.n_frames_max = 0;
|
||||||
|
transport_fifo_reset(self);
|
||||||
|
#endif // TRANSPORT_PROTOCOL
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sends an application MIN frame on the wire (do not put into the transport queue)
|
||||||
|
void min_send_frame(struct min_context *self, uint8_t min_id, uint8_t *payload, uint8_t payload_len)
|
||||||
|
{
|
||||||
|
if((ON_WIRE_SIZE(payload_len) <= min_tx_space(self->port))) {
|
||||||
|
on_wire_bytes(self, min_id & (uint8_t) 0x3fU, 0, payload, 0, 0xffffU, payload_len);
|
||||||
|
}
|
||||||
|
}
|
208
Arduino/MassiveKnob/min.h
Normal file
208
Arduino/MassiveKnob/min.h
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
// MIN Protocol v2.0.
|
||||||
|
//
|
||||||
|
// MIN is a lightweight reliable protocol for exchanging information from a microcontroller (MCU) to a host.
|
||||||
|
// It is designed to run on an 8-bit MCU but also scale up to more powerful devices. A typical use case is to
|
||||||
|
// send data from a UART on a small MCU over a UART-USB converter plugged into a PC host. A Python implementation
|
||||||
|
// of host code is provided (or this code could be compiled for a PC).
|
||||||
|
//
|
||||||
|
// MIN supports frames of 0-255 bytes (with a lower limit selectable at compile time to reduce RAM). MIN frames
|
||||||
|
// have identifier values between 0 and 63.
|
||||||
|
//
|
||||||
|
// An optional transport layer T-MIN can be compiled in. This provides sliding window reliable transmission of frames.
|
||||||
|
//
|
||||||
|
// Compile options:
|
||||||
|
//
|
||||||
|
// - Define NO_TRANSPORT_PROTOCOL to remove the code and other overheads of dealing with transport frames. Any
|
||||||
|
// transport frames sent from the other side are dropped.
|
||||||
|
//
|
||||||
|
// - Define MAX_PAYLOAD if the size of the frames is to be limited. This is particularly useful with the transport
|
||||||
|
// protocol where a deep FIFO is wanted but not for large frames.
|
||||||
|
//
|
||||||
|
// The API is as follows:
|
||||||
|
//
|
||||||
|
// - min_init_context()
|
||||||
|
// A MIN context is a structure allocated by the programmer that stores details of the protocol. This permits
|
||||||
|
// the code to be reentrant and multiple serial ports to be used. The port parameter is used in a callback to
|
||||||
|
// allow the programmer's serial port drivers to place bytes in the right port. In a typical scenario there will
|
||||||
|
// be just one context.
|
||||||
|
//
|
||||||
|
// - min_send_frame()
|
||||||
|
// This sends a non-transport frame and will be dropped if the line is noisy.
|
||||||
|
//
|
||||||
|
// - min_queue_frame()
|
||||||
|
// This queues a transport frame which will will be retransmitted until the other side receives it correctly.
|
||||||
|
//
|
||||||
|
// - min_poll()
|
||||||
|
// This passes in received bytes to the context associated with the source. Note that if the transport protocol
|
||||||
|
// is included then this must be called regularly to operate the transport state machine even if there are no
|
||||||
|
// incoming bytes.
|
||||||
|
//
|
||||||
|
// There are several callbacks: these must be provided by the programmer and are called by the library:
|
||||||
|
//
|
||||||
|
// - min_tx_space()
|
||||||
|
// The programmer's serial drivers must return the number of bytes of space available in the sending buffer.
|
||||||
|
// This helps cut down on the number of lost frames (and hence improve throughput) if a doomed attempt to transmit a
|
||||||
|
// frame can be avoided.
|
||||||
|
//
|
||||||
|
// - min_tx_byte()
|
||||||
|
// The programmer's drivers must send a byte on the given port. The implementation of the serial port drivers
|
||||||
|
// is in the domain of the programmer: they might be interrupt-based, polled, etc.
|
||||||
|
//
|
||||||
|
// - min_application_handler()
|
||||||
|
// This is the callback that provides a MIN frame received on a given port to the application. The programmer
|
||||||
|
// should then deal with the frame as part of the application.
|
||||||
|
//
|
||||||
|
// - min_time_ms()
|
||||||
|
// This is called to obtain current time in milliseconds. This is used by the MIN transport protocol to drive
|
||||||
|
// timeouts and retransmits.
|
||||||
|
|
||||||
|
|
||||||
|
#ifndef MIN_H
|
||||||
|
#define MIN_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
#ifdef ASSERTION_CHECKING
|
||||||
|
#include <assert.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef NO_TRANSPORT_PROTOCOL
|
||||||
|
#define TRANSPORT_PROTOCOL
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef MAX_PAYLOAD
|
||||||
|
#define MAX_PAYLOAD (255U)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Powers of two for FIFO management. Default is 16 frames in the FIFO, total of 1024 bytes for frame data
|
||||||
|
#ifndef TRANSPORT_FIFO_SIZE_FRAMES_BITS
|
||||||
|
#define TRANSPORT_FIFO_SIZE_FRAMES_BITS (4U)
|
||||||
|
#endif
|
||||||
|
#ifndef TRANSPORT_FIFO_SIZE_FRAME_DATA_BITS
|
||||||
|
#define TRANSPORT_FIFO_SIZE_FRAME_DATA_BITS (10U)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define TRANSPORT_FIFO_MAX_FRAMES (1U << TRANSPORT_FIFO_SIZE_FRAMES_BITS)
|
||||||
|
#define TRANSPORT_FIFO_MAX_FRAME_DATA (1U << TRANSPORT_FIFO_SIZE_FRAME_DATA_BITS)
|
||||||
|
|
||||||
|
#if (MAX_PAYLOAD > 255)
|
||||||
|
#error "MIN frame payloads can be no bigger than 255 bytes"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Indices into the frames FIFO are uint8_t and so can't have more than 256 frames in a FIFO
|
||||||
|
#if (TRANSPORT_FIFO_MAX_FRAMES > 256)
|
||||||
|
#error "Transport FIFO frames cannot exceed 256"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Using a 16-bit offset into the frame data FIFO so it has to be addressable within 64Kbytes
|
||||||
|
#if (TRANSPORT_FIFO_MAX_FRAME_DATA > 65536)
|
||||||
|
#error "Transport FIFO data allocated cannot exceed 64Kbytes"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
struct crc32_context {
|
||||||
|
uint32_t crc;
|
||||||
|
};
|
||||||
|
|
||||||
|
#ifdef TRANSPORT_PROTOCOL
|
||||||
|
|
||||||
|
struct transport_frame {
|
||||||
|
uint32_t last_sent_time_ms; // When frame was last sent (used for re-send timeouts)
|
||||||
|
uint16_t payload_offset; // Where in the ring buffer the payload is
|
||||||
|
uint8_t payload_len; // How big the payload is
|
||||||
|
uint8_t min_id; // ID of frame
|
||||||
|
uint8_t seq; // Sequence number of frame
|
||||||
|
};
|
||||||
|
|
||||||
|
struct transport_fifo {
|
||||||
|
struct transport_frame frames[TRANSPORT_FIFO_MAX_FRAMES];
|
||||||
|
uint32_t last_sent_ack_time_ms;
|
||||||
|
uint32_t last_received_anything_ms;
|
||||||
|
uint32_t last_received_frame_ms;
|
||||||
|
uint32_t dropped_frames; // Diagnostic counters
|
||||||
|
uint32_t spurious_acks;
|
||||||
|
uint32_t sequence_mismatch_drop;
|
||||||
|
uint32_t resets_received;
|
||||||
|
uint16_t n_ring_buffer_bytes; // Number of bytes used in the payload ring buffer
|
||||||
|
uint16_t n_ring_buffer_bytes_max; // Largest number of bytes ever used
|
||||||
|
uint16_t ring_buffer_tail_offset; // Tail of the payload ring buffer
|
||||||
|
uint8_t n_frames; // Number of frames in the FIFO
|
||||||
|
uint8_t n_frames_max; // Larger number of frames in the FIFO
|
||||||
|
uint8_t head_idx; // Where frames are taken from in the FIFO
|
||||||
|
uint8_t tail_idx; // Where new frames are added
|
||||||
|
uint8_t sn_min; // Sequence numbers for transport protocol
|
||||||
|
uint8_t sn_max;
|
||||||
|
uint8_t rn;
|
||||||
|
};
|
||||||
|
#endif
|
||||||
|
|
||||||
|
struct min_context {
|
||||||
|
#ifdef TRANSPORT_PROTOCOL
|
||||||
|
struct transport_fifo transport_fifo; // T-MIN queue of outgoing frames
|
||||||
|
#endif
|
||||||
|
uint8_t rx_frame_payload_buf[MAX_PAYLOAD]; // Payload received so far
|
||||||
|
uint32_t rx_frame_checksum; // Checksum received over the wire
|
||||||
|
struct crc32_context rx_checksum; // Calculated checksum for receiving frame
|
||||||
|
struct crc32_context tx_checksum; // Calculated checksum for sending frame
|
||||||
|
uint8_t rx_header_bytes_seen; // Countdown of header bytes to reset state
|
||||||
|
uint8_t rx_frame_state; // State of receiver
|
||||||
|
uint8_t rx_frame_payload_bytes; // Length of payload received so far
|
||||||
|
uint8_t rx_frame_id_control; // ID and control bit of frame being received
|
||||||
|
uint8_t rx_frame_seq; // Sequence number of frame being received
|
||||||
|
uint8_t rx_frame_length; // Length of frame
|
||||||
|
uint8_t rx_control; // Control byte
|
||||||
|
uint8_t tx_header_byte_countdown; // Count out the header bytes
|
||||||
|
uint8_t port; // Number of the port associated with the context
|
||||||
|
};
|
||||||
|
|
||||||
|
#ifdef TRANSPORT_PROTOCOL
|
||||||
|
// Queue a MIN frame in the transport queue
|
||||||
|
bool min_queue_frame(struct min_context *self, uint8_t min_id, uint8_t *payload, uint8_t payload_len);
|
||||||
|
|
||||||
|
// Determine if MIN has space to queue a transport frame
|
||||||
|
bool min_queue_has_space_for_frame(struct min_context *self, uint8_t payload_len);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Send a non-transport frame MIN frame
|
||||||
|
void min_send_frame(struct min_context *self, uint8_t min_id, uint8_t *payload, uint8_t payload_len);
|
||||||
|
|
||||||
|
// Must be regularly called, with the received bytes since the last call.
|
||||||
|
// NB: if the transport protocol is being used then even if there are no bytes
|
||||||
|
// this call must still be made in order to drive the state machine for retransmits.
|
||||||
|
void min_poll(struct min_context *self, uint8_t *buf, uint32_t buf_len);
|
||||||
|
|
||||||
|
// Reset the state machine and (optionally) tell the other side that we have done so
|
||||||
|
void min_transport_reset(struct min_context *self, bool inform_other_side);
|
||||||
|
|
||||||
|
// CALLBACK. Handle incoming MIN frame
|
||||||
|
void min_application_handler(uint8_t min_id, uint8_t *min_payload, uint8_t len_payload, uint8_t port);
|
||||||
|
|
||||||
|
#ifdef TRANSPORT_PROTOCOL
|
||||||
|
// CALLBACK. Must return current time in milliseconds.
|
||||||
|
// Typically a tick timer interrupt will increment a 32-bit variable every 1ms (e.g. SysTick on Cortex M ARM devices).
|
||||||
|
uint32_t min_time_ms(void);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// CALLBACK. Must return current buffer space in the given port. Used to check that a frame can be
|
||||||
|
// queued.
|
||||||
|
uint16_t min_tx_space(uint8_t port);
|
||||||
|
|
||||||
|
// CALLBACK. Send a byte on the given line.
|
||||||
|
void min_tx_byte(uint8_t port, uint8_t byte);
|
||||||
|
|
||||||
|
// CALLBACK. Indcates when frame transmission is finished; useful for buffering bytes into a single serial call.
|
||||||
|
void min_tx_start(uint8_t port);
|
||||||
|
void min_tx_finished(uint8_t port);
|
||||||
|
|
||||||
|
// Initialize a MIN context ready for receiving bytes from a serial link
|
||||||
|
// (Can have multiple MIN contexts)
|
||||||
|
void min_init_context(struct min_context *self, uint8_t port);
|
||||||
|
|
||||||
|
#ifdef MIN_DEBUG_PRINTING
|
||||||
|
// Debug print
|
||||||
|
void min_debug_print(const char *msg, ...);
|
||||||
|
#else
|
||||||
|
#define min_debug_print(...)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif //MIN_H
|
Binary file not shown.
Binary file not shown.
48
README.md
48
README.md
@ -1,25 +1,47 @@
|
|||||||
|
|
||||||
# Massive Knob
|
# Massive Knob
|
||||||
|
|
||||||
Control audio devices using physical knobs.
|
Control audio devices using physical knobs. And more.
|
||||||
|
|
||||||
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. The original requirements were:
|
||||||
|
|
||||||
**Must have**
|
1. Control volume using a potentiometer (fixed position) instead of a rotary encoder (endless rotation)
|
||||||
1. ✔ Control multiple audio devices, one set of physical controls per device
|
2. Control specific audio devices, not the current default device
|
||||||
2. ✔ Volume is set to an absolute value (potentiometer instead of a rotary encoder)
|
3. Provide means of switching the default device by pressing a button
|
||||||
|
|
||||||
Because of the second requirement, a simple media keys HID device does not suffice and extra software is required on the desktop.
|
Because of these requirements, a simple media keys HID device does not suffice and extra software is required on the desktop. This opens up a range of possibilities.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 🔊 Set the volume for specific devices / send the current volume to an analog output
|
||||||
|
- 🔇 Mute / unmute specific devices / send the muted state to a digital output (*e.g. LED*)
|
||||||
|
- 🎧 Set the default device / set a digital output based on the default device
|
||||||
|
- 💬 Optional OSD (On-Screen Display)
|
||||||
|
- 🔌 VoiceMeeter (Standard, Banana & Potato) plugin to execute macros or read the current state
|
||||||
|
|
||||||
|
|
||||||
|
Massive Knob is basically a host for plugins. A plugin can implement a device or actions which either process signals from the device to perform an action (for example, change the volume when a knob is turned) or send signals to the device based on the system state (for example, light up an LED to indicate the default device).
|
||||||
|
|
||||||
|
### Devices
|
||||||
|
Devices can provide the following inputs and outputs, up to 255 for each type (unless you're Look Mum No Computer I assume this will be enough):
|
||||||
|
|
||||||
|
1. Analog input (*e.g. a potentiometer*)
|
||||||
|
2. Digital input (*e.g. a button or switch*)
|
||||||
|
3. Analog output (*e.g. a PWM output, though not yet supported by the reference Arduino implementation*)
|
||||||
|
4. Digital output (*e.g. an LED*)
|
||||||
|
|
||||||
|
#### Serial
|
||||||
|
Connects to a compatible device on a Serial port, probably a USB device like an Arduino. The device must implement the Massive Knob protocol which uses the [MIN protocol](https://github.com/min-protocol/min) to send and receive frames. An Arduino Sketch is included with this repository which can be customized to suit your hardware layout.
|
||||||
|
|
||||||
|
#### Emulator
|
||||||
|
Useful for development, this one emulates an actual device. The number of inputs and outputs are configurable, a popup allows changing the inputs and shows the state of the outputs.
|
||||||
|
|
||||||
**Nice to have**
|
|
||||||
1. Physical buttons to switch the active device
|
|
||||||
- by changing the Windows default output device
|
|
||||||
- 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
|
## Developing
|
||||||
The hardware side uses an Arduino sketch to communicate the hardware state over the serial port.
|
The hardware side uses an Arduino sketch to communicate the hardware state over the serial port.
|
||||||
|
|
||||||
The Windows software is written in C# using .NET Framework 4.7.2 and Visual Studio 2019.
|
The Windows software is written in C# using .NET Framework 4.7.2 and Visual Studio 2019.
|
||||||
|
|
||||||
|
Refer to the bundled plugins for examples.
|
||||||
|
|
||||||
|
Some icons courtesy of https://feathericons.com/
|
41
Windows/BuildRelease.ps1
Normal file
41
Windows/BuildRelease.ps1
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# Run this script from the Developer PowerShell found in Visual Studio 2019
|
||||||
|
# or the start menu to get the correct msbuild version on the path
|
||||||
|
#
|
||||||
|
# GitVersion is also required and must be available on the path
|
||||||
|
# Inno Setup 5 is used to compile the setup, it's path is specified below
|
||||||
|
#
|
||||||
|
$innoSetupCompiler = "C:\Program Files (x86)\Inno Setup 5\ISCC.exe"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
$versionJson = & GitVersion | Out-String
|
||||||
|
try
|
||||||
|
{
|
||||||
|
$version = ConvertFrom-Json $versionJson
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
Write-Host "Error while parsing GitVersion output: $($_.Exception.Message)" -ForegroundColor Red
|
||||||
|
Write-Host $versionJson -ForegroundColor Gray
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "GitVersion: $($version.LegacySemVer)"
|
||||||
|
$env:BUILD_VERSION = $version.LegacySemVer
|
||||||
|
|
||||||
|
|
||||||
|
& msbuild MassiveKnob.sln /t:Clean /t:Build /p:Configuration=Release
|
||||||
|
if (!$?)
|
||||||
|
{
|
||||||
|
Write-Host "MSBuild failed, aborting..." -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
& $innoSetupCompiler "Setup\MassiveKnobSetup.iss"
|
||||||
|
if (!$?)
|
||||||
|
{
|
||||||
|
Write-Host "Inno Setup failed, aborting..." -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
@ -1,69 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace MassiveKnob.Hardware
|
|
||||||
{
|
|
||||||
public abstract class AbstractMassiveKnobHardware : IMassiveKnobHardware
|
|
||||||
{
|
|
||||||
protected ObserverProxy Observers = new ObserverProxy();
|
|
||||||
|
|
||||||
public void AttachObserver(IMassiveKnobHardwareObserver observer)
|
|
||||||
{
|
|
||||||
Observers.AttachObserver(observer);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DetachObserver(IMassiveKnobHardwareObserver observer)
|
|
||||||
{
|
|
||||||
Observers.DetachObserver(observer);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public abstract Task TryConnect();
|
|
||||||
public abstract Task Disconnect();
|
|
||||||
|
|
||||||
|
|
||||||
public class ObserverProxy : IMassiveKnobHardwareObserver
|
|
||||||
{
|
|
||||||
private readonly List<IMassiveKnobHardwareObserver> observers = new List<IMassiveKnobHardwareObserver>();
|
|
||||||
|
|
||||||
|
|
||||||
public void AttachObserver(IMassiveKnobHardwareObserver observer)
|
|
||||||
{
|
|
||||||
observers.Add(observer);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DetachObserver(IMassiveKnobHardwareObserver observer)
|
|
||||||
{
|
|
||||||
observers.Remove(observer);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void Connecting()
|
|
||||||
{
|
|
||||||
foreach (var observer in observers)
|
|
||||||
observer.Connecting();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void Connected(int knobCount)
|
|
||||||
{
|
|
||||||
foreach (var observer in observers)
|
|
||||||
observer.Connected(knobCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void Disconnected()
|
|
||||||
{
|
|
||||||
foreach (var observer in observers)
|
|
||||||
observer.Disconnected();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void VolumeChanged(int knob, int volume)
|
|
||||||
{
|
|
||||||
foreach (var observer in observers)
|
|
||||||
observer.VolumeChanged(knob, volume);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,82 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using AudioSwitcher.AudioApi;
|
|
||||||
using AudioSwitcher.AudioApi.CoreAudio;
|
|
||||||
|
|
||||||
namespace MassiveKnob.Hardware
|
|
||||||
{
|
|
||||||
public class CoreAudioDeviceManager : IAudioDeviceManager
|
|
||||||
{
|
|
||||||
private readonly Lazy<CoreAudioController> audioController = new Lazy<CoreAudioController>();
|
|
||||||
private List<IAudioDevice> devices;
|
|
||||||
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
if (audioController.IsValueCreated)
|
|
||||||
audioController.Value.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public async Task<IEnumerable<IAudioDevice>> GetDevices()
|
|
||||||
{
|
|
||||||
return devices ?? (devices = (await audioController.Value.GetPlaybackDevicesAsync())
|
|
||||||
.Select(device => new AudioDevice(device) as IAudioDevice)
|
|
||||||
.ToList());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public Task<IAudioDevice> GetDeviceById(Guid deviceId)
|
|
||||||
{
|
|
||||||
return Task.FromResult(devices?.FirstOrDefault(device => device.Id == deviceId));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private class AudioDevice : IAudioDevice
|
|
||||||
{
|
|
||||||
private readonly IDevice device;
|
|
||||||
|
|
||||||
public Guid Id { get; }
|
|
||||||
public string DisplayName { get; }
|
|
||||||
|
|
||||||
|
|
||||||
public AudioDevice(IDevice device)
|
|
||||||
{
|
|
||||||
this.device = device;
|
|
||||||
Id = device.Id;
|
|
||||||
|
|
||||||
string displayFormat;
|
|
||||||
|
|
||||||
if ((device.State & DeviceState.Disabled) != 0)
|
|
||||||
displayFormat = Strings.DeviceDisplayNameDisabled;
|
|
||||||
else if ((device.State & DeviceState.Unplugged) != 0)
|
|
||||||
displayFormat = Strings.DeviceDisplayNameUnplugged;
|
|
||||||
else if ((device.State & DeviceState.NotPresent) != 0)
|
|
||||||
displayFormat = Strings.DeviceDisplayNameNotPresent;
|
|
||||||
else if ((device.State & DeviceState.Active) == 0)
|
|
||||||
displayFormat = Strings.DeviceDisplayNameInactive;
|
|
||||||
else
|
|
||||||
displayFormat = Strings.DeviceDisplayNameActive;
|
|
||||||
|
|
||||||
DisplayName = string.Format(displayFormat, device.FullName);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public Task SetVolume(int volume)
|
|
||||||
{
|
|
||||||
return device.SetVolumeAsync(volume);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public class CoreAudioDeviceManagerFactory : IAudioDeviceManagerFactory
|
|
||||||
{
|
|
||||||
public IAudioDeviceManager Create()
|
|
||||||
{
|
|
||||||
return new CoreAudioDeviceManager();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace MassiveKnob.Hardware
|
|
||||||
{
|
|
||||||
public interface IAudioDevice
|
|
||||||
{
|
|
||||||
Guid Id { get; }
|
|
||||||
string DisplayName { get; }
|
|
||||||
|
|
||||||
Task SetVolume(int volume);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public interface IAudioDeviceManager : IDisposable
|
|
||||||
{
|
|
||||||
Task<IEnumerable<IAudioDevice>> GetDevices();
|
|
||||||
Task<IAudioDevice> GetDeviceById(Guid deviceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public interface IAudioDeviceManagerFactory
|
|
||||||
{
|
|
||||||
IAudioDeviceManager Create();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace MassiveKnob.Hardware
|
|
||||||
{
|
|
||||||
public interface IMassiveKnobHardwareObserver
|
|
||||||
{
|
|
||||||
void Connecting();
|
|
||||||
void Connected(int knobCount);
|
|
||||||
void Disconnected();
|
|
||||||
|
|
||||||
void VolumeChanged(int knob, int volume);
|
|
||||||
// void ButtonPress(int index); -- for switching the active device, TBD
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public interface IMassiveKnobHardware
|
|
||||||
{
|
|
||||||
void AttachObserver(IMassiveKnobHardwareObserver observer);
|
|
||||||
void DetachObserver(IMassiveKnobHardwareObserver observer);
|
|
||||||
|
|
||||||
Task TryConnect();
|
|
||||||
Task Disconnect();
|
|
||||||
// Task SetActiveKnob(int knob); -- for providing LED feedback when switching the active device, TBD
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public interface IMassiveKnobHardwareFactory
|
|
||||||
{
|
|
||||||
IMassiveKnobHardware Create(string portName);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,71 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace MassiveKnob.Hardware
|
|
||||||
{
|
|
||||||
public class MockMassiveKnobHardware : AbstractMassiveKnobHardware
|
|
||||||
{
|
|
||||||
private readonly int knobCount;
|
|
||||||
private readonly TimeSpan volumeChangeInterval;
|
|
||||||
private readonly int maxVolume;
|
|
||||||
private Timer changeVolumeTimer;
|
|
||||||
private readonly Random random = new Random();
|
|
||||||
|
|
||||||
|
|
||||||
public MockMassiveKnobHardware(int knobCount, TimeSpan volumeChangeInterval, int maxVolume)
|
|
||||||
{
|
|
||||||
this.knobCount = knobCount;
|
|
||||||
this.volumeChangeInterval = volumeChangeInterval;
|
|
||||||
this.maxVolume = maxVolume;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public override async Task TryConnect()
|
|
||||||
{
|
|
||||||
if (changeVolumeTimer != null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
await Task.Delay(2000);
|
|
||||||
|
|
||||||
Observers.Connected(knobCount);
|
|
||||||
changeVolumeTimer = new Timer(
|
|
||||||
state =>
|
|
||||||
{
|
|
||||||
Observers.VolumeChanged(random.Next(0, knobCount), random.Next(0, maxVolume));
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
volumeChangeInterval,
|
|
||||||
volumeChangeInterval);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public override Task Disconnect()
|
|
||||||
{
|
|
||||||
changeVolumeTimer?.Dispose();
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// ReSharper disable once UnusedMember.Global - for testing purposes only
|
|
||||||
public class MockMassiveKnobHardwareFactory : IMassiveKnobHardwareFactory
|
|
||||||
{
|
|
||||||
private readonly int knobCount;
|
|
||||||
private readonly TimeSpan volumeChangeInterval;
|
|
||||||
private readonly int maxVolume;
|
|
||||||
|
|
||||||
public MockMassiveKnobHardwareFactory(int knobCount, TimeSpan volumeChangeInterval, int maxVolume)
|
|
||||||
{
|
|
||||||
this.knobCount = knobCount;
|
|
||||||
this.volumeChangeInterval = volumeChangeInterval;
|
|
||||||
this.maxVolume = maxVolume;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public IMassiveKnobHardware Create(string portName)
|
|
||||||
{
|
|
||||||
return new MockMassiveKnobHardware(knobCount, volumeChangeInterval, maxVolume);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,180 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Binary file not shown.
Before Width: | Height: | Size: 169 KiB |
@ -0,0 +1,12 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace MassiveKnob.Plugin.CoreAudio.Base
|
||||||
|
{
|
||||||
|
public class BaseDeviceSettings
|
||||||
|
{
|
||||||
|
public Guid? DeviceId { get; set; }
|
||||||
|
|
||||||
|
// TODO (nice to have) more options, like positioning and style
|
||||||
|
public bool OSD { get; set; } = true;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
<UserControl x:Class="MassiveKnob.Plugin.CoreAudio.Base.BaseDeviceSettingsView"
|
||||||
|
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:base="clr-namespace:MassiveKnob.Plugin.CoreAudio.Base"
|
||||||
|
xmlns:coreAudio="clr-namespace:MassiveKnob.Plugin.CoreAudio"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DesignHeight="200" d:DesignWidth="800"
|
||||||
|
d:DataContext="{d:DesignInstance base:BaseDeviceSettingsViewModel}">
|
||||||
|
<StackPanel Orientation="Vertical">
|
||||||
|
<TextBlock Text="{x:Static coreAudio:Strings.SettingPlaybackDevice}" />
|
||||||
|
<ComboBox Margin="0,4,0,0" ItemsSource="{Binding PlaybackDevices}" SelectedItem="{Binding SelectedDevice}" DisplayMemberPath="DisplayName" />
|
||||||
|
|
||||||
|
<CheckBox Margin="0,8,0,0" IsChecked="{Binding OSD}">
|
||||||
|
<TextBlock Text="{x:Static coreAudio:Strings.SettingOSD}" />
|
||||||
|
</CheckBox>
|
||||||
|
</StackPanel>
|
||||||
|
</UserControl>
|
@ -0,0 +1,13 @@
|
|||||||
|
namespace MassiveKnob.Plugin.CoreAudio.Base
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Interaction logic for BaseDeviceSettingsView.xaml
|
||||||
|
/// </summary>
|
||||||
|
public partial class BaseDeviceSettingsView
|
||||||
|
{
|
||||||
|
public BaseDeviceSettingsView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,135 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows;
|
||||||
|
using AudioSwitcher.AudioApi;
|
||||||
|
|
||||||
|
namespace MassiveKnob.Plugin.CoreAudio.Base
|
||||||
|
{
|
||||||
|
public class BaseDeviceSettingsViewModel<T> : BaseDeviceSettingsViewModel where T : BaseDeviceSettings
|
||||||
|
{
|
||||||
|
protected new T Settings => (T)base.Settings;
|
||||||
|
|
||||||
|
public BaseDeviceSettingsViewModel(T settings) : base(settings)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public class BaseDeviceSettingsViewModel : INotifyPropertyChanged
|
||||||
|
{
|
||||||
|
protected readonly BaseDeviceSettings Settings;
|
||||||
|
public event PropertyChangedEventHandler PropertyChanged;
|
||||||
|
|
||||||
|
private IList<PlaybackDeviceViewModel> playbackDevices;
|
||||||
|
private PlaybackDeviceViewModel selectedDevice;
|
||||||
|
|
||||||
|
// ReSharper disable UnusedMember.Global - used by WPF Binding
|
||||||
|
public IList<PlaybackDeviceViewModel> PlaybackDevices
|
||||||
|
{
|
||||||
|
get => playbackDevices;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
playbackDevices = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlaybackDeviceViewModel SelectedDevice
|
||||||
|
{
|
||||||
|
get => selectedDevice;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == selectedDevice)
|
||||||
|
return;
|
||||||
|
|
||||||
|
selectedDevice = value;
|
||||||
|
Settings.DeviceId = selectedDevice?.Id;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public bool OSD
|
||||||
|
{
|
||||||
|
get => Settings.OSD;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == Settings.OSD)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Settings.OSD = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ReSharper restore UnusedMember.Global
|
||||||
|
|
||||||
|
|
||||||
|
public BaseDeviceSettingsViewModel(BaseDeviceSettings settings)
|
||||||
|
{
|
||||||
|
Settings = settings;
|
||||||
|
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
var coreAudioController = CoreAudioControllerInstance.Acquire();
|
||||||
|
var devices = await coreAudioController.GetPlaybackDevicesAsync();
|
||||||
|
var deviceViewModels = devices
|
||||||
|
.OrderBy(d => d.FullName)
|
||||||
|
.Select(PlaybackDeviceViewModel.FromDevice)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
Application.Current.Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
PlaybackDevices = deviceViewModels;
|
||||||
|
SelectedDevice = deviceViewModels.SingleOrDefault(d => d.Id == settings.DeviceId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public virtual bool IsSettingsProperty(string propertyName)
|
||||||
|
{
|
||||||
|
return propertyName != nameof(PlaybackDevices);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
|
||||||
|
{
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public class PlaybackDeviceViewModel
|
||||||
|
{
|
||||||
|
public Guid Id { get; private set; }
|
||||||
|
public string DisplayName { get; private set; }
|
||||||
|
|
||||||
|
|
||||||
|
public static PlaybackDeviceViewModel FromDevice(IDevice device)
|
||||||
|
{
|
||||||
|
string displayFormat;
|
||||||
|
|
||||||
|
if ((device.State & DeviceState.Disabled) != 0)
|
||||||
|
displayFormat = Strings.DeviceDisplayNameDisabled;
|
||||||
|
else if ((device.State & DeviceState.Unplugged) != 0)
|
||||||
|
displayFormat = Strings.DeviceDisplayNameUnplugged;
|
||||||
|
else if ((device.State & DeviceState.NotPresent) != 0)
|
||||||
|
displayFormat = Strings.DeviceDisplayNameNotPresent;
|
||||||
|
else if ((device.State & DeviceState.Active) == 0)
|
||||||
|
displayFormat = Strings.DeviceDisplayNameInactive;
|
||||||
|
else
|
||||||
|
displayFormat = Strings.DeviceDisplayNameActive;
|
||||||
|
|
||||||
|
return new PlaybackDeviceViewModel
|
||||||
|
{
|
||||||
|
Id = device.Id,
|
||||||
|
DisplayName = string.Format(displayFormat, device.FullName)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
using System;
|
||||||
|
using AudioSwitcher.AudioApi.CoreAudio;
|
||||||
|
|
||||||
|
namespace MassiveKnob.Plugin.CoreAudio
|
||||||
|
{
|
||||||
|
public static class CoreAudioControllerInstance
|
||||||
|
{
|
||||||
|
private static readonly Lazy<CoreAudioController> Instance = new Lazy<CoreAudioController>();
|
||||||
|
|
||||||
|
|
||||||
|
public static CoreAudioController Acquire()
|
||||||
|
{
|
||||||
|
return Instance.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,101 @@
|
|||||||
|
using System;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using AudioSwitcher.AudioApi;
|
||||||
|
using MassiveKnob.Plugin.CoreAudio.OSD;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MassiveKnob.Plugin.CoreAudio.GetDefault
|
||||||
|
{
|
||||||
|
public class DeviceGetDefaultAction : IMassiveKnobAction
|
||||||
|
{
|
||||||
|
public Guid ActionId { get; } = new Guid("3c427e28-493f-489f-abb3-1a7ef23ca6c9");
|
||||||
|
public MassiveKnobActionType ActionType { get; } = MassiveKnobActionType.OutputDigital;
|
||||||
|
public string Name { get; } = Strings.GetDefaultName;
|
||||||
|
public string Description { get; } = Strings.GetDefaultDescription;
|
||||||
|
|
||||||
|
|
||||||
|
public IMassiveKnobActionInstance Create(ILogger logger)
|
||||||
|
{
|
||||||
|
return new Instance();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private class Instance : IMassiveKnobActionInstance
|
||||||
|
{
|
||||||
|
private IMassiveKnobActionContext actionContext;
|
||||||
|
private DeviceGetDefaultActionSettings settings;
|
||||||
|
private IDevice playbackDevice;
|
||||||
|
private IDisposable deviceChanged;
|
||||||
|
|
||||||
|
|
||||||
|
public void Initialize(IMassiveKnobActionContext context)
|
||||||
|
{
|
||||||
|
actionContext = context;
|
||||||
|
settings = context.GetSettings<DeviceGetDefaultActionSettings>();
|
||||||
|
ApplySettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
deviceChanged?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void ApplySettings()
|
||||||
|
{
|
||||||
|
if (playbackDevice != null && playbackDevice.Id == settings.DeviceId)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var coreAudioController = CoreAudioControllerInstance.Acquire();
|
||||||
|
playbackDevice = settings.DeviceId.HasValue ? coreAudioController.GetDevice(settings.DeviceId.Value) : null;
|
||||||
|
|
||||||
|
deviceChanged?.Dispose();
|
||||||
|
deviceChanged = playbackDevice?.PropertyChanged.Subscribe(PropertyChanged);
|
||||||
|
|
||||||
|
CheckActive();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public UserControl CreateSettingsControl()
|
||||||
|
{
|
||||||
|
var viewModel = new DeviceGetDefaultActionSettingsViewModel(settings);
|
||||||
|
viewModel.PropertyChanged += (sender, args) =>
|
||||||
|
{
|
||||||
|
if (!viewModel.IsSettingsProperty(args.PropertyName))
|
||||||
|
return;
|
||||||
|
|
||||||
|
actionContext.SetSettings(settings);
|
||||||
|
ApplySettings();
|
||||||
|
};
|
||||||
|
|
||||||
|
return new DeviceGetDefaultActionSettingsView(viewModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void PropertyChanged(DevicePropertyChangedArgs args)
|
||||||
|
{
|
||||||
|
if (args.ChangedType != DeviceChangedType.DefaultChanged)
|
||||||
|
return;
|
||||||
|
|
||||||
|
CheckActive();
|
||||||
|
|
||||||
|
// TODO default OSD
|
||||||
|
//if (settings.OSD)
|
||||||
|
//OSDManager.Show(args.Device);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void CheckActive()
|
||||||
|
{
|
||||||
|
if (playbackDevice == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var isDefault = (settings.Playback && playbackDevice.IsDefaultDevice) ||
|
||||||
|
(settings.Communications && playbackDevice.IsDefaultCommunicationsDevice);
|
||||||
|
|
||||||
|
actionContext.SetDigitalOutput(settings.Inverted ? !isDefault : isDefault);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
using MassiveKnob.Plugin.CoreAudio.Base;
|
||||||
|
|
||||||
|
namespace MassiveKnob.Plugin.CoreAudio.GetDefault
|
||||||
|
{
|
||||||
|
public class DeviceGetDefaultActionSettings : BaseDeviceSettings
|
||||||
|
{
|
||||||
|
public bool Playback { get; set; } = true;
|
||||||
|
public bool Communications { get; set; } = true;
|
||||||
|
|
||||||
|
public bool Inverted { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
<UserControl x:Class="MassiveKnob.Plugin.CoreAudio.GetDefault.DeviceGetDefaultActionSettingsView"
|
||||||
|
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:getDefault="clr-namespace:MassiveKnob.Plugin.CoreAudio.GetDefault"
|
||||||
|
xmlns:base="clr-namespace:MassiveKnob.Plugin.CoreAudio.Base"
|
||||||
|
xmlns:coreAudio="clr-namespace:MassiveKnob.Plugin.CoreAudio"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DesignHeight="200" d:DesignWidth="800"
|
||||||
|
d:DataContext="{d:DesignInstance getDefault:DeviceGetDefaultActionSettingsViewModel}">
|
||||||
|
<StackPanel Orientation="Vertical">
|
||||||
|
<base:BaseDeviceSettingsView />
|
||||||
|
|
||||||
|
<TextBlock Margin="0,24,0,0" Text="{x:Static coreAudio:Strings.SettingGetDefaultWhen}" />
|
||||||
|
<CheckBox Margin="0,8,0,0" IsChecked="{Binding Playback}">
|
||||||
|
<TextBlock Text="{x:Static coreAudio:Strings.SettingGetDefaultPlayback}" />
|
||||||
|
</CheckBox>
|
||||||
|
|
||||||
|
<CheckBox Margin="0,8,0,0" IsChecked="{Binding Communications}">
|
||||||
|
<TextBlock Text="{x:Static coreAudio:Strings.SettingGetDefaultCommunications}" />
|
||||||
|
</CheckBox>
|
||||||
|
|
||||||
|
<CheckBox Margin="0,24,0,0" IsChecked="{Binding Inverted}">
|
||||||
|
<TextBlock Text="{x:Static coreAudio:Strings.SettingGetDefaultInverted}" />
|
||||||
|
</CheckBox>
|
||||||
|
</StackPanel>
|
||||||
|
</UserControl>
|
@ -0,0 +1,14 @@
|
|||||||
|
namespace MassiveKnob.Plugin.CoreAudio.GetDefault
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Interaction logic for DeviceGetDefaultActionSettingsView.xaml
|
||||||
|
/// </summary>
|
||||||
|
public partial class DeviceGetDefaultActionSettingsView
|
||||||
|
{
|
||||||
|
public DeviceGetDefaultActionSettingsView(DeviceGetDefaultActionSettingsViewModel viewModel)
|
||||||
|
{
|
||||||
|
DataContext = viewModel;
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,54 @@
|
|||||||
|
using MassiveKnob.Plugin.CoreAudio.Base;
|
||||||
|
|
||||||
|
namespace MassiveKnob.Plugin.CoreAudio.GetDefault
|
||||||
|
{
|
||||||
|
public class DeviceGetDefaultActionSettingsViewModel : BaseDeviceSettingsViewModel<DeviceGetDefaultActionSettings>
|
||||||
|
{
|
||||||
|
// ReSharper disable UnusedMember.Global - used by WPF Binding
|
||||||
|
public bool Playback
|
||||||
|
{
|
||||||
|
get => Settings.Playback;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == Settings.Playback)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Settings.Playback = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Communications
|
||||||
|
{
|
||||||
|
get => Settings.Communications;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == Settings.Communications)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Settings.Communications = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Inverted
|
||||||
|
{
|
||||||
|
get => Settings.Inverted;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == Settings.Inverted)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Settings.Inverted = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ReSharper restore UnusedMember.Global
|
||||||
|
|
||||||
|
|
||||||
|
// ReSharper disable once SuggestBaseTypeForParameter - by design
|
||||||
|
public DeviceGetDefaultActionSettingsViewModel(DeviceGetDefaultActionSettings settings) : base(settings)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,86 @@
|
|||||||
|
using System;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using AudioSwitcher.AudioApi;
|
||||||
|
using MassiveKnob.Plugin.CoreAudio.OSD;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MassiveKnob.Plugin.CoreAudio.GetMuted
|
||||||
|
{
|
||||||
|
public class DeviceGetMutedAction : IMassiveKnobAction
|
||||||
|
{
|
||||||
|
public Guid ActionId { get; } = new Guid("86646ca7-f472-4c5a-8d0f-7e5d2d162ab9");
|
||||||
|
public MassiveKnobActionType ActionType { get; } = MassiveKnobActionType.OutputDigital;
|
||||||
|
public string Name { get; } = Strings.GetMutedName;
|
||||||
|
public string Description { get; } = Strings.GetMutedDescription;
|
||||||
|
|
||||||
|
|
||||||
|
public IMassiveKnobActionInstance Create(ILogger logger)
|
||||||
|
{
|
||||||
|
return new Instance();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private class Instance : IMassiveKnobActionInstance
|
||||||
|
{
|
||||||
|
private IMassiveKnobActionContext actionContext;
|
||||||
|
private DeviceGetMutedActionSettings settings;
|
||||||
|
private IDevice playbackDevice;
|
||||||
|
private IDisposable deviceChanged;
|
||||||
|
|
||||||
|
|
||||||
|
public void Initialize(IMassiveKnobActionContext context)
|
||||||
|
{
|
||||||
|
actionContext = context;
|
||||||
|
settings = context.GetSettings<DeviceGetMutedActionSettings>();
|
||||||
|
ApplySettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
deviceChanged?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void ApplySettings()
|
||||||
|
{
|
||||||
|
if (playbackDevice != null && playbackDevice.Id == settings.DeviceId)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var coreAudioController = CoreAudioControllerInstance.Acquire();
|
||||||
|
playbackDevice = settings.DeviceId.HasValue ? coreAudioController.GetDevice(settings.DeviceId.Value) : null;
|
||||||
|
|
||||||
|
deviceChanged?.Dispose();
|
||||||
|
deviceChanged = playbackDevice?.MuteChanged.Subscribe(MuteChanged);
|
||||||
|
|
||||||
|
if (playbackDevice != null)
|
||||||
|
actionContext.SetDigitalOutput(settings.Inverted ? !playbackDevice.IsMuted : playbackDevice.IsMuted);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public UserControl CreateSettingsControl()
|
||||||
|
{
|
||||||
|
var viewModel = new DeviceGetMutedActionSettingsViewModel(settings);
|
||||||
|
viewModel.PropertyChanged += (sender, args) =>
|
||||||
|
{
|
||||||
|
if (!viewModel.IsSettingsProperty(args.PropertyName))
|
||||||
|
return;
|
||||||
|
|
||||||
|
actionContext.SetSettings(settings);
|
||||||
|
ApplySettings();
|
||||||
|
};
|
||||||
|
|
||||||
|
return new DeviceGetMutedActionSettingsView(viewModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void MuteChanged(DeviceMuteChangedArgs args)
|
||||||
|
{
|
||||||
|
actionContext.SetDigitalOutput(settings.Inverted ? !args.IsMuted : args.IsMuted);
|
||||||
|
|
||||||
|
if (settings.OSD)
|
||||||
|
OSDManager.Show(args.Device);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
using MassiveKnob.Plugin.CoreAudio.Base;
|
||||||
|
|
||||||
|
namespace MassiveKnob.Plugin.CoreAudio.GetMuted
|
||||||
|
{
|
||||||
|
public class DeviceGetMutedActionSettings : BaseDeviceSettings
|
||||||
|
{
|
||||||
|
public bool Inverted { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
<UserControl x:Class="MassiveKnob.Plugin.CoreAudio.GetMuted.DeviceGetMutedActionSettingsView"
|
||||||
|
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:getMuted="clr-namespace:MassiveKnob.Plugin.CoreAudio.GetMuted"
|
||||||
|
xmlns:base="clr-namespace:MassiveKnob.Plugin.CoreAudio.Base"
|
||||||
|
xmlns:coreAudio="clr-namespace:MassiveKnob.Plugin.CoreAudio"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DesignHeight="200" d:DesignWidth="800"
|
||||||
|
d:DataContext="{d:DesignInstance getMuted:DeviceGetMutedActionSettingsViewModel}">
|
||||||
|
<StackPanel Orientation="Vertical">
|
||||||
|
<base:BaseDeviceSettingsView />
|
||||||
|
|
||||||
|
<CheckBox Margin="0,8,0,0" IsChecked="{Binding Inverted}">
|
||||||
|
<TextBlock Text="{x:Static coreAudio:Strings.SettingGetMutedInverted}" />
|
||||||
|
</CheckBox>
|
||||||
|
</StackPanel>
|
||||||
|
</UserControl>
|
@ -0,0 +1,14 @@
|
|||||||
|
namespace MassiveKnob.Plugin.CoreAudio.GetMuted
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Interaction logic for DeviceGetMutedActionSettingsView.xaml
|
||||||
|
/// </summary>
|
||||||
|
public partial class DeviceGetMutedActionSettingsView
|
||||||
|
{
|
||||||
|
public DeviceGetMutedActionSettingsView(DeviceGetMutedActionSettingsViewModel viewModel)
|
||||||
|
{
|
||||||
|
DataContext = viewModel;
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
using MassiveKnob.Plugin.CoreAudio.Base;
|
||||||
|
|
||||||
|
namespace MassiveKnob.Plugin.CoreAudio.GetMuted
|
||||||
|
{
|
||||||
|
public class DeviceGetMutedActionSettingsViewModel : BaseDeviceSettingsViewModel<DeviceGetMutedActionSettings>
|
||||||
|
{
|
||||||
|
// ReSharper disable UnusedMember.Global - used by WPF Binding
|
||||||
|
public bool Inverted
|
||||||
|
{
|
||||||
|
get => Settings.Inverted;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == Settings.Inverted)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Settings.Inverted = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ReSharper restore UnusedMember.Global
|
||||||
|
|
||||||
|
|
||||||
|
// ReSharper disable once SuggestBaseTypeForParameter - by design
|
||||||
|
public DeviceGetMutedActionSettingsViewModel(DeviceGetMutedActionSettings settings) : base(settings)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,86 @@
|
|||||||
|
using System;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using AudioSwitcher.AudioApi;
|
||||||
|
using MassiveKnob.Plugin.CoreAudio.OSD;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MassiveKnob.Plugin.CoreAudio.GetVolume
|
||||||
|
{
|
||||||
|
public class DeviceGetVolumeAction : IMassiveKnobAction
|
||||||
|
{
|
||||||
|
public Guid ActionId { get; } = new Guid("6ebf91af-8240-4a75-9729-c6a1eb60dcba");
|
||||||
|
public MassiveKnobActionType ActionType { get; } = MassiveKnobActionType.OutputAnalog;
|
||||||
|
public string Name { get; } = Strings.GetVolumeName;
|
||||||
|
public string Description { get; } = Strings.GetVolumeDescription;
|
||||||
|
|
||||||
|
|
||||||
|
public IMassiveKnobActionInstance Create(ILogger logger)
|
||||||
|
{
|
||||||
|
return new Instance();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private class Instance : IMassiveKnobActionInstance
|
||||||
|
{
|
||||||
|
private IMassiveKnobActionContext actionContext;
|
||||||
|
private DeviceGetVolumeActionSettings settings;
|
||||||
|
private IDevice playbackDevice;
|
||||||
|
private IDisposable deviceChanged;
|
||||||
|
|
||||||
|
|
||||||
|
public void Initialize(IMassiveKnobActionContext context)
|
||||||
|
{
|
||||||
|
actionContext = context;
|
||||||
|
settings = context.GetSettings<DeviceGetVolumeActionSettings>();
|
||||||
|
ApplySettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
deviceChanged?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void ApplySettings()
|
||||||
|
{
|
||||||
|
if (playbackDevice != null && playbackDevice.Id == settings.DeviceId)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var coreAudioController = CoreAudioControllerInstance.Acquire();
|
||||||
|
playbackDevice = settings.DeviceId.HasValue ? coreAudioController.GetDevice(settings.DeviceId.Value) : null;
|
||||||
|
|
||||||
|
deviceChanged?.Dispose();
|
||||||
|
deviceChanged = playbackDevice?.VolumeChanged.Subscribe(VolumeChanged);
|
||||||
|
|
||||||
|
if (playbackDevice != null)
|
||||||
|
actionContext.SetAnalogOutput((byte)playbackDevice.Volume);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public UserControl CreateSettingsControl()
|
||||||
|
{
|
||||||
|
var viewModel = new DeviceGetVolumeActionSettingsViewModel(settings);
|
||||||
|
viewModel.PropertyChanged += (sender, args) =>
|
||||||
|
{
|
||||||
|
if (!viewModel.IsSettingsProperty(args.PropertyName))
|
||||||
|
return;
|
||||||
|
|
||||||
|
actionContext.SetSettings(settings);
|
||||||
|
ApplySettings();
|
||||||
|
};
|
||||||
|
|
||||||
|
return new DeviceGetVolumeActionSettingsView(viewModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void VolumeChanged(DeviceVolumeChangedArgs args)
|
||||||
|
{
|
||||||
|
actionContext.SetAnalogOutput((byte)args.Volume);
|
||||||
|
|
||||||
|
if (settings.OSD)
|
||||||
|
OSDManager.Show(args.Device);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
using MassiveKnob.Plugin.CoreAudio.Base;
|
||||||
|
|
||||||
|
namespace MassiveKnob.Plugin.CoreAudio.GetVolume
|
||||||
|
{
|
||||||
|
public class DeviceGetVolumeActionSettings : BaseDeviceSettings
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
<UserControl x:Class="MassiveKnob.Plugin.CoreAudio.GetVolume.DeviceGetVolumeActionSettingsView"
|
||||||
|
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:getVolume="clr-namespace:MassiveKnob.Plugin.CoreAudio.GetVolume"
|
||||||
|
xmlns:base="clr-namespace:MassiveKnob.Plugin.CoreAudio.Base"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DesignHeight="200" d:DesignWidth="800"
|
||||||
|
d:DataContext="{d:DesignInstance getVolume:DeviceGetVolumeActionSettingsViewModel}">
|
||||||
|
<StackPanel Orientation="Vertical">
|
||||||
|
<base:BaseDeviceSettingsView />
|
||||||
|
</StackPanel>
|
||||||
|
</UserControl>
|
@ -0,0 +1,14 @@
|
|||||||
|
namespace MassiveKnob.Plugin.CoreAudio.GetVolume
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Interaction logic for DeviceGetVolumeActionSettingsView.xaml
|
||||||
|
/// </summary>
|
||||||
|
public partial class DeviceGetVolumeActionSettingsView
|
||||||
|
{
|
||||||
|
public DeviceGetVolumeActionSettingsView(DeviceGetVolumeActionSettingsViewModel viewModel)
|
||||||
|
{
|
||||||
|
DataContext = viewModel;
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
using MassiveKnob.Plugin.CoreAudio.Base;
|
||||||
|
|
||||||
|
namespace MassiveKnob.Plugin.CoreAudio.GetVolume
|
||||||
|
{
|
||||||
|
public class DeviceGetVolumeActionSettingsViewModel : BaseDeviceSettingsViewModel<DeviceGetVolumeActionSettings>
|
||||||
|
{
|
||||||
|
// ReSharper disable once SuggestBaseTypeForParameter - by design
|
||||||
|
public DeviceGetVolumeActionSettingsViewModel(DeviceGetVolumeActionSettings settings) : base(settings)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,175 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||||
|
<PropertyGroup>
|
||||||
|
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||||
|
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||||
|
<ProjectGuid>{5BD5E2F2-9923-4F74-AC69-ACDA0B847937}</ProjectGuid>
|
||||||
|
<OutputType>Library</OutputType>
|
||||||
|
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||||
|
<RootNamespace>MassiveKnob.Plugin.CoreAudio</RootNamespace>
|
||||||
|
<AssemblyName>MassiveKnob.Plugin.CoreAudio</AssemblyName>
|
||||||
|
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
|
||||||
|
<FileAlignment>512</FileAlignment>
|
||||||
|
<Deterministic>true</Deterministic>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||||
|
<DebugSymbols>true</DebugSymbols>
|
||||||
|
<DebugType>full</DebugType>
|
||||||
|
<Optimize>false</Optimize>
|
||||||
|
<OutputPath>bin\Debug\</OutputPath>
|
||||||
|
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||||
|
<ErrorReport>prompt</ErrorReport>
|
||||||
|
<WarningLevel>4</WarningLevel>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||||
|
<DebugType>pdbonly</DebugType>
|
||||||
|
<Optimize>true</Optimize>
|
||||||
|
<OutputPath>bin\Release\</OutputPath>
|
||||||
|
<DefineConstants>TRACE</DefineConstants>
|
||||||
|
<ErrorReport>prompt</ErrorReport>
|
||||||
|
<WarningLevel>4</WarningLevel>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="PresentationCore" />
|
||||||
|
<Reference Include="PresentationFramework" />
|
||||||
|
<Reference Include="System" />
|
||||||
|
<Reference Include="System.Core" />
|
||||||
|
<Reference Include="System.Drawing" />
|
||||||
|
<Reference Include="System.Xaml" />
|
||||||
|
<Reference Include="System.Xml.Linq" />
|
||||||
|
<Reference Include="System.Data.DataSetExtensions" />
|
||||||
|
<Reference Include="Microsoft.CSharp" />
|
||||||
|
<Reference Include="System.Data" />
|
||||||
|
<Reference Include="System.Net.Http" />
|
||||||
|
<Reference Include="System.Xml" />
|
||||||
|
<Reference Include="WindowsBase" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="Base\BaseDeviceSettingsView.xaml.cs">
|
||||||
|
<DependentUpon>BaseDeviceSettingsView.xaml</DependentUpon>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="GetDefault\DeviceGetDefaultAction.cs" />
|
||||||
|
<Compile Include="GetDefault\DeviceGetDefaultActionSettings.cs" />
|
||||||
|
<Compile Include="GetDefault\DeviceGetDefaultActionSettingsView.xaml.cs">
|
||||||
|
<DependentUpon>DeviceGetDefaultActionSettingsView.xaml</DependentUpon>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="GetDefault\DeviceGetDefaultActionSettingsViewModel.cs" />
|
||||||
|
<Compile Include="GetMuted\DeviceGetMutedAction.cs" />
|
||||||
|
<Compile Include="GetMuted\DeviceGetMutedActionSettings.cs" />
|
||||||
|
<Compile Include="GetMuted\DeviceGetMutedActionSettingsView.xaml.cs">
|
||||||
|
<DependentUpon>DeviceGetMutedActionSettingsView.xaml</DependentUpon>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="GetMuted\DeviceGetMutedActionSettingsViewModel.cs" />
|
||||||
|
<Compile Include="GetVolume\DeviceGetVolumeAction.cs" />
|
||||||
|
<Compile Include="OSD\OSDWindow.xaml.cs">
|
||||||
|
<DependentUpon>OSDWindow.xaml</DependentUpon>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="OSD\OSDManager.cs" />
|
||||||
|
<Compile Include="OSD\OSDWindowViewModel.cs" />
|
||||||
|
<Compile Include="SetDefault\DeviceSetDefaultAction.cs" />
|
||||||
|
<Compile Include="SetDefault\DeviceSetDefaultActionSettings.cs" />
|
||||||
|
<Compile Include="SetDefault\DeviceSetDefaultActionSettingsView.xaml.cs">
|
||||||
|
<DependentUpon>DeviceSetDefaultActionSettingsView.xaml</DependentUpon>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="SetDefault\DeviceSetDefaultActionSettingsViewModel.cs" />
|
||||||
|
<Compile Include="SetMuted\DeviceSetMutedAction.cs" />
|
||||||
|
<Compile Include="SetMuted\DeviceSetMutedActionSettings.cs" />
|
||||||
|
<Compile Include="SetMuted\DeviceSetMutedActionSettingsView.xaml.cs">
|
||||||
|
<DependentUpon>DeviceSetMutedActionSettingsView.xaml</DependentUpon>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="SetMuted\DeviceSetMutedActionSettingsViewModel.cs" />
|
||||||
|
<Compile Include="SetVolume\DeviceSetVolumeAction.cs" />
|
||||||
|
<Compile Include="CoreAudioControllerInstance.cs" />
|
||||||
|
<Compile Include="GetVolume\DeviceGetVolumeActionSettingsViewModel.cs" />
|
||||||
|
<Compile Include="GetVolume\DeviceGetVolumeActionSettingsView.xaml.cs">
|
||||||
|
<DependentUpon>DeviceGetVolumeActionSettingsView.xaml</DependentUpon>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="MassiveKnobCoreAudioPlugin.cs" />
|
||||||
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
|
<Compile Include="GetVolume\DeviceGetVolumeActionSettings.cs" />
|
||||||
|
<Compile Include="SetVolume\DeviceSetVolumeActionSettings.cs" />
|
||||||
|
<Compile Include="SetVolume\DeviceSetVolumeActionSettingsView.xaml.cs">
|
||||||
|
<DependentUpon>DeviceSetVolumeActionSettingsView.xaml</DependentUpon>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="Base\BaseDeviceSettings.cs" />
|
||||||
|
<Compile Include="Base\BaseDeviceSettingsViewModel.cs" />
|
||||||
|
<Compile Include="SetVolume\DeviceSetVolumeActionSettingsViewModel.cs" />
|
||||||
|
<Compile Include="Strings.Designer.cs">
|
||||||
|
<DependentUpon>Strings.resx</DependentUpon>
|
||||||
|
<AutoGen>True</AutoGen>
|
||||||
|
<DesignTime>True</DesignTime>
|
||||||
|
</Compile>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\MassiveKnob.Plugin\MassiveKnob.Plugin.csproj">
|
||||||
|
<Project>{a1298be4-1d23-416c-8c56-fc9264487a95}</Project>
|
||||||
|
<Name>MassiveKnob.Plugin</Name>
|
||||||
|
</ProjectReference>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="AudioSwitcher.AudioApi.CoreAudio">
|
||||||
|
<Version>4.0.0-alpha5</Version>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions">
|
||||||
|
<Version>5.0.0</Version>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="System.Reactive">
|
||||||
|
<Version>5.0.0</Version>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="System.Threading.Tasks.Extensions">
|
||||||
|
<Version>4.5.4</Version>
|
||||||
|
</PackageReference>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<EmbeddedResource Include="Strings.resx">
|
||||||
|
<Generator>PublicResXFileCodeGenerator</Generator>
|
||||||
|
<LastGenOutput>Strings.Designer.cs</LastGenOutput>
|
||||||
|
</EmbeddedResource>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Page Include="Base\BaseDeviceSettingsView.xaml">
|
||||||
|
<SubType>Designer</SubType>
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
</Page>
|
||||||
|
<Page Include="GetDefault\DeviceGetDefaultActionSettingsView.xaml">
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
<SubType>Designer</SubType>
|
||||||
|
</Page>
|
||||||
|
<Page Include="GetMuted\DeviceGetMutedActionSettingsView.xaml">
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
<SubType>Designer</SubType>
|
||||||
|
</Page>
|
||||||
|
<Page Include="GetVolume\DeviceGetVolumeActionSettingsView.xaml">
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
<SubType>Designer</SubType>
|
||||||
|
</Page>
|
||||||
|
<Page Include="OSD\OSDWindow.xaml">
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
<SubType>Designer</SubType>
|
||||||
|
</Page>
|
||||||
|
<Page Include="OSD\SpeakerIcon.xaml">
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
<SubType>Designer</SubType>
|
||||||
|
</Page>
|
||||||
|
<Page Include="SetDefault\DeviceSetDefaultActionSettingsView.xaml">
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
<SubType>Designer</SubType>
|
||||||
|
</Page>
|
||||||
|
<Page Include="SetMuted\DeviceSetMutedActionSettingsView.xaml">
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
<SubType>Designer</SubType>
|
||||||
|
</Page>
|
||||||
|
<Page Include="SetVolume\DeviceSetVolumeActionSettingsView.xaml">
|
||||||
|
<SubType>Designer</SubType>
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
</Page>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="MassiveKnobPlugin.json">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup />
|
||||||
|
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||||
|
</Project>
|
@ -0,0 +1,46 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using MassiveKnob.Plugin.CoreAudio.GetDefault;
|
||||||
|
using MassiveKnob.Plugin.CoreAudio.GetMuted;
|
||||||
|
using MassiveKnob.Plugin.CoreAudio.GetVolume;
|
||||||
|
using MassiveKnob.Plugin.CoreAudio.SetDefault;
|
||||||
|
using MassiveKnob.Plugin.CoreAudio.SetMuted;
|
||||||
|
using MassiveKnob.Plugin.CoreAudio.SetVolume;
|
||||||
|
|
||||||
|
namespace MassiveKnob.Plugin.CoreAudio
|
||||||
|
{
|
||||||
|
[MassiveKnobPlugin]
|
||||||
|
public class MassiveKnobCoreAudioPlugin : IMassiveKnobActionPlugin
|
||||||
|
{
|
||||||
|
public Guid PluginId { get; } = new Guid("eaa5d3f8-8f9b-4a4b-8e29-827228d23e95");
|
||||||
|
public string Name { get; } = Strings.PluginName;
|
||||||
|
public string Description { get; } = Strings.PluginDescription;
|
||||||
|
public string Author { get; } = "Mark van Renswoude <mark@x2software.net>";
|
||||||
|
public string Url { get; } = "https://www.github.com/MvRens/MassiveKnob/";
|
||||||
|
|
||||||
|
public IEnumerable<IMassiveKnobAction> Actions { get; } = new IMassiveKnobAction[]
|
||||||
|
{
|
||||||
|
new DeviceSetVolumeAction(),
|
||||||
|
new DeviceGetVolumeAction(),
|
||||||
|
|
||||||
|
new DeviceSetMutedAction(),
|
||||||
|
new DeviceGetMutedAction(),
|
||||||
|
|
||||||
|
new DeviceSetDefaultAction(),
|
||||||
|
new DeviceGetDefaultAction()
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
public MassiveKnobCoreAudioPlugin()
|
||||||
|
{
|
||||||
|
// My system suffers from this issue: https://github.com/xenolightning/AudioSwitcher/issues/40
|
||||||
|
// ...which causes the first call to the CoreAudioController to take up to 10 seconds,
|
||||||
|
// so initialise it as soon as possible. Bit of a workaround, but eh.
|
||||||
|
Task.Run(() =>
|
||||||
|
{
|
||||||
|
CoreAudioControllerInstance.Acquire();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"EntryAssembly": "MassiveKnob.Plugin.CoreAudio.dll"
|
||||||
|
}
|
60
Windows/MassiveKnob.Plugin.CoreAudio/OSD/OSDManager.cs
Normal file
60
Windows/MassiveKnob.Plugin.CoreAudio/OSD/OSDManager.cs
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Windows;
|
||||||
|
using AudioSwitcher.AudioApi;
|
||||||
|
|
||||||
|
namespace MassiveKnob.Plugin.CoreAudio.OSD
|
||||||
|
{
|
||||||
|
public static class OSDManager
|
||||||
|
{
|
||||||
|
private const int OSDTimeout = 2500;
|
||||||
|
|
||||||
|
private static OSDWindowViewModel windowViewModel;
|
||||||
|
private static Window window;
|
||||||
|
private static Timer hideTimer;
|
||||||
|
|
||||||
|
public static void Show(IDevice device)
|
||||||
|
{
|
||||||
|
Application.Current.Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
if (window == null)
|
||||||
|
{
|
||||||
|
windowViewModel = new OSDWindowViewModel();
|
||||||
|
window = new OSDWindow(windowViewModel);
|
||||||
|
window.Closed += WindowOnClosed;
|
||||||
|
|
||||||
|
hideTimer = new Timer(state =>
|
||||||
|
{
|
||||||
|
Hide();
|
||||||
|
}, null, OSDTimeout, Timeout.Infinite);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
hideTimer.Change(OSDTimeout, Timeout.Infinite);
|
||||||
|
|
||||||
|
windowViewModel.SetDevice(device);
|
||||||
|
window.Show();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static void WindowOnClosed(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
hideTimer?.Dispose();
|
||||||
|
hideTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static void Hide()
|
||||||
|
{
|
||||||
|
Application.Current?.Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
window?.Close();
|
||||||
|
window = null;
|
||||||
|
windowViewModel = null;
|
||||||
|
|
||||||
|
hideTimer?.Dispose();
|
||||||
|
hideTimer = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
55
Windows/MassiveKnob.Plugin.CoreAudio/OSD/OSDWindow.xaml
Normal file
55
Windows/MassiveKnob.Plugin.CoreAudio/OSD/OSDWindow.xaml
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
<Window x:Class="MassiveKnob.Plugin.CoreAudio.OSD.OSDWindow"
|
||||||
|
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"
|
||||||
|
xmlns:osd="clr-namespace:MassiveKnob.Plugin.CoreAudio.OSD"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
Title="Massive Knob OSD" Height="60" Width="360"
|
||||||
|
WindowStartupLocation="Manual" WindowStyle="None" AllowsTransparency="True" ShowInTaskbar="False" Topmost="True"
|
||||||
|
Loaded="OSDWindow_OnLoaded" Closing="OSDWindow_OnClosing"
|
||||||
|
d:DataContext="{d:DesignInstance osd:OSDWindowViewModel}">
|
||||||
|
<Window.Triggers>
|
||||||
|
<EventTrigger RoutedEvent="Window.Loaded">
|
||||||
|
<BeginStoryboard>
|
||||||
|
<Storyboard>
|
||||||
|
<DoubleAnimation Storyboard.TargetProperty="Opacity" From="0" To="1" Duration="0:0:0.250" FillBehavior="HoldEnd" />
|
||||||
|
</Storyboard>
|
||||||
|
</BeginStoryboard>
|
||||||
|
</EventTrigger>
|
||||||
|
</Window.Triggers>
|
||||||
|
<Window.Resources>
|
||||||
|
<ResourceDictionary>
|
||||||
|
<ResourceDictionary.MergedDictionaries>
|
||||||
|
<ResourceDictionary Source="SpeakerIcon.xaml" />
|
||||||
|
</ResourceDictionary.MergedDictionaries>
|
||||||
|
|
||||||
|
<!-- ReSharper disable once Xaml.RedundantResource - used in runtime -->
|
||||||
|
<Storyboard x:Key="CloseStoryboard" Completed="CloseStoryboard_Completed">
|
||||||
|
<DoubleAnimation Storyboard.TargetProperty="Opacity" From="1" To="0" Duration="0:0:0.250" FillBehavior="HoldEnd" />
|
||||||
|
</Storyboard>
|
||||||
|
|
||||||
|
<Style TargetType="DockPanel" x:Key="OSDWindow">
|
||||||
|
<Setter Property="Background" Value="#2d2d30" />
|
||||||
|
</Style>
|
||||||
|
<Style TargetType="TextBlock" x:Key="DeviceName">
|
||||||
|
<Setter Property="Foreground" Value="White" />
|
||||||
|
<Setter Property="Margin" Value="8,4,8,4" />
|
||||||
|
<Setter Property="FontSize" Value="14" />
|
||||||
|
<Setter Property="FontWeight" Value="Bold" />
|
||||||
|
<Setter Property="TextTrimming" Value="CharacterEllipsis"></Setter>
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="SpeakerIconStyle">
|
||||||
|
<Setter Property="Control.Margin" Value="8,4,8,4" />
|
||||||
|
</Style>
|
||||||
|
</ResourceDictionary>
|
||||||
|
</Window.Resources>
|
||||||
|
<DockPanel Style="{StaticResource OSDWindow}">
|
||||||
|
<TextBlock DockPanel.Dock="Top" Text="{Binding DeviceName}" Style="{StaticResource DeviceName}"></TextBlock>
|
||||||
|
<ContentControl DockPanel.Dock="Left" Content="{StaticResource SpeakerIcon}" Style="{StaticResource SpeakerIconStyle}" />
|
||||||
|
<Canvas Width="300" Height="20" Margin="8,0,8,0">
|
||||||
|
<Line X1="0" X2="300" Y1="10" Y2="10" Stroke="#80FFFFFF" StrokeThickness="2" />
|
||||||
|
<Line X1="{Binding VolumeIndicatorLeft}" X2="{Binding VolumeIndicatorLeft}" Y1="0" Y2="20" Stroke="White" StrokeThickness="2" />
|
||||||
|
</Canvas>
|
||||||
|
</DockPanel>
|
||||||
|
</Window>
|
49
Windows/MassiveKnob.Plugin.CoreAudio/OSD/OSDWindow.xaml.cs
Normal file
49
Windows/MassiveKnob.Plugin.CoreAudio/OSD/OSDWindow.xaml.cs
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
using System;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
using System.Windows.Media.Animation;
|
||||||
|
|
||||||
|
namespace MassiveKnob.Plugin.CoreAudio.OSD
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Interaction logic for OSDWindow.xaml
|
||||||
|
/// </summary>
|
||||||
|
public partial class OSDWindow
|
||||||
|
{
|
||||||
|
private bool closeStoryBoardCompleted;
|
||||||
|
|
||||||
|
|
||||||
|
public OSDWindow(OSDWindowViewModel viewModel)
|
||||||
|
{
|
||||||
|
DataContext = viewModel;
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void OSDWindow_OnLoaded(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var desktopArea = Screen.PrimaryScreen.WorkingArea;
|
||||||
|
|
||||||
|
Left = (desktopArea.Width - Width) / 2;
|
||||||
|
Top = desktopArea.Bottom - Height - 25;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void OSDWindow_OnClosing(object sender, CancelEventArgs e)
|
||||||
|
{
|
||||||
|
if (closeStoryBoardCompleted)
|
||||||
|
return;
|
||||||
|
|
||||||
|
((Storyboard)FindResource("CloseStoryboard")).Begin(this);
|
||||||
|
e.Cancel = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void CloseStoryboard_Completed(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
closeStoryBoardCompleted = true;
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,91 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Windows;
|
||||||
|
using AudioSwitcher.AudioApi;
|
||||||
|
|
||||||
|
namespace MassiveKnob.Plugin.CoreAudio.OSD
|
||||||
|
{
|
||||||
|
public class OSDWindowViewModel : INotifyPropertyChanged
|
||||||
|
{
|
||||||
|
// ReSharper disable UnusedMember.Global - used by WPF Binding
|
||||||
|
private string deviceName;
|
||||||
|
public string DeviceName
|
||||||
|
{
|
||||||
|
get => deviceName;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == deviceName)
|
||||||
|
return;
|
||||||
|
|
||||||
|
deviceName = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int volume;
|
||||||
|
public int Volume
|
||||||
|
{
|
||||||
|
get => volume;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == volume)
|
||||||
|
return;
|
||||||
|
|
||||||
|
volume = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
OnDependantPropertyChanged(nameof(VolumeLowVisibility));
|
||||||
|
OnDependantPropertyChanged(nameof(VolumeMediumVisibility));
|
||||||
|
OnDependantPropertyChanged(nameof(VolumeHighVisibility));
|
||||||
|
OnDependantPropertyChanged(nameof(VolumeIndicatorLeft));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private bool isMuted;
|
||||||
|
public bool IsMuted
|
||||||
|
{
|
||||||
|
get => isMuted;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == isMuted)
|
||||||
|
return;
|
||||||
|
|
||||||
|
isMuted = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
OnDependantPropertyChanged(nameof(IsMutedVisibility));
|
||||||
|
OnDependantPropertyChanged(nameof(IsNotMutedVisibility));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public Visibility IsMutedVisibility => IsMuted ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
public Visibility IsNotMutedVisibility => IsMuted ? Visibility.Collapsed : Visibility.Visible;
|
||||||
|
public Visibility VolumeLowVisibility => Volume > 0 ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
public Visibility VolumeMediumVisibility => Volume > 33 ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
public Visibility VolumeHighVisibility => Volume > 66 ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
|
||||||
|
public int VolumeIndicatorLeft => Volume * 3;
|
||||||
|
// ReSharper enable UnusedMember.Global
|
||||||
|
|
||||||
|
|
||||||
|
public void SetDevice(IDevice device)
|
||||||
|
{
|
||||||
|
DeviceName = device.FullName;
|
||||||
|
Volume = (int)device.Volume;
|
||||||
|
IsMuted = device.IsMuted;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public event PropertyChangedEventHandler PropertyChanged;
|
||||||
|
|
||||||
|
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
|
||||||
|
{
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void OnDependantPropertyChanged(string propertyName)
|
||||||
|
{
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
53
Windows/MassiveKnob.Plugin.CoreAudio/OSD/SpeakerIcon.xaml
Normal file
53
Windows/MassiveKnob.Plugin.CoreAudio/OSD/SpeakerIcon.xaml
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<!--
|
||||||
|
AI saved to SVG, converted to XAML using Inkscape, then modified manually to provide
|
||||||
|
interactivity. Be aware of this when overwriting this file.
|
||||||
|
-->
|
||||||
|
<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
|
||||||
|
xmlns:osd="clr-namespace:MassiveKnob.Plugin.CoreAudio.OSD"
|
||||||
|
Stretch="Uniform"
|
||||||
|
x:Key="SpeakerIcon"
|
||||||
|
d:DataContext="{d:DesignInstance osd:OSDWindowViewModel}">
|
||||||
|
<Canvas Width="256" Height="256">
|
||||||
|
<Polygon Visibility="{Binding IsNotMutedVisibility}"
|
||||||
|
Points=" 133.5,215.101 61.75,168 8.75,168 8.75,88 61.75,88 133.5,40.899 " Name="Speaker_1_" FillRule="NonZero" StrokeThickness="12" Stroke="#FFFFFFFF" StrokeMiterLimit="10" StrokeLineJoin="Round"/>
|
||||||
|
<Path Visibility="{Binding VolumeLowVisibility}" Name="Low" StrokeThickness="12" Stroke="#FFFFFFFF" StrokeMiterLimit="10" StrokeLineJoin="Round" StrokeStartLineCap="Round" StrokeEndLineCap="Round">
|
||||||
|
<Path.Data>
|
||||||
|
<PathGeometry Figures=" M166.806 86c0 0 12.528 15.833 12.528 40.167s-12.528 43.823-12.528 43.823" FillRule="NonZero"/>
|
||||||
|
</Path.Data>
|
||||||
|
</Path>
|
||||||
|
<Path Visibility="{Binding VolumeMediumVisibility}" Name="Medium" StrokeThickness="12" Stroke="#FFFFFFFF" StrokeMiterLimit="10" StrokeLineJoin="Round" StrokeStartLineCap="Round" StrokeEndLineCap="Round">
|
||||||
|
<Path.Data>
|
||||||
|
<PathGeometry Figures=" M188.479 57c0 0 21.183 26.769 21.183 67.91c0 41.141-21.183 74.089-21.183 74.089" FillRule="NonZero"/>
|
||||||
|
</Path.Data>
|
||||||
|
</Path>
|
||||||
|
<Path Visibility="{Binding VolumeHighVisibility}" Name="High" StrokeThickness="12" Stroke="#FFFFFFFF" StrokeMiterLimit="10" StrokeLineJoin="Round" StrokeStartLineCap="Round" StrokeEndLineCap="Round">
|
||||||
|
<Path.Data>
|
||||||
|
<PathGeometry Figures=" M216.737 35.517c0 0 27.944 35.316 27.944 89.593s-27.944 97.75-27.944 97.75" FillRule="NonZero"/>
|
||||||
|
</Path.Data>
|
||||||
|
</Path>
|
||||||
|
|
||||||
|
<Path Visibility="{Binding IsMutedVisibility}" Name="path4561" Fill="#FFFFFFFF">
|
||||||
|
<Path.Data>
|
||||||
|
<PathGeometry Figures="M160.503 221.101c-1.717 0-3.421-0.732-4.608-2.153L10.395 44.746c-2.125-2.543-1.785-6.327 0.759-8.451 c2.544-2.125 6.328-1.784 8.451 0.759l145.5 174.201c2.124 2.544 1.784 6.327-0.759 8.452 C163.224 220.644 161.859 221.101 160.503 221.101z" FillRule="NonZero"/>
|
||||||
|
</Path.Data>
|
||||||
|
</Path>
|
||||||
|
<Path Visibility="{Binding IsMutedVisibility}" Name="path4563" Fill="#FFFFFFFF">
|
||||||
|
<Path.Data>
|
||||||
|
<PathGeometry Figures="M127.5 203.984l-62.458-41C64.064 162.342 62.92 162 61.75 162h-47V94h28.967L33.694 82H8.75c-3.313 0-6 2.687-6 6v80 c0 3.313 2.687 6 6 6h51.207l70.25 46.116c0.997 0.654 2.144 0.984 3.293 0.984c0.979 0 1.958-0.238 2.85-0.72 c1.94-1.048 3.15-3.075 3.15-5.28v-6.423l-12-14.367V203.984z" FillRule="NonZero"/>
|
||||||
|
</Path.Data>
|
||||||
|
</Path>
|
||||||
|
<Path Visibility="{Binding IsMutedVisibility}" Name="path4565" Fill="#FFFFFFFF">
|
||||||
|
<Path.Data>
|
||||||
|
<PathGeometry Figures="M127.5 52.016v104.856l12 14.367V40.899c0-2.205-1.21-4.232-3.15-5.28c-1.939-1.047-4.299-0.947-6.143 0.264L63.19 79.877 l7.744 9.271L127.5 52.016z" FillRule="NonZero"/>
|
||||||
|
</Path.Data>
|
||||||
|
</Path>
|
||||||
|
</Canvas>
|
||||||
|
</Viewbox>
|
||||||
|
</ResourceDictionary>
|
||||||
|
|
@ -0,0 +1,35 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
// General Information about an assembly is controlled through the following
|
||||||
|
// set of attributes. Change these attribute values to modify the information
|
||||||
|
// associated with an assembly.
|
||||||
|
[assembly: AssemblyTitle("MassiveKnob.Plugin.CoreAudio")]
|
||||||
|
[assembly: AssemblyDescription("")]
|
||||||
|
[assembly: AssemblyConfiguration("")]
|
||||||
|
[assembly: AssemblyCompany("")]
|
||||||
|
[assembly: AssemblyProduct("MassiveKnob.Plugin.CoreAudio")]
|
||||||
|
[assembly: AssemblyCopyright("Copyright © 2021")]
|
||||||
|
[assembly: AssemblyTrademark("")]
|
||||||
|
[assembly: AssemblyCulture("")]
|
||||||
|
|
||||||
|
// Setting ComVisible to false makes the types in this assembly not visible
|
||||||
|
// to COM components. If you need to access a type in this assembly from
|
||||||
|
// COM, set the ComVisible attribute to true on that type.
|
||||||
|
[assembly: ComVisible(false)]
|
||||||
|
|
||||||
|
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||||
|
[assembly: Guid("5bd5e2f2-9923-4f74-ac69-acda0b847937")]
|
||||||
|
|
||||||
|
// Version information for an assembly consists of the following four values:
|
||||||
|
//
|
||||||
|
// Major Version
|
||||||
|
// Minor Version
|
||||||
|
// Build Number
|
||||||
|
// Revision
|
||||||
|
//
|
||||||
|
// You can specify all the values or you can default the Build and Revision Numbers
|
||||||
|
// by using the '*' as shown below:
|
||||||
|
// [assembly: AssemblyVersion("1.0.*")]
|
||||||
|
[assembly: AssemblyVersion("1.0.0.0")]
|
||||||
|
[assembly: AssemblyFileVersion("1.0.0.0")]
|
48
Windows/MassiveKnob.Plugin.CoreAudio/Resources/Muted.svg
Normal file
48
Windows/MassiveKnob.Plugin.CoreAudio/Resources/Muted.svg
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="256px"
|
||||||
|
height="256px" viewBox="0 0 256 256" enable-background="new 0 0 256 256" xml:space="preserve">
|
||||||
|
<g id="Speaker" display="none">
|
||||||
|
|
||||||
|
<polygon id="Speaker_1_" display="inline" fill="none" stroke="#000000" stroke-width="12" stroke-linejoin="round" stroke-miterlimit="10" points="
|
||||||
|
133.5,215.101 61.75,168 8.75,168 8.75,88 61.75,88 133.5,40.899 "/>
|
||||||
|
|
||||||
|
<path id="Low" display="inline" fill="none" stroke="#000000" stroke-width="12" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" d="
|
||||||
|
M166.806,86c0,0,12.528,15.833,12.528,40.167s-12.528,43.823-12.528,43.823"/>
|
||||||
|
|
||||||
|
<path id="Medium" display="inline" fill="none" stroke="#000000" stroke-width="12" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" d="
|
||||||
|
M188.479,57c0,0,21.183,26.769,21.183,67.91c0,41.141-21.183,74.089-21.183,74.089"/>
|
||||||
|
|
||||||
|
<path id="High" display="inline" fill="none" stroke="#000000" stroke-width="12" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" d="
|
||||||
|
M216.737,35.517c0,0,27.944,35.316,27.944,89.593s-27.944,97.75-27.944,97.75"/>
|
||||||
|
</g>
|
||||||
|
<g id="Speaker_outline" display="none">
|
||||||
|
<path display="inline" d="M133.5,221.101c-1.149,0-2.296-0.33-3.293-0.984L59.957,174H8.75c-3.313,0-6-2.687-6-6V88
|
||||||
|
c0-3.313,2.687-6,6-6h51.207l70.25-46.116c1.844-1.211,4.203-1.312,6.143-0.264c1.94,1.047,3.15,3.075,3.15,5.28v174.201
|
||||||
|
c0,2.205-1.21,4.232-3.15,5.28C135.458,220.862,134.479,221.101,133.5,221.101z M14.75,162h47c1.17,0,2.314,0.342,3.292,0.984
|
||||||
|
l62.458,41V52.016l-62.458,41C64.064,93.658,62.92,94,61.75,94h-47V162z"/>
|
||||||
|
<path display="inline" d="M166.8,175.99c-1.11,0-2.234-0.309-3.238-0.954c-2.788-1.792-3.595-5.504-1.803-8.291
|
||||||
|
c0.109-0.173,11.575-18.406,11.575-40.579c0-21.992-11.121-36.301-11.233-36.443c-2.057-2.599-1.616-6.372,0.982-8.428
|
||||||
|
c2.598-2.057,6.371-1.617,8.428,0.982c0.564,0.713,13.823,17.77,13.823,43.89c0,25.799-12.931,46.21-13.481,47.067
|
||||||
|
C170.706,175.018,168.773,175.99,166.8,175.99z"/>
|
||||||
|
<path display="inline" d="M188.473,205c-1.111,0-2.234-0.309-3.239-0.954c-2.787-1.792-3.594-5.504-1.802-8.292
|
||||||
|
c0.199-0.311,20.229-32.06,20.229-70.844c0-38.607-19.688-63.935-19.888-64.187c-2.057-2.599-1.616-6.372,0.981-8.428
|
||||||
|
c2.602-2.057,6.373-1.616,8.429,0.982c0.918,1.16,22.478,28.897,22.478,71.633c0,42.416-21.231,75.928-22.136,77.334
|
||||||
|
C192.379,204.027,190.446,205,188.473,205z"/>
|
||||||
|
<path display="inline" d="M216.731,228.861c-1.11,0-2.234-0.309-3.238-0.954c-2.788-1.791-3.595-5.504-1.803-8.291
|
||||||
|
c0.267-0.418,26.991-42.736,26.991-94.506c0-51.593-26.383-85.533-26.649-85.87c-2.057-2.599-1.616-6.372,0.982-8.428
|
||||||
|
c2.599-2.057,6.371-1.616,8.428,0.982c1.194,1.509,29.239,37.593,29.239,93.316c0,55.402-27.717,99.16-28.897,100.995
|
||||||
|
C220.638,227.889,218.705,228.861,216.731,228.861z"/>
|
||||||
|
</g>
|
||||||
|
<g id="Muted">
|
||||||
|
<path d="M160.503,221.101c-1.717,0-3.421-0.732-4.608-2.153L10.395,44.746c-2.125-2.543-1.785-6.327,0.759-8.451
|
||||||
|
c2.544-2.125,6.328-1.784,8.451,0.759l145.5,174.201c2.124,2.544,1.784,6.327-0.759,8.452
|
||||||
|
C163.224,220.644,161.859,221.101,160.503,221.101z"/>
|
||||||
|
<path d="M127.5,203.984l-62.458-41C64.064,162.342,62.92,162,61.75,162h-47V94h28.967L33.694,82H8.75c-3.313,0-6,2.687-6,6v80
|
||||||
|
c0,3.313,2.687,6,6,6h51.207l70.25,46.116c0.997,0.654,2.144,0.984,3.293,0.984c0.979,0,1.958-0.238,2.85-0.72
|
||||||
|
c1.94-1.048,3.15-3.075,3.15-5.28v-6.423l-12-14.367V203.984z"/>
|
||||||
|
<path d="M127.5,52.016v104.856l12,14.367V40.899c0-2.205-1.21-4.232-3.15-5.28c-1.939-1.047-4.299-0.947-6.143,0.264L63.19,79.877
|
||||||
|
l7.744,9.271L127.5,52.016z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.7 KiB |
441
Windows/MassiveKnob.Plugin.CoreAudio/Resources/OSDIcon.ai
Normal file
441
Windows/MassiveKnob.Plugin.CoreAudio/Resources/OSDIcon.ai
Normal file
File diff suppressed because one or more lines are too long
@ -0,0 +1,84 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using AudioSwitcher.AudioApi;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MassiveKnob.Plugin.CoreAudio.SetDefault
|
||||||
|
{
|
||||||
|
public class DeviceSetDefaultAction : IMassiveKnobAction
|
||||||
|
{
|
||||||
|
public Guid ActionId { get; } = new Guid("b76f1eb7-2419-42b4-9de4-9bfe6f65a841");
|
||||||
|
public MassiveKnobActionType ActionType { get; } = MassiveKnobActionType.InputDigital;
|
||||||
|
public string Name { get; } = Strings.SetDefaultName;
|
||||||
|
public string Description { get; } = Strings.SetDefaultDescription;
|
||||||
|
|
||||||
|
|
||||||
|
public IMassiveKnobActionInstance Create(ILogger logger)
|
||||||
|
{
|
||||||
|
return new Instance();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private class Instance : IMassiveKnobDigitalAction
|
||||||
|
{
|
||||||
|
private IMassiveKnobActionContext actionContext;
|
||||||
|
private DeviceSetDefaultActionSettings settings;
|
||||||
|
private IDevice playbackDevice;
|
||||||
|
|
||||||
|
|
||||||
|
public void Initialize(IMassiveKnobActionContext context)
|
||||||
|
{
|
||||||
|
actionContext = context;
|
||||||
|
settings = context.GetSettings<DeviceSetDefaultActionSettings>();
|
||||||
|
ApplySettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void ApplySettings()
|
||||||
|
{
|
||||||
|
var coreAudioController = CoreAudioControllerInstance.Acquire();
|
||||||
|
playbackDevice = settings.DeviceId.HasValue ? coreAudioController.GetDevice(settings.DeviceId.Value) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public UserControl CreateSettingsControl()
|
||||||
|
{
|
||||||
|
var viewModel = new DeviceSetDefaultActionSettingsViewModel(settings);
|
||||||
|
viewModel.PropertyChanged += (sender, args) =>
|
||||||
|
{
|
||||||
|
if (!viewModel.IsSettingsProperty(args.PropertyName))
|
||||||
|
return;
|
||||||
|
|
||||||
|
actionContext.SetSettings(settings);
|
||||||
|
ApplySettings();
|
||||||
|
};
|
||||||
|
|
||||||
|
return new DeviceSetDefaultActionSettingsView(viewModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async ValueTask DigitalChanged(bool on)
|
||||||
|
{
|
||||||
|
if (playbackDevice == null || !on)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (settings.Playback)
|
||||||
|
await playbackDevice.SetAsDefaultAsync();
|
||||||
|
|
||||||
|
if (settings.Communications)
|
||||||
|
await playbackDevice.SetAsDefaultCommunicationsAsync();
|
||||||
|
|
||||||
|
|
||||||
|
// TODO OSD for default device
|
||||||
|
//if (settings.OSD)
|
||||||
|
//OSDManager.Show(playbackDevice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
using MassiveKnob.Plugin.CoreAudio.Base;
|
||||||
|
|
||||||
|
namespace MassiveKnob.Plugin.CoreAudio.SetDefault
|
||||||
|
{
|
||||||
|
public class DeviceSetDefaultActionSettings : BaseDeviceSettings
|
||||||
|
{
|
||||||
|
public bool Playback { get; set; } = true;
|
||||||
|
public bool Communications { get; set; } = true;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
<UserControl x:Class="MassiveKnob.Plugin.CoreAudio.SetDefault.DeviceSetDefaultActionSettingsView"
|
||||||
|
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:base="clr-namespace:MassiveKnob.Plugin.CoreAudio.Base"
|
||||||
|
xmlns:coreAudio="clr-namespace:MassiveKnob.Plugin.CoreAudio"
|
||||||
|
xmlns:setDefault="clr-namespace:MassiveKnob.Plugin.CoreAudio.SetDefault"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DesignHeight="200" d:DesignWidth="800"
|
||||||
|
d:DataContext="{d:DesignInstance setDefault:DeviceSetDefaultActionSettingsViewModel}">
|
||||||
|
<StackPanel Orientation="Vertical">
|
||||||
|
<base:BaseDeviceSettingsView />
|
||||||
|
|
||||||
|
<CheckBox Margin="0,24,0,0" IsChecked="{Binding Playback}">
|
||||||
|
<TextBlock Text="{x:Static coreAudio:Strings.SettingSetDefaultPlayback}" />
|
||||||
|
</CheckBox>
|
||||||
|
|
||||||
|
<CheckBox Margin="0,8,0,0" IsChecked="{Binding Communications}">
|
||||||
|
<TextBlock Text="{x:Static coreAudio:Strings.SettingSetDefaultCommunications}" />
|
||||||
|
</CheckBox>
|
||||||
|
</StackPanel>
|
||||||
|
</UserControl>
|
@ -0,0 +1,14 @@
|
|||||||
|
namespace MassiveKnob.Plugin.CoreAudio.SetDefault
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Interaction logic for DeviceSetDefaultActionSettingsView.xaml
|
||||||
|
/// </summary>
|
||||||
|
public partial class DeviceSetDefaultActionSettingsView
|
||||||
|
{
|
||||||
|
public DeviceSetDefaultActionSettingsView(DeviceSetDefaultActionSettingsViewModel viewModel)
|
||||||
|
{
|
||||||
|
DataContext = viewModel;
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
using MassiveKnob.Plugin.CoreAudio.Base;
|
||||||
|
|
||||||
|
namespace MassiveKnob.Plugin.CoreAudio.SetDefault
|
||||||
|
{
|
||||||
|
public class DeviceSetDefaultActionSettingsViewModel : BaseDeviceSettingsViewModel<DeviceSetDefaultActionSettings>
|
||||||
|
{
|
||||||
|
// ReSharper disable UnusedMember.Global - used by WPF Binding
|
||||||
|
public bool Playback
|
||||||
|
{
|
||||||
|
get => Settings.Playback;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == Settings.Playback)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Settings.Playback = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Communications
|
||||||
|
{
|
||||||
|
get => Settings.Communications;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == Settings.Communications)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Settings.Communications = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ReSharper restore UnusedMember.Global
|
||||||
|
|
||||||
|
|
||||||
|
// ReSharper disable once SuggestBaseTypeForParameter - by design
|
||||||
|
public DeviceSetDefaultActionSettingsViewModel(DeviceSetDefaultActionSettings settings) : base(settings)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,88 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using AudioSwitcher.AudioApi;
|
||||||
|
using MassiveKnob.Plugin.CoreAudio.OSD;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MassiveKnob.Plugin.CoreAudio.SetMuted
|
||||||
|
{
|
||||||
|
public class DeviceSetMutedAction : IMassiveKnobAction
|
||||||
|
{
|
||||||
|
public Guid ActionId { get; } = new Guid("032eb405-a1df-4178-b2d5-6cf556305a8c");
|
||||||
|
public MassiveKnobActionType ActionType { get; } = MassiveKnobActionType.InputDigital;
|
||||||
|
public string Name { get; } = Strings.SetMutedName;
|
||||||
|
public string Description { get; } = Strings.SetMutedDescription;
|
||||||
|
|
||||||
|
|
||||||
|
public IMassiveKnobActionInstance Create(ILogger logger)
|
||||||
|
{
|
||||||
|
return new Instance();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private class Instance : IMassiveKnobDigitalAction
|
||||||
|
{
|
||||||
|
private IMassiveKnobActionContext actionContext;
|
||||||
|
private DeviceSetMutedActionSettings settings;
|
||||||
|
private IDevice playbackDevice;
|
||||||
|
|
||||||
|
|
||||||
|
public void Initialize(IMassiveKnobActionContext context)
|
||||||
|
{
|
||||||
|
actionContext = context;
|
||||||
|
settings = context.GetSettings<DeviceSetMutedActionSettings>();
|
||||||
|
ApplySettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void ApplySettings()
|
||||||
|
{
|
||||||
|
var coreAudioController = CoreAudioControllerInstance.Acquire();
|
||||||
|
playbackDevice = settings.DeviceId.HasValue ? coreAudioController.GetDevice(settings.DeviceId.Value) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public UserControl CreateSettingsControl()
|
||||||
|
{
|
||||||
|
var viewModel = new DeviceSetMutedActionSettingsViewModel(settings);
|
||||||
|
viewModel.PropertyChanged += (sender, args) =>
|
||||||
|
{
|
||||||
|
if (!viewModel.IsSettingsProperty(args.PropertyName))
|
||||||
|
return;
|
||||||
|
|
||||||
|
actionContext.SetSettings(settings);
|
||||||
|
ApplySettings();
|
||||||
|
};
|
||||||
|
|
||||||
|
return new DeviceSetMutedActionSettingsView(viewModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async ValueTask DigitalChanged(bool on)
|
||||||
|
{
|
||||||
|
if (playbackDevice == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (settings.Toggle)
|
||||||
|
{
|
||||||
|
if (!on)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await playbackDevice.SetMuteAsync(!playbackDevice.IsMuted);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
await playbackDevice.SetMuteAsync(settings.SetInverted ? !on : on);
|
||||||
|
|
||||||
|
|
||||||
|
if (settings.OSD)
|
||||||
|
OSDManager.Show(playbackDevice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
using MassiveKnob.Plugin.CoreAudio.Base;
|
||||||
|
|
||||||
|
namespace MassiveKnob.Plugin.CoreAudio.SetMuted
|
||||||
|
{
|
||||||
|
public class DeviceSetMutedActionSettings : BaseDeviceSettings
|
||||||
|
{
|
||||||
|
public bool Toggle { get; set; }
|
||||||
|
public bool SetInverted { get; set;}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
<UserControl x:Class="MassiveKnob.Plugin.CoreAudio.SetMuted.DeviceSetMutedActionSettingsView"
|
||||||
|
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:setMuted="clr-namespace:MassiveKnob.Plugin.CoreAudio.SetMuted"
|
||||||
|
xmlns:base="clr-namespace:MassiveKnob.Plugin.CoreAudio.Base"
|
||||||
|
xmlns:coreAudio="clr-namespace:MassiveKnob.Plugin.CoreAudio"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DesignHeight="200" d:DesignWidth="800"
|
||||||
|
d:DataContext="{d:DesignInstance setMuted:DeviceSetMutedActionSettingsViewModel}">
|
||||||
|
<StackPanel Orientation="Vertical">
|
||||||
|
<base:BaseDeviceSettingsView />
|
||||||
|
|
||||||
|
<RadioButton Margin="0,8,0,0" IsChecked="{Binding ToggleTrue}">
|
||||||
|
<TextBlock Text="{x:Static coreAudio:Strings.SettingSetMutedToggleTrue}" />
|
||||||
|
</RadioButton>
|
||||||
|
|
||||||
|
<RadioButton Margin="0,8,0,0" IsChecked="{Binding ToggleFalse}">
|
||||||
|
<TextBlock Text="{x:Static coreAudio:Strings.SettingSetMutedToggleFalse}" />
|
||||||
|
</RadioButton>
|
||||||
|
|
||||||
|
<CheckBox Margin="24,8,0,0" IsChecked="{Binding SetInverted}">
|
||||||
|
<TextBlock Text="{x:Static coreAudio:Strings.SettingSetMutedSetInverted}" />
|
||||||
|
</CheckBox>
|
||||||
|
</StackPanel>
|
||||||
|
</UserControl>
|
@ -0,0 +1,14 @@
|
|||||||
|
namespace MassiveKnob.Plugin.CoreAudio.SetMuted
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Interaction logic for DeviceSetMutedActionSettingsView.xaml
|
||||||
|
/// </summary>
|
||||||
|
public partial class DeviceSetMutedActionSettingsView
|
||||||
|
{
|
||||||
|
public DeviceSetMutedActionSettingsView(DeviceSetMutedActionSettingsViewModel viewModel)
|
||||||
|
{
|
||||||
|
DataContext = viewModel;
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,62 @@
|
|||||||
|
using MassiveKnob.Plugin.CoreAudio.Base;
|
||||||
|
|
||||||
|
namespace MassiveKnob.Plugin.CoreAudio.SetMuted
|
||||||
|
{
|
||||||
|
public class DeviceSetMutedActionSettingsViewModel : BaseDeviceSettingsViewModel<DeviceSetMutedActionSettings>
|
||||||
|
{
|
||||||
|
// ReSharper disable UnusedMember.Global - used by WPF Binding
|
||||||
|
public bool ToggleTrue
|
||||||
|
{
|
||||||
|
get => Settings.Toggle;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (!value)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (Settings.Toggle)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Settings.Toggle = true;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public bool ToggleFalse
|
||||||
|
{
|
||||||
|
get => !Settings.Toggle;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (!value)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!Settings.Toggle)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Settings.Toggle = false;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public bool SetInverted
|
||||||
|
{
|
||||||
|
get => Settings.SetInverted;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == Settings.SetInverted)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Settings.SetInverted = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ReSharper restore UnusedMember.Global
|
||||||
|
|
||||||
|
|
||||||
|
// ReSharper disable once SuggestBaseTypeForParameter - by design
|
||||||
|
public DeviceSetMutedActionSettingsViewModel(DeviceSetMutedActionSettings settings) : base(settings)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,79 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using AudioSwitcher.AudioApi;
|
||||||
|
using MassiveKnob.Plugin.CoreAudio.OSD;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MassiveKnob.Plugin.CoreAudio.SetVolume
|
||||||
|
{
|
||||||
|
public class DeviceSetVolumeAction : IMassiveKnobAction
|
||||||
|
{
|
||||||
|
public Guid ActionId { get; } = new Guid("aabd329c-8be5-4d1e-90ab-5114143b21dd");
|
||||||
|
public MassiveKnobActionType ActionType { get; } = MassiveKnobActionType.InputAnalog;
|
||||||
|
public string Name { get; } = Strings.SetVolumeName;
|
||||||
|
public string Description { get; } = Strings.SetVolumeDescription;
|
||||||
|
|
||||||
|
|
||||||
|
public IMassiveKnobActionInstance Create(ILogger logger)
|
||||||
|
{
|
||||||
|
return new Instance();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private class Instance : IMassiveKnobAnalogAction
|
||||||
|
{
|
||||||
|
private IMassiveKnobActionContext actionContext;
|
||||||
|
private DeviceSetVolumeActionSettings settings;
|
||||||
|
private IDevice playbackDevice;
|
||||||
|
|
||||||
|
|
||||||
|
public void Initialize(IMassiveKnobActionContext context)
|
||||||
|
{
|
||||||
|
actionContext = context;
|
||||||
|
settings = context.GetSettings<DeviceSetVolumeActionSettings>();
|
||||||
|
ApplySettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void ApplySettings()
|
||||||
|
{
|
||||||
|
var coreAudioController = CoreAudioControllerInstance.Acquire();
|
||||||
|
playbackDevice = settings.DeviceId.HasValue ? coreAudioController.GetDevice(settings.DeviceId.Value) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public UserControl CreateSettingsControl()
|
||||||
|
{
|
||||||
|
var viewModel = new DeviceSetVolumeActionSettingsViewModel(settings);
|
||||||
|
viewModel.PropertyChanged += (sender, args) =>
|
||||||
|
{
|
||||||
|
if (!viewModel.IsSettingsProperty(args.PropertyName))
|
||||||
|
return;
|
||||||
|
|
||||||
|
actionContext.SetSettings(settings);
|
||||||
|
ApplySettings();
|
||||||
|
};
|
||||||
|
|
||||||
|
return new DeviceSetVolumeActionSettingsView(viewModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async ValueTask AnalogChanged(byte value)
|
||||||
|
{
|
||||||
|
if (playbackDevice == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await playbackDevice.SetVolumeAsync(value);
|
||||||
|
|
||||||
|
if (settings.OSD)
|
||||||
|
OSDManager.Show(playbackDevice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
using MassiveKnob.Plugin.CoreAudio.Base;
|
||||||
|
|
||||||
|
namespace MassiveKnob.Plugin.CoreAudio.SetVolume
|
||||||
|
{
|
||||||
|
public class DeviceSetVolumeActionSettings : BaseDeviceSettings
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
<UserControl x:Class="MassiveKnob.Plugin.CoreAudio.SetVolume.DeviceSetVolumeActionSettingsView"
|
||||||
|
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:base="clr-namespace:MassiveKnob.Plugin.CoreAudio.Base"
|
||||||
|
xmlns:setVolume="clr-namespace:MassiveKnob.Plugin.CoreAudio.SetVolume"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DesignHeight="200" d:DesignWidth="800"
|
||||||
|
d:DataContext="{d:DesignInstance setVolume:DeviceSetVolumeActionSettingsViewModel}">
|
||||||
|
<StackPanel Orientation="Vertical">
|
||||||
|
<base:BaseDeviceSettingsView />
|
||||||
|
</StackPanel>
|
||||||
|
</UserControl>
|
@ -0,0 +1,14 @@
|
|||||||
|
namespace MassiveKnob.Plugin.CoreAudio.SetVolume
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Interaction logic for DeviceSetVolumeActionSettingsView.xaml
|
||||||
|
/// </summary>
|
||||||
|
public partial class DeviceSetVolumeActionSettingsView
|
||||||
|
{
|
||||||
|
public DeviceSetVolumeActionSettingsView(DeviceSetVolumeActionSettingsViewModel viewModel)
|
||||||
|
{
|
||||||
|
DataContext = viewModel;
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
using MassiveKnob.Plugin.CoreAudio.Base;
|
||||||
|
|
||||||
|
namespace MassiveKnob.Plugin.CoreAudio.SetVolume
|
||||||
|
{
|
||||||
|
public class DeviceSetVolumeActionSettingsViewModel : BaseDeviceSettingsViewModel<DeviceSetVolumeActionSettings>
|
||||||
|
{
|
||||||
|
// ReSharper disable once SuggestBaseTypeForParameter - by design
|
||||||
|
public DeviceSetVolumeActionSettingsViewModel(DeviceSetVolumeActionSettings settings) : base(settings)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
342
Windows/MassiveKnob.Plugin.CoreAudio/Strings.Designer.cs
generated
Normal file
342
Windows/MassiveKnob.Plugin.CoreAudio/Strings.Designer.cs
generated
Normal file
@ -0,0 +1,342 @@
|
|||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// <auto-generated>
|
||||||
|
// This code was generated by a tool.
|
||||||
|
// Runtime Version:4.0.30319.42000
|
||||||
|
//
|
||||||
|
// Changes to this file may cause incorrect behavior and will be lost if
|
||||||
|
// the code is regenerated.
|
||||||
|
// </auto-generated>
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
namespace MassiveKnob.Plugin.CoreAudio {
|
||||||
|
using System;
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A strongly-typed resource class, for looking up localized strings, etc.
|
||||||
|
/// </summary>
|
||||||
|
// This class was auto-generated by the StronglyTypedResourceBuilder
|
||||||
|
// class via a tool like ResGen or Visual Studio.
|
||||||
|
// To add or remove a member, edit your .ResX file then rerun ResGen
|
||||||
|
// with the /str option, or rebuild your VS project.
|
||||||
|
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
|
||||||
|
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||||
|
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||||
|
public class Strings {
|
||||||
|
|
||||||
|
private static global::System.Resources.ResourceManager resourceMan;
|
||||||
|
|
||||||
|
private static global::System.Globalization.CultureInfo resourceCulture;
|
||||||
|
|
||||||
|
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
||||||
|
internal Strings() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the cached ResourceManager instance used by this class.
|
||||||
|
/// </summary>
|
||||||
|
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||||
|
public static global::System.Resources.ResourceManager ResourceManager {
|
||||||
|
get {
|
||||||
|
if (object.ReferenceEquals(resourceMan, null)) {
|
||||||
|
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("MassiveKnob.Plugin.CoreAudio.Strings", typeof(Strings).Assembly);
|
||||||
|
resourceMan = temp;
|
||||||
|
}
|
||||||
|
return resourceMan;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Overrides the current thread's CurrentUICulture property for all
|
||||||
|
/// resource lookups using this strongly typed resource class.
|
||||||
|
/// </summary>
|
||||||
|
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||||
|
public static global::System.Globalization.CultureInfo Culture {
|
||||||
|
get {
|
||||||
|
return resourceCulture;
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
resourceCulture = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to {0}.
|
||||||
|
/// </summary>
|
||||||
|
public static string DeviceDisplayNameActive {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("DeviceDisplayNameActive", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to {0} (Disabled).
|
||||||
|
/// </summary>
|
||||||
|
public static string DeviceDisplayNameDisabled {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("DeviceDisplayNameDisabled", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to {0} (Inactive).
|
||||||
|
/// </summary>
|
||||||
|
public static string DeviceDisplayNameInactive {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("DeviceDisplayNameInactive", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to {0} (Not present).
|
||||||
|
/// </summary>
|
||||||
|
public static string DeviceDisplayNameNotPresent {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("DeviceDisplayNameNotPresent", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to {0} (Unplugged).
|
||||||
|
/// </summary>
|
||||||
|
public static string DeviceDisplayNameUnplugged {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("DeviceDisplayNameUnplugged", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Sets the digital output depending on whether the selected device is the active playback or communications device..
|
||||||
|
/// </summary>
|
||||||
|
public static string GetDefaultDescription {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("GetDefaultDescription", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Is default device.
|
||||||
|
/// </summary>
|
||||||
|
public static string GetDefaultName {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("GetDefaultName", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Sets the digital output to the muted state for the selected device, regardless of the current default device..
|
||||||
|
/// </summary>
|
||||||
|
public static string GetMutedDescription {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("GetMutedDescription", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Muted / unmuted.
|
||||||
|
/// </summary>
|
||||||
|
public static string GetMutedName {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("GetMutedName", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Sets the analog output to the volume for the selected device, regardless of the current default device..
|
||||||
|
/// </summary>
|
||||||
|
public static string GetVolumeDescription {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("GetVolumeDescription", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Volume.
|
||||||
|
/// </summary>
|
||||||
|
public static string GetVolumeName {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("GetVolumeName", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Included with Massive Knob by default. Provides volume control per device and default device switching..
|
||||||
|
/// </summary>
|
||||||
|
public static string PluginDescription {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("PluginDescription", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Windows Core Audio.
|
||||||
|
/// </summary>
|
||||||
|
public static string PluginName {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("PluginName", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Changes the default playback and/or communications device when the input turns on..
|
||||||
|
/// </summary>
|
||||||
|
public static string SetDefaultDescription {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("SetDefaultDescription", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Set default device.
|
||||||
|
/// </summary>
|
||||||
|
public static string SetDefaultName {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("SetDefaultName", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Toggles the muted state for the selected device, regardless of the current default device..
|
||||||
|
/// </summary>
|
||||||
|
public static string SetMutedDescription {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("SetMutedDescription", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Mute / unmute.
|
||||||
|
/// </summary>
|
||||||
|
public static string SetMutedName {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("SetMutedName", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to is the default communications device.
|
||||||
|
/// </summary>
|
||||||
|
public static string SettingGetDefaultCommunications {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("SettingGetDefaultCommunications", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Inverted (off when the device is the default).
|
||||||
|
/// </summary>
|
||||||
|
public static string SettingGetDefaultInverted {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("SettingGetDefaultInverted", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to is the default playback device.
|
||||||
|
/// </summary>
|
||||||
|
public static string SettingGetDefaultPlayback {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("SettingGetDefaultPlayback", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Turn on when the selected device.
|
||||||
|
/// </summary>
|
||||||
|
public static string SettingGetDefaultWhen {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("SettingGetDefaultWhen", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Inverted (off when muted).
|
||||||
|
/// </summary>
|
||||||
|
public static string SettingGetMutedInverted {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("SettingGetMutedInverted", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Show On-Screen Display.
|
||||||
|
/// </summary>
|
||||||
|
public static string SettingOSD {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("SettingOSD", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Playback device.
|
||||||
|
/// </summary>
|
||||||
|
public static string SettingPlaybackDevice {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("SettingPlaybackDevice", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Set as the default communications device.
|
||||||
|
/// </summary>
|
||||||
|
public static string SettingSetDefaultCommunications {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("SettingSetDefaultCommunications", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Set as the default playback device.
|
||||||
|
/// </summary>
|
||||||
|
public static string SettingSetDefaultPlayback {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("SettingSetDefaultPlayback", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Inverted (muted when off).
|
||||||
|
/// </summary>
|
||||||
|
public static string SettingSetMutedSetInverted {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("SettingSetMutedSetInverted", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Set mute depending on value (eg. switch).
|
||||||
|
/// </summary>
|
||||||
|
public static string SettingSetMutedToggleFalse {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("SettingSetMutedToggleFalse", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Toggle mute when input turns on (eg. push button).
|
||||||
|
/// </summary>
|
||||||
|
public static string SettingSetMutedToggleTrue {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("SettingSetMutedToggleTrue", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Sets the volume for the selected device to the value of the analog input, regardless of the current default device..
|
||||||
|
/// </summary>
|
||||||
|
public static string SetVolumeDescription {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("SetVolumeDescription", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Volume.
|
||||||
|
/// </summary>
|
||||||
|
public static string SetVolumeName {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("SetVolumeName", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
213
Windows/MassiveKnob.Plugin.CoreAudio/Strings.resx
Normal file
213
Windows/MassiveKnob.Plugin.CoreAudio/Strings.resx
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<root>
|
||||||
|
<!--
|
||||||
|
Microsoft ResX Schema
|
||||||
|
|
||||||
|
Version 2.0
|
||||||
|
|
||||||
|
The primary goals of this format is to allow a simple XML format
|
||||||
|
that is mostly human readable. The generation and parsing of the
|
||||||
|
various data types are done through the TypeConverter classes
|
||||||
|
associated with the data types.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
... ado.net/XML headers & schema ...
|
||||||
|
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||||
|
<resheader name="version">2.0</resheader>
|
||||||
|
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||||
|
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||||
|
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||||
|
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||||
|
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||||
|
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||||
|
</data>
|
||||||
|
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||||
|
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||||
|
<comment>This is a comment</comment>
|
||||||
|
</data>
|
||||||
|
|
||||||
|
There are any number of "resheader" rows that contain simple
|
||||||
|
name/value pairs.
|
||||||
|
|
||||||
|
Each data row contains a name, and value. The row also contains a
|
||||||
|
type or mimetype. Type corresponds to a .NET class that support
|
||||||
|
text/value conversion through the TypeConverter architecture.
|
||||||
|
Classes that don't support this are serialized and stored with the
|
||||||
|
mimetype set.
|
||||||
|
|
||||||
|
The mimetype is used for serialized objects, and tells the
|
||||||
|
ResXResourceReader how to depersist the object. This is currently not
|
||||||
|
extensible. For a given mimetype the value must be set accordingly:
|
||||||
|
|
||||||
|
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||||
|
that the ResXResourceWriter will generate, however the reader can
|
||||||
|
read any of the formats listed below.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.binary.base64
|
||||||
|
value : The object must be serialized with
|
||||||
|
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.soap.base64
|
||||||
|
value : The object must be serialized with
|
||||||
|
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||||
|
value : The object must be serialized into a byte array
|
||||||
|
: using a System.ComponentModel.TypeConverter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
-->
|
||||||
|
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||||
|
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||||
|
<xsd:element name="root" msdata:IsDataSet="true">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:choice maxOccurs="unbounded">
|
||||||
|
<xsd:element name="metadata">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="assembly">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:attribute name="alias" type="xsd:string" />
|
||||||
|
<xsd:attribute name="name" type="xsd:string" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="data">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="resheader">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:choice>
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:schema>
|
||||||
|
<resheader name="resmimetype">
|
||||||
|
<value>text/microsoft-resx</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="version">
|
||||||
|
<value>2.0</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="reader">
|
||||||
|
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="writer">
|
||||||
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<data name="DeviceDisplayNameActive" xml:space="preserve">
|
||||||
|
<value>{0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeviceDisplayNameDisabled" xml:space="preserve">
|
||||||
|
<value>{0} (Disabled)</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeviceDisplayNameInactive" xml:space="preserve">
|
||||||
|
<value>{0} (Inactive)</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeviceDisplayNameNotPresent" xml:space="preserve">
|
||||||
|
<value>{0} (Not present)</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeviceDisplayNameUnplugged" xml:space="preserve">
|
||||||
|
<value>{0} (Unplugged)</value>
|
||||||
|
</data>
|
||||||
|
<data name="GetDefaultDescription" xml:space="preserve">
|
||||||
|
<value>Sets the digital output depending on whether the selected device is the active playback or communications device.</value>
|
||||||
|
</data>
|
||||||
|
<data name="GetDefaultName" xml:space="preserve">
|
||||||
|
<value>Is default device</value>
|
||||||
|
</data>
|
||||||
|
<data name="GetMutedDescription" xml:space="preserve">
|
||||||
|
<value>Sets the digital output to the muted state for the selected device, regardless of the current default device.</value>
|
||||||
|
</data>
|
||||||
|
<data name="GetMutedName" xml:space="preserve">
|
||||||
|
<value>Muted / unmuted</value>
|
||||||
|
</data>
|
||||||
|
<data name="GetVolumeDescription" xml:space="preserve">
|
||||||
|
<value>Sets the analog output to the volume for the selected device, regardless of the current default device.</value>
|
||||||
|
</data>
|
||||||
|
<data name="GetVolumeName" xml:space="preserve">
|
||||||
|
<value>Volume</value>
|
||||||
|
</data>
|
||||||
|
<data name="PluginDescription" xml:space="preserve">
|
||||||
|
<value>Included with Massive Knob by default. Provides volume control per device and default device switching.</value>
|
||||||
|
</data>
|
||||||
|
<data name="PluginName" xml:space="preserve">
|
||||||
|
<value>Windows Core Audio</value>
|
||||||
|
</data>
|
||||||
|
<data name="SetDefaultDescription" xml:space="preserve">
|
||||||
|
<value>Changes the default playback and/or communications device when the input turns on.</value>
|
||||||
|
</data>
|
||||||
|
<data name="SetDefaultName" xml:space="preserve">
|
||||||
|
<value>Set default device</value>
|
||||||
|
</data>
|
||||||
|
<data name="SetMutedDescription" xml:space="preserve">
|
||||||
|
<value>Toggles the muted state for the selected device, regardless of the current default device.</value>
|
||||||
|
</data>
|
||||||
|
<data name="SetMutedName" xml:space="preserve">
|
||||||
|
<value>Mute / unmute</value>
|
||||||
|
</data>
|
||||||
|
<data name="SettingGetDefaultCommunications" xml:space="preserve">
|
||||||
|
<value>is the default communications device</value>
|
||||||
|
</data>
|
||||||
|
<data name="SettingGetDefaultInverted" xml:space="preserve">
|
||||||
|
<value>Inverted (off when the device is the default)</value>
|
||||||
|
</data>
|
||||||
|
<data name="SettingGetDefaultPlayback" xml:space="preserve">
|
||||||
|
<value>is the default playback device</value>
|
||||||
|
</data>
|
||||||
|
<data name="SettingGetDefaultWhen" xml:space="preserve">
|
||||||
|
<value>Turn on when the selected device</value>
|
||||||
|
</data>
|
||||||
|
<data name="SettingGetMutedInverted" xml:space="preserve">
|
||||||
|
<value>Inverted (off when muted)</value>
|
||||||
|
</data>
|
||||||
|
<data name="SettingOSD" xml:space="preserve">
|
||||||
|
<value>Show On-Screen Display</value>
|
||||||
|
</data>
|
||||||
|
<data name="SettingPlaybackDevice" xml:space="preserve">
|
||||||
|
<value>Playback device</value>
|
||||||
|
</data>
|
||||||
|
<data name="SettingSetDefaultCommunications" xml:space="preserve">
|
||||||
|
<value>Set as the default communications device</value>
|
||||||
|
</data>
|
||||||
|
<data name="SettingSetDefaultPlayback" xml:space="preserve">
|
||||||
|
<value>Set as the default playback device</value>
|
||||||
|
</data>
|
||||||
|
<data name="SettingSetMutedSetInverted" xml:space="preserve">
|
||||||
|
<value>Inverted (muted when off)</value>
|
||||||
|
</data>
|
||||||
|
<data name="SettingSetMutedToggleFalse" xml:space="preserve">
|
||||||
|
<value>Set mute depending on value (eg. switch)</value>
|
||||||
|
</data>
|
||||||
|
<data name="SettingSetMutedToggleTrue" xml:space="preserve">
|
||||||
|
<value>Toggle mute when input turns on (eg. push button)</value>
|
||||||
|
</data>
|
||||||
|
<data name="SetVolumeDescription" xml:space="preserve">
|
||||||
|
<value>Sets the volume for the selected device to the value of the analog input, regardless of the current default device.</value>
|
||||||
|
</data>
|
||||||
|
<data name="SetVolumeName" xml:space="preserve">
|
||||||
|
<value>Volume</value>
|
||||||
|
</data>
|
||||||
|
</root>
|
@ -0,0 +1,97 @@
|
|||||||
|
using System;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using MassiveKnob.Plugin.EmulatorDevice.Settings;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MassiveKnob.Plugin.EmulatorDevice.Devices
|
||||||
|
{
|
||||||
|
public class EmulatorDevice : IMassiveKnobDevice
|
||||||
|
{
|
||||||
|
public Guid DeviceId { get; } = new Guid("e1a4977a-abf4-4c75-a17d-fd8d3a8451ff");
|
||||||
|
public string Name { get; } = "Emulator";
|
||||||
|
public string Description { get; } = "Emulates an actual device but does not communicate with anything.";
|
||||||
|
|
||||||
|
public IMassiveKnobDeviceInstance Create(ILogger logger)
|
||||||
|
{
|
||||||
|
return new Instance();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private class Instance : IMassiveKnobDeviceInstance
|
||||||
|
{
|
||||||
|
private IMassiveKnobDeviceContext deviceContext;
|
||||||
|
private EmulatorDeviceSettings settings;
|
||||||
|
|
||||||
|
private DeviceSpecs reportedSpecs;
|
||||||
|
private EmulatorDeviceWindow window;
|
||||||
|
private EmulatorDeviceWindowViewModel windowViewModel;
|
||||||
|
|
||||||
|
|
||||||
|
public void Initialize(IMassiveKnobDeviceContext context)
|
||||||
|
{
|
||||||
|
deviceContext = context;
|
||||||
|
settings = deviceContext.GetSettings<EmulatorDeviceSettings>();
|
||||||
|
|
||||||
|
windowViewModel = new EmulatorDeviceWindowViewModel(settings, context);
|
||||||
|
window = new EmulatorDeviceWindow(windowViewModel);
|
||||||
|
ApplySettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
window.Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void ApplySettings()
|
||||||
|
{
|
||||||
|
if (settings.AnalogInputCount != reportedSpecs.AnalogInputCount ||
|
||||||
|
settings.DigitalInputCount != reportedSpecs.DigitalInputCount ||
|
||||||
|
settings.AnalogOutputCount != reportedSpecs.AnalogOutputCount ||
|
||||||
|
settings.DigitalOutputCount != reportedSpecs.DigitalOutputCount)
|
||||||
|
{
|
||||||
|
reportedSpecs = new DeviceSpecs(
|
||||||
|
settings.AnalogInputCount, settings.DigitalInputCount,
|
||||||
|
settings.AnalogOutputCount, settings.DigitalOutputCount);
|
||||||
|
|
||||||
|
deviceContext.Connected(reportedSpecs);
|
||||||
|
}
|
||||||
|
|
||||||
|
windowViewModel.ApplySettings();
|
||||||
|
window.Show();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public UserControl CreateSettingsControl()
|
||||||
|
{
|
||||||
|
var viewModel = new EmulatorDeviceSettingsViewModel(settings);
|
||||||
|
viewModel.PropertyChanged += (sender, args) =>
|
||||||
|
{
|
||||||
|
deviceContext.SetSettings(settings);
|
||||||
|
ApplySettings();
|
||||||
|
};
|
||||||
|
|
||||||
|
return new EmulatorDeviceSettingsView(viewModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void SetAnalogOutput(int analogOutputIndex, byte value)
|
||||||
|
{
|
||||||
|
if (analogOutputIndex >= windowViewModel.AnalogOutputCount)
|
||||||
|
return;
|
||||||
|
|
||||||
|
windowViewModel.AnalogOutputs[analogOutputIndex].AnalogValue = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void SetDigitalOutput(int digitalOutputIndex, bool on)
|
||||||
|
{
|
||||||
|
if (digitalOutputIndex >= windowViewModel.DigitalOutputCount)
|
||||||
|
return;
|
||||||
|
|
||||||
|
windowViewModel.DigitalOutputs[digitalOutputIndex].DigitalValue = on;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,75 @@
|
|||||||
|
<Window x:Class="MassiveKnob.Plugin.EmulatorDevice.Devices.EmulatorDeviceWindow"
|
||||||
|
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"
|
||||||
|
xmlns:devices="clr-namespace:MassiveKnob.Plugin.EmulatorDevice.Devices"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
Title="Massive Knob - Device Emulator" Height="400" Width="300"
|
||||||
|
WindowStartupLocation="CenterScreen"
|
||||||
|
WindowStyle="ToolWindow"
|
||||||
|
Topmost="True"
|
||||||
|
d:DataContext="{d:DesignInstance devices:EmulatorDeviceWindowViewModelDesignTime, IsDesignTimeCreatable=True}">
|
||||||
|
<Window.Resources>
|
||||||
|
<ResourceDictionary>
|
||||||
|
<Style TargetType="DockPanel" x:Key="Row">
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style TargetType="TextBlock" x:Key="Label">
|
||||||
|
<Setter Property="Margin" Value="4,4,8,4" />
|
||||||
|
<Setter Property="DockPanel.Dock" Value="Left" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="Value">
|
||||||
|
<Setter Property="Control.Margin" Value="4,4,8,4" />
|
||||||
|
</Style>
|
||||||
|
</ResourceDictionary>
|
||||||
|
</Window.Resources>
|
||||||
|
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||||
|
<StackPanel>
|
||||||
|
<ItemsControl ItemsSource="{Binding AnalogInputs}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<DockPanel Style="{StaticResource Row}">
|
||||||
|
<TextBlock Text="{Binding DisplayName}" Style="{StaticResource Label}" />
|
||||||
|
<Slider Minimum="0" Maximum="100" Value="{Binding AnalogValue}" Style="{StaticResource Value}" />
|
||||||
|
</DockPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
|
||||||
|
<ItemsControl ItemsSource="{Binding DigitalInputs}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<DockPanel Style="{StaticResource Row}">
|
||||||
|
<TextBlock Text="{Binding DisplayName}" Style="{StaticResource Label}" />
|
||||||
|
<CheckBox IsChecked="{Binding DigitalValue}" Style="{StaticResource Value}" />
|
||||||
|
</DockPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
|
||||||
|
<ItemsControl ItemsSource="{Binding AnalogOutputs}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<DockPanel Style="{StaticResource Row}">
|
||||||
|
<TextBlock Text="{Binding DisplayName}" Style="{StaticResource Label}" />
|
||||||
|
<TextBlock Text="{Binding AnalogValue}" Style="{StaticResource Value}" />
|
||||||
|
</DockPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
|
||||||
|
<ItemsControl ItemsSource="{Binding DigitalOutputs}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<DockPanel Style="{StaticResource Row}">
|
||||||
|
<TextBlock Text="{Binding DisplayName}" Style="{StaticResource Label}" />
|
||||||
|
<TextBlock Text="{Binding DigitalValueDisplayText}" Style="{StaticResource Value}" />
|
||||||
|
</DockPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Window>
|
@ -0,0 +1,14 @@
|
|||||||
|
namespace MassiveKnob.Plugin.EmulatorDevice.Devices
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Interaction logic for EmulatorDeviceWindow.xaml
|
||||||
|
/// </summary>
|
||||||
|
public partial class EmulatorDeviceWindow
|
||||||
|
{
|
||||||
|
public EmulatorDeviceWindow(EmulatorDeviceWindowViewModel viewModel)
|
||||||
|
{
|
||||||
|
DataContext = viewModel;
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,263 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using MassiveKnob.Plugin.EmulatorDevice.Settings;
|
||||||
|
|
||||||
|
namespace MassiveKnob.Plugin.EmulatorDevice.Devices
|
||||||
|
{
|
||||||
|
public class EmulatorDeviceWindowViewModel : INotifyPropertyChanged
|
||||||
|
{
|
||||||
|
private readonly EmulatorDeviceSettings settings;
|
||||||
|
private readonly IMassiveKnobDeviceContext context;
|
||||||
|
public event PropertyChangedEventHandler PropertyChanged;
|
||||||
|
|
||||||
|
|
||||||
|
// ReSharper disable UnusedMember.Global - used by WPF Binding
|
||||||
|
private int analogInputCount;
|
||||||
|
public int AnalogInputCount
|
||||||
|
{
|
||||||
|
get => analogInputCount;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == analogInputCount)
|
||||||
|
return;
|
||||||
|
|
||||||
|
analogInputCount = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
|
||||||
|
AnalogInputs = Enumerable.Range(0, AnalogInputCount)
|
||||||
|
.Select(i => new InputOutputViewModel(context, MassiveKnobActionType.InputAnalog, i))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private IList<InputOutputViewModel> analogInputs;
|
||||||
|
public IList<InputOutputViewModel> AnalogInputs
|
||||||
|
{
|
||||||
|
get => analogInputs;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
analogInputs = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private int digitalInputCount;
|
||||||
|
public int DigitalInputCount
|
||||||
|
{
|
||||||
|
get => digitalInputCount;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == digitalInputCount)
|
||||||
|
return;
|
||||||
|
|
||||||
|
digitalInputCount = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
|
||||||
|
DigitalInputs = Enumerable.Range(0, DigitalInputCount)
|
||||||
|
.Select(i => new InputOutputViewModel(context, MassiveKnobActionType.InputDigital, i))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private IList<InputOutputViewModel> digitalInputs;
|
||||||
|
public IList<InputOutputViewModel> DigitalInputs
|
||||||
|
{
|
||||||
|
get => digitalInputs;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
digitalInputs = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private int analogOutputCount;
|
||||||
|
public int AnalogOutputCount
|
||||||
|
{
|
||||||
|
get => analogOutputCount;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == analogOutputCount)
|
||||||
|
return;
|
||||||
|
|
||||||
|
analogOutputCount = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
|
||||||
|
AnalogOutputs = Enumerable.Range(0, AnalogOutputCount)
|
||||||
|
.Select(i => new InputOutputViewModel(context, MassiveKnobActionType.OutputAnalog, i))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private IList<InputOutputViewModel> analogOutputs;
|
||||||
|
public IList<InputOutputViewModel> AnalogOutputs
|
||||||
|
{
|
||||||
|
get => analogOutputs;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
analogOutputs = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private int digitalOutputCount;
|
||||||
|
public int DigitalOutputCount
|
||||||
|
{
|
||||||
|
get => digitalOutputCount;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == digitalOutputCount)
|
||||||
|
return;
|
||||||
|
|
||||||
|
digitalOutputCount = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
|
||||||
|
DigitalOutputs = Enumerable.Range(0, DigitalOutputCount)
|
||||||
|
.Select(i => new InputOutputViewModel(context, MassiveKnobActionType.OutputDigital, i))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private IList<InputOutputViewModel> digitalOutputs;
|
||||||
|
public IList<InputOutputViewModel> DigitalOutputs
|
||||||
|
{
|
||||||
|
get => digitalOutputs;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
digitalOutputs = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ReSharper restore UnusedMember.Global
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public EmulatorDeviceWindowViewModel(EmulatorDeviceSettings settings, IMassiveKnobDeviceContext context)
|
||||||
|
{
|
||||||
|
this.settings = settings;
|
||||||
|
this.context = context;
|
||||||
|
|
||||||
|
ApplySettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void ApplySettings()
|
||||||
|
{
|
||||||
|
AnalogInputCount = settings.AnalogInputCount;
|
||||||
|
DigitalInputCount = settings.DigitalInputCount;
|
||||||
|
AnalogOutputCount = settings.AnalogOutputCount;
|
||||||
|
DigitalOutputCount = settings.DigitalOutputCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
|
||||||
|
{
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public class InputOutputViewModel : INotifyPropertyChanged
|
||||||
|
{
|
||||||
|
private readonly IMassiveKnobDeviceContext context;
|
||||||
|
public MassiveKnobActionType ActionType { get; }
|
||||||
|
public int Index { get; }
|
||||||
|
|
||||||
|
public string DisplayName
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
switch (ActionType)
|
||||||
|
{
|
||||||
|
case MassiveKnobActionType.InputAnalog:
|
||||||
|
return $"Analog input #{Index + 1}";
|
||||||
|
|
||||||
|
case MassiveKnobActionType.InputDigital:
|
||||||
|
return $"Digital input #{Index + 1}";
|
||||||
|
|
||||||
|
case MassiveKnobActionType.OutputAnalog:
|
||||||
|
return $"Analog output #{Index + 1}";
|
||||||
|
|
||||||
|
case MassiveKnobActionType.OutputDigital:
|
||||||
|
return $"Digital output #{Index + 1}";
|
||||||
|
|
||||||
|
default:
|
||||||
|
return (Index + 1).ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private byte analogValue;
|
||||||
|
public byte AnalogValue
|
||||||
|
{
|
||||||
|
get => analogValue;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
analogValue = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
|
||||||
|
if (ActionType == MassiveKnobActionType.InputAnalog)
|
||||||
|
// Context can be null in DesignTime
|
||||||
|
context?.AnalogChanged(Index, analogValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool digitalValue;
|
||||||
|
public bool DigitalValue
|
||||||
|
{
|
||||||
|
get => digitalValue;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
digitalValue = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
OnDependantPropertyChanged("DigitalValueDisplayText");
|
||||||
|
|
||||||
|
if (ActionType == MassiveKnobActionType.InputDigital)
|
||||||
|
context?.DigitalChanged(Index, digitalValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string DigitalValueDisplayText => DigitalValue ? "On" : "Off";
|
||||||
|
|
||||||
|
|
||||||
|
public InputOutputViewModel(IMassiveKnobDeviceContext context, MassiveKnobActionType actionType, int index)
|
||||||
|
{
|
||||||
|
this.context = context;
|
||||||
|
ActionType = actionType;
|
||||||
|
Index = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public event PropertyChangedEventHandler PropertyChanged;
|
||||||
|
|
||||||
|
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
|
||||||
|
{
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void OnDependantPropertyChanged(string propertyName)
|
||||||
|
{
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public class EmulatorDeviceWindowViewModelDesignTime : EmulatorDeviceWindowViewModel
|
||||||
|
{
|
||||||
|
public EmulatorDeviceWindowViewModelDesignTime() : base(
|
||||||
|
new EmulatorDeviceSettings
|
||||||
|
{
|
||||||
|
AnalogInputCount = 2,
|
||||||
|
DigitalInputCount = 2,
|
||||||
|
AnalogOutputCount = 2,
|
||||||
|
DigitalOutputCount = 2
|
||||||
|
}, null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,88 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||||
|
<PropertyGroup>
|
||||||
|
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||||
|
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||||
|
<ProjectGuid>{674DE974-B134-4DB5-BFAF-7BC3D05E16DE}</ProjectGuid>
|
||||||
|
<OutputType>Library</OutputType>
|
||||||
|
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||||
|
<RootNamespace>MassiveKnob.Plugin.EmulatorDevice</RootNamespace>
|
||||||
|
<AssemblyName>MassiveKnob.Plugin.EmulatorDevice</AssemblyName>
|
||||||
|
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
|
||||||
|
<FileAlignment>512</FileAlignment>
|
||||||
|
<Deterministic>true</Deterministic>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||||
|
<DebugSymbols>true</DebugSymbols>
|
||||||
|
<DebugType>full</DebugType>
|
||||||
|
<Optimize>false</Optimize>
|
||||||
|
<OutputPath>bin\Debug\</OutputPath>
|
||||||
|
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||||
|
<ErrorReport>prompt</ErrorReport>
|
||||||
|
<WarningLevel>4</WarningLevel>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||||
|
<DebugType>pdbonly</DebugType>
|
||||||
|
<Optimize>true</Optimize>
|
||||||
|
<OutputPath>bin\Release\</OutputPath>
|
||||||
|
<DefineConstants>TRACE</DefineConstants>
|
||||||
|
<ErrorReport>prompt</ErrorReport>
|
||||||
|
<WarningLevel>4</WarningLevel>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="PresentationCore" />
|
||||||
|
<Reference Include="PresentationFramework" />
|
||||||
|
<Reference Include="System" />
|
||||||
|
<Reference Include="System.Core" />
|
||||||
|
<Reference Include="System.Xaml" />
|
||||||
|
<Reference Include="System.Xml.Linq" />
|
||||||
|
<Reference Include="System.Data.DataSetExtensions" />
|
||||||
|
<Reference Include="Microsoft.CSharp" />
|
||||||
|
<Reference Include="System.Data" />
|
||||||
|
<Reference Include="System.Net.Http" />
|
||||||
|
<Reference Include="System.Xml" />
|
||||||
|
<Reference Include="WindowsBase" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="Devices\EmulatorDevice.cs" />
|
||||||
|
<Compile Include="Devices\EmulatorDeviceWindow.xaml.cs">
|
||||||
|
<DependentUpon>EmulatorDeviceWindow.xaml</DependentUpon>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="Devices\EmulatorDeviceWindowViewModel.cs" />
|
||||||
|
<Compile Include="MassiveKnobEmulatorDevicePlugin.cs" />
|
||||||
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
|
<Compile Include="Settings\EmulatorDeviceSettingsView.xaml.cs">
|
||||||
|
<DependentUpon>EmulatorDeviceSettingsView.xaml</DependentUpon>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="Settings\EmulatorDeviceSettings.cs" />
|
||||||
|
<Compile Include="Settings\EmulatorDeviceSettingsViewModel.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\MassiveKnob.Plugin\MassiveKnob.Plugin.csproj">
|
||||||
|
<Project>{A1298BE4-1D23-416C-8C56-FC9264487A95}</Project>
|
||||||
|
<Name>MassiveKnob.Plugin</Name>
|
||||||
|
</ProjectReference>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions">
|
||||||
|
<Version>5.0.0</Version>
|
||||||
|
</PackageReference>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Page Include="Devices\EmulatorDeviceWindow.xaml">
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
<SubType>Designer</SubType>
|
||||||
|
</Page>
|
||||||
|
<Page Include="Settings\EmulatorDeviceSettingsView.xaml">
|
||||||
|
<SubType>Designer</SubType>
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
</Page>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="MassiveKnobPlugin.json">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
|
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||||
|
</Project>
|
@ -0,0 +1,20 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace MassiveKnob.Plugin.EmulatorDevice
|
||||||
|
{
|
||||||
|
[MassiveKnobPlugin]
|
||||||
|
public class MassiveKnobEmulatorDevicePlugin : IMassiveKnobDevicePlugin
|
||||||
|
{
|
||||||
|
public Guid PluginId { get; } = new Guid("85f04232-d70f-494c-94a2-41452591ffb3");
|
||||||
|
public string Name { get; } = "Mock Device";
|
||||||
|
public string Description { get; } = "Emulates the actual device but does not communicate with anything.";
|
||||||
|
public string Author { get; } = "Mark van Renswoude <mark@x2software.net>";
|
||||||
|
public string Url { get; } = "https://www.github.com/MvRens/MassiveKnob/";
|
||||||
|
|
||||||
|
public IEnumerable<IMassiveKnobDevice> Devices { get; } = new IMassiveKnobDevice[]
|
||||||
|
{
|
||||||
|
new EmulatorDevice.Devices.EmulatorDevice()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"EntryAssembly": "MassiveKnob.Plugin.EmulatorDevice.dll"
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
// General Information about an assembly is controlled through the following
|
||||||
|
// set of attributes. Change these attribute values to modify the information
|
||||||
|
// associated with an assembly.
|
||||||
|
[assembly: AssemblyTitle("MassiveKnob.Plugin.MockDevice")]
|
||||||
|
[assembly: AssemblyDescription("")]
|
||||||
|
[assembly: AssemblyConfiguration("")]
|
||||||
|
[assembly: AssemblyCompany("")]
|
||||||
|
[assembly: AssemblyProduct("MassiveKnob.Plugin.MockDevice")]
|
||||||
|
[assembly: AssemblyCopyright("Copyright © 2021")]
|
||||||
|
[assembly: AssemblyTrademark("")]
|
||||||
|
[assembly: AssemblyCulture("")]
|
||||||
|
|
||||||
|
// Setting ComVisible to false makes the types in this assembly not visible
|
||||||
|
// to COM components. If you need to access a type in this assembly from
|
||||||
|
// COM, set the ComVisible attribute to true on that type.
|
||||||
|
[assembly: ComVisible(false)]
|
||||||
|
|
||||||
|
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||||
|
[assembly: Guid("674de974-b134-4db5-bfaf-7bc3d05e16de")]
|
||||||
|
|
||||||
|
// Version information for an assembly consists of the following four values:
|
||||||
|
//
|
||||||
|
// Major Version
|
||||||
|
// Minor Version
|
||||||
|
// Build Number
|
||||||
|
// Revision
|
||||||
|
//
|
||||||
|
// You can specify all the values or you can default the Build and Revision Numbers
|
||||||
|
// by using the '*' as shown below:
|
||||||
|
// [assembly: AssemblyVersion("1.0.*")]
|
||||||
|
[assembly: AssemblyVersion("1.0.0.0")]
|
||||||
|
[assembly: AssemblyFileVersion("1.0.0.0")]
|
@ -0,0 +1,10 @@
|
|||||||
|
namespace MassiveKnob.Plugin.EmulatorDevice.Settings
|
||||||
|
{
|
||||||
|
public class EmulatorDeviceSettings
|
||||||
|
{
|
||||||
|
public int AnalogInputCount { get; set; } = 2;
|
||||||
|
public int DigitalInputCount { get; set; } = 2;
|
||||||
|
public int AnalogOutputCount { get; set; } = 2;
|
||||||
|
public int DigitalOutputCount { get; set; } = 2;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
<UserControl x:Class="MassiveKnob.Plugin.EmulatorDevice.Settings.EmulatorDeviceSettingsView"
|
||||||
|
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:settings="clr-namespace:MassiveKnob.Plugin.EmulatorDevice.Settings"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800"
|
||||||
|
d:DataContext="{d:DesignInstance Type=settings:EmulatorDeviceSettingsViewModel}">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="0" Margin="4">Analog inputs</TextBlock>
|
||||||
|
<TextBox Grid.Row="0" Grid.Column="1" Margin="4" Width="150" HorizontalAlignment="Left" Text="{Binding AnalogInputCount}" />
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="1" Grid.Column="0" Margin="4">Digital inputs</TextBlock>
|
||||||
|
<TextBox Grid.Row="1" Grid.Column="1" Margin="4" Width="150" HorizontalAlignment="Left" Text="{Binding DigitalInputCount}" />
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="2" Grid.Column="0" Margin="4">Analog outputs</TextBlock>
|
||||||
|
<TextBox Grid.Row="2" Grid.Column="1" Margin="4" Width="150" HorizontalAlignment="Left" Text="{Binding AnalogOutputCount}" />
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="3" Grid.Column="0" Margin="4">Digital outputs</TextBlock>
|
||||||
|
<TextBox Grid.Row="3" Grid.Column="1" Margin="4" Width="150" HorizontalAlignment="Left" Text="{Binding DigitalOutputCount}" />
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
@ -0,0 +1,15 @@
|
|||||||
|
namespace MassiveKnob.Plugin.EmulatorDevice.Settings
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Interaction logic for EmulatorDeviceSettingsView.xaml
|
||||||
|
/// </summary>
|
||||||
|
public partial class EmulatorDeviceSettingsView
|
||||||
|
{
|
||||||
|
public EmulatorDeviceSettingsView(EmulatorDeviceSettingsViewModel settingsViewModel)
|
||||||
|
{
|
||||||
|
DataContext = settingsViewModel;
|
||||||
|
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,81 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace MassiveKnob.Plugin.EmulatorDevice.Settings
|
||||||
|
{
|
||||||
|
public class EmulatorDeviceSettingsViewModel : INotifyPropertyChanged
|
||||||
|
{
|
||||||
|
private readonly EmulatorDeviceSettings settings;
|
||||||
|
public event PropertyChangedEventHandler PropertyChanged;
|
||||||
|
|
||||||
|
|
||||||
|
// ReSharper disable UnusedMember.Global - used by WPF Binding
|
||||||
|
public int AnalogInputCount
|
||||||
|
{
|
||||||
|
get => settings.AnalogInputCount;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == settings.AnalogInputCount)
|
||||||
|
return;
|
||||||
|
|
||||||
|
settings.AnalogInputCount = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public int DigitalInputCount
|
||||||
|
{
|
||||||
|
get => settings.DigitalInputCount;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == settings.DigitalInputCount)
|
||||||
|
return;
|
||||||
|
|
||||||
|
settings.DigitalInputCount = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public int AnalogOutputCount
|
||||||
|
{
|
||||||
|
get => settings.AnalogOutputCount;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == settings.AnalogOutputCount)
|
||||||
|
return;
|
||||||
|
|
||||||
|
settings.AnalogOutputCount = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public int DigitalOutputCount
|
||||||
|
{
|
||||||
|
get => settings.DigitalOutputCount;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == settings.DigitalOutputCount)
|
||||||
|
return;
|
||||||
|
|
||||||
|
settings.DigitalOutputCount = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ReSharper restore UnusedMember.Global
|
||||||
|
|
||||||
|
|
||||||
|
public EmulatorDeviceSettingsViewModel(EmulatorDeviceSettings settings)
|
||||||
|
{
|
||||||
|
this.settings = settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
|
||||||
|
{
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,84 @@
|
|||||||
|
using System;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using MassiveKnob.Plugin.SerialDevice.Settings;
|
||||||
|
using MassiveKnob.Plugin.SerialDevice.Worker;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MassiveKnob.Plugin.SerialDevice.Devices
|
||||||
|
{
|
||||||
|
public class SerialDevice : IMassiveKnobDevice
|
||||||
|
{
|
||||||
|
public Guid DeviceId { get; } = new Guid("65255f25-d8f6-426b-8f12-cf03c44a1bf5");
|
||||||
|
public string Name { get; } = "Serial device";
|
||||||
|
public string Description { get; } = "A Serial (USB) device which implements the Massive Knob Protocol.";
|
||||||
|
|
||||||
|
public IMassiveKnobDeviceInstance Create(ILogger logger)
|
||||||
|
{
|
||||||
|
return new Instance(logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private class Instance : IMassiveKnobDeviceInstance
|
||||||
|
{
|
||||||
|
private readonly ILogger logger;
|
||||||
|
private IMassiveKnobDeviceContext deviceContext;
|
||||||
|
private SerialDeviceSettings settings;
|
||||||
|
private SerialWorker worker;
|
||||||
|
|
||||||
|
|
||||||
|
public Instance(ILogger logger)
|
||||||
|
{
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void Initialize(IMassiveKnobDeviceContext context)
|
||||||
|
{
|
||||||
|
deviceContext = context;
|
||||||
|
settings = deviceContext.GetSettings<SerialDeviceSettings>();
|
||||||
|
|
||||||
|
worker = new SerialWorker(context, logger);
|
||||||
|
ApplySettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
worker.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void ApplySettings()
|
||||||
|
{
|
||||||
|
worker.Connect(settings.PortName, settings.BaudRate, settings.DtrEnable);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public UserControl CreateSettingsControl()
|
||||||
|
{
|
||||||
|
var viewModel = new SerialDeviceSettingsViewModel(settings);
|
||||||
|
viewModel.PropertyChanged += (sender, args) =>
|
||||||
|
{
|
||||||
|
if (!viewModel.IsSettingsProperty(args.PropertyName))
|
||||||
|
return;
|
||||||
|
|
||||||
|
deviceContext.SetSettings(settings);
|
||||||
|
ApplySettings();
|
||||||
|
};
|
||||||
|
|
||||||
|
return new SerialDeviceSettingsView(viewModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void SetAnalogOutput(int analogOutputIndex, byte value)
|
||||||
|
{
|
||||||
|
worker.SetAnalogOutput(analogOutputIndex, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetDigitalOutput(int digitalOutputIndex, bool on)
|
||||||
|
{
|
||||||
|
worker.SetDigitalOutput(digitalOutputIndex, on);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,95 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||||
|
<PropertyGroup>
|
||||||
|
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||||
|
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||||
|
<ProjectGuid>{FC0D22D8-5F1B-4D85-8269-FA4837CDE3A2}</ProjectGuid>
|
||||||
|
<OutputType>Library</OutputType>
|
||||||
|
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||||
|
<RootNamespace>MassiveKnob.Plugin.SerialDevice</RootNamespace>
|
||||||
|
<AssemblyName>MassiveKnob.Plugin.SerialDevice</AssemblyName>
|
||||||
|
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
|
||||||
|
<FileAlignment>512</FileAlignment>
|
||||||
|
<Deterministic>true</Deterministic>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||||
|
<DebugSymbols>true</DebugSymbols>
|
||||||
|
<DebugType>full</DebugType>
|
||||||
|
<Optimize>false</Optimize>
|
||||||
|
<OutputPath>bin\Debug\</OutputPath>
|
||||||
|
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||||
|
<ErrorReport>prompt</ErrorReport>
|
||||||
|
<WarningLevel>4</WarningLevel>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||||
|
<DebugType>pdbonly</DebugType>
|
||||||
|
<Optimize>true</Optimize>
|
||||||
|
<OutputPath>bin\Release\</OutputPath>
|
||||||
|
<DefineConstants>TRACE</DefineConstants>
|
||||||
|
<ErrorReport>prompt</ErrorReport>
|
||||||
|
<WarningLevel>4</WarningLevel>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="PresentationCore" />
|
||||||
|
<Reference Include="PresentationFramework" />
|
||||||
|
<Reference Include="System" />
|
||||||
|
<Reference Include="System.Core" />
|
||||||
|
<Reference Include="System.Xaml" />
|
||||||
|
<Reference Include="System.Xml.Linq" />
|
||||||
|
<Reference Include="System.Data.DataSetExtensions" />
|
||||||
|
<Reference Include="Microsoft.CSharp" />
|
||||||
|
<Reference Include="System.Data" />
|
||||||
|
<Reference Include="System.Net.Http" />
|
||||||
|
<Reference Include="System.Xml" />
|
||||||
|
<Reference Include="WindowsBase" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="Devices\SerialDevice.cs" />
|
||||||
|
<Compile Include="MassiveKnobSerialDevicePlugin.cs" />
|
||||||
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
|
<Compile Include="Settings\SerialDeviceSettingsView.xaml.cs">
|
||||||
|
<DependentUpon>SerialDeviceSettingsView.xaml</DependentUpon>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="Settings\SerialDeviceSettingsViewModel.cs" />
|
||||||
|
<Compile Include="Settings\SerialDeviceSettings.cs" />
|
||||||
|
<Compile Include="Worker\SerialWorker.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\MassiveKnob.Plugin\MassiveKnob.Plugin.csproj">
|
||||||
|
<Project>{A1298BE4-1D23-416C-8C56-FC9264487A95}</Project>
|
||||||
|
<Name>MassiveKnob.Plugin</Name>
|
||||||
|
</ProjectReference>
|
||||||
|
<ProjectReference Include="..\min.NET\MIN.SerialPort\MIN.SerialPort.csproj">
|
||||||
|
<Project>{db8819eb-d2b7-4aae-a699-bd200f2c113a}</Project>
|
||||||
|
<Name>MIN.SerialPort</Name>
|
||||||
|
</ProjectReference>
|
||||||
|
<ProjectReference Include="..\min.NET\MIN\MIN.csproj">
|
||||||
|
<Project>{fc1c9cb5-8b71-4039-9636-90e578a71061}</Project>
|
||||||
|
<Name>MIN</Name>
|
||||||
|
</ProjectReference>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Page Include="Settings\SerialDeviceSettingsView.xaml">
|
||||||
|
<SubType>Designer</SubType>
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
</Page>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<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>
|
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"EntryAssembly": "MassiveKnob.Plugin.SerialDevice.dll"
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace MassiveKnob.Plugin.SerialDevice
|
||||||
|
{
|
||||||
|
[MassiveKnobPlugin]
|
||||||
|
public class MassiveKnobSerialDevicePlugin : IMassiveKnobDevicePlugin
|
||||||
|
{
|
||||||
|
public Guid PluginId { get; } = new Guid("276475e6-5ff0-420f-82dc-8aff5e8631d5");
|
||||||
|
public string Name { get; } = "Serial Device";
|
||||||
|
public string Description { get; } = "A Serial (USB) device which implements the Massive Knob Protocol.";
|
||||||
|
public string Author { get; } = "Mark van Renswoude <mark@x2software.net>";
|
||||||
|
public string Url { get; } = "https://www.github.com/MvRens/MassiveKnob/";
|
||||||
|
|
||||||
|
public IEnumerable<IMassiveKnobDevice> Devices { get; } = new IMassiveKnobDevice[]
|
||||||
|
{
|
||||||
|
new Devices.SerialDevice()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
// General Information about an assembly is controlled through the following
|
||||||
|
// set of attributes. Change these attribute values to modify the information
|
||||||
|
// associated with an assembly.
|
||||||
|
[assembly: AssemblyTitle("MassiveKnob.Plugin.SerialDevice")]
|
||||||
|
[assembly: AssemblyDescription("")]
|
||||||
|
[assembly: AssemblyConfiguration("")]
|
||||||
|
[assembly: AssemblyCompany("")]
|
||||||
|
[assembly: AssemblyProduct("MassiveKnob.Plugin.SerialDevice")]
|
||||||
|
[assembly: AssemblyCopyright("Copyright © 2021")]
|
||||||
|
[assembly: AssemblyTrademark("")]
|
||||||
|
[assembly: AssemblyCulture("")]
|
||||||
|
|
||||||
|
// Setting ComVisible to false makes the types in this assembly not visible
|
||||||
|
// to COM components. If you need to access a type in this assembly from
|
||||||
|
// COM, set the ComVisible attribute to true on that type.
|
||||||
|
[assembly: ComVisible(false)]
|
||||||
|
|
||||||
|
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||||
|
[assembly: Guid("fc0d22d8-5f1b-4d85-8269-fa4837cde3a2")]
|
||||||
|
|
||||||
|
// Version information for an assembly consists of the following four values:
|
||||||
|
//
|
||||||
|
// Major Version
|
||||||
|
// Minor Version
|
||||||
|
// Build Number
|
||||||
|
// Revision
|
||||||
|
//
|
||||||
|
// You can specify all the values or you can default the Build and Revision Numbers
|
||||||
|
// by using the '*' as shown below:
|
||||||
|
// [assembly: AssemblyVersion("1.0.*")]
|
||||||
|
[assembly: AssemblyVersion("1.0.0.0")]
|
||||||
|
[assembly: AssemblyFileVersion("1.0.0.0")]
|
@ -0,0 +1,9 @@
|
|||||||
|
namespace MassiveKnob.Plugin.SerialDevice.Settings
|
||||||
|
{
|
||||||
|
public class SerialDeviceSettings
|
||||||
|
{
|
||||||
|
public string PortName { get; set; } = null;
|
||||||
|
public int BaudRate { get; set; } = 115200;
|
||||||
|
public bool DtrEnable { get; set; } = false;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
<UserControl x:Class="MassiveKnob.Plugin.SerialDevice.Settings.SerialDeviceSettingsView"
|
||||||
|
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:settings="clr-namespace:MassiveKnob.Plugin.SerialDevice.Settings"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DesignHeight="100" d:DesignWidth="800"
|
||||||
|
d:DataContext="{d:DesignInstance settings:SerialDeviceSettingsViewModel}">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="0" Margin="4">Serial port</TextBlock>
|
||||||
|
<ComboBox Grid.Row="0" Grid.Column="1" Margin="4" Width="150" HorizontalAlignment="Left" ItemsSource="{Binding SerialPorts}" SelectedItem="{Binding PortName}" />
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="1" Grid.Column="0" Margin="4">Baud rate</TextBlock>
|
||||||
|
<TextBox Grid.Row="1" Grid.Column="1" Margin="4" Width="150" HorizontalAlignment="Left" Text="{Binding BaudRate}" />
|
||||||
|
|
||||||
|
|
||||||
|
<CheckBox Grid.Row="2" Grid.Column="1" Margin="4" HorizontalAlignment="Left" IsChecked="{Binding DtrEnable}">
|
||||||
|
<TextBlock>Enable DTR (may be required on some Arduino's like Leonardo / Pro Micro)</TextBlock>
|
||||||
|
</CheckBox>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
@ -0,0 +1,22 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace MassiveKnob.Plugin.SerialDevice.Settings
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Interaction logic for SerialDeviceSettingsView.xaml
|
||||||
|
/// </summary>
|
||||||
|
public partial class SerialDeviceSettingsView : IDisposable
|
||||||
|
{
|
||||||
|
public SerialDeviceSettingsView(SerialDeviceSettingsViewModel viewModel)
|
||||||
|
{
|
||||||
|
DataContext = viewModel;
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
(DataContext as SerialDeviceSettingsViewModel)?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,126 @@
|
|||||||
|
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 : IDisposable, INotifyPropertyChanged, IObserver<DeviceNotificationEvent>
|
||||||
|
{
|
||||||
|
private readonly SerialDeviceSettings settings;
|
||||||
|
private IList<string> serialPorts;
|
||||||
|
private readonly IDisposable deviceSubscription;
|
||||||
|
public event PropertyChangedEventHandler PropertyChanged;
|
||||||
|
|
||||||
|
|
||||||
|
// ReSharper disable UnusedMember.Global - used by WPF Binding
|
||||||
|
public IList<string> SerialPorts
|
||||||
|
{
|
||||||
|
get => serialPorts;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
serialPorts = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public string PortName
|
||||||
|
{
|
||||||
|
get => settings.PortName;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == settings.PortName || value == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
settings.PortName = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public int BaudRate
|
||||||
|
{
|
||||||
|
get => settings.BaudRate;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == settings.BaudRate)
|
||||||
|
return;
|
||||||
|
|
||||||
|
settings.BaudRate = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public bool DtrEnable
|
||||||
|
{
|
||||||
|
get => settings.DtrEnable;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == settings.DtrEnable)
|
||||||
|
return;
|
||||||
|
|
||||||
|
settings.DtrEnable = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ReSharper restore UnusedMember.Global
|
||||||
|
|
||||||
|
|
||||||
|
public SerialDeviceSettingsViewModel(SerialDeviceSettings settings)
|
||||||
|
{
|
||||||
|
this.settings = settings;
|
||||||
|
|
||||||
|
serialPorts = SerialPort.GetPortNames();
|
||||||
|
deviceSubscription = DeviceNotification.OnNotification.Subscribe(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
deviceSubscription.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public bool IsSettingsProperty(string propertyName)
|
||||||
|
{
|
||||||
|
return propertyName != nameof(SerialPorts);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
|
||||||
|
{
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
377
Windows/MassiveKnob.Plugin.SerialDevice/Worker/SerialWorker.cs
Normal file
377
Windows/MassiveKnob.Plugin.SerialDevice/Worker/SerialWorker.cs
Normal file
@ -0,0 +1,377 @@
|
|||||||
|
using System;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using MIN;
|
||||||
|
using MIN.Abstractions;
|
||||||
|
using MIN.SerialPort;
|
||||||
|
|
||||||
|
namespace MassiveKnob.Plugin.SerialDevice.Worker
|
||||||
|
{
|
||||||
|
public class SerialWorker : IDisposable
|
||||||
|
{
|
||||||
|
private readonly IMassiveKnobDeviceContext context;
|
||||||
|
private readonly ILogger logger;
|
||||||
|
|
||||||
|
private readonly object minProtocolLock = new object();
|
||||||
|
private IMINProtocol minProtocol;
|
||||||
|
private string lastPortName;
|
||||||
|
private int lastBaudRate;
|
||||||
|
private bool lastDtrEnable;
|
||||||
|
|
||||||
|
|
||||||
|
private enum MassiveKnobFrameID
|
||||||
|
{
|
||||||
|
Handshake = 42,
|
||||||
|
HandshakeResponse = 43,
|
||||||
|
AnalogInput = 1,
|
||||||
|
DigitalInput = 2,
|
||||||
|
AnalogOutput = 3,
|
||||||
|
DigitalOutput = 4,
|
||||||
|
Quit = 62,
|
||||||
|
Error = 63
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public SerialWorker(IMassiveKnobDeviceContext context, ILogger logger)
|
||||||
|
{
|
||||||
|
this.context = context;
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void Connect(string portName, int baudRate, bool dtrEnable)
|
||||||
|
{
|
||||||
|
context.Connecting();
|
||||||
|
|
||||||
|
lock (minProtocolLock)
|
||||||
|
{
|
||||||
|
if (portName == lastPortName && baudRate == lastBaudRate && dtrEnable == lastDtrEnable)
|
||||||
|
return;
|
||||||
|
|
||||||
|
lastPortName = portName;
|
||||||
|
lastBaudRate = baudRate;
|
||||||
|
lastDtrEnable = dtrEnable;
|
||||||
|
|
||||||
|
Disconnect();
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(portName) || baudRate == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
|
||||||
|
minProtocol?.Dispose();
|
||||||
|
minProtocol = new MINProtocol(new MINSerialTransport(portName, baudRate, dtrEnable: dtrEnable), logger);
|
||||||
|
minProtocol.OnConnected += MinProtocolOnOnConnected;
|
||||||
|
minProtocol.OnFrame += MinProtocolOnOnFrame;
|
||||||
|
minProtocol.Start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void SetAnalogOutput(int analogOutputIndex, byte value)
|
||||||
|
{
|
||||||
|
IMINProtocol instance;
|
||||||
|
|
||||||
|
lock (minProtocolLock)
|
||||||
|
{
|
||||||
|
instance = minProtocol;
|
||||||
|
}
|
||||||
|
|
||||||
|
instance?.QueueFrame(
|
||||||
|
(byte)MassiveKnobFrameID.AnalogOutput,
|
||||||
|
new [] { (byte)analogOutputIndex, value });
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetDigitalOutput(int digitalOutputIndex, bool on)
|
||||||
|
{
|
||||||
|
IMINProtocol instance;
|
||||||
|
|
||||||
|
lock (minProtocolLock)
|
||||||
|
{
|
||||||
|
instance = minProtocol;
|
||||||
|
}
|
||||||
|
|
||||||
|
instance?.QueueFrame(
|
||||||
|
(byte)MassiveKnobFrameID.DigitalOutput,
|
||||||
|
new [] { (byte)digitalOutputIndex, on ? (byte)1 : (byte)0 });
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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 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)
|
||||||
|
{
|
||||||
|
case MassiveKnobFrameID.HandshakeResponse:
|
||||||
|
if (e.Payload.Length < 4)
|
||||||
|
{
|
||||||
|
logger.LogError("Invalid handshake response length, expected 4, got {length}: {payload}",
|
||||||
|
e.Payload.Length, BitConverter.ToString(e.Payload));
|
||||||
|
|
||||||
|
Disconnect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var specs = new DeviceSpecs(e.Payload[0], e.Payload[1], e.Payload[2], e.Payload[3]);
|
||||||
|
context.Connected(specs);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MassiveKnobFrameID.AnalogInput:
|
||||||
|
if (e.Payload.Length < 2)
|
||||||
|
{
|
||||||
|
logger.LogError("Invalid analog input payload length, expected 2, got {length}: {payload}",
|
||||||
|
e.Payload.Length, BitConverter.ToString(e.Payload));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.AnalogChanged(e.Payload[0], e.Payload[1]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MassiveKnobFrameID.DigitalInput:
|
||||||
|
if (e.Payload.Length < 2)
|
||||||
|
{
|
||||||
|
logger.LogError("Invalid digital input payload length, expected 2, got {length}: {payload}",
|
||||||
|
e.Payload.Length, BitConverter.ToString(e.Payload));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.DigitalChanged(e.Payload[0], e.Payload[1] != 0);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MassiveKnobFrameID.Error:
|
||||||
|
logger.LogError("Error message received from device: {message}", Encoding.ASCII.GetString(e.Payload));
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
logger.LogWarning("Unknown frame ID received: {frameId}", e.Id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void Disconnect()
|
||||||
|
{
|
||||||
|
lock (minProtocolLock)
|
||||||
|
{
|
||||||
|
minProtocol?.Dispose();
|
||||||
|
minProtocol = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.Disconnected();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
void SafeCloseSerialPort()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
serialPort?.Dispose();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
|
||||||
|
serialPort = null;
|
||||||
|
context.Connecting();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
while (serialPort == null && !cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
if (!TryConnect(ref serialPort, settings, out specs))
|
||||||
|
{
|
||||||
|
SafeCloseSerialPort();
|
||||||
|
Thread.Sleep(500);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cancellationToken.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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
context.Connected(specs);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// This is where sending data to the hardware would be implemented
|
||||||
|
while (serialPort.IsOpen && !cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
Thread.Sleep(10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
|
||||||
|
context.Disconnected();
|
||||||
|
SafeCloseSerialPort();
|
||||||
|
|
||||||
|
if (!cancellationToken.IsCancellationRequested)
|
||||||
|
Thread.Sleep(500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static bool TryConnect(ref SerialPort serialPort, ConnectionSettings settings, out DeviceSpecs specs)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
serialPort = new SerialPort(settings.PortName, settings.BaudRate)
|
||||||
|
{
|
||||||
|
Encoding = Encoding.ASCII,
|
||||||
|
ReadTimeout = 1000,
|
||||||
|
WriteTimeout = 1000,
|
||||||
|
DtrEnable = settings.DtrEnable
|
||||||
|
};
|
||||||
|
serialPort.Open();
|
||||||
|
|
||||||
|
// Send handshake
|
||||||
|
serialPort.Write(new[] { 'H', 'M', 'K', 'B' }, 0, 4);
|
||||||
|
|
||||||
|
// Wait for reply
|
||||||
|
var response = serialPort.ReadByte();
|
||||||
|
|
||||||
|
if ((char) response == 'H')
|
||||||
|
{
|
||||||
|
specs = new DeviceSpecs(serialPort.ReadByte(), serialPort.ReadByte(), serialPort.ReadByte(), serialPort.ReadByte());
|
||||||
|
if (specs.AnalogInputCount > -1 && specs.DigitalInputCount > -1 && specs.AnalogOutputCount > -1 && specs.DigitalOutputCount > -1)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
CheckForError(serialPort, (char)response);
|
||||||
|
|
||||||
|
specs = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
specs = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
context.AnalogChanged(knobIndex, volume);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static void CheckForError(SerialPort serialPort, char message)
|
||||||
|
{
|
||||||
|
if (message != 'E')
|
||||||
|
return;
|
||||||
|
|
||||||
|
var length = serialPort.ReadByte();
|
||||||
|
if (length <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var buffer = new byte[length];
|
||||||
|
var bytesRead = 0;
|
||||||
|
|
||||||
|
while (bytesRead < length)
|
||||||
|
bytesRead += serialPort.Read(buffer, bytesRead, length - bytesRead);
|
||||||
|
|
||||||
|
var errorMessage = Encoding.ASCII.GetString(buffer);
|
||||||
|
Debug.Print(errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private readonly struct ConnectionSettings
|
||||||
|
{
|
||||||
|
public readonly string PortName;
|
||||||
|
public readonly int BaudRate;
|
||||||
|
public readonly bool DtrEnable;
|
||||||
|
|
||||||
|
public ConnectionSettings(string portName, int baudRate, bool dtrEnable)
|
||||||
|
{
|
||||||
|
PortName = portName;
|
||||||
|
BaudRate = baudRate;
|
||||||
|
DtrEnable = dtrEnable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
using Voicemeeter;
|
||||||
|
|
||||||
|
namespace MassiveKnob.Plugin.VoiceMeeter.Base
|
||||||
|
{
|
||||||
|
public class BaseVoiceMeeterSettings
|
||||||
|
{
|
||||||
|
public RunVoicemeeterParam Version
|
||||||
|
{
|
||||||
|
get => InstanceRegister.Version;
|
||||||
|
set => InstanceRegister.Version = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
<UserControl x:Class="MassiveKnob.Plugin.VoiceMeeter.Base.BaseVoiceMeeterSettingsView"
|
||||||
|
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:base="clr-namespace:MassiveKnob.Plugin.VoiceMeeter.Base"
|
||||||
|
xmlns:coreAudio="clr-namespace:MassiveKnob.Plugin.VoiceMeeter"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DesignHeight="200" d:DesignWidth="800"
|
||||||
|
d:DataContext="{d:DesignInstance base:BaseVoiceMeeterSettingsViewModel}">
|
||||||
|
<StackPanel Orientation="Vertical">
|
||||||
|
<TextBlock Text="{x:Static coreAudio:Strings.SettingVoiceMeeterVersion}" />
|
||||||
|
<ComboBox Margin="0,4,0,0" ItemsSource="{Binding Versions}" SelectedItem="{Binding SelectedVersion}" DisplayMemberPath="DisplayName" />
|
||||||
|
</StackPanel>
|
||||||
|
</UserControl>
|
@ -0,0 +1,13 @@
|
|||||||
|
namespace MassiveKnob.Plugin.VoiceMeeter.Base
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Interaction logic for BaseVoiceMeeterSettingsView.xaml
|
||||||
|
/// </summary>
|
||||||
|
public partial class BaseVoiceMeeterSettingsView
|
||||||
|
{
|
||||||
|
public BaseVoiceMeeterSettingsView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,84 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using Voicemeeter;
|
||||||
|
|
||||||
|
namespace MassiveKnob.Plugin.VoiceMeeter.Base
|
||||||
|
{
|
||||||
|
public class BaseVoiceMeeterSettingsViewModel<T> : BaseVoiceMeeterSettingsViewModel where T : BaseVoiceMeeterSettings
|
||||||
|
{
|
||||||
|
protected new T Settings => (T)base.Settings;
|
||||||
|
|
||||||
|
public BaseVoiceMeeterSettingsViewModel(T settings) : base(settings)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public class BaseVoiceMeeterSettingsViewModel : INotifyPropertyChanged
|
||||||
|
{
|
||||||
|
protected readonly BaseVoiceMeeterSettings Settings;
|
||||||
|
public event PropertyChangedEventHandler PropertyChanged;
|
||||||
|
|
||||||
|
// ReSharper disable UnusedMember.Global - used by WPF Binding
|
||||||
|
public IList<VoiceMeeterVersionViewModel> Versions { get; }
|
||||||
|
|
||||||
|
private VoiceMeeterVersionViewModel selectedVersion;
|
||||||
|
public VoiceMeeterVersionViewModel SelectedVersion
|
||||||
|
{
|
||||||
|
get => selectedVersion;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == selectedVersion)
|
||||||
|
return;
|
||||||
|
|
||||||
|
selectedVersion = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
|
||||||
|
Settings.Version = value?.Version ?? RunVoicemeeterParam.None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ReSharper restore UnusedMember.Global
|
||||||
|
|
||||||
|
|
||||||
|
public BaseVoiceMeeterSettingsViewModel(BaseVoiceMeeterSettings settings)
|
||||||
|
{
|
||||||
|
Settings = settings;
|
||||||
|
|
||||||
|
Versions = new List<VoiceMeeterVersionViewModel>
|
||||||
|
{
|
||||||
|
new VoiceMeeterVersionViewModel(RunVoicemeeterParam.Voicemeeter, "VoiceMeeter Standard"),
|
||||||
|
new VoiceMeeterVersionViewModel(RunVoicemeeterParam.VoicemeeterBanana, "VoiceMeeter Banana"),
|
||||||
|
new VoiceMeeterVersionViewModel(RunVoicemeeterParam.VoicemeeterPotato, "VoiceMeeter Potato")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public virtual bool IsSettingsProperty(string propertyName)
|
||||||
|
{
|
||||||
|
// SelectedVersion already trigger a VoiceMeeterVersionChanged for all instances,
|
||||||
|
// which causes the settings to be stored
|
||||||
|
return propertyName != nameof(Versions) && propertyName != nameof(SelectedVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
|
||||||
|
{
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class VoiceMeeterVersionViewModel
|
||||||
|
{
|
||||||
|
public RunVoicemeeterParam Version { get; }
|
||||||
|
public string DisplayName { get; }
|
||||||
|
|
||||||
|
|
||||||
|
public VoiceMeeterVersionViewModel(RunVoicemeeterParam version, string displayName)
|
||||||
|
{
|
||||||
|
Version = version;
|
||||||
|
DisplayName = displayName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,139 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Voicemeeter;
|
||||||
|
|
||||||
|
namespace MassiveKnob.Plugin.VoiceMeeter.GetParameter
|
||||||
|
{
|
||||||
|
public class VoiceMeeterGetParameterAction : IMassiveKnobAction
|
||||||
|
{
|
||||||
|
public Guid ActionId { get; } = new Guid("4904fffb-aaec-4f19-88bb-49f6ed38c3ec");
|
||||||
|
public MassiveKnobActionType ActionType { get; } = MassiveKnobActionType.OutputDigital;
|
||||||
|
public string Name { get; } = Strings.GetParameterName;
|
||||||
|
public string Description { get; } = Strings.GetParameterDescription;
|
||||||
|
|
||||||
|
|
||||||
|
public IMassiveKnobActionInstance Create(ILogger logger)
|
||||||
|
{
|
||||||
|
return new Instance();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private class Instance : IMassiveKnobActionInstance, IVoiceMeeterAction
|
||||||
|
{
|
||||||
|
private IMassiveKnobActionContext actionContext;
|
||||||
|
private VoiceMeeterGetParameterActionSettings settings;
|
||||||
|
private Parameters parameters;
|
||||||
|
private IDisposable parameterChanged;
|
||||||
|
|
||||||
|
|
||||||
|
public void Initialize(IMassiveKnobActionContext context)
|
||||||
|
{
|
||||||
|
actionContext = context;
|
||||||
|
settings = context.GetSettings<VoiceMeeterGetParameterActionSettings>();
|
||||||
|
ApplySettings();
|
||||||
|
|
||||||
|
InstanceRegister.Register(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
InstanceRegister.Unregister(this);
|
||||||
|
parameterChanged?.Dispose();
|
||||||
|
parameters?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void ApplySettings()
|
||||||
|
{
|
||||||
|
if (InstanceRegister.Version == RunVoicemeeterParam.None)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (parameters == null)
|
||||||
|
parameters = new Parameters();
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(settings.Parameter))
|
||||||
|
{
|
||||||
|
parameterChanged?.Dispose();
|
||||||
|
parameterChanged = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parameterChanged == null)
|
||||||
|
parameterChanged = parameters.Subscribe(x => ParametersChanged());
|
||||||
|
|
||||||
|
// TODO directly update output depending on value
|
||||||
|
/*
|
||||||
|
if (playbackDevice != null)
|
||||||
|
actionContext.SetDigitalOutput(settings.Inverted ? !playbackDevice.IsMuted : playbackDevice.IsMuted);
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public UserControl CreateSettingsControl()
|
||||||
|
{
|
||||||
|
var viewModel = new VoiceMeeterGetParameterActionSettingsViewModel(settings);
|
||||||
|
viewModel.PropertyChanged += (sender, args) =>
|
||||||
|
{
|
||||||
|
if (!viewModel.IsSettingsProperty(args.PropertyName))
|
||||||
|
return;
|
||||||
|
|
||||||
|
actionContext.SetSettings(settings);
|
||||||
|
ApplySettings();
|
||||||
|
};
|
||||||
|
|
||||||
|
return new VoiceMeeterGetParameterActionSettingsView(viewModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void VoiceMeeterVersionChanged()
|
||||||
|
{
|
||||||
|
// TODO update viewModel
|
||||||
|
// TODO reset parameterChanged subscription
|
||||||
|
|
||||||
|
actionContext.SetSettings(settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void ParametersChanged()
|
||||||
|
{
|
||||||
|
if (InstanceRegister.Version == RunVoicemeeterParam.None || string.IsNullOrEmpty(settings.Parameter))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// TODO if another task is already running, wait / chain
|
||||||
|
// TODO only start task if not yet initialized
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await InstanceRegister.InitializeVoicemeeter();
|
||||||
|
bool on;
|
||||||
|
|
||||||
|
if (float.TryParse(settings.Value, out var settingsFloatValue))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Even on/off values are returned as floating point "1.000" in text form,
|
||||||
|
// so try to compare in native format first
|
||||||
|
var floatValue = global::VoiceMeeter.Remote.GetParameter(settings.Parameter);
|
||||||
|
on = Math.Abs(settingsFloatValue - floatValue) < 0.001;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Fall back to text comparison
|
||||||
|
var value = global::VoiceMeeter.Remote.GetTextParameter(settings.Parameter);
|
||||||
|
on = string.Equals(value, settings.Value, StringComparison.InvariantCultureIgnoreCase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var value = global::VoiceMeeter.Remote.GetTextParameter(settings.Parameter);
|
||||||
|
on = string.Equals(value, settings.Value, StringComparison.InvariantCultureIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO check specific parameter for changes, not just any parameter
|
||||||
|
actionContext.SetDigitalOutput(settings.Inverted ? !on : on);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
using MassiveKnob.Plugin.VoiceMeeter.Base;
|
||||||
|
|
||||||
|
namespace MassiveKnob.Plugin.VoiceMeeter.GetParameter
|
||||||
|
{
|
||||||
|
public class VoiceMeeterGetParameterActionSettings : BaseVoiceMeeterSettings
|
||||||
|
{
|
||||||
|
public string Parameter { get; set; }
|
||||||
|
public string Value { get; set; }
|
||||||
|
public bool Inverted { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
<UserControl x:Class="MassiveKnob.Plugin.VoiceMeeter.GetParameter.VoiceMeeterGetParameterActionSettingsView"
|
||||||
|
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:getParameter="clr-namespace:MassiveKnob.Plugin.VoiceMeeter.GetParameter"
|
||||||
|
xmlns:coreAudio="clr-namespace:MassiveKnob.Plugin.VoiceMeeter"
|
||||||
|
xmlns:base="clr-namespace:MassiveKnob.Plugin.VoiceMeeter.Base"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DesignHeight="200" d:DesignWidth="800"
|
||||||
|
d:DataContext="{d:DesignInstance getParameter:VoiceMeeterGetParameterActionSettingsViewModel}">
|
||||||
|
<StackPanel Orientation="Vertical">
|
||||||
|
<base:BaseVoiceMeeterSettingsView />
|
||||||
|
|
||||||
|
<TextBlock Margin="0,24,0,0" Text="{x:Static coreAudio:Strings.SettingGetParameterParameter}" />
|
||||||
|
<TextBox Margin="0,4,0,0" Text="{Binding Parameter}" />
|
||||||
|
|
||||||
|
<TextBlock Margin="0,8,0,0" Text="{x:Static coreAudio:Strings.SettingGetParameterValue}" />
|
||||||
|
<TextBox Margin="0,4,0,0" Text="{Binding Value}" />
|
||||||
|
|
||||||
|
<CheckBox Margin="0,8,0,0" IsChecked="{Binding Inverted}">
|
||||||
|
<TextBlock Text="{x:Static coreAudio:Strings.SettingGetParameterInverted}" />
|
||||||
|
</CheckBox>
|
||||||
|
</StackPanel>
|
||||||
|
</UserControl>
|
@ -0,0 +1,14 @@
|
|||||||
|
namespace MassiveKnob.Plugin.VoiceMeeter.GetParameter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Interaction logic for VoiceMeeterGetParameterActionSettingsView.xaml
|
||||||
|
/// </summary>
|
||||||
|
public partial class VoiceMeeterGetParameterActionSettingsView
|
||||||
|
{
|
||||||
|
public VoiceMeeterGetParameterActionSettingsView(VoiceMeeterGetParameterActionSettingsViewModel viewModel)
|
||||||
|
{
|
||||||
|
DataContext = viewModel;
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,54 @@
|
|||||||
|
using MassiveKnob.Plugin.VoiceMeeter.Base;
|
||||||
|
|
||||||
|
namespace MassiveKnob.Plugin.VoiceMeeter.GetParameter
|
||||||
|
{
|
||||||
|
public class VoiceMeeterGetParameterActionSettingsViewModel : BaseVoiceMeeterSettingsViewModel<VoiceMeeterGetParameterActionSettings>
|
||||||
|
{
|
||||||
|
// ReSharper disable UnusedMember.Global - used by WPF Binding
|
||||||
|
public string Parameter
|
||||||
|
{
|
||||||
|
get => Settings.Parameter;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == Settings.Parameter)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Settings.Parameter = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Value
|
||||||
|
{
|
||||||
|
get => Settings.Value;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == Settings.Value)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Settings.Value = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Inverted
|
||||||
|
{
|
||||||
|
get => Settings.Inverted;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == Settings.Inverted)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Settings.Inverted = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ReSharper restore UnusedMember.Global
|
||||||
|
|
||||||
|
|
||||||
|
// ReSharper disable once SuggestBaseTypeForParameter - by design
|
||||||
|
public VoiceMeeterGetParameterActionSettingsViewModel(VoiceMeeterGetParameterActionSettings settings) : base(settings)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
namespace MassiveKnob.Plugin.VoiceMeeter
|
||||||
|
{
|
||||||
|
public interface IVoiceMeeterAction
|
||||||
|
{
|
||||||
|
void VoiceMeeterVersionChanged();
|
||||||
|
}
|
||||||
|
}
|
69
Windows/MassiveKnob.Plugin.VoiceMeeter/InstanceRegister.cs
Normal file
69
Windows/MassiveKnob.Plugin.VoiceMeeter/InstanceRegister.cs
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Voicemeeter;
|
||||||
|
|
||||||
|
namespace MassiveKnob.Plugin.VoiceMeeter
|
||||||
|
{
|
||||||
|
public static class InstanceRegister
|
||||||
|
{
|
||||||
|
private static readonly object InstancesLock = new object();
|
||||||
|
private static readonly HashSet<IVoiceMeeterAction> Instances = new HashSet<IVoiceMeeterAction>();
|
||||||
|
private static Task initializeTask;
|
||||||
|
|
||||||
|
|
||||||
|
// The VoiceMeeter Remote only connects to one instance, so all actions need to be in sync
|
||||||
|
private static RunVoicemeeterParam version;
|
||||||
|
public static RunVoicemeeterParam Version
|
||||||
|
{
|
||||||
|
get => version;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == version)
|
||||||
|
return;
|
||||||
|
|
||||||
|
version = value;
|
||||||
|
Notify(action => action.VoiceMeeterVersionChanged());
|
||||||
|
|
||||||
|
initializeTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await global::VoiceMeeter.Remote.Initialize(version);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static Task InitializeVoicemeeter()
|
||||||
|
{
|
||||||
|
return initializeTask ?? Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static void Register(IVoiceMeeterAction instance)
|
||||||
|
{
|
||||||
|
lock (InstancesLock)
|
||||||
|
{
|
||||||
|
Instances.Add(instance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static void Unregister(IVoiceMeeterAction instance)
|
||||||
|
{
|
||||||
|
lock (InstancesLock)
|
||||||
|
{
|
||||||
|
Instances.Remove(instance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static void Notify(Action<IVoiceMeeterAction> action)
|
||||||
|
{
|
||||||
|
lock (InstancesLock)
|
||||||
|
{
|
||||||
|
foreach (var instance in Instances)
|
||||||
|
action(instance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,110 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||||
|
<PropertyGroup>
|
||||||
|
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||||
|
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||||
|
<ProjectGuid>{19533600-D4F6-4BD4-82A3-C0234FDF044C}</ProjectGuid>
|
||||||
|
<OutputType>Library</OutputType>
|
||||||
|
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||||
|
<RootNamespace>MassiveKnob.Plugin.VoiceMeeter</RootNamespace>
|
||||||
|
<AssemblyName>MassiveKnob.Plugin.VoiceMeeter</AssemblyName>
|
||||||
|
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
|
||||||
|
<FileAlignment>512</FileAlignment>
|
||||||
|
<Deterministic>true</Deterministic>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||||
|
<DebugSymbols>true</DebugSymbols>
|
||||||
|
<DebugType>full</DebugType>
|
||||||
|
<Optimize>false</Optimize>
|
||||||
|
<OutputPath>$(localappdata)\MassiveKnob\Plugins\VoiceMeeter\</OutputPath>
|
||||||
|
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||||
|
<ErrorReport>prompt</ErrorReport>
|
||||||
|
<WarningLevel>4</WarningLevel>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||||
|
<DebugType>pdbonly</DebugType>
|
||||||
|
<Optimize>true</Optimize>
|
||||||
|
<OutputPath>bin\Release\</OutputPath>
|
||||||
|
<DefineConstants>TRACE</DefineConstants>
|
||||||
|
<ErrorReport>prompt</ErrorReport>
|
||||||
|
<WarningLevel>4</WarningLevel>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="PresentationCore" />
|
||||||
|
<Reference Include="PresentationFramework" />
|
||||||
|
<Reference Include="System" />
|
||||||
|
<Reference Include="System.Core" />
|
||||||
|
<Reference Include="System.Xaml" />
|
||||||
|
<Reference Include="System.Xml.Linq" />
|
||||||
|
<Reference Include="System.Data.DataSetExtensions" />
|
||||||
|
<Reference Include="Microsoft.CSharp" />
|
||||||
|
<Reference Include="System.Data" />
|
||||||
|
<Reference Include="System.Net.Http" />
|
||||||
|
<Reference Include="System.Xml" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="Base\BaseVoiceMeeterSettings.cs" />
|
||||||
|
<Compile Include="Base\BaseVoiceMeeterSettingsView.xaml.cs">
|
||||||
|
<DependentUpon>BaseVoiceMeeterSettingsView.xaml</DependentUpon>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="Base\BaseVoiceMeeterSettingsViewModel.cs" />
|
||||||
|
<Compile Include="GetParameter\VoiceMeeterGetParameterAction.cs" />
|
||||||
|
<Compile Include="GetParameter\VoiceMeeterGetParameterActionSettings.cs" />
|
||||||
|
<Compile Include="GetParameter\VoiceMeeterGetParameterActionSettingsView.xaml.cs">
|
||||||
|
<DependentUpon>VoiceMeeterGetParameterActionSettingsView.xaml</DependentUpon>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="GetParameter\VoiceMeeterGetParameterActionSettingsViewModel.cs" />
|
||||||
|
<Compile Include="InstanceRegister.cs" />
|
||||||
|
<Compile Include="IVoiceMeeterAction.cs" />
|
||||||
|
<Compile Include="MassiveKnobVoiceMeeterPlugin.cs" />
|
||||||
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
|
<Compile Include="RunMacro\VoiceMeeterRunMacroAction.cs" />
|
||||||
|
<Compile Include="RunMacro\VoiceMeeterRunMacroActionSettings.cs" />
|
||||||
|
<Compile Include="RunMacro\VoiceMeeterRunMacroActionSettingsView.xaml.cs">
|
||||||
|
<DependentUpon>VoiceMeeterRunMacroActionSettingsView.xaml</DependentUpon>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="RunMacro\VoiceMeeterRunMacroActionSettingsViewModel.cs" />
|
||||||
|
<Compile Include="Strings.Designer.cs">
|
||||||
|
<DependentUpon>Strings.resx</DependentUpon>
|
||||||
|
<AutoGen>True</AutoGen>
|
||||||
|
<DesignTime>True</DesignTime>
|
||||||
|
</Compile>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<EmbeddedResource Include="Strings.resx">
|
||||||
|
<Generator>PublicResXFileCodeGenerator</Generator>
|
||||||
|
<LastGenOutput>Strings.Designer.cs</LastGenOutput>
|
||||||
|
</EmbeddedResource>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\MassiveKnob.Plugin\MassiveKnob.Plugin.csproj">
|
||||||
|
<Project>{A1298BE4-1D23-416C-8C56-FC9264487A95}</Project>
|
||||||
|
<Name>MassiveKnob.Plugin</Name>
|
||||||
|
</ProjectReference>
|
||||||
|
<ProjectReference Include="..\VoicemeeterRemote\Voicemeeter\Voicemeeter.csproj">
|
||||||
|
<Project>{f35dd8e5-91fa-403e-b6f6-8d2b4ae84198}</Project>
|
||||||
|
<Name>Voicemeeter</Name>
|
||||||
|
</ProjectReference>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="MassiveKnobPlugin.json">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Page Include="Base\BaseVoiceMeeterSettingsView.xaml">
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
<SubType>Designer</SubType>
|
||||||
|
</Page>
|
||||||
|
<Page Include="GetParameter\VoiceMeeterGetParameterActionSettingsView.xaml">
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
<SubType>Designer</SubType>
|
||||||
|
</Page>
|
||||||
|
<Page Include="RunMacro\VoiceMeeterRunMacroActionSettingsView.xaml">
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
<SubType>Designer</SubType>
|
||||||
|
</Page>
|
||||||
|
</ItemGroup>
|
||||||
|
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||||
|
</Project>
|
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"EntryAssembly": "MassiveKnob.Plugin.VoiceMeeter.dll"
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using MassiveKnob.Plugin.VoiceMeeter.GetParameter;
|
||||||
|
using MassiveKnob.Plugin.VoiceMeeter.RunMacro;
|
||||||
|
|
||||||
|
namespace MassiveKnob.Plugin.VoiceMeeter
|
||||||
|
{
|
||||||
|
[MassiveKnobPlugin]
|
||||||
|
public class MassiveKnobVoiceMeeterPlugin : IMassiveKnobActionPlugin
|
||||||
|
{
|
||||||
|
public Guid PluginId { get; } = new Guid("cf6634f1-97e3-4a18-a4aa-289b558c0e82");
|
||||||
|
public string Name { get; } = Strings.PluginName;
|
||||||
|
public string Description { get; } = Strings.PluginDescription;
|
||||||
|
public string Author { get; } = "Mark van Renswoude <mark@x2software.net>";
|
||||||
|
public string Url { get; } = "https://www.github.com/MvRens/MassiveKnob/";
|
||||||
|
|
||||||
|
public IEnumerable<IMassiveKnobAction> Actions { get; } = new IMassiveKnobAction[]
|
||||||
|
{
|
||||||
|
new VoiceMeeterRunMacroAction(),
|
||||||
|
new VoiceMeeterGetParameterAction()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user