LCOV - code coverage report
Current view: top level - src/MIDI_Inputs/MCU - LCD.hpp (source / functions) Coverage Total Hit
Test: 73449d9b107c772cf65493691543348214e5d5eb Lines: 93.0 % 43 40
Test Date: 2026-06-06 17:44:35 Functions: 92.3 % 26 24
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 2.4-beta