Changed serial protocol to MIN

Implemented logging
This commit is contained in:
Mark van Renswoude 2021-02-28 11:55:23 +01:00
parent a1eb61a6a9
commit 9869f46a49
26 changed files with 1173 additions and 186 deletions

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "Windows/min.NET"]
path = Windows/min.NET
url = https://github.com/MvRens/min.NET

View File

@ -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());
}

641
Arduino/MassiveKnob/min.c Normal file
View 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
View 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

View File

@ -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();
}

View File

@ -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();
}

View File

@ -99,6 +99,9 @@
<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>

View File

@ -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();
}

View File

@ -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;

View File

@ -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();
}

View File

@ -64,7 +64,11 @@
<Name>MassiveKnob.Plugin</Name>
</ProjectReference>
</ItemGroup>
<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>

View File

@ -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<SerialDeviceSettings>();
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);
}

View File

@ -60,6 +60,14 @@
<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">
@ -67,5 +75,13 @@
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Crc32.NET">
<Version>1.2.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions">
<Version>5.0.0</Version>
</PackageReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View File

@ -4,5 +4,6 @@
{
public string PortName { get; set; } = null;
public int BaudRate { get; set; } = 115200;
public bool DtrEnable { get; set; } = false;
}
}

View File

@ -16,6 +16,7 @@
<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>
@ -23,5 +24,10 @@
<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>

View File

@ -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

View File

@ -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;
}
}
*/
}
}

View File

@ -1,4 +1,5 @@
using System;
using Microsoft.Extensions.Logging;
namespace MassiveKnob.Plugin
{
@ -58,6 +59,6 @@ namespace MassiveKnob.Plugin
/// <summary>
/// Called when an action is bound to a knob or button to create an instance of the action.
/// </summary>
IMassiveKnobActionInstance Create();
IMassiveKnobActionInstance Create(ILogger logger);
}
}

View File

@ -1,4 +1,5 @@
using System;
using Microsoft.Extensions.Logging;
namespace MassiveKnob.Plugin
{
@ -25,6 +26,6 @@ namespace MassiveKnob.Plugin
/// <summary>
/// Called when the device is selected.
/// </summary>
IMassiveKnobDeviceInstance Create();
IMassiveKnobDeviceInstance Create(ILogger logger);
}
}

View File

@ -61,6 +61,9 @@
<Compile Include="ReSharper\JetBrains.Annotations.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions">
<Version>5.0.0</Version>
</PackageReference>
<PackageReference Include="System.Threading.Tasks.Extensions">
<Version>4.5.4</Version>
</PackageReference>

View File

@ -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

View File

@ -1,4 +1,6 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=EOF/@EntryIndexedValue">EOF</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=OSD/@EntryIndexedValue">OSD</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=SOF/@EntryIndexedValue">SOF</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=UI/@EntryIndexedValue">UI</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/XamlNaming/Abbreviations/=OSD/@EntryIndexedValue">OSD</s:String></wpf:ResourceDictionary>

View File

@ -108,12 +108,24 @@
<PackageReference Include="Hardcodet.NotifyIcon.Wpf">
<Version>1.0.8</Version>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions">
<Version>5.0.0</Version>
</PackageReference>
<PackageReference Include="Newtonsoft.Json">
<Version>12.0.3</Version>
</PackageReference>
<PackageReference Include="Nito.AsyncEx">
<Version>5.1.0</Version>
</PackageReference>
<PackageReference Include="Serilog">
<Version>2.10.0</Version>
</PackageReference>
<PackageReference Include="Serilog.Extensions.Logging">
<Version>3.0.1</Version>
</PackageReference>
<PackageReference Include="Serilog.Sinks.File">
<Version>4.1.0</Version>
</PackageReference>
<PackageReference Include="SimpleInjector">
<Version>5.2.1</Version>
</PackageReference>

View File

@ -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<MassiveKnobDeviceInfo> 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);

View File

@ -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<IPluginManager>(pluginManager);
container.RegisterInstance<IMassiveKnobOrchestrator>(orchestrator);

1
Windows/min.NET Submodule

@ -0,0 +1 @@
Subproject commit 223cfafaf40e0e26e7660860ddfd755cb671f81b