//
// Programmbeispiel mit Speicherobjekt
//
#include "type_traits.h"
#include <Streaming.h>
Print &cout {Serial};

constexpr uint8_t SELECT_PINS[] {2, 3, 4};
constexpr uint8_t ANALOG_PINS[] {A0, A1, A2};
constexpr uint8_t NUM_CHANNELS_TOTAL {22};
constexpr uint8_t NUM_CANNELS_PER_DEVICE {8};

constexpr uint8_t NUM_DEVICES {sizeof(ANALOG_PINS) / sizeof(ANALOG_PINS[0])};

//
// Klasse zum Speichern von Daten, die über einen oder mehrere Multiplexer ausgelesen werden
//
template <typename T, size_t SIZE> class SegmentedArray {
  static_assert(is_signed<T>(), "signed integer expected!");

public:
  SegmentedArray(const size_t row, const size_t colsPerRow) : row {row}, colsPerRow {colsPerRow} {}

  // Indexoperator für veränderliche Vektoren. Eindimensionaler Zugriff
  T &operator[](size_t index) {
    return (index < SIZE) ? pStore[index] : error = -1;   //  -1 wenn falscher Kanal (Speicherüberlauf abfangen)
  }

  // Indexoperator für NICHT veränderliche Vektoren. Eindimensionaler Zugriff
  const T &operator[](size_t index) const { return (index < SIZE) ? pStore[index] : error = -1; }

  // Zweidimensionaler Zugriff (veränderlich)
  T &operator()(const size_t row, const size_t column) {
    size_t index = colsPerRow * row + column;
    return (index < SIZE) ? pStore[index] : error = -1;   // -1 wenn falscher Kanal (Speicherüberlauf abfangen)
  }

  // Zweidimensionaler Zugriff (const nicht veränderlich)
  const T &operator()(const size_t row, const size_t column) const {
    size_t index = colsPerRow * row + column;
    return (index < SIZE) ? pStore[index] : error = -1;   // -1 wenn falscher Kanal (Speicherüberlauf abfangen)
  }

  void clearAll() { memset(pStore, 0, bytes); }

public:
  const size_t size {SIZE};                // Größe des Arrays
  const size_t bytes {sizeof(T) * SIZE};   // Größe in Bytes

private:
  const size_t row;
  const size_t colsPerRow;
  T error {-1};
  T pStore[SIZE] {};
};

SegmentedArray<int, NUM_CHANNELS_TOTAL> dataStore(NUM_DEVICES, NUM_CANNELS_PER_DEVICE);   // Speicherinstanz erzeugen

//////////////////////////////////////////////////////////////////////////////
/// @brief Setzen der Steuerpins für die Kanalauswahl eines Multiplexer ICs
///
/// @param pins[N]   Array mit den Select Pins zum Einstellen des Auslesekanals. N wird automatich ermittelt
/// @param channel   Kanalnummer die eingestellt werden soll
//////////////////////////////////////////////////////////////////////////////
template <size_t N> void setChannel(const uint8_t (&pins)[N], uint8_t channel) {
  for (size_t i = 0; i < N; ++i) { digitalWrite(pins[i], bitRead(channel, i)); }
}

//////////////////////////////////////////////////////////////////////////////
/// @brief Auslesen eines analogen Signals aus einem gegebenen Kanal eines Multiplexer ICs
///        Sind mehrere Ics vorhanden, deren Kanal über die gleichen Steuerpins parallel geschaltet
///        wird, so wird er für jeden IC sukzessive ausgelesen.
///        Der ausgelesene Wert wird in einem in einem eindimensionalen Array gespeichert.
///        Bei mehreren ICs ist die Speicherposition (index) im Array = Ports pro Device * IC-Nummer + Kanalnummer
///        Beispiel Multiplexer mit 8 Ports:   Kanal 5 des ersten ICs = 8 * 0 + 5 = 5
///                                            Kanal 5 des dritten ICs = 8 * 2 + 5 = 21
///
/// @tparam analogPins   Array  analoge Pinnummern (Elementanzahl N wird beim compilieren ermittelt)
/// @param storage       Speicherobjekt für die ausgelesenen Daten
/// @param channel       Kanal der ausgelesen werden soll
//////////////////////////////////////////////////////////////////////////////

template <size_t N, typename T, size_t SIZE>
void readData(const uint8_t (&analogPins)[N], SegmentedArray<T, SIZE> &storage, uint8_t channel) {
  uint8_t numDevice {0};
  for (auto pin : analogPins) {
    storage(numDevice, channel) = analogRead(pin);
    ++numDevice;
  }
}

void setup() {
  Serial.begin(115200);
  for (auto pin : SELECT_PINS) { pinMode(pin, OUTPUT); }   // Selectpins auf OUTPUT konfigurieren
  cout << "Start...\n" << "Arrayelemente: " << dataStore.size << " Bytes: " << dataStore.bytes << endl;
  delay(5000);
}

