Line data Source code
1 : #pragma once 2 : 3 : #include <AH/Debug/Debug.hpp> 4 : #include <AH/Math/MinMaxFix.hpp> 5 : #include <MIDI_Inputs/MIDIInputElement.hpp> 6 : #include <string.h> // memcpy 7 : 8 : #ifndef ARDUINO 9 : #include <cassert> 10 : #endif 11 : 12 : BEGIN_CS_NAMESPACE 13 : 14 : using AH::max; 15 : using AH::min; 16 : 17 : namespace MCU { 18 : 19 : /// Counts the number of instances of the LCD class. 20 : /// If there are multiple LCD objects, we have to update all of them before 21 : /// breaking out of the MIDI input handling loop. 22 : class LCDCounter { 23 : public: 24 13 : LCDCounter() { instances++; } 25 13 : ~LCDCounter() { instances--; } 26 : 27 16 : static uint8_t getInstances() { return instances; } 28 : 29 : private: 30 : static uint8_t instances; 31 : }; 32 : /** 33 : * @brief A class that represents the Mackie Control Universal LCD display and 34 : * saves the text it receives. 35 : * 36 : * The format of the MIDI message is as follows (hex): 37 : * | SysEx Start | Data 1 | Data 2 | Data 3 | Data 4 | Data 5 | Data 6 | Data 7 |  ...  | SysEx End | 38 : * |:----:|:----:|:----:|:----:|:----:|:----:|:----:|:----:|:---:|:----:| 39 : * | `F0` | `mm` | `mm` | `mm` | `nn` | `12` | `oo` | `yy` | ... | `F7` | 40 : * 41 : * - `mm` is manufacturer ID (`00 00 66` for Mackie) 42 : * - `nn` is model number (`10` for Logic Control, `11` for Logic Control XT) 43 : * - `oo` is offset relative to the first character of the LCD [0x00, 0x6F] 44 : * - `yy`... is the ASCII text data 45 : * 46 : * The manufacturer ID and model number are ignored by this class. 47 : * 48 : * @ingroup MIDIInputElements 49 : */ 50 : template <uint8_t BufferSize = 112> 51 : class LCD : public MIDIInputElementSysEx, private LCDCounter { 52 : public: 53 : /// @param offset 54 : /// The text sent over MIDI is 112 characters long, by changing the 55 : /// offset within this text and the length of the text, we can 56 : /// listen to just a section of the text. E.g. `offset = 56` and 57 : /// `BufferSize = 56` will listen to just the second line of the 58 : /// LCD display. The default is `offset = 0` and `BufferSize = 112`, 59 : /// i.e. listening to the entire display. 60 : /// @param cable 61 : /// The MIDI USB cable number to listen for. 62 13 : LCD(uint8_t offset = 0, Cable cable = Cable_1) 63 13 : : offset(offset), cable(cable) { 64 : // Null-terminate the buffer 65 13 : buffer[BufferSize] = '\0'; 66 : // Fill the buffer with spaces 67 185 : for (uint8_t i = 0; i < BufferSize; i++) buffer[i] = ' '; 68 13 : } 69 : 70 : protected: 71 16 : bool updateWith(SysExMessage midimsg) override { 72 : // If this message is meant for a different cable than ours, return: 73 16 : if (midimsg.getCable() != this->cable) 74 0 : return false; 75 : 76 : // We can't handle chunked SysEx data (yet), and it wouldn't make a ton 77 : // of sense, since the default SysEx buffer size is the same size as the 78 : // SysEx message we expect, so it shouldn't arrive in chunks. 79 16 : if (!midimsg.isCompleteMessage()) 80 0 : return false; 81 : 82 : // Format: 83 : // F0 mm mm mm nn 12 oo yy... F7 84 : // mm = manufacturer ID (00 00 66 for Mackie) 85 : // nn = model number (10 for Logic Control, 11 for Logic Control XT) 86 : // oo = offset [0x00, 0x6F] 87 : // yy... = ASCII data 88 16 : if (midimsg.data[5] != 0x12) 89 0 : return false; 90 : 91 16 : const uint8_t midiOffset = midimsg.data[6]; 92 16 : const uint8_t midiLength = midimsg.length - 8; 93 16 : const uint8_t *text = midimsg.data + 7; 94 16 : const uint8_t midiBufferEnd = midiOffset + midiLength; 95 : 96 16 : const uint8_t bufferEnd = this->offset + BufferSize; 97 : 98 : // If there's no overlap between incoming range and the range that we're 99 : // listening for, return: 100 16 : if (midiOffset >= bufferEnd || this->offset >= midiBufferEnd) 101 : // If there are other instances, maybe it'll match one of those, 102 : // otherwise, stop handling this message: 103 2 : return getInstances() == 1; 104 : 105 : // Find the ranges that overlap between the text data in the message 106 : // (src) and the range of characters we're listening for (dst): 107 14 : uint8_t srcStart = max(0, this->offset - midiOffset); 108 14 : uint8_t dstStart = max(0, midiOffset - this->offset); 109 14 : uint8_t length = midiBufferEnd - midiOffset - 110 42 : max(0, this->offset - midiOffset) - 111 28 : max(0, midiBufferEnd - bufferEnd); 112 : 113 : // Copy the interesting part to our buffer: 114 : #ifdef ARDUINO 115 : memcpy(&buffer[dstStart], &text[srcStart], length); 116 : #else // Tests 117 72 : for (uint8_t i = 0; i < length; ++i) { 118 58 : buffer[dstStart + i] = text[srcStart + i]; 119 58 : assert(dstStart + i < BufferSize); 120 58 : assert(srcStart + i < midiLength); 121 : } 122 : #endif 123 : 124 14 : markDirty(); 125 : 126 : // If this is the only instance, the others don't have to be updated 127 : // anymore, so we return true to break the loop: 128 14 : return getInstances() == 1; 129 : } 130 : 131 : public: 132 1 : void begin() override { markDirty(); } 133 : 134 : /// @name Data access 135 : /// @{ 136 : 137 : /// Get a pointer to the null-terminated display text. 138 45 : const char *getText() const { return buffer.data; } 139 : 140 : /// @} 141 : 142 : /// @name Detecting changes 143 : /// @{ 144 : 145 : /// 146 : /// Check if the text was updated since the last time the dirty flag was 147 : /// cleared. 148 27 : bool getDirty() const { return dirty > 0; } 149 : /// Clear the dirty flag. 150 20 : void clearDirty() { 151 20 : if (dirty > 0) 152 7 : --dirty; 153 20 : } 154 : /// Set the dirty counter to the number of subscribers (or one). 155 15 : void markDirty() { dirty = num_subscribers > 0 ? num_subscribers : 1; } 156 2 : void addSubscriber() { ++num_subscribers; } 157 2 : void removeSubscriber() { --num_subscribers; } 158 : 159 : /// @} 160 : 161 : private: 162 : Array<char, BufferSize + 1> buffer; 163 : uint8_t offset; 164 : Cable cable; 165 : uint8_t dirty = 0; 166 : uint8_t num_subscribers = 0; 167 : }; 168 : 169 : } // namespace MCU 170 : 171 : END_CS_NAMESPACE