From 9869f46a494c61f8a30e83603d0cb8d77cbe7c4f Mon Sep 17 00:00:00 2001 From: Mark van Renswoude Date: Sun, 28 Feb 2021 11:55:23 +0100 Subject: [PATCH] Changed serial protocol to MIN Implemented logging --- .gitmodules | 3 + Arduino/MassiveKnob/MassiveKnob.ino | 203 ++---- Arduino/MassiveKnob/min.c | 641 ++++++++++++++++++ Arduino/MassiveKnob/min.h | 208 ++++++ .../GetMuted/DeviceGetMutedAction.cs | 3 +- .../GetVolume/DeviceGetVolumeAction.cs | 3 +- .../MassiveKnob.Plugin.CoreAudio.csproj | 3 + .../SetMuted/DeviceSetMutedAction.cs | 3 +- .../SetVolume/DeviceSetVolumeAction.cs | 5 +- .../Devices/EmulatorDevice.cs | 4 +- .../MassiveKnob.Plugin.EmulatorDevice.csproj | 6 +- .../Devices/SerialDevice.cs | 17 +- .../MassiveKnob.Plugin.SerialDevice.csproj | 16 + .../Settings/SerialDeviceSettings.cs | 1 + .../Settings/SerialDeviceSettingsView.xaml | 6 + .../Settings/SerialDeviceSettingsViewModel.cs | 14 + .../Worker/SerialWorker.cs | 149 +++- .../MassiveKnob.Plugin/IMassiveKnobAction.cs | 3 +- .../MassiveKnob.Plugin/IMassiveKnobDevice.cs | 3 +- .../MassiveKnob.Plugin.csproj | 3 + Windows/MassiveKnob.sln | 12 + Windows/MassiveKnob.sln.DotSettings | 2 + Windows/MassiveKnob/MassiveKnob.csproj | 12 + .../Model/MassiveKnobOrchestrator.cs | 16 +- Windows/MassiveKnob/Program.cs | 22 +- Windows/min.NET | 1 + 26 files changed, 1173 insertions(+), 186 deletions(-) create mode 100644 .gitmodules create mode 100644 Arduino/MassiveKnob/min.c create mode 100644 Arduino/MassiveKnob/min.h create mode 160000 Windows/min.NET diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..7ccb927 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "Windows/min.NET"] + path = Windows/min.NET + url = https://github.com/MvRens/min.NET diff --git a/Arduino/MassiveKnob/MassiveKnob.ino b/Arduino/MassiveKnob/MassiveKnob.ino index 9e795ee..12f237b 100644 --- a/Arduino/MassiveKnob/MassiveKnob.ino +++ b/Arduino/MassiveKnob/MassiveKnob.ino @@ -9,7 +9,7 @@ const byte AnalogInputCount = 1; // For each potentiometer, specify the port const byte AnalogInputPin[AnalogInputCount] = { - A2 + A0 }; // Minimum time between reporting changing values, reduces serial traffic @@ -30,19 +30,38 @@ const byte EMASeedCount = 5; * Here be dragons. * */ +#include "./min.h" +#include "./min.c" + + +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; + + 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 analogValue[AnalogInputCount]; unsigned long lastChange[AnalogInputCount]; int analogReadValue[AnalogInputCount]; float emaValue[AnalogInputCount]; -unsigned long currentTime; unsigned long lastPlot; @@ -54,6 +73,10 @@ void setup() while (!Serial) {} + // Set up the MIN protocol (http://github.com/min-protocol/min) + min_init_context(&minContext, 0); + + // Seed the moving average for (byte analogInputIndex = 0; analogInputIndex < AnalogInputCount; analogInputIndex++) { @@ -61,142 +84,95 @@ void setup() emaValue[analogInputIndex] = analogRead(AnalogInputPin[analogInputIndex]); } - for (byte seed = 1; seed < EMASeedCount - 1; seed++) - for (byte analogInputIndex = 0; analogInputIndex < AnalogInputCount; analogInputIndex++) + for (byte analogInputIndex = 0; analogInputIndex < AnalogInputCount; analogInputIndex++) + for (byte seed = 1; seed < EMASeedCount - 1; seed++) getAnalogValue(analogInputIndex); // Read the initial values - currentTime = millis(); for (byte analogInputIndex = 0; analogInputIndex < AnalogInputCount; analogInputIndex++) { analogValue[analogInputIndex] = getAnalogValue(analogInputIndex); - lastChange[analogInputIndex] = currentTime; + lastChange[analogInputIndex] = millis(); } } void loop() { - if (Serial.available()) - processMessage(Serial.read()); - - // Not that due to ADC checking and Serial communication, currentTime will not be - // accurate throughout the loop. But since we don't need exact timing for the interval this - // is acceptable and saves a few calls to millis. - currentTime = millis(); + char readBuffer[32]; + size_t readBufferSize = Serial.available() > 0 ? Serial.readBytes(readBuffer, 32U) : 0; + + min_poll(&minContext, (uint8_t*)readBuffer, (uint8_t)readBufferSize); + // Check analog inputs byte newAnalogValue; for (byte analogInputIndex = 0; analogInputIndex < AnalogInputCount; analogInputIndex++) { newAnalogValue = getAnalogValue(analogInputIndex); - if (newAnalogValue != analogValue[analogInputIndex] && (currentTime - lastChange[analogInputIndex] >= MinimumInterval)) + if (newAnalogValue != analogValue[analogInputIndex] && (millis() - lastChange[analogInputIndex] >= MinimumInterval)) { if (active) // Send out new value outputAnalogValue(analogInputIndex, newAnalogValue); analogValue[analogInputIndex] = newAnalogValue; - lastChange[analogInputIndex] = currentTime; + lastChange[analogInputIndex] = millis(); } } - - if (outputMode == Plotter && (currentTime - lastPlot) >= 50) - { - outputPlotter(); - lastPlot = currentTime; - } } -void processMessage(byte message) +void min_application_handler(uint8_t min_id, uint8_t *min_payload, uint8_t len_payload, uint8_t port) { - switch (message) + switch (min_id) { - case 'H': // Handshake - processHandshakeMessage(); + case FrameIDHandshake: + processHandshakeMessage(min_payload, len_payload); break; - - case 'Q': // Quit + + case FrameIDAnalogOutput: + //processAnalogOutputMessage(); + break; + + case FrameIDDigitalOutput: + //processDigitalOutputMessage(); + break; + + case FrameIDQuit: processQuitMessage(); break; default: - outputError("Unknown message: " + (char)message); - break; + outputError("Unknown frame ID: " + String(min_id)); + break; } } - -void processHandshakeMessage() +void processHandshakeMessage(uint8_t *min_payload, uint8_t len_payload) { - byte buffer[3]; - if (Serial.readBytes(buffer, 3) < 3) + if (len_payload < 2) { outputError("Invalid handshake length"); return; } - if (buffer[0] != 'M' || buffer[1] != 'K') + if (min_payload[0] != 'M' || min_payload[1] != 'K') { - outputError("Invalid handshake: " + String((char)buffer[0]) + String((char)buffer[1]) + String((char)buffer[2])); + outputError("Invalid handshake: " + String((char)min_payload[0]) + String((char)min_payload[1])); return; } - switch (buffer[2]) - { - case 'B': - outputMode = Binary; - break; - - case 'P': - outputMode = PlainText; - break; - - case 'G': - outputMode = Plotter; - break; - - default: - outputMode = PlainText; - outputError("Unknown output mode: " + String((char)buffer[2])); - return; - } - - - switch (outputMode) - { - case Binary: - Serial.write('H'); - Serial.write(AnalogInputCount); - Serial.write((byte)0); - Serial.write((byte)0); - Serial.write((byte)0); - break; - - case PlainText: - Serial.print("Hello! I have "); - Serial.print(AnalogInputCount); - Serial.println(" analog inputs and no support yet for everything else."); - break; - } - - active = true; + byte payload[4] { AnalogInputCount, 0, 0, 0 }; + if (min_queue_frame(&minContext, FrameIDHandshakeResponse, (uint8_t *)payload, 4)) + active = true; } void processQuitMessage() -{ - switch (outputMode) - { - case Binary: - case PlainText: - Serial.write('Q'); - break; - } - +{ active = false; } @@ -217,55 +193,12 @@ byte getAnalogValue(byte analogInputIndex) void outputAnalogValue(byte analogInputIndex, byte newValue) { - switch (outputMode) - { - case Binary: - Serial.write('V'); - Serial.write(analogInputIndex); - Serial.write(newValue); - break; - - case PlainText: - Serial.print("Analog value #"); - Serial.print(analogInputIndex); - Serial.print(" = "); - Serial.println(newValue); - break; - } -} - - -void outputPlotter() -{ - for (byte i = 0; i < AnalogInputCount; i++) - { - if (i > 0) - Serial.print('\t'); - - Serial.print(analogReadValue[i]); - Serial.print('\t'); - Serial.print(emaValue[i]); - Serial.print('\t'); - Serial.print(analogValue[i]); - } - - Serial.println(); + byte payload[2] = { analogInputIndex, newValue }; + min_send_frame(&minContext, FrameIDAnalogInput, (uint8_t *)payload, 2); } void outputError(String message) { - switch (outputMode) - { - case Binary: - Serial.write('E'); - Serial.write((byte)message.length()); - Serial.print(message); - break; - - case PlainText: - Serial.print("Error: "); - Serial.println(message); - break; - } + min_send_frame(&minContext, FrameIDError, (uint8_t *)message.c_str(), message.length()); } diff --git a/Arduino/MassiveKnob/min.c b/Arduino/MassiveKnob/min.c new file mode 100644 index 0000000..98f11f2 --- /dev/null +++ b/Arduino/MassiveKnob/min.c @@ -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); + } +} diff --git a/Arduino/MassiveKnob/min.h b/Arduino/MassiveKnob/min.h new file mode 100644 index 0000000..9c48815 --- /dev/null +++ b/Arduino/MassiveKnob/min.h @@ -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 +#include + +#ifdef ASSERTION_CHECKING +#include +#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 diff --git a/Windows/MassiveKnob.Plugin.CoreAudio/GetMuted/DeviceGetMutedAction.cs b/Windows/MassiveKnob.Plugin.CoreAudio/GetMuted/DeviceGetMutedAction.cs index dde57d3..dde700e 100644 --- a/Windows/MassiveKnob.Plugin.CoreAudio/GetMuted/DeviceGetMutedAction.cs +++ b/Windows/MassiveKnob.Plugin.CoreAudio/GetMuted/DeviceGetMutedAction.cs @@ -2,6 +2,7 @@ using System.Windows.Controls; using AudioSwitcher.AudioApi; using MassiveKnob.Plugin.CoreAudio.OSD; +using Microsoft.Extensions.Logging; namespace MassiveKnob.Plugin.CoreAudio.GetMuted { @@ -13,7 +14,7 @@ namespace MassiveKnob.Plugin.CoreAudio.GetMuted public string Description { get; } = Strings.GetMutedDescription; - public IMassiveKnobActionInstance Create() + public IMassiveKnobActionInstance Create(ILogger logger) { return new Instance(); } diff --git a/Windows/MassiveKnob.Plugin.CoreAudio/GetVolume/DeviceGetVolumeAction.cs b/Windows/MassiveKnob.Plugin.CoreAudio/GetVolume/DeviceGetVolumeAction.cs index a8c2cb5..c929900 100644 --- a/Windows/MassiveKnob.Plugin.CoreAudio/GetVolume/DeviceGetVolumeAction.cs +++ b/Windows/MassiveKnob.Plugin.CoreAudio/GetVolume/DeviceGetVolumeAction.cs @@ -2,6 +2,7 @@ using System.Windows.Controls; using AudioSwitcher.AudioApi; using MassiveKnob.Plugin.CoreAudio.OSD; +using Microsoft.Extensions.Logging; namespace MassiveKnob.Plugin.CoreAudio.GetVolume { @@ -13,7 +14,7 @@ namespace MassiveKnob.Plugin.CoreAudio.GetVolume public string Description { get; } = Strings.GetVolumeDescription; - public IMassiveKnobActionInstance Create() + public IMassiveKnobActionInstance Create(ILogger logger) { return new Instance(); } diff --git a/Windows/MassiveKnob.Plugin.CoreAudio/MassiveKnob.Plugin.CoreAudio.csproj b/Windows/MassiveKnob.Plugin.CoreAudio/MassiveKnob.Plugin.CoreAudio.csproj index 879f89a..ba6f0d3 100644 --- a/Windows/MassiveKnob.Plugin.CoreAudio/MassiveKnob.Plugin.CoreAudio.csproj +++ b/Windows/MassiveKnob.Plugin.CoreAudio/MassiveKnob.Plugin.CoreAudio.csproj @@ -99,6 +99,9 @@ 4.0.0-alpha5 + + 5.0.0 + 5.0.0 diff --git a/Windows/MassiveKnob.Plugin.CoreAudio/SetMuted/DeviceSetMutedAction.cs b/Windows/MassiveKnob.Plugin.CoreAudio/SetMuted/DeviceSetMutedAction.cs index b1e587a..1736d7f 100644 --- a/Windows/MassiveKnob.Plugin.CoreAudio/SetMuted/DeviceSetMutedAction.cs +++ b/Windows/MassiveKnob.Plugin.CoreAudio/SetMuted/DeviceSetMutedAction.cs @@ -3,6 +3,7 @@ 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 { @@ -14,7 +15,7 @@ namespace MassiveKnob.Plugin.CoreAudio.SetMuted public string Description { get; } = Strings.SetMutedDescription; - public IMassiveKnobActionInstance Create() + public IMassiveKnobActionInstance Create(ILogger logger) { return new Instance(); } diff --git a/Windows/MassiveKnob.Plugin.CoreAudio/SetVolume/DeviceSetVolumeAction.cs b/Windows/MassiveKnob.Plugin.CoreAudio/SetVolume/DeviceSetVolumeAction.cs index 4c8006e..f03e1b8 100644 --- a/Windows/MassiveKnob.Plugin.CoreAudio/SetVolume/DeviceSetVolumeAction.cs +++ b/Windows/MassiveKnob.Plugin.CoreAudio/SetVolume/DeviceSetVolumeAction.cs @@ -3,6 +3,7 @@ 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 { @@ -14,7 +15,7 @@ namespace MassiveKnob.Plugin.CoreAudio.SetVolume public string Description { get; } = Strings.SetVolumeDescription; - public IMassiveKnobActionInstance Create() + public IMassiveKnobActionInstance Create(ILogger logger) { return new Instance(); } @@ -26,7 +27,7 @@ namespace MassiveKnob.Plugin.CoreAudio.SetVolume private DeviceSetVolumeActionSettings settings; private IDevice playbackDevice; - + public void Initialize(IMassiveKnobActionContext context) { actionContext = context; diff --git a/Windows/MassiveKnob.Plugin.EmulatorDevice/Devices/EmulatorDevice.cs b/Windows/MassiveKnob.Plugin.EmulatorDevice/Devices/EmulatorDevice.cs index 542ad36..05c089c 100644 --- a/Windows/MassiveKnob.Plugin.EmulatorDevice/Devices/EmulatorDevice.cs +++ b/Windows/MassiveKnob.Plugin.EmulatorDevice/Devices/EmulatorDevice.cs @@ -1,7 +1,7 @@ using System; -using System.Threading; using System.Windows.Controls; using MassiveKnob.Plugin.EmulatorDevice.Settings; +using Microsoft.Extensions.Logging; namespace MassiveKnob.Plugin.EmulatorDevice.Devices { @@ -11,7 +11,7 @@ namespace MassiveKnob.Plugin.EmulatorDevice.Devices public string Name { get; } = "Mock device"; public string Description { get; } = "Emulates the actual device but does not communicate with anything."; - public IMassiveKnobDeviceInstance Create() + public IMassiveKnobDeviceInstance Create(ILogger logger) { return new Instance(); } diff --git a/Windows/MassiveKnob.Plugin.EmulatorDevice/MassiveKnob.Plugin.EmulatorDevice.csproj b/Windows/MassiveKnob.Plugin.EmulatorDevice/MassiveKnob.Plugin.EmulatorDevice.csproj index 454d0d2..c2e488d 100644 --- a/Windows/MassiveKnob.Plugin.EmulatorDevice/MassiveKnob.Plugin.EmulatorDevice.csproj +++ b/Windows/MassiveKnob.Plugin.EmulatorDevice/MassiveKnob.Plugin.EmulatorDevice.csproj @@ -64,7 +64,11 @@ MassiveKnob.Plugin - + + + 5.0.0 + + MSBuild:Compile diff --git a/Windows/MassiveKnob.Plugin.SerialDevice/Devices/SerialDevice.cs b/Windows/MassiveKnob.Plugin.SerialDevice/Devices/SerialDevice.cs index a3c1244..40e62a6 100644 --- a/Windows/MassiveKnob.Plugin.SerialDevice/Devices/SerialDevice.cs +++ b/Windows/MassiveKnob.Plugin.SerialDevice/Devices/SerialDevice.cs @@ -2,6 +2,7 @@ using System.Windows.Controls; using MassiveKnob.Plugin.SerialDevice.Settings; using MassiveKnob.Plugin.SerialDevice.Worker; +using Microsoft.Extensions.Logging; namespace MassiveKnob.Plugin.SerialDevice.Devices { @@ -11,24 +12,32 @@ namespace MassiveKnob.Plugin.SerialDevice.Devices public string Name { get; } = "Serial device"; public string Description { get; } = "A Serial (USB) device which implements the Massive Knob Protocol."; - public IMassiveKnobDeviceInstance Create() + public IMassiveKnobDeviceInstance Create(ILogger logger) { - return new Instance(); + 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(); - worker = new SerialWorker(context); + worker = new SerialWorker(context, logger); ApplySettings(); } @@ -41,7 +50,7 @@ namespace MassiveKnob.Plugin.SerialDevice.Devices private void ApplySettings() { - worker.Connect(settings.PortName, settings.BaudRate); + worker.Connect(settings.PortName, settings.BaudRate, settings.DtrEnable); } diff --git a/Windows/MassiveKnob.Plugin.SerialDevice/MassiveKnob.Plugin.SerialDevice.csproj b/Windows/MassiveKnob.Plugin.SerialDevice/MassiveKnob.Plugin.SerialDevice.csproj index f85d09a..ff6babc 100644 --- a/Windows/MassiveKnob.Plugin.SerialDevice/MassiveKnob.Plugin.SerialDevice.csproj +++ b/Windows/MassiveKnob.Plugin.SerialDevice/MassiveKnob.Plugin.SerialDevice.csproj @@ -60,6 +60,14 @@ {A1298BE4-1D23-416C-8C56-FC9264487A95} MassiveKnob.Plugin + + {db8819eb-d2b7-4aae-a699-bd200f2c113a} + MIN.SerialPort + + + {fc1c9cb5-8b71-4039-9636-90e578a71061} + MIN + @@ -67,5 +75,13 @@ MSBuild:Compile + + + 1.2.0 + + + 5.0.0 + + \ No newline at end of file diff --git a/Windows/MassiveKnob.Plugin.SerialDevice/Settings/SerialDeviceSettings.cs b/Windows/MassiveKnob.Plugin.SerialDevice/Settings/SerialDeviceSettings.cs index 3abc590..5c422bb 100644 --- a/Windows/MassiveKnob.Plugin.SerialDevice/Settings/SerialDeviceSettings.cs +++ b/Windows/MassiveKnob.Plugin.SerialDevice/Settings/SerialDeviceSettings.cs @@ -4,5 +4,6 @@ { public string PortName { get; set; } = null; public int BaudRate { get; set; } = 115200; + public bool DtrEnable { get; set; } = false; } } diff --git a/Windows/MassiveKnob.Plugin.SerialDevice/Settings/SerialDeviceSettingsView.xaml b/Windows/MassiveKnob.Plugin.SerialDevice/Settings/SerialDeviceSettingsView.xaml index 12150f1..eb48155 100644 --- a/Windows/MassiveKnob.Plugin.SerialDevice/Settings/SerialDeviceSettingsView.xaml +++ b/Windows/MassiveKnob.Plugin.SerialDevice/Settings/SerialDeviceSettingsView.xaml @@ -16,6 +16,7 @@ + Serial port @@ -23,5 +24,10 @@ Baud rate + + + + Enable DTR (may be required on some Arduino's like Leonardo / Pro Micro) + diff --git a/Windows/MassiveKnob.Plugin.SerialDevice/Settings/SerialDeviceSettingsViewModel.cs b/Windows/MassiveKnob.Plugin.SerialDevice/Settings/SerialDeviceSettingsViewModel.cs index 56485a1..d56ed9d 100644 --- a/Windows/MassiveKnob.Plugin.SerialDevice/Settings/SerialDeviceSettingsViewModel.cs +++ b/Windows/MassiveKnob.Plugin.SerialDevice/Settings/SerialDeviceSettingsViewModel.cs @@ -50,6 +50,20 @@ namespace MassiveKnob.Plugin.SerialDevice.Settings OnPropertyChanged(); } } + + + public bool DtrEnable + { + get => settings.DtrEnable; + set + { + if (value == settings.DtrEnable) + return; + + settings.DtrEnable = value; + OnPropertyChanged(); + } + } // ReSharper restore UnusedMember.Global diff --git a/Windows/MassiveKnob.Plugin.SerialDevice/Worker/SerialWorker.cs b/Windows/MassiveKnob.Plugin.SerialDevice/Worker/SerialWorker.cs index 052fb84..02b11ea 100644 --- a/Windows/MassiveKnob.Plugin.SerialDevice/Worker/SerialWorker.cs +++ b/Windows/MassiveKnob.Plugin.SerialDevice/Worker/SerialWorker.cs @@ -1,24 +1,28 @@ using System; -using System.Diagnostics; -using System.IO.Ports; using System.Text; -using System.Threading; +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 workerLock = new object(); + private readonly object minProtocolLock = new object(); + private IMINProtocol minProtocol; private string lastPortName; private int lastBaudRate; - private Thread workerThread; - private CancellationTokenSource workerThreadCancellation = new CancellationTokenSource(); + private bool lastDtrEnable; - public SerialWorker(IMassiveKnobDeviceContext context) + public SerialWorker(IMassiveKnobDeviceContext context, ILogger logger) { this.context = context; + this.logger = logger; } @@ -28,15 +32,16 @@ namespace MassiveKnob.Plugin.SerialDevice.Worker } - public void Connect(string portName, int baudRate) + public void Connect(string portName, int baudRate, bool dtrEnable) { - lock (workerLock) + lock (minProtocolLock) { - if (portName == lastPortName && baudRate == lastBaudRate) + if (portName == lastPortName && baudRate == lastBaudRate && dtrEnable == lastDtrEnable) return; lastPortName = portName; lastBaudRate = baudRate; + lastDtrEnable = dtrEnable; Disconnect(); @@ -44,37 +49,99 @@ namespace MassiveKnob.Plugin.SerialDevice.Worker return; - workerThreadCancellation = new CancellationTokenSource(); - workerThread = new Thread(() => RunWorker(workerThreadCancellation.Token, portName, baudRate)) - { - Name = "MassiveKnobSerialDevice Worker" - }; - workerThread.Start(); + minProtocol?.Dispose(); + minProtocol = new MINProtocol(new MINSerialTransport(portName, baudRate, dtrEnable: dtrEnable), logger); + minProtocol.OnConnected += MinProtocolOnOnConnected; + minProtocol.OnFrame += MinProtocolOnOnFrame; + minProtocol.Start(); + } + } + + + private enum MassiveKnobFrameID + { + Handshake = 42, + HandshakeResponse = 43, + AnalogInput = 1, + DigitalInput = 2, + AnalogOutput = 3, + DigitalOutput = 4, + Quit = 62, + Error = 63 + } + + + private void MinProtocolOnOnConnected(object sender, EventArgs e) + { + Task.Run(async () => + { + await minProtocol.Reset(); + await minProtocol.QueueFrame((byte)MassiveKnobFrameID.Handshake, new[] { (byte)'M', (byte)'K' }); + }); + } + + + private void MinProtocolOnOnFrame(object sender, MINFrameEventArgs e) + { + // 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)); + 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 (workerLock) + lock (minProtocolLock) { - workerThreadCancellation?.Cancel(); - - workerThreadCancellation = null; - workerThread = null; + minProtocol?.Dispose(); } } - private void RunWorker(CancellationToken cancellationToken, string portName, int baudRate) - { - context.Connecting(); - while (!cancellationToken.IsCancellationRequested) - { - SerialPort serialPort = null; - DeviceSpecs specs = default; - + /* void SafeCloseSerialPort() { try @@ -93,7 +160,7 @@ namespace MassiveKnob.Plugin.SerialDevice.Worker while (serialPort == null && !cancellationToken.IsCancellationRequested) { - if (!TryConnect(ref serialPort, portName, baudRate, out specs)) + if (!TryConnect(ref serialPort, settings, out specs)) { SafeCloseSerialPort(); Thread.Sleep(500); @@ -153,16 +220,16 @@ namespace MassiveKnob.Plugin.SerialDevice.Worker } - private static bool TryConnect(ref SerialPort serialPort, string portName, int baudRate, out DeviceSpecs specs) + private static bool TryConnect(ref SerialPort serialPort, ConnectionSettings settings, out DeviceSpecs specs) { try { - serialPort = new SerialPort(portName, baudRate) + serialPort = new SerialPort(settings.PortName, settings.BaudRate) { Encoding = Encoding.ASCII, ReadTimeout = 1000, WriteTimeout = 1000, - DtrEnable = true // TODO make setting + DtrEnable = settings.DtrEnable }; serialPort.Open(); @@ -226,5 +293,21 @@ namespace MassiveKnob.Plugin.SerialDevice.Worker 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; + } + } + */ } } diff --git a/Windows/MassiveKnob.Plugin/IMassiveKnobAction.cs b/Windows/MassiveKnob.Plugin/IMassiveKnobAction.cs index bba4713..cd245a6 100644 --- a/Windows/MassiveKnob.Plugin/IMassiveKnobAction.cs +++ b/Windows/MassiveKnob.Plugin/IMassiveKnobAction.cs @@ -1,4 +1,5 @@ using System; +using Microsoft.Extensions.Logging; namespace MassiveKnob.Plugin { @@ -58,6 +59,6 @@ namespace MassiveKnob.Plugin /// /// Called when an action is bound to a knob or button to create an instance of the action. /// - IMassiveKnobActionInstance Create(); + IMassiveKnobActionInstance Create(ILogger logger); } } diff --git a/Windows/MassiveKnob.Plugin/IMassiveKnobDevice.cs b/Windows/MassiveKnob.Plugin/IMassiveKnobDevice.cs index 3a9f6c3..9a8d19d 100644 --- a/Windows/MassiveKnob.Plugin/IMassiveKnobDevice.cs +++ b/Windows/MassiveKnob.Plugin/IMassiveKnobDevice.cs @@ -1,4 +1,5 @@ using System; +using Microsoft.Extensions.Logging; namespace MassiveKnob.Plugin { @@ -25,6 +26,6 @@ namespace MassiveKnob.Plugin /// /// Called when the device is selected. /// - IMassiveKnobDeviceInstance Create(); + IMassiveKnobDeviceInstance Create(ILogger logger); } } diff --git a/Windows/MassiveKnob.Plugin/MassiveKnob.Plugin.csproj b/Windows/MassiveKnob.Plugin/MassiveKnob.Plugin.csproj index 5659c17..79e90f8 100644 --- a/Windows/MassiveKnob.Plugin/MassiveKnob.Plugin.csproj +++ b/Windows/MassiveKnob.Plugin/MassiveKnob.Plugin.csproj @@ -61,6 +61,9 @@ + + 5.0.0 + 4.5.4 diff --git a/Windows/MassiveKnob.sln b/Windows/MassiveKnob.sln index f992761..6a480a2 100644 --- a/Windows/MassiveKnob.sln +++ b/Windows/MassiveKnob.sln @@ -13,6 +13,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MassiveKnob.Plugin.Emulator EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MassiveKnob.Plugin.SerialDevice", "MassiveKnob.Plugin.SerialDevice\MassiveKnob.Plugin.SerialDevice.csproj", "{FC0D22D8-5F1B-4D85-8269-FA4837CDE3A2}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MIN", "min.NET\MIN\MIN.csproj", "{FC1C9CB5-8B71-4039-9636-90E578A71061}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MIN.SerialPort", "min.NET\MIN.SerialPort\MIN.SerialPort.csproj", "{DB8819EB-D2B7-4AAE-A699-BD200F2C113A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -39,6 +43,14 @@ Global {FC0D22D8-5F1B-4D85-8269-FA4837CDE3A2}.Debug|Any CPU.Build.0 = Debug|Any CPU {FC0D22D8-5F1B-4D85-8269-FA4837CDE3A2}.Release|Any CPU.ActiveCfg = Release|Any CPU {FC0D22D8-5F1B-4D85-8269-FA4837CDE3A2}.Release|Any CPU.Build.0 = Release|Any CPU + {FC1C9CB5-8B71-4039-9636-90E578A71061}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FC1C9CB5-8B71-4039-9636-90E578A71061}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FC1C9CB5-8B71-4039-9636-90E578A71061}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FC1C9CB5-8B71-4039-9636-90E578A71061}.Release|Any CPU.Build.0 = Release|Any CPU + {DB8819EB-D2B7-4AAE-A699-BD200F2C113A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DB8819EB-D2B7-4AAE-A699-BD200F2C113A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DB8819EB-D2B7-4AAE-A699-BD200F2C113A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DB8819EB-D2B7-4AAE-A699-BD200F2C113A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Windows/MassiveKnob.sln.DotSettings b/Windows/MassiveKnob.sln.DotSettings index edc850c..88c1248 100644 --- a/Windows/MassiveKnob.sln.DotSettings +++ b/Windows/MassiveKnob.sln.DotSettings @@ -1,4 +1,6 @@  + EOF OSD + SOF UI OSD \ No newline at end of file diff --git a/Windows/MassiveKnob/MassiveKnob.csproj b/Windows/MassiveKnob/MassiveKnob.csproj index 6d627cd..04c9df8 100644 --- a/Windows/MassiveKnob/MassiveKnob.csproj +++ b/Windows/MassiveKnob/MassiveKnob.csproj @@ -108,12 +108,24 @@ 1.0.8 + + 5.0.0 + 12.0.3 5.1.0 + + 2.10.0 + + + 3.0.1 + + + 4.1.0 + 5.2.1 diff --git a/Windows/MassiveKnob/Model/MassiveKnobOrchestrator.cs b/Windows/MassiveKnob/Model/MassiveKnobOrchestrator.cs index aa7186d..1fa0b82 100644 --- a/Windows/MassiveKnob/Model/MassiveKnobOrchestrator.cs +++ b/Windows/MassiveKnob/Model/MassiveKnobOrchestrator.cs @@ -5,13 +5,17 @@ using System.Reactive.Subjects; using MassiveKnob.Helpers; using MassiveKnob.Plugin; using MassiveKnob.Settings; +using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; +using Serilog.Extensions.Logging; +using ILogger = Serilog.ILogger; namespace MassiveKnob.Model { public class MassiveKnobOrchestrator : IMassiveKnobOrchestrator { private readonly IPluginManager pluginManager; + private readonly ILogger logger; private readonly object settingsLock = new object(); private Settings.Settings settings; @@ -43,9 +47,10 @@ namespace MassiveKnob.Model public IObservable ActiveDeviceSubject => activeDeviceInfoSubject; - public MassiveKnobOrchestrator(IPluginManager pluginManager) + public MassiveKnobOrchestrator(IPluginManager pluginManager, ILogger logger) { this.pluginManager = pluginManager; + this.logger = logger; } @@ -170,7 +175,7 @@ namespace MassiveKnob.Model if (device != null) { - var instance = device.Create(); + var instance = device.Create(new SerilogLoggerProvider(logger.ForContext("Device", device.DeviceId)).CreateLogger(null)); ActiveDevice = new MassiveKnobDeviceInfo(device, instance, null); activeDeviceContext = new DeviceContext(this, device); @@ -456,8 +461,13 @@ namespace MassiveKnob.Model { if (action == null) return null; + + var actionLogger = logger + .ForContext("Action", action.ActionId) + .ForContext("ActionType", action.ActionType) + .ForContext("Index", index); - var instance = action.Create(); + var instance = action.Create(new SerilogLoggerProvider(actionLogger).CreateLogger(null)); var context = new ActionContext(this, action, index); var mapping = new ActionMapping(new MassiveKnobActionInfo(action, instance), context); diff --git a/Windows/MassiveKnob/Program.cs b/Windows/MassiveKnob/Program.cs index dadea3e..4566fee 100644 --- a/Windows/MassiveKnob/Program.cs +++ b/Windows/MassiveKnob/Program.cs @@ -1,9 +1,13 @@ using System; +using System.IO; using System.Text; using System.Windows; using MassiveKnob.Model; using MassiveKnob.View; using MassiveKnob.ViewModel; +using Serilog; +using Serilog.Core; +using Serilog.Events; using SimpleInjector; namespace MassiveKnob @@ -16,6 +20,20 @@ namespace MassiveKnob [STAThread] public static int Main() { + // TODO make configurable + var loggingLevelSwitch = new LoggingLevelSwitch(); + //var loggingLevelSwitch = new LoggingLevelSwitch(LogEventLevel.Verbose); + + var logger = new LoggerConfiguration() + //.MinimumLevel.Verbose() + .MinimumLevel.ControlledBy(loggingLevelSwitch) + .Enrich.FromLogContext() + .WriteTo.File( + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"MassiveKnob", @"Logs", @".log"), + LogEventLevel.Verbose, rollingInterval: RollingInterval.Day) + .CreateLogger(); + + var pluginManager = new PluginManager(); var messages = new StringBuilder(); @@ -30,13 +48,15 @@ namespace MassiveKnob return 1; } - var orchestrator = new MassiveKnobOrchestrator(pluginManager); + var orchestrator = new MassiveKnobOrchestrator(pluginManager, logger); orchestrator.Load(); var container = new Container(); container.Options.EnableAutoVerification = false; + container.RegisterInstance(logger); + container.RegisterInstance(loggingLevelSwitch); container.RegisterInstance(pluginManager); container.RegisterInstance(orchestrator); diff --git a/Windows/min.NET b/Windows/min.NET new file mode 160000 index 0000000..223cfaf --- /dev/null +++ b/Windows/min.NET @@ -0,0 +1 @@ +Subproject commit 223cfafaf40e0e26e7660860ddfd755cb671f81b