void loop() {
  // Die CD4051 Ports auslesen und in einem Array speichern
  for (size_t channel = 0; channel < NUM_CANNELS_PER_DEVICE; ++channel) {
    setChannel(SELECT_PINS, channel);
    readData(ANALOG_PINS, dataStore, channel);
  }

  // Gespeicherte Daten auf serieller Konsole ausgeben (eindimensionaler Zugriff)
  uint8_t devNr {0};
  for (size_t i = 0; i < NUM_CHANNELS_TOTAL + 2; ++i) {
    if (!(i % 8)) {
      delay(500);
      ++devNr;
      cout << "\nDevice Nr.: " << devNr << " Data: ";
    }
    cout << _WIDTH(dataStore[i], 4) << " ";
  }
  cout << endl;
  delay(5000);
}

//
// Programmbeispiel ohne Speicherobjekt
//

/*
#include <Streaming.h>
Print &cout {Serial};

constexpr uint8_t SELECT_PINS[] {2, 3, 4};
constexpr uint8_t ANALOG_PINS[] {A0, A1, A2};
constexpr uint8_t NUM_PORTS_TOTAL {22};
constexpr uint8_t NUM_PORTS_DEVICE {8};

constexpr uint8_t NUM_DEVICES {sizeof(ANALOG_PINS) / sizeof(ANALOG_PINS[0])};
// constexpr uint8_t NUM_DEVICES {NUM_PORTS_TOTAL / NUM_PORTS_DEVICE + (NUM_PORTS_TOTAL % NUM_PORTS_DEVICE > 0 ? 1 : 0)};
int dataStore[NUM_PORTS_TOTAL];

//////////////////////////////////////////////////////////////////////////////
/// @brief Setzen der Steuerpins für die Kanalauswahl eines Multiplexer ICs
///
/// @tparam pins[N]   Array mit den Select Pins zum Einstellen des Auslesekanals. N wird automatich ermittelt
/// @param channel    Kanalnummer die eingestellt werden soll
//////////////////////////////////////////////////////////////////////////////
template <size_t N> void setChannel(const uint8_t (&pins)[N], uint8_t channel) {
  for (size_t i = 0; i < N; ++i) { digitalWrite(pins[i], bitRead(channel, i)); }
}

//////////////////////////////////////////////////////////////////////////////
/// @brief Auslesen eines analogen Signals aus einem gegebenen Kanal eines Multiplexer ICs
///        Sind mehrere Ics vorhanden, deren Kanal über die gleichen Steuerpins parallel geschaltet
///        wird, so wird er für jeden IC sukzessive ausgelesen.
///        Der ausgelesene Wert wird in einem eindimensionalen Array gespeichert.
///        Bei mehreren ICs ist die Speicherposition (index) im Array = Ports pro Device * IC-Nummer + Kanalnummer
///        Beispiel Multiplexer mit 8 Ports:   Kanal 5 des ersten ICs = 8 * 0 + 5 = 5
///                                            Kanal 5 des dritten ICs = 8 * 2 + 5 = 21
///
/// @tparam analogPins          Array  analoge Pinnummern (Elementanzahl N wird beim compilieren ermittelt)
/// @tparam storage             Speicherarray (Typ und Anzahl Elemente M werden beim compilieren ermittelt)
/// @param channel              Kanal der ausgelesen werden soll
/// @param numPortsDevice       Anzahl ansteuerbare Kanäle pro IC
//////////////////////////////////////////////////////////////////////////////
template <size_t N, typename T, size_t M>
void readData(const uint8_t (&analogPins)[N], const T (&storage)[M], size_t channel, const uint8_t numPortsDevice) {
  uint8_t numDevice {0};
  for (auto pin : analogPins) {
    uint8_t storeIndex = numPortsDevice * numDevice + channel;
    if (storeIndex < M) { dataStore[storeIndex] = analogRead(pin); }
    ++numDevice;
  }
}

void setup() {
  Serial.begin(115200);
  for (auto pin : SELECT_PINS) { pinMode(pin, OUTPUT); }   // Selectpins auf OUTPUT konfigurieren
}

void loop() {
  // Die CD4051 Ports auslesen und in einem Array speichern
  for (size_t channel = 0; channel < NUM_PORTS_DEVICE; ++channel) {
    setChannel(SELECT_PINS, channel);
    readData(ANALOG_PINS, dataStore, channel, NUM_PORTS_DEVICE);
  }

  // Gespeicherte Daten auf serieller Konsole ausgeben
  uint8_t devNum {0};
  for (size_t i = 0; i < (sizeof(dataStore) / sizeof(dataStore[0])); ++i) {
    if (!(i % 8)) { ++devNum; }
    cout << "Mux Nr.: " << devNum << " Speicher Position: " << _WIDTH(i, 2) << " Wert: " << _WIDTH(dataStore[i], 4)
         << endl;
    delay(500);  // Nur zur Verlangsamung der Ausgabe. Im Echtbetrieb muss das weg!
  }
  delay(5000);
}
*/

CD4051BBreakout
CD4051BBreakout
CD4051BBreakout
Mux Nr. 1
Mux Nr. 2
Mux Nr. 3