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
|