LCOV - code coverage report
Current view: top level - src/MIDI_Inputs/MCU - LCD.hpp (source / functions) Hit Total Coverage
Test: b8a30b4b7040ae1abf162fd0a258beaa2de43626 Lines: 40 43 93.0 %
Date: 2024-12-21 21:28:55 Functions: 24 26 92.3 %
Legend: Lines: hit not hit

          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 | &emsp;...&emsp; | 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

Generated by: LCOV version 1.15