[
  {
    "path": ".gitignore",
    "content": ".DS_Store\n*.swp\nsecret.h\n.vscode\n"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"esp8266_artnet_dmx512\"]\n\tpath = esp8266_artnet_dmx512\n\turl = git@github.com:robertoostenveld/esp8266_artnet_dmx512.git\n"
  },
  {
    "path": "PulseSensor_v0/AllSerialHandling.cpp",
    "content": "\n//////////\n/////////  All Serial Handling Code, \n/////////  It's Changeable with the 'serialVisual' variable\n/////////  Set it to 'true' or 'false' when it's declared at start of code.  \n/////////\n\nvoid serialOutput(){   // Decide How To Output Serial. \n if (serialVisual == true){  \n     arduinoSerialMonitorVisual('-', Signal);   // goes to function that makes Serial Monitor Visualizer\n } else{\n      sendDataToSerial('S', Signal);     // goes to sendDataToSerial function\n }        \n}\n\n\n//  Decides How To OutPut BPM and IBI Data\nvoid serialOutputWhenBeatHappens(){    \n if (serialVisual == true){            //  Code to Make the Serial Monitor Visualizer Work\n    Serial.print(\"*** Heart-Beat Happened *** \");  //ASCII Art Madness\n    Serial.print(\"BPM: \");\n    Serial.print(BPM);\n    Serial.print(\"  \");\n } else{\n        sendDataToSerial('B',BPM);   // send heart rate with a 'B' prefix\n        sendDataToSerial('Q',IBI);   // send time between beats with a 'Q' prefix\n }   \n}\n\n\n\n//  Sends Data to Pulse Sensor Processing App, Native Mac App, or Third-party Serial Readers. \nvoid sendDataToSerial(char symbol, int data ){\n    Serial.print(symbol);\n\n    Serial.println(data);                \n  }\n\n\n//  Code to Make the Serial Monitor Visualizer Work\nvoid arduinoSerialMonitorVisual(char symbol, int data ){    \n  const int sensorMin = 0;      // sensor minimum, discovered through experiment\nconst int sensorMax = 1024;    // sensor maximum, discovered through experiment\n\n  int sensorReading = data;\n  // map the sensor range to a range of 12 options:\n  int range = map(sensorReading, sensorMin, sensorMax, 0, 11);\n\n  // do something different depending on the \n  // range value:\n  switch (range) {\n  case 0:     \n    Serial.println(\"\");     /////ASCII Art Madness\n    break;\n  case 1:   \n    Serial.println(\"---\");\n    break;\n  case 2:    \n    Serial.println(\"------\");\n    break;\n  case 3:    \n    Serial.println(\"---------\");\n    break;\n  case 4:   \n    Serial.println(\"------------\");\n    break;\n  case 5:   \n    Serial.println(\"--------------|-\");\n    break;\n  case 6:   \n    Serial.println(\"--------------|---\");\n    break;\n  case 7:   \n    Serial.println(\"--------------|-------\");\n    break;\n  case 8:  \n    Serial.println(\"--------------|----------\");\n    break;\n  case 9:    \n    Serial.println(\"--------------|----------------\");\n    break;\n  case 10:   \n    Serial.println(\"--------------|-------------------\");\n    break;\n  case 11:   \n    Serial.println(\"--------------|-----------------------\");\n    break;\n  \n  } \n}\n\n\n"
  },
  {
    "path": "PulseSensor_v0/Interrupt.cpp",
    "content": "\n\n\nvolatile int rate[10];                    // array to hold last ten IBI values\nvolatile unsigned long sampleCounter = 0;          // used to determine pulse timing\nvolatile unsigned long lastBeatTime = 0;           // used to find IBI\nvolatile int P =512;                      // used to find peak in pulse wave, seeded\nvolatile int T = 512;                     // used to find trough in pulse wave, seeded\nvolatile int thresh = 525;                // used to find instant moment of heart beat, seeded\nvolatile int amp = 100;                   // used to hold amplitude of pulse waveform, seeded\nvolatile boolean firstBeat = true;        // used to seed rate array so we startup with reasonable BPM\nvolatile boolean secondBeat = false;      // used to seed rate array so we startup with reasonable BPM\n\n\nvoid interruptSetup(){     \n  // Initializes Timer2 to throw an interrupt every 2mS.\n  TCCR2A = 0x02;     // DISABLE PWM ON DIGITAL PINS 3 AND 11, AND GO INTO CTC MODE\n  TCCR2B = 0x06;     // DON'T FORCE COMPARE, 256 PRESCALER \n  OCR2A = 0X7C;      // SET THE TOP OF THE COUNT TO 124 FOR 500Hz SAMPLE RATE\n  TIMSK2 = 0x02;     // ENABLE INTERRUPT ON MATCH BETWEEN TIMER2 AND OCR2A\n  sei();             // MAKE SURE GLOBAL INTERRUPTS ARE ENABLED      \n} \n\n\n// THIS IS THE TIMER 2 INTERRUPT SERVICE ROUTINE. \n// Timer 2 makes sure that we take a reading every 2 miliseconds\nISR(TIMER2_COMPA_vect){                         // triggered when Timer2 counts to 124\n  cli();                                      // disable interrupts while we do this\n  Signal = analogRead(pulsePin);              // read the Pulse Sensor \n  sampleCounter += 2;                         // keep track of the time in mS with this variable\n  int N = sampleCounter - lastBeatTime;       // monitor the time since the last beat to avoid noise\n\n    //  find the peak and trough of the pulse wave\n  if(Signal < thresh && N > (IBI/5)*3){       // avoid dichrotic noise by waiting 3/5 of last IBI\n    if (Signal < T){                        // T is the trough\n      T = Signal;                         // keep track of lowest point in pulse wave \n    }\n  }\n\n  if(Signal > thresh && Signal > P){          // thresh condition helps avoid noise\n    P = Signal;                             // P is the peak\n  }                                        // keep track of highest point in pulse wave\n\n  //  NOW IT'S TIME TO LOOK FOR THE HEART BEAT\n  // signal surges up in value every time there is a pulse\n  if (N > 250){                                   // avoid high frequency noise\n    if ( (Signal > thresh) && (Pulse == false) && (N > (IBI/5)*3) ){        \n      Pulse = true;                               // set the Pulse flag when we think there is a pulse\n      digitalWrite(blinkPin,HIGH);                // turn on pin 13 LED\n      IBI = sampleCounter - lastBeatTime;         // measure time between beats in mS\n      lastBeatTime = sampleCounter;               // keep track of time for next pulse\n\n      if(secondBeat){                        // if this is the second beat, if secondBeat == TRUE\n        secondBeat = false;                  // clear secondBeat flag\n        for(int i=0; i<=9; i++){             // seed the running total to get a realisitic BPM at startup\n          rate[i] = IBI;                      \n        }\n      }\n\n      if(firstBeat){                         // if it's the first time we found a beat, if firstBeat == TRUE\n        firstBeat = false;                   // clear firstBeat flag\n        secondBeat = true;                   // set the second beat flag\n        sei();                               // enable interrupts again\n        return;                              // IBI value is unreliable so discard it\n      }   \n\n\n      // keep a running total of the last 10 IBI values\n      word runningTotal = 0;                  // clear the runningTotal variable    \n\n      for(int i=0; i<=8; i++){                // shift data in the rate array\n        rate[i] = rate[i+1];                  // and drop the oldest IBI value \n        runningTotal += rate[i];              // add up the 9 oldest IBI values\n      }\n\n      rate[9] = IBI;                          // add the latest IBI to the rate array\n      runningTotal += rate[9];                // add the latest IBI to runningTotal\n      runningTotal /= 10;                     // average the last 10 IBI values \n      BPM = 60000/runningTotal;               // how many beats can fit into a minute? that's BPM!\n      QS = true;                              // set Quantified Self flag \n      // QS FLAG IS NOT CLEARED INSIDE THIS ISR\n    }                       \n  }\n\n  if (Signal < thresh && Pulse == true){   // when the values are going down, the beat is over\n    digitalWrite(blinkPin,LOW);            // turn off pin 13 LED\n    Pulse = false;                         // reset the Pulse flag so we can do it again\n    amp = P - T;                           // get amplitude of the pulse wave\n    thresh = amp/2 + T;                    // set thresh at 50% of the amplitude\n    P = thresh;                            // reset these for next time\n    T = thresh;\n  }\n\n  if (N > 2500){                           // if 2.5 seconds go by without a beat\n    thresh = 512;                          // set thresh default\n    P = 512;                               // set P default\n    T = 512;                               // set T default\n    lastBeatTime = sampleCounter;          // bring the lastBeatTime up to date        \n    firstBeat = true;                      // set these to avoid noise\n    secondBeat = false;                    // when we get the heartbeat back\n  }\n\n  sei();                                   // enable interrupts when youre done!\n}// end isr\n\n\n\n\n\n"
  },
  {
    "path": "PulseSensor_v0/PulseSensor_v0.ino",
    "content": "\n/*  Pulse Sensor Amped 1.4    by Joel Murphy and Yury Gitman   http://www.pulsesensor.com\n\n----------------------  Notes ----------------------  ---------------------- \nThis code:\n1) Blinks an LED to User's Live Heartbeat   PIN 13\n2) Fades an LED to User's Live HeartBeat\n3) Determines BPM\n4) Prints All of the Above to Serial\n\nRead Me:\nhttps://github.com/WorldFamousElectronics/PulseSensor_Amped_Arduino/blob/master/README.md   \n ----------------------       ----------------------  ----------------------\n*/\n\n//  Variables\nint pulsePin = 0;                 // Pulse Sensor purple wire connected to analog pin 0\nint blinkPin = 13;                // pin to blink led at each beat\nint fadePin = 5;                  // pin to do fancy classy fading blink at each beat\nint fadeRate = 0;                 // used to fade LED on with PWM on fadePin\n\n// Volatile Variables, used in the interrupt service routine!\nvolatile int BPM;                   // int that holds raw Analog in 0. updated every 2mS\nvolatile int Signal;                // holds the incoming raw data\nvolatile int IBI = 600;             // int that holds the time interval between beats! Must be seeded! \nvolatile boolean Pulse = false;     // \"True\" when User's live heartbeat is detected. \"False\" when not a \"live beat\". \nvolatile boolean QS = false;        // becomes true when Arduoino finds a beat.\n\n// Regards Serial OutPut  -- Set This Up to your needs\nstatic boolean serialVisual = false;   // Set to 'false' by Default.  Re-set to 'true' to see Arduino Serial Monitor ASCII Visual Pulse \n\n\nvoid setup(){\n  pinMode(blinkPin,OUTPUT);         // pin that will blink to your heartbeat!\n  pinMode(fadePin,OUTPUT);          // pin that will fade to your heartbeat!\n  Serial.begin(115200);             // we agree to talk fast!\n  interruptSetup();                 // sets up to read Pulse Sensor signal every 2mS \n   // IF YOU ARE POWERING The Pulse Sensor AT VOLTAGE LESS THAN THE BOARD VOLTAGE, \n   // UN-COMMENT THE NEXT LINE AND APPLY THAT VOLTAGE TO THE A-REF PIN\n//   analogReference(EXTERNAL);   \n}\n\n\n//  Where the Magic Happens\nvoid loop(){\n  \n    serialOutput() ;       \n    \n  if (QS == true){     // A Heartbeat Was Found\n                       // BPM and IBI have been Determined\n                       // Quantified Self \"QS\" true when arduino finds a heartbeat\n        fadeRate = 255;         // Makes the LED Fade Effect Happen\n                                // Set 'fadeRate' Variable to 255 to fade LED with pulse\n        serialOutputWhenBeatHappens();   // A Beat Happened, Output that to serial.     \n        QS = false;                      // reset the Quantified Self flag for next time    \n  }\n     \n  ledFadeToBeat();                      // Makes the LED Fade Effect Happen \n  delay(20);                             //  take a break\n}\n\n\n\n\n\nvoid ledFadeToBeat(){\n    fadeRate -= 15;                         //  set LED fade value\n    fadeRate = constrain(fadeRate,0,255);   //  keep LED fade value from going into negative numbers!\n    analogWrite(fadePin,fadeRate);          //  fade LED\n  }\n\n\n\n\n"
  },
  {
    "path": "PulseSensor_v0/README.md",
    "content": "# Arduino sketch for PulseSensor, see http://pulsesensor.com\n\nThis is the original code from https://github.com/WorldFamousElectronics/PulseSensor_Amped_Arduino. I have only renamed it for consistency with my other PulseSense sketches.\n"
  },
  {
    "path": "PulseSensor_v0/Timer_Interrupt_Notes.cpp",
    "content": "/*\n  These notes put together by Joel Murphy for Pulse Sensor Amped, 2015\n\n  The code that this section is attached to uses a timer interrupt\n  to sample the Pulse Sensor with consistent and regular timing.\n  The code is setup to read Pulse Sensor signal at 500Hz (every 2mS).\n  The reasoning for this can be found here:\n  http://pulsesensor.com/pages/pulse-sensor-amped-arduino-v1dot1\n  \n  There are issues with using different timers to control the Pulse Sensor sample rate.\n  Sometimes, user will need to switch timers for access to other code libraries.\n  Also, some other hardware may have different timer setup requirements. This page\n  will cover those different needs and reveal the necessary settings. There are two\n  part of the code that will be discussed. The interruptSetup() routine, and\n  the interrupt function call. Depending on your needs, or the Arduino variant that you use,\n  check below for the correct settings.\n  \n  \n  ******************************************************************************************\n  ARDUINO UNO, Pro 328-5V/16MHZ, Pro-Mini 328-5V/16MHz (or any board with ATmega328P running at 16MHz)\n  \n >> Timer2\n  \n    Pulse Sensor Arduino UNO uses Timer2 by default.\n    Use of Timer2 interferes with PWM on pins 3 and 11.\n    There is also a conflict with the Tone library, so if you want tones, use Timer1 below.\n    \n      void interruptSetup(){     \n        // Initializes Timer2 to throw an interrupt every 2mS.\n        TCCR2A = 0x02;     // DISABLE PWM ON DIGITAL PINS 3 AND 11, AND GO INTO CTC MODE\n        TCCR2B = 0x06;     // DON'T FORCE COMPARE, 256 PRESCALER \n        OCR2A = 0X7C;      // SET THE TOP OF THE COUNT TO 124 FOR 500Hz SAMPLE RATE\n        TIMSK2 = 0x02;     // ENABLE INTERRUPT ON MATCH BETWEEN TIMER2 AND OCR2A\n        sei();             // MAKE SURE GLOBAL INTERRUPTS ARE ENABLED      \n      } \n  \n    use the following interrupt vector with Timer2\n    \n      ISR(TIMER2_COMPA_vect)\n      \n >> Timer1\n    \n    Use of Timer1 interferes with PWM on pins 9 and 10.\n    The Servo library also uses Timer1, so if you want servos, use Timer2 above.\n    \n      void interruptSetup(){     \n        // Initializes Timer1 to throw an interrupt every 2mS.\n        TCCR1A = 0x00; // DISABLE OUTPUTS AND PWM ON DIGITAL PINS 9 & 10\n        TCCR1B = 0x11; // GO INTO 'PHASE AND FREQUENCY CORRECT' MODE, NO PRESCALER\n        TCCR1C = 0x00; // DON'T FORCE COMPARE\n        TIMSK1 = 0x01; // ENABLE OVERFLOW INTERRUPT (TOIE1)\n        ICR1 = 16000;  // TRIGGER TIMER INTERRUPT EVERY 2mS  \n        sei();         // MAKE SURE GLOBAL INTERRUPTS ARE ENABLED     \n      } \n      \n    Use the following ISR vector for the Timer1 setup above\n    \n      ISR(TIMER1_OVF_vect)\n      \n >> Timer0\n \n    DON'T USE TIMER0! Timer0 is used for counting delay(), millis(), and micros(). \n                      Messing with Timer0 is highly unadvised!\n  \n  ******************************************************************************************\n  ARDUINO Fio, Lilypad, ProMini328-3V/8MHz (or any board with ATmega328P running at 8MHz)\n  \n  >> Timer2\n  \n    Pulse Sensor Arduino UNO uses Timer2 by default.\n    Use of Timer2 interferes with PWM on pins 3 and 11.\n    There is also a conflict with the Tone library, so if you want tones, use Timer1 below.\n    \n      void interruptSetup(){     \n        // Initializes Timer2 to throw an interrupt every 2mS.\n        TCCR2A = 0x02;     // DISABLE PWM ON DIGITAL PINS 3 AND 11, AND GO INTO CTC MODE\n        TCCR2B = 0x05;     // DON'T FORCE COMPARE, 128 PRESCALER \n        OCR2A = 0X7C;      // SET THE TOP OF THE COUNT TO 124 FOR 500Hz SAMPLE RATE\n        TIMSK2 = 0x02;     // ENABLE INTERRUPT ON MATCH BETWEEN TIMER2 AND OCR2A\n        sei();             // MAKE SURE GLOBAL INTERRUPTS ARE ENABLED      \n      } \n  \n    use the following interrupt vector with Timer2\n    \n      ISR(TIMER2_COMPA_vect)\n      \n >> Timer1\n    \n    Use of Timer1 interferes with PWM on pins 9 and 10.\n    The Servo library also uses Timer1, so if you want servos, use Timer2 above.\n    \n      void interruptSetup(){     \n        // Initializes Timer1 to throw an interrupt every 2mS.\n        TCCR1A = 0x00; // DISABLE OUTPUTS AND PWM ON DIGITAL PINS 9 & 10\n        TCCR1B = 0x11; // GO INTO 'PHASE AND FREQUENCY CORRECT' MODE, NO PRESCALER\n        TCCR1C = 0x00; // DON'T FORCE COMPARE\n        TIMSK1 = 0x01; // ENABLE OVERFLOW INTERRUPT (TOIE1)\n        ICR1 = 8000;  // TRIGGER TIMER INTERRUPT EVERY 2mS  \n        sei();         // MAKE SURE GLOBAL INTERRUPTS ARE ENABLED     \n      } \n      \n    Use the following ISR vector for the Timer1 setup above\n    \n      ISR(TIMER1_OVF_vect)\n      \n >> Timer0\n \n    DON'T USE TIMER0! Timer0 is used for counting delay(), millis(), and micros(). \n                      Messing with Timer0 is highly unadvised!\n  \n  \n  ******************************************************************************************\n  ARDUINO Leonardo (or any board with ATmega32u4 running at 16MHz) \n  \n  >> Timer1\n  \n    Use of Timer1 interferes with PWM on pins 9 and 10.\n    \n      void interruptSetup(){\n          TCCR1A = 0x00;\n          TCCR1B = 0x0C; // prescaler = 256\n          OCR1A = 0x7C;  // count to 124  \n          TIMSK1 = 0x02; \n          sei();\n      }\n \n  The only other thing you will need is the correct ISR vector in the next step.\n       \n      ISR(TIMER1_COMPA_vect)\n  \n  \n  ******************************************************************************************\n  ADAFRUIT Flora, ARDUINO Fio v3 (or any other board with ATmega32u4 running at 8MHz)\n  \n  >> Timer1\n  \n    Use of Timer1 interferes with PWM on pins 9 and 10.\n    \n      void interruptSetup(){\n          TCCR1A = 0x00;\n          TCCR1B = 0x0C; // prescaler = 256\n          OCR1A = 0x3E;  // count to 62\n          TIMSK1 = 0x02; \n          sei();\n      }\n \n  The only other thing you will need is the correct ISR vector in the next step.\n       \n      ISR(TIMER1_COMPA_vect)\n\n  ******************************************************************************************\n  ADAFRUIT Gemma (or any other board with ATtiny85 running at 8MHz)\n  \n    NOTE: Gemma does not do serial communication! \n          Comment out or remove the Serial code in the Arduino sketch!\n  \n  Timer1\n  \n    Use of Timer1 breaks PWM output on pin D1\n    \n      void interruptSetup(){     \n        TCCR1 = 0x88;      // Clear Timer on Compare, Set Prescaler to 128 TEST VALUE\n        GTCCR &= 0x81;     // Disable PWM, don't connect pins to events\n        OCR1C = 0x7C;      // Set the top of the count to  124 TEST VALUE\n        OCR1A = 0x7C;      // Set the timer to interrupt after counting to TEST VALUE\n        bitSet(TIMSK,6);   // Enable interrupt on match between TCNT1 and OCR1A\n        sei();             // Enable global interrupts     \n      } \n\n    The only other thing you will need is the correct ISR vector in the next step.\n    \n      ISR(TIMER1_COMPA_vect)\n  \n  \n  ******************************************************************************************\n  \n  \n  \n  \n  \n  ******************************************************************************************\n  \n  \n  \n  \n  \n  ******************************************************************************************\n  \n  \n  \n  \n  \n  ******************************************************************************************\n  \n  \n  \n  \n  \n  ******************************************************************************************\n  \n  \n  \n  \n  \n  ******************************************************************************************\n  \n  \n  \n  \n  \n  ******************************************************************************************\n  \n  \n  \n  \n*/\n"
  },
  {
    "path": "PulseSensor_v1/PulseSensor_v1.ino",
    "content": "// PulseSensor application that streams continuous data at 500Hz using the OpenEEG data format\n// over the serial port or a BlueTooth connection\n\n#define LSB(x) (byte)(x    & 0xff);\n#define MSB(x) (byte)(x>>8 & 0xff);\n\n#define hasUSB\n// #define hasBT\n\nunsigned int value = 0, sample = 0;\nunsigned long tsample = 4; // should be 4 to approximate 256 Hz according to protocol\n\nbyte buf[17];\n\nvoid setup() {\n\n  pinMode(A0, INPUT);  // connected to PulseSensor analog output\n\n#ifdef hasUSB\n  Serial.begin(57600);  // connected to USB\n  unsigned long tic = millis();\n  while ((millis() - tic) < 1000 && !(Serial)); // max 1 second\n#endif\n\n#ifdef hasBT\n  Serial1.begin(57600); // connected to BlueSMiRF\n  unsigned long tic = millis();\n  while ((millis() - tic) < 1000 && !(Serial1)); // max 1 second\n#endif\n\n  // the OpenEEG protocol sends data in 17 byte packets\n  buf[ 0] = 0xA5;\n  buf[ 1] = 0x5A;\n  buf[ 2] = 2; // version\n  buf[ 3] = 0; // count\n  buf[ 4] = 0; // channel 1\n  buf[ 5] = 0;\n  buf[ 6] = 0; // channel 2\n  buf[ 7] = 0;\n  buf[ 8] = 0; // channel 3\n  buf[ 9] = 0;\n  buf[10] = 0; // channel 4\n  buf[11] = 0;\n  buf[12] = 0; // channel 5\n  buf[13] = 0;\n  buf[14] = 0; // channel 6\n  buf[15] = 0;\n  buf[16] = 0; // switches\n}\n\nvoid loop() {\n\n  // read the analog value\n  value = analogRead(A0);\n\n  // scale to appropriate values\n  value *= 32;\n\n  // increment the sample number\n  sample++;\n\n  // update the data packet\n  buf[ 3]++;\n  buf[ 4] = MSB(value);  // channel 1, MSB\n  buf[ 5] = LSB(value);  // channel 1, LSB\n  buf[ 6] = MSB(sample); // channel 2, MSB\n  buf[ 7] = LSB(sample); // channel 2, LSB\n\n#ifdef hasUSB\n  Serial.flush();\n  for (int i = 0; i < 17; i++) {\n    Serial.write(buf[i]);\n  }\n#endif\n\n#ifdef hasBT\n  Serial1.flush();\n  for (int i = 0; i < 17; i++) {\n    Serial1.write(buf[i]);\n  }\n#endif\n\n  delay(tsample);\n\n}  // loop\n"
  },
  {
    "path": "PulseSensor_v1/README.md",
    "content": "# Arduino sketch for PulseSensor, see http://pulsesensor.com\n\nThis reads the analog value of the sensor and sends it over the serial port in OpenEEG format. This allows the data to be visualized in [any of these software packages](http://openeeg.sourceforge.net/doc/sw/) or in realtime EGE processing software such as [BCI2000](http://www.bci2000.org) or [FieldTrip](http://www.fieldtriptoolbox.org/development/realtime/modulareeg).\n\nThe sample timing is relatively inaccurate. It assumes that the code to read the analog value and to write serial output takes negligible time. At the end of each loop there is a delay of 4ms, resulting in approximately 250 Hz sampling rate.\n"
  },
  {
    "path": "PulseSensor_v2/Interrupt.cpp",
    "content": "\nvolatile int rate[10];                      // array to hold last ten IBI values\nvolatile unsigned long lastBeatTime = 0;    // used to find IBI\nvolatile int P = 512;                       // used to find peak in pulse wave, seeded\nvolatile int T = 512;                       // used to find trough in pulse wave, seeded\nvolatile int thresh = 525;                  // used to find instant moment of heart beat, seeded\nvolatile int amp = 100;                     // used to hold amplitude of pulse waveform, seeded\nvolatile boolean firstBeat = true;          // used to seed rate array so we startup with reasonable BPM\nvolatile boolean secondBeat = false;        // used to seed rate array so we startup with reasonable BPM\n\n\nvoid interruptSetup() {\n  // Initializes Timer2 to throw an interrupt every 2mS.\n  TCCR2A = 0x02;     // DISABLE PWM ON DIGITAL PINS 3 AND 11, AND GO INTO CTC MODE\n  TCCR2B = 0x06;     // DON'T FORCE COMPARE, 256 PRESCALER\n  OCR2A = 0X7C;      // SET THE TOP OF THE COUNT TO 124 FOR 500Hz SAMPLE RATE\n  TIMSK2 = 0x02;     // ENABLE INTERRUPT ON MATCH BETWEEN TIMER2 AND OCR2A\n  sei();             // MAKE SURE GLOBAL INTERRUPTS ARE ENABLED\n}\n\n\n// THIS IS THE TIMER 2 INTERRUPT SERVICE ROUTINE.\n// Timer 2 makes sure that we take a reading every 2 miliseconds\nISR(TIMER2_COMPA_vect) {                      // triggered when Timer2 counts to 124\n  cli();                                      // disable interrupts while we do this\n  Signal = analogRead(pulsePin);              // read the Pulse Sensor\n  sampleCounter += 2;                         // keep track of the time in mS with this variable\n  int N = sampleCounter - lastBeatTime;       // monitor the time since the last beat to avoid noise\n\n  //  find the peak and trough of the pulse wave\n  if (Signal < thresh && N > (IBI / 5) * 3) { // avoid dichrotic noise by waiting 3/5 of last IBI\n    if (Signal < T) {                         // T is the trough\n      T = Signal;                             // keep track of lowest point in pulse wave\n    }\n  }\n\n  if (Signal > thresh && Signal > P) {        // thresh condition helps avoid noise\n    P = Signal;                               // P is the peak\n  }                                           // keep track of highest point in pulse wave\n\n  //  NOW IT'S TIME TO LOOK FOR THE HEART BEAT\n  // signal surges up in value every time there is a pulse\n  if (N > 250) {                                  // avoid high frequency noise\n    if ( (Signal > thresh) && (Pulse == false) && (N > (IBI / 5) * 3) ) {\n      Pulse = true;                               // set the Pulse flag when we think there is a pulse\n      digitalWrite(blinkPin, HIGH);               // turn on pin 13 LED\n      IBI = sampleCounter - lastBeatTime;         // measure time between beats in mS\n      lastBeatTime = sampleCounter;               // keep track of time for next pulse\n\n      if (secondBeat) {                      // if this is the second beat, if secondBeat == TRUE\n        secondBeat = false;                  // clear secondBeat flag\n        for (int i = 0; i <= 9; i++) {       // seed the running total to get a realisitic BPM at startup\n          rate[i] = IBI;\n        }\n      }\n\n      if (firstBeat) {                       // if it's the first time we found a beat, if firstBeat == TRUE\n        firstBeat = false;                   // clear firstBeat flag\n        secondBeat = true;                   // set the second beat flag\n        sei();                               // enable interrupts again\n        return;                              // IBI value is unreliable so discard it\n      }\n\n\n      // keep a running total of the last 10 IBI values\n      word runningTotal = 0;                  // clear the runningTotal variable\n\n      for (int i = 0; i <= 8; i++) {          // shift data in the rate array\n        rate[i] = rate[i + 1];                // and drop the oldest IBI value\n        runningTotal += rate[i];              // add up the 9 oldest IBI values\n      }\n\n      rate[9] = IBI;                          // add the latest IBI to the rate array\n      runningTotal += rate[9];                // add the latest IBI to runningTotal\n      runningTotal /= 10;                     // average the last 10 IBI values\n      BPM = 60000 / runningTotal;             // how many beats can fit into a minute? that's BPM!\n      QS = true;                              // set Quantified Self flag\n      // QS FLAG IS NOT CLEARED INSIDE THIS ISR\n    }\n  }\n\n  if (Signal < thresh && Pulse == true) {  // when the values are going down, the beat is over\n    digitalWrite(blinkPin, LOW);           // turn off pin 13 LED\n    Pulse = false;                         // reset the Pulse flag so we can do it again\n    amp = P - T;                           // get amplitude of the pulse wave\n    thresh = amp / 2 + T;                  // set thresh at 50% of the amplitude\n    P = thresh;                            // reset these for next time\n    T = thresh;\n  }\n\n  if (N > 2500) {                          // if 2.5 seconds go by without a beat\n    thresh = 512;                          // set thresh default\n    P = 512;                               // set P default\n    T = 512;                               // set T default\n    lastBeatTime = sampleCounter;          // bring the lastBeatTime up to date\n    firstBeat = true;                      // set these to avoid noise\n    secondBeat = false;                    // when we get the heartbeat back\n  }\n\n  sei();                                   // enable interrupts when youre done!\n}// end isr\n\n\n\n"
  },
  {
    "path": "PulseSensor_v2/PulseSensor_v2.ino",
    "content": "#define LSB(x) (byte)(x    & 0xff);\n#define MSB(x) (byte)(x>>8 & 0xff);\n\n#define pulsePin  A0                // Pulse Sensor purple wire connected to analog pin 0\n#define blinkPin  13                // pin to blink led at each beat\n\n// Variables\nint TTL = 0;\nunsigned long ledTime = 0, flushTime = 0;\nunsigned long prevSample = 0;\nbyte buf[17];\n\n// Volatile Variables, used in the interrupt service routine!\nvolatile unsigned long sampleCounter = 0;   // used to determine pulse timing\nvolatile int Signal;                // int that holds raw Analog in 0. This is updated every 2mS\nvolatile int BPM;                   // int that holds the beats per minute\nvolatile int IBI = 600;             // int that holds the inter beat interval. Must be seeded!\nvolatile boolean Pulse = false;     // \"True\" when User's live heartbeat is detected. \"False\" when not a \"live beat\".\nvolatile boolean QS = false;        // becomes true when Arduino finds a beat.\n\nvoid setup() {\n  pinMode(pulsePin, INPUT);         // connected to PulseSensor analog output\n  pinMode(blinkPin, OUTPUT);        // pin that will blink to your heartbeat!\n\n  Serial.begin(57600);\n  unsigned long tic = millis();\n  while ((millis() - tic) < 1000 && !(Serial)); // max 1 second\n\n  interruptSetup();                 // sets up to read Pulse Sensor signal every 2mS\n\n  // analogReference(EXTERNAL);\n\n  // the OpenEEG protocol sends data in 17 byte packets\n  buf[ 0] = 0xA5;\n  buf[ 1] = 0x5A;\n  buf[ 2] = 2; // version\n  buf[ 3] = 0; // count\n  buf[ 4] = 0; // channel 1\n  buf[ 5] = 0;\n  buf[ 6] = 0; // channel 2\n  buf[ 7] = 0;\n  buf[ 8] = 0; // channel 3\n  buf[ 9] = 0;\n  buf[10] = 0; // channel 4\n  buf[11] = 0;\n  buf[12] = 0; // channel 5\n  buf[13] = 0;\n  buf[14] = 0; // channel 6\n  buf[15] = 0;\n  buf[16] = 0; // switches\n}\n\n\nvoid loop() {\n\n  // the processing by the interrupt handler is done every 2 ms, i.e. at 500Hz\n  // the OpenEEG data format expects sampling to be at 256 Hz, so we write every 4 ms\n  if ((sampleCounter > prevSample) && (sampleCounter % 4) == 0) {\n    buf[ 3]++;\n    buf[ 4] = MSB(Signal*32);\n    buf[ 5] = LSB(Signal*32);\n    buf[ 6] = MSB(TTL);\n    buf[ 7] = LSB(TTL);\n    buf[ 8] = MSB(BPM);\n    buf[ 9] = LSB(BPM);\n    buf[10] = MSB(IBI);\n    buf[11] = LSB(IBI);\n    buf[12] = MSB(sampleCounter);\n    buf[13] = LSB(sampleCounter);\n    prevSample = sampleCounter;\n    // Write the packet to the serial port\n    for (int i = 0; i < 17; i++) {\n      Serial.write(buf[i]);\n    }\n  }\n\n  if ((millis() - flushTime) > 100) {\n    // Flush the serial port now and then\n    Serial.flush();\n    flushTime = millis();\n  }\n\n  if ((millis() - ledTime) > 100) {\n    // Switch off the LED and the TTL signal\n    analogWrite(blinkPin, 0);\n    TTL = 0;\n  }\n\n  if (QS == true) {\n  // Switch on the LED and the TTL signal\n  ledTime = millis();\n    analogWrite(blinkPin, 255);\n    TTL = 512;\n    QS = false;\n  }\n\n  // All timing critical code is implemented by means of an interrupt\n  // Take a short break in the main loop\n  delay(2);\n}\n\n\n"
  },
  {
    "path": "PulseSensor_v2/README.md",
    "content": "# Arduino sketch for PulseSensor, see http://pulsesensor.com\n\nThis code is derived from the [original code](https://github.com/WorldFamousElectronics/PulseSensor_Amped_Arduino). It uses the same interrupt driven sampling and processing of the data, but the more fancy features have been removed (ASCII art serial output, fading led). It outputs the data at 250Hz over the serial connection in OpenEEG format with the following channels.\n\n1. contains the continuously sampled value\n2. contains a TTL-level representation of heart beat events\n3. contains the beats per minute\n4. contains the inter beat interval in milliseconds\n5. contains the sample time in milliseconds\n"
  },
  {
    "path": "README.md",
    "content": "# Arduino electronics hacking\n\nMost of the code here corresponds to electronics hardware that is documented on my [personal homepage](http://robertoostenveld.nl/category/arduino/).\n\nThe code that I wrote and/or collected in this repository is mostly a mash-up of Arduino code snippets and examples that I found at various places on the internet. There are pieces where I am not even sure if I could be considered the original author. For the pieces of code here in which I invested more substantial (creative) efforts, I am happy to share back to the community without restrictions. So unless otherwise stated, please consider the code here to be in the public domain, i.e. you can do with it what you want. See https://fairuse.stanford.edu/overview/public-domain/welcome/\n\nPlease let me know if you see code here for which you consider yourself to be the creator and for which I have not properly credited you, for example through an issue or pull request.\n\nNote that the code for the ESP8266-based ArtNet to DMX512 module that is documented [here](https://robertoostenveld.nl/art-net-to-dmx512-with-esp8266/) is not part of this repository any more but has moved to a stand-alone [esp8266_artnet_dmx512](https://github.com/robertoostenveld/esp8266_artnet_dmx512) repository.\n"
  },
  {
    "path": "bitsi/bitsi.ino",
    "content": "/*\n * BITSI - Bits to Serial Interface\n *\n * This is an Arduino Uno based serial/usb interface that can be used to monitor responses from\n * button boxes and to send TTL-level stimulus triggers to the parallel input port of EEG or MEG \n * acquisition systems.\n * \n * This code originates from https://code.google.com/archive/p/dccn-lab/ where it was released under the GNU GPL v2\n *\n * Copyright (C) 2009-2010 Erik van der Boogert & Bram Daams (for the original implementation)\n * Copyright (C) 2025-2026 Robert Oostenveld (for this cleaned up version)  \n *\n * From version v94, the Bitsi trigger pulse length can be programmed. In theory, the pulse length can be \n * between 1 and 255 ms. Therefore we send three codes in sequence to the Bitsi: [0], [1], [pulselength]. \n * By sending [0], we put the Bitsi in programming mode. Sending [1] tells the Bitsi we want to change \n * the pulse length, and the [pulselength] will be the value we want the trigger pulse to last.\n * \n * Version v94 also added the level mode to the functionality of the Bitsi. This means that when you \n * trigger an output, the output will remain 'high', until you send another code or reset the output.\n * The level mode functionality is NOT SUPPORTED by this specific implementation of the Bitsi firmware.\n *  \n */\n\n// Constants\nconst unsigned long TIMEOUT_DEBOUNCE = 8; // Debounce interval in ms\nconst int PIN_BUILTIN_LED = 13;\n\n// Input pins configuration\nconst int INPUT_PINS[8] = {10, 11, 12, 14, 15, 16, 17, 18};\n\n// Output pins configuration\nconst int OUTPUT_PINS[8] = {2, 3, 4, 5, 6, 7, 8, 9};\nunsigned long TIMEOUT_PULSE_LENGTH = 15;  // Pulse length in ms\n\n// Variables\nbyte serialInChar;                  // To receive desired Parallel_Out value\nunsigned long timeNow = 0;          // Current time reference\nunsigned long timeOnsetOut = 0;     // When Parallel_Out was set\nbool isParallelOutActive = false;   // Whether output pulse is active\n\n// Input state tracking\nbool buttonState[8] = {LOW, LOW, LOW, LOW, LOW, LOW, LOW, LOW};\nbool prevInputState[8] = {HIGH, HIGH, HIGH, HIGH, HIGH, HIGH, HIGH, HIGH};\n\n// Debounce tracking\nunsigned long debounceStartTime[8] = {0, 0, 0, 0, 0, 0, 0, 0};\nbool isDebouncing[8] = {false, false, false, false, false, false, false, false};\n\n// For initialization of the duration and/or levelmode programming\ntypedef enum operatingMode {\n  NORMAL,\n  PROGRAMMING,\n  PULSELENGTH,\n  LEVELMODE\n} operatingMode_t;\noperatingMode_t operatingMode;\n\nvoid setup() {\n  // Initialize serial port\n  Serial.begin(115200);\n\n  // Set the initial operating mode\n  operatingMode = NORMAL;\n\n  // Configure ports\n  setupInputPort();\n  setupOutputPort();\n  writeOutputPort(0);\n\n  // Configure built-in LED\n  pinMode(PIN_BUILTIN_LED, OUTPUT);\n  digitalWrite(PIN_BUILTIN_LED, LOW);\n\n  // Startup LED sequence\n  for (byte i = 0; i < 8; i++) {\n    writeOutputPort((B00000001 << (i + 1)) - 1);\n    digitalWrite(PIN_BUILTIN_LED, !digitalRead(PIN_BUILTIN_LED));\n    delay(100);\n  }\n  writeOutputPort(0);\n  delay(100);\n\n  // Show ready message\n  Serial.println(\"BITSI mode, Ready!\");\n}\n\nvoid loop() {\n  // Get current time reference\n  timeNow = millis();\n\n  // Process serial input\n  processSerialInput();\n\n  // Process button pins\n  processButtonPins();\n\n  // Handle debounce timeouts\n  handleDebounceTimeouts();\n}\n\nvoid processSerialInput() {\n  if (Serial.available()) {\n    serialInChar = Serial.read();\n\n    // deal with the initialization of the pulse length and/or levelmode programming\n    if (operatingMode == NORMAL && serialInChar == 0) {\n      operatingMode = PROGRAMMING;\n    }\n    else if (operatingMode == PROGRAMMING && serialInChar == 1) {\n      operatingMode = PULSELENGTH;\n    }\n    else if (operatingMode == PROGRAMMING && serialInChar == 2) {\n      operatingMode = LEVELMODE;\n    }\n    else if (operatingMode == PULSELENGTH) {\n      // the third byte is the pulse duration\n      TIMEOUT_PULSE_LENGTH = serialInChar;\n      operatingMode = NORMAL;\n    }\n    else if (operatingMode == LEVELMODE) {\n      // the third byte would be the bitmask, but level mode is not supported here\n      operatingMode = NORMAL;\n    }\n    else {\n      // output the incoming character as TTL signals\n      writeOutputPort(serialInChar);\n      timeOnsetOut = timeNow;\n      isParallelOutActive = true;\n    }\n  } \n  else if (isParallelOutActive && ((timeNow - timeOnsetOut) > TIMEOUT_PULSE_LENGTH)) {\n    writeOutputPort(0);\n    isParallelOutActive = false;\n  }\n}\n\nvoid processButtonPins() {\n  for (byte i = 0; i < 8; i++) {\n    buttonState[i] = digitalRead(INPUT_PINS[i]);\n    \n    if ((prevInputState[i] != buttonState[i]) && !isDebouncing[i]) {\n      // Send corresponding character (A-H for high, a-h for low)\n      Serial.print(char(buttonState[i] == HIGH ? (65 + i) : (97 + i)));\n      \n      // Start debouncing\n      debounceStartTime[i] = millis();\n      isDebouncing[i] = true;\n      prevInputState[i] = buttonState[i];\n    }\n  }\n}\n\nvoid handleDebounceTimeouts() {\n  for (byte i = 0; i < 8; i++) {\n    if (isDebouncing[i] && ((timeNow - debounceStartTime[i]) > TIMEOUT_DEBOUNCE)) {\n      isDebouncing[i] = false;\n    }\n  }\n}\n\nvoid setupOutputPort() {\n  for (int pin = 0; pin <8; pin++) {\n    pinMode(OUTPUT_PINS[pin], OUTPUT);\n    digitalWrite(OUTPUT_PINS[pin], LOW);  // Initialize outputs to LOW\n  }\n}\n\nvoid setupInputPort() {\n  for (int i = 0; i < 8; i++) {\n    pinMode(INPUT_PINS[i], INPUT);\n    digitalWrite(INPUT_PINS[i], HIGH);  // Enable pull-up resistors\n  }\n}\n\nbyte readInputPort() {\n  byte portB = PINB;\n  byte portC = PINC;\n  return ((portB & B00011100) >> 2) | ((portC & B00011111) << 3);\n}\n\nbyte readOutputPort() {\n  byte portC = PINC;\n  byte portD = PIND;\n  return ((portD & B11111100) >> 2) | ((portC & B00000011) << 6);\n}\n\nvoid writeOutputPort(byte code) {\n  // Calculate new port states\n  byte portB = (code & B11000000) >> 6 | (PINB & B11111100);\n  byte portD = (code & B00111111) << 2;\n\n  // Update ports\n  PORTB = portB;\n  PORTD = portD;\n}"
  },
  {
    "path": "blenderdefender/blenderdefender.ino",
    "content": "#include <JeeLib.h>\n#include <Ports.h>\n\n#define pirPin      9     // the number of the pushbutton pin\n#define redLed      5     // the number of the LED pin\n#define buttonPin   4     // the number of the pushbutton pin\n#define greenLed    3     // the number of the LED pin\n\n//uncomment this line for debugging through the serial interface\n// #define BLENDER_DEBUG\n\n#define KAKU_GROUP 1\n#define KAKU_ADDR  1\n\n// this is used to save some energy while sleeping\nISR(WDT_vect) { \n  Sleepy::watchdogEvent(); \n}\n\nint active  = LOW;\nint armed   = LOW;\nint engaged = LOW; \n\nint buttonState;              // the current value from the input pin\nint pirState;                 // the current value from the input pin\nint lastButtonState = HIGH;   // the previous value from the input pin\nint lastPirState = LOW;       // the previous value from the input pin\nint lastSwitchState = -1;    // the last known state of the kaku switch\n\nlong buttonDebounceTime = 0;  // the last time the button pin was toggled\nlong pirDebounceTime = 0;     // the last time the PIR pin was toggled\nlong lastSwitchTime = 0;\n\n#define debounceDelay      5  // the debounce time; increase if the output flickers\n#define armedDelay      5000\n#define blinkTime        500\n\n/****************************************************************************************/\n\nvoid setup() {\n#ifdef BLENDER_DEBUG\n  Serial.begin(57600);\n  while (!Serial) {\n    ; // wait for serial port to connect. Needed for Leonardo only\n  }\n  Serial.print(\"\\n[blenderdefender / \");\n  Serial.print(__DATE__);\n  Serial.print(\" / \");\n  Serial.print(__TIME__);\n  Serial.println(\"]\");\n#endif\n\n  // these should start in the expected value at rest \n  digitalWrite(buttonPin, HIGH);\n  digitalWrite(pirPin, LOW);\n\n  pinMode(buttonPin, INPUT_PULLUP);\n  pinMode(pirPin, INPUT);\n  pinMode(redLed, OUTPUT);\n  pinMode(greenLed, OUTPUT);\n\n  // start with the switch off\n  kakuSwitch(0);\n\n  // set initial LED state\n  ledStatus();\n}\n\n/****************************************************************************************/\n\nvoid ledStatus() {\n#ifdef BLENDER_DEBUG\n  Serial.print(\"active = \");\n  Serial.print(active);\n  Serial.print(\"\\tarmed = \");\n  Serial.print(armed);\n  Serial.print(\"\\tengaged = \");\n  Serial.println(engaged);\n  delay(10);\n#endif\n\n  if (engaged==HIGH) {\n    digitalWrite(redLed,   HIGH);\n    digitalWrite(greenLed, HIGH);\n  }\n  else if (active==LOW) {\n    digitalWrite(redLed,   LOW);\n    digitalWrite(greenLed, HIGH);\n  }\n  else if (active==HIGH && armed==LOW) {\n    digitalWrite(redLed,   (int)((millis()-buttonDebounceTime)/blinkTime) % 2);\n    digitalWrite(greenLed, LOW);\n  }\n  else if (active==HIGH && armed==HIGH) {\n    digitalWrite(redLed,   HIGH);\n    digitalWrite(greenLed, LOW);\n  }\n} // ledStatus\n\n/****************************************************************************************/\n\nvoid loop() {\n  int buttonValue = digitalRead(buttonPin);\n  int pirValue    = digitalRead(pirPin);\n\n  if (buttonValue != lastButtonState)\n    buttonDebounceTime = millis();\n\n  if (pirValue != lastPirState) \n    pirDebounceTime = millis();\n\n  if ((millis() - buttonDebounceTime) > debounceDelay) {\n    if (buttonValue != buttonState) {\n      buttonState = buttonValue;\n#ifdef BLENDER_DEBUG\n      Serial.print(\"buttonState = \");\n      Serial.println(buttonState);\n#endif\n      if (buttonState == LOW) {\n        active  = !active;\n        armed   = LOW;\n        engaged = LOW;\n      }\n    }\n  }\n\n  if (active && (millis() - buttonDebounceTime) > armedDelay)\n    armed = HIGH;\n\n  if ((millis() - pirDebounceTime) > debounceDelay) {\n    if (pirValue != pirState) {\n#ifdef BLENDER_DEBUG\n      Serial.print(\"pirState = \");\n      Serial.println(pirState);\n#endif\n      pirState = pirValue;\n      if ((pirState == HIGH) && active && armed) {\n        engaged = HIGH;\n      }\n      else {\n        engaged = LOW;\n      }\n    }\n  }\n\n  // update the LEDs\n  ledStatus();\n\n  if (active && armed && !engaged && (millis() - lastSwitchTime) > 60000) {\n    // switch the blender off every minute to safe-guard that it remains off most of the time\n    lastSwitchTime = millis();\n    kakuSwitch(0); \n  }\n\n  if (lastSwitchState!=engaged) {\n    // toggle the blender on or off\n    kakuSwitch(engaged); \n    lastSwitchState = engaged;\n    lastSwitchTime = millis();\n  }\n\n  // remember these for the next iteration of the loop\n  lastButtonState  = buttonValue;\n  lastPirState     = pirValue;\n\n  // spend some time in sleep mode to save energy\n  Sleepy::loseSomeTime(20);\n} // loop\n\n/****************************************************************************************/\n\nvoid kakuSwitch(int engaged) {\n#ifdef BLENDER_DEBUG\n  Serial.print(\"kakuSwitch \");\n#endif\n  rf12_initialize(0, RF12_433MHZ, 0);\n#ifdef BLENDER_DEBUG\n  Serial.println(engaged);\n#endif\n  // send it twice to improve the robustness\n  kakuSend(KAKU_GROUP, KAKU_ADDR, engaged);\n  kakuSend(KAKU_GROUP, KAKU_ADDR, engaged);\n}\n\n/****************************************************************************************/\n\n// this is from RF12demo\nstatic void kakuSend(char addr, byte device, byte on) {\n  int cmd = 0x600 | ((device - 1) << 4) | ((addr - 1) & 0xF);\n  if (on)\n    cmd |= 0x800;\n  for (byte i = 0; i < 4; ++i) {\n    for (byte bit = 0; bit < 12; ++bit) {\n      ookPulse(375, 1125);\n      int on = bitRead(cmd, bit) ? 1125 : 375;\n      ookPulse(on, 1500 - on);\n    }\n    ookPulse(375, 375);\n    delay(11); // approximate\n  }\n}\n\n/****************************************************************************************/\n\n// this is from RF12demo\nstatic void ookPulse(int on, int off) {\n  rf12_onOff(1);\n  delayMicroseconds(on + 150);\n  rf12_onOff(0);\n  delayMicroseconds(off - 200);\n}\n\n\n"
  },
  {
    "path": "digispark_skateboard/README.md",
    "content": "# Digispark Skateboard\n\nThis is an Arduino sketch to implement a strip of WS2812b Neopixels\nunder a skateboard. It features multiple blinking and moving patterns\nof light, which can be selected using three latching pushbuttons,\nallowing for in total 8 modes.\n\nIt is implemneted using a\n[Digispark](https://www.kickstarter.com/projects/digistump/digispark-the-tiny-arduino-enabled-usb-dev-board)\nUSB development board and two 50 cm long LED strips, each with 30\nLEDS. The LED strips are directly powered from a 3.7-4.2 V 18650\nLiPo battery, the Digispark is powered using a small DC-DC transformer\nmodule.\n\n## Light modes\n\nOne button is used to switch the power on/off, the other three\nbuttons allow selecting between up to 8 different light modes. The\nimplementation of the light modes is borrowed from one of my other\nprojects: which are [ESP8266 powered ArtNet neopixel\nmodules](https://robertoostenveld.nl/esp-8266-art-net-neopixel-module/).\n\n- static single uniform color\n- uniform blinking between two colors\n- rotating color wheel with another color background\n- rotating HSV rainbow\n\n## Components\n\n- Digispark rev4\n- 18650 LiPo battery with builit-in protection\n- battery holder\n- USB DC-DC transformer with 5V output\n- 4 latching switches, 1x for power, 3x for mode selection\n- 3 pull-up resistors, few kOhm\n- capacitor, 1000uF\n- 3-D printed enclosure\n"
  },
  {
    "path": "digispark_skateboard/colorspace.cpp",
    "content": "#include \"colorspace.h\"\n\nhsv rgb2hsv(rgb in)\n{\n  hsv         out;\n  double      min, max, delta;\n\n  min = in.r < in.g ? in.r : in.g;\n  min = min  < in.b ? min  : in.b;\n\n  max = in.r > in.g ? in.r : in.g;\n  max = max  > in.b ? max  : in.b;\n\n  out.v = max;                              // v\n  delta = max - min;\n  if (delta < 0.00001)\n  {\n    out.s = 0;\n    out.h = 0; // undefined, maybe nan?\n    return out;\n  }\n  if ( max > 0.0 ) { // NOTE: if Max is == 0, this divide would cause a crash\n    out.s = (delta / max);                  // s\n  } else {\n    // if max is 0, then r = g = b = 0\n    // s = 0, v is undefined\n    out.s = 0.0;\n    out.h = 0. / 0.;                        // its now undefined\n    return out;\n  }\n  if ( in.r >= max )                        // > is bogus, just keeps compilor happy\n    out.h = ( in.g - in.b ) / delta;        // between yellow & magenta\n  else if ( in.g >= max )\n    out.h = 2.0 + ( in.b - in.r ) / delta;  // between cyan & yellow\n  else\n    out.h = 4.0 + ( in.r - in.g ) / delta;  // between magenta & cyan\n\n  out.h *= 60.0;                            // degrees\n\n  if ( out.h < 0.0 )\n    out.h += 360.0;\n\n  return out;\n}\n\n\nrgb hsv2rgb(hsv in)\n{\n  double      hh, p, q, t, ff;\n  long        i;\n  rgb         out;\n\n  if (in.s <= 0.0) {      // < is bogus, just shuts up warnings\n    out.r = in.v;\n    out.g = in.v;\n    out.b = in.v;\n    return out;\n  }\n  hh = in.h;\n  if (hh >= 360.0) hh = 0.0;\n  hh /= 60.0;\n  i = (long)hh;\n  ff = hh - i;\n  p = in.v * (1.0 - in.s);\n  q = in.v * (1.0 - (in.s * ff));\n  t = in.v * (1.0 - (in.s * (1.0 - ff)));\n\n  switch (i) {\n    case 0:\n      out.r = in.v;\n      out.g = t;\n      out.b = p;\n      break;\n    case 1:\n      out.r = q;\n      out.g = in.v;\n      out.b = p;\n      break;\n    case 2:\n      out.r = p;\n      out.g = in.v;\n      out.b = t;\n      break;\n\n    case 3:\n      out.r = p;\n      out.g = q;\n      out.b = in.v;\n      break;\n    case 4:\n      out.r = t;\n      out.g = p;\n      out.b = in.v;\n      break;\n    case 5:\n    default:\n      out.r = in.v;\n      out.g = p;\n      out.b = q;\n      break;\n  }\n  return out;\n}\n"
  },
  {
    "path": "digispark_skateboard/colorspace.h",
    "content": "#ifndef _COLORSPACE_H_\n#define _COLORSPACE_H_\n\ntypedef struct {\n  double r;       // percent\n  double g;       // percent\n  double b;       // percent\n} rgb;\n\ntypedef struct {\n  double h;       // angle in degrees\n  double s;       // percent\n  double v;       // percent\n} hsv;\n\nhsv rgb2hsv(rgb);\nrgb hsv2rgb(hsv);\n\n#endif\n"
  },
  {
    "path": "digispark_skateboard/digispark_skateboard.ino",
    "content": "#include <Arduino.h>\n#include \"neopixel_mode.h\"\n\n// the neopixel strip is connected to pin 0\n#define BUILTIN_LED 1\n#define BUTTON0     2\n#define BUTTON1     3\n#define BUTTON2     4\n#define BRIGHTNESS  128  // can be up to 255\n\n// mode1  details are specified as r, g, b\n// mode4  details are specified as r1, g1, b1, r2, g2, b2, speed\n// mode10 details are specified as r1, g1, b1, r2, g2, b2, speed\n// mode12 details are specified as saturation, value, speed\n\n\n\nuint8_t specification0[] = {BRIGHTNESS, BRIGHTNESS, BRIGHTNESS};    // mode1\nuint8_t specification1[] = {BRIGHTNESS, 0, 0, 0, BRIGHTNESS, 0, 2}; // mode4\nuint8_t specification2[] = {0, BRIGHTNESS, 0, 0, 0, BRIGHTNESS, 2}; // mode4\nuint8_t specification3[] = {0, 0, BRIGHTNESS, BRIGHTNESS, 0, 0, 2}; // mode4\nuint8_t specification4[] = {BRIGHTNESS, 0, 0, 0, BRIGHTNESS, 0, 2}; // mode10\nuint8_t specification5[] = {0, BRIGHTNESS, 0, 0, 0, BRIGHTNESS, 2}; // mode10\nuint8_t specification6[] = {0, 0, BRIGHTNESS, BRIGHTNESS, 0, 0, 2}; // mode10\nuint8_t specification7[] = {220, BRIGHTNESS, 1};                    // mode12\n\nunsigned long previous = 0, now;\n\nvoid setup() {\n  pinMode(BUILTIN_LED, OUTPUT);\n  pinMode(BUTTON0, INPUT);\n  pinMode(BUTTON1, INPUT);\n  pinMode(BUTTON2, INPUT);\n  strip.begin(); // this is declared in neopixel_mode\n} // setup\n\nvoid loop() {\n  // blink the builtin LED for diagnostics\n  now = millis();\n  if ((now - previous) > 1000) {\n    digitalWrite(BUILTIN_LED, !digitalRead(BUILTIN_LED));\n    previous = now;\n  }\n\n  // combine the three buttons in a single binary mode\n  uint8_t mode = (digitalRead(BUTTON0) << 0) | (digitalRead(BUTTON1) << 1) | (digitalRead(BUTTON2) << 2);\n\n  // update the Neopixel LED strip according to the current mode\n  switch (mode) {\n    case 0:\n      mode1(specification0);\n      break;\n    case 1:\n      mode4(specification1);\n      break;\n    case 2:\n      mode4(specification2);\n      break;\n    case 3:\n      mode4(specification3);\n      break;\n    case 4:\n      mode10(specification4);\n      break;\n    case 5:\n      mode10(specification5);\n      break;\n    case 6:\n      mode10(specification6);\n      break;\n    case 7:\n      mode12(specification7);\n      break;\n  }\n} // loop\n"
  },
  {
    "path": "digispark_skateboard/neopixel_mode.cpp",
    "content": "/*\n    This is a subset of the color modes supported by my esp8266_artnet_neopixel sketch, see\n    https://github.com/robertoostenveld/arduino/tree/master/esp8266_artnet_neopixel\n\n    The functions have the same name and more or less similar functionality, but have been\n    simplified to save memory.\n*/\n\n#include \"neopixel_mode.h\"\n#include \"colorspace.h\"\n\nAdafruit_NeoPixel strip = Adafruit_NeoPixel(NUMPIXELS, PIN, NEO_GRB);\n\n#define ENABLE_MODE1\n#define ENABLE_MODE4\n#define ENABLE_MODE10\n#define ENABLE_MODE12\n\n/************************************************************************************/\n\nvoid mode1(uint8_t * data) {\n#ifdef ENABLE_MODE1\n  int r, g, b;\n\n  r = data[0];\n  g = data[1];\n  b = data[2];\n\n  for (int pixel = 0; pixel < strip.numPixels(); pixel++)\n    strip.setPixelColor(pixel, r, g, b);\n  strip.show();\n#endif\n}\n\n/************************************************************************************/\n\nvoid mode4(uint8_t * data) {\n#ifdef ENABLE_MODE4\n  int r, g, b;\n\n  // determine the current phase in the temporal cycle\n  float phase = CONFIG_SPEED * data[6] * millis() * 0.360;\n  phase = WRAP360(phase);\n\n  // pick between the two colors\n  if (phase < 180) {\n    r = data[0];\n    g = data[1];\n    b = data[2];\n  }\n  else {\n    r = data[3];\n    g = data[4];\n    b = data[5];\n  }\n\n  for (int pixel = 0; pixel < strip.numPixels(); pixel++)\n    strip.setPixelColor(pixel, r, g, b);\n  strip.show();\n#endif\n}\n\n/************************************************************************************/\n\nvoid mode10(uint8_t * data) {\n#ifdef ENABLE_MODE10\n  int r, g, b;\n\n  // determine the current phase in the temporal cycle\n  float phase = CONFIG_SPEED * data[6] * millis() * 0.360;\n\n  for (int pixel = 0; pixel < strip.numPixels(); pixel++) {\n    float position = (360. * pixel / (strip.numPixels() - 1)) * CONFIG_SPLIT - phase;\n    position = WRAP360(position);\n\n    if (position < 180) {\n      r = data[0];\n      g = data[1];\n      b = data[2];\n    }\n    else {\n      r = data[3];\n      g = data[4];\n      b = data[5];\n    }\n\n    strip.setPixelColor(pixel, r, g, b);\n  }\n  strip.show();\n#endif\n};\n\n/************************************************************************************/\n\nvoid mode12(uint8_t * data) {\n#ifdef ENABLE_MODE12\n\n  float saturation = 1. * data[0];\n  float value      = 1. * data[1] ;\n  float speed      = CONFIG_SPEED * data[2];\n\n  // determine the current phase in the temporal cycle\n  float phase = (speed * millis()) * 0.360;\n\n  for (int pixel = 0; pixel < strip.numPixels(); pixel++) {\n    float hue = (360. * pixel / strip.numPixels()) * CONFIG_SPLIT - phase;\n    hue = WRAP360(hue);\n\n    int r = hue;             // hue, between 0-360\n    int g = saturation;      // saturation, between 0-255\n    int b = value;           // value, between 0-255\n    map_hsv_to_rgb(&r, &g, &b);\n\n    strip.setPixelColor(pixel, r, g, b);\n  }\n  strip.show();\n#endif\n};\n\n/************************************************************************************/\n\nvoid map_hsv_to_rgb(int *r, int *g, int *b) {\n  hsv in;\n  rgb out;\n  in.h = 360. * (*r) / 256.;\n  in.s =   1. * (*g) / 255.;\n  in.v =   1. * (*b) / 255.;\n  out = hsv2rgb(in);\n  (*r) = out.r * 255;\n  (*g) = out.g * 255;\n  (*b) = out.b * 255;\n}\n"
  },
  {
    "path": "digispark_skateboard/neopixel_mode.h",
    "content": "#ifndef _NEOPIXEL_MODE_H_\n#define _NEOPIXEL_MODE_H_\n\n#include <Adafruit_NeoPixel.h>\n\n#define PIN            0\n#define NUMPIXELS      60\n\n// in the original version these can be configured dynamically\n#define CONFIG_SPEED   1.0\n#define CONFIG_SPLIT   1\n\n#define ROUND(x)   (int(x + 0.5))\n#define ABS(x)     (x * (x < 0 ? -1 : 1))\n#define MIN(x, y)  (x < y ? x : y)\n#define MAX(x, y)  (x > y ? x : y)\n#define WRAP360(x) (x > 0 ? (x - int((x)/360)*360) : (x - int((x)/360 - 1)*360))  // between    0 and 360\n#define WRAP180(x) (WRAP360(x) < 180 ? WRAP360(x) : WRAP360(x) - 360)             // between -180 and 180\n#define BALANCE(l, x1, x2)  ((x1) * (1. - l) + (x2) * l)\n\nextern Adafruit_NeoPixel strip;\n\n// the naming of these modes corresponds to my ESP8266 Artnet Neopixel module\nvoid mode1(uint8_t *);\nvoid mode4(uint8_t *);\nvoid mode10(uint8_t *);\nvoid mode12(uint8_t *);\n\nvoid map_hsv_to_rgb(int *, int *, int *);\n\n#endif\n"
  },
  {
    "path": "eegsynth_cvgate_mcp4725/README.md",
    "content": "# Arduino sketch for an MCP4725 digital-to-analog converter\n\nThis is a sketch to control Control Voltages (CV) and Gates for an analog synthesizer using an MCP4725 connected to an Arduino board.\n\nSee https://robertoostenveld.nl/usb-to-cvgate-converter-schematics-and-bill-of-materials/ for more details.\n"
  },
  {
    "path": "eegsynth_cvgate_mcp4725/eegsynth_cvgate_mcp4725.ino",
    "content": "/*\n* EEGSynth Arduino based CV/Gate controller. This sketch allows\n* one to use control voltages and gates to interface a computer\n* through an Arduino with an analog synthesizer. The hardware\n* comprises an Arduino Nano v3.0 with a MCP4725 12-bit DAC.\n* Optionally it can be extended with a number of sample-and-hold\n* ICs, a step-up voltage converter and some opamps.\n*\n* Some example sequences of characters are\n*   *c1v1024#  control 1 voltage 5*1024/4095 = 1.25 V\n*   *g1v1#     gate 1 value ON\n*\n* This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.\n* See http://creativecommons.org/licenses/by-sa/4.0/\n*\n* Copyright (C) 2015, Robert Oostenveld, http://www.eegsynth.org/\n*/\n\n#include <Wire.h>//Include the Wire library to talk I2C\n\n#define voltage1pin    2      // the pin controlling the voltage output\n#define voltage2pin    3      // the pin controlling the voltage output\n#define voltage3pin    4      // the pin controlling the voltage output\n#define voltage4pin    5      // the pin controlling the voltage output\n#define gate1pin A0      // the pin controlling the digital gate\n#define gate2pin A1      // the pin controlling the digital gate\n#define gate3pin A2      // the pin controlling the digital gate\n#define gate4pin A3      // the pin controlling the digital gate\n\n#define MCP4726_CMD_WRITEDAC            (0x40)  // Writes data to the DAC\n#define MCP4726_CMD_WRITEDACEEPROM      (0x60)  // Writes data to the DAC and the EEPROM (persisting the assigned value after reset)\n\n#define NONE    0\n#define VOLTAGE 1\n#define GATE    2\n\n#define OK    1\n#define ERROR 2\n\n//This is the I2C Address of the MCP4725, by default (A0 pulled to GND).\n//Please note that this breakout is for the MCP4725A0.\n//For devices with A0 pulled HIGH, use 0x61\n#define MCP4725_ADDR 0x60\n\n// these remember the state of all CV and gate outputs\nint voltage1 = 0, voltage2 = 0, voltage3 = 0, voltage4 = 0;\nint gate1 = 0, gate2 = 0, gate3 = 0, gate4 = 0;\nint enable1 = 1, enable2 = 0, enable3 = 0, enable4 = 0;\n\nvoid sampleAndHold(int pin, uint16_t value) {\n  uint8_t msb, lsb;\n  msb = (value / 16);\n  lsb = (value % 16) << 4;\n  Wire.beginTransmission(MCP4725_ADDR);\n  Wire.write(MCP4726_CMD_WRITEDAC);   // cmd to update the DAC\n  Wire.write(msb);                    // the 8 most significant bits...\n  Wire.write(lsb);                    // the 4 least significant bits...\n  Wire.endTransmission();\n  digitalWrite(pin, HIGH);\n  delay(1);                           // give it some time to Sample and Hold\n  digitalWrite(pin, LOW);\n  return;\n}\n\nvoid setup() {\n  // initialize the serial communication:\n  Serial.begin(115200);\n  Serial.print(\"\\n[eegsynth_cvgate / \");\n  Serial.print(__DATE__);\n  Serial.print(\" / \");\n  Serial.print(__TIME__);\n  Serial.println(\"]\");\n  Serial.setTimeout(1000);\n\n  Wire.begin();\n\n  // initialize the gate pins as output:\n  pinMode(gate1pin, OUTPUT);\n  pinMode(gate2pin, OUTPUT);\n  pinMode(gate3pin, OUTPUT);\n  pinMode(gate4pin, OUTPUT);\n\n  // initialize the control voltage pins as output:\n  pinMode(voltage1pin, OUTPUT);\n  pinMode(voltage2pin, OUTPUT);\n  pinMode(voltage3pin, OUTPUT);\n  pinMode(voltage4pin, OUTPUT);\n}\n\nvoid loop() {\n  byte b, channel = 0, command = NONE, status = OK;\n  int value = 0;\n\n  if (Serial.available()) {\n\n    // parse the input over the serial connection\n    b = Serial.read();\n    if (b == '*') {\n      Serial.readBytes(&b, 1);\n      if (b == 'c') {\n        command = VOLTAGE;\n        value = 0;\n        Serial.readBytes(&b, 1); channel = b - 48; // character '1' is ascii value 49\n        Serial.readBytes(&b, 1); // 'v'\n        Serial.readBytes(&b, 1); value  = (b - 48) * 1000;\n        Serial.readBytes(&b, 1); value += (b - 48) * 100;\n        Serial.readBytes(&b, 1); value += (b - 48) * 10;\n        Serial.readBytes(&b, 1); value += (b - 48) * 1;\n        Serial.readBytes(&b, 1); command = (b == '#' ? command : NONE);\n      }\n      else if (b == 'g') {\n        command = GATE;\n        Serial.readBytes(&b, 1); channel = b - 48; // character '1' is ascii value 49\n        Serial.readBytes(&b, 1); // 'v'\n        Serial.readBytes(&b, 1); value  = (b == '1');\n        Serial.readBytes(&b, 1); command = (b == '#' ? command : NONE);\n      }\n      else {\n        command = NONE;\n      }\n    }\n    else {\n      command = NONE;\n    }\n\n    // update the internal state of all output channels\n    if (command == VOLTAGE) {\n      switch (channel) {\n        case 1:\n          voltage1 = value;\n          status = (enable1 ? OK : ERROR);\n          break;\n        case 2:\n          voltage2 = value;\n          status = (enable2 ? OK : ERROR);\n          break;\n        case 3:\n          voltage3 = value;\n          status = (enable3 ? OK : ERROR);\n          break;\n        case 4:\n          voltage4 = value;\n          status = (enable4 ? OK : ERROR);\n          break;\n        default:\n          status = ERROR;\n      }\n\n    }\n    else if (command == GATE) {\n      switch (channel) {\n        case 1:\n          gate1 = (value != 0);\n          status = OK;\n          break;\n        case 2:\n          gate2 = (value != 0);\n          status = OK;\n          break;\n        case 3:\n          gate3 = (value != 0);\n          status = OK;\n          break;\n        case 4:\n          gate4 = (value != 0);\n          status = OK;\n          break;\n        default:\n          status = ERROR;\n      }\n    }\n    else {\n      status = ERROR;\n    }\n    if (status == OK)\n      Serial.println(\"ok\");\n    else if (status == ERROR)\n      Serial.println(\"error\");\n  }\n  else {\n    // refresh all enabled output channels\n    if (enable1) {\n      sampleAndHold(voltage1pin, voltage1);\n      digitalWrite(gate1pin, gate1);\n    }\n    if (enable2) {\n      sampleAndHold(voltage2pin, voltage2);\n      digitalWrite(gate2pin, gate2);\n    }\n    if (enable3) {\n      sampleAndHold(voltage3pin, voltage3);\n      digitalWrite(gate3pin, gate3);\n    }\n    if (enable4) {\n      sampleAndHold(voltage4pin, voltage4);\n      digitalWrite(gate4pin, gate4);\n    }\n  }\n} //main\n\n\n"
  },
  {
    "path": "eegsynth_cvgate_mcp4822/eegsynth_cvgate_mcp4822.ino",
    "content": "/*\n* EEGSynth Arduino based CV/Gate controller. This sketch allows\n* one to use control voltages and gates to interface a computer\n* through an Arduino with an analog synthesizer. The hardware\n* comprises an Arduino Nano v3.0 with one or multiple MCP4822\n* 12-bit DACs.\n*\n* Some example sequences of characters are\n*   *c1v1024#  control 1 voltage 5*1024/4095 = 1.25 V\n*   *g1v1#     gate 1 value ON\n*\n* This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.\n* See http://creativecommons.org/licenses/by-sa/4.0/\n*\n* Copyright (C) 2015, Robert Oostenveld, http://www.eegsynth.org/\n*/\n\n#include <SPI.h>\n\nconst int GAIN_1 = 0x1;\nconst int GAIN_2 = 0x0;\n\n#define voltage12cs     2      // the slave-select pin for channel 1 and 2 on DAC #1\n#define voltage34cs     3      // the slave-select pin for channel 3 and 4 on DAC #2\n#define gate1pin A0            // the pin controlling the digital gate\n#define gate2pin A1            // the pin controlling the digital gate\n#define gate3pin A2            // the pin controlling the digital gate\n#define gate4pin A3            // the pin controlling the digital gate\n\n#define NONE    0\n#define VOLTAGE 1\n#define GATE    2\n\n#define OK    1\n#define ERROR 2\n\n\n// these remember the state of all CV and gate outputs\nint voltage1 = 0, voltage2 = 0, voltage3 = 0, voltage4 = 0;\nint gate1 = 0, gate2 = 0, gate3 = 0, gate4 = 0;\nconst int enable1 = 1, enable2 = 1, enable3 = 1, enable4 = 1;\n\nvoid setDacOutput(byte channel, byte gain, byte shutdown, unsigned int val)\n{\n  byte lowByte = val & 0xff;\n  byte highByte = ((val >> 8) & 0xff) | channel << 7 | gain << 5 | shutdown << 4;\n\n  // note that we are not using the default pin 10 for slave select, see further down\n  // PORTB &= 0xfb; // toggle pin 10 as Slave Select low\n  SPI.transfer(highByte);\n  SPI.transfer(lowByte);\n  // PORTB |= 0x4; // toggle pin 10 as Slave Select high\n}\n\nvoid setup() {\n  // initialize the serial communication:\n  Serial.begin(115200);\n  Serial.print(\"\\n[eegsynth_cvgate_mcp4822 / \");\n  Serial.print(__DATE__);\n  Serial.print(\" / \");\n  Serial.print(__TIME__);\n  Serial.println(\"]\");\n  Serial.setTimeout(1000);\n\n  SPI.begin();\n\n  // initialize the gate pins as output:\n  pinMode(gate1pin, OUTPUT);\n  pinMode(gate2pin, OUTPUT);\n  pinMode(gate3pin, OUTPUT);\n  pinMode(gate4pin, OUTPUT);\n\n  // initialize the control voltage pins as output:\n  pinMode(voltage12cs, OUTPUT);\n  pinMode(voltage34cs, OUTPUT);\n\n  // default is to disable the communication with all SPI devices\n  digitalWrite(voltage12cs, HIGH);\n  digitalWrite(voltage34cs, HIGH);\n}\n\nvoid loop() {\n  byte b, channel = 0, command = NONE, status = OK;\n  int value = 0;\n\n  if (Serial.available()) {\n\n    // parse the input over the serial connection\n    b = Serial.read();\n    if (b == '*') {\n      Serial.readBytes(&b, 1);\n      if (b == 'c') {\n        command = VOLTAGE;\n        value = 0;\n        Serial.readBytes(&b, 1); channel = b - 48; // character '1' is ascii value 49\n        Serial.readBytes(&b, 1); // 'v'\n        Serial.readBytes(&b, 1); value  = (b - 48) * 1000;\n        Serial.readBytes(&b, 1); value += (b - 48) * 100;\n        Serial.readBytes(&b, 1); value += (b - 48) * 10;\n        Serial.readBytes(&b, 1); value += (b - 48) * 1;\n        Serial.readBytes(&b, 1); command = (b == '#' ? command : NONE);\n      }\n      else if (b == 'g') {\n        command = GATE;\n        Serial.readBytes(&b, 1); channel = b - 48; // character '1' is ascii value 49\n        Serial.readBytes(&b, 1); // 'v'\n        Serial.readBytes(&b, 1); value  = (b == '1');\n        Serial.readBytes(&b, 1); command = (b == '#' ? command : NONE);\n      }\n      else {\n        command = NONE;\n      }\n    }\n    else {\n      command = NONE;\n    }\n\n    // update the internal state of all output channels\n    if (command == VOLTAGE) {\n      switch (channel) {\n        case 1:\n          voltage1 = value;\n          status = (enable1 ? OK : ERROR);\n          break;\n        case 2:\n          voltage2 = value;\n          status = (enable2 ? OK : ERROR);\n          break;\n        case 3:\n          voltage3 = value;\n          status = (enable3 ? OK : ERROR);\n          break;\n        case 4:\n          voltage4 = value;\n          status = (enable4 ? OK : ERROR);\n          break;\n        default:\n          status = ERROR;\n      }\n\n    }\n    else if (command == GATE) {\n      switch (channel) {\n        case 1:\n          gate1 = (value != 0);\n          status = OK;\n          break;\n        case 2:\n          gate2 = (value != 0);\n          status = OK;\n          break;\n        case 3:\n          gate3 = (value != 0);\n          status = OK;\n          break;\n        case 4:\n          gate4 = (value != 0);\n          status = OK;\n          break;\n        default:\n          status = ERROR;\n      }\n    }\n    else {\n      status = ERROR;\n    }\n    if (status == OK)\n      Serial.println(\"ok\");\n    else if (status == ERROR)\n      Serial.println(\"error\");\n  }\n  else {\n    // refresh all enabled output channels\n    if (enable1) {\n      digitalWrite(voltage12cs, LOW); // enable slave select\n      setDacOutput(0, GAIN_2, 1, voltage1);\n      digitalWrite(voltage12cs, HIGH); // disable slave select\n      digitalWrite(gate1pin, gate1);\n    }\n    if (enable2) {\n      digitalWrite(voltage12cs, LOW); // enable slave select\n      setDacOutput(1, GAIN_2, 1, voltage2);\n      digitalWrite(voltage12cs, HIGH); // disable slave select\n      digitalWrite(gate2pin, gate2);\n    }\n    if (enable3) {\n      digitalWrite(voltage34cs, LOW); // enable slave select\n      setDacOutput(0, GAIN_2, 1, voltage3);\n      digitalWrite(voltage34cs, HIGH); // disable slave select\n      digitalWrite(gate3pin, gate3);\n    }\n    if (enable4) {\n      digitalWrite(voltage34cs, LOW); // enable slave select\n      setDacOutput(1, GAIN_2, 1, voltage4);\n      digitalWrite(voltage34cs, HIGH); // disable slave select\n      digitalWrite(gate4pin, gate4);\n    }\n  }\n} //main\n\n"
  },
  {
    "path": "eegsynth_devirtualizer/eegsynth_devirtualizer.ino",
    "content": "/*\n* EEGSynth Arduino based CV/Gate controller. This sketch allows\n* one to use control voltages and gates to interface a computer\n* through an Arduino with an analog synthesizer. The hardware\n* comprises an Arduino Nano v3.0 with one or multiple MCP4822\n* 12-bit DACs.\n*\n* Some example sequences of characters are\n*   *c1v1024#  control 1 voltage 5*1024/4095 = 1.25 V\n*   *g1v1#     gate 1 value ON\n*\n* This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.\n* See http://creativecommons.org/licenses/by-sa/4.0/\n*\n* Copyright (C) 2015, Robert Oostenveld, http://www.eegsynth.org/\n*/\n\n#include <SPI.h>\n\nconst int GAIN_1 = 0x1;\nconst int GAIN_2 = 0x0;\n\n#define voltage12cs   A0      // the slave-select pin for channel 1 and 2 on DAC #1\n#define voltage34cs   A1      // the slave-select pin for channel 3 and 4 on DAC #2\n#define voltage56cs   A2      // the slave-select pin for channel 1 and 2 on DAC #1\n#define voltage78cs   A3      // the slave-select pin for channel 3 and 4 on DAC #2\n#define gate1pin 2            // the pin controlling the digital gate\n#define gate2pin 3            // the pin controlling the digital gate\n#define gate3pin 4            // the pin controlling the digital gate\n#define gate4pin 5            // the pin controlling the digital gate\n#define gate5pin 6            // the pin controlling the digital gate\n#define gate6pin 7            // the pin controlling the digital gate\n#define gate7pin 8            // the pin controlling the digital gate\n#define gate8pin 9            // the pin controlling the digital gate\n\n#define NONE    0\n#define VOLTAGE 1\n#define GATE    2\n\n#define OK    1\n#define ERROR 2\n\n\n// these remember the state of all CV and gate outputs\nint voltage1 = 0, voltage2 = 0, voltage3 = 0, voltage4 = 0, voltage5, voltage6, voltage7, voltage8;\nint gate1 = 0, gate2 = 0, gate3 = 0, gate4 = 0, gate5, gate6, gate7, gate8;\nconst int enable1 = 1, enable2 = 1, enable3 = 1, enable4 = 1, enable5 = 1, enable6 = 1, enable7 = 1, enable8 = 1;\n\nvoid setDacOutput(byte channel, byte gain, byte shutdown, unsigned int val)\n{\n  byte lowByte = val & 0xff;\n  byte highByte = ((val >> 8) & 0xff) | channel << 7 | gain << 5 | shutdown << 4;\n\n  // note that we are not using the default pin 10 for slave select, see further down\n  // PORTB &= 0xfb; // toggle pin 10 as Slave Select low\n  SPI.transfer(highByte);\n  SPI.transfer(lowByte);\n  // PORTB |= 0x4; // toggle pin 10 as Slave Select high\n}\n\nvoid setup() {\n  // initialize the serial communication:\n  Serial.begin(115200);\n  Serial.print(\"\\n[eegsynth_devirtualizer / \");\n  Serial.print(__DATE__);\n  Serial.print(\" / \");\n  Serial.print(__TIME__);\n  Serial.println(\"]\");\n  Serial.setTimeout(1000);\n\n  SPI.begin();\n\n  // initialize the gate pins as output:\n  pinMode(gate1pin, OUTPUT);\n  pinMode(gate2pin, OUTPUT);\n  pinMode(gate3pin, OUTPUT);\n  pinMode(gate4pin, OUTPUT);\n  pinMode(gate5pin, OUTPUT);\n  pinMode(gate6pin, OUTPUT);\n  pinMode(gate7pin, OUTPUT);\n  pinMode(gate8pin, OUTPUT);\n\n  // initialize the control voltage pins as output:\n  pinMode(voltage12cs, OUTPUT);\n  pinMode(voltage34cs, OUTPUT);\n  pinMode(voltage56cs, OUTPUT);\n  pinMode(voltage78cs, OUTPUT);\n\n  // default is to disable the communication with all SPI devices\n  digitalWrite(voltage12cs, HIGH);\n  digitalWrite(voltage34cs, HIGH);\n  digitalWrite(voltage56cs, HIGH);\n  digitalWrite(voltage78cs, HIGH);\n}\n\nvoid loop() {\n  byte b, channel = 0, command = NONE, status = OK;\n  int value = 0;\n\n  if (Serial.available()) {\n\n    // parse the input over the serial connection\n    b = Serial.read();\n    if (b == '*') {\n      Serial.readBytes(&b, 1);\n      if (b == 'c') {\n        command = VOLTAGE;\n        value = 0;\n        Serial.readBytes(&b, 1); channel = b - 48; // character '1' is ascii value 49\n        Serial.readBytes(&b, 1); // 'v'\n        Serial.readBytes(&b, 1); value  = (b - 48) * 1000;\n        Serial.readBytes(&b, 1); value += (b - 48) * 100;\n        Serial.readBytes(&b, 1); value += (b - 48) * 10;\n        Serial.readBytes(&b, 1); value += (b - 48) * 1;\n        Serial.readBytes(&b, 1); command = (b == '#' ? command : NONE);\n      }\n      else if (b == 'g') {\n        command = GATE;\n        Serial.readBytes(&b, 1); channel = b - 48; // character '1' is ascii value 49\n        Serial.readBytes(&b, 1); // 'v'\n        Serial.readBytes(&b, 1); value  = (b == '1');\n        Serial.readBytes(&b, 1); command = (b == '#' ? command : NONE);\n      }\n      else {\n        command = NONE;\n      }\n    }\n    else {\n      command = NONE;\n    }\n\n    // update the internal state of all output channels\n    if (command == VOLTAGE) {\n      switch (channel) {\n        case 1:\n          voltage1 = value;\n          status = (enable1 ? OK : ERROR);\n          break;\n        case 2:\n          voltage2 = value;\n          status = (enable2 ? OK : ERROR);\n          break;\n        case 3:\n          voltage3 = value;\n          status = (enable3 ? OK : ERROR);\n          break;\n        case 4:\n          voltage4 = value;\n          status = (enable4 ? OK : ERROR);\n          break;\n        case 5:\n          voltage5 = value;\n          status = (enable5 ? OK : ERROR);\n          break;\n        case 6:\n          voltage6 = value;\n          status = (enable6 ? OK : ERROR);\n          break;\n        case 7:\n          voltage7 = value;\n          status = (enable7 ? OK : ERROR);\n          break;\n        case 8:\n          voltage8 = value;\n          status = (enable8 ? OK : ERROR);\n          break;\n        default:\n          status = ERROR;\n      }\n\n    }\n    else if (command == GATE) {\n      switch (channel) {\n        case 1:\n          gate1 = (value != 0);\n          status = OK;\n          break;\n        case 2:\n          gate2 = (value != 0);\n          status = OK;\n          break;\n        case 3:\n          gate3 = (value != 0);\n          status = OK;\n          break;\n        case 4:\n          gate4 = (value != 0);\n          status = OK;\n          break;\n        case 5:\n          gate5 = (value != 0);\n          status = OK;\n          break;\n        case 6:\n          gate6 = (value != 0);\n          status = OK;\n          break;\n        case 7:\n          gate7 = (value != 0);\n          status = OK;\n          break;\n        case 8:\n          gate8 = (value != 0);\n          status = OK;\n          break;\n        default:\n          status = ERROR;\n      }\n    }\n    else {\n      status = ERROR;\n    }\n    if (status == OK)\n      Serial.println(\"ok\");\n    else if (status == ERROR)\n      Serial.println(\"error\");\n  }\n  else {\n    // refresh all enabled output channels\n    if (enable1) {\n      digitalWrite(voltage12cs, LOW); // enable slave select\n      setDacOutput(0, GAIN_2, 1, voltage1);\n      digitalWrite(voltage12cs, HIGH); // disable slave select\n      digitalWrite(gate1pin, gate1);\n    }\n    if (enable2) {\n      digitalWrite(voltage12cs, LOW); // enable slave select\n      setDacOutput(1, GAIN_2, 1, voltage2);\n      digitalWrite(voltage12cs, HIGH); // disable slave select\n      digitalWrite(gate2pin, gate2);\n    }\n    if (enable3) {\n      digitalWrite(voltage34cs, LOW); // enable slave select\n      setDacOutput(0, GAIN_2, 1, voltage3);\n      digitalWrite(voltage34cs, HIGH); // disable slave select\n      digitalWrite(gate3pin, gate3);\n    }\n    if (enable4) {\n      digitalWrite(voltage34cs, LOW); // enable slave select\n      setDacOutput(1, GAIN_2, 1, voltage4);\n      digitalWrite(voltage34cs, HIGH); // disable slave select\n      digitalWrite(gate4pin, gate4);\n    }\n    if (enable5) {\n      digitalWrite(voltage56cs, LOW); // enable slave select\n      setDacOutput(1, GAIN_2, 1, voltage5);\n      digitalWrite(voltage56cs, HIGH); // disable slave select\n      digitalWrite(gate5pin, gate5);\n    }\n    if (enable6) {\n      digitalWrite(voltage56cs, LOW); // enable slave select\n      setDacOutput(1, GAIN_2, 1, voltage6);\n      digitalWrite(voltage56cs, HIGH); // disable slave select\n      digitalWrite(gate6pin, gate6);\n    }\n    if (enable7) {\n      digitalWrite(voltage78cs, LOW); // enable slave select\n      setDacOutput(1, GAIN_2, 1, voltage7);\n      digitalWrite(voltage78cs, HIGH); // disable slave select\n      digitalWrite(gate7pin, gate7);\n    }\n    if (enable8) {\n      digitalWrite(voltage78cs, LOW); // enable slave select\n      setDacOutput(1, GAIN_2, 1, voltage8);\n      digitalWrite(voltage78cs, HIGH); // disable slave select\n      digitalWrite(gate8pin, gate8);\n    }\n  }\n} //main\n\n"
  },
  {
    "path": "eegsynth_usbdmxpro/COPYING.txt",
    "content": "/*==============================================================================\n  Copyright (c) 2013 Soixante circuits\n\n  Permission is hereby granted, free of charge, to any person obtaining a copy\n  of this software and associated documentation files (the \"Software\"), to deal\n  in the Software without restriction, including without limitation the rights\n  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n  copies of the Software, and to permit persons to whom the Software is\n  furnished to do so, subject to the following conditions:\n\n  The above copyright notice and this permission notice shall be included in\n  all copies or substantial portions of the Software.\n\n  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n  THE SOFTWARE.\n  ==============================================================================*/\n\n"
  },
  {
    "path": "eegsynth_usbdmxpro/eegsynth_usbdmxpro.ino",
    "content": "/*\n   The purpose of this sketch is to implement a module that converts from USB to DMX512.\n   This allows to use computer software to control stage lighting lamps\n\n   This sketch is partially compatible with the Enttec DMX USB Pro module, but the startup\n   sequence of this Arduino sketch is different and other/commercial software is likely not\n   to recognize the module as DMX USB Pro .\n\n   Components\n   - Arduino Nano or compatible 5V board, e.g. http://ebay.to/2iAeUON\n   - MAX485 module, e.g.  http://ebay.to/2iuKQlr\n   - 3 or 5 pin female XLR connector\n\n   Wiring scheme\n   - connect 3.3V and GND from the Arduino to Vcc and GND of the MAX485 module\n   - connect pin DE (data enable) and RE (receive enable) of the MAX485 module to 3.3V\n   - connect pin D2 of the Arduino to the DI (data in) pin of the MAX485 module\n   - connect pin A to XLR 3\n   - connect pin B to XLR 2\n   - connect GND   to XLR 1\n\n*/\n\n#include <DmxSimple.h>\n\n#define DI_PIN 2 // data out of the Arduino, data in to the MAX485\n\n// these are defined in https://www.enttec.com/docs/dmx_usb_pro_api_spec.pdf\n#define DMX_PRO_START_MSG             126  // 0x7E\n#define DMX_PRO_END_MSG               231  // 0xE7\n\n// the packets can have the following labels, i.e. content\n#define DMX_PRO_REPROGRAM_FIRMWARE    1\n#define DMX_PRO_FLASH_PAGE            2\n#define DMX_PRO_GET_WIDGET_PARAM      3\n#define DMX_PRO_SET_WIDGET_PARAM      4\n#define DMX_PRO_RECEIVED_DMX          5\n#define DMX_PRO_SEND_DMX              6\n#define DMX_PRO_SEND_RDM              7\n#define DMX_PRO_RECEIVE_DMX_ON_CHANGE 8\n#define DMX_PRO_RECEIVED_DMX_CHANGE   9\n#define DMX_PRO_GET_SERIAL_NUMBER     10\n#define DMX_PRO_SENT_DRM_DISCOVERY    11\n\n// each DMX data packet starts with this code\n#define DMX_PRO_START_CODE            0\n\n// these are some local states\n#define DMX_PRO_SEND_DMX_LSB          240\n#define DMX_PRO_SEND_DMX_MSB          241\n#define DMX_PRO_SEND_DMX_DATA         242\n\nunsigned char state = DMX_PRO_END_MSG;\nunsigned int dataSize;\nunsigned int channel;\n\nvoid setup() {\n  DmxSimple.usePin(DI_PIN);\n  Serial.begin(57600);\n  while (!Serial);\n}\n\nvoid loop() {\n  while (!Serial.available())\n    yield();\n\n  unsigned char c = Serial.read();\n\n  if (c == DMX_PRO_START_MSG && state != DMX_PRO_END_MSG) {\n    state = c; // this should not happen, it means that part of the previous packet was lost\n  }\n  if (c == DMX_PRO_START_MSG && state == DMX_PRO_END_MSG) {\n    state = c;\n  }\n  else if (c == DMX_PRO_SEND_DMX && state == DMX_PRO_START_MSG) {\n    state = c;\n  }\n  else if (state == DMX_PRO_SEND_DMX) {\n    dataSize = c & 0xff;\n    state = DMX_PRO_SEND_DMX_LSB;\n  }\n  else if (state == DMX_PRO_SEND_DMX_LSB) {\n    dataSize += (c << 8) & 0xff00;\n    state = DMX_PRO_SEND_DMX_MSB;\n  }\n  else if (state == DMX_PRO_SEND_DMX_MSB && c == DMX_PRO_START_CODE) {\n    state = DMX_PRO_SEND_DMX_DATA;\n    channel = 1;\n  }\n  else if (state == DMX_PRO_SEND_DMX_DATA && channel < dataSize) {\n    DmxSimple.write(channel, c);\n    channel++;\n  }\n  else if (state == DMX_PRO_SEND_DMX_DATA && channel == dataSize && c == DMX_PRO_END_MSG) {\n    state = c;\n  }\n}\n"
  },
  {
    "path": "esp32_3wd_servo/README.md",
    "content": "# Three-wheel-drive omni-wheel robot platform\n\nThis is an Arduino sketch for an ESP32 (LOLIN32 Lite) controlled\nthree-wheel-drive omni-wheel robot based on servo motors, similar\nto the one documented [here][1] and at various other places online.\n\nThe motors are continuous rotation TS90D servo motors, i.e., these\nrotate at a given speed, not towards a given location like normal\nservo motors.\n\nThe control of the robot is implemented using Open Sound Control\n(OSC), using the [TouchOSC][5] app on my iPhone. Actually, the\nfirmware version in this sketch should not (yet) be considered a\nrobot since there is no autonomous movement, but more like a \nradio-controlled car.\n\nThe reason for me **not continuing** the development of this robot\nplatform is that the servo motors are too noisy for the intended\npurpose, and that they stall when running them at a too low speed.\nI switched to using 28BYJ-48 stepper motors; the sketch for that\ncan be found elsewhere in this repository.\n\n[1]: https://github.com/manav20/3-wheel-omni\n[2]: https://en.wikipedia.org/wiki/Omni_wheel\n[3]: https://www.piscinarobots.nl/robots-y-kits/38mm%20(1.5%20inch)%20double%20plastic%20omni%20wheel%20(compatible%20met%20servo%20motor%20)%20-%2014184\n[4]: https://nl.aliexpress.com/item/32478938051.html\n[5]: https://hexler.net\n"
  },
  {
    "path": "esp32_3wd_servo/esp32_3wd_servo.ino",
    "content": "#include <Arduino.h>\n#include <WiFi.h>\n#include <WiFiManager.h>  // https://github.com/tzapu/WiFiManager\n#include <WiFiUdp.h>\n#include <OSCMessage.h>   // https://github.com/CNMAT/OSC\n#include <ESP32Servo.h>\n#include <math.h>\n\nWiFiUDP Udp;\nOSCErrorCode error;\nServo servo1, servo2, servo3;\n\nconst char *host = \"3WD-SERVO\";\nconst char *version = __DATE__ \" / \" __TIME__;\n\nconst unsigned int inPort = 8000;   // local OSC port\nconst unsigned int outPort = 9000;  // remote OSC port (not used here)\n\n// GPIO connections to the servo control\nconst int servo1_pin = 25;\nconst int servo2_pin = 26;\nconst int servo3_pin = 27;\n\n// published values for the TS90D servo, neutral is 1500 uS\nconst int minUs = 500;\nconst int maxUs = 2500;\n\nconst float pr = 0.06;            // platform radius, distance from the platform center to each of the wheels, in meter\nfloat vx = 0, vy = 0, vt = 0;     // speed in meter per second, and in radians per second\nfloat x = 0, y = 0, theta = 0;    // absolute position (in meter) and rotation (in radians)\nfloat r1 = 0, r2 = 0, r3 = 0;     // speed of the stepper motors, in steps per second\n\n// don't use the built-in version of map(), as that is only for long int values\n// see https://docs.arduino.cc/language-reference/en/functions/math/map/\n#define map(value, fromLow, fromHigh, toLow, toHigh) ((toHigh - toLow) * (value - fromLow) / (fromHigh - fromLow) + toLow)\n\nvoid printCallback(OSCMessage &msg) {\n  Serial.print(msg.getAddress());\n  Serial.print(\" : \");\n\n  for (int i = 0; i < msg.size(); i++) {\n    if (msg.isInt(i)) {\n      Serial.print(msg.getInt(i));\n    } else if (msg.isFloat(i)) {\n      Serial.print(msg.getFloat(i));\n    } else if (msg.isDouble(i)) {\n      Serial.print(msg.getDouble(i));\n    } else if (msg.isBoolean(i)) {\n      Serial.print(msg.getBoolean(i));\n    } else if (msg.isString(i)) {\n      char buffer[256];\n      msg.getString(i, buffer);\n      Serial.print(buffer);\n    } else {\n      Serial.print(\"?\");\n    }\n\n    if (i < (msg.size() - 1)) {\n      Serial.print(\", \");  // there are more to come\n    }\n  }\n  Serial.println();\n}\n\nvoid accxyzCallback(OSCMessage &msg) {\n  vx = msg.getFloat(0);\n  vy = msg.getFloat(1);\n  vt = 0;\n  updateSpeed();\n}\n\nvoid updateSpeed() {\n  // convert the speed in world-coordinates into rotation speed of the motors and into servo controls\n  // see https://github.com/manav20/3-wheel-omni\n\n  // pr is the platform radius, i.e. the distance of the center of the platform to each wheel \n  // vx, vy, and vt is the speed in world-coordinates\n  // r1, r2, and r3 is the rotation speed of the three motors\n  // theta is the heading of the robot (FIXME, needs to be integrated over time)\n\n  r1 = -sin(theta) * vx + cos(theta) * vy + pr * vt;\n  r2 = -sin(PI / 3 - theta) * vx - cos(PI / 3 - theta) * vy + pr * vt;\n  r3 = sin(PI / 3 + theta) * vx - cos(PI / 3 + theta) * vy + pr * vt;\n\n  // convert the rotation speed of the motors into servo control values between 0 and 180, where 90 is neutral\n  r1 = map(r1, -1, 1, 0, 180);\n  r2 = map(r2, -1, 1, 0, 180);\n  r3 = map(r3, -1, 1, 0, 180);\n\n  servo1.write(r1);\n  servo2.write(r2);\n  servo3.write(r3);\n\n  Serial.print(r1);\n  Serial.print(\", \");\n  Serial.print(r2);\n  Serial.print(\", \");\n  Serial.print(r3);\n  Serial.println();\n}\n\nvoid wheel1Callback(OSCMessage &msg) {\n  r1 = msg.getFloat(0);\n  servo1.write(r1);\n}\n\nvoid wheel2Callback(OSCMessage &msg) {\n  r2 = msg.getFloat(0);\n  servo2.write(r2);\n}\n\nvoid wheel3Callback(OSCMessage &msg) {\n  r3 = msg.getFloat(0);\n  servo3.write(r3);\n}\n\nvoid setup() {\n  Serial.begin(115200);\n\n  Serial.print(\"[ \");\n  Serial.print(host);\n  Serial.print(\" / \");\n  Serial.print(version);\n  Serial.println(\" ]\");\n\n  pinMode(LED_BUILTIN, OUTPUT);\n  digitalWrite(LED_BUILTIN, LOW);\n\n  WiFi.hostname(host);\n  WiFi.begin();\n\n  WiFiManager wifiManager;\n  wifiManager.setAPStaticIPConfig(IPAddress(192, 168, 1, 1), IPAddress(192, 168, 1, 1), IPAddress(255, 255, 255, 0));\n  wifiManager.autoConnect(host);\n  Serial.println(\"connected\");\n\n  // Used for OSC\n  Udp.begin(inPort);\n\n  servo1.attach(servo1_pin, minUs, maxUs);\n  servo2.attach(servo2_pin, minUs, maxUs);\n  servo3.attach(servo3_pin, minUs, maxUs);\n}\n\nvoid loop() {\n  OSCMessage msg;\n  int size = Udp.parsePacket();\n\n  if (size > 0) {\n    while (size--) {\n      msg.fill(Udp.read());\n    }\n    if (!msg.hasError()) {\n      msg.dispatch(\"/accxyz\", accxyzCallback);\n      msg.dispatch(\"/wheel1\", wheel1Callback);\n      msg.dispatch(\"/wheel2\", wheel2Callback);\n      msg.dispatch(\"/wheel3\", wheel3Callback);\n      msg.dispatch(\"/*/*/*\", printCallback) || msg.dispatch(\"/*/*\", printCallback) || msg.dispatch(\"/*\", printCallback);\n    } else {\n      error = msg.getError();\n      Serial.print(\"error: \");\n      Serial.println(error);\n    }\n  }\n}\n"
  },
  {
    "path": "esp32_3wd_stepper/README.md",
    "content": "# Three-wheel-drive omni-wheel robot platform\n\nThis is an Arduino sketch for an ESP32 (LOLIN32 Lite) controlled three-wheel-drive omni-wheel robot based on 28BYJ-48 stepper motors, similar to the one documented [here][1] and various other places online.\n\nThe robot can rotate in-place and move in any direction using [omni wheels][2]. I ordered 38 mm diameter wheels from [Piscinarobots][3], but they are also available on [AliExpress][4].\n\nTo position the motors and wheels at 120 degree angles, I designed and 3D-printed a circular base. I also designed and 3D-printed the first set of omni-wheels following the dimensions of the commercial 38 mm wheels, but found that those were too rough and not running sideways smoothly enough. Hence I switched to the commercially fabricated wheels.\n\nThe control of the robot is implemented using Open Sound Control (OSC), using the [TouchOSC][5] app on my iPhone. The robot can move by itself along a prespecified list of waypoints. The waypoints are uploaded as tabular CSV file, which also contains a column with the time at which each waypoint is to be reached.\n\n## Coordinate system\n\nThis sketch aligns with the convention used by [WPILib][6] and uses an NWU axes convention (North-West-Up as external reference in the world frame.) In the NWU axes convention, the positive X axis points ahead, the positive Y axis points left, and the positive Z axis points up referenced from the floor. When viewed with each positive axis pointing toward you, counter-clockwise (CCW) is a positive value and clockwise (CW) is a negative value.\n\nConsidering the 3-wheel-drive robot platform, wheel 1 is placed at the front (the \"nose\") and wheel 2 and 3 at the left and right back. The x-axis is pointing fron the center to wheel 1, and the y-axix pointing to the left. The angle theta is positive when (seen from the top) the robot turns to the left (CCW) and negative to the right.\n\n## Settings\n\nIn the settings menu you can change the following parameters.\n\n### Repeat\n\nThis can be 0 (false) or 1 (true) and specifies whether a route should be repeated when the end of the route is reached.\n\n### Absolute\n\nThis can be 0 (false) for relative coordinates) or 1 (true) for absolute coordinates.\n\nWhen configured as relative, you specify the waypoints and movements _relative_ to the robot's position at the moment each route starts. The starting position (and angle) is set to zero and every new route is executed without taking any previously executed routes into consideration.\n\nWhen configured as absolute, you specify all waypoints relative to the robot's initial position and orientation in the room. WHen starting a new route, the latest known position (and orientation) is taken as the starting point.\n\n### Warp\n\nThis is a floating point number by which the time and distances are scaled. By default the warp is 1.00.\n\nIf you specify the warp as 2.00, all positions along the route will be multiplied by a factor of 2x and the time between waypoints will be also be multiplied by a factor of 2x. Consequently, the speed for each segment will remain the same, and the overall route will be 2x larger and will take 2x longer.\n\nIf you specify the warp as 0.5, the overall route will be 2x shorter and faster\n\n### Serial\n\nThis can be 0 (false) or 1 (true) and specifies how much information will be printed on the serial console when the robot is connected to a computer. This is only for debugging.\n\n## Links\n\n[1]: https://github.com/manav20/3-wheel-omni\n[2]: https://en.wikipedia.org/wiki/Omni_wheel\n[3]: https://www.piscinarobots.nl/robots-y-kits/38mm%20(1.5%20inch)%20double%20plastic%20omni%20wheel%20(compatible%20met%20servo%20motor%20)%20-%2014184\n[4]: https://nl.aliexpress.com/item/32478938051.html\n[5]: https://hexler.net\n[6]: https://docs.wpilib.org/en/stable/docs/software/basic-programming/coordinate-system.html\n"
  },
  {
    "path": "esp32_3wd_stepper/blink_led.cpp",
    "content": "#include \"blink_led.h\"\n\nTicker blinker;\n\nenum {\n  LED_ON,\n  LED_OFF,\n  LED_SLOW,\n  LED_MEDIUM,\n  LED_FAST,\n} ledState;\n\nvoid changeState() {\n  digitalWrite(LED, !(digitalRead(LED)));  // Invert the current state of the LED\n}\n\nvoid ledInit() {\n  pinMode(LED, OUTPUT);\n}\n\nvoid ledOn() {\n  if (ledState != LED_ON) {\n    ledState = LED_ON;\n    blinker.detach();\n    digitalWrite(LED, LOW);\n  }\n}\n\nvoid ledOff() {\n  if (ledState != LED_OFF) {\n    ledState = LED_OFF;\n    blinker.detach();\n    digitalWrite(LED, HIGH);\n  }\n}\n\nvoid ledSlow() {\n  if (ledState != LED_SLOW) {\n    ledState = LED_SLOW;\n    blinker.detach();\n    blinker.attach_ms(1000, changeState);\n  }\n}\n\nvoid ledMedium() {\n  if (ledState != LED_MEDIUM) {\n    ledState = LED_MEDIUM;\n    blinker.detach();\n    blinker.attach_ms(250, changeState);\n  }\n}\n\nvoid ledFast() {\n  if (ledState != LED_FAST) {\n    ledState = LED_FAST;\n    blinker.detach();\n    blinker.attach_ms(100, changeState);\n  }\n}\n"
  },
  {
    "path": "esp32_3wd_stepper/blink_led.h",
    "content": "#ifndef _BLINK_LED_H_\n#define _BLINK_LED_H_\n\n#include <Arduino.h>\n#include <Ticker.h>\n\n#define LED 22 // GPIO22 is connected to the builtin LED on the Lolin32 Lite\n\nvoid ledInit(void);\nvoid ledOn(void);\nvoid ledOff(void);\nvoid ledSlow(void);\nvoid ledMedium(void);\nvoid ledFast(void);\n\n#endif\n"
  },
  {
    "path": "esp32_3wd_stepper/data/config.json",
    "content": "{\n\"repeat\":0,\n\"absolute\":0,\n\"warp\": 1,\n\"debug\":0\n}\n"
  },
  {
    "path": "esp32_3wd_stepper/data/hello.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Hello</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n<p>Hello tortoise!</p>\n<img src=\"tortoise.jpg\">\n</body>\n</html>\n"
  },
  {
    "path": "esp32_3wd_stepper/data/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Index</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n<a href=\"/hello.html\">Hello</a></br>\n<a href=\"/monitor.html\">Monitor</a></br>\n<a href=\"/settings.html\">Change settings</a></br>\n<a href=\"/waypoints.html\">Edit waypoints</a></br>\n<a href=\"/reconnect\">Reconnect WiFi</a></br>\n<a href=\"/defaults\">Default settings</a></br>\n<a href=\"/restart\">Restart hardware</a></br>\n</body>\n</html>\n"
  },
  {
    "path": "esp32_3wd_stepper/data/monitor.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Monitor</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n\n<h1>Monitor</h1>\n\nUptime:\n<div id=\"uptime\" name\"uptime\">?</div>\n\nFirmware version:\n<div id=\"version\" name=\"version\">?</div>\n\nMAC address:\n<div id=\"macaddress\" name\"macaddress\">?</div>\n\n<script>\n  async function updateContent() {\n    const response = await fetch(\"json\");\n    const data = await response.json();\n    console.log(data);\n    document.getElementById(\"uptime\").innerHTML = data[\"uptime\"];\n    document.getElementById(\"version\").innerHTML = data[\"version\"];\n    document.getElementById(\"macaddress\").innerHTML = data[\"macaddress\"];\n  }\n  updateContent();\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "esp32_3wd_stepper/data/reload_failure.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Failure</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n<p>Failure</p>\n\n<script type=\"text/javascript\">\n  setTimeout(function(){location=\"/index.html\"},1000);\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "esp32_3wd_stepper/data/reload_success.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Success</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n<p>Success</p>\n\n<script type=\"text/javascript\">\n  setTimeout(function(){location=\"/index.html\"},1000);\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "esp32_3wd_stepper/data/settings.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Settings</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n<h1>Settings</h1>\n\n<form id=\"settings-form\" method=\"post\" action=\"json\">\n    <div class=\"field\">\n        <label for=\"repeat\">repeat:</label><br>\n        <input type=\"text\" id=\"repeat\" name=\"repeat\" value=\"?\" required>\n    </div>\n\n    <div class=\"field\">\n      <label for=\"absolute\">absolute:</label><br>\n      <input type=\"text\" id=\"absolute\" name=\"absolute\" value=\"?\" required>\n    </div>\n\n    <div class=\"field\">\n      <label for=\"warp\">warp:</label><br>\n      <input type=\"text\" id=\"warp\" name=\"warp\" value=\"?\" required>\n    </div>\n\n    <div class=\"field\">\n      <label for=\"debug\">debug:</label><br>\n      <input type=\"text\" id=\"debug\" name=\"debug\" value=\"?\" required>\n    </div>\n\n    <div class=\"field\">\n        <button type=\"submit\">Save</button>\n    </div>\n</form>\n\n<script>\n  async function updateContent() {\n    const response = await fetch(\"json\");\n    const data = await response.json();\n    console.log(data);\n    document.getElementById(\"repeat\").value = data[\"repeat\"];\n    document.getElementById(\"absolute\").value = data[\"absolute\"];\n    document.getElementById(\"warp\").value = data[\"warp\"];\n    document.getElementById(\"debug\").value = data[\"debug\"];\n  }\n  updateContent();\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "esp32_3wd_stepper/data/style.css",
    "content": ".c{\n    text-align: center;\n}\ndiv,input{\n    padding:5px;font-size:1em;\n}\ninput{\n    width:95%;\n}\nbody{\n    text-align: center;font-family:verdana;\n}\nbutton{\n    border:0;border-radius:0.3rem;background-color:#1fa3ec;color:#fff;line-height:2.4rem;font-size:1.2rem;width:100%;\n}\n.q{\n    float: right;width: 64px;text-align: right;\n}\n.l{\n    background: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAALVBMVEX///8EBwfBwsLw8PAzNjaCg4NTVVUjJiZDRUUUFxdiZGSho6OSk5Pg4eFydHTCjaf3AAAAZElEQVQ4je2NSw7AIAhEBamKn97/uMXEGBvozkWb9C2Zx4xzWykBhFAeYp9gkLyZE0zIMno9n4g19hmdY39scwqVkOXaxph0ZCXQcqxSpgQpONa59wkRDOL93eAXvimwlbPbwwVAegLS1HGfZAAAAABJRU5ErkJggg==\") no-repeat left center;background-size: 1em;\n}\n"
  },
  {
    "path": "esp32_3wd_stepper/data/waypoints.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Edit waypoints</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n<h1>Edit waypoints</h1>\n\n<form id=\"waypoints-form\" method=\"post\" action=\"json\">\n    <div class=\"field\">\n      <label for=\"waypoints1\">waypoints route 1:</label><br>\n      <textarea type=\"text\" rows=\"10\" cols=\"80\" id=\"waypoints1\" name=\"waypoints1\" required>?</textarea>\n    </div>\n\n    <div class=\"field\">\n      <label for=\"waypoints2\">waypoints route 2:</label><br>\n      <textarea type=\"text\" rows=\"10\" cols=\"80\" id=\"waypoints2\" name=\"waypoints2\" required>?</textarea>\n    </div>\n\n    <div class=\"field\">\n      <label for=\"waypoints3\">waypoints route 3:</label><br>\n      <textarea type=\"text\" rows=\"10\" cols=\"80\" id=\"waypoints3\" name=\"waypoints3\" required>?</textarea>\n    </div>\n\n    <div class=\"field\">\n      <label for=\"waypoints4\">waypoints route 4:</label><br>\n      <textarea type=\"text\" rows=\"10\" cols=\"80\" id=\"waypoints4\" name=\"waypoints4\" required>?</textarea>\n    </div>\n\n    <div class=\"field\">\n      <label for=\"waypoints5\">waypoints route 5:</label><br>\n      <textarea type=\"text\" rows=\"10\" cols=\"80\" id=\"waypoints5\" name=\"waypoints5\" required>?</textarea>\n    </div>\n\n    <div class=\"field\">\n      <label for=\"waypoints6\">waypoints route 6:</label><br>\n      <textarea type=\"text\" rows=\"10\" cols=\"80\" id=\"waypoints6\" name=\"waypoints6\" required>?</textarea>\n    </div>\n\n    <div class=\"field\">\n      <label for=\"waypoints7\">waypoints route 7:</label><br>\n      <textarea type=\"text\" rows=\"10\" cols=\"80\" id=\"waypoints7\" name=\"waypoints7\" required>?</textarea>\n    </div>\n\n    <div class=\"field\">\n      <label for=\"waypoints8\">waypoints route 8:</label><br>\n      <textarea type=\"text\" rows=\"10\" cols=\"80\" id=\"waypoints8\" name=\"waypoints8\" required>?</textarea>\n    </div>\n\n    <div class=\"field\">\n        <button type=\"submit\">Save</button>\n    </div>\n</form>\n\n<script>\n  async function updateContent() {\n    const response = await fetch(\"json\");\n    const data = await response.json();\n    console.log(data);\n    document.getElementById(\"waypoints1\").value = data[\"waypoints1\"];\n    document.getElementById(\"waypoints2\").value = data[\"waypoints2\"];\n    document.getElementById(\"waypoints3\").value = data[\"waypoints3\"];\n    document.getElementById(\"waypoints4\").value = data[\"waypoints4\"];\n    document.getElementById(\"waypoints5\").value = data[\"waypoints5\"];\n    document.getElementById(\"waypoints6\").value = data[\"waypoints6\"];\n    document.getElementById(\"waypoints7\").value = data[\"waypoints7\"];\n    document.getElementById(\"waypoints8\").value = data[\"waypoints8\"];\n  }\n  updateContent();\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "esp32_3wd_stepper/data/waypoints1.csv",
    "content": "0,0,0,0\n"
  },
  {
    "path": "esp32_3wd_stepper/data/waypoints2.csv",
    "content": "0,0,0,0\n"
  },
  {
    "path": "esp32_3wd_stepper/data/waypoints3.csv",
    "content": "0,0,0,0\n"
  },
  {
    "path": "esp32_3wd_stepper/data/waypoints4.csv",
    "content": "0,0,0,0\n"
  },
  {
    "path": "esp32_3wd_stepper/data/waypoints5.csv",
    "content": "0,0,0,0\n"
  },
  {
    "path": "esp32_3wd_stepper/data/waypoints6.csv",
    "content": "0,0,0,0\n"
  },
  {
    "path": "esp32_3wd_stepper/data/waypoints7.csv",
    "content": "0,0,0,0\n"
  },
  {
    "path": "esp32_3wd_stepper/data/waypoints8.csv",
    "content": "0,0,0,0\n"
  },
  {
    "path": "esp32_3wd_stepper/esp32_3wd_stepper.ino",
    "content": "#include <Arduino.h>\n#include <WiFi.h>\n#include <WiFiManager.h>        // https://github.com/tzapu/WiFiManager\n#include <WiFiUdp.h>\n#include <ESPmDNS.h>\n#include <math.h>\n\n#include \"webinterface.h\"\n#include \"waypoints.h\"\n#include \"parseosc.h\"\n#include \"stepper.h\"\n#include \"blink_led.h\"\n#include \"util.h\"\n\nconst char *host = \"3WD-STEPPER\";\nconst char *version = __DATE__ \" / \" __TIME__;\n\nWebServer server(80);\nWiFiUDP Udp;\n\nStepper wheel1;\nStepper wheel2;\nStepper wheel3;\n\nconst unsigned int inPort = 8000;   // local OSC port for receiving commands\nconst float pd = 0.084252;          // platform diameter, in meter\nconst float wd = 0.038;             // wheel diameter, in meter\nconst float pc = pd * M_PI;         // platform circumference, in meter\nconst float wc = wd * M_PI;         // wheel circumference, in meter\nconst float totalsteps = 2048;      // steps per revolution, see http://www.mjblythe.com/hacks/2016/09/28byj-48-stepper-motor/\nconst float maxsteps = 512;         // maximum steps per seconds, determined experimentally\n\nunsigned long previous = 0;         // timer to integrate the speed over time\nunsigned long feedback = 0;         // timer for feedback on the serial console\n\nfloat x = 0, y = 0, a = 0;          // absolute position (in meter) and angle (in radians)\nfloat vx = 0, vy = 0, va = 0;       // speed in meter per second, and angular speed in radians per second\nfloat r1 = 0, r2 = 0, r3 = 0;       // speed of the stepper motors, in steps per second\n\nfloat target_x = 0, target_y = 0, target_a = 0;\nunsigned long route_starttime = 4294967295;\nunsigned long target_eta = 4294967295;\n\n// when there is an active route, the speed is determined by the distance to the target waypoint\n// when there is no route, the speed is determined manually by the user\nint current_route = -1;\nint current_segment = -1;\nbool route_pause = false;\n\n/********************************************************************************/\n\nvoid startRoute(int route) {\n  unsigned long now = millis();\n\n  // stop the current movement\n  vx = 0;\n  vy = 0;\n  va = 0;\n\n  if (!config.absolute) {\n    // set the current position and orientation as zero\n    x = 0;\n    y = 0;\n    a = 0;\n  }\n\n  // read the waypoints from the SPIFFS filesystem\n  parseWaypoints(route);\n\n  // insert the current position and orientation as the starting point\n  waypoints_t.insert(waypoints_t.begin(), 0);\n  waypoints_x.insert(waypoints_x.begin(), x);\n  waypoints_y.insert(waypoints_y.begin(), y);\n  waypoints_a.insert(waypoints_a.begin(), a);\n\n  // start the new route\n  route_starttime = now;\n  current_route = route;\n  route_pause = false;\n\n  if (config.debug)\n    printWaypoints(route);\n} // startRoute\n\n/********************************************************************************/\n\nvoid pauseRoute() {\n  // pause the current route\n  vx = 0;\n  vy = 0;\n  va = 0;\n  route_pause = true;\n} // pauseRoute\n\n/********************************************************************************/\n\nvoid resumeRoute() {\n  // resume the current route\n  route_pause = false;\n} // resumeRoute\n\n/********************************************************************************/\n\nvoid stopRoute() {\n  // stop the current route\n  vx = 0;\n  vy = 0;\n  va = 0;\n  current_route = -1;\n  route_starttime = 4294967295;\n  target_eta = 4294967295;\n  route_pause = false;\n} // stopRoute\n\n/********************************************************************************/\n\nvoid resetRoute() {\n  // stop the current route and reset the current position to zero\n  stopRoute();\n  x = 0;\n  y = 0;\n  a = 0;\n} // resetRoute\n\n/********************************************************************************/\n\nvoid updatePosition() {\n  unsigned long now = millis();\n  if (route_pause) {\n    // shift the time so that we remain in the same state\n    // this is as if the time has stopped\n    route_starttime += (now - previous);\n    target_eta += (now - previous);\n  }\n  else {\n    // integrate the speed over time to keep track of the current position and angle\n    x += vx * (now - previous) / 1000;\n    y += vy * (now - previous) / 1000;\n    a += va * (now - previous) / 1000;\n  }\n  previous = now;\n}  // updatePosition\n\n/********************************************************************************/\n\nvoid updateTarget() {\n  unsigned long now = millis();\n\n  if (current_route < 0) {\n    // when there is no route, the speed is determined manually by the user\n    // hence the current position is as good a target as anywhere else\n    target_x = x;\n    target_y = y;\n    target_a = a;\n    target_eta = 4294967295;\n  }\n  else {\n    // when there is an active route, the speed is determined by the distance to the target waypoint\n    // determine the segment or leg that we are currently on\n    current_segment = -1;\n\n    int n = waypoints_t.size();\n    unsigned long route_endtime = route_starttime + 1000 * waypoints_t.at(n - 1);\n\n    if (now < route_starttime) {\n      // the route has not yet started\n      // stay where we are\n      target_x = x;\n      target_y = y;\n      target_a = a;\n      target_eta = 4294967295;\n    }\n    else if (now >= route_endtime) {\n      // the route has finished\n      if (config.repeat) {\n        // start the same route again\n        startRoute(current_route);\n      }\n      else {\n        // stop the route\n        current_route = -1;\n        vx = 0;\n        vy = 0;\n        va = 0;\n        // stay where we are\n        target_x = x;\n        target_y = y;\n        target_a = a;\n        target_eta = 4294967295;\n      }\n    }\n    else {\n      // determine the segment or leg that we are currently on\n      // the N waypoints are connected by N-1 segments\n      for (int i = 0; i < (n - 1); i++) {\n        unsigned long segment_starttime = route_starttime + 1000 * waypoints_t.at(i);\n        unsigned long segment_endtime   = route_starttime + 1000 * waypoints_t.at(i + 1);\n        if (now >= segment_starttime && now < segment_endtime) {\n          current_segment = i + 1;\n          target_x = waypoints_x.at(i + 1);\n          target_y = waypoints_y.at(i + 1);\n          target_a = waypoints_a.at(i + 1);\n          target_eta = segment_endtime;\n          break;\n        }\n      } // for all segments\n    } // if route_starttime\n  } // if current_route\n\n} // updateTarget\n\n/********************************************************************************/\n\nvoid updateSpeed() {\n  unsigned long now = millis();\n\n  if (current_route < 0) {\n    // when there is no route, the speed is determined manually by the user\n  }\n  else if (route_pause) {\n    // don't move if the route is paused\n    vx = 0;\n    vy = 0;\n    va = 0;\n  }\n  else {\n    // when there is an active route, the speed is determined by the distance to the target waypoint\n    if (current_segment > 0) {\n      // determine the speed needed to reach the target at the desired time\n      float dt = 0.001 * (target_eta - now); // in seconds\n      if (dt > 0) {\n        vx = (target_x - x) / dt;\n        vy = (target_y - y) / dt;\n        va = (target_a - a) / dt;\n      }\n      else {\n        // infinite or negative speed is not allowed\n        vx = 0;\n        vy = 0;\n        va = 0;\n      }\n    }\n    else {\n      // the current segment could not be determined\n      vx = 0;\n      vy = 0;\n      va = 0;\n    } // if current_segment\n  } // if current_route\n\n} // updateSpeed\n\n/********************************************************************************/\n\nvoid updateWheels() {\n  // convert the speed from world-coordinates into the rotation speed of the motors\n  r1 = sin(a               ) * vx / wc - cos(a               ) * vy / wc - va * (pc / wc) / (M_PI * 2);\n  r2 = sin(a + M_PI * 2 / 3) * vx / wc - cos(a + M_PI * 2 / 3) * vy / wc - va * (pc / wc) / (M_PI * 2);\n  r3 = sin(a - M_PI * 2 / 3) * vx / wc - cos(a - M_PI * 2 / 3) * vy / wc - va * (pc / wc) / (M_PI * 2);\n\n  // convert from rotations per second into steps per second\n  r1 *= totalsteps;\n  r2 *= totalsteps;\n  r3 *= totalsteps;\n\n  // the stepper motors cannot rotate at more than ~512 steps/second\n  float r = maxOfThree(abs(r1), abs(r2), abs(r3));\n  if (r > maxsteps) {\n    float reduction = r / maxsteps;\n    Serial.print(\"reducing speed by factor \");\n    Serial.println(reduction);\n    vx /= reduction;\n    vy /= reduction;\n    va /= reduction;\n    r1 /= reduction;\n    r2 /= reduction;\n    r3 /= reduction;\n  }\n\n  if (r1 == 0 && r2 == 0 && r3 == 0) {\n    // save energy by switching the motors off\n    wheel1.powerOff();\n    wheel2.powerOff();\n    wheel3.powerOff();\n    ledOn();\n  }\n  else {\n    wheel1.powerOn();\n    wheel2.powerOn();\n    wheel3.powerOn();\n    ledSlow();\n  }\n\n  // set the motor speed in steps per seconds\n  wheel1.spin(r1);\n  wheel2.spin(r2);\n  wheel3.spin(r3);\n}  // updateWheels\n\n/********************************************************************************/\n\nvoid printDebug() {\n  unsigned long now = millis();\n\n  // do not print more often than once every 500ms\n  if ((now - feedback) > 500) {\n\n    Serial.print(\"current = \");\n    Serial.print(current_route);\n    Serial.print(\", \");\n    Serial.print(current_segment);\n\n    Serial.print(\", time = \");\n    Serial.print(now);\n    Serial.print(\", \");\n    Serial.print(target_eta);\n\n    Serial.print(\", position = \");\n    Serial.print(x);\n    Serial.print(\", \");\n    Serial.print(y);\n    Serial.print(\", \");\n    Serial.print(a * 180 / M_PI); // in degrees\n\n    Serial.print(\", target = \");\n    Serial.print(target_x);\n    Serial.print(\", \");\n    Serial.print(target_y);\n    Serial.print(\", \");\n    Serial.print(target_a * 180 / M_PI); // in degrees\n\n    Serial.print(\", speed = \");\n    Serial.print(vx);\n    Serial.print(\", \");\n    Serial.print(vy);\n    Serial.print(\", \");\n    Serial.print(va * 180 / M_PI); // in degrees per second\n\n    Serial.print(\", wheel speed = \");\n    Serial.print(r1);\n    Serial.print(\", \");\n    Serial.print(r2);\n    Serial.print(\", \");\n    Serial.print(r3);\n\n    Serial.println();\n    feedback = now;\n  }\n}\n/********************************************************************************/\n\nvoid setup() {\n  Serial.begin(115200);\n\n  Serial.print(\"[ \");\n  Serial.print(host);\n  Serial.print(\" / \");\n  Serial.print(version);\n  Serial.println(\" ]\");\n\n  ledInit();\n  ledFast();\n\n  // The SPIFFS file system contains the html and javascript code for the web interface, and the \"config.json\" file\n  if (!SPIFFS.begin()) {\n    Serial.println(\"Error opening file system\");\n    while (true);\n  }\n  else {\n    Serial.println(\"Opened file system\");\n    loadConfig();\n    printConfig();\n  }\n\n  WiFi.hostname(host);\n  WiFi.begin();\n\n  printMacAddress();\n\n  WiFiManager wifiManager;\n  wifiManager.setAPStaticIPConfig(IPAddress(192, 168, 1, 1), IPAddress(192, 168, 1, 1), IPAddress(255, 255, 255, 0));\n  wifiManager.autoConnect(host);\n  Serial.println(\"Connected to WiFi\");\n\n  // incoming port for OSC messages\n  Udp.begin(inPort);\n\n  // this serves all URIs that can be resolved to a file on the SPIFFS filesystem\n  server.onNotFound(handleNotFound);\n\n  server.on(\"/\", HTTP_GET, []() {\n    handleRedirect(\"/index.html\");\n  });\n\n  server.on(\"/defaults\", HTTP_GET, []() {\n    Serial.println(\"handleDefaults\");\n    handleStaticFile(\"/reload_success.html\");\n    defaultConfig();\n    printConfig();\n    saveConfig();\n    server.close();\n    server.stop();\n    ESP.restart();\n  });\n\n  server.on(\"/reconnect\", HTTP_GET, []() {\n    Serial.println(\"handleReconnect\");\n    handleStaticFile(\"/reload_success.html\");\n    server.close();\n    server.stop();\n    delay(5000);\n    WiFiManager wifiManager;\n    wifiManager.resetSettings();\n    wifiManager.setAPStaticIPConfig(IPAddress(192, 168, 1, 1), IPAddress(192, 168, 1, 1), IPAddress(255, 255, 255, 0));\n    wifiManager.startConfigPortal(host);\n    server.begin();\n    if (WiFi.status() == WL_CONNECTED)\n      Serial.println(\"WiFi status ok\");\n  });\n\n  server.on(\"/restart\", HTTP_GET, []() {\n    Serial.println(\"handleRestart\");\n    handleStaticFile(\"/reload_success.html\");\n    server.close();\n    server.stop();\n    SPIFFS.end();\n    delay(5000);\n    ESP.restart();\n  });\n\n  server.on(\"/json\", HTTP_PUT, handleJSON);\n\n  server.on(\"/json\", HTTP_POST, handleJSON);\n\n  server.on(\"/json\", HTTP_GET, [] {\n    JsonDocument root;\n    N_CONFIG_TO_JSON(repeat, \"repeat\");\n    N_CONFIG_TO_JSON(absolute, \"absolute\");\n    N_CONFIG_TO_JSON(warp, \"warp\");\n    N_CONFIG_TO_JSON(debug, \"debug\");\n    root[\"version\"] = version;\n    root[\"uptime\"] = long(millis() / 1000);\n    root[\"macaddress\"] = getMacAddress();\n    root[\"waypoints1\"] = loadWaypoints(1);\n    root[\"waypoints2\"] = loadWaypoints(2);\n    root[\"waypoints3\"] = loadWaypoints(3);\n    root[\"waypoints4\"] = loadWaypoints(4);\n    root[\"waypoints5\"] = loadWaypoints(5);\n    root[\"waypoints6\"] = loadWaypoints(6);\n    root[\"waypoints7\"] = loadWaypoints(7);\n    root[\"waypoints8\"] = loadWaypoints(8);\n    String str;\n    serializeJson(root, str);\n    server.setContentLength(str.length());\n    server.send(200, \"application/json\", str);\n  });\n\n  // start the web server\n  server.begin();\n\n  // announce the hostname and web server through zeroconf\n  MDNS.begin(host);\n  MDNS.addService(\"http\", \"tcp\", 80);\n\n  // connect the timer to the stepper motor driver pins\n  wheel1.begin(14, 27, 26, 25);\n  wheel2.begin(13, 15, 2, 4);\n  wheel3.begin(16, 17, 5, 18);\n\n  Serial.println(\"Setup done\");\n} // setup\n\n/********************************************************************************/\n\nvoid loop() {\n  updatePosition();       // update the current location\n  updateTarget();         // update the target location\n  updateSpeed();          // update the world speed\n  updateWheels();         // update the speed of the wheels\n\n  if (config.debug)\n    printDebug();\n\n  parseOSC();             // parse the incoming OSC messages\n  server.handleClient();  // handle webserver requests, note that this may disrupt other processes\n} // loop\n"
  },
  {
    "path": "esp32_3wd_stepper/parseosc.cpp",
    "content": "#include \"parseosc.h\"\n\n/********************************************************************************/\n\nvoid parseOSC() {\n  int size = Udp.parsePacket();\n\n  if (size > 0) {\n    OSCMessage msg;\n    OSCBundle bundle;\n    OSCErrorCode error;\n\n    char c = Udp.peek();\n    if (c == '#') {\n      // it is a bundle, these are sent by TouchDesigner\n      while (size--) {\n        bundle.fill(Udp.read());\n      }\n      if (!bundle.hasError()) {\n        bundle.dispatch(\"/*\", printCallback) || bundle.dispatch(\"/*/*\", printCallback) || bundle.dispatch(\"/*/*/*\", printCallback);\n        bundle.dispatch(\"/route/1\",     route1Callback);\n        bundle.dispatch(\"/route/2\",     route2Callback);\n        bundle.dispatch(\"/route/3\",     route3Callback);\n        bundle.dispatch(\"/route/4\",     route4Callback);\n        bundle.dispatch(\"/route/5\",     route5Callback);\n        bundle.dispatch(\"/route/6\",     route6Callback);\n        bundle.dispatch(\"/route/7\",     route7Callback);\n        bundle.dispatch(\"/route/8\",     route8Callback);\n        bundle.dispatch(\"/pause\",       pauseCallback);\n        bundle.dispatch(\"/resume\",      resumeCallback);\n        bundle.dispatch(\"/stop\",        stopCallback);\n        bundle.dispatch(\"/reset\",       resetCallback);\n        bundle.dispatch(\"/adjust/x\",    dxCallback);\n        bundle.dispatch(\"/adjust/y\",    dyCallback);\n        bundle.dispatch(\"/adjust/a\",    daCallback);\n        bundle.dispatch(\"/manual/x\",    xCallback);\n        bundle.dispatch(\"/manual/y\",    yCallback);\n        bundle.dispatch(\"/manual/a\",    aCallback);\n        bundle.dispatch(\"/manual/xy/1\", xyCallback);      // this is for the 2D panel in touchOSC\n        bundle.dispatch(\"/accxyz\",      accxyzCallback);  // this is for the accelerometer in touchOSC\n      } else {\n        error = bundle.getError();\n        Serial.print(\"error: \");\n        Serial.println(error);\n      }\n    } else if (c == '/') {\n      // it is a message, these are sent by touchOSC\n      while (size--) {\n        msg.fill(Udp.read());\n      }\n      if (!msg.hasError()) {\n        msg.dispatch(\"/*\", printCallback) || msg.dispatch(\"/*/*\", printCallback) || msg.dispatch(\"/*/*/*\", printCallback);\n        msg.dispatch(\"/route/1\",     route1Callback);\n        msg.dispatch(\"/route/2\",     route2Callback);\n        msg.dispatch(\"/route/3\",     route3Callback);\n        msg.dispatch(\"/route/4\",     route4Callback);\n        msg.dispatch(\"/route/5\",     route5Callback);\n        msg.dispatch(\"/route/6\",     route6Callback);\n        msg.dispatch(\"/route/7\",     route7Callback);\n        msg.dispatch(\"/route/8\",     route8Callback);\n        msg.dispatch(\"/pause\",       pauseCallback);\n        msg.dispatch(\"/resume\",      resumeCallback);\n        msg.dispatch(\"/stop\",        stopCallback);\n        msg.dispatch(\"/reset\",       resetCallback);\n        msg.dispatch(\"/adjust/x\",    dxCallback);\n        msg.dispatch(\"/adjust/y\",    dyCallback);\n        msg.dispatch(\"/adjust/a\",    daCallback);\n        msg.dispatch(\"/manual/x\",    xCallback);\n        msg.dispatch(\"/manual/y\",    yCallback);\n        msg.dispatch(\"/manual/a\",    aCallback);\n        msg.dispatch(\"/manual/xy/1\", xyCallback);      // this is for the 2D panel in touchOSC\n        msg.dispatch(\"/accxyz\",      accxyzCallback);  // this is for the accelerometer in touchOSC\n      } else {\n        error = msg.getError();\n        Serial.print(\"error: \");\n        Serial.println(error);\n      }\n    }\n  }  // if size\n}  // parseOSC\n\n/********************************************************************************/\n\nvoid printCallback(OSCMessage &msg) {\n  Serial.print(msg.getAddress());\n  Serial.print(\" : \");\n\n  for (int i = 0; i < msg.size(); i++) {\n    if (msg.isInt(i)) {\n      Serial.print(msg.getInt(i));\n    } else if (msg.isFloat(i)) {\n      Serial.print(msg.getFloat(i));\n    } else if (msg.isDouble(i)) {\n      Serial.print(msg.getDouble(i));\n    } else if (msg.isBoolean(i)) {\n      Serial.print(msg.getBoolean(i));\n    } else if (msg.isString(i)) {\n      char buffer[256];\n      msg.getString(i, buffer);\n      Serial.print(buffer);\n    } else {\n      Serial.print(\"?\");\n    }\n\n    if (i < (msg.size() - 1)) {\n      Serial.print(\", \");  // there are more to come\n    }\n  }\n  Serial.println();\n}\n\n/********************************************************************************/\n\nvoid route1Callback(OSCMessage &msg) {\n  if (msg.getInt(0))\n    startRoute(1);\n}\n\nvoid route2Callback(OSCMessage &msg) {\n  if (msg.getInt(0))\n    startRoute(2);\n}\n\nvoid route3Callback(OSCMessage &msg) {\n  if (msg.getInt(0))\n    startRoute(3);\n}\n\nvoid route4Callback(OSCMessage &msg) {\n  if (msg.getInt(0))\n    startRoute(4);\n}\n\nvoid route5Callback(OSCMessage &msg) {\n  if (msg.getInt(0))\n    startRoute(5);\n}\n\nvoid route6Callback(OSCMessage &msg) {\n  if (msg.getInt(0))\n    startRoute(6);\n}\n\nvoid route7Callback(OSCMessage &msg) {\n  if (msg.getInt(0))\n    startRoute(7);\n}\n\nvoid route8Callback(OSCMessage &msg) {\n  if (msg.getInt(0))\n    startRoute(8);\n}\n\nvoid pauseCallback(OSCMessage &msg) {\n  if (msg.getInt(0))\n    pauseRoute();\n}\n\nvoid resumeCallback(OSCMessage &msg) {\n  if (msg.getInt(0))\n    resumeRoute();\n}\n\nvoid stopCallback(OSCMessage &msg) {\n  if (msg.getInt(0))\n    stopRoute();\n}\n\nvoid resetCallback(OSCMessage & msg) {\n  if (msg.getInt(0))\n    resetRoute();\n}\n\nvoid xCallback(OSCMessage & msg) {\n  vx = msg.getFloat(0);\n}\n\nvoid yCallback(OSCMessage & msg) {\n  vy = msg.getFloat(0);\n}\n\nvoid aCallback(OSCMessage & msg) {\n  va = msg.getFloat(0);\n}\n\nvoid dxCallback(OSCMessage & msg) {\n  // adjust the current position with 1 cm\n  // the negative adjustment will cause the robot to move towards the positive side\n  x -= msg.getFloat(0) * 0.01;\n}\n\nvoid dyCallback(OSCMessage & msg) {\n  // adjust the current position with 1 cm\n  // the negative adjustment will cause the robot to move towards the positive side\n  y -= msg.getFloat(0) * 0.01;\n}\n\nvoid daCallback(OSCMessage & msg) {\n  // adjust the current angle with 5 degrees (in radians)\n  // the negative adjustment will cause the robot to move towards a positive angle\n  a -= msg.getFloat(0) * 5.00 * PI / 180;\n}\n\nvoid xyCallback(OSCMessage & msg) {\n  vx = msg.getFloat(0);\n  vy = msg.getFloat(1);\n}\n\nvoid accxyzCallback(OSCMessage & msg) {\n  // values are between -1 and 1, scale them to a more appropriate speed of 3 cm/s\n  vx = msg.getFloat(0) * 0.03;\n  vy = msg.getFloat(1) * 0.03;\n}\n"
  },
  {
    "path": "esp32_3wd_stepper/parseosc.h",
    "content": "#ifndef _PARSEOSC_H_\n#define _PARSEOSC_H_\n\n#include <WiFiUdp.h>\n#include <OSCMessage.h>         // https://github.com/CNMAT/OSC\n#include <OSCBundle.h>\n\n// these are defined in the main sketch\nextern float vx, vy, va;\nextern float x, y, a;\nextern WiFiUDP Udp;\nvoid startRoute(int);\nvoid pauseRoute();\nvoid resumeRoute();\nvoid stopRoute();\nvoid resetRoute();\n\n// these are defined in the corresponding CPP file\nvoid parseOSC();\nvoid printCallback(OSCMessage &);\nvoid route1Callback(OSCMessage &);\nvoid route2Callback(OSCMessage &);\nvoid route3Callback(OSCMessage &);\nvoid route4Callback(OSCMessage &);\nvoid route5Callback(OSCMessage &);\nvoid route6Callback(OSCMessage &);\nvoid route7Callback(OSCMessage &);\nvoid route8Callback(OSCMessage &);\nvoid pauseCallback(OSCMessage &);\nvoid resumeCallback(OSCMessage &);\nvoid stopCallback(OSCMessage &);\nvoid resetCallback(OSCMessage &);\nvoid xCallback(OSCMessage &);\nvoid yCallback(OSCMessage &);\nvoid aCallback(OSCMessage &);\nvoid dxCallback(OSCMessage &);\nvoid dyCallback(OSCMessage &);\nvoid daCallback(OSCMessage &);\nvoid xyCallback(OSCMessage &);\nvoid accxyzCallback(OSCMessage &);\n\n#endif\n"
  },
  {
    "path": "esp32_3wd_stepper/stepper.cpp",
    "content": "#include \"stepper.h\"\n\n// This implements the ESP32 control for the 28BYJ-48 motor and the ULN2003 driver board.\n//\n// It is specifically written to keep up to four stepper motors spinning at a constant speed\n// and it uses ESP32 hardware timers for precise timing.\n//\n// The https://docs.arduino.cc/libraries/stepper/ was not convenient to set and \n// keep the stepper motor runnning at a specific speed.\n//\n// The https://github.com/bblanchon/ArduinoContinuousStepper library was not \n// precise enough to simultaneously control three stepper motors and to keep \n// my 3-wheel robot platform with omni wheels going straight.\n//\n// See https://espressif-docs.readthedocs-hosted.com/projects/arduino-esp32/en/latest/api/timer.html\n\n/******************************************************************************/\n\nStepper::Stepper() {\n  // constructor\n}\n\n/******************************************************************************/\n\nvoid Stepper::begin(unsigned int _in1, int _in2, int _in3, int _in4) {\n  in1 = _in1;\n  in2 = _in2;\n  in3 = _in3;\n  in4 = _in4;\n\n  pinMode(in1, OUTPUT);\n  pinMode(in2, OUTPUT);\n  pinMode(in3, OUTPUT);\n  pinMode(in4, OUTPUT);\n\n  timer = timerBegin(frequency);\n  timerAttachInterruptArg(timer, &onTimer, (void*)this);\n}\n\n/******************************************************************************/\n\nvoid Stepper::spin(int speed) {\n  if (speed > 0) {\n    direction = +1;\n    timerAlarm(timer, frequency / (+speed * 2), true, 0); // speed times two since half-steps\n  }\n  else if (speed < 0) {\n    direction = -1;\n    timerAlarm(timer, frequency / (-speed * 2), true, 0); // speed times two since half-steps\n  }\n  else if (speed == 0) {\n    direction = 0;\n    timerAlarm(timer, 0, false, 0); // execute the alarm once, do not repeat\n  }\n}\n\n/******************************************************************************/\n\nvoid Stepper::powerOn() {\n  power = true;\n}\n\nvoid Stepper::powerOff() {\n  power = false;\n}\n\n/******************************************************************************/\n\nvoid IRAM_ATTR Stepper::onTimer(void* arg) {\n  // This is to work around the error that ISO C++ forbids taking the address of\n  // an unqualified or parenthesized non-static member function to form a pointer\n  // to member function.\n  Stepper* pObj = (Stepper*) arg;\n  pObj->doStep();\n}\n\n/******************************************************************************/\n\nvoid IRAM_ATTR Stepper::doStep() {\n  if (power) {\n    digitalWrite(in1, bitRead(halfstep[step], 0));\n    digitalWrite(in2, bitRead(halfstep[step], 1));\n    digitalWrite(in3, bitRead(halfstep[step], 2));\n    digitalWrite(in4, bitRead(halfstep[step], 3));\n\n    // increment or decrement, depending on the direction\n    step += direction;\n\n    // wrap between 0 and 7\n    if (step < 0)\n      step = 7;\n    else if (step > 7)\n      step = 0;\n  }\n  else {\n    digitalWrite(in1, 0);\n    digitalWrite(in2, 0);\n    digitalWrite(in3, 0);\n    digitalWrite(in4, 0);\n  }\n}\n"
  },
  {
    "path": "esp32_3wd_stepper/stepper.h",
    "content": "#ifndef _STEPPER_H_\n#define _STEPPER_H_\n\n#include <Arduino.h>\n\nclass Stepper {\n  public:\n    Stepper();\n    void begin(unsigned int in1, int in2, int in3, int in4);\n    void spin(int speed);\n    void powerOn();\n    void powerOff();\n\n  private:\n    unsigned int frequency = 10000000;\n    unsigned int halfstep[8] = {0b1000, 0b1100, 0b0100, 0b0110, 0b0010, 0b0011, 0b0001, 0b1001};  // half-step drive\n    unsigned int in1, in2, in3, in4;  // the ESP32 pins connected to the ULN2003 driver board\n    int step = 0;                     // from 0 to 7, wraps around\n    int direction = 1;                // +1 or -1\n    bool power = true;                // true or false\n    hw_timer_t *timer = NULL;\n\n    static void IRAM_ATTR onTimer(void* arg);   // this calls doStep\n    void IRAM_ATTR doStep();                    // this implements the actual stepping\n};\n\n#endif // _STEPPER_H_\n"
  },
  {
    "path": "esp32_3wd_stepper/util.cpp",
    "content": "#include \"util.h\"\n\n/********************************************************************************/\n\nvoid printMacAddress() {\n  uint8_t baseMac[6];\n  esp_err_t ret = esp_wifi_get_mac(WIFI_IF_STA, baseMac);\n  if (ret == ESP_OK) {\n    Serial.printf(\"%02x:%02x:%02x:%02x:%02x:%02x\\n\",\n                  baseMac[0], baseMac[1], baseMac[2],\n                  baseMac[3], baseMac[4], baseMac[5]);\n  } else {\n    Serial.println(\"Failed to read MAC address\");\n  }\n}\n\n\n/********************************************************************************/\n\nString getMacAddress() {\n  uint8_t baseMac[6];\n  String s;\n  esp_err_t ret = esp_wifi_get_mac(WIFI_IF_STA, baseMac);\n  if (ret != ESP_OK) {\n    s += \"unknown\";\n  }\n  else {\n    for (byte i = 0; i < 6; ++i) {\n      char buf[3];\n      sprintf(buf, \"%02X\", baseMac[i]);\n      s += buf;\n      if (i < 5) s += ':';\n    }\n  }\n  return s;\n}\n\n\n/********************************************************************************/\n\nfloat maxOfThree(float a, float b, float c) {\n  if (a >= b && a >= c) {\n    return a;\n  }\n  else if (b >= a && b >= c) {\n    return b;\n  }\n  else {\n    return c;\n  }\n}\n"
  },
  {
    "path": "esp32_3wd_stepper/util.h",
    "content": "#ifndef _UTIL_H_\n#define _UTIL_H_\n\n#include <Arduino.h>\n#include <WiFi.h>\n#include <esp_wifi.h>\n\nvoid printMacAddress(void);\nString getMacAddress(void);\nfloat maxOfThree(float, float, float);\n\n#endif // _UTIL_H_\n"
  },
  {
    "path": "esp32_3wd_stepper/waypoints.cpp",
    "content": "#include \"waypoints.h\"\n\n// read the sequence of waypoints from the CSV file on SPIFFS\n//\n// the file should have no header\n// each line should consist of four comma-separated values: time, x, y, theta\n// time is incremental and in seconds\n// x and y are the position in meter\n// theta is the angle in degrees in the file, it is converted to radians in memory\n\nvector<float> waypoints_t;\nvector<float> waypoints_x;\nvector<float> waypoints_y;\nvector<float> waypoints_a;\n\n/****************************************************************************************/\n\nvoid printWaypoints(int route) {\n  Serial.print(\"printWaypoints \");\n  Serial.println(route);\n\n  for (int i = 0; i < waypoints_t.size(); i++) {\n    Serial.print(waypoints_t.at(i));\n    Serial.print(\",\");\n    Serial.print(waypoints_x.at(i));\n    Serial.print(\",\");\n    Serial.print(waypoints_y.at(i));\n    Serial.print(\",\");\n    Serial.print(waypoints_a.at(i));\n    Serial.print(\"\\n\");\n  }\n  return;\n}\n\n/****************************************************************************************/\n\nsize_t saveWaypoints(int route, String s) {\n  Serial.print(\"saveWaypoints \");\n  Serial.println(route);\n\n  if (!SPIFFS.begin(true)) {\n    Serial.println(\"An error has occurred while mounting SPIFFS\");\n    return 0;\n  }\n\n  String filename = String(\"/waypoints\") + String(route) + String(\".csv\");\n  File file = SPIFFS.open(filename, \"w\");\n  if (!file) {\n    Serial.print(\"Failed to open \");\n    Serial.print(filename);\n    Serial.println(\" for writing\");\n  }\n  \n  size_t bytes = file.print(s);\n  return bytes;\n}\n\n/****************************************************************************************/\n\nString loadWaypoints(int route) {\n  Serial.print(\"loadWaypoints \");\n  Serial.println(route);\n\n  if (!SPIFFS.begin(true)) {\n    Serial.println(\"An error has occurred while mounting SPIFFS\");\n    String s = \"\";\n    return s;\n  }\n\n  String filename = String(\"/waypoints\") + String(route) + String(\".csv\");\n  File file = SPIFFS.open(filename, \"r\");\n  if (!file) {\n    Serial.print(\"Failed to open \");\n    Serial.print(filename);\n    Serial.println(\" for reading\");\n  }\n\n  String s;\n  while (file.available())\n  {\n    char charRead = file.read();\n    s += charRead;\n  }\n  return s;\n}\n\n/****************************************************************************************/\n\nvoid parseWaypoints(int route) {\n  Serial.print(\"parseWaypoints \");\n  Serial.println(route);\n\n  if (!SPIFFS.begin(true)) {\n    Serial.println(\"An error has occurred while mounting SPIFFS\");\n    return;\n  }\n\n  String filename = String(\"/waypoints\") + String(route) + String(\".csv\");\n  File file = SPIFFS.open(filename, \"r\");\n  if (!file) {\n    Serial.print(\"Failed to open \");\n    Serial.print(filename);\n    Serial.println(\" for reading\");\n  }\n\n  waypoints_t.clear();\n  waypoints_x.clear();\n  waypoints_y.clear();\n  waypoints_a.clear();\n\n  vector<String> v;\n  while (file.available()) {\n    String s = file.readStringUntil('\\n');\n    v.push_back(s);\n\n    int ind1 = s.indexOf(',');                    // finds location of 1st ,\n    int ind2 = s.indexOf(',', ind1 + 1);          // finds location of 2nd ,\n    int ind3 = s.indexOf(',', ind2 + 1);          // finds location of 3rd ,\n    int ind4 = s.length();                        // end of the string\n\n    String tok1 = s.substring(0, ind1);           //captures 1st value\n    String tok2 = s.substring(ind1 + 1, ind2);    //captures 2nd value\n    String tok3 = s.substring(ind2 + 1, ind3);    //captures 3rd value\n    String tok4 = s.substring(ind3 + 1, ind4);    //captures 4th value\n\n    // convert the values to float and store each in their own vector\n    waypoints_t.push_back(atof(tok1.c_str()));\n    waypoints_x.push_back(atof(tok2.c_str()));\n    waypoints_y.push_back(atof(tok3.c_str()));\n    waypoints_a.push_back(atof(tok4.c_str()) * M_PI / 180);  // convert from degrees to radians\n  }\n\n  file.close();\n  return;\n}\n"
  },
  {
    "path": "esp32_3wd_stepper/waypoints.h",
    "content": "#ifndef _WAYPOINTS_H_\n#define _WAYPOINTS_H_\n\n#include <FS.h>\n#include <SPIFFS.h>\n#include <vector>\n\nusing namespace std;\n\nextern vector<float> waypoints_t;\nextern vector<float> waypoints_x;\nextern vector<float> waypoints_y;\nextern vector<float> waypoints_a;\n\n// the following functions take an integer as first input, which is the route number\n// the route number is between 1 to 8, so you can use 8 predefined sets of waypoints\n\nvoid printWaypoints(int);            // print the waypoints on the serial console\nvoid parseWaypoints(int);            // read the waypoints from the file and represent as vectors\nString loadWaypoints(int);           // read the waypoints from the file and return as a string\nsize_t saveWaypoints(int, String);   // save the waypoints (as string) to the file\n\n#endif // _WAYPOINTS_H_\n"
  },
  {
    "path": "esp32_3wd_stepper/webinterface.cpp",
    "content": "#include \"webinterface.h\"\n#include \"waypoints.h\"\n\nConfig config;\nextern WebServer server;\n\n/***************************************************************************/\n\nstatic String getContentType(const String& path) {\n  if (path.endsWith(\".html\")) return \"text/html\";\n  else if (path.endsWith(\".htm\")) return \"text/html\";\n  else if (path.endsWith(\".css\")) return \"text/css\";\n  else if (path.endsWith(\".txt\")) return \"text/plain\";\n  else if (path.endsWith(\".js\")) return \"application/javascript\";\n  else if (path.endsWith(\".png\")) return \"image/png\";\n  else if (path.endsWith(\".gif\")) return \"image/gif\";\n  else if (path.endsWith(\".jpg\")) return \"image/jpeg\";\n  else if (path.endsWith(\".jpeg\")) return \"image/jpeg\";\n  else if (path.endsWith(\".ico\")) return \"image/x-icon\";\n  else if (path.endsWith(\".svg\")) return \"image/svg+xml\";\n  else if (path.endsWith(\".xml\")) return \"text/xml\";\n  else if (path.endsWith(\".pdf\")) return \"application/pdf\";\n  else if (path.endsWith(\".zip\")) return \"application/zip\";\n  else if (path.endsWith(\".gz\")) return \"application/x-gzip\";\n  else if (path.endsWith(\".json\")) return \"application/json\";\n  return \"application/octet-stream\";\n}\n\n/***************************************************************************/\n\nbool defaultConfig() {\n  Serial.println(\"defaultConfig\");\n\n  config.repeat = 0;\n  config.absolute = 0;\n  config.warp = 1.000;\n  config.debug = 0;\n  return true;\n}\n\nbool loadConfig() {\n  Serial.println(\"loadConfig\");\n\n  File configFile = SPIFFS.open(\"/config.json\", \"r\");\n  if (!configFile) {\n    Serial.println(\"Failed to open config file\");\n    return false;\n  }\n\n  size_t size = configFile.size();\n  if (size > 1024) {\n    Serial.println(\"Config file size is too large\");\n    return false;\n  }\n\n  std::unique_ptr<char[]> buf(new char[size]);\n  configFile.readBytes(buf.get(), size);\n  configFile.close();\n\n  JsonDocument root;\n  DeserializationError error = deserializeJson(root, buf.get());\n  if (error) {\n    Serial.println(\"Failed to parse config file\");\n    return false;\n  }\n\n  N_JSON_TO_CONFIG(repeat, \"repeat\");\n  N_JSON_TO_CONFIG(absolute, \"absolute\");\n  N_JSON_TO_CONFIG(warp, \"warp\");\n  N_JSON_TO_CONFIG(debug, \"debug\");\n\n  return true;\n}\n\nbool saveConfig() {\n  Serial.println(\"saveConfig\");\n  JsonDocument root;\n\n  N_CONFIG_TO_JSON(repeat, \"repeat\");\n  N_CONFIG_TO_JSON(absolute, \"absolute\");\n  N_CONFIG_TO_JSON(warp, \"warp\");\n  N_CONFIG_TO_JSON(debug, \"debug\");\n\n  File configFile = SPIFFS.open(\"/config.json\", \"w\");\n  if (!configFile) {\n    Serial.println(\"Failed to open config file for writing\");\n    return false;\n  } else {\n    Serial.println(\"Writing to config file\");\n    serializeJson(root, configFile);\n    configFile.close();\n    return true;\n  }\n}\n\nvoid printConfig() {\n  Serial.print(\"repeat = \");\n  Serial.println(config.repeat);\n  Serial.print(\"absolute = \");\n  Serial.println(config.absolute);\n  Serial.print(\"warp = \");\n  Serial.println(config.warp);\n  Serial.print(\"debug = \");\n  Serial.println(config.debug);\n}\n\nvoid printRequest() {\n  String message = \"HTTP Request\\n\\n\";\n  message += \"URI: \";\n  message += server.uri();\n  message += \"\\nMethod: \";\n  message += (server.method() == HTTP_GET) ? \"GET\" : \"POST\";\n  message += \"\\nHeaders: \";\n  message += server.headers();\n  message += \"\\n\";\n  for (uint8_t i = 0; i < server.headers(); i++) {\n    message += \" \" + server.headerName(i) + \": \" + server.header(i) + \"\\n\";\n  }\n  message += \"\\nArguments: \";\n  message += server.args();\n  message += \"\\n\";\n  for (uint8_t i = 0; i < server.args(); i++) {\n    message += \" \" + server.argName(i) + \": \" + server.arg(i) + \"\\n\";\n  }\n  Serial.println(message);\n}\n\nvoid handleNotFound() {\n  Serial.print(\"handleNotFound: \");\n  Serial.println(server.uri());\n  if (SPIFFS.exists(server.uri())) {\n    handleStaticFile(server.uri());\n  } else {\n    String message = \"File Not Found\\n\\n\";\n    message += \"URI: \";\n    message += server.uri();\n    message += \"\\nMethod: \";\n    message += (server.method() == HTTP_GET) ? \"GET\" : \"POST\";\n    message += \"\\nArguments: \";\n    message += server.args();\n    message += \"\\n\";\n    for (uint8_t i = 0; i < server.args(); i++) {\n      message += \" \" + server.argName(i) + \": \" + server.arg(i) + \"\\n\";\n    }\n    server.setContentLength(message.length());\n    server.send(404, \"text/plain\", message);\n  }\n}\n\nvoid handleRedirect(const char* filename) {\n  handleRedirect((String)filename);\n}\n\nvoid handleRedirect(String filename) {\n  Serial.println(\"handleRedirect: \" + filename);\n  server.sendHeader(\"Location\", filename, true);\n  server.setContentLength(0);\n  server.send(302, \"text/plain\", \"\");\n}\n\nbool handleStaticFile(const char* path) {\n  return handleStaticFile((String)path);\n}\n\nbool handleStaticFile(String path) {\n  Serial.println(\"handleStaticFile: \" + path);\n  String contentType = getContentType(path);  // Get the MIME type\n  if (SPIFFS.exists(path)) {                  // If the file exists\n    File file = SPIFFS.open(path, \"r\");       // Open it\n    server.setContentLength(file.size());\n    server.streamFile(file, contentType);  // And send it to the client\n    file.close();                          // Then close the file again\n    return true;\n  }\n  Serial.println(\"\\tFile Not Found\");\n  return false;  // If the file doesn't exist, return false\n}\n\nvoid handleJSON() {\n  // this gets called in response to either a PUT or a POST\n  Serial.println(\"handleJSON\");\n  printRequest();\n\n  if (server.hasArg(\"repeat\") || server.hasArg(\"absolute\") || server.hasArg(\"warp\") || server.hasArg(\"debug\")\n      || server.hasArg(\"waypoints1\")\n      || server.hasArg(\"waypoints2\")\n      || server.hasArg(\"waypoints3\")\n      || server.hasArg(\"waypoints4\")\n      || server.hasArg(\"waypoints5\")\n      || server.hasArg(\"waypoints6\")\n      || server.hasArg(\"waypoints7\")\n      || server.hasArg(\"waypoints8\")) {\n    // the body is key1=val1&key2=val2&key3=val3 and the ESP8266Webserver has already parsed it\n    N_KEYVAL_TO_CONFIG(repeat, \"repeat\");\n    N_KEYVAL_TO_CONFIG(absolute, \"absolute\");\n    N_KEYVAL_TO_CONFIG(warp, \"warp\");\n    N_KEYVAL_TO_CONFIG(debug, \"debug\");\n\n    if (server.hasArg(\"waypoints1\"))\n      saveWaypoints(1, server.arg(\"waypoints1\"));\n    if (server.hasArg(\"waypoints2\"))\n      saveWaypoints(2, server.arg(\"waypoints2\"));\n    if (server.hasArg(\"waypoints3\"))\n      saveWaypoints(3, server.arg(\"waypoints3\"));\n    if (server.hasArg(\"waypoints4\"))\n      saveWaypoints(4, server.arg(\"waypoints4\"));\n    if (server.hasArg(\"waypoints5\"))\n      saveWaypoints(5, server.arg(\"waypoints5\"));\n    if (server.hasArg(\"waypoints6\"))\n      saveWaypoints(6, server.arg(\"waypoints6\"));\n    if (server.hasArg(\"waypoints7\"))\n      saveWaypoints(7, server.arg(\"waypoints7\"));\n    if (server.hasArg(\"waypoints8\"))\n      saveWaypoints(8, server.arg(\"waypoints8\"));\n\n    handleStaticFile(\"/reload_success.html\");\n\n  } else if (server.hasArg(\"plain\")) {\n    // parse the body as JSON object\n    JsonDocument root;\n    DeserializationError error = deserializeJson(root, server.arg(\"plain\"));\n    if (error) {\n      Serial.println(\"either here\");\n      handleStaticFile(\"/reload_failure.html\");\n      return;\n    }\n    N_JSON_TO_CONFIG(repeat, \"repeat\");\n    N_JSON_TO_CONFIG(absolute, \"absolute\");\n    N_JSON_TO_CONFIG(warp, \"warp\");\n    N_JSON_TO_CONFIG(debug, \"debug\");\n\n    if (root.containsKey(\"waypoints1\"))\n      saveWaypoints(1, root[\"waypoints1\"]);\n    if (root.containsKey(\"waypoints2\"))\n      saveWaypoints(2, root[\"waypoints2\"]);\n    if (root.containsKey(\"waypoints3\"))\n      saveWaypoints(3, root[\"waypoints3\"]);\n    if (root.containsKey(\"waypoints4\"))\n      saveWaypoints(4, root[\"waypoints4\"]);\n    if (root.containsKey(\"waypoints5\"))\n      saveWaypoints(5, root[\"waypoints5\"]);\n    if (root.containsKey(\"waypoints6\"))\n      saveWaypoints(6, root[\"waypoints6\"]);\n    if (root.containsKey(\"waypoints7\"))\n      saveWaypoints(7, root[\"waypoints7\"]);\n    if (root.containsKey(\"waypoints8\"))\n      saveWaypoints(8, root[\"waypoints8\"]);\n\n    handleStaticFile(\"/reload_success.html\");\n\n  } else {\n    handleStaticFile(\"/reload_failure.html\");\n    return;  // do not save the configuration\n  }\n\n  saveConfig();\n  printConfig();\n}\n"
  },
  {
    "path": "esp32_3wd_stepper/webinterface.h",
    "content": "#ifndef _WEBINTERFACE_H_\n#define _WEBINTERFACE_H_\n\n#include <Arduino.h>\n#include <WebServer.h>\n#include <ArduinoJson.h>  // https://arduinojson.org\n#include <FS.h>\n#include <SPIFFS.h>\n\n#ifndef ARDUINOJSON_VERSION\n#error ArduinoJson version 7 not found, please include ArduinoJson.h in your .ino file\n#endif\n\n#if ARDUINOJSON_VERSION_MAJOR != 7\n#error ArduinoJson version 7 is required\n#endif\n\n/* these are for numbers */\n#define N_JSON_TO_CONFIG(x, y)   { if (root.containsKey(y)) { config.x = root[y]; } }\n#define N_CONFIG_TO_JSON(x, y)   { root[y] = config.x; }\n#define N_KEYVAL_TO_CONFIG(x, y) { if (server.hasArg(y))    { String str = server.arg(y); config.x = str.toFloat(); } }\n\n/* these are for strings */\n#define S_JSON_TO_CONFIG(x, y)   { if (root.containsKey(y)) { strcpy(config.x, root[y]); } }\n#define S_CONFIG_TO_JSON(x, y)   { root.set(y, config.x); }\n#define S_KEYVAL_TO_CONFIG(x, y) { if (server.hasArg(y))    { String str = server.arg(y); strcpy(config.x, str.c_str()); } }\n\nstruct Config {\n  int repeat;\n  int absolute;\n  float warp;\n  int debug;\n};\n\nextern Config config;\n\nbool defaultConfig(void);\nbool loadConfig(void);\nbool saveConfig(void);\nvoid printConfig(void);\n\nvoid handleNotFound(void);\nvoid handleRedirect(String);\nvoid handleRedirect(const char *);\nbool handleStaticFile(String);\nbool handleStaticFile(const char *);\nvoid handleJSON();\n\n#endif // _WEBINTERFACE_H_\n"
  },
  {
    "path": "esp32_config_webinterface_v5/README.md",
    "content": "# Overview\n\nThis is a demonstration and test sketch for configuring persistent options via the web interface. This strategy is used in a number of my functional ESP32 and ESP8266 sketches. It consists of a `settings.json` file in the SPIFFS filesystem and a `settings.html` file with some javascript.\n\n**This specific version is for ArduinoJson 5, there are other examples for version 6 and 7.**\n\nThe settings can be changed in the webbrowser at http://192.168.1.xxx/settings and clicking \"Send\". This results in `Webserver` parsing the arguments and placing the variables in the header, but also places the query string in the body.\n\nThe following results in `Webserver` parsing the arguments and placing the variables in the header.\n\n    curl -X POST 'http://192.168.1.xxx/json?var1=11&var2=22&var3=33'\n    curl -X PUT  'http://192.168.1.xxx/json?var1=11&var2=22&var3=33'\n\nThe following is not parsed, but only places the JSON data in the body.\n\n    curl -X POST http://192.168.1.xxx/json -d '{\"var1\":11,\"var2\":22,\"var3\":33}'\n    curl -X PUT  http://192.168.1.xxx/json -d '{\"var1\":11,\"var2\":22,\"var3\":33}'\n\n## SPIFFS for static files\n\nYou should not only write the firmware to the ESP32 module, but also the static content for the web interface. The html, css and javascript files located in the data directory should be written to the SPIFS filesystem on the ESP32. See https://github.com/me-no-dev/arduino-esp32fs-plugin.\n\nYou will get a \"file not found\" error if the firmware cannot access the data files.\n\n## Arduino ESP32 filesystem uploader\n\nThis Arduino sketch includes a `data` directory with a number of files that should be uploaded to the ESP32 using the [SPIFFS filesystem uploader](https://github.com/me-no-dev/arduino-esp32fs-plugin) tool. At the moment (Feb 2024) the Arduino 2.x IDE does *not* support the SPIFFS filesystem uploader plugin. You have to use the Arduino 1.8.x IDE (recommended), or the command line utilities for uploading the data.\n"
  },
  {
    "path": "esp32_config_webinterface_v5/data/config.json",
    "content": "{\n\"var1\":10,\n\"var2\":20,\n\"var3\":30,\n\"var4\":40\n}\n"
  },
  {
    "path": "esp32_config_webinterface_v5/data/hello.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Hello</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n<p>Hello panda!</p>\n<img src=\"panda.jpg\">\n</body>\n</html>\n"
  },
  {
    "path": "esp32_config_webinterface_v5/data/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Index</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n<a href=\"/hello.html\">Hello</a></br>\n<a href=\"/monitor.html\">Monitor</a></br>\n<a href=\"/settings.html\">Change settings</a></br>\n<a href=\"/reconnect\">Reconnect WiFi</a></br>\n<a href=\"/defaults\">Default settings</a></br>\n<a href=\"/restart\">Restart hardware</a></br>\n</body>\n</html>\n"
  },
  {
    "path": "esp32_config_webinterface_v5/data/monitor.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Monitor</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n\n<h1>Monitor</h1>\n\nFirmware version:\n<div id=\"version\" name=\"version\">?</div>\n\nUptime:\n<div id=\"uptime\" name\"uptime\">?</div>\n\n<script>\n  async function updateContent() {\n    const response = await fetch(\"json\");\n    const data = await response.json();\n    console.log(data);\n    document.getElementById(\"version\").innerHTML = data[\"version\"];\n    document.getElementById(\"uptime\").innerHTML = data[\"uptime\"];\n  }\n  updateContent();\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "esp32_config_webinterface_v5/data/reload_failure.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Failure</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n<p>Failure</p>\n\n<script type=\"text/javascript\">\n  setTimeout(function(){location=\"/index.html\"},1000);\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "esp32_config_webinterface_v5/data/reload_success.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Success</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n<p>Success</p>\n\n<script type=\"text/javascript\">\n  setTimeout(function(){location=\"/index.html\"},1000);\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "esp32_config_webinterface_v5/data/settings.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Settings</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n<h1>Settings</h1>\n\n<form id=\"settings-form\" method=\"post\" action=\"json\">\n    <div class=\"field\">\n        <label for=\"var1\">var1:</label>\n        <input type=\"text\" id=\"var1\" name=\"var1\" value=\"?\" required>\n    </div>\n\n    <div class=\"field\">\n        <label for=\"var2\">var2:</label>\n        <input type=\"text\" id=\"var2\" name=\"var2\" value=\"?\" required>\n    </div>\n\n    <div class=\"field\">\n        <label for=\"var2\">var3:</label>\n        <input type=\"text\" id=\"var3\" name=\"var3\" value=\"?\" required>\n    </div>\n\n    <div class=\"field\">\n        <button type=\"submit\">Save</button>\n    </div>\n</form>\n\n<script>\n  async function updateContent() {\n    const response = await fetch(\"json\");\n    const data = await response.json();\n    console.log(data);\n    document.getElementById(\"var1\").value = data[\"var1\"];\n    document.getElementById(\"var2\").value = data[\"var2\"];\n    document.getElementById(\"var3\").value = data[\"var3\"];\n  }\n  updateContent();\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "esp32_config_webinterface_v5/data/style.css",
    "content": ".c{\n    text-align: center;\n}\ndiv,input{\n    padding:5px;font-size:1em;\n}\ninput{\n    width:95%;\n}\nbody{\n    text-align: center;font-family:verdana;\n}\nbutton{\n    border:0;border-radius:0.3rem;background-color:#1fa3ec;color:#fff;line-height:2.4rem;font-size:1.2rem;width:100%;\n}\n.q{\n    float: right;width: 64px;text-align: right;\n}\n.l{\n    background: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAALVBMVEX///8EBwfBwsLw8PAzNjaCg4NTVVUjJiZDRUUUFxdiZGSho6OSk5Pg4eFydHTCjaf3AAAAZElEQVQ4je2NSw7AIAhEBamKn97/uMXEGBvozkWb9C2Zx4xzWykBhFAeYp9gkLyZE0zIMno9n4g19hmdY39scwqVkOXaxph0ZCXQcqxSpgQpONa59wkRDOL93eAXvimwlbPbwwVAegLS1HGfZAAAAABJRU5ErkJggg==\") no-repeat left center;background-size: 1em;\n}\n"
  },
  {
    "path": "esp32_config_webinterface_v5/esp32_config_webinterface_v5.ino",
    "content": "#if not defined(ESP32)\n#error This is a sketch for an ESP32 board, like the NodeMCU 32S, LOLIN32 Lite, or the Adafruit Huzzah32\n#endif\n\n#include <Arduino.h>\n#include <ArduinoJson.h>  // https://arduinojson.org\n#include <WiFi.h>\n#include <WiFiManager.h>  // https://github.com/tzapu/WiFiManager\n#include <WiFiClient.h>\n#include <WebServer.h>\n#include <ESPmDNS.h>\n#include <FS.h>\n#include <SPIFFS.h>\n\n#ifndef ARDUINOJSON_VERSION\n#error ArduinoJson version 5 not found, please include ArduinoJson.h in your .ino file\n#endif\n\n#if ARDUINOJSON_VERSION_MAJOR != 5\n#error ArduinoJson version 5 is required\n#endif\n\nWebServer server(80);\nconst char* host = \"ESP32\";\nconst char* version = __DATE__ \" / \" __TIME__;\nint var1, var2, var3;\n\nstatic String getContentType(const String& path) {\n  if (path.endsWith(\".html\")) return \"text/html\";\n  else if (path.endsWith(\".htm\")) return \"text/html\";\n  else if (path.endsWith(\".css\")) return \"text/css\";\n  else if (path.endsWith(\".txt\")) return \"text/plain\";\n  else if (path.endsWith(\".js\")) return \"application/javascript\";\n  else if (path.endsWith(\".png\")) return \"image/png\";\n  else if (path.endsWith(\".gif\")) return \"image/gif\";\n  else if (path.endsWith(\".jpg\")) return \"image/jpeg\";\n  else if (path.endsWith(\".jpeg\")) return \"image/jpeg\";\n  else if (path.endsWith(\".ico\")) return \"image/x-icon\";\n  else if (path.endsWith(\".svg\")) return \"image/svg+xml\";\n  else if (path.endsWith(\".xml\")) return \"text/xml\";\n  else if (path.endsWith(\".pdf\")) return \"application/pdf\";\n  else if (path.endsWith(\".zip\")) return \"application/zip\";\n  else if (path.endsWith(\".gz\")) return \"application/x-gzip\";\n  else if (path.endsWith(\".json\")) return \"application/json\";\n  return \"application/octet-stream\";\n}\n\nbool defaultConfig() {\n  Serial.println(\"defaultConfig\");\n  var1 = 11;\n  var2 = 22;\n  var3 = 33;\n  return true;\n}\n\nbool loadConfig() {\n  Serial.println(\"loadConfig\");\n  File configFile = SPIFFS.open(\"/config.json\", \"r\");\n  if (!configFile) {\n    Serial.println(\"Failed to open config file\");\n    return false;\n  }\n\n  size_t size = configFile.size();\n  if (size > 1024) {\n    Serial.println(\"Config file size is too large\");\n    return false;\n  }\n\n  std::unique_ptr<char[]> buf(new char[size]);\n  configFile.readBytes(buf.get(), size);\n\n  StaticJsonBuffer<200> jsonBuffer;\n  JsonObject& root = jsonBuffer.parseObject(buf.get());\n\n  if (!root.success()) {\n    Serial.println(\"Failed to parse config file\");\n    return false;\n  }\n\n  if (root.containsKey(\"var1\"))\n    var1 = root[\"var1\"];\n  if (root.containsKey(\"var2\"))\n    var2 = root[\"var2\"];\n  if (root.containsKey(\"var3\"))\n    var3 = root[\"var3\"];\n\n  return true;\n}\n\nbool saveConfig() {\n  Serial.println(\"saveConfig\");\n  printConfig();\n  StaticJsonBuffer<200> jsonBuffer;\n  JsonObject& root = jsonBuffer.createObject();\n  root[\"var1\"] = var1;\n  root[\"var2\"] = var2;\n  root[\"var3\"] = var3;\n\n  File configFile = SPIFFS.open(\"/config.json\", \"w\");\n  if (!configFile) {\n    Serial.println(\"Failed to open config file for writing\");\n    return false;\n  } else {\n    root.printTo(configFile);\n    return true;\n  }\n}\n\nvoid printConfig() {\n  Serial.print(\"var1 = \");\n  Serial.println(var1);\n  Serial.print(\"var2 = \");\n  Serial.println(var2);\n  Serial.print(\"var3 = \");\n  Serial.println(var3);\n}\n\nvoid printRequest() {\n  String message = \"HTTP Request\\n\\n\";\n  message += \"URI: \";\n  message += server.uri();\n  message += \"\\nMethod: \";\n  message += (server.method() == HTTP_GET) ? \"GET\" : \"POST\";\n  message += \"\\nHeaders: \";\n  message += server.headers();\n  message += \"\\n\";\n  for (uint8_t i = 0; i < server.headers(); i++) {\n    message += \" \" + server.headerName(i) + \": \" + server.header(i) + \"\\n\";\n  }\n  message += \"\\nArguments: \";\n  message += server.args();\n  message += \"\\n\";\n  for (uint8_t i = 0; i < server.args(); i++) {\n    message += \" \" + server.argName(i) + \": \" + server.arg(i) + \"\\n\";\n  }\n  Serial.println(message);\n}\n\nvoid handleNotFound() {\n  Serial.print(\"handleNotFound: \");\n  Serial.println(server.uri());\n  if (SPIFFS.exists(server.uri())) {\n    handleStaticFile(server.uri());\n  } else {\n    String message = \"File Not Found\\n\\n\";\n    message += \"URI: \";\n    message += server.uri();\n    message += \"\\nMethod: \";\n    message += (server.method() == HTTP_GET) ? \"GET\" : \"POST\";\n    message += \"\\nArguments: \";\n    message += server.args();\n    message += \"\\n\";\n    for (uint8_t i = 0; i < server.args(); i++) {\n      message += \" \" + server.argName(i) + \": \" + server.arg(i) + \"\\n\";\n    }\n    server.setContentLength(message.length());\n    server.send(404, \"text/plain\", message);\n  }\n}\n\nvoid handleRedirect(const char* filename) {\n  handleRedirect((String)filename);\n}\n\nvoid handleRedirect(String filename) {\n  Serial.println(\"handleRedirect: \" + filename);\n  server.sendHeader(\"Location\", filename, true);\n  server.setContentLength(0);\n  server.send(302, \"text/plain\", \"\");\n}\n\nbool handleStaticFile(const char* path) {\n  return handleStaticFile((String)path);\n}\n\nbool handleStaticFile(String path) {\n  Serial.println(\"handleStaticFile: \" + path);\n  String contentType = getContentType(path);  // Get the MIME type\n  if (SPIFFS.exists(path)) {                  // If the file exists\n    File file = SPIFFS.open(path, \"r\");       // Open it\n    server.setContentLength(file.size());\n    server.streamFile(file, contentType);     // And send it to the client\n    file.close();                             // Then close the file again\n    return true;\n  } else {\n    Serial.println(\"\\tFile Not Found\");\n    return false;  // If the file doesn't exist, return false\n  }\n}\n\nvoid handleJSON() {\n  // this gets called in response to either a PUT or a POST\n  Serial.println(\"handleJSON\");\n  printRequest();\n\n  if (server.hasArg(\"var1\") || server.hasArg(\"var2\") || server.hasArg(\"var3\")) {\n    // the body is key1=val1&key2=val2&key3=val3 and the Webserver has already parsed it\n    String str;\n    if (server.hasArg(\"var1\")) {\n      str = server.arg(\"var1\");\n      var1 = str.toInt();\n    }\n    if (server.hasArg(\"var2\")) {\n      str = server.arg(\"var2\");\n      var2 = str.toInt();\n    }\n    if (server.hasArg(\"var3\")) {\n      str = server.arg(\"var3\");\n      var3 = str.toInt();\n    }\n    handleStaticFile(\"/reload_success.html\");\n  } else if (server.hasArg(\"plain\")) {\n    // parse the body as JSON object\n    String body = server.arg(\"plain\");\n    StaticJsonBuffer<200> jsonBuffer;\n    JsonObject& root = jsonBuffer.parseObject(body);\n    if (!root.success()) {\n      handleStaticFile(\"/reload_failure.html\");\n      return;\n    }\n    if (root.containsKey(\"var1\"))\n      var1 = root[\"var1\"];\n    if (root.containsKey(\"var2\"))\n      var2 = root[\"var2\"];\n    if (root.containsKey(\"var3\"))\n      var3 = root[\"var3\"];\n    handleStaticFile(\"/reload_success.html\");\n  } else {\n    handleStaticFile(\"/reload_failure.html\");\n    return; // do not save the configuration\n  }\n  \n  saveConfig();\n}\n\nvoid setup() {\n  Serial.begin(115200);\n  while (!Serial) {\n    ;\n  }\n  Serial.println(\"\");\n  Serial.println(\"setup\");\n\n  // The SPIFFS file system contains the config.json, and the html and javascript code for the web interface\n  SPIFFS.begin();\n  loadConfig();\n  printConfig();\n\n  WiFiManager wifiManager;\n  wifiManager.setAPStaticIPConfig(IPAddress(192, 168, 1, 1), IPAddress(192, 168, 1, 1), IPAddress(255, 255, 255, 0));\n  wifiManager.autoConnect(host);\n  Serial.println(\"connected\");\n\n  // this serves all URIs that can be resolved to a file on the SPIFFS filesystem\n  server.onNotFound(handleNotFound);\n\n  server.on(\"/\", HTTP_GET, []() {\n    handleRedirect(\"/index.html\");\n  });\n\n  server.on(\"/defaults\", HTTP_GET, []() {\n    Serial.println(\"handleDefaults\");\n    handleStaticFile(\"/reload_success.html\");\n    defaultConfig();\n    printConfig();\n    saveConfig();\n    server.close();\n    server.stop();\n    delay(5000);\n    ESP.restart();\n  });\n\n  server.on(\"/reconnect\", HTTP_GET, []() {\n    Serial.println(\"handleReconnect\");\n    handleStaticFile(\"/reload_success.html\");\n    server.close();\n    server.stop();\n    delay(5000);\n    WiFiManager wifiManager;\n    wifiManager.resetSettings();\n    wifiManager.setAPStaticIPConfig(IPAddress(192, 168, 1, 1), IPAddress(192, 168, 1, 1), IPAddress(255, 255, 255, 0));\n    wifiManager.startConfigPortal(host);\n    server.begin();\n    if (WiFi.status() == WL_CONNECTED)\n      Serial.println(\"WiFi status ok\");\n  });\n\n  server.on(\"/restart\", HTTP_GET, []() {\n    Serial.println(\"handleRestart\");\n    handleStaticFile(\"/reload_success.html\");\n    server.close();\n    server.stop();\n    SPIFFS.end();\n    delay(5000);\n    ESP.restart();\n  });\n\n  server.on(\"/json\", HTTP_PUT, handleJSON);\n\n  server.on(\"/json\", HTTP_POST, handleJSON);\n\n  server.on(\"/json\", HTTP_GET, [] {\n    StaticJsonBuffer<200> jsonBuffer;\n    JsonObject& root = jsonBuffer.createObject();\n    root[\"var1\"] = var1;\n    root[\"var2\"] = var2;\n    root[\"var3\"] = var3;\n    root[\"version\"] = version;\n    root[\"uptime\"] = long(millis() / 1000);\n    String content;\n    root.printTo(content);\n    server.send(200, \"application/json\", content);\n  });\n\n  // ask server to track these headers\n  const char* headerkeys[] = { \"User-Agent\", \"Content-Type\" };\n  size_t headerkeyssize = sizeof(headerkeys) / sizeof(char*);\n  server.collectHeaders(headerkeys, headerkeyssize);\n\n  // start the web server\n  server.begin();\n\n  // announce the hostname and web server through zeroconf\n  MDNS.begin(host);\n  MDNS.addService(\"http\", \"tcp\", 80);\n\n  Serial.println(\"Setup done\");\n}\n\nvoid loop() {\n  // put your main code here, to run repeatedly\n  server.handleClient();\n  delay(10);  // in milliseconds\n}\n"
  },
  {
    "path": "esp32_config_webinterface_v6/README.md",
    "content": "# Overview\n\nThis is a demonstration and test sketch for configuring persistent options via the web interface. This strategy is used in a number of my functional ESP32 and ESP8266 sketches. It consists of a `settings.json` file in the SPIFFS filesystem and a `settings.html` file with some javascript.\n\n**This specific version is for ArduinoJson 6, there are other examples for version 5 and 7.**\n\nThe settings can be changed in the webbrowser at http://192.168.1.xxx/settings and clicking \"Send\". This results in `Webserver` parsing the arguments and placing the variables in the header, but also places the query string in the body.\n\nThe following also results in `Webserver` parsing the arguments and placing the variables in the header.\n\n    curl -X POST 'http://192.168.1.xxx/json?var1=11&var2=22&var3=33'\n    curl -X PUT  'http://192.168.1.xxx/json?var1=11&var2=22&var3=33'\n\nThe following is not parsed, but only places the JSON data in the body.\n\n    curl -X POST http://192.168.1.xxx/json -d '{\"var1\":11,\"var2\":22,\"var3\":33}'\n    curl -X PUT  http://192.168.1.xxx/json -d '{\"var1\":11,\"var2\":22,\"var3\":33}'\n\n## SPIFFS for static files\n\nYou should not only write the firmware to the ESP32 module, but also the static content for the web interface. The html, css and javascript files located in the data directory should be written to the SPIFS filesystem on the ESP32. See https://github.com/me-no-dev/arduino-esp32fs-plugin.\n\nYou will get a \"file not found\" error if the firmware cannot access the data files.\n\n## Arduino ESP32 filesystem uploader\n\nThis Arduino sketch includes a `data` directory with a number of files that should be uploaded to the ESP32 using the [SPIFFS filesystem uploader](https://github.com/me-no-dev/arduino-esp32fs-plugin) tool. At the moment (Feb 2024) the Arduino 2.x IDE does *not* support the SPIFFS filesystem uploader plugin. You have to use the Arduino 1.8.x IDE (recommended), or the command line utilities for uploading the data.\n"
  },
  {
    "path": "esp32_config_webinterface_v6/data/config.json",
    "content": "{\n\"var1\":10,\n\"var2\":20,\n\"var3\":30\n}\n"
  },
  {
    "path": "esp32_config_webinterface_v6/data/hello.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Hello</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n<p>Hello panda!</p>\n<img src=\"panda.jpg\">\n</body>\n</html>\n"
  },
  {
    "path": "esp32_config_webinterface_v6/data/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Index</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n<a href=\"/hello.html\">Hello</a></br>\n<a href=\"/monitor.html\">Monitor</a></br>\n<a href=\"/settings.html\">Change settings</a></br>\n<a href=\"/reconnect\">Reconnect WiFi</a></br>\n<a href=\"/defaults\">Default settings</a></br>\n<a href=\"/restart\">Restart hardware</a></br>\n</body>\n</html>\n"
  },
  {
    "path": "esp32_config_webinterface_v6/data/monitor.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Monitor</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n\n<h1>Monitor</h1>\n\nFirmware version:\n<div id=\"version\" name=\"version\">?</div>\n\nUptime:\n<div id=\"uptime\" name\"uptime\">?</div>\n\n<script>\n  async function updateContent() {\n    const response = await fetch(\"json\");\n    const data = await response.json();\n    console.log(data);\n    document.getElementById(\"version\").innerHTML = data[\"version\"];\n    document.getElementById(\"uptime\").innerHTML = data[\"uptime\"];\n  }\n  updateContent();\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "esp32_config_webinterface_v6/data/reload_failure.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Failure</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n<p>Failure</p>\n\n<script type=\"text/javascript\">\n  setTimeout(function(){location=\"/index.html\"},1000);\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "esp32_config_webinterface_v6/data/reload_success.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Success</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n<p>Success</p>\n\n<script type=\"text/javascript\">\n  setTimeout(function(){location=\"/index.html\"},1000);\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "esp32_config_webinterface_v6/data/settings.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Settings</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n<h1>Settings</h1>\n\n<form id=\"settings-form\" method=\"post\" action=\"json\">\n    <div class=\"field\">\n        <label for=\"var1\">var1:</label>\n        <input type=\"text\" id=\"var1\" name=\"var1\" value=\"?\" required>\n    </div>\n\n    <div class=\"field\">\n        <label for=\"var2\">var2:</label>\n        <input type=\"text\" id=\"var2\" name=\"var2\" value=\"?\" required>\n    </div>\n\n    <div class=\"field\">\n        <label for=\"var2\">var3:</label>\n        <input type=\"text\" id=\"var3\" name=\"var3\" value=\"?\" required>\n    </div>\n\n    <div class=\"field\">\n        <button type=\"submit\">Save</button>\n    </div>\n</form>\n\n<script>\n  async function updateContent() {\n    const response = await fetch(\"json\");\n    const data = await response.json();\n    console.log(data);\n    document.getElementById(\"var1\").value = data[\"var1\"];\n    document.getElementById(\"var2\").value = data[\"var2\"];\n    document.getElementById(\"var3\").value = data[\"var3\"];\n  }\n  updateContent();\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "esp32_config_webinterface_v6/data/style.css",
    "content": ".c{\n    text-align: center;\n}\ndiv,input{\n    padding:5px;font-size:1em;\n}\ninput{\n    width:95%;\n}\nbody{\n    text-align: center;font-family:verdana;\n}\nbutton{\n    border:0;border-radius:0.3rem;background-color:#1fa3ec;color:#fff;line-height:2.4rem;font-size:1.2rem;width:100%;\n}\n.q{\n    float: right;width: 64px;text-align: right;\n}\n.l{\n    background: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAALVBMVEX///8EBwfBwsLw8PAzNjaCg4NTVVUjJiZDRUUUFxdiZGSho6OSk5Pg4eFydHTCjaf3AAAAZElEQVQ4je2NSw7AIAhEBamKn97/uMXEGBvozkWb9C2Zx4xzWykBhFAeYp9gkLyZE0zIMno9n4g19hmdY39scwqVkOXaxph0ZCXQcqxSpgQpONa59wkRDOL93eAXvimwlbPbwwVAegLS1HGfZAAAAABJRU5ErkJggg==\") no-repeat left center;background-size: 1em;\n}\n"
  },
  {
    "path": "esp32_config_webinterface_v6/esp32_config_webinterface_v6.ino",
    "content": "#if not defined(ESP32)\n#error This is a sketch for an ESP32 board, like the NodeMCU 32S, LOLIN32 Lite, or the Adafruit Huzzah32\n#endif\n\n#include <Arduino.h>\n#include <ArduinoJson.h>  // https://arduinojson.org\n#include <WiFi.h>\n#include <WiFiManager.h>  // https://github.com/tzapu/WiFiManager\n#include <WiFiClient.h>\n#include <WebServer.h>\n#include <ESPmDNS.h>\n#include <FS.h>\n#include <SPIFFS.h>\n\n#include \"webinterface.h\"\n\n/*********************************************************************************/\n\nconst char* host = \"ESP32\";\nconst char* version = __DATE__ \" / \" __TIME__;\n\nWebServer server(80);\nWiFiManager wifiManager;\n\n/*********************************************************************************/\n\nvoid setup() {\n\n  Serial.begin(115200);\n  while (!Serial) {\n    ;\n  }\n  Serial.println(\"Setup starting\");\n\n  // The SPIFFS file system contains the config.json, and the html and javascript code for the web interface\n  SPIFFS.begin();\n  loadConfig();\n  printConfig();\n\n  WiFiManager wifiManager;\n  wifiManager.setAPStaticIPConfig(IPAddress(192, 168, 1, 1), IPAddress(192, 168, 1, 1), IPAddress(255, 255, 255, 0));\n  wifiManager.autoConnect(host);\n  Serial.println(\"connected\");\n\n  // this serves all URIs that can be resolved to a file on the SPIFFS filesystem\n  server.onNotFound(handleNotFound);\n\n  server.on(\"/\", HTTP_GET, []() {\n    handleRedirect(\"/index.html\");\n  });\n\n  server.on(\"/defaults\", HTTP_GET, []() {\n    Serial.println(\"handleDefaults\");\n    handleStaticFile(\"/reload_success.html\");\n    defaultConfig();\n    printConfig();\n    saveConfig();\n    server.close();\n    server.stop();\n    ESP.restart();\n  });\n\n  server.on(\"/reconnect\", HTTP_GET, []() {\n    Serial.println(\"handleReconnect\");\n    handleStaticFile(\"/reload_success.html\");\n    server.close();\n    server.stop();\n    delay(5000);\n    WiFiManager wifiManager;\n    wifiManager.resetSettings();\n    wifiManager.setAPStaticIPConfig(IPAddress(192, 168, 1, 1), IPAddress(192, 168, 1, 1), IPAddress(255, 255, 255, 0));\n    wifiManager.startConfigPortal(host);\n    server.begin();\n    if (WiFi.status() == WL_CONNECTED)\n      Serial.println(\"WiFi status ok\");\n  });\n\n  server.on(\"/restart\", HTTP_GET, []() {\n    Serial.println(\"handleRestart\");\n    handleStaticFile(\"/reload_success.html\");\n    server.close();\n    server.stop();\n    SPIFFS.end();\n    delay(5000);\n    ESP.restart();\n  });\n\n  server.on(\"/json\", HTTP_PUT, handleJSON);\n\n  server.on(\"/json\", HTTP_POST, handleJSON);\n\n  server.on(\"/json\", HTTP_GET, [] {\n    DynamicJsonDocument root(300);\n    N_CONFIG_TO_JSON(var1, \"var1\");\n    N_CONFIG_TO_JSON(var2, \"var2\");\n    N_CONFIG_TO_JSON(var3, \"var3\");\n    root[\"version\"] = version;\n    root[\"uptime\"] = long(millis() / 1000);\n    String str;\n    serializeJson(root, str);\n    server.setContentLength(str.length());\n    server.send(200, \"application/json\", str);\n  });\n\n  // start the web server\n  server.begin();\n\n  // announce the hostname and web server through zeroconf\n  MDNS.begin(host);\n  MDNS.addService(\"http\", \"tcp\", 80);\n\n  Serial.println(\"Setup done\");\n}\n\n/*********************************************************************************/\n\nvoid loop() {\n  server.handleClient();\n  delay(10);  // in milliseconds\n}\n"
  },
  {
    "path": "esp32_config_webinterface_v6/webinterface.cpp",
    "content": "#include \"webinterface.h\"\n\nConfig config;\nextern WebServer server;\n\n/***************************************************************************/\n\nstatic String getContentType(const String& path) {\n  if (path.endsWith(\".html\"))       return \"text/html\";\n  else if (path.endsWith(\".htm\"))   return \"text/html\";\n  else if (path.endsWith(\".css\"))   return \"text/css\";\n  else if (path.endsWith(\".txt\"))   return \"text/plain\";\n  else if (path.endsWith(\".js\"))    return \"application/javascript\";\n  else if (path.endsWith(\".png\"))   return \"image/png\";\n  else if (path.endsWith(\".gif\"))   return \"image/gif\";\n  else if (path.endsWith(\".jpg\"))   return \"image/jpeg\";\n  else if (path.endsWith(\".jpeg\"))  return \"image/jpeg\";\n  else if (path.endsWith(\".ico\"))   return \"image/x-icon\";\n  else if (path.endsWith(\".svg\"))   return \"image/svg+xml\";\n  else if (path.endsWith(\".xml\"))   return \"text/xml\";\n  else if (path.endsWith(\".pdf\"))   return \"application/pdf\";\n  else if (path.endsWith(\".zip\"))   return \"application/zip\";\n  else if (path.endsWith(\".gz\"))    return \"application/x-gzip\";\n  else if (path.endsWith(\".json\"))  return \"application/json\";\n  return \"application/octet-stream\";\n}\n\n/***************************************************************************/\n\nbool defaultConfig() {\n  Serial.println(\"defaultConfig\");\n  \n  config.var1 = 11;\n  config.var2 = 22;\n  config.var3 = 33;\n  return true;\n}\n\nbool loadConfig() {\n  Serial.println(\"loadConfig\");\n\n  File configFile = SPIFFS.open(\"/config.json\", \"r\");\n  if (!configFile) {\n    Serial.println(\"Failed to open config file\");\n    return false;\n  }\n\n  size_t size = configFile.size();\n  if (size > 1024) {\n    Serial.println(\"Config file size is too large\");\n    return false;\n  }\n\n  std::unique_ptr<char[]> buf(new char[size]);\n  configFile.readBytes(buf.get(), size);\n  configFile.close();\n\n  StaticJsonDocument<300> jsonBuffer;\n  DynamicJsonDocument root(1024);\n  DeserializationError error = deserializeJson(root, buf.get());\n  if (error) {\n    Serial.println(\"Failed to parse config file\");\n    return false;\n  }\n\n  N_JSON_TO_CONFIG(var1, \"var1\");\n  N_JSON_TO_CONFIG(var2, \"var2\");\n  N_JSON_TO_CONFIG(var3, \"var3\");\n\n  return true;\n}\n\nbool saveConfig() {\n  Serial.println(\"saveConfig\");\n  DynamicJsonDocument root(300);\n\n  N_CONFIG_TO_JSON(var1, \"var1\");\n  N_CONFIG_TO_JSON(var2, \"var2\");\n  N_CONFIG_TO_JSON(var3, \"var3\");\n\n  File configFile = SPIFFS.open(\"/config.json\", \"w\");\n  if (!configFile) {\n    Serial.println(\"Failed to open config file for writing\");\n    return false;\n  }\n  else {\n    Serial.println(\"Writing to config file\");\n    serializeJson(root, configFile);\n    configFile.close();\n    return true;\n  }\n}\n\nvoid printConfig() {\n  Serial.print(\"var1 = \");\n  Serial.println(config.var1);\n  Serial.print(\"var2 = \");\n  Serial.println(config.var2);\n  Serial.print(\"var3 = \");\n  Serial.println(config.var3);\n}\n\nvoid printRequest() {\n  String message = \"HTTP Request\\n\\n\";\n  message += \"URI: \";\n  message += server.uri();\n  message += \"\\nMethod: \";\n  message += (server.method() == HTTP_GET) ? \"GET\" : \"POST\";\n  message += \"\\nHeaders: \";\n  message += server.headers();\n  message += \"\\n\";\n  for (uint8_t i = 0; i < server.headers(); i++ ) {\n    message += \" \" + server.headerName(i) + \": \" + server.header(i) + \"\\n\";\n  }\n  message += \"\\nArguments: \";\n  message += server.args();\n  message += \"\\n\";\n  for (uint8_t i = 0; i < server.args(); i++) {\n    message += \" \" + server.argName(i) + \": \" + server.arg(i) + \"\\n\";\n  }\n  Serial.println(message);\n}\n\nvoid handleNotFound() {\n  Serial.print(\"handleNotFound: \");\n  Serial.println(server.uri());\n  if (SPIFFS.exists(server.uri())) {\n    handleStaticFile(server.uri());\n  }\n  else {\n    String message = \"File Not Found\\n\\n\";\n    message += \"URI: \";\n    message += server.uri();\n    message += \"\\nMethod: \";\n    message += (server.method() == HTTP_GET) ? \"GET\" : \"POST\";\n    message += \"\\nArguments: \";\n    message += server.args();\n    message += \"\\n\";\n    for (uint8_t i = 0; i < server.args(); i++) {\n      message += \" \" + server.argName(i) + \": \" + server.arg(i) + \"\\n\";\n    }\n    server.setContentLength(message.length());\n    server.send(404, \"text/plain\", message);\n  }\n}\n\nvoid handleRedirect(const char * filename) {\n  handleRedirect((String)filename);\n}\n\nvoid handleRedirect(String filename) {\n  Serial.println(\"handleRedirect: \" + filename);\n  server.sendHeader(\"Location\", filename, true);\n  server.setContentLength(0);\n  server.send(302, \"text/plain\", \"\");\n}\n\nbool handleStaticFile(const char * path) {\n  return handleStaticFile((String)path);\n}\n\nbool handleStaticFile(String path) {\n  Serial.println(\"handleStaticFile: \" + path);\n  String contentType = getContentType(path);            // Get the MIME type\n  if (SPIFFS.exists(path)) {                            // If the file exists\n    File file = SPIFFS.open(path, \"r\");                 // Open it\n    server.setContentLength(file.size());\n    server.streamFile(file, contentType);               // And send it to the client\n    file.close();                                       // Then close the file again\n    return true;\n  }\n  Serial.println(\"\\tFile Not Found\");\n  return false;                                         // If the file doesn't exist, return false\n}\n\nvoid handleJSON() {\n  // this gets called in response to either a PUT or a POST\n  Serial.println(\"handleJSON\");\n  printRequest();\n\n  if (server.hasArg(\"var1\") || server.hasArg(\"var2\") || server.hasArg(\"var3\")) {\n    // the body is key1=val1&key2=val2&key3=val3 and the ESP8266Webserver has already parsed it\n    N_KEYVAL_TO_CONFIG(var1, \"var1\");\n    N_KEYVAL_TO_CONFIG(var2, \"var2\");\n    N_KEYVAL_TO_CONFIG(var3, \"var3\");\n    handleStaticFile(\"/reload_success.html\");\n  }\n  else if (server.hasArg(\"plain\")) {\n    // parse the body as JSON object\n    DynamicJsonDocument root(300);\n    DeserializationError error = deserializeJson(root, server.arg(\"plain\"));\n    if (error) {\n      handleStaticFile(\"/reload_failure.html\");\n      return;\n    }\n    N_JSON_TO_CONFIG(var1, \"var1\");\n    N_JSON_TO_CONFIG(var2, \"var2\");\n    N_JSON_TO_CONFIG(var3, \"var3\");\n    handleStaticFile(\"/reload_success.html\");\n  }\n  else {\n    handleStaticFile(\"/reload_failure.html\");\n    return; // do not save the configuration\n  }\n  \n  saveConfig();\n}\n"
  },
  {
    "path": "esp32_config_webinterface_v6/webinterface.h",
    "content": "#ifndef _WEBINTERFACE_H_\n#define _WEBINTERFACE_H_\n\n#include <Arduino.h>\n#include <WebServer.h>\n#include <ArduinoJson.h>  // https://arduinojson.org\n#include <FS.h>\n#include <SPIFFS.h>\n\n#ifndef ARDUINOJSON_VERSION\n#error ArduinoJson version 6 not found, please include ArduinoJson.h in your .ino file\n#endif\n\n#if ARDUINOJSON_VERSION_MAJOR != 6\n#error ArduinoJson version 6 is required\n#endif\n\n/* these are for numbers */\n#define N_JSON_TO_CONFIG(x, y)   { if (root.containsKey(y)) { config.x = root[y]; } }\n#define N_CONFIG_TO_JSON(x, y)   { root[y] = config.x; }\n#define N_KEYVAL_TO_CONFIG(x, y) { if (server.hasArg(y))    { String str = server.arg(y); config.x = str.toFloat(); } }\n\n/* these are for strings */\n#define S_JSON_TO_CONFIG(x, y)   { if (root.containsKey(y)) { strcpy(config.x, root[y]); } }\n#define S_CONFIG_TO_JSON(x, y)   { root.set(y, config.x); }\n#define S_KEYVAL_TO_CONFIG(x, y) { if (server.hasArg(y))    { String str = server.arg(y); strcpy(config.x, str.c_str()); } }\n\nstruct Config {\n  int var1;\n  int var2;\n  int var3;\n};\n\nextern Config config;\n\nbool defaultConfig(void);\nbool loadConfig(void);\nbool saveConfig(void);\nvoid printConfig(void);\n\nvoid handleNotFound(void);\nvoid handleRedirect(String);\nvoid handleRedirect(const char *);\nbool handleStaticFile(String);\nbool handleStaticFile(const char *);\nvoid handleJSON();\n\n#endif // _WEBINTERFACE_H_\n"
  },
  {
    "path": "esp32_config_webinterface_v7/README.md",
    "content": "# Overview\n\nThis is a demonstration and test sketch for configuring persistent options via the web interface. This strategy is used in a number of my functional ESP32 and ESP8266 sketches. It consists of a `settings.json` file in the SPIFFS filesystem and a `settings.html` file with some javascript.\n\n**This specific version is for ArduinoJson 7, there are other examples for version 5 and 6.**\n\nThe settings can be changed in the webbrowser at http://192.168.1.xxx/settings and clicking \"Send\". This results in `Webserver` parsing the arguments and placing the variables in the header, but also places the query string in the body.\n\nThe following also results in `Webserver` parsing the arguments and placing the variables in the header.\n\n    curl -X POST 'http://192.168.1.xxx/json?var1=11&var2=22&var3=33'\n    curl -X PUT  'http://192.168.1.xxx/json?var1=11&var2=22&var3=33'\n\nThe following is not parsed, but only places the JSON data in the body.\n\n    curl -X POST http://192.168.1.xxx/json -d '{\"var1\":11,\"var2\":22,\"var3\":33}'\n    curl -X PUT  http://192.168.1.xxx/json -d '{\"var1\":11,\"var2\":22,\"var3\":33}'\n\n## SPIFFS for static files\n\nYou should not only write the firmware to the ESP32 module, but also the static content for the web interface. The html, css and javascript files located in the data directory should be written to the SPIFS filesystem on the ESP32. See https://github.com/me-no-dev/arduino-esp32fs-plugin.\n\nYou will get a \"file not found\" error if the firmware cannot access the data files.\n\n## Arduino ESP32 filesystem uploader\n\nThis Arduino sketch includes a `data` directory with a number of files that should be uploaded to the ESP32 using the [SPIFFS filesystem uploader](https://github.com/me-no-dev/arduino-esp32fs-plugin) tool. At the moment (Feb 2024) the Arduino 2.x IDE does *not* support the SPIFFS filesystem uploader plugin. You have to use the Arduino 1.8.x IDE (recommended), or the command line utilities for uploading the data.\n"
  },
  {
    "path": "esp32_config_webinterface_v7/data/config.json",
    "content": "{\n\"var1\":10,\n\"var2\":20,\n\"var3\":30\n}\n"
  },
  {
    "path": "esp32_config_webinterface_v7/data/hello.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Hello</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n<p>Hello panda!</p>\n<img src=\"panda.jpg\">\n</body>\n</html>\n"
  },
  {
    "path": "esp32_config_webinterface_v7/data/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Index</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n<a href=\"/hello.html\">Hello</a></br>\n<a href=\"/monitor.html\">Monitor</a></br>\n<a href=\"/settings.html\">Change settings</a></br>\n<a href=\"/reconnect\">Reconnect WiFi</a></br>\n<a href=\"/defaults\">Default settings</a></br>\n<a href=\"/restart\">Restart hardware</a></br>\n</body>\n</html>\n"
  },
  {
    "path": "esp32_config_webinterface_v7/data/monitor.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Monitor</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n\n<h1>Monitor</h1>\n\nFirmware version:\n<div id=\"version\" name=\"version\">?</div>\n\nUptime:\n<div id=\"uptime\" name\"uptime\">?</div>\n\n<script>\n  async function updateContent() {\n    const response = await fetch(\"json\");\n    const data = await response.json();\n    console.log(data);\n    document.getElementById(\"version\").innerHTML = data[\"version\"];\n    document.getElementById(\"uptime\").innerHTML = data[\"uptime\"];\n  }\n  updateContent();\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "esp32_config_webinterface_v7/data/reload_failure.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Failure</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n<p>Failure</p>\n\n<script type=\"text/javascript\">\n  setTimeout(function(){location=\"/index.html\"},1000);\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "esp32_config_webinterface_v7/data/reload_success.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Success</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n<p>Success</p>\n\n<script type=\"text/javascript\">\n  setTimeout(function(){location=\"/index.html\"},1000);\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "esp32_config_webinterface_v7/data/settings.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Settings</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n<h1>Settings</h1>\n\n<form id=\"settings-form\" method=\"post\" action=\"json\">\n    <div class=\"field\">\n        <label for=\"var1\">var1:</label>\n        <input type=\"text\" id=\"var1\" name=\"var1\" value=\"?\" required>\n    </div>\n\n    <div class=\"field\">\n        <label for=\"var2\">var2:</label>\n        <input type=\"text\" id=\"var2\" name=\"var2\" value=\"?\" required>\n    </div>\n\n    <div class=\"field\">\n        <label for=\"var2\">var3:</label>\n        <input type=\"text\" id=\"var3\" name=\"var3\" value=\"?\" required>\n    </div>\n\n    <div class=\"field\">\n        <button type=\"submit\">Save</button>\n    </div>\n</form>\n\n<script>\n  async function updateContent() {\n    const response = await fetch(\"json\");\n    const data = await response.json();\n    console.log(data);\n    document.getElementById(\"var1\").value = data[\"var1\"];\n    document.getElementById(\"var2\").value = data[\"var2\"];\n    document.getElementById(\"var3\").value = data[\"var3\"];\n  }\n  updateContent();\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "esp32_config_webinterface_v7/data/style.css",
    "content": ".c{\n    text-align: center;\n}\ndiv,input{\n    padding:5px;font-size:1em;\n}\ninput{\n    width:95%;\n}\nbody{\n    text-align: center;font-family:verdana;\n}\nbutton{\n    border:0;border-radius:0.3rem;background-color:#1fa3ec;color:#fff;line-height:2.4rem;font-size:1.2rem;width:100%;\n}\n.q{\n    float: right;width: 64px;text-align: right;\n}\n.l{\n    background: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAALVBMVEX///8EBwfBwsLw8PAzNjaCg4NTVVUjJiZDRUUUFxdiZGSho6OSk5Pg4eFydHTCjaf3AAAAZElEQVQ4je2NSw7AIAhEBamKn97/uMXEGBvozkWb9C2Zx4xzWykBhFAeYp9gkLyZE0zIMno9n4g19hmdY39scwqVkOXaxph0ZCXQcqxSpgQpONa59wkRDOL93eAXvimwlbPbwwVAegLS1HGfZAAAAABJRU5ErkJggg==\") no-repeat left center;background-size: 1em;\n}\n"
  },
  {
    "path": "esp32_config_webinterface_v7/esp32_config_webinterface_v7.ino",
    "content": "#if not defined(ESP32)\n#error This is a sketch for an ESP32 board, like the NodeMCU 32S, LOLIN32 Lite, or the Adafruit Huzzah32\n#endif\n\n#include <Arduino.h>\n#include <ArduinoJson.h>  // https://arduinojson.org\n#include <WiFi.h>\n#include <WiFiManager.h>  // https://github.com/tzapu/WiFiManager\n#include <WiFiClient.h>\n#include <WebServer.h>\n#include <ESPmDNS.h>\n#include <FS.h>\n#include <SPIFFS.h>\n\n#include \"webinterface.h\"\n\n/*********************************************************************************/\n\nconst char* host = \"ESP32\";\nconst char* version = __DATE__ \" / \" __TIME__;\n\nWebServer server(80);\nWiFiManager wifiManager;\n\n/*********************************************************************************/\n\nvoid setup() {\n\n  Serial.begin(115200);\n  while (!Serial) {\n    ;\n  }\n  Serial.println(\"Setup starting\");\n\n  // The SPIFFS file system contains the config.json, and the html and javascript code for the web interface\n  SPIFFS.begin();\n  loadConfig();\n  printConfig();\n\n  WiFiManager wifiManager;\n  wifiManager.setAPStaticIPConfig(IPAddress(192, 168, 1, 1), IPAddress(192, 168, 1, 1), IPAddress(255, 255, 255, 0));\n  wifiManager.autoConnect(host);\n  Serial.println(\"connected\");\n\n  // this serves all URIs that can be resolved to a file on the SPIFFS filesystem\n  server.onNotFound(handleNotFound);\n\n  server.on(\"/\", HTTP_GET, []() {\n    handleRedirect(\"/index.html\");\n  });\n\n  server.on(\"/defaults\", HTTP_GET, []() {\n    Serial.println(\"handleDefaults\");\n    handleStaticFile(\"/reload_success.html\");\n    defaultConfig();\n    printConfig();\n    saveConfig();\n    server.close();\n    server.stop();\n    ESP.restart();\n  });\n\n  server.on(\"/reconnect\", HTTP_GET, []() {\n    Serial.println(\"handleReconnect\");\n    handleStaticFile(\"/reload_success.html\");\n    server.close();\n    server.stop();\n    delay(5000);\n    WiFiManager wifiManager;\n    wifiManager.resetSettings();\n    wifiManager.setAPStaticIPConfig(IPAddress(192, 168, 1, 1), IPAddress(192, 168, 1, 1), IPAddress(255, 255, 255, 0));\n    wifiManager.startConfigPortal(host);\n    server.begin();\n    if (WiFi.status() == WL_CONNECTED)\n      Serial.println(\"WiFi status ok\");\n  });\n\n  server.on(\"/restart\", HTTP_GET, []() {\n    Serial.println(\"handleRestart\");\n    handleStaticFile(\"/reload_success.html\");\n    server.close();\n    server.stop();\n    SPIFFS.end();\n    delay(5000);\n    ESP.restart();\n  });\n\n  server.on(\"/json\", HTTP_PUT, handleJSON);\n\n  server.on(\"/json\", HTTP_POST, handleJSON);\n\n  server.on(\"/json\", HTTP_GET, [] {\n    JsonDocument root;\n    N_CONFIG_TO_JSON(var1, \"var1\");\n    N_CONFIG_TO_JSON(var2, \"var2\");\n    N_CONFIG_TO_JSON(var3, \"var3\");\n    root[\"version\"] = version;\n    root[\"uptime\"] = long(millis() / 1000);\n    String str;\n    serializeJson(root, str);\n    server.setContentLength(str.length());\n    server.send(200, \"application/json\", str);\n  });\n\n  // start the web server\n  server.begin();\n\n  // announce the hostname and web server through zeroconf\n  MDNS.begin(host);\n  MDNS.addService(\"http\", \"tcp\", 80);\n\n  Serial.println(\"Setup done\");\n}\n\n/*********************************************************************************/\n\nvoid loop() {\n  server.handleClient();\n  delay(10);  // in milliseconds\n}\n"
  },
  {
    "path": "esp32_config_webinterface_v7/webinterface.cpp",
    "content": "#include \"webinterface.h\"\n\nConfig config;\nextern WebServer server;\n\n/***************************************************************************/\n\nstatic String getContentType(const String& path) {\n  if (path.endsWith(\".html\"))       return \"text/html\";\n  else if (path.endsWith(\".htm\"))   return \"text/html\";\n  else if (path.endsWith(\".css\"))   return \"text/css\";\n  else if (path.endsWith(\".txt\"))   return \"text/plain\";\n  else if (path.endsWith(\".js\"))    return \"application/javascript\";\n  else if (path.endsWith(\".png\"))   return \"image/png\";\n  else if (path.endsWith(\".gif\"))   return \"image/gif\";\n  else if (path.endsWith(\".jpg\"))   return \"image/jpeg\";\n  else if (path.endsWith(\".jpeg\"))  return \"image/jpeg\";\n  else if (path.endsWith(\".ico\"))   return \"image/x-icon\";\n  else if (path.endsWith(\".svg\"))   return \"image/svg+xml\";\n  else if (path.endsWith(\".xml\"))   return \"text/xml\";\n  else if (path.endsWith(\".pdf\"))   return \"application/pdf\";\n  else if (path.endsWith(\".zip\"))   return \"application/zip\";\n  else if (path.endsWith(\".gz\"))    return \"application/x-gzip\";\n  else if (path.endsWith(\".json\"))  return \"application/json\";\n  return \"application/octet-stream\";\n}\n\n/***************************************************************************/\n\nbool defaultConfig() {\n  Serial.println(\"defaultConfig\");\n  \n  config.var1 = 11;\n  config.var2 = 22;\n  config.var3 = 33;\n  return true;\n}\n\nbool loadConfig() {\n  Serial.println(\"loadConfig\");\n\n  File configFile = SPIFFS.open(\"/config.json\", \"r\");\n  if (!configFile) {\n    Serial.println(\"Failed to open config file\");\n    return false;\n  }\n\n  size_t size = configFile.size();\n  if (size > 1024) {\n    Serial.println(\"Config file size is too large\");\n    return false;\n  }\n\n  std::unique_ptr<char[]> buf(new char[size]);\n  configFile.readBytes(buf.get(), size);\n  configFile.close();\n\n  JsonDocument root;\n  DeserializationError error = deserializeJson(root, buf.get());\n  if (error) {\n    Serial.println(\"Failed to parse config file\");\n    return false;\n  }\n\n  N_JSON_TO_CONFIG(var1, \"var1\");\n  N_JSON_TO_CONFIG(var2, \"var2\");\n  N_JSON_TO_CONFIG(var3, \"var3\");\n\n  return true;\n}\n\nbool saveConfig() {\n  Serial.println(\"saveConfig\");\n  JsonDocument root;\n\n  N_CONFIG_TO_JSON(var1, \"var1\");\n  N_CONFIG_TO_JSON(var2, \"var2\");\n  N_CONFIG_TO_JSON(var3, \"var3\");\n\n  File configFile = SPIFFS.open(\"/config.json\", \"w\");\n  if (!configFile) {\n    Serial.println(\"Failed to open config file for writing\");\n    return false;\n  }\n  else {\n    Serial.println(\"Writing to config file\");\n    serializeJson(root, configFile);\n    configFile.close();\n    return true;\n  }\n}\n\nvoid printConfig() {\n  Serial.print(\"var1 = \");\n  Serial.println(config.var1);\n  Serial.print(\"var2 = \");\n  Serial.println(config.var2);\n  Serial.print(\"var3 = \");\n  Serial.println(config.var3);\n}\n\nvoid printRequest() {\n  String message = \"HTTP Request\\n\\n\";\n  message += \"URI: \";\n  message += server.uri();\n  message += \"\\nMethod: \";\n  message += (server.method() == HTTP_GET) ? \"GET\" : \"POST\";\n  message += \"\\nHeaders: \";\n  message += server.headers();\n  message += \"\\n\";\n  for (uint8_t i = 0; i < server.headers(); i++ ) {\n    message += \" \" + server.headerName(i) + \": \" + server.header(i) + \"\\n\";\n  }\n  message += \"\\nArguments: \";\n  message += server.args();\n  message += \"\\n\";\n  for (uint8_t i = 0; i < server.args(); i++) {\n    message += \" \" + server.argName(i) + \": \" + server.arg(i) + \"\\n\";\n  }\n  Serial.println(message);\n}\n\nvoid handleNotFound() {\n  Serial.print(\"handleNotFound: \");\n  Serial.println(server.uri());\n  if (SPIFFS.exists(server.uri())) {\n    handleStaticFile(server.uri());\n  }\n  else {\n    String message = \"File Not Found\\n\\n\";\n    message += \"URI: \";\n    message += server.uri();\n    message += \"\\nMethod: \";\n    message += (server.method() == HTTP_GET) ? \"GET\" : \"POST\";\n    message += \"\\nArguments: \";\n    message += server.args();\n    message += \"\\n\";\n    for (uint8_t i = 0; i < server.args(); i++) {\n      message += \" \" + server.argName(i) + \": \" + server.arg(i) + \"\\n\";\n    }\n    server.setContentLength(message.length());\n    server.send(404, \"text/plain\", message);\n  }\n}\n\nvoid handleRedirect(const char * filename) {\n  handleRedirect((String)filename);\n}\n\nvoid handleRedirect(String filename) {\n  Serial.println(\"handleRedirect: \" + filename);\n  server.sendHeader(\"Location\", filename, true);\n  server.setContentLength(0);\n  server.send(302, \"text/plain\", \"\");\n}\n\nbool handleStaticFile(const char * path) {\n  return handleStaticFile((String)path);\n}\n\nbool handleStaticFile(String path) {\n  Serial.println(\"handleStaticFile: \" + path);\n  String contentType = getContentType(path);            // Get the MIME type\n  if (SPIFFS.exists(path)) {                            // If the file exists\n    File file = SPIFFS.open(path, \"r\");                 // Open it\n    server.setContentLength(file.size());\n    server.streamFile(file, contentType);               // And send it to the client\n    file.close();                                       // Then close the file again\n    return true;\n  }\n  Serial.println(\"\\tFile Not Found\");\n  return false;                                         // If the file doesn't exist, return false\n}\n\nvoid handleJSON() {\n  // this gets called in response to either a PUT or a POST\n  Serial.println(\"handleJSON\");\n  printRequest();\n\n  if (server.hasArg(\"var1\") || server.hasArg(\"var2\") || server.hasArg(\"var3\")) {\n    // the body is key1=val1&key2=val2&key3=val3 and the ESP8266Webserver has already parsed it\n    N_KEYVAL_TO_CONFIG(var1, \"var1\");\n    N_KEYVAL_TO_CONFIG(var2, \"var2\");\n    N_KEYVAL_TO_CONFIG(var3, \"var3\");\n    handleStaticFile(\"/reload_success.html\");\n  }\n  else if (server.hasArg(\"plain\")) {\n    // parse the body as JSON object\n    JsonDocument root;\n    DeserializationError error = deserializeJson(root, server.arg(\"plain\"));\n    if (error) {\n      handleStaticFile(\"/reload_failure.html\");\n      return;\n    }\n    N_JSON_TO_CONFIG(var1, \"var1\");\n    N_JSON_TO_CONFIG(var2, \"var2\");\n    N_JSON_TO_CONFIG(var3, \"var3\");\n    handleStaticFile(\"/reload_success.html\");\n  }\n  else {\n    handleStaticFile(\"/reload_failure.html\");\n    return; // do not save the configuration\n  }\n  \n  saveConfig();\n}\n"
  },
  {
    "path": "esp32_config_webinterface_v7/webinterface.h",
    "content": "#ifndef _WEBINTERFACE_H_\n#define _WEBINTERFACE_H_\n\n#include <Arduino.h>\n#include <WebServer.h>\n#include <ArduinoJson.h>  // https://arduinojson.org\n#include <FS.h>\n#include <SPIFFS.h>\n\n#ifndef ARDUINOJSON_VERSION\n#error ArduinoJson version 7 not found, please include ArduinoJson.h in your .ino file\n#endif\n\n#if ARDUINOJSON_VERSION_MAJOR != 7\n#error ArduinoJson version 7 is required\n#endif\n\n/* these are for numbers */\n#define N_JSON_TO_CONFIG(x, y)   { if (root.containsKey(y)) { config.x = root[y]; } }\n#define N_CONFIG_TO_JSON(x, y)   { root[y] = config.x; }\n#define N_KEYVAL_TO_CONFIG(x, y) { if (server.hasArg(y))    { String str = server.arg(y); config.x = str.toFloat(); } }\n\n/* these are for strings */\n#define S_JSON_TO_CONFIG(x, y)   { if (root.containsKey(y)) { strcpy(config.x, root[y]); } }\n#define S_CONFIG_TO_JSON(x, y)   { root.set(y, config.x); }\n#define S_KEYVAL_TO_CONFIG(x, y) { if (server.hasArg(y))    { String str = server.arg(y); strcpy(config.x, str.c_str()); } }\n\nstruct Config {\n  int var1;\n  int var2;\n  int var3;\n};\n\nextern Config config;\n\nbool defaultConfig(void);\nbool loadConfig(void);\nbool saveConfig(void);\nvoid printConfig(void);\n\nvoid handleNotFound(void);\nvoid handleRedirect(String);\nvoid handleRedirect(const char *);\nbool handleStaticFile(String);\nbool handleStaticFile(const char *);\nvoid handleJSON();\n\n#endif // _WEBINTERFACE_H_\n"
  },
  {
    "path": "esp32_exgpill/README.md",
    "content": "# Overview\n\nThis is an Arduino sketch for an ExGpill connected to a LOLIN32 Lite development board. It features a webinterface that allows to change the configuration and it streams the EMG/ECG/EEG data as a FieldTrip buffer client to a server elsewhere on the wifi network.\n\nIt is compatible with the [EEGsynth](https://www.eegsynth.org).\n\n## SPIFFS for static files\n\nYou should not only write the firmware to the ESP32 module, but also the static content for the web interface. The html, css and javascript files located in the data directory should be written to the SPIFS filesystem on the ESP32. See https://github.com/me-no-dev/arduino-esp32fs-plugin.\n\nYou will get a \"file not found\" error if the firmware cannot access the data files.\n\n## Arduino ESP32 filesystem uploader\n\nThis Arduino sketch includes a `data` directory with a number of files that should be uploaded to the ESP8266 using the [SPIFFS filesystem uploader](https://github.com/esp8266/arduino-esp8266fs-plugin) tool. At the moment (Feb 2024) the Arduino 2.x IDE does *not* support the SPIFFS filesystem uploader plugin. You have to use the Arduino 1.8.x IDE (recommended), or the command line utilities for uploading the data.\n"
  },
  {
    "path": "esp32_exgpill/data/config.json",
    "content": "{\n\"address\":\"192.168.1.34\",\n\"port\":1972\n}\n"
  },
  {
    "path": "esp32_exgpill/data/hello.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Hello</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n<p>Hello arctic fox!</p>\n<img src=\"arctic-fox.jpg\">\n</body>\n</html>\n"
  },
  {
    "path": "esp32_exgpill/data/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Index</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n<a href=\"/hello.html\">Hello</a></br>\n<a href=\"/monitor.html\">Monitor</a></br>\n<a href=\"/settings.html\">Change settings</a></br>\n<a href=\"/reconnect\">Reconnect WiFi</a></br>\n<a href=\"/defaults\">Default settings</a></br>\n<a href=\"/restart\">Restart hardware</a></br>\n</body>\n</html>\n"
  },
  {
    "path": "esp32_exgpill/data/monitor.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Monitor</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n\n<h1>Monitor</h1>\n\nFirmware version:\n<div id=\"version\" name=\"version\">?</div>\n\nUptime:\n<div id=\"uptime\" name\"uptime\">?</div>\n\nSamples transferred:\n<div id=\"total\" name\"total\">?</div>\n\n<script>\n  async function updateContent() {\n    const response = await fetch(\"json\");\n    const data = await response.json();\n    console.log(data);\n    document.getElementById(\"version\").innerHTML = data[\"version\"];\n    document.getElementById(\"uptime\").innerHTML = data[\"uptime\"];\n    document.getElementById(\"total\").innerHTML = data[\"total\"];\n  }\n  updateContent();\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "esp32_exgpill/data/reload_failure.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Failure</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n<p>Failure</p>\n\n<script type=\"text/javascript\">\n  setTimeout(function(){location=\"/index.html\"},1000);\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "esp32_exgpill/data/reload_success.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Success</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n<p>Success</p>\n\n<script type=\"text/javascript\">\n  setTimeout(function(){location=\"/index.html\"},1000);\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "esp32_exgpill/data/settings.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Settings</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n<h1>Settings</h1>\n\n<form id=\"settings-form\" method=\"post\" action=\"json\">\n    <div class=\"field\">\n        <label for=\"address\">address:</label>\n        <input type=\"text\" id=\"address\" name=\"address\" value=\"?\" required>\n    </div>\n\n    <div class=\"field\">\n        <label for=\"port\">port:</label>\n        <input type=\"text\" id=\"port\" name=\"port\" value=\"?\" required>\n    </div>\n\n    <div class=\"field\">\n        <button type=\"submit\">Save</button>\n    </div>\n</form>\n\n<script>\n  async function updateContent() {\n    const response = await fetch(\"json\");\n    const data = await response.json();\n    console.log(data);\n    document.getElementById(\"address\").value = data[\"address\"];\n    document.getElementById(\"port\").value = data[\"port\"];\n  }\n  updateContent();\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "esp32_exgpill/data/style.css",
    "content": ".c{\n    text-align: center;\n}\ndiv,input{\n    padding:5px;font-size:1em;\n}\ninput{\n    width:95%;\n}\nbody{\n    text-align: center;font-family:verdana;\n}\nbutton{\n    border:0;border-radius:0.3rem;background-color:#1fa3ec;color:#fff;line-height:2.4rem;font-size:1.2rem;width:100%;\n}\n.q{\n    float: right;width: 64px;text-align: right;\n}\n.l{\n    background: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAALVBMVEX///8EBwfBwsLw8PAzNjaCg4NTVVUjJiZDRUUUFxdiZGSho6OSk5Pg4eFydHTCjaf3AAAAZElEQVQ4je2NSw7AIAhEBamKn97/uMXEGBvozkWb9C2Zx4xzWykBhFAeYp9gkLyZE0zIMno9n4g19hmdY39scwqVkOXaxph0ZCXQcqxSpgQpONa59wkRDOL93eAXvimwlbPbwwVAegLS1HGfZAAAAABJRU5ErkJggg==\") no-repeat left center;background-size: 1em;\n}\n"
  },
  {
    "path": "esp32_exgpill/esp32_exgpill.ino",
    "content": "#if not defined(ESP32)\n#error This is a sketch for an ESP32 board, like the NodeMCU 32S, LOLIN32 Lite, or the Adafruit Huzzah32\n#endif\n\n#include <Arduino.h>\n#include <ArduinoJson.h>\n#include <WiFi.h>\n#include <WiFiManager.h>          // https://github.com/tzapu/WiFiManager\n#include <WiFiClient.h>\n#include <WebServer.h>\n#include <ESPmDNS.h>\n#include <Ticker.h>               // https://github.com/sstaub/Ticker\n#include <FS.h>\n#include <SPIFFS.h>\n\n#include \"fieldtrip_buffer.h\"\n#include \"rgb_led.h\"\n#include \"webinterface.h\"\n\n#ifndef ARDUINOJSON_VERSION\n#error ArduinoJson version 5 not found, please include ArduinoJson.h in your .ino file\n#endif\n\n#if ARDUINOJSON_VERSION_MAJOR != 5\n#error ArduinoJson version 5 is required\n#endif\n\nWebServer server(80);\nTicker sampler;\n\n// when SERIAL_PLOTTER is defined the ADC value is printed for each sample and some of the other debugging output is disabled\n#define SERIAL_PLOTTER\n\n#define ADC       33              // https://randomnerdtutorials.com/esp32-pinout-reference-gpios/\n#define NCHANS    1\n#define FSAMPLE   250\n#define BLOCKSIZE (FSAMPLE/10)\n\nconst char* host = \"EXGPILL\";\nconst char* version = __DATE__ \" / \" __TIME__;\n\nunsigned int sample = 0, block = 0, total = 0;\nbool flush0 = false, flush1 = false;\nuint16_t block0[BLOCKSIZE], block1[BLOCKSIZE];\nint ftserver = 0, status = 0;\n\nvoid getSample() {\n  if (sample == BLOCKSIZE) {\n    // switch to the start of the other block\n    switch (block) {\n      case 0:\n        sample = 0;\n        flush0 = true;\n        block = 1;\n        break;\n      case 1:\n        sample = 0;\n        flush1 = true;\n        block = 0;\n        break;\n    }\n  }\n\n  // get the current ExG value, it is a 12-bits ADC with a value between 0 and 4095\n  uint16_t value = analogRead(ADC);\n#ifdef SERIAL_PLOTTER\n  Serial.println(value);\n#endif\n\n  // store it in the active block\n  switch (block) {\n    case 0:\n      block0[sample++] = value;\n      break;\n    case 1:\n      block1[sample++] = value;\n      break;\n  }\n}\n\nvoid setup() {\n  Serial.begin(115200);\n  while (!Serial) {\n    ;\n  }\n  Serial.println(\"\");\n  Serial.println(\"Setup start\");\n\n  ledInit();\n  ledRed();\n\n  SPIFFS.begin();\n  loadConfig();\n\n  WiFiManager wifiManager;\n  wifiManager.setAPStaticIPConfig(IPAddress(192, 168, 1, 1), IPAddress(192, 168, 1, 1), IPAddress(255, 255, 255, 0));\n  wifiManager.autoConnect(host);\n  Serial.println(\"connected\");\n  ledGreen();\n\n  // this serves all URIs that can be resolved to a file on the SPIFFS filesystem\n  server.onNotFound(handleNotFound);\n\n  server.on(\"/\", HTTP_GET, []() {\n    handleRedirect(\"/index.html\");\n  });\n\n  server.on(\"/defaults\", HTTP_GET, []() {\n    Serial.println(\"handleDefaults\");\n    handleStaticFile(\"/reload_success.html\");\n    defaultConfig();\n    saveConfig();\n    server.close();\n    server.stop();\n    delay(5000);\n    ESP.restart();\n  });\n\n  server.on(\"/reconnect\", HTTP_GET, []() {\n    Serial.println(\"handleReconnect\");\n    handleStaticFile(\"/reload_success.html\");\n    server.close();\n    server.stop();\n    delay(5000);\n    WiFiManager wifiManager;\n    wifiManager.resetSettings();\n    wifiManager.setAPStaticIPConfig(IPAddress(192, 168, 1, 1), IPAddress(192, 168, 1, 1), IPAddress(255, 255, 255, 0));\n    wifiManager.startConfigPortal(host);\n    Serial.println(\"connected\");\n    server.begin();\n  });\n\n  server.on(\"/restart\", HTTP_GET, []() {\n    Serial.println(\"handleRestart\");\n    handleStaticFile(\"/reload_success.html\");\n    server.close();\n    server.stop();\n    SPIFFS.end();\n    delay(5000);\n    ESP.restart();\n  });\n\n  server.on(\"/json\", HTTP_PUT, handleJSON);\n\n  server.on(\"/json\", HTTP_POST, handleJSON);\n\n  server.on(\"/json\", HTTP_GET, [] {\n    StaticJsonBuffer<200> jsonBuffer;\n    JsonObject& root = jsonBuffer.createObject();\n    root[\"address\"] = config.address;\n    root[\"port\"]    = config.port;\n    root[\"total\"]   = total;\n    root[\"version\"] = version;\n    root[\"uptime\"]  = long(millis() / 1000);\n    String content;\n    root.printTo(content);\n    server.send(200, \"application/json\", content);\n  });\n\n  // ask server to track these headers\n  const char * headerkeys[] = {\"User-Agent\", \"Content-Type\"} ;\n  size_t headerkeyssize = sizeof(headerkeys) / sizeof(char*);\n  server.collectHeaders(headerkeys, headerkeyssize );\n\n  server.begin();\n\n  MDNS.begin(host);\n  MDNS.addService(\"http\", \"tcp\", 80);\n\n  Serial.print(\"Connecting to \");\n  Serial.print(config.address);\n  Serial.print(\" on port \");\n  Serial.println(config.port);\n\n  ftserver = fieldtrip_open_connection(config.address, config.port);\n  if (ftserver > 0) {\n    Serial.println(\"Connection opened\");\n    status = fieldtrip_write_header(ftserver, DATATYPE_UINT16, NCHANS, FSAMPLE);\n    if (status == 0)\n      Serial.println(\"Wrote header\");\n    else\n      Serial.println(\"Failed writing header\");\n  }\n  else {\n    Serial.println(\"Failed opening connection\");\n  }\n\n  // start sampling the ExG\n  sampler.attach_ms(1000 / FSAMPLE, getSample);\n\n  Serial.println(\"Setup done\");\n}\n\nvoid loop() {\n  server.handleClient();\n\n  byte *ptr = NULL;\n  if (flush0) {\n    // block0 is ready to be sent\n    ptr = (byte *)block0;\n    flush0 = false;\n  }\n  else if (flush1) {\n    // block1 is ready to be sent\n    ptr = (byte *)block1;\n    flush1 = false;\n  }\n\n  if (ptr) {\n    if (ftserver == 0) {\n      ftserver = fieldtrip_open_connection(config.address, config.port);\n      if (ftserver > 0)\n        Serial.println(\"Connection opened\");\n      else\n        Serial.println(\"Failed opening connection\");\n    }\n\n    if (ftserver > 0) {\n      status = fieldtrip_write_data(ftserver, DATATYPE_UINT16, NCHANS, BLOCKSIZE, ptr);\n      if (status == 0) {\n        total += BLOCKSIZE;\n#ifndef SERIAL_PLOTTER\n        Serial.print(\"Wrote \");\n        Serial.print(total);\n        Serial.println(\" samples\");\n#endif\n      }\n      else {\n        Serial.println(\"Failed writing data\");\n        status = fieldtrip_close_connection(ftserver);\n        if (status == 0)\n          Serial.println(\"Connection closed\");\n        else\n          Serial.println(\"Failed closing connection\");\n        ftserver = 0;\n      }\n    }\n  }\n\n  delay(10); // in milliseconds\n}\n"
  },
  {
    "path": "esp32_exgpill/fieldtrip_buffer.cpp",
    "content": "#include \"fieldtrip_buffer.h\"\n\nWiFiClient client;\n\n#undef DEBUG\n\n/*******************************************************************************\n   OPEN CONNECTION\n   returns file descriptor that should be >0 on success\n *******************************************************************************/\nint fieldtrip_open_connection(const char *address, int port) {\n  int status;\n  status = client.connect(address, port);\n  if (status == 1)\n    return 1;\n  else\n    return -1;\n};\n\n/*******************************************************************************\n   CLOSE CONNECTION\n   returns 0 on success\n *******************************************************************************/\nint fieldtrip_close_connection(int s) {\n  client.stop();\n  return 0;\n};\n\n/*******************************************************************************\n   WRITE HEADER\n   returns 0 on success\n *******************************************************************************/\nint fieldtrip_write_header(int server, uint32_t datatype, uint32_t nchans, float fsample) {\n  int status;\n  messagedef_t *request  = NULL;\n  messagedef_t *response = NULL;\n  headerdef_t  *header   = NULL;\n\n  byte msg[8+24];\n  for (int i = 0; i < 8+24; i++)\n    msg[i] = 0;\n\n  request = (messagedef_t *)(msg + 0);\n  request->version = VERSION;\n  request->command = PUT_HDR_NORESPONSE;\n  request->bufsize = sizeof(headerdef_t);\n\n  header = (headerdef_t *)(msg + sizeof(messagedef_t)); // the first 8 bytes are version, command and bufsize\n  header->nchans     = nchans;\n  header->nsamples   = 0;\n  header->nevents    = 0;\n  header->fsample    = fsample;\n  header->data_type  = datatype;\n  header->bufsize    = 0;\n\n#ifdef DEBUG\n  Serial.print(\"msg =  \");\n  for (int i = 0; i < sizeof(messagedef_t) + sizeof(headerdef_t); i++) {\n    Serial.print(msg[i]);\n    Serial.print(\" \");\n  }\n  Serial.println();\n#endif\n\n  int n = 0;\n  n += client.write(msg, sizeof(messagedef_t) + sizeof(headerdef_t));\n  client.flush();\n\n#ifdef DEBUG\n  Serial.print(\"Wrote \");\n  Serial.print(n);\n  Serial.println(\" bytes\");\n#endif\n\n  if (n == sizeof(messagedef_t) + sizeof(headerdef_t))\n    return 0;\n  else\n    return -1;\n};\n\n/*******************************************************************************\n   WRITE DATA\n   returns 0 on success\n *******************************************************************************/\nint fieldtrip_write_data(int server, uint32_t datatype, uint32_t nchans, uint32_t nsamples, byte *buffer) {\n  int status;\n  messagedef_t *request  = NULL;\n  messagedef_t *response = NULL;\n  datadef_t    *data   = NULL;\n\n  byte msg[8+16];\n  for (int i = 0; i < 8+16; i++)\n    msg[i] = 0;\n\n  request = (messagedef_t *)(msg + 0);\n  request->version = VERSION;\n  request->command = PUT_DAT_NORESPONSE;\n  request->bufsize = sizeof(datadef_t) + nchans * nsamples * wordsize_from_type(datatype);\n\n  data = (datadef_t *)(msg + sizeof(messagedef_t)); // the first 8 bytes are version, command and bufsize\n  data->nchans     = nchans;\n  data->nsamples   = nsamples;\n  data->data_type  = datatype;\n  data->bufsize    = nchans * nsamples * wordsize_from_type(datatype);\n\n#ifdef DEBUG\n  Serial.print(\"msg =  \");\n  for (int i = 0; i < sizeof(messagedef_t) + sizeof(datadef_t); i++) {\n    Serial.print(msg[i]);\n    Serial.print(\" \");\n  }\n  Serial.println();\n#endif\n\n  int n = 0;\n  n += client.write(msg, sizeof(messagedef_t) + sizeof(datadef_t));\n  client.flush();\n  n += client.write(buffer, nchans * nsamples * wordsize_from_type(datatype));\n  client.flush();\n\n#ifdef DEBUG\n  Serial.print(\"Wrote \");\n  Serial.print(n);\n  Serial.println(\" bytes\");\n#endif\n\n  if (n == sizeof(messagedef_t) + sizeof(datadef_t) + nchans * nsamples * wordsize_from_type(datatype))\n    return 0;\n  else\n    return -1;\n};\n\nint wordsize_from_type(uint32_t datatype) {\n  int wordsize = 0;\n  switch (datatype) {\n    case DATATYPE_CHAR:\n      wordsize = 1;\n      break;\n    case DATATYPE_UINT8:\n      wordsize = 1;\n      break;\n    case DATATYPE_UINT16:\n      wordsize = 2;\n      break;\n    case DATATYPE_UINT32:\n      wordsize = 4;\n      break;\n    case DATATYPE_UINT64:\n      wordsize = 8;\n      break;\n    case DATATYPE_INT8:\n      wordsize = 1;\n      break;\n    case DATATYPE_INT16:\n      wordsize = 2;\n      break;\n    case DATATYPE_INT32:\n      wordsize = 4;\n      break;\n    case DATATYPE_INT64:\n      wordsize = 8;\n      break;\n    case DATATYPE_FLOAT32:\n      wordsize = 4;\n      break;\n    case DATATYPE_FLOAT64:\n      wordsize = 8;\n      break;\n    }\n  return wordsize;\n};\n"
  },
  {
    "path": "esp32_exgpill/fieldtrip_buffer.h",
    "content": "#ifndef __BUFFER_H_\n#define __BUFFER_H_\n\n#include <WiFiClient.h>\n\n// definition of simplified interface functions, not all of them are implemented\nint fieldtrip_start_server(int port);\nint fieldtrip_open_connection(const char *hostname, int port);\nint fieldtrip_close_connection(int s);\nint fieldtrip_read_header(int server, uint32_t *datatype, uint32_t *nchans, float *fsample, uint32_t *nsamples, uint32_t *nevents);\nint fieldtrip_read_data(int server, uint32_t begsample, uint32_t endsample, byte *buffer);\nint fieldtrip_write_header(int server, uint32_t datatype, uint32_t nchans, float fsample);\nint fieldtrip_write_data(int server, uint32_t datatype, uint32_t nchans, uint32_t nsamples, byte *buffer);\nint fieldtrip_wait_data(int server, uint32_t nsamples, uint32_t nevents, uint32_t milliseconds);\nint wordsize_from_type(uint32_t datatype);\n\n// define the version of the message packet\n#define VERSION    (uint16_t)0x0001\n\n// these define the commands that can be used, which are split over the two available bytes\n#define PUT_HDR    (uint16_t)0x0101 /* decimal 257 */\n#define PUT_DAT    (uint16_t)0x0102 /* decimal 258 */\n#define PUT_EVT    (uint16_t)0x0103 /* decimal 259 */\n#define PUT_OK     (uint16_t)0x0104 /* decimal 260 */\n#define PUT_ERR    (uint16_t)0x0105 /* decimal 261 */\n\n#define GET_HDR    (uint16_t)0x0201 /* decimal 513 */\n#define GET_DAT    (uint16_t)0x0202 /* decimal 514 */\n#define GET_EVT    (uint16_t)0x0203 /* decimal 515 */\n#define GET_OK     (uint16_t)0x0204 /* decimal 516 */\n#define GET_ERR    (uint16_t)0x0205 /* decimal 517 */\n\n#define FLUSH_HDR  (uint16_t)0x0301 /* decimal 769 */\n#define FLUSH_DAT  (uint16_t)0x0302 /* decimal 770 */\n#define FLUSH_EVT  (uint16_t)0x0303 /* decimal 771 */\n#define FLUSH_OK   (uint16_t)0x0304 /* decimal 772 */\n#define FLUSH_ERR  (uint16_t)0x0305 /* decimal 773 */\n\n#define WAIT_DAT   (uint16_t)0x0402 /* decimal 1026 */\n#define WAIT_OK    (uint16_t)0x0404 /* decimal 1027 */\n#define WAIT_ERR   (uint16_t)0x0405 /* decimal 1028 */\n\n#define PUT_HDR_NORESPONSE (uint16_t)0x0501 /* decimal 1281 */\n#define PUT_DAT_NORESPONSE (uint16_t)0x0502 /* decimal 1282 */\n#define PUT_EVT_NORESPONSE (uint16_t)0x0503 /* decimal 1283 */\n\n// these are used in the data_t and event_t structure\n#define DATATYPE_CHAR    (uint32_t)0\n#define DATATYPE_UINT8   (uint32_t)1\n#define DATATYPE_UINT16  (uint32_t)2\n#define DATATYPE_UINT32  (uint32_t)3\n#define DATATYPE_UINT64  (uint32_t)4\n#define DATATYPE_INT8    (uint32_t)5\n#define DATATYPE_INT16   (uint32_t)6\n#define DATATYPE_INT32   (uint32_t)7\n#define DATATYPE_INT64   (uint32_t)8\n#define DATATYPE_FLOAT32 (uint32_t)9\n#define DATATYPE_FLOAT64 (uint32_t)10\n\n// a packet that is sent over the network should contain the following\ntypedef struct {\n  uint16_t version;   // see VERSION\n  uint16_t command;   // see PUT_xxx, GET_xxx and FLUSH_xxx\n  uint32_t bufsize;   // size of the buffer in bytes\n}\nmessagedef_t; // 8 bytes\n\n// the header definition is fixed, it can be followed by additional header chunks\ntypedef struct {\n  uint32_t  nchans;\n  uint32_t  nsamples;\n  uint32_t  nevents;\n  float     fsample;\n  uint32_t  data_type;\n  uint32_t  bufsize;   // size of the buffer in bytes\n}\nheaderdef_t; // 24 bytes\n\n// the data definition is fixed, it should be followed by additional data\ntypedef struct {\n  uint32_t nchans;\n  uint32_t nsamples;\n  uint32_t data_type;\n  uint32_t bufsize;   // size of the buffer in bytes\n}\ndatadef_t; // 16 bytes\n\n// the event definition is fixed, it should be followed by additional data\ntypedef struct {\n  uint32_t type_type;   /* usual would be DATATYPE_CHAR */\n  uint32_t type_numel;  /* length of the type string */\n  uint32_t value_type;\n  uint32_t value_numel;\n  int32_t  sample;\n  int32_t  offset;\n  int32_t  duration;\n  uint32_t bufsize;     /* size of the buffer in bytes */\n} eventdef_t; // 64 bytes\n\n#endif\n"
  },
  {
    "path": "esp32_exgpill/rgb_led.cpp",
    "content": "Yellow#include \"rgb_led.h\"\n\nTicker blinker;\n\n#ifdef COMMON_ANODE\n  #define LED_ON  LOW\n  #define LED_OFF HIGH\n#else\n  #define LED_ON  HIGH\n  #define LED_OFF LOW\n#endif\n\nenum LedColor {\n  RED,\n  GREEN,\n  BLUE,\n  WHITE\n} ledColor;\n\nenum LedState {\n  OFF,\n  ON,\n  SLOW,\n  MEDIUM,\n  FAST\n} ledState;\n\nvoid ledInit() {\n  pinMode(LED_R, OUTPUT);\n  pinMode(LED_G, OUTPUT);\n  pinMode(LED_B, OUTPUT);\n}\n\nvoid ledBlack()\n  ledSet(WHITE, OFF);\n\nvoid ledRed()\n  ledSet(RED, ON);\n\nvoid ledRedSlow()\n  ledSet(RED, SLOW);\n\nvoid ledRedMedium()\n  ledSet(RED, MEDIUM);\n\nvoid ledRedFast()\n  ledSet(RED, FAST);\n\nvoid ledGreen()\n  ledSet(GREEN, ON);\n\nvoid ledGreenSlow()\n  ledSet(GREEN, SLOW);\n\nvoid ledGreenMedium()\n  ledSet(GREEN, MEDIUM);\n\nvoid ledGreenFast()\n  ledSet(GREEN, FAST);\n\nvoid ledBlue()\n  ledSet(BLUE, ON);\n\nvoid ledBlueSlow()\n  ledSet(BLUE, SLOW);\n\nvoid ledBlueMedium()\n  ledSet(BLUE, MEDIUM);\n\nvoid ledBlueFast()\n  ledSet(BLUE, FAST);\n\nvoid ledYellow()\n  ledSet(YELLOW, ON);\n\nvoid ledYellowSlow()\n  ledSet(YELLOW, SLOW);\n\nvoid ledYellowMedium()\n  ledSet(YELLOW, MEDIUM);\n\nvoid ledYellowFast()\n  ledSet(YELLOW, FAST);\n\nvoid ledMagenta()\n  ledSet(MAGENTA, ON);\n\nvoid ledMagentaSlow()\n  ledSet(MAGENTA, SLOW);\n\nvoid ledMagentaMedium()\n  ledSet(MAGENTA, MEDIUM);\n\nvoid ledMagentaFast()\n  ledSet(MAGENTA, FAST);\n\nvoid ledCyan()\n  ledSet(CYAN, ON);\n\nvoid ledCyanSlow()\n  ledSet(CYAN, SLOW);\n\nvoid ledCyanMedium()\n  ledSet(CYAN, MEDIUM);\n\nvoid ledCyanFast()\n  ledSet(CYAN, FAST);\n\nvoid ledWhite()\n  ledSet(WHITE, ON);\n\nvoid ledWhiteSlow()\n  ledSet(WHITE, SLOW);\n\nvoid ledWhiteMedium()\n  ledSet(WHITE, MEDIUM);\n\nvoid ledWhiteFast()\n  ledSet(WHITE, FAST);\n\nvoid ledSet(LedColor color, LedState state){\n  if (ledColor!=color || ledState!=state) {\n    // remember the color and state, required to toggle\n    ledColor = color;\n    ledState = state;\n\n    // switch all off\n    digitalWrite(LED_R, LED_OFF);\n    digitalWrite(LED_G, LED_OFF);\n    digitalWrite(LED_B, LED_OFF);\n\n    // set the initial color\n    if (ledColor==RED || ledColor==YELLOW || ledColor==MAGENTA || ledColor=WHITE)\n      digitalWrite(LED_R, LED_ON);\n    if (ledColor==GREEN || ledColor==YELLOW || ledColor==CYAN || ledColor=WHITE)\n      digitalWrite(LED_G, LED_ON);\n    if (ledColor==BLUE || ledColor==CYAN || ledColor==MAGENTA || ledColor=WHITE)\n      digitalWrite(LED_B, LED_ON);\n\n    // attach the blinker\n    blinker.detach();\n    if (ledState==SLOW)\n      blinker.attach_ms(1000, toggleLed);\n    else if (ledState==MEDIUM)\n      blinker.attach_ms(250, toggleLed);\n    else if (ledState==FAST)\n      blinker.attach_ms(100, toggleLed);\n  }\n}\n\nvoid toggleLed() {\n  if (ledColor==RED || ledColor==YELLOW || ledColor==MAGENTA || ledColor=WHITE)\n    digitalWrite(LED_R, !(digitalRead(LED)));\n  if (ledColor==GREEN || ledColor==YELLOW || ledColor==CYAN || ledColor=WHITE)\n    digitalWrite(LED_G, !(digitalRead(LED_G)));\n  if (ledColor==BLUE || ledColor==CYAN || ledColor==MAGENTA || ledColor=WHITE)\n    digitalWrite(LED_B, !(digitalRead(LED_B)));\n}\n"
  },
  {
    "path": "esp32_exgpill/rgb_led.h",
    "content": "#ifndef _RGB_LED_H_\n#define _RGB_LED_H_\n\n#include <Arduino.h>\n#include <Ticker.h>\n\n// #define COMMON_ANODE\n#define LED_R 12\n#define LED_G 14\n#define LED_B 27\n\nvoid ledInit();\nvoid ledBlack();\n\n// constant color\nvoid ledRed();\nvoid ledGreen();\nvoid ledBlue();\nvoid ledYellow();\nvoid ledMagenta();\nvoid ledCyan();\nvoid ledWhite();\n\n// blink every 1000, 250 or 100 ms\nvoid ledRedSlow();\nvoid ledRedMedium();\nvoid ledRedFast();\nvoid ledGreenSlow();\nvoid ledGreenMedium();\nvoid ledGreenFast();\nvoid ledBlueSlow();\nvoid ledBleMedium();\nvoid ledBlueFast();\nvoid ledYellowSlow();\nvoid ledYellowMedium();\nvoid ledYellowFast();\nvoid ledMagentaSlow();\nvoid ledMagentaMedium();\nvoid ledMagentaFast();\nvoid ledCyanSlow();\nvoid ledCyanMedium();\nvoid ledCyanFast();\nvoid ledWhiteSlow();\nvoid ledWhiteMedium();\nvoid ledWhiteFast();\n\n#endif\n"
  },
  {
    "path": "esp32_exgpill/webinterface.cpp",
    "content": "#include <WebServer.h>\n#include <Arduino.h>\n#include <ArduinoJson.h>\n#include <FS.h>\n#include <SPIFFS.h>\n\n#include \"webinterface.h\"\n\nConfig config;\nextern WebServer server;\nextern unsigned int total;\n\nstatic String getContentType(const String& path) {\n  if (path.endsWith(\".html\"))       return \"text/html\";\n  else if (path.endsWith(\".htm\"))   return \"text/html\";\n  else if (path.endsWith(\".css\"))   return \"text/css\";\n  else if (path.endsWith(\".txt\"))   return \"text/plain\";\n  else if (path.endsWith(\".js\"))    return \"application/javascript\";\n  else if (path.endsWith(\".png\"))   return \"image/png\";\n  else if (path.endsWith(\".gif\"))   return \"image/gif\";\n  else if (path.endsWith(\".jpg\"))   return \"image/jpeg\";\n  else if (path.endsWith(\".jpeg\"))  return \"image/jpeg\";\n  else if (path.endsWith(\".ico\"))   return \"image/x-icon\";\n  else if (path.endsWith(\".svg\"))   return \"image/svg+xml\";\n  else if (path.endsWith(\".xml\"))   return \"text/xml\";\n  else if (path.endsWith(\".pdf\"))   return \"application/pdf\";\n  else if (path.endsWith(\".zip\"))   return \"application/zip\";\n  else if (path.endsWith(\".gz\"))    return \"application/x-gzip\";\n  else if (path.endsWith(\".json\"))  return \"application/json\";\n  return \"application/octet-stream\";\n}\n\nbool defaultConfig() {\n  Serial.println(\"defaultConfig\");\n  strncpy(config.address, \"192.168.1.34\", 32);\n  config.port = 1972;\n  return true;\n}\n\nbool loadConfig() {\n  Serial.println(\"loadConfig\");\n  File configFile = SPIFFS.open(\"/config.json\", \"r\");\n  if (!configFile) {\n    Serial.println(\"Failed to open config file\");\n    return false;\n  }\n\n  size_t size = configFile.size();\n  if (size > 1024) {\n    Serial.println(\"Config file size is too large\");\n    return false;\n  }\n\n  std::unique_ptr<char[]> buf(new char[size]);\n  configFile.readBytes(buf.get(), size);\n\n  StaticJsonBuffer<200> jsonBuffer;\n  JsonObject& root = jsonBuffer.parseObject(buf.get());\n\n  if (!root.success()) {\n    Serial.println(\"Failed to parse config file\");\n    return false;\n  }\n\n  if (root.containsKey(\"address\"))\n    strncpy(config.address, root[\"address\"], 32);\n  if (root.containsKey(\"port\"))\n    config.port = root[\"port\"];\n\n  printConfig();\n  return true;\n}\n\nbool saveConfig() {\n  Serial.println(\"saveConfig\");\n  printConfig();\n  StaticJsonBuffer<200> jsonBuffer;\n  JsonObject& root = jsonBuffer.createObject();\n  root[\"address\"] = config.address;\n  root[\"port\"] = config.port;\n\n  File configFile = SPIFFS.open(\"/config.json\", \"w\");\n  if (!configFile) {\n    Serial.println(\"Failed to open config file for writing\");\n    return false;\n  }\n  else {\n    root.printTo(configFile);\n    return true;\n  }\n}\n\nvoid printConfig() {\n  Serial.print(\"address = \");\n  Serial.println(config.address);\n  Serial.print(\"port = \");\n  Serial.println(config.port);\n}\n\nvoid printRequest() {\n  String message = \"HTTP Request\\n\\n\";\n  message += \"URI: \";\n  message += server.uri();\n  message += \"\\nMethod: \";\n  message += (server.method() == HTTP_GET) ? \"GET\" : \"POST\";\n  message += \"\\nHeaders: \";\n  message += server.headers();\n  message += \"\\n\";\n  for (uint8_t i = 0; i < server.headers(); i++ ) {\n    message += \" \" + server.headerName(i) + \": \" + server.header(i) + \"\\n\";\n  }\n  message += \"\\nArguments: \";\n  message += server.args();\n  message += \"\\n\";\n  for (uint8_t i = 0; i < server.args(); i++) {\n    message += \" \" + server.argName(i) + \": \" + server.arg(i) + \"\\n\";\n  }\n  Serial.println(message);\n}\n\nvoid handleNotFound() {\n  Serial.print(\"handleNotFound: \");\n  Serial.println(server.uri());\n  if (SPIFFS.exists(server.uri())) {\n    handleStaticFile(server.uri());\n  }\n  else {\n    String message = \"File Not Found\\n\\n\";\n    message += \"URI: \";\n    message += server.uri();\n    message += \"\\nMethod: \";\n    message += (server.method() == HTTP_GET) ? \"GET\" : \"POST\";\n    message += \"\\nArguments: \";\n    message += server.args();\n    message += \"\\n\";\n    for (uint8_t i = 0; i < server.args(); i++) {\n      message += \" \" + server.argName(i) + \": \" + server.arg(i) + \"\\n\";\n    }\n    server.setContentLength(message.length());\n    server.send(404, \"text/plain\", message);\n  }\n}\n\nvoid handleRedirect(const char * filename) {\n  handleRedirect((String)filename);\n}\n\nvoid handleRedirect(String filename) {\n  Serial.println(\"handleRedirect: \" + filename);\n  server.sendHeader(\"Location\", filename, true);\n  server.setContentLength(0);\n  server.send(302, \"text/plain\", \"\");\n}\n\nbool handleStaticFile(const char * path) {\n  return handleStaticFile((String)path);\n}\n\nbool handleStaticFile(String path) {\n  Serial.println(\"handleStaticFile: \" + path);\n  String contentType = getContentType(path);            // Get the MIME type\n  if (SPIFFS.exists(path)) {                            // If the file exists\n    File file = SPIFFS.open(path, \"r\");                 // Open it\n    server.setContentLength(file.size());\n    server.streamFile(file, contentType);               // And send it to the client\n    file.close();                                       // Then close the file again\n    return true;\n  }\n  else {\n    Serial.println(\"\\tFile Not Found\");\n    return false;                                         // If the file doesn't exist, return false\n  }\n}\n\nvoid handleJSON() {\n  // this gets called in response to either a PUT or a POST\n  Serial.println(\"handleJSON\");\n  printRequest();\n\n  if (server.hasArg(\"address\") || server.hasArg(\"port\") || server.hasArg(\"var1\") || server.hasArg(\"var2\") || server.hasArg(\"var3\")) {\n    // the body is key1=val1&key2=val2&key3=val3 and the Webserver has already parsed it\n    String str;\n    if (server.hasArg(\"address\")) {\n      str = server.arg(\"address\");\n      strncpy(config.address, str.c_str(), 32);\n    }\n    if (server.hasArg(\"port\")) {\n      str = server.arg(\"port\");\n      config.port = str.toInt();\n    }\n    handleStaticFile(\"/reload_success.html\");\n  }\n  else if (server.hasArg(\"plain\")) {\n    // parse the body as JSON object\n    String body = server.arg(\"plain\");\n    StaticJsonBuffer<200> jsonBuffer;\n    JsonObject& root = jsonBuffer.parseObject(body);\n    if (!root.success()) {\n      handleStaticFile(\"/reload_failure.html\");\n      return;\n    }\n    if (root.containsKey(\"address\"))\n      strncpy(config.address, root[\"address\"], 32);\n    if (root.containsKey(\"port\"))\n      config.port = root[\"port\"];\n    handleStaticFile(\"/reload_success.html\");\n  }\n  else {\n    handleStaticFile(\"/reload_failure.html\");\n    return;\n  }\n  \n  saveConfig();\n}\n"
  },
  {
    "path": "esp32_exgpill/webinterface.h",
    "content": "#ifndef _WEBINTERFACE_H_\n#define _WEBINTERFACE_H_\n\nstruct Config {\n  char address[32];\n  int port;\n};\n\nextern Config config;\n\nbool defaultConfig(void);\nbool loadConfig(void);\nbool saveConfig(void);\nvoid printConfig(void);\nvoid printRequest(void);\n\nvoid handleNotFound(void);\nvoid handleRedirect(String);\nvoid handleRedirect(const char *);\nbool handleStaticFile(String);\nbool handleStaticFile(const char *);\nvoid handleJSON();\n\n#endif\n"
  },
  {
    "path": "esp32_inmp441/RunningStat.h",
    "content": "/*\n   See https://www.johndcook.com/blog/standard_deviation/ and https://www.johndcook.com/blog/skewness_kurtosis/\n*/\n\n#ifndef _RUNNINGSTAT_H_\n#define _RUNNINGSTAT_H_\n\nclass RunningStat\n{\n  public:\n    RunningStat() : m_n(0) {}\n\n    void Clear()\n    {\n      m_n = 0;\n      m_prev = 0;\n      m_crossing = 0;\n    }\n\n    void Push(double x)\n    {\n      m_n++;\n\n      // see https://en.wikipedia.org/wiki/Zero-crossing_rate\n      m_crossing += ((x > 0) ^ (m_prev > 0)); // bitwise xor\n      m_prev = x;\n\n      // See Knuth TAOCP vol 2, 3rd edition, page 232\n      if (m_n == 1)\n      {\n        m_oldM = m_newM = x;\n        m_oldS = 0.0;\n      }\n      else\n      {\n        m_newM = m_oldM + (x - m_oldM) / m_n;\n        m_newS = m_oldS + (x - m_oldM) * (x - m_newM);\n\n        // set up for next iteration\n        m_oldM = m_newM;\n        m_oldS = m_newS;\n      }\n\n      if (m_n == 1) {\n        m_min = x;\n        m_max = x;\n      }\n      else {\n        m_min = (x < m_min ? x : m_min);\n        m_max = (x > m_max ? x : m_max);\n      }\n    }\n\n    int NumDataValues() const\n    {\n      return m_n;\n    }\n\n    double Mean() const\n    {\n      return (m_n > 0) ? m_newM : 0.0;\n    }\n\n    double Variance() const\n    {\n      return ( (m_n > 1) ? m_newS / (m_n - 1) : 0.0 );\n    }\n\n    double StandardDeviation() const\n    {\n      return sqrt( Variance() );\n    }\n\n    double Min() const\n    {\n      return m_min;\n    }\n\n    double Max() const\n    {\n      return m_max;\n    }\n\n    double ZeroCrossingRate() const\n    {\n      return double(m_crossing) / double(m_n) / 2;\n    }\n\n  private:\n    int m_n, m_crossing;\n    double m_oldM, m_newM, m_oldS, m_newS, m_min, m_max, m_prev;\n};\n\n#endif _RUNNINGSTAT_H_\n"
  },
  {
    "path": "esp32_inmp441/esp32_inmp441.ino",
    "content": "/*\n    Sketch for an ESP32 board, like the NodeMCU 32S, LOLIN32 Lite, or the Adafruit Huzzah32\n    connected to a INMP411 I2S microphone\n\n    See https://diyi0t.com/i2s-sound-tutorial-for-esp32/\n    and https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/i2s.html\n\n    This is how I connected the LOLIN32 Lite to the INMP411 module:\n      3V3 - VDD (red)\n      GND - GND (black)\n      26  - SD  (orange)\n      32  - L/R (blue)\n      33  - WS  (green)\n      25  - SCK (purple)\n*/\n\n#if not defined(ESP32)\n#error This is a sketch for an ESP32 board, like the NodeMCU 32S, LOLIN32 Lite, or the Adafruit Huzzah32\n#endif\n\n#include <WiFi.h>\n#include <WiFiUdp.h>\n#include <driver/i2s.h>\n#include <endian.h>\n#include <math.h>\n#include <limits.h>\n\n#include \"secret.h\"\n#include \"RunningStat.h\"\n\n#define USE_DHCP\n#define CONNECT_LOLIN32\n//#define DO_RECONNECT\n//#define SEND_TCP\n//#define PRINT_VALUE\n//#define PRINT_RANGE\n//#define PRINT_FREQUENCY\n#define PRINT_VOLUME\n//#define PRINT_HEADER\n\n#ifndef USE_DHCP\nIPAddress localAddress(192, 168, 1, 100);\nIPAddress gateway(192, 168, 1, 1);\nIPAddress subnet(255, 255, 0, 0);\nIPAddress primaryDNS(8, 8, 8, 8);   //optional\nIPAddress secondaryDNS(8, 8, 4, 4); //optional\n#endif\n\n// IPAddress serverAddress(192, 168, 1, 21);\nIPAddress serverAddress(192, 168, 1, 34);\n\nconst unsigned int serverPort = 4000;\nconst unsigned int recvPort = 4001;\n\nWiFiUDP Udp;    // this is for the local UDP server\nWiFiClient Tcp;\n\nunsigned long blinkTime = 250;\nunsigned long lastBlink = 0;\nunsigned int reconnectInterval = 30000;\nunsigned long lastConnect = 0;\nbool connected = false;\nconst unsigned int sampleRate = 22050;\nconst unsigned int nMessage = 512; // it can be up to 720\nconst unsigned int nBuffer = 64;\nbool meanInitialized = 0;\nconst double alpha = 10. / sampleRate; // if the sampling time dT is much smaller than the time constant T, then alpha=1/(T*sampleRate) and T=1/(alpha*sampleRate)\ndouble signalMean = sqrt(-1); // initialize as not-a-number\ndouble signalScale = 1000;\n\nstruct message_t {\n  uint32_t version = 1;\n  uint32_t id = 0;\n  uint32_t counter;\n  uint32_t samples;\n  int16_t data[nMessage];\n} message __attribute__((packed));\n\nesp_err_t err;\nuint32_t bytes_read;\nint32_t buffer[nBuffer];\n\nconst i2s_port_t I2S_PORT = I2S_NUM_0;\n\nRunningStat shortstat;\nRunningStat longstat;\n\nvoid WiFiEvent(WiFiEvent_t event) {\n  switch (event) {\n    case SYSTEM_EVENT_STA_GOT_IP:\n      Serial.println(\"\");\n      Serial.println(\"WiFi connected.\");\n      Serial.println(\"IP address: \");\n      Serial.println(WiFi.localIP());\n      break;\n    case SYSTEM_EVENT_STA_DISCONNECTED:\n      Serial.println(\"WiFi lost connection.\");\n      connected = false;\n      break;\n    default: break;\n  }\n}\n\nvoid setup() {\n  esp_err_t err;\n\n  Serial.begin(115200);\n\n#ifdef CONNECT_LOLIN32\n#define LED_BUILTIN 22\n  pinMode(32, OUTPUT); \n  digitalWrite(32, LOW);  // L/R\n\n  const i2s_pin_config_t pin_config = {\n    .bck_io_num = 25,                   // Serial Clock (SCK on the INMP441)\n    .ws_io_num = 33,                    // Word Select  (WS on the INMP441)\n    .data_out_num = I2S_PIN_NO_CHANGE,  // not used     (only for speakers)\n    .data_in_num = 26                   // Serial Data  (SD on the INMP441)\n  };\n#endif\n\n#ifdef CONNECT_NODEMCU32\n  const i2s_pin_config_t pin_config = {\n    .bck_io_num = 13,                   // Serial Clock (SCK on the INMP441)\n    .ws_io_num = 15,                    // Word Select  (WS on the INMP441)\n    .data_out_num = I2S_PIN_NO_CHANGE,  // not used     (only for speakers)\n    .data_in_num = 21                   // Serial Data  (SD on the INMP441)\n  };\n#endif\n\n#ifdef CONNECT_HUZZAH32\n  const i2s_pin_config_t pin_config = {\n    .bck_io_num = 32,                   // Serial Clock (SCK on the INMP441)\n    .ws_io_num = 22,                    // Word Select  (WS on the INMP441)\n    .data_out_num = I2S_PIN_NO_CHANGE,  // not used     (only for speakers)\n    .data_in_num = 14                   // Serial Data  (SD on the INMP441)\n  };\n#endif\n\n  // initialize status LED\n  pinMode(LED_BUILTIN, OUTPUT);\n  lastBlink = millis();\n\n  // delete old config\n  WiFi.disconnect(true);\n\n  //register event handler\n  WiFi.onEvent(WiFiEvent);\n\n#ifndef USE_DHCP\n  if (!WiFi.config(localAddress, gateway, subnet, primaryDNS, secondaryDNS)) {\n    Serial.println(\"STA Failed to configure, halt!\");\n    while (true);\n  }\n#endif\n\n  //Initiate connection\n  Serial.println(\"Connecting to WiFi network: \" + String(ssid));\n  WiFi.begin(ssid, password);\n  Serial.println(\"Waiting for WIFI connection...\");\n\n  while (WiFi.status() != WL_CONNECTED) {\n    digitalWrite(LED_BUILTIN, LOW);\n    delay(100);\n    digitalWrite(LED_BUILTIN, HIGH);\n    delay(100);\n    digitalWrite(LED_BUILTIN, LOW);\n    delay(100);\n    digitalWrite(LED_BUILTIN, HIGH);\n    delay(100);\n  }\n\n  // The I2S config as per the example\n  const i2s_config_t i2s_config = {\n    .mode = i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_RX),  // receive, not transfer\n    .sample_rate = sampleRate,                          // sampling rate\n    .bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT,       // could only get it to work with 32bits\n    .channel_format = I2S_CHANNEL_FMT_ONLY_RIGHT,        // channel to use\n    .communication_format = i2s_comm_format_t(I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB),\n    .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,           // interrupt level 1\n    .dma_buf_count = 16,                                // number of buffers, 128 max.\n    .dma_buf_len = nBuffer                              // samples per buffer (minimum is 8)\n  };\n\n  // Configuring the I2S driver and pins.\n  // This function must be called before any I2S driver read/write operations.\n  err = i2s_driver_install(I2S_PORT, &i2s_config, 0, NULL);\n  if (err != ESP_OK) {\n    Serial.printf(\"Failed installing driver: %d, halt!\\n\", err);\n    while (true);\n  }\n  Serial.println(\"I2S driver installed.\");\n\n  err = i2s_set_pin(I2S_PORT, &pin_config);\n  if (err != ESP_OK) {\n    Serial.printf(\"Failed setting pin: %d, halt!\\n\", err);\n    while (true);\n  }\n  Serial.println(\"I2S pins set.\");\n\n  // use the last number of the IP address as identifier\n  message.id = WiFi.localIP()[3];\n\n  for (int i=0; i < sampleRate; i++) {\n    err = i2s_read(I2S_PORT, buffer, 4, &bytes_read, 0);\n  }\n\n} // setup\n\nvoid loop() {\n\n  if ((millis() - lastBlink) > 2000) {\n    digitalWrite(LED_BUILTIN, LOW);\n    lastBlink = millis();\n  }\n  else if ((millis() - lastBlink) > 1000) {\n    digitalWrite(LED_BUILTIN, HIGH);\n  }\n\n  size_t samples = nMessage - message.samples;\n\n  if (samples > nBuffer) {\n    // do not read more than nBuffer at a time\n    samples = nBuffer;\n  }\n\n  err = i2s_read(I2S_PORT, buffer, samples * 4, &bytes_read, 0);\n\n  if (err == ESP_OK) {\n    for (unsigned int sample=0; sample < bytes_read / 4; sample++) {\n\n      double value = buffer[sample];\n\n      // compute a smooth running mean with https://en.wikipedia.org/wiki/Exponential_smoothing\n      if (isnan(signalMean)) {\n        signalMean = value;\n      }\n      else {\n        signalMean = alpha * value + (1 - alpha) * signalMean;\n      }\n      value -= signalMean;\n      value /= signalScale;\n\n      // https://en.wikipedia.org/wiki/Sigmoid_function\n      value /= 32767;\n      value /= (1 + fabs(value));\n      value *= 32767;\n\n#ifdef PRINT_VALUE\n      Serial.println(value);\n#endif\n\n      message.data[message.samples] = value; // this casts the value to an int16\n      message.samples++;\n\n      shortstat.Push(value);\n      longstat.Push(value);\n    }\n\n    if (message.samples == nMessage) {\n\n#ifdef PRINT_RANGE\n      Serial.print(shortstat.Min());\n      Serial.print(\", \");\n      Serial.print(shortstat.Mean());\n      Serial.print(\", \");\n      Serial.println(shortstat.Max());\n#endif\n\n#ifdef PRINT_FREQUENCY\n      Serial.println(shortstat.ZeroCrossingRate() * sampleRate);\n#endif\n\n#ifdef PRINT_VOLUME\n      Serial.println(10*log10(shortstat.Variance()));\n#endif\n\n#ifdef PRINT_HEADER\n      Serial.print(message.version); Serial.print(\", \");\n      Serial.print(message.counter); Serial.print(\", \");\n      Serial.print(message.samples); Serial.print(\", \");\n      Serial.print(message.data[0]); Serial.println();\n#endif\n\n#ifdef DO_RECONNECT\n      if  ((millis() - lastConnect) > reconnectInterval) {\n        // reconnect, but don't try to reconnect too often\n        lastConnect = millis();\n\n        // turn the status LED on\n        digitalWrite(LED_BUILTIN, LOW);\n        lastBlink = millis();\n\n        if (!connected) {\n          connected = Tcp.connect(serverAddress, serverPort);\n          if (connected) {\n            Serial.print(\"Connected to \");\n            Serial.println(serverAddress);\n          }\n          else {\n            Serial.print(\"Failed to connect to \");\n            Serial.println(serverAddress);\n          }\n        }\n      }\n#endif\n\n#ifdef SEND_TCP\n      if (connected) {\n        blinkTime = 1000;\n        int count = Tcp.write((uint8_t *)(&message), sizeof(message));\n        connected = (count == sizeof(message));\n      }\n      else {\n        blinkTime = 250;\n      }\n#endif\n\n      shortstat.Clear();\n      message.counter++;\n      message.samples = 0;\n    }\n  }\n  else {\n    Serial.println(\"i2s_read error, halt!\");\n    while (true);\n  }\n\n} // loop\n"
  },
  {
    "path": "esp32_inmp441/util.h",
    "content": "#ifndef _UTIL_H_\n#define _UTIL_H_\n\nvoid print_binary(uint32_t val) {\n  for (int l = 31; l >= 24; l--) {\n    uint8_t b = (val >> l) & 0x01;\n    Serial.print(b);\n  }\n  Serial.print(' ');\n  for (int l = 23; l >= 16; l--) {\n    uint8_t b = (val >> l) & 0x01;\n    Serial.print(b);\n  }\n  Serial.print(' ');\n  for (int l = 15; l >= 8; l--) {\n    uint8_t b = (val >> l) & 0x01;\n    Serial.print(b);\n  }\n  Serial.print(' ');\n  for (int l = 7; l >= 0; l--) {\n    uint8_t b = (val >> l) & 0x01;\n    Serial.print(b);\n  }\n  Serial.println();\n}\n\n#endif\n"
  },
  {
    "path": "esp32_sph0645/RunningStat.h",
    "content": "/*\n   See https://www.johndcook.com/blog/standard_deviation/\n*/\n\nclass RunningStat\n{\n  public:\n    RunningStat() : m_n(0) {}\n\n    void Clear()\n    {\n      m_n = 0;\n    }\n\n    void Push(double x)\n    {\n      m_n++;\n\n      // See Knuth TAOCP vol 2, 3rd edition, page 232\n      if (m_n == 1)\n      {\n        m_oldM = m_newM = x;\n        m_oldS = 0.0;\n      }\n      else\n      {\n        m_newM = m_oldM + (x - m_oldM) / m_n;\n        m_newS = m_oldS + (x - m_oldM) * (x - m_newM);\n\n        // set up for next iteration\n        m_oldM = m_newM;\n        m_oldS = m_newS;\n      }\n\n      if (m_n == 1) {\n        m_min = x;\n        m_max = x;\n      }\n      else {\n        m_min = (x < m_min ? x : m_min);\n        m_max = (x > m_max ? x : m_max);\n      }\n    }\n\n    int NumDataValues() const\n    {\n      return m_n;\n    }\n\n    double Mean() const\n    {\n      return (m_n > 0) ? m_newM : 0.0;\n    }\n\n    double Variance() const\n    {\n      return ( (m_n > 1) ? m_newS / (m_n - 1) : 0.0 );\n    }\n\n    double StandardDeviation() const\n    {\n      return sqrt( Variance() );\n    }\n\n    double Min() const\n    {\n      return m_min;\n    }\n\n    double Max() const\n    {\n      return m_max;\n    }\n\n  private:\n    int m_n;\n    double m_oldM, m_newM, m_oldS, m_newS, m_min, m_max;\n};\n"
  },
  {
    "path": "esp32_sph0645/compress.cpp",
    "content": "#include <Arduino.h>\n\nuint32_t compress(uint32_t *data, uint32_t nsamples, uint8_t *dest) {\n  uint32_t nbytes = 0;\n  uint32_t *ptr = (uint32_t *)dest;\n  for (int i; i < nsamples; i++) {\n    ptr[i] = data[i];\n    nbytes += 4;\n  }\n  return nbytes;\n} // compress\n\n\nuint32_t decompress(uint32_t *dest, uint32_t nsamples, uint8_t *data) {\n  uint32_t nbytes = 0;\n  uint32_t *ptr = (uint32_t *)data;\n  for (int i; i < nsamples; i++) {\n    dest[i] = ptr[i];\n    nbytes += 4;\n  }\n  return nbytes;\n} // decompress\n"
  },
  {
    "path": "esp32_sph0645/esp32_sph0645.ino",
    "content": "/*\n   Sketch for ESP32 board, like the NodeMCU 32S, LOLIN32 Lite, or the Adafruit Huzzah32\n   connected to a SPH0645 I2S microphone\n\n   See https://diyi0t.com/i2s-sound-tutorial-for-esp32/\n   and https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/i2s.html\n\n*/\n\n#if not defined(ESP32)\n#error This is a sketch for an ESP32 board, like the NodeMCU 32S, LOLIN32 Lite, or the Adafruit Huzzah32\n#endif\n\n#include <WiFi.h>\n#include <WiFiUdp.h>\n#include <driver/i2s.h>\n#include <endian.h>\n#include <math.h>\n\n#include \"secret.h\"\n#include \"RunningStat.h\"\n\n#ifndef htonl\n#define htonl htobe32\n#endif\n\n#ifndef htons\n#define htons htobe16\n#endif\n\n#ifndef ntohl\n#define htons be32toh\n#endif\n\n#ifndef ntohs\n#define htons be16toh\n#endif\n\n#define USE_DHCP\n\n#ifdef USE_DHCP\nIPAddress localAddress(192, 168, 1, 100);\nIPAddress gateway(192, 168, 1, 1);\nIPAddress subnet(255, 255, 0, 0);\nIPAddress primaryDNS(8, 8, 8, 8);   //optional\nIPAddress secondaryDNS(8, 8, 4, 4); //optional\n#endif\n\n// this is for the remote UDP server\nIPAddress sendAddress(192, 168, 1, 21);\nconst unsigned int sendPort = 4000;\n\n// this is for the local UDP server\nWiFiUDP Udp;\nconst unsigned int recvPort = 4001;\n\nconst unsigned int nMessage = 720;\nconst unsigned int nBuffer = 64;\nconst float decay = 0.1;\nunsigned long lastBlink = 0;\nunsigned long failedPackets = 0;\nunsigned long previous = 0;\nbool connected = false;\nconst unsigned int sampleRate = 22050;\nfloat runningMean = -1;\n\nstruct message_t {\n  uint32_t version = 1;\n  uint32_t id = 0;\n  uint32_t counter;\n  uint32_t samples;\n  int16_t data[nMessage];\n} message __attribute__((packed));\n\nstruct response_t {\n  uint32_t version = 1;\n  uint32_t id = 0;\n  uint32_t counter;\n} response __attribute__((packed));\n\nconst i2s_port_t I2S_PORT = I2S_NUM_0;\n\nRunningStat shortstat;\nRunningStat longstat;\n\nvoid WiFiEvent(WiFiEvent_t event) {\n  switch (event) {\n    case SYSTEM_EVENT_STA_GOT_IP:\n      Serial.println(\"\");\n      Serial.println(\"WiFi connected.\");\n      Serial.println(\"IP address: \");\n      Serial.println(WiFi.localIP());\n      Udp.begin(WiFi.localIP(), recvPort);\n      connected = true;\n      break;\n    case SYSTEM_EVENT_STA_DISCONNECTED:\n      Serial.println(\"WiFi lost connection.\");\n      connected = false;\n      break;\n    default: break;\n  }\n}\n\nvoid print_binary(uint32_t val) {\n  for (int l = 31; l >= 24; l--) {\n    uint8_t b = (val >> l) & 0x01;\n    Serial.print(b);\n  }\n  Serial.print(' ');\n  for (int l = 23; l >= 16; l--) {\n    uint8_t b = (val >> l) & 0x01;\n    Serial.print(b);\n  }\n  Serial.print(' ');\n  for (int l = 15; l >= 8; l--) {\n    uint8_t b = (val >> l) & 0x01;\n    Serial.print(b);\n  }\n  Serial.print(' ');\n  for (int l = 7; l >= 0; l--) {\n    uint8_t b = (val >> l) & 0x01;\n    Serial.print(b);\n  }\n  Serial.println();\n}\n\nvoid setup() {\n  esp_err_t err;\n\n  Serial.begin(115200);\n\n  // initialize status LED\n  pinMode(LED_BUILTIN, OUTPUT);\n  lastBlink = millis();\n\n  // delete old config\n  WiFi.disconnect(true);\n\n  //register event handler\n  WiFi.onEvent(WiFiEvent);\n\n#ifndef USE_DHCP\n  if (!WiFi.config(localAddress, gateway, subnet, primaryDNS, secondaryDNS)) {\n    Serial.println(\"STA Failed to configure\");\n    while (true);\n  }\n#endif\n\n  //Initiate connection\n  Serial.println(\"Connecting to WiFi network: \" + String(ssid));\n  WiFi.begin(ssid, password);\n  Serial.println(\"Waiting for WIFI connection...\");\n\n  while (WiFi.status() != WL_CONNECTED) {\n    digitalWrite(LED_BUILTIN, LOW);\n    delay(100);\n    digitalWrite(LED_BUILTIN, HIGH);\n    delay(100);\n    digitalWrite(LED_BUILTIN, LOW);\n    delay(100);\n    digitalWrite(LED_BUILTIN, HIGH);\n    delay(100);\n  }\n\n  // The I2S config as per the example\n  const i2s_config_t i2s_config = {\n    .mode = i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_RX),  // receive, not transfer\n    .sample_rate = sampleRate,                          // sampling rate\n    .bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT,       // could only get it to work with 32bits\n    .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,        // channel to use\n    .communication_format = i2s_comm_format_t(I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB),\n    .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,           // interrupt level 1\n    .dma_buf_count = 16,                                // number of buffers, 128 max.\n    .dma_buf_len = nBuffer                              // samples per buffer (minimum is 8)\n  };\n\n  // setup for the NODEMCU32\n  const i2s_pin_config_t NODEMCU32_pin_config = {\n    .bck_io_num = 26,                   // Serial Clock (BCLK on the SPH0645)\n    .ws_io_num = 25,                    // Word Select  (LRCL on the SPH0645)\n    .data_out_num = I2S_PIN_NO_CHANGE,  // not used     (only for speakers)\n    .data_in_num = 33                   // Serial Data  (DOUT on the SPH0645)\n  };\n\n  // setup for the HUZZAH32\n  const i2s_pin_config_t HUZZAH32_pin_config = {\n    .bck_io_num = 32,                   // Serial Clock (BCLK on the SPH0645)\n    .ws_io_num = 22,                    // Word Select  (LRCL on the SPH0645)\n    .data_out_num = I2S_PIN_NO_CHANGE,  // not used     (only for speakers)\n    .data_in_num = 14                   // Serial Data  (DOUT on the SPH0645)\n  };\n\n  // Configuring the I2S driver and pins.\n  // This function must be called before any I2S driver read/write operations.\n  err = i2s_driver_install(I2S_PORT, &i2s_config, 0, NULL);\n  if (err != ESP_OK) {\n    Serial.printf(\"Failed installing driver: %d\\n\", err);\n    while (true);\n  }\n  Serial.println(\"I2S driver installed.\");\n\n  err = i2s_set_pin(I2S_PORT, &NODEMCU32_pin_config);\n  if (err != ESP_OK) {\n    Serial.printf(\"Failed setting pin: %d\\n\", err);\n    while (true);\n  }\n  Serial.println(\"I2S pins set.\");\n\n  // use the last digit of the IP address as identifier\n  message.id = WiFi.localIP()[3];\n\n} // setup\n\nvoid loop() {\n  esp_err_t err;\n  uint32_t bytes_read;\n  uint32_t buffer[nBuffer];\n\n  if ((millis() - lastBlink) > 2000) {\n    digitalWrite(LED_BUILTIN, LOW);\n    lastBlink = millis();\n  }\n  else if ((millis() - lastBlink) > 1000) {\n    digitalWrite(LED_BUILTIN, HIGH);\n  }\n\n  size_t samples = nMessage - message.samples;\n\n  if (samples > nBuffer) {\n    // do not read more than nBuffer at a time\n    samples = nBuffer;\n  }\n\n  err = i2s_read(I2S_PORT, buffer, samples * 4, &bytes_read, 0);\n\n  if (err == ESP_OK) {\n    for (unsigned int sample; sample < bytes_read / 4; sample++) {\n\n      uint32_t value = buffer[sample];\n      value = value >> 14;  // convert to 18 bit\n      value = value >> 3;   // convert to 15 bit\n\n      if (runningMean < 0) {\n        runningMean = value;\n      }\n      else {\n        runningMean = decay * value + (1 - decay) * runningMean;\n      }\n\n      int16_t demeaned = ((float)value - (float)runningMean);\n\n      message.data[message.samples] = htons(demeaned);\n      message.samples++;\n\n      shortstat.Push(demeaned);\n      longstat.Push(demeaned);\n    }\n\n    if (message.samples == nMessage) {\n\n      /*\n        Serial.print(message.version); Serial.print(\", \");\n        Serial.print(message.counter); Serial.print(\", \");\n        Serial.print(message.samples); Serial.print(\", \");\n        Serial.print(ntohs(message.data[0])); Serial.println();\n\n        Serial.print(shortstat.Min());\n        Serial.print(\", \");\n        Serial.print(shortstat.Mean());\n        Serial.print(\", \");\n        Serial.println(shortstat.Max());\n      */\n\n      if (connected) {\n        Udp.beginPacket(sendAddress, sendPort);\n        Udp.write((uint8_t *)(&message), sizeof(message));\n        Udp.endPacket();\n      }\n\n      shortstat.Clear();\n      message.counter++;\n      message.samples = 0;\n    }\n  }\n\n  // receive incoming UDP packets\n  int packetSize = Udp.parsePacket();\n  if (packetSize) {\n    // Serial.printf(\"Received %d bytes from %s, port %d\\n\", packetSize, Udp.remoteIP().toString().c_str(), Udp.remotePort());\n    int len = Udp.read((char *) &response, sizeof(response));\n    if (len == sizeof(response)) {\n      if (response.counter != previous+1) {\n        failedPackets++;\n        Serial.print(\"missing response: \");\n        Serial.print(previous);\n        Serial.print(\", \");\n        Serial.print(response.counter);\n        Serial.println();\n      }\n      previous = response.counter;\n    }\n  }\n\n} // loop\n"
  },
  {
    "path": "esp32c6_feeder/README.md",
    "content": "# Pet feeder\n\nThis Arduino sketch is for a battery operated pet feeder based on a XIAO ESP32c6. It uses deep \nsleep to conserve power. When it wakes up, it gets a JSON document from a URL that specifies whether \nand how much to feed. It then moves a servo to provide the required number of portions and goes\nback to sleep.\n\nThe JSON document is dynamically hosted by Node-Red running on a Raspberri Pi, which allows for\nreprogramming the feeding schedule.\n\nTo prevent the servo motor from jerking to the default 0 angle upon every wake up and also to \npreserve power, it is connected over a bs170 transistor. This alows the ESP to switch the servo\nmotor power on only when needed.\n"
  },
  {
    "path": "esp32c6_feeder/esp32c6_feeder.ino",
    "content": "/* \nThis Arduino sketch is for a battery operated pet feeder based on a XIAO ESP32c6. It uses deep \nsleep to conserve power. When it wakes up, it gets a JSON document from a URL that specifies whether \nand how much to feed. It then moves a servo to provide the required number of portions and goes\nback to sleep.\n\nThe JSON document is dynamically hosted by Node-Red running on a Raspberri Pi, which allows for\nreprogramming the feeding schedule.\n\nTo prevent the servo motor from jerking to the default 0 angle upon every wake up and also to \npreserve power, it is connected over a bs170 transistor. This alows the ESP to switch the servo\nmotor power on only when needed.\n*/\n\n#include <WiFi.h>\n#include <HTTPClient.h>\n#include <ArduinoJson.h>\n#include <ESP32Servo.h>\n\n#include \"secret.h\"\n\n#define SERVO_CONTROL 20          // D9 is GPIO20, this is the control line for the servo\n#define SERVO_ENABLE 18           // D10 is GPIO18, this goes over a 1 kOhm resistor to a BS170 transistor \n#define uS_TO_S_FACTOR 1000000ULL // conversion factor for micro seconds to seconds\n\nWiFiClient wifi_client;\nHTTPClient http_client;\nServo myservo;\n\n// defaults following a hard reset\nRTC_DATA_ATTR int interval = 60;\nRTC_DATA_ATTR int amount = 0;\n\nconst long wifiTimeout = 10000;\nconst char* version = __DATE__ \" / \" __TIME__;\n\nvoid deepsleep() {\n  Serial.print(\"Going asleep for \");\n  Serial.print(interval);\n  Serial.println(\" seconds.\");\n  Serial.flush();\n  wifi_client.flush();\n  wifi_client.stop();\n  esp_sleep_enable_timer_wakeup(interval * uS_TO_S_FACTOR);\n  esp_deep_sleep_start();\n}\n\nvoid moveServo() {\n  // these have been experimentally calibrated\n  const int left = 10;\n  const int right = 74;\n  const int middle = (left + right) / 2;\n\n  Serial.println(\"Moving servo.\");\n\n  // start in the middle position\n  myservo.write(middle);\n  // switch on the power to the servo motor\n  digitalWrite(SERVO_ENABLE, HIGH);\n\n  // move to the right to pick up the food\n  for (int posDegrees = middle; posDegrees <= right; posDegrees++) {\n    myservo.write(posDegrees);\n    delay(50);\n  }\n\n  // wait for it to drop into the slider\n  delay(1000);\n\n  // move to the left to drop the food\n  for (int posDegrees = right; posDegrees >= left; posDegrees--) {\n    myservo.write(posDegrees);\n    delay(50);\n  }\n\n  // wait for it to drop out of the slider\n  delay(1000);\n\n  // move back to the middle position\n  for (int posDegrees = left; posDegrees <= middle; posDegrees++) {\n    myservo.write(posDegrees);\n    delay(50);\n  }\n\n  // switch off the power to the servo motor\n  digitalWrite(SERVO_ENABLE, LOW);\n}\n\nvoid setup() {\n  Serial.begin(115200);\n  Serial.print(\"\\n[esp32c6_feeder / \");\n  Serial.print(version);\n  Serial.println(\"]\");\n\n  // switch on the LED\n  pinMode(LED_BUILTIN, OUTPUT);\n  digitalWrite(LED_BUILTIN, LOW);  // it is inverted, this switches it on\n\n  // set up the servo, but initially with the power disabled\n  pinMode(SERVO_ENABLE, OUTPUT);\n  digitalWrite(SERVO_ENABLE, LOW);\n  myservo.setPeriodHertz(50);\n  myservo.attach(SERVO_CONTROL, 500, 2500);\n\n  Serial.println(\"Connecting to WiFi...\");\n  WiFi.begin(ssid, password);\n  long now = millis();\n  while (WiFi.status() != WL_CONNECTED) {\n    Serial.print(\".\");\n    if ((millis() - now) > wifiTimeout) {\n      Serial.println(\"\");\n      Serial.println(\"failed!\");\n      deepsleep();  // try again later\n    }\n    delay(500);\n  }\n  Serial.println(\"\");\n  Serial.println(\"WiFi connected\");\n  Serial.print(\"IP address: \");\n  Serial.println(WiFi.localIP());\n  Serial.print(\"RRSI: \");\n  Serial.println(WiFi.RSSI());\n\n  // get the instructions from a Raspberry Pi that is running Node-Red\n  http_client.useHTTP10(true);\n  http_client.begin(\"http://192.168.1.16:1880/feeder\");\n  if (http_client.GET()) {\n    String payload = http_client.getString();\n    Serial.println(payload);\n\n    StaticJsonDocument<256> doc;\n    deserializeJson(doc, payload);\n\n    // these default to 0 when they are not present in the JSON document\n    interval = doc[\"interval\"].as<int>();\n    amount = doc[\"amount\"].as<int>();\n\n    Serial.print(\"interval = \");\n    Serial.println(interval);\n\n    Serial.print(\"amount = \");\n    Serial.println(amount);\n\n    // provide food by moving the servo\n    for (int i = 0; i < amount; i++)\n      moveServo();\n  }\n  http_client.end();\n\n  deepsleep();\n}\n\nvoid loop() {\n  // it never gets here\n}\n"
  },
  {
    "path": "esp8266_12v_trigger/esp8266_12v_trigger.ino",
    "content": "/*\n *  This sketch serves as a 12 volt trigger to switch a NAD-D3020 audio amplifier on and off.\n *  The on and off state are controlled over a http call. The current status can also be probed.\n *\n *  The GPOI output of pin D6 is used to switch a PC900v optocoupler. Its output voltage can be\n *  toggled between 0 and 5 volt, which is is enough to trigger the amplifier.\n *\n *  See https://robertoostenveld.nl/12-volt-trigger-for-nad-d3020-amplifier/\n *\n *  Valid HTTP calls are\n *    http://nad-d3020.local/on\n *    http://nad-d3020.local/off\n *    http://nad-d3020.local/status   this returns on or off\n *    http://nad-d3020.local/version\n *    http://nad-d3020.local/reset\n *    http://nad-d3020.local/         this returns an identifier\n *\n *  (C) 2017-2019 Robert Oostenveld\n *\n */\n\n#include <ESP8266WiFi.h>\n#include <WiFiClient.h>\n#include <ESP8266WebServer.h>\n#include <ESP8266mDNS.h>\n#include <DNSServer.h>\n#include <WiFiManager.h>         //https://github.com/tzapu/WiFiManager\n\n#define PINOUT D6\n\nconst char* version = __DATE__ \" / \" __TIME__;\nconst char* host = \"NAD-D3020\";\n\nESP8266WebServer server(80);\n\nvoid handleRoot() {\n  Serial.println(\"/\");\n  server.send(200, \"text/plain\", host);\n}\n\nvoid handleNotFound() {\n  Serial.print(\"handleNotFound: \");\n  Serial.println(server.uri());\n  if (SPIFFS.exists(server.uri())) {\n    handleStaticFile(server.uri());\n  }\n  else {\n    String message = \"File Not Found\\n\\n\";\n    message += \"URI: \";\n    message += server.uri();\n    message += \"\\nMethod: \";\n    message += (server.method() == HTTP_GET) ? \"GET\" : \"POST\";\n    message += \"\\nArguments: \";\n    message += server.args();\n    message += \"\\n\";\n    for (uint8_t i = 0; i < server.args(); i++) {\n      message += \" \" + server.argName(i) + \": \" + server.arg(i) + \"\\n\";\n    }\n    server.setContentLength(message.length());\n    server.send(404, \"text/plain\", message);\n  }\n}\n\nlong checkpoint = millis();\n\nvoid setup(void) {\n  pinMode(PINOUT, OUTPUT);\n  digitalWrite(PINOUT, HIGH); // note that the logic is inverted\n\n  Serial.begin(115200);\n  while (!Serial) {\n    ;\n  }\n \n  Serial.print(\"\\n[esp8266_12v_trigger / \");\n  Serial.print(version);\n  Serial.println(\"]\");\n\n  WiFiManager wifiManager;\n  wifiManager.setAPStaticIPConfig(IPAddress(192, 168, 1, 1), IPAddress(192, 168, 1, 1), IPAddress(255, 255, 255, 0));\n  wifiManager.autoConnect(host);\n  Serial.println(\"connected\");\n\n  Serial.println(\"\");\n  Serial.println(\"Connected\");\n  Serial.print(\"IP address: \");\n  Serial.println(WiFi.localIP());\n\n  if (MDNS.begin(host)) {\n    Serial.println(\"MDNS responder started\");\n  }\n\n  server.on(\"/\", handleRoot);\n\n  server.on(\"/on\", []() {\n    Serial.println(\"/on\");\n    digitalWrite(PINOUT, LOW); // note that the logic is inverted\n    server.send(200, \"text/plain\", \"on\");\n  });\n\n  server.on(\"/off\", []() {\n    Serial.println(\"/off\");\n    digitalWrite(PINOUT, HIGH); // note that the logic is inverted\n    server.send(200, \"text/plain\", \"off\");\n  });\n\n  server.on(\"/status\", []() {\n    Serial.println(\"/status\");\n    if (digitalRead(PINOUT)) // note that the logic is inverted\n      server.send(200, \"text/plain\", \"off\");\n    else\n      server.send(200, \"text/plain\", \"on\");\n  });\n\n  server.on(\"/version\", []() {\n    Serial.println(\"/version\");\n    server.send(200, \"text/plain\", version);\n  });\n\n  server.on(\"/restart\", []() {\n    Serial.println(\"/restart\");\n    server.send(200, \"text/plain\", \"restart\");\n    ESP.restart();\n  });\n\n  server.on(\"/reset\", []() {\n    Serial.println(\"/reset\");\n    server.send(200, \"text/plain\", \"reset\");\n    ESP.reset();\n  });\n\n  server.onNotFound(handleNotFound);\n\n  server.begin();\n  Serial.println(\"HTTP server started\");\n} // setup\n\nvoid loop(void) {\n  // check the connection status every 10 seconds\n  if ((millis()-checkpoint) > 10000) {\n    if (WiFi.status() != WL_CONNECTED) {\n      Serial.println(\"restart\");\n      ESP.restart();\n    }\n    else\n      checkpoint = millis();\n    }\n  // process incoming http requests\n  server.handleClient();\n} // loop\n"
  },
  {
    "path": "esp8266_ad8232_ecg/README.md",
    "content": "# Overview\n\nThis is an Arduino sketch for an ESP8266 module connected to an AD8232 ECG sensor module. It uses the ADC to sample the data and writes it as a continuous stream to a computer that is running a FieldTrip buffer.\n\nYou can use the webinterface to set the IP address and port of the receiving computer.\n\n## SPIFFS for static files\n\nYou should not only write the firmware to the ESP8266 module, but also the static content for the web interface. The html, css and javascript files located in the data directory should be written to the SPIFS filesystem on the ESP8266. See for example http://esp8266.github.io/Arduino/versions/2.0.0/doc/filesystem.html and https://www.instructables.com/id/Using-ESP8266-SPIFFS for instructions.\n\nYou will get a \"file not found\" error if the firmware cannot access the data files.\n\n## Arduino ESP8266 filesystem uploader\n\nThis Arduino sketch includes a `data` directory with a number of files that should be uploaded to the ESP8266 using the [SPIFFS filesystem uploader](https://github.com/esp8266/arduino-esp8266fs-plugin) tool. At the moment (Feb 2024) the Arduino 2.x IDE does *not* support the SPIFFS filesystem uploader plugin. You have to use the Arduino 1.8.x IDE (recommended), or the command line utilities for uploading the data.\n"
  },
  {
    "path": "esp8266_ad8232_ecg/blink_led.cpp",
    "content": "#include \"blink_led.h\"\n\nTicker blinker;\n\nenum {\n  LED_ON,\n  LED_OFF,\n  LED_SLOW,\n  LED_MEDIUM,\n  LED_FAST,\n} ledState;\n\nvoid changeState() {\n  digitalWrite(LED, !(digitalRead(LED)));  // Invert the current state of the LED\n}\n\nvoid ledInit() {\n  pinMode(LED, OUTPUT);\n}\n\nvoid ledOn() {\n  if (ledState != LED_ON) {\n    ledState = LED_ON;\n    blinker.detach();\n    digitalWrite(LED, LOW);\n  }\n}\n\nvoid ledOff() {\n  if (ledState != LED_OFF) {\n    ledState = LED_OFF;\n    blinker.detach();\n    digitalWrite(LED, HIGH);\n  }\n}\n\nvoid ledSlow() {\n  if (ledState != LED_SLOW) {\n    ledState = LED_SLOW;\n    blinker.detach();\n    blinker.attach_ms(1000, changeState);\n  }\n}\n\nvoid ledMedium() {\n  if (ledState != LED_MEDIUM) {\n    ledState = LED_MEDIUM;\n    blinker.detach();\n    blinker.attach_ms(250, changeState);\n  }\n}\n\nvoid ledFast() {\n  if (ledState != LED_FAST) {\n    ledState = LED_FAST;\n    blinker.detach();\n    blinker.attach_ms(100, changeState);\n  }\n}\n"
  },
  {
    "path": "esp8266_ad8232_ecg/blink_led.h",
    "content": "#ifndef _BLINK_LED_H_\n#define _BLINK_LED_H_\n\n#include <Arduino.h>\n#include <Ticker.h>\n\n#define LED 2 // GPIO2 is connected to the builtin LED\n\nvoid ledInit(void);\nvoid ledOn(void);\nvoid ledOff(void);\nvoid ledSlow(void);\nvoid ledMedium(void);\nvoid ledFast(void);\n\n#endif\n"
  },
  {
    "path": "esp8266_ad8232_ecg/data/config.json",
    "content": "{\n  \"address\": \"192.168.1.34\",\n  \"port\": 1972\n}\n"
  },
  {
    "path": "esp8266_ad8232_ecg/data/hello.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Hello</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n<p>Hello llama!</p>\n<img src=\"llama.jpg\">\n</body>\n</html>\n"
  },
  {
    "path": "esp8266_ad8232_ecg/data/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Index</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n<a href=\"/hello.html\">Hello</a></br>\n<a href=\"/monitor.html\">Monitor</a></br>\n<a href=\"/settings.html\">Change settings</a></br>\n<a href=\"/reconnect\">Reconnect WiFi</a></br>\n<a href=\"/defaults\">Default settings</a></br>\n<a href=\"/update.html\">Update firmware</a></br>\n<a href=\"/restart\">Restart hardware</a></br>\n</body>\n</html>\n"
  },
  {
    "path": "esp8266_ad8232_ecg/data/monitor.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Monitor</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n\n<h1>Monitor</h1>\n\nFirmware version:\n<div id=\"version\" name=\"version\">?</div>\n\nUptime:\n<div id=\"uptime\" name\"uptime\">?</div>\n\n<script>\n  async function updateContent() {\n    const response = await fetch(\"json\");\n    const data = await response.json();\n    console.log(data);\n    document.getElementById(\"version\").innerHTML = data[\"version\"];\n    document.getElementById(\"uptime\").innerHTML = data[\"uptime\"];\n  }\n  updateContent();\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "esp8266_ad8232_ecg/data/reload_failure.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Failure</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n\n<p>Failure</p>\n\n<script type=\"text/javascript\">\n  setTimeout(function(){location=\"/index.html\"},1000);\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "esp8266_ad8232_ecg/data/reload_success.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Success</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n\n<p>Success</p>\n\n<script type=\"text/javascript\">\n  setTimeout(function(){location=\"/index.html\"},1000);\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "esp8266_ad8232_ecg/data/settings.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Settings</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n\n<h1>Settings</h1>\n<form id=\"settings-form\" method=\"post\" action=\"json\">\n\n    <div class=\"field\">\n        <label for=\"address\">address:</label>\n        <input type=\"text\" id=\"address\" name=\"address\" value=\"?\" required>\n    </div>\n\n    <div class=\"field\">\n        <label for=\"port\">port:</label>\n        <input type=\"text\" id=\"port\" name=\"port\" value=\"?\" required>\n    </div>\n\n    <div class=\"field\">\n        <button type=\"submit\">Save</button>\n    </div>\n</form>\n\n<script>\n  async function updateContent() {\n    const response = await fetch(\"json\");\n    const data = await response.json();\n    console.log(data);\n    document.getElementById(\"address\").value = data[\"address\"];\n    document.getElementById(\"port\").value = data[\"port\"];\n  }\n  updateContent();\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "esp8266_ad8232_ecg/data/style.css",
    "content": ".c{\n    text-align: center;\n}\ndiv,input{\n    padding:5px;font-size:1em;\n}\ninput{\n    width:85%;\n}\nbody{\n    text-align: center;font-family:verdana;\n}\nbutton{\n    border:0;border-radius:0.3rem;background-color:#1fa3ec;color:#fff;line-height:2.4rem;font-size:1.2rem;width:100%;\n}\n.q{\n    float: right;width: 64px;text-align: right;\n}\n.l{\n    background: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAALVBMVEX///8EBwfBwsLw8PAzNjaCg4NTVVUjJiZDRUUUFxdiZGSho6OSk5Pg4eFydHTCjaf3AAAAZElEQVQ4je2NSw7AIAhEBamKn97/uMXEGBvozkWb9C2Zx4xzWykBhFAeYp9gkLyZE0zIMno9n4g19hmdY39scwqVkOXaxph0ZCXQcqxSpgQpONa59wkRDOL93eAXvimwlbPbwwVAegLS1HGfZAAAAABJRU5ErkJggg==\") no-repeat left center;background-size: 1em;\n}\n"
  },
  {
    "path": "esp8266_ad8232_ecg/data/update.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Update</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n\n<h1>Update the ESP8266 firmware</h1>\n<form method='POST' action='/update' enctype='multipart/form-data'>\n\n<div class=\"field\">\n<input type='file' name='update'>\n</div>\n\n<div class=\"field\">\n<button type=\"submit\">Update</button>\n</div>\n\n</form>\n</body>\n</html>\n"
  },
  {
    "path": "esp8266_ad8232_ecg/esp8266_ad8232_ecg.ino",
    "content": "/*\n  This sketch is for an ESP8266 connected to an AD8232 module to record the ECG\n  and to send the continuous signal with the Fieldtrip buffer protocol to a server.\n\n  It uses WIFiManager for initial configuration and includes a web-interface\n  that allows to monitor and change parameters.\n\n  The status of the wifi connection, http server and received data\n  is indicated by the builtin LED.\n*/\n\n#include <Arduino.h>\n#include <ESP8266WiFi.h>          // https://github.com/esp8266/Arduino\n#include <ESP8266WebServer.h>\n#include <ESP8266mDNS.h>\n#include <WiFiManager.h>          // https://github.com/tzapu/WiFiManager\n#include <Ticker.h>               // https://github.com/sstaub/Ticker\n\n#include \"webinterface.h\"\n#include \"blink_led.h\"\n#include \"fieldtrip_buffer.h\"\n\n// this allows some sections of the code to be disabled for debugging purposes\n#define ENABLE_WEBINTERFACE\n#define ENABLE_MDNS\n#define ENABLE_BUFFER\n\nconst char* host = \"AD8232-ECG\";\nconst char* version = __DATE__ \" / \" __TIME__;\n\nESP8266WebServer server(80);\nTicker sampler;\n\n#define ADC       A0\n#define NCHANS    1\n#define FSAMPLE   200\n#define BLOCKSIZE (FSAMPLE/10)\n#define LO1 D6 // Lead-off detection\n#define LO2 D7 // Lead-off detection\n\nlong tic_web = 0;\nint sample = 0, block = 0;\nbool flush0 = false, flush1 = false;\nuint16_t block0[BLOCKSIZE], block1[BLOCKSIZE];\nint ftserver = 0, status = 0;\nint lo1 = 0, lo2 = 0;\n\n/************************************************************************************************/\n\nvoid getSample() {\n  if (sample == BLOCKSIZE) {\n    // switch to the start of the other block\n    switch (block) {\n      case 0:\n        sample = 0;\n        flush0 = true;\n        block = 1;\n        break;\n      case 1:\n        sample = 0;\n        flush1 = true;\n        block = 0;\n        break;\n    }\n    // sample the lead-off detection every block\n    // they don't behave as they should\n    lo1 = digitalRead(LO1);\n    lo2 = digitalRead(LO2);\n  }\n\n  // get the current ECG value, it is from a 10-bits ADC, value between 0 and 1023\n  uint16_t value = analogRead(ADC);\n\n  // store it in the active block\n  switch (block) {\n    case 0:\n      block0[sample++] = value;\n      break;\n    case 1:\n      block1[sample++] = value;\n      break;\n  }\n}\n\n/************************************************************************************************/\n\nvoid setup() {\n  Serial.begin(115200);\n  while (!Serial) {\n    ;\n  }\n\n  Serial.print(\"\\n[esp8266_ad8232 / \");\n  Serial.print(__DATE__ \" / \" __TIME__);\n  Serial.println(\"]\");\n\n  pinMode(LO1, INPUT); // Setup for leads off detection LO +\n  pinMode(LO2, INPUT); // Setup for leads off detection LO -\n\n  ledInit();\n\n  WiFi.hostname(host);\n  WiFi.begin();\n\n  SPIFFS.begin();\n\n  if (loadConfig()) {\n    ledSlow();\n    delay(1000);\n  }\n  else {\n    ledFast();\n    delay(1000);\n  }\n  Serial.println(config.address);\n  Serial.println(config.port);\n\n  if (WiFi.status() != WL_CONNECTED)\n    ledFast();\n\n  WiFiManager wifiManager;\n  wifiManager.setAPStaticIPConfig(IPAddress(192, 168, 1, 1), IPAddress(192, 168, 1, 1), IPAddress(255, 255, 255, 0));\n  wifiManager.autoConnect(host);\n  Serial.println(\"connected\");\n\n  if (WiFi.status() == WL_CONNECTED)\n    ledSlow();\n\n#ifdef ENABLE_WEBINTERFACE\n  // this serves all URIs that cannot be resolved to a file on the SPIFFS filesystem\n  server.onNotFound(handleNotFound);\n\n  server.on(\"/\", HTTP_GET, []() {\n    tic_web = millis();\n    handleRedirect(\"/index.html\");\n  });\n\n  server.on(\"/defaults\", HTTP_GET, []() {\n    tic_web = millis();\n    Serial.println(\"handleDefaults\");\n    handleStaticFile(\"/reload_success.html\");\n    defaultConfig();\n    saveConfig();\n    server.close();\n    server.stop();\n    delay(5000);\n    ESP.restart();\n  });\n\n  server.on(\"/reconnect\", HTTP_GET, []() {\n    tic_web = millis();\n    Serial.println(\"handleReconnect\");\n    handleStaticFile(\"/reload_success.html\");\n    ledFast();\n    server.close();\n    server.stop();\n    delay(5000);\n    WiFiManager wifiManager;\n    wifiManager.resetSettings();\n    wifiManager.setAPStaticIPConfig(IPAddress(192, 168, 1, 1), IPAddress(192, 168, 1, 1), IPAddress(255, 255, 255, 0));\n    wifiManager.startConfigPortal(host);\n    Serial.println(\"connected\");\n    server.begin();\n    if (WiFi.status() == WL_CONNECTED)\n      ledSlow();\n  });\n\n  server.on(\"/restart\", HTTP_GET, []() {\n    tic_web = millis();\n    Serial.println(\"handleRestart\");\n    handleStaticFile(\"/reload_success.html\");\n    ledFast();\n    server.close();\n    server.stop();\n    SPIFFS.end();\n    delay(5000);\n    ESP.restart();\n  });\n\n  server.on(\"/dir\", HTTP_GET, [] {\n    tic_web = millis();\n    handleDirList();\n  });\n\n  server.on(\"/json\", HTTP_PUT, [] {\n    tic_web = millis();\n    handleJSON();\n  });\n\n  server.on(\"/json\", HTTP_POST, [] {\n    tic_web = millis();\n    handleJSON();\n  });\n\n  server.on(\"/json\", HTTP_GET, [] {\n    tic_web = millis();\n    StaticJsonBuffer<300> jsonBuffer;\n    JsonObject& root = jsonBuffer.createObject();\n    S_CONFIG_TO_JSON(address, \"address\");\n    N_CONFIG_TO_JSON(port, \"port\");\n    root[\"version\"] = version;\n    root[\"uptime\"]  = long(millis() / 1000);\n    String str;\n    root.printTo(str);\n    server.setContentLength(str.length());\n    server.send(200, \"application/json\", str);\n  });\n\n  server.on(\"/update\", HTTP_GET, [] {\n    tic_web = millis();\n    handleStaticFile(\"/update.html\");\n  });\n\n  server.on(\"/update\", HTTP_POST, handleUpdate1, handleUpdate2);\n\n  // start the web server\n  server.begin();\n#endif\n\n#ifdef ENABLE_MDNS\n  // announce the hostname and web server through zeroconf\n  MDNS.begin(host);\n  MDNS.addService(\"http\", \"tcp\", 80);\n#endif\n\n#ifdef ENABLE_BUFFER\n  Serial.print(\"Connecting to \");\n  Serial.print(config.address);\n  Serial.print(\" on port \");\n  Serial.println(config.port);\n\n  ftserver = fieldtrip_open_connection(config.address, config.port);\n  if (ftserver > 0) {\n    Serial.println(\"Connection opened\");\n    status = fieldtrip_write_header(ftserver, DATATYPE_UINT16, NCHANS, FSAMPLE);\n    if (status == 0)\n      Serial.println(\"Wrote header\");\n    else\n      Serial.println(\"Failed writing header\");\n  }\n  else {\n    Serial.println(\"Failed opening connection\");\n  }\n\n#endif\n\n  // start sampling the ECG\n  sampler.attach_ms(1000 / FSAMPLE, getSample);\n\n  Serial.println(\"====================================================\");\n  Serial.println(\"Setup done\");\n  Serial.println(\"====================================================\");\n\n  return;\n} // setup\n\n/************************************************************************************************/\n\nvoid loop() {\n\n#ifdef ENABLE_WEBINTERFACE\n  server.handleClient();\n#endif\n\n#ifdef ENABLE_BUFFER\n  byte *ptr = NULL;\n\n  if (flush0) {\n    ptr = (byte *)block0;\n    flush0 = false;\n  }\n  else if (flush1) {\n    ptr = (byte *)block1;\n    flush1 = false;\n  }\n\n  if (ptr) {\n    if (ftserver == 0) {\n      ftserver = fieldtrip_open_connection(config.address, config.port);\n      if (ftserver > 0)\n        Serial.println(\"Connection opened\");\n      else\n        Serial.println(\"Failed opening connection\");\n    }\n\n    if (ftserver > 0) {\n      status = fieldtrip_write_data(ftserver, DATATYPE_UINT16, NCHANS, BLOCKSIZE, ptr);\n      if (status == 0)\n        Serial.println(\"Wrote data\");\n      else {\n        Serial.println(\"Failed writing data\");\n        status = fieldtrip_close_connection(ftserver);\n        if (status == 0)\n          Serial.println(\"Connection closed\");\n        else\n          Serial.println(\"Failed closing connection\");\n        ftserver = 0;\n      }\n    }\n  }\n\n#endif\n\n  delay(10);\n  return;\n} // loop\n"
  },
  {
    "path": "esp8266_ad8232_ecg/fieldtrip_buffer.cpp",
    "content": "#include \"fieldtrip_buffer.h\"\n\nWiFiClient client;\n\n#undef DEBUG\n\n/*******************************************************************************\n   OPEN CONNECTION\n   returns file descriptor that should be >0 on success\n *******************************************************************************/\nint fieldtrip_open_connection(const char *address, int port) {\n#ifdef DEBUG\n  Serial.println(\"fieldtrip_open_connection\");\n#endif\n  if (client.connect(address, port))\n    return 1;\n  else\n    return -1;\n};\n\n/*******************************************************************************\n   CLOSE CONNECTION\n   returns 0 on success\n *******************************************************************************/\nint fieldtrip_close_connection(int s) {\n#ifdef DEBUG\n  Serial.println(\"fieldtrip_close_connection\");\n#endif\n  client.stop();\n  return 0;\n};\n\n/*******************************************************************************\n   WRITE HEADER\n   returns 0 on success\n *******************************************************************************/\nint fieldtrip_write_header(int server, uint32_t datatype, uint32_t nchans, float fsample) {\n  int status;\n  messagedef_t *request  = NULL;\n  messagedef_t *response = NULL;\n  headerdef_t  *header   = NULL;\n\n#ifdef DEBUG\n  Serial.println(\"fieldtrip_write_header\");\n#endif\n\n  byte msg[8+24];\n  for (int i = 0; i < 8+24; i++)\n    msg[i] = 0;\n\n  request = (messagedef_t *)(msg + 0);\n  request->version = VERSION;\n  request->command = PUT_HDR;\n  request->bufsize = sizeof(headerdef_t);\n\n  header = (headerdef_t *)(msg + sizeof(messagedef_t)); // the first 8 bytes are version, command and bufsize\n  header->nchans     = nchans;\n  header->nsamples   = 0;\n  header->nevents    = 0;\n  header->fsample    = fsample;\n  header->data_type  = datatype;\n  header->bufsize    = 0;\n\n#ifdef DEBUG\n  Serial.print(\"msg =  \");\n  for (int i = 0; i < sizeof(messagedef_t) + sizeof(headerdef_t); i++) {\n    Serial.print(msg[i]);\n    Serial.print(\" \");\n  }\n  Serial.println();\n#endif\n\n  int n = 0;\n  n += client.write(msg, sizeof(messagedef_t) + sizeof(headerdef_t));\n  client.flush();\n\n#ifdef DEBUG\n  Serial.print(\"Wrote \");\n  Serial.print(n);\n  Serial.println(\" bytes\");\n#endif\n\n  if (n != sizeof(messagedef_t) + sizeof(headerdef_t))\n    status++;\n\n  // read and ignore whatever response we get\n  while (client.available())\n    client.read();\n\n  return status;\n};\n\n/*******************************************************************************\n   WRITE DATA\n   returns 0 on success\n *******************************************************************************/\nint fieldtrip_write_data(int server, uint32_t datatype, uint32_t nchans, uint32_t nsamples, byte *buffer) {\n  int status = 0;\n  messagedef_t *request  = NULL;\n  messagedef_t *response = NULL;\n  datadef_t    *data   = NULL;\n\n#ifdef DEBUG\n  Serial.println(\"fieldtrip_write_data\");\n#endif\n\n  byte msg[8+16];\n  for (int i = 0; i < 8+16; i++)\n    msg[i] = 0;\n\n  request = (messagedef_t *)(msg + 0);\n  request->version = VERSION;\n  request->command = PUT_DAT;\n  request->bufsize = sizeof(datadef_t) + nchans * nsamples * wordsize_from_type(datatype);\n\n  data = (datadef_t *)(msg + sizeof(messagedef_t)); // the first 8 bytes are version, command and bufsize\n  data->nchans     = nchans;\n  data->nsamples   = nsamples;\n  data->data_type  = datatype;\n  data->bufsize    = nchans * nsamples * wordsize_from_type(datatype);\n\n#ifdef DEBUG\n  Serial.print(\"msg =  \");\n  for (int i = 0; i < sizeof(messagedef_t) + sizeof(datadef_t); i++) {\n    Serial.print(msg[i]);\n    Serial.print(\" \");\n  }\n  Serial.println();\n#endif\n\n  int n = 0;\n  n += client.write(msg, sizeof(messagedef_t) + sizeof(datadef_t));\n  client.flush();\n  n += client.write(buffer, nchans * nsamples * wordsize_from_type(datatype));\n  client.flush();\n\n#ifdef DEBUG\n  Serial.print(\"Wrote \");\n  Serial.print(n);\n  Serial.println(\" bytes\");\n#endif\n\n  if (n != sizeof(messagedef_t) + sizeof(datadef_t) + nchans * nsamples * wordsize_from_type(datatype))\n    status++;\n\n  // read and ignore whatever response we get\n  while (client.available())\n    client.read();\n\n  return status;\n};\n\nint wordsize_from_type(uint32_t datatype) {\n  int wordsize = 0;\n  switch (datatype) {\n    case DATATYPE_CHAR:\n      wordsize = 1;\n      break;\n    case DATATYPE_UINT8:\n      wordsize = 1;\n      break;\n    case DATATYPE_UINT16:\n      wordsize = 2;\n      break;\n    case DATATYPE_UINT32:\n      wordsize = 4;\n      break;\n    case DATATYPE_UINT64:\n      wordsize = 8;\n      break;\n    case DATATYPE_INT8:\n      wordsize = 1;\n      break;\n    case DATATYPE_INT16:\n      wordsize = 2;\n      break;\n    case DATATYPE_INT32:\n      wordsize = 4;\n      break;\n    case DATATYPE_INT64:\n      wordsize = 8;\n      break;\n    case DATATYPE_FLOAT32:\n      wordsize = 4;\n      break;\n    case DATATYPE_FLOAT64:\n      wordsize = 8;\n      break;\n    }\n  return wordsize;\n};\n"
  },
  {
    "path": "esp8266_ad8232_ecg/fieldtrip_buffer.h",
    "content": "#ifndef __BUFFER_H_\n#define __BUFFER_H_\n\n#include <WiFiClient.h>\n\n// definition of simplified interface functions, not all of them are implemented\nint fieldtrip_start_server(int port);\nint fieldtrip_open_connection(const char *hostname, int port);\nint fieldtrip_close_connection(int s);\nint fieldtrip_read_header(int server, uint32_t *datatype, uint32_t *nchans, float *fsample, uint32_t *nsamples, uint32_t *nevents);\nint fieldtrip_read_data(int server, uint32_t begsample, uint32_t endsample, byte *buffer);\nint fieldtrip_write_header(int server, uint32_t datatype, uint32_t nchans, float fsample);\nint fieldtrip_write_data(int server, uint32_t datatype, uint32_t nchans, uint32_t nsamples, byte *buffer);\nint fieldtrip_wait_data(int server, uint32_t nsamples, uint32_t nevents, uint32_t milliseconds);\nint wordsize_from_type(uint32_t datatype);\n\n// define the version of the message packet\n#define VERSION    (uint16_t)0x0001\n\n// these define the commands that can be used, which are split over the two available bytes\n#define PUT_HDR    (uint16_t)0x0101 /* decimal 257 */\n#define PUT_DAT    (uint16_t)0x0102 /* decimal 258 */\n#define PUT_EVT    (uint16_t)0x0103 /* decimal 259 */\n#define PUT_OK     (uint16_t)0x0104 /* decimal 260 */\n#define PUT_ERR    (uint16_t)0x0105 /* decimal 261 */\n\n#define GET_HDR    (uint16_t)0x0201 /* decimal 513 */\n#define GET_DAT    (uint16_t)0x0202 /* decimal 514 */\n#define GET_EVT    (uint16_t)0x0203 /* decimal 515 */\n#define GET_OK     (uint16_t)0x0204 /* decimal 516 */\n#define GET_ERR    (uint16_t)0x0205 /* decimal 517 */\n\n#define FLUSH_HDR  (uint16_t)0x0301 /* decimal 769 */\n#define FLUSH_DAT  (uint16_t)0x0302 /* decimal 770 */\n#define FLUSH_EVT  (uint16_t)0x0303 /* decimal 771 */\n#define FLUSH_OK   (uint16_t)0x0304 /* decimal 772 */\n#define FLUSH_ERR  (uint16_t)0x0305 /* decimal 773 */\n\n#define WAIT_DAT   (uint16_t)0x0402 /* decimal 1026 */\n#define WAIT_OK    (uint16_t)0x0404 /* decimal 1027 */\n#define WAIT_ERR   (uint16_t)0x0405 /* decimal 1028 */\n\n#define PUT_HDR_NORESPONSE (uint16_t)0x0501 /* decimal 1281 */\n#define PUT_DAT_NORESPONSE (uint16_t)0x0502 /* decimal 1282 */\n#define PUT_EVT_NORESPONSE (uint16_t)0x0503 /* decimal 1283 */\n\n// these are used in the data_t and event_t structure\n#define DATATYPE_CHAR    (uint32_t)0\n#define DATATYPE_UINT8   (uint32_t)1\n#define DATATYPE_UINT16  (uint32_t)2\n#define DATATYPE_UINT32  (uint32_t)3\n#define DATATYPE_UINT64  (uint32_t)4\n#define DATATYPE_INT8    (uint32_t)5\n#define DATATYPE_INT16   (uint32_t)6\n#define DATATYPE_INT32   (uint32_t)7\n#define DATATYPE_INT64   (uint32_t)8\n#define DATATYPE_FLOAT32 (uint32_t)9\n#define DATATYPE_FLOAT64 (uint32_t)10\n\n// a packet that is sent over the network should contain the following\ntypedef struct {\n  uint16_t version;   // see VERSION\n  uint16_t command;   // see PUT_xxx, GET_xxx and FLUSH_xxx\n  uint32_t bufsize;   // size of the buffer in bytes\n}\nmessagedef_t; // 8 bytes\n\n// the header definition is fixed, it can be followed by additional header chunks\ntypedef struct {\n  uint32_t  nchans;\n  uint32_t  nsamples;\n  uint32_t  nevents;\n  float     fsample;\n  uint32_t  data_type;\n  uint32_t  bufsize;   // size of the buffer in bytes\n}\nheaderdef_t; // 24 bytes\n\n// the data definition is fixed, it should be followed by additional data\ntypedef struct {\n  uint32_t nchans;\n  uint32_t nsamples;\n  uint32_t data_type;\n  uint32_t bufsize;   // size of the buffer in bytes\n}\ndatadef_t; // 16 bytes\n\n// the event definition is fixed, it should be followed by additional data\ntypedef struct {\n  uint32_t type_type;   /* usual would be DATATYPE_CHAR */\n  uint32_t type_numel;  /* length of the type string */\n  uint32_t value_type;\n  uint32_t value_numel;\n  int32_t  sample;\n  int32_t  offset;\n  int32_t  duration;\n  uint32_t bufsize;     /* size of the buffer in bytes */\n} eventdef_t; // 64 bytes\n\n#endif\n"
  },
  {
    "path": "esp8266_ad8232_ecg/webinterface.cpp",
    "content": "#include \"webinterface.h\"\n#include \"blink_led.h\"\n\nConfig config;\nextern ESP8266WebServer server;\n\n/***************************************************************************/\n\nstatic String getContentType(const String& path) {\n  if (path.endsWith(\".html\"))       return \"text/html\";\n  else if (path.endsWith(\".htm\"))   return \"text/html\";\n  else if (path.endsWith(\".css\"))   return \"text/css\";\n  else if (path.endsWith(\".txt\"))   return \"text/plain\";\n  else if (path.endsWith(\".js\"))    return \"application/javascript\";\n  else if (path.endsWith(\".png\"))   return \"image/png\";\n  else if (path.endsWith(\".gif\"))   return \"image/gif\";\n  else if (path.endsWith(\".jpg\"))   return \"image/jpeg\";\n  else if (path.endsWith(\".jpeg\"))  return \"image/jpeg\";\n  else if (path.endsWith(\".ico\"))   return \"image/x-icon\";\n  else if (path.endsWith(\".svg\"))   return \"image/svg+xml\";\n  else if (path.endsWith(\".xml\"))   return \"text/xml\";\n  else if (path.endsWith(\".pdf\"))   return \"application/pdf\";\n  else if (path.endsWith(\".zip\"))   return \"application/zip\";\n  else if (path.endsWith(\".gz\"))    return \"application/x-gzip\";\n  else if (path.endsWith(\".json\"))  return \"application/json\";\n  return \"application/octet-stream\";\n}\n\n/***************************************************************************/\n\nbool defaultConfig() {\n  Serial.println(\"defaultConfig\");\n  \n  strncpy(config.address, \"192.168.1.34\", 32);\n  config.port = 1972;\n  return true;\n}\n\nbool loadConfig() {\n  Serial.println(\"loadConfig\");\n\n  File configFile = SPIFFS.open(\"/config.json\", \"r\");\n  if (!configFile) {\n    Serial.println(\"Failed to open config file\");\n    return false;\n  }\n\n  size_t size = configFile.size();\n  if (size > 1024) {\n    Serial.println(\"Config file size is too large\");\n    return false;\n  }\n\n  std::unique_ptr<char[]> buf(new char[size]);\n  configFile.readBytes(buf.get(), size);\n  configFile.close();\n\n  StaticJsonBuffer<300> jsonBuffer;\n  JsonObject& root = jsonBuffer.parseObject(buf.get());\n\n  if (!root.success()) {\n    Serial.println(\"Failed to parse config file\");\n    return false;\n  }\n\n  S_JSON_TO_CONFIG(address, \"address\");\n  N_JSON_TO_CONFIG(port, \"port\");\n\n  return true;\n}\n\nbool saveConfig() {\n  Serial.println(\"saveConfig\");\n  \n  StaticJsonBuffer<300> jsonBuffer;\n  JsonObject& root = jsonBuffer.createObject();\n\n  S_CONFIG_TO_JSON(address, \"address\");\n  N_CONFIG_TO_JSON(port, \"port\");\n\n  File configFile = SPIFFS.open(\"/config.json\", \"w\");\n  if (!configFile) {\n    Serial.println(\"Failed to open config file for writing\");\n    return false;\n  }\n  else {\n    Serial.println(\"Writing to config file\");\n    root.printTo(configFile);\n    configFile.close();\n    return true;\n  }\n}\n\nvoid printRequest() {\n  String message = \"HTTP Request\\n\\n\";\n  message += \"URI: \";\n  message += server.uri();\n  message += \"\\nMethod: \";\n  message += (server.method() == HTTP_GET) ? \"GET\" : \"POST\";\n  message += \"\\nHeaders: \";\n  message += server.headers();\n  message += \"\\n\";\n  for (uint8_t i = 0; i < server.headers(); i++ ) {\n    message += \" \" + server.headerName(i) + \": \" + server.header(i) + \"\\n\";\n  }\n  message += \"\\nArguments: \";\n  message += server.args();\n  message += \"\\n\";\n  for (uint8_t i = 0; i < server.args(); i++) {\n    message += \" \" + server.argName(i) + \": \" + server.arg(i) + \"\\n\";\n  }\n  Serial.println(message);\n}\n\nvoid handleUpdate1() {\n  server.sendHeader(\"Connection\", \"close\");\n  server.sendHeader(\"Access-Control-Allow-Origin\", \"*\");\n  server.send(200, \"text/plain\", (Update.hasError()) ? \"FAIL\" : \"OK\");\n  ESP.restart();\n}\n\nvoid handleUpdate2() {\n  HTTPUpload& upload = server.upload();\n  if (upload.status == UPLOAD_FILE_START) {\n    Serial.setDebugOutput(true);\n    WiFiUDP::stopAll();\n    Serial.printf(\"Update: %s\\n\", upload.filename.c_str());\n    uint32_t maxSketchSpace = (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000;\n    if (!Update.begin(maxSketchSpace)) { //start with max available size\n      Update.printError(Serial);\n    }\n  } else if (upload.status == UPLOAD_FILE_WRITE) {\n    if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {\n      Update.printError(Serial);\n    }\n  } else if (upload.status == UPLOAD_FILE_END) {\n    if (Update.end(true)) { //true to set the size to the current progress\n      Serial.printf(\"Update Success: %u\\nRebooting...\\n\", upload.totalSize);\n    } else {\n      Update.printError(Serial);\n    }\n    Serial.setDebugOutput(false);\n  }\n  yield();\n}\n\nvoid handleDirList() {\n  Serial.println(\"handleDirList\");\n  String str = \"\";\n  Dir dir = SPIFFS.openDir(\"/\");\n  while (dir.next()) {\n    str += dir.fileName();\n    str += \" \";\n    str += dir.fileSize();\n    str += \" bytes\\r\\n\";\n  }\n  server.send(200, \"text/plain\", str);\n}\n\nvoid handleNotFound() {\n  Serial.print(\"handleNotFound: \");\n  Serial.println(server.uri());\n  if (SPIFFS.exists(server.uri())) {\n    handleStaticFile(server.uri());\n  }\n  else {\n    String message = \"File Not Found\\n\\n\";\n    message += \"URI: \";\n    message += server.uri();\n    message += \"\\nMethod: \";\n    message += (server.method() == HTTP_GET) ? \"GET\" : \"POST\";\n    message += \"\\nArguments: \";\n    message += server.args();\n    message += \"\\n\";\n    for (uint8_t i = 0; i < server.args(); i++) {\n      message += \" \" + server.argName(i) + \": \" + server.arg(i) + \"\\n\";\n    }\n    server.setContentLength(message.length());\n    server.send(404, \"text/plain\", message);\n  }\n}\n\nvoid handleRedirect(const char * filename) {\n  handleRedirect((String)filename);\n}\n\nvoid handleRedirect(String filename) {\n  Serial.println(\"handleRedirect: \" + filename);\n  server.sendHeader(\"Location\", filename, true);\n  server.setContentLength(0);\n  server.send(302, \"text/plain\", \"\");\n}\n\nbool handleStaticFile(const char * path) {\n  return handleStaticFile((String)path);\n}\n\nbool handleStaticFile(String path) {\n  Serial.println(\"handleStaticFile: \" + path);\n  String contentType = getContentType(path);            // Get the MIME type\n  if (SPIFFS.exists(path)) {                            // If the file exists\n    File file = SPIFFS.open(path, \"r\");                 // Open it\n    server.setContentLength(file.size());\n    server.streamFile(file, contentType);               // And send it to the client\n    file.close();                                       // Then close the file again\n    return true;\n  }\n  Serial.println(\"\\tFile Not Found\");\n  return false;                                         // If the file doesn't exist, return false\n}\n\nvoid handleJSON() {\n  // this gets called in response to either a PUT or a POST\n  Serial.println(\"handleJSON\");\n  printRequest();\n\n  if (server.hasArg(\"address\") || server.hasArg(\"port\")) {\n    // the body is key1=val1&key2=val2&key3=val3 and the ESP8266Webserver has already parsed it\n    S_KEYVAL_TO_CONFIG(address, \"address\");\n    N_KEYVAL_TO_CONFIG(port, \"port\");\n    handleStaticFile(\"/reload_success.html\");\n  }\n  else if (server.hasArg(\"plain\")) {\n    // parse the body as JSON object\n    StaticJsonBuffer<300> jsonBuffer;\n    JsonObject& root = jsonBuffer.parseObject(server.arg(\"plain\"));\n    if (!root.success()) {\n      handleStaticFile(\"/reload_failure.html\");\n      return;\n    }\n    S_JSON_TO_CONFIG(address, \"address\");\n    N_JSON_TO_CONFIG(port, \"port\");\n    handleStaticFile(\"/reload_success.html\");\n  }\n  else {\n    handleStaticFile(\"/reload_failure.html\");\n    return; // do not save the configuration\n  }\n\n  saveConfig();\n\n  // blink five times\n  for (int i = 0; i < 5; i++) {\n    ledOn();\n    delay(200);\n    ledOff();\n    delay(200);\n  }\n\n  // some of the settings require re-initialization\n  ESP.restart();\n}\n"
  },
  {
    "path": "esp8266_ad8232_ecg/webinterface.h",
    "content": "#ifndef _WEBINTERFACE_H_\n#define _WEBINTERFACE_H_\n\n#include <Arduino.h>\n#include <ArduinoJson.h>\n#include <string.h>\n#include <ESP8266WebServer.h>\n#include <WiFiUdp.h>\n#include <FS.h>\n\n#ifndef ARDUINOJSON_VERSION\n#error ArduinoJson version 5 not found, please include ArduinoJson.h in your .ino file\n#endif\n\n#if ARDUINOJSON_VERSION_MAJOR != 5\n#error ArduinoJson version 5 is required\n#endif\n\n/* these are for numbers */\n#define N_JSON_TO_CONFIG(x, y)   { if (root.containsKey(y)) { config.x = root[y]; } }\n#define N_CONFIG_TO_JSON(x, y)   { root.set(y, config.x); }\n#define N_KEYVAL_TO_CONFIG(x, y) { if (server.hasArg(y))    { String str = server.arg(y); config.x = str.toFloat(); } }\n\n/* these are for strings */\n#define S_JSON_TO_CONFIG(x, y)   { if (root.containsKey(y)) { strcpy(config.x, root[y]); } }\n#define S_CONFIG_TO_JSON(x, y)   { root.set(y, config.x); }\n#define S_KEYVAL_TO_CONFIG(x, y) { if (server.hasArg(y))    { String str = server.arg(y); strcpy(config.x, str.c_str()); } }\n\nstruct Config {\n  char address[32];\n  int port;\n};\n\nextern Config config;\n\nbool defaultConfig(void);\nbool loadConfig(void);\nbool saveConfig(void);\n\nvoid handleUpdate1(void);\nvoid handleUpdate2(void);\nvoid handleDirList(void);\nvoid handleNotFound(void);\nvoid handleRedirect(String);\nvoid handleRedirect(const char *);\nbool handleStaticFile(String);\nbool handleStaticFile(const char *);\nvoid handleJSON();\n\n#endif\n"
  },
  {
    "path": "esp8266_artnet_bci/esp8266_artnet_bci.ino",
    "content": "#include <ESP8266WiFi.h>         // https://github.com/esp8266/Arduino\n#include <ESP8266WebServer.h>\n#include <ESP8266mDNS.h>\n#include <WiFiManager.h>         // https://github.com/tzapu/WiFiManager\n#include <WiFiClient.h>\n#include <WiFiUdp.h>\n#include <ArduinoOTA.h>\n#include <ArtnetWifi.h>          // https://github.com/rstephan/ArtnetWifi\n#include <Adafruit_NeoPixel.h>   // https://learn.adafruit.com/adafruit-neopixel-uberguide/arduino-library\n\nextern \"C\" {\n#include \"user_interface.h\"      // https://github.com/willemwouters/ESP8266/wiki/Timer-example\n}\n\nconst char* host = \"ARTNET-BCI\";\nconst char* version = __DATE__ \" / \" __TIME__;\n\n#define UNIVERSE  1\n#define OFFSET    0\n#define NUMPIXELS 1\n#define PIN       5\n#define NUMEL(x)  (sizeof(x) / sizeof((x)[0]))\n#define WRAP(x,y) (x>=y ? x-y : x)\n\nWiFiManager wifiManager;\nAdafruit_NeoPixel pixels = Adafruit_NeoPixel(NUMPIXELS, PIN, NEO_RGB + NEO_KHZ800);\nESP8266WebServer server(80);\nstatic ETSTimer sequenceTimer;\n\n// Artnet settings\nArtnetWifi artnet;\nunsigned long packetCounter = 0;\n\n// keep track of the time just after a http request\nlong tic_web = 0;\n\n// Blink sequence settings\nbyte enable = 0, step = 0, current = 0;\nbyte r = 255, g = 255, b = 255;\n\nunsigned int sequence0[] = {125, 125};  // default, rapid blinking at 4Hz\nunsigned int sequence1[] = {100, 0};    // constant on\nunsigned int sequence2[] = {0, 100};    // constant off\nunsigned int sequence3[] = {8, 8};      // approximately 60Hz\nunsigned int sequence4[] = {80, 16, 96, 96, 16, 48, 48, 80, 48, 64, 32, 128, 80, 112, 96, 80, 48, 48, 80, 80, 48, 64, 32, 80, 96, 96, 48, 48, 96, 64, 96, 32, 48, 112, 112, 80, 48, 32, 96, 112, 144, 16, 48, 128, 64, 80, 80, 48, 48, 64, 112, 64, 112, 80, 128, 32, 48, 96, 64, 80, 64, 16, 32, 64, 64, 112, 128, 0, 112, 112, 32, 80, 144, 128, 112, 64, 16, 112, 48, 64, 64, 96, 64, 96, 80, 48, 80, 80, 64, 80, 64, 16, 64, 80, 96, 96, 48, 80, 64, 96};\nunsigned int sequence5[] = {80, 100, 60, 60, 100, 80, 100, 100, 80, 100, 140, 60, 120, 160, 120, 160, 40, 80, 100, 200, 200, 160, 60, 80, 120, 100, 80, 160, 80, 180, 80, 80, 160, 100, 100, 100, 40, 60, 140, 160, 100, 80, 100, 100, 20, 60, 60, 100, 160, 100, 180, 200, 60, 220, 120, 60, 120, 100, 160, 80, 100, 160, 80, 140, 60, 140, 20, 100, 120, 100, 80, 60, 100, 60, 140, 140, 40, 140, 80, 100, 140, 180, 60, 100, 200, 140, 140, 60, 60, 80, 80, 60, 80, 40, 80, 60, 80, 220, 160, 80};\nunsigned int sequence6[] = {96, 96, 256, 224, 96, 96, 256, 224, 96, 192, 64, 96, 64, 128, 160, 224, 128, 352, 160, 64, 160, 160, 32, 128, 96, 288, 128, 128, 128, 224, 160, 192, 64, 128, 32, 96, 192, 96, 160, 96, 96, 128, 192, 192, 256, 192, 192, 192, 96, 32, 160, 96, 64, 64, 192, 96, 32, 224, 320, 32, 96, 256, 224, 128, 128, 224, 192, 192, 192, 96, 96, 160, 192, 224, 128, 128, 96, 128, 192, 160, 224, 128, 192, 96, 96, 352, 256, 96, 128, 160, 288, 256, 224, 224, 160, 256, 96, 192, 128, 224};\n\n#define NUMSEQUENCE 7\nunsigned int *sequence[NUMSEQUENCE] = {sequence0, sequence1, sequence2, sequence3, sequence4, sequence5, sequence6};\nunsigned int length[NUMSEQUENCE] = {2, 2, 2, 2, 100, 100, 100};\n\n/***********************************************************************************************/\n\n//this is perpetually scheduled for the blink sequence updating\nvoid sequence_update() {\n  os_timer_disarm(&sequenceTimer);\n\n  step = WRAP(step, length[current]);\n  unsigned int interval = sequence[current][step];\n\n  if (interval == 0 ) {\n    // skip to the next step\n    step++;\n    step = WRAP(step, length[current]);\n    interval = sequence[current][step];\n  }\n\n  if (enable) {\n    if (step % 2 == 0)\n      singleBright();\n    else\n      singleBlack();\n  }\n\n  step++;\n\n  os_timer_arm(&sequenceTimer, interval, 0);\n} // sequence_update\n\n/***********************************************************************************************/\n\n//this will be called for each UDP packet received\nvoid packet_receive(uint16_t universe, uint16_t length, uint8_t sequence, uint8_t * data) {\n  Serial.print(\"received packet = \");\n  Serial.println(packetCounter++);\n\n  if (universe != UNIVERSE)\n    return;\n\n  // shift to the specified channels in the DMX packet\n  data += OFFSET;\n  length -= OFFSET;\n\n  // 0 = current sequence\n  if (length > 0) {\n    if (data[0] >= 0 && data[0] < NUMSEQUENCE) {\n      current = data[0];\n      step = 0;\n    }\n  }\n\n  // 1, 2, 3 = color\n  if (length > 3) {\n    r = data[1];\n    g = data[2];\n    b = data[3];\n  }\n} // packet_receive\n\n/***********************************************************************************************/\n\nvoid setup() {\n  Serial.begin(115200);\n  while (!Serial) {\n    ;\n  }\n  Serial.println(\"setup starting\");\n\n  pixels.setBrightness(150);\n  pixels.begin();\n\n  current = 0;\n\n  if (WiFi.status() != WL_CONNECTED)\n    singleRed();\n\n  WiFiManager wifiManager;\n  wifiManager.setAPStaticIPConfig(IPAddress(192, 168, 1, 1), IPAddress(192, 168, 1, 1), IPAddress(255, 255, 255, 0));\n  wifiManager.autoConnect(host);\n\n  if (WiFi.status() == WL_CONNECTED) {\n    singleGreen();\n    Serial.println(\"connected\");\n  }\n\n  // Port defaults to 8266\n  ArduinoOTA.setPort(8266);\n  // Hostname defaults to esp8266-[ChipID]\n  ArduinoOTA.setHostname(host);\n  // No authentication by default\n  // ArduinoOTA.setPassword((const char *)\"123\");\n\n  ArduinoOTA.onStart([]() {\n    Serial.println(\"Start OTA\");\n    server = ESP8266WebServer(); // See https://github.com/esp8266/Arduino/issues/686\n    artnet = ArtnetWifi();\n    os_timer_disarm(&sequenceTimer);\n    singleBlue();\n  });\n  ArduinoOTA.onEnd([]() {\n    Serial.println(\"\\nEnd OTA\");\n    ESP.restart();\n  });\n  ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {\n    Serial.printf(\"Progress: %u%%\\r\", (progress / (total / 100)));\n  });\n  ArduinoOTA.onError([](ota_error_t error) {\n    Serial.printf(\"Error[%u]: \", error);\n    if      (error == OTA_AUTH_ERROR) Serial.println(\"Auth Failed\");\n    else if (error == OTA_BEGIN_ERROR) Serial.println(\"Begin Failed\");\n    else if (error == OTA_CONNECT_ERROR) Serial.println(\"Connect Failed\");\n    else if (error == OTA_RECEIVE_ERROR) Serial.println(\"Receive Failed\");\n    else if (error == OTA_END_ERROR) Serial.println(\"End Failed\");\n  });\n\n  server.onNotFound([]() {\n    tic_web = millis();\n    Serial.println(\"handleNotFound\");\n    server.send(404, \"text/plain\", \"not found\");\n  });\n\n  server.on(\"/\", HTTP_GET, []() {\n    tic_web = millis();\n    Serial.println(\"handleRoot\");\n    server.send(200, \"text/plain\", host);\n  });\n\n  server.on(\"/reconnect\", HTTP_GET, []() {\n    tic_web = millis();\n    Serial.println(\"handleReconnect\");\n    singleRed();\n    server.close();\n    server.stop();\n    delay(5000);\n    wifiManager.resetSettings();\n    wifiManager.startConfigPortal(host);\n    Serial.println(\"connected\");\n    server.begin();\n    if (WiFi.status() == WL_CONNECTED)\n      singleGreen();\n  });\n\n  server.on(\"/restart\", HTTP_GET, []() {\n    tic_web = millis();\n    Serial.println(\"handleRestart\");\n    singleRed();\n    server.close();\n    server.stop();\n    SPIFFS.end();\n    delay(5000);\n    ESP.restart();\n  });\n\n  // start the web server\n  server.begin();\n\n  // start the OTA server\n  ArduinoOTA.begin();\n\n  // announce the web server through zeroconf\n  MDNS.begin(host);\n  MDNS.addService(\"http\", \"tcp\", 80);\n\n  // start the Artnet server\n  artnet.begin();\n  artnet.setArtDmxCallback(packet_receive);\n\n  // start the timer\n  os_timer_disarm(&sequenceTimer);\n  os_timer_setfn(&sequenceTimer, (os_timer_func_t *) sequence_update, NULL);\n  os_timer_arm(&sequenceTimer, 1000, 0);\n\n  Serial.println(\"setup done\");\n\n} // setup\n\n/***********************************************************************************************/\n\nvoid loop() {\n\n  if (WiFi.status() != WL_CONNECTED) {\n    enable = 0;\n    singleRed();\n  }\n  else if ((millis() - tic_web) < 1000) {\n    enable = 0;\n    singleBlue();\n  }\n  else  {\n    enable = 1;\n    server.handleClient();\n    artnet.read();\n    ArduinoOTA.handle();\n    // the remainder of the work gets done by the timer and callback functions\n  }\n\n} // loop\n\n/***********************************************************************************************/\n\nvoid singleRed() {\n  pixels.setPixelColor(0, 255, 0, 0);\n  pixels.show();\n}\n\nvoid singleGreen() {\n  pixels.setPixelColor(0, 0, 255, 0);\n  pixels.show();\n}\n\nvoid singleBlue() {\n  pixels.setPixelColor(0, 0, 0, 255);\n  pixels.show();\n}\n\nvoid singleYellow() {\n  pixels.setPixelColor(0, 255, 255, 0);\n  pixels.show();\n}\n\nvoid singleBlack() {\n  pixels.setPixelColor(0, 0, 0, 0);\n  pixels.show();\n}\n\nvoid singleWhite() {\n  pixels.setPixelColor(0, 255, 255, 255);\n  pixels.show();\n}\n\nvoid singleBright() {\n  pixels.setPixelColor(0, r, g, b);\n  pixels.show();\n}\n"
  },
  {
    "path": "esp8266_artnet_neopixel/README.md",
    "content": "# Overview\n\nThis is an Arduino sketch for an ESP8266 module connected to a neopixel LED strip. It uses ArtNet to control features of the light pattern, such as the brightness and the speed. There are a number of modes implemented to make nice patterns, such as a slider that moves from one to the other side.\n\n## Webinterface\n\nThe Neopixel and Art-Net settings can be updated on the fly using the webinterface or like this\n\n    curl -X PUT -d '{\"universe\":1,\"offset\":0,\"pixels\":24,\"leds\":4,\"white\":0,\"brightness\":100,\"hsv\":0,\"mode\":10,\"speed\":8,\"split\":1,\"reverse\":0}' artnet.local/json\n\n## Operating modes\n\n    mode 0: individual pixel control\n    channel 1 = pixel 1 red\n    channel 2 = pixel 1 green\n    channel 3 = pixel 1 blue\n    channel 4 = pixel 1 white\n    channel 5 = pixel 2 red\n    etc.\n\n    mode 1: single uniform color\n    channel 1 = red\n    channel 2 = green\n    channel 3 = blue\n    channel 4 = white\n    channel 5 = intensity (this allows scaling a preset RGBW color with a single channel)\n\n    mode 2: two color mixing\n    channel 1  = color 1 red\n    channel 2  = color 1 green\n    channel 3  = color 1 blue\n    channel 4  = color 1 white\n    channel 5  = color 2 red\n    channel 6  = color 2 green\n    channel 7  = color 2 blue\n    channel 8  = color 2 white\n    channel 9  = intensity (this also allows to black out the colors)\n    channel 10 = balance (between color 1 and color2)\n\n    mode 3: single uniform color, blinking between the color and black\n    channel 1 = red\n    channel 2 = green\n    channel 3 = blue\n    channel 4 = white\n    channel 5 = intensity\n    channel 6 = speed (number of flashes per unit of time)\n    channel 7 = ramp (whether there is a abrubt or more smooth transition)\n    channel 8 = duty cycle (the time ratio between the color and black)\n\n    mode 4: uniform color, blinking between color 1 and color 2\n    channel 1  = color 1 red\n    channel 2  = color 1 green\n    channel 3  = color 1 blue\n    channel 4  = color 1 white\n    channel 5  = color 2 red\n    channel 6  = color 2 green\n    channel 7  = color 2 blue\n    channel 8  = color 2 white\n    channel 9  = intensity\n    channel 10 = speed\n    channel 11 = ramp\n    channel 12 = duty cycle\n\n    mode 5: single color slider, segment that can be moved along the array (between the edges)\n    channel 1 = red\n    channel 2 = green\n    channel 3 = blue\n    channel 4 = white\n    channel 5 = intensity\n    channel 6 = position (from 0-255 or 0-360 degrees, relative to the length of the array)\n    channel 7 = width    (from 0-255 or 0-360 degrees, relative to the length of the array)\n\n    mode 6: dual color slider, segment can be moved along the array (between the edges)\n    channel 1  = color 1 red\n    channel 2  = color 1 green\n    channel 3  = color 1 blue\n    channel 4  = color 1 white\n    channel 5  = color 2 red\n    channel 6  = color 2 green\n    channel 7  = color 2 blue\n    channel 8  = color 2 white\n    channel 9  = intensity\n    channel 10 = position (from 0-255 or 0-360 degrees, relative to the length of the array)\n    channel 11 = width    (from 0-255 or 0-360 degrees, relative to the length of the array)\n\n    mode 7: single color smooth slider, segment can be moved along the array (continuous over the edge)\n    channel 1 = red\n    channel 2 = green\n    channel 3 = blue\n    channel 4 = white\n    channel 5 = intensity\n    channel 6 = position (from 0-255 or 0-360 degrees, relative to the length of the array)\n    channel 7 = width    (from 0-255 or 0-360 degrees, relative to the length of the array)\n    channel 8 = ramp     (from 0-255 or 0-360 degrees, relative to the length of the array)\n\n    mode 8: dual color smooth slider, segment can be moved along the array (continuous over the edge)\n    channel 1  = color 1 red\n    channel 2  = color 1 green\n    channel 3  = color 1 blue\n    channel 4  = color 1 white\n    channel 5  = color 2 red\n    channel 6  = color 2 green\n    channel 7  = color 2 blue\n    channel 8  = color 2 white\n    channel 9  = intensity\n    channel 10 = position (from 0-255 or 0-360 degrees, relative to the length of the array)\n    channel 11 = width    (from 0-255 or 0-360 degrees, relative to the length of the array)\n    channel 12 = ramp     (from 0-255 or 0-360 degrees, relative to the length of the array)\n\n    mode 9: spinning color wheel\n    channel 1 = red\n    channel 2 = green\n    channel 3 = blue\n    channel 4 = white\n    channel 5 = intensity\n    channel 6 = speed\n    channel 7 = width\n    channel 8 = ramp\n\n    mode 10: spinning color wheel with color background\n    channel 1  = color 1 red\n    channel 2  = color 1 green\n    channel 3  = color 1 blue\n    channel 4  = color 1 white\n    channel 5  = color 2 red\n    channel 6  = color 2 green\n    channel 7  = color 2 blue\n    channel 8  = color 2 white\n    channel 9  = intensity\n    channel 10 = speed\n    channel 11 = width\n    channel 12 = ramp\n\n    mode 11: rainbow slider\n    channel 1 = saturation\n    channel 2 = value\n    channel 3 = position\n\n    mode 12: rainbow spinner\n    channel 1 = saturation\n    channel 2 = value\n    channel 3 = speed\n\n    mode 13: dual color letter for 8x8 RGBW neopixel array\n    channel 1  = color 1 red\n    channel 2  = color 1 green\n    channel 3  = color 1 blue\n    channel 4  = color 1 white\n    channel 5  = color 2 red\n    channel 6  = color 2 green\n    channel 7  = color 2 blue\n    channel 8  = color 2 white\n    channel 9  = intensity\n    channel 10 = ASCII code\n\n## SPIFFS for static files\n\nYou should not only write the firmware to the ESP8266 module, but also the static content for the web interface. The html, css and javascript files located in the data directory should be written to the SPIFS filesystem on the ESP8266. See for example http://esp8266.github.io/Arduino/versions/2.0.0/doc/filesystem.html and https://www.instructables.com/id/Using-ESP8266-SPIFFS for instructions.\n\nYou will get a \"file not found\" error if the firmware cannot access the data files.\n\n## Arduino ESP8266 filesystem uploader\n\nThis Arduino sketch includes a `data` directory with a number of files that should be uploaded to the ESP8266 using the [SPIFFS filesystem uploader](https://github.com/esp8266/arduino-esp8266fs-plugin) tool. At the moment (Feb 2024) the Arduino 2.x IDE does *not* support the SPIFFS filesystem uploader plugin. You have to use the Arduino 1.8.x IDE (recommended), or the command line utilities for uploading the data.\n"
  },
  {
    "path": "esp8266_artnet_neopixel/colorspace.cpp",
    "content": "#include \"colorspace.h\"\n\nhsv rgb2hsv(rgb in)\n{\n  hsv         out;\n  double      min, max, delta;\n\n  min = in.r < in.g ? in.r : in.g;\n  min = min  < in.b ? min  : in.b;\n\n  max = in.r > in.g ? in.r : in.g;\n  max = max  > in.b ? max  : in.b;\n\n  out.v = max;                              // v\n  delta = max - min;\n  if (delta < 0.00001)\n  {\n    out.s = 0;\n    out.h = 0; // undefined, maybe nan?\n    return out;\n  }\n  if ( max > 0.0 ) { // NOTE: if Max is == 0, this divide would cause a crash\n    out.s = (delta / max);                  // s\n  } else {\n    // if max is 0, then r = g = b = 0\n    // s = 0, v is undefined\n    out.s = 0.0;\n    out.h = 0. / 0.;                        // its now undefined\n    return out;\n  }\n  if ( in.r >= max )                        // > is bogus, just keeps compilor happy\n    out.h = ( in.g - in.b ) / delta;        // between yellow & magenta\n  else if ( in.g >= max )\n    out.h = 2.0 + ( in.b - in.r ) / delta;  // between cyan & yellow\n  else\n    out.h = 4.0 + ( in.r - in.g ) / delta;  // between magenta & cyan\n\n  out.h *= 60.0;                            // degrees\n\n  if ( out.h < 0.0 )\n    out.h += 360.0;\n\n  return out;\n}\n\n\nrgb hsv2rgb(hsv in)\n{\n  double      hh, p, q, t, ff;\n  long        i;\n  rgb         out;\n\n  if (in.s <= 0.0) {      // < is bogus, just shuts up warnings\n    out.r = in.v;\n    out.g = in.v;\n    out.b = in.v;\n    return out;\n  }\n  hh = in.h;\n  if (hh >= 360.0) hh = 0.0;\n  hh /= 60.0;\n  i = (long)hh;\n  ff = hh - i;\n  p = in.v * (1.0 - in.s);\n  q = in.v * (1.0 - (in.s * ff));\n  t = in.v * (1.0 - (in.s * (1.0 - ff)));\n\n  switch (i) {\n    case 0:\n      out.r = in.v;\n      out.g = t;\n      out.b = p;\n      break;\n    case 1:\n      out.r = q;\n      out.g = in.v;\n      out.b = p;\n      break;\n    case 2:\n      out.r = p;\n      out.g = in.v;\n      out.b = t;\n      break;\n\n    case 3:\n      out.r = p;\n      out.g = q;\n      out.b = in.v;\n      break;\n    case 4:\n      out.r = t;\n      out.g = p;\n      out.b = in.v;\n      break;\n    case 5:\n    default:\n      out.r = in.v;\n      out.g = p;\n      out.b = q;\n      break;\n  }\n  return out;\n}\n"
  },
  {
    "path": "esp8266_artnet_neopixel/colorspace.h",
    "content": "#ifndef _COLORSPACE_H_\n#define _COLORSPACE_H_\n\ntypedef struct {\n    double r;       // percent\n    double g;       // percent\n    double b;       // percent\n} rgb;\n\ntypedef struct {\n    double h;       // angle in degrees\n    double s;       // percent\n    double v;       // percent\n} hsv;\n\nhsv rgb2hsv(rgb);\nrgb hsv2rgb(hsv);\n\n#endif\n\n"
  },
  {
    "path": "esp8266_artnet_neopixel/data/hello.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Hello</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n<p>Hello kitty!</p>\n<img src=\"cat.jpg\">\n</body>\n</html>\n"
  },
  {
    "path": "esp8266_artnet_neopixel/data/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Index</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n<a href=\"/hello.html\">Hello</a></br>\n<a href=\"/monitor.html\">Monitor</a></br>\n<a href=\"/settings.html\">Change settings</a></br>\n<a href=\"/reconnect\">Reconnect WiFi</a></br>\n<a href=\"/defaults\">Default settings</a></br>\n<a href=\"/update.html\">Update firmware</a></br>\n<a href=\"/restart\">Restart hardware</a></br>\n</body>\n</html>\n"
  },
  {
    "path": "esp8266_artnet_neopixel/data/monitor.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Monitor</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n\n<h1>Monitor</h1>\n\nFirmware version:\n<div id=\"version\" name=\"version\">?</div>\n\nUptime:\n<div id=\"uptime\" name\"uptime\">?</div>\n\nPackets received:\n<div id=\"packets\" name=\"packets\">?</div>\n\nFrames per second:\n<div id=\"fps\" name=\"fps\">?</div>\n\n<script>\n  async function updateContent() {\n    const response = await fetch(\"json\");\n    const data = await response.json();\n    console.log(data);\n    document.getElementById(\"version\").innerHTML = data[\"version\"];\n    document.getElementById(\"uptime\").innerHTML = data[\"uptime\"];\n    document.getElementById(\"packets\").innerHTML = data[\"packets\"];\n    document.getElementById(\"fps\").innerHTML = data[\"fps\"];\n  }\n  updateContent();\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "esp8266_artnet_neopixel/data/reload_failure.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Failure</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n\n<p>Failure</p>\n\n<script type=\"text/javascript\">\n  setTimeout(function(){location=\"/index.html\"},1000);\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "esp8266_artnet_neopixel/data/reload_success.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Success</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n\n<p>Success</p>\n\n<script type=\"text/javascript\">\n  setTimeout(function(){location=\"/index.html\"},1000);\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "esp8266_artnet_neopixel/data/settings.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Settings</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n\n<h1>Settings</h1>\n<form id=\"settings-form\" method=\"post\" action=\"json\">\n\n    <div class=\"field\">\n        <label for=\"universe\">universe:</label>\n        <input type=\"text\" id=\"universe\" name=\"universe\" value=\"?\" required>\n    </div>\n\n    <div class=\"field\">\n        <label for=\"offset\">offset:</label>\n        <input type=\"text\" id=\"offset\" name=\"offset\" value=\"?\" required>\n    </div>\n\n    <div class=\"field\">\n        <label for=\"pixels\">pixels:</label>\n        <input type=\"text\" id=\"pixels\" name=\"pixels\" value=\"?\" required>\n    </div>\n\n    <div class=\"field\">\n        <label for=\"leds\">leds:</label>\n        <input type=\"text\" id=\"leds\" name=\"leds\" value=\"?\" required>\n    </div>\n\n    <div class=\"field\">\n        <label for=\"white\">white:</label>\n        <input type=\"text\" id=\"white\" name=\"white\" value=\"?\" required>\n    </div>\n\n    <div class=\"field\">\n        <label for=\"brightness\">brightness:</label>\n        <input type=\"text\" id=\"brightness\" name=\"brightness\" value=\"?\" required>\n    </div>\n\n    <div class=\"field\">\n        <label for=\"hsv\">hsv:</label>\n        <input type=\"text\" id=\"hsv\" name=\"hsv\" value=\"?\" required>\n    </div>\n\n    <div class=\"field\">\n        <label for=\"mode\">mode:</label>\n        <input type=\"text\" id=\"mode\" name=\"mode\" value=\"?\" required>\n    </div>\n\n    <div class=\"field\">\n        <label for=\"reverse\">reverse:</label>\n        <input type=\"text\" id=\"reverse\" name=\"reverse\" value=\"?\" required>\n    </div>\n\n    <div class=\"field\">\n        <label for=\"speed\">speed:</label>\n        <input type=\"text\" id=\"speed\" name=\"speed\" value=\"?\" required>\n    </div>\n\n    <div class=\"field\">\n        <label for=\"split\">split:</label>\n        <input type=\"text\" id=\"split\" name=\"split\" value=\"?\" required>\n    </div>\n\n    <div class=\"field\">\n        <button type=\"submit\">Save</button>\n    </div>\n</form>\n\n<script>\n  async function updateContent() {\n    const response = await fetch(\"json\");\n    const data = await response.json();\n    console.log(data);\n    document.getElementById(\"var1\").value = data[\"var1\"];\n    document.getElementById(\"var2\").value = data[\"var2\"];\n    document.getElementById(\"var3\").value = data[\"var3\"];\n  }\n  updateContent();\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "esp8266_artnet_neopixel/data/style.css",
    "content": ".c{\n    text-align: center;\n}\ndiv,input{\n    padding:5px;font-size:1em;\n}\ninput{\n    width:85%;\n}\nbody{\n    text-align: center;font-family:verdana;\n}\nbutton{\n    border:0;border-radius:0.3rem;background-color:#1fa3ec;color:#fff;line-height:2.4rem;font-size:1.2rem;width:100%;\n}\n.q{\n    float: right;width: 64px;text-align: right;\n}\n.l{\n    background: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAALVBMVEX///8EBwfBwsLw8PAzNjaCg4NTVVUjJiZDRUUUFxdiZGSho6OSk5Pg4eFydHTCjaf3AAAAZElEQVQ4je2NSw7AIAhEBamKn97/uMXEGBvozkWb9C2Zx4xzWykBhFAeYp9gkLyZE0zIMno9n4g19hmdY39scwqVkOXaxph0ZCXQcqxSpgQpONa59wkRDOL93eAXvimwlbPbwwVAegLS1HGfZAAAAABJRU5ErkJggg==\") no-repeat left center;background-size: 1em;\n}\n"
  },
  {
    "path": "esp8266_artnet_neopixel/data/update.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Update</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n\n<h1>Update the ESP8266 firmware</h1>\n<form method='POST' action='/update' enctype='multipart/form-data'>\n\n<div class=\"field\">\n<input type='file' name='update'>\n</div>\n\n<div class=\"field\">\n<button type=\"submit\">Update</button>\n</div>\n\n</form>\n</body>\n</html>\n"
  },
  {
    "path": "esp8266_artnet_neopixel/esp8266_artnet_neopixel.ino",
    "content": "/*\n  This sketch receive a DMX universes via Artnet to control a\n  strip of ws2811 leds via Adafruit's NeoPixel library:\n\n  https://github.com/rstephan/ArtnetWifi\n  https://github.com/adafruit/Adafruit_NeoPixel\n*/\n\n#include <Arduino.h>\n#include <ESP8266WiFi.h>         // https://github.com/esp8266/Arduino\n#include <ESP8266WebServer.h>\n#include <ESP8266mDNS.h>\n#include <WiFiManager.h>         // https://github.com/tzapu/WiFiManager\n#include <ArtnetWifi.h>          // https://github.com/rstephan/ArtnetWifi\n#include <Adafruit_NeoPixel.h>\n\n#include \"webinterface.h\"\n#include \"neopixel_mode.h\"\n\nESP8266WebServer server(80);\nconst char* host = \"ARTNET\";\nconst char* version = __DATE__ \" / \" __TIME__;\nfloat fps = 0;\n\n// Neopixel settings\nconst byte dataPin = D2;\nAdafruit_NeoPixel strip = Adafruit_NeoPixel(1, dataPin, NEO_GRBW + NEO_KHZ800); // start with one pixel\n\n// Artnet settings\nArtnetWifi artnet;\nunsigned int packetCounter = 0;\n\n// Global universe buffer\nstruct {\n  uint16_t universe;\n  uint16_t length;\n  uint8_t sequence;\n  uint8_t *data;\n} global;\n\n// use an array of function pointers to jump to the desired mode\nvoid (*mode[]) (uint16_t, uint16_t, uint8_t, uint8_t *) {\n  mode0, mode1, mode2, mode3, mode4, mode5, mode6, mode7, mode8, mode9, mode10, mode11, mode12, mode13, mode14, mode15, mode16\n};\n\n// keep the timing of the function calls\nlong tic_loop = 0, tic_fps = 0, tic_packet = 0, tic_web = 0;\nlong frameCounter = 0;\n\n//this will be called for each UDP packet received\nvoid onDmxPacket(uint16_t universe, uint16_t length, uint8_t sequence, uint8_t * data) {\n  // print some feedback\n  Serial.print(\"packetCounter = \");\n  Serial.print(packetCounter++);\n  if ((millis() - tic_fps) > 1000 && frameCounter > 100) {\n    // don't estimate the FPS too quickly\n    fps = 1000 * frameCounter / (millis() - tic_fps);\n    tic_fps = millis();\n    frameCounter = 0;\n    Serial.print(\",  FPS = \");\n    Serial.print(fps);\n  }\n  Serial.println();\n\n  // copy the data from the UDP packet over to the global universe buffer\n  global.universe = universe;\n  global.sequence = sequence;\n  if (length < 512)\n    global.length = length;\n  for (int i = 0; i < global.length; i++)\n    global.data[i] = data[i];\n} // onDmxpacket\n\nvoid updateNeopixelStrip(void) {\n  // update the neopixel strip configuration\n  strip.updateLength(config.pixels);\n  strip.setBrightness(config.brightness);\n  if (config.leds == 3)\n    strip.updateType(NEO_GRB + NEO_KHZ800);\n  else if (config.leds == 4 && config.white)\n    strip.updateType(NEO_GRBW + NEO_KHZ800);\n  else if (config.leds == 4 && !config.white)\n    strip.updateType(NEO_GRBW + NEO_KHZ800);\n}\n\nvoid setup() {\n  Serial.begin(115200);\n  while (!Serial) {\n    ;\n  }\n  Serial.println(\"setup starting\");\n\n  global.universe = 0;\n  global.sequence = 0;\n  global.length = 512;\n  global.data = (uint8_t *)malloc(512);\n\n  SPIFFS.begin();\n  strip.begin();\n\n  if (loadConfig()) {\n    updateNeopixelStrip();\n    strip.setBrightness(255);\n    singleYellow();\n    delay(1000);\n  }\n  else {\n    updateNeopixelStrip();\n    strip.setBrightness(255);\n    singleRed();\n    delay(1000);\n  }\n\n  WiFiManager wifiManager;\n  wifiManager.setAPStaticIPConfig(IPAddress(192, 168, 1, 1), IPAddress(192, 168, 1, 1), IPAddress(255, 255, 255, 0));\n  wifiManager.autoConnect(host);\n  Serial.println(\"connected\");\n\n  if (WiFi.status() == WL_CONNECTED)\n    singleGreen();\n\n  // this serves all URIs that can be resolved to a file on the SPIFFS filesystem\n  server.onNotFound(handleNotFound);\n\n  server.on(\"/\", HTTP_GET, []() {\n    tic_web = millis();\n    handleRedirect(\"/index.html\");\n  });\n\n  server.on(\"/version\", HTTP_GET, []() {\n    Serial.println(\"version\");\n    server.send(200, \"text/plain\", version);\n  });\n  \n  server.on(\"/defaults\", HTTP_GET, []() {\n    tic_web = millis();\n    Serial.println(\"handleDefaults\");\n    handleStaticFile(\"/reload_success.html\");\n    defaultConfig();\n    saveConfig();\n    server.close();\n    server.stop();\n    ESP.restart();\n  });\n\n  server.on(\"/reconnect\", HTTP_GET, []() {\n    tic_web = millis();\n    Serial.println(\"handleReconnect\");\n    handleStaticFile(\"/reload_success.html\");\n    singleYellow();\n    server.close();\n    server.stop();\n    delay(5000);\n    WiFiManager wifiManager;\n    wifiManager.resetSettings();\n    wifiManager.setAPStaticIPConfig(IPAddress(192, 168, 1, 1), IPAddress(192, 168, 1, 1), IPAddress(255, 255, 255, 0));\n    wifiManager.startConfigPortal(host);\n    Serial.println(\"connected\");\n    server.begin();\n    if (WiFi.status() == WL_CONNECTED)\n      singleGreen();\n  });\n\n  server.on(\"/restart\", HTTP_GET, []() {\n    tic_web = millis();\n    Serial.println(\"handleRestart\");\n    handleStaticFile(\"/reload_success.html\");\n    singleRed();\n    server.close();\n    server.stop();\n    SPIFFS.end();\n    delay(5000);\n    ESP.restart();\n  });\n\n  server.on(\"/dir\", HTTP_GET, [] {\n    tic_web = millis();\n    handleDirList();\n  });\n\n  server.on(\"/json\", HTTP_PUT, [] {\n    Serial.println(\"HTTP_PUT /json\");\n    tic_web = millis();\n    handleJSON();\n  });\n\n  server.on(\"/json\", HTTP_POST, [] {\n    Serial.println(\"HTTP_POST /json\");\n    tic_web = millis();\n    handleJSON();\n  });\n\n  server.on(\"/json\", HTTP_GET, [] {\n    Serial.println(\"HTTP_GET /json\");\n    tic_web = millis();\n    StaticJsonBuffer<300> jsonBuffer;\n    JsonObject& root = jsonBuffer.createObject();\n    N_CONFIG_TO_JSON(universe, \"universe\");\n    N_CONFIG_TO_JSON(offset, \"offset\");\n    N_CONFIG_TO_JSON(pixels, \"pixels\");\n    N_CONFIG_TO_JSON(leds, \"leds\");\n    N_CONFIG_TO_JSON(white, \"white\");\n    N_CONFIG_TO_JSON(brightness, \"brightness\");\n    N_CONFIG_TO_JSON(hsv, \"hsv\");\n    N_CONFIG_TO_JSON(mode, \"mode\");\n    N_CONFIG_TO_JSON(reverse, \"reverse\");\n    N_CONFIG_TO_JSON(speed, \"speed\");\n    N_CONFIG_TO_JSON(split, \"split\");\n    root[\"version\"] = version;\n    root[\"uptime\"]  = long(millis() / 1000);\n    root[\"packets\"] = packetCounter;\n    root[\"fps\"]     = fps;\n    String str;\n    root.printTo(str);\n    server.setContentLength(str.length());\n    server.send(200, \"application/json\", str);\n  });\n\n  server.on(\"/update\", HTTP_GET, [] {\n    tic_web = millis();\n    handleStaticFile(\"/update.html\");\n  });\n\n  server.on(\"/update\", HTTP_POST, handleUpdate1, handleUpdate2);\n\n  // start the web server\n  server.begin();\n\n  // announce the hostname and web server through zeroconf\n  MDNS.begin(host);\n  MDNS.addService(\"http\", \"tcp\", 80);\n\n  artnet.begin();\n  artnet.setArtDmxCallback(onDmxPacket);\n\n  // initialize all timers\n  tic_loop   = millis();\n  tic_packet = millis();\n  tic_fps    = millis();\n  tic_web    = 0;\n\n  Serial.println(\"setup done\");\n} // setup\n\nvoid loop() {\n  server.handleClient();\n\n  if (WiFi.status() != WL_CONNECTED) {\n    singleRed();\n  }\n  else if ((millis() - tic_web) < 5000) {\n    singleBlue();\n  }\n  else  {\n    artnet.read();\n\n    // this section gets executed at a maximum rate of around 1Hz\n    if ((millis() - tic_loop) > 999)\n      updateNeopixelStrip();\n\n    // this section gets executed at a maximum rate of around 100Hz\n    if ((millis() - tic_loop) > 9) {\n      if (config.mode >= 0 && config.mode < (sizeof(mode) / 4)) {\n        // call the function corresponding to the current mode\n        (*mode[config.mode]) (global.universe, global.length, global.sequence, global.data);\n        tic_loop = millis();\n        frameCounter++;\n      }\n    }\n  }\n\n  delay(1);\n} // loop\n"
  },
  {
    "path": "esp8266_artnet_neopixel/font8x8_basic.h",
    "content": "/** \n * 8x8 monochrome bitmap fonts for rendering\n * Author: Daniel Hepper <daniel@hepper.net>\n * \n * Modified from https://github.com/dhepper/font8x8 by Robert Oostenveld\n * \n * License: Public Domain\n * \n * Based on:\n * // Summary: font8x8.h\n * // 8x8 monochrome bitmap fonts for rendering\n * //\n * // Author:\n * //     Marcel Sondaar\n * //     International Business Machines (public domain VGA fonts)\n * //\n * // License:\n * //     Public Domain\n * \n * Fetched from: http://dimensionalrift.homelinux.net/combuster/mos3/?p=viewsource&file=/modules/gfx/font8_8.asm\n **/\n\n// Constant: font8x8_basic\n// Contains an 8x8 font map for unicode points U+0000 - U+007F (basic latin)\nchar font8x8_basic[128][8] = {\n    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+0000 (nul)\n    { B00110110, B01111111, B01111111, B01111111, B00111110, B00011100, B00001000, B00000000}, // U+2764 (heart, see https://en.wikipedia.org/wiki/Dingbat)\n    { B00000000, B00000000, B00000000, B00000000, B00000000, B00000000, B00000000, B00000000},   // U+0002\n    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+0003\n    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+0004\n    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+0005\n    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+0006\n    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+0007\n    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+0008\n    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+0009\n    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+000A\n    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+000B\n    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+000C\n    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+000D\n    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+000E\n    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+000F\n    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+0010\n    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+0011\n    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+0012\n    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+0013\n    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+0014\n    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+0015\n    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+0016\n    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+0017\n    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+0018\n    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+0019\n    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+001A\n    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+001B\n    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+001C\n    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+001D\n    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+001E\n    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+001F\n    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+0020 (space)\n    { 0x18, 0x3C, 0x3C, 0x18, 0x18, 0x00, 0x18, 0x00},   // U+0021 (!)\n    { 0x36, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+0022 (\")\n    { 0x36, 0x36, 0x7F, 0x36, 0x7F, 0x36, 0x36, 0x00},   // U+0023 (#)\n    { 0x0C, 0x3E, 0x03, 0x1E, 0x30, 0x1F, 0x0C, 0x00},   // U+0024 ($)\n    { 0x00, 0x63, 0x33, 0x18, 0x0C, 0x66, 0x63, 0x00},   // U+0025 (%)\n    { 0x1C, 0x36, 0x1C, 0x6E, 0x3B, 0x33, 0x6E, 0x00},   // U+0026 (&)\n    { 0x06, 0x06, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+0027 (')\n    { 0x18, 0x0C, 0x06, 0x06, 0x06, 0x0C, 0x18, 0x00},   // U+0028 (()\n    { 0x06, 0x0C, 0x18, 0x18, 0x18, 0x0C, 0x06, 0x00},   // U+0029 ())\n    { 0x00, 0x66, 0x3C, 0xFF, 0x3C, 0x66, 0x00, 0x00},   // U+002A (*)\n    { 0x00, 0x0C, 0x0C, 0x3F, 0x0C, 0x0C, 0x00, 0x00},   // U+002B (+)\n    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x06},   // U+002C (,)\n    { 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00},   // U+002D (-)\n    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x00},   // U+002E (.)\n    { 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x00},   // U+002F (/)\n    { 0x3E, 0x63, 0x73, 0x7B, 0x6F, 0x67, 0x3E, 0x00},   // U+0030 (0)\n    { 0x0C, 0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x3F, 0x00},   // U+0031 (1)\n    { 0x1E, 0x33, 0x30, 0x1C, 0x06, 0x33, 0x3F, 0x00},   // U+0032 (2)\n    { 0x1E, 0x33, 0x30, 0x1C, 0x30, 0x33, 0x1E, 0x00},   // U+0033 (3)\n    { 0x38, 0x3C, 0x36, 0x33, 0x7F, 0x30, 0x78, 0x00},   // U+0034 (4)\n    { 0x3F, 0x03, 0x1F, 0x30, 0x30, 0x33, 0x1E, 0x00},   // U+0035 (5)\n    { 0x1C, 0x06, 0x03, 0x1F, 0x33, 0x33, 0x1E, 0x00},   // U+0036 (6)\n    { 0x3F, 0x33, 0x30, 0x18, 0x0C, 0x0C, 0x0C, 0x00},   // U+0037 (7)\n    { 0x1E, 0x33, 0x33, 0x1E, 0x33, 0x33, 0x1E, 0x00},   // U+0038 (8)\n    { 0x1E, 0x33, 0x33, 0x3E, 0x30, 0x18, 0x0E, 0x00},   // U+0039 (9)\n    { 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x00},   // U+003A (:)\n    { 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x06},   // U+003B (//)\n    { 0x18, 0x0C, 0x06, 0x03, 0x06, 0x0C, 0x18, 0x00},   // U+003C (<)\n    { 0x00, 0x00, 0x3F, 0x00, 0x00, 0x3F, 0x00, 0x00},   // U+003D (=)\n    { 0x06, 0x0C, 0x18, 0x30, 0x18, 0x0C, 0x06, 0x00},   // U+003E (>)\n    { 0x1E, 0x33, 0x30, 0x18, 0x0C, 0x00, 0x0C, 0x00},   // U+003F (?)\n    { 0x3E, 0x63, 0x7B, 0x7B, 0x7B, 0x03, 0x1E, 0x00},   // U+0040 (@)\n    { 0x0C, 0x1E, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x00},   // U+0041 (A)\n    { 0x3F, 0x66, 0x66, 0x3E, 0x66, 0x66, 0x3F, 0x00},   // U+0042 (B)\n    { 0x3C, 0x66, 0x03, 0x03, 0x03, 0x66, 0x3C, 0x00},   // U+0043 (C)\n    { 0x1F, 0x36, 0x66, 0x66, 0x66, 0x36, 0x1F, 0x00},   // U+0044 (D)\n    { 0x7F, 0x46, 0x16, 0x1E, 0x16, 0x46, 0x7F, 0x00},   // U+0045 (E)\n    { 0x7F, 0x46, 0x16, 0x1E, 0x16, 0x06, 0x0F, 0x00},   // U+0046 (F)\n    { 0x3C, 0x66, 0x03, 0x03, 0x73, 0x66, 0x7C, 0x00},   // U+0047 (G)\n    { 0x33, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x33, 0x00},   // U+0048 (H)\n    { 0x1E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00},   // U+0049 (I)\n    { 0x78, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E, 0x00},   // U+004A (J)\n    { 0x67, 0x66, 0x36, 0x1E, 0x36, 0x66, 0x67, 0x00},   // U+004B (K)\n    { 0x0F, 0x06, 0x06, 0x06, 0x46, 0x66, 0x7F, 0x00},   // U+004C (L)\n    { 0x63, 0x77, 0x7F, 0x7F, 0x6B, 0x63, 0x63, 0x00},   // U+004D (M)\n    { 0x63, 0x67, 0x6F, 0x7B, 0x73, 0x63, 0x63, 0x00},   // U+004E (N)\n    { 0x1C, 0x36, 0x63, 0x63, 0x63, 0x36, 0x1C, 0x00},   // U+004F (O)\n    { 0x3F, 0x66, 0x66, 0x3E, 0x06, 0x06, 0x0F, 0x00},   // U+0050 (P)\n    { 0x1E, 0x33, 0x33, 0x33, 0x3B, 0x1E, 0x38, 0x00},   // U+0051 (Q)\n    { 0x3F, 0x66, 0x66, 0x3E, 0x36, 0x66, 0x67, 0x00},   // U+0052 (R)\n    { 0x1E, 0x33, 0x07, 0x0E, 0x38, 0x33, 0x1E, 0x00},   // U+0053 (S)\n    { 0x3F, 0x2D, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00},   // U+0054 (T)\n    { 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x3F, 0x00},   // U+0055 (U)\n    { 0x33, 0x33, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00},   // U+0056 (V)\n    { 0x63, 0x63, 0x63, 0x6B, 0x7F, 0x77, 0x63, 0x00},   // U+0057 (W)\n    { 0x63, 0x63, 0x36, 0x1C, 0x1C, 0x36, 0x63, 0x00},   // U+0058 (X)\n    { 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x0C, 0x1E, 0x00},   // U+0059 (Y)\n    { 0x7F, 0x63, 0x31, 0x18, 0x4C, 0x66, 0x7F, 0x00},   // U+005A (Z)\n    { 0x1E, 0x06, 0x06, 0x06, 0x06, 0x06, 0x1E, 0x00},   // U+005B ([)\n    { 0x03, 0x06, 0x0C, 0x18, 0x30, 0x60, 0x40, 0x00},   // U+005C (\\)\n    { 0x1E, 0x18, 0x18, 0x18, 0x18, 0x18, 0x1E, 0x00},   // U+005D (])\n    { 0x08, 0x1C, 0x36, 0x63, 0x00, 0x00, 0x00, 0x00},   // U+005E (^)\n    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF},   // U+005F (_)\n    { 0x0C, 0x0C, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+0060 (`)\n    { 0x00, 0x00, 0x1E, 0x30, 0x3E, 0x33, 0x6E, 0x00},   // U+0061 (a)\n    { 0x07, 0x06, 0x06, 0x3E, 0x66, 0x66, 0x3B, 0x00},   // U+0062 (b)\n    { 0x00, 0x00, 0x1E, 0x33, 0x03, 0x33, 0x1E, 0x00},   // U+0063 (c)\n    { 0x38, 0x30, 0x30, 0x3e, 0x33, 0x33, 0x6E, 0x00},   // U+0064 (d)\n    { 0x00, 0x00, 0x1E, 0x33, 0x3f, 0x03, 0x1E, 0x00},   // U+0065 (e)\n    { 0x1C, 0x36, 0x06, 0x0f, 0x06, 0x06, 0x0F, 0x00},   // U+0066 (f)\n    { 0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x1F},   // U+0067 (g)\n    { 0x07, 0x06, 0x36, 0x6E, 0x66, 0x66, 0x67, 0x00},   // U+0068 (h)\n    { 0x0C, 0x00, 0x0E, 0x0C, 0x0C, 0x0C, 0x1E, 0x00},   // U+0069 (i)\n    { 0x30, 0x00, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E},   // U+006A (j)\n    { 0x07, 0x06, 0x66, 0x36, 0x1E, 0x36, 0x67, 0x00},   // U+006B (k)\n    { 0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00},   // U+006C (l)\n    { 0x00, 0x00, 0x33, 0x7F, 0x7F, 0x6B, 0x63, 0x00},   // U+006D (m)\n    { 0x00, 0x00, 0x1F, 0x33, 0x33, 0x33, 0x33, 0x00},   // U+006E (n)\n    { 0x00, 0x00, 0x1E, 0x33, 0x33, 0x33, 0x1E, 0x00},   // U+006F (o)\n    { 0x00, 0x00, 0x3B, 0x66, 0x66, 0x3E, 0x06, 0x0F},   // U+0070 (p)\n    { 0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x78},   // U+0071 (q)\n    { 0x00, 0x00, 0x3B, 0x6E, 0x66, 0x06, 0x0F, 0x00},   // U+0072 (r)\n    { 0x00, 0x00, 0x3E, 0x03, 0x1E, 0x30, 0x1F, 0x00},   // U+0073 (s)\n    { 0x08, 0x0C, 0x3E, 0x0C, 0x0C, 0x2C, 0x18, 0x00},   // U+0074 (t)\n    { 0x00, 0x00, 0x33, 0x33, 0x33, 0x33, 0x6E, 0x00},   // U+0075 (u)\n    { 0x00, 0x00, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00},   // U+0076 (v)\n    { 0x00, 0x00, 0x63, 0x6B, 0x7F, 0x7F, 0x36, 0x00},   // U+0077 (w)\n    { 0x00, 0x00, 0x63, 0x36, 0x1C, 0x36, 0x63, 0x00},   // U+0078 (x)\n    { 0x00, 0x00, 0x33, 0x33, 0x33, 0x3E, 0x30, 0x1F},   // U+0079 (y)\n    { 0x00, 0x00, 0x3F, 0x19, 0x0C, 0x26, 0x3F, 0x00},   // U+007A (z)\n    { 0x38, 0x0C, 0x0C, 0x07, 0x0C, 0x0C, 0x38, 0x00},   // U+007B ({)\n    { 0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x18, 0x00},   // U+007C (|)\n    { 0x07, 0x0C, 0x0C, 0x38, 0x0C, 0x0C, 0x07, 0x00},   // U+007D (})\n    { 0x6E, 0x3B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+007E (~)\n    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}    // U+007F\n};\n\n"
  },
  {
    "path": "esp8266_artnet_neopixel/neopixel_mode.cpp",
    "content": "#include \"neopixel_mode.h\"\n#include \"webinterface.h\"\n#include \"colorspace.h\"\n#include \"font8x8_basic.h\"\n\nextern Adafruit_NeoPixel strip;\nextern long tic_frame;\nfloat prev;\n\nint gamma_l[] = {\n  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,\n  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  1,  1,  1,  1,\n  1,  1,  1,  1,  1,  1,  1,  1,  1,  2,  2,  2,  2,  2,  2,  2,\n  2,  3,  3,  3,  3,  3,  3,  3,  4,  4,  4,  4,  4,  5,  5,  5,\n  5,  6,  6,  6,  6,  7,  7,  7,  7,  8,  8,  8,  9,  9,  9, 10,\n  10, 10, 11, 11, 11, 12, 12, 13, 13, 13, 14, 14, 15, 15, 16, 16,\n  17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22, 23, 24, 24, 25,\n  25, 26, 27, 27, 28, 29, 29, 30, 31, 32, 32, 33, 34, 35, 35, 36,\n  37, 38, 39, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 50,\n  51, 52, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 66, 67, 68,\n  69, 70, 72, 73, 74, 75, 77, 78, 79, 81, 82, 83, 85, 86, 87, 89,\n  90, 92, 93, 95, 96, 98, 99, 101, 102, 104, 105, 107, 109, 110, 112, 114,\n  115, 117, 119, 120, 122, 124, 126, 127, 129, 131, 133, 135, 137, 138, 140, 142,\n  144, 146, 148, 150, 152, 154, 156, 158, 160, 162, 164, 167, 169, 171, 173, 175,\n  177, 180, 182, 184, 186, 189, 191, 193, 196, 198, 200, 203, 205, 208, 210, 213,\n  215, 218, 220, 223, 225, 228, 231, 233, 236, 239, 241, 244, 247, 249, 252, 255\n};\n\n#define RGB  (config.leds==3 || (config.leds==4 && !config.white))\n#define RGBW (                  (config.leds==4 &&  config.white))\n\n/*\n  mode 0: individual pixel control\n  channel 1 = pixel 1 red\n  channel 2 = pixel 1 green\n  channel 3 = pixel 1 blue\n  channel 4 = pixel 1 white\n  channel 5 = pixel 2 red\n  etc.\n*/\n\nvoid mode0(uint16_t universe, uint16_t length, uint8_t sequence, uint8_t * data) {\n  int i = 0, r, g, b, w;\n  if (universe != config.universe)\n    return;\n  if (RGB && (length - config.offset) < 3 * strip.numPixels() + 1)\n    return;\n  if (RGBW && (length - config.offset) < 4 * strip.numPixels() + 1)\n    return;\n\n  for (int pixel = 0; pixel < strip.numPixels(); pixel++) {\n    r         = data[config.offset + i++];\n    g         = data[config.offset + i++];\n    b         = data[config.offset + i++];\n    if (RGBW)\n      w       = data[config.offset + i++];\n\n    if (config.hsv)\n      map_hsv_to_rgb(&r, &g, &b);\n\n    if (RGB)\n      strip.setPixelColor(pixel, r, g, b);\n    else if (RGBW)\n      strip.setPixelColor(pixel, r, g, b, w);\n    yield();\n  }\n  strip.show();\n}\n\n/*\n  mode 1: single uniform color\n  channel 1 = red\n  channel 2 = green\n  channel 3 = blue\n  channel 4 = white\n  channel 5 = intensity (this allows scaling a preset RGBW color with a single channel)\n*/\n\nvoid mode1(uint16_t universe, uint16_t length, uint8_t sequence, uint8_t * data) {\n  int i = 0, r, g, b, w;\n  float intensity;\n  if (universe != config.universe)\n    return;\n  if (RGB && (length - config.offset) < 3 + 1)\n    return;\n  if (RGBW && (length - config.offset) < 4 + 1)\n    return;\n  r         = data[config.offset + i++];\n  g         = data[config.offset + i++];\n  b         = data[config.offset + i++];\n  if (RGBW)\n    w       = data[config.offset + i++];\n  intensity = 1. * data[config.offset + i++] / 255.;\n\n  if (config.hsv)\n    map_hsv_to_rgb(&r, &g, &b);\n\n  // scale with the intensity\n  r = intensity * r;\n  g = intensity * g;\n  b = intensity * b;\n  w = intensity * w;\n\n  for (int pixel = 0; pixel < strip.numPixels(); pixel++) {\n    if (RGB)\n      strip.setPixelColor(pixel, r, g, b);\n    else if (RGBW)\n      strip.setPixelColor(pixel, r, g, b, w);\n    yield();\n  }\n  strip.show();\n}\n\n/*\n  mode 2: two color mixing\n  channel 1  = color 1 red\n  channel 2  = color 1 green\n  channel 3  = color 1 blue\n  channel 4  = color 1 white\n  channel 5  = color 2 red\n  channel 6  = color 2 green\n  channel 7  = color 2 blue\n  channel 8  = color 2 white\n  channel 9  = intensity (this also allows to black out the colors)\n  channel 10 = balance (between color 1 and color2)\n*/\n\nvoid mode2(uint16_t universe, uint16_t length, uint8_t sequence, uint8_t * data) {\n  int i = 0, r, g, b, w, r2, g2, b2, w2;\n  float balance, intensity;\n  if (universe != config.universe)\n    return;\n  if (RGB && (length - config.offset) < 2 * 3 + 2)\n    return;\n  if (RGBW && (length - config.offset) < 2 * 4 + 2)\n    return;\n  r         = data[config.offset + i++];\n  g         = data[config.offset + i++];\n  b         = data[config.offset + i++];\n  if (RGBW)\n    w       = data[config.offset + i++];\n  r2        = data[config.offset + i++];\n  g2        = data[config.offset + i++];\n  b2        = data[config.offset + i++];\n  if (RGBW)\n    w2      = data[config.offset + i++];\n  intensity = 1. * data[config.offset + i++] / 255.;\n  balance   = 1. * data[config.offset + i++] / 255.;\n\n  if (config.hsv) {\n    map_hsv_to_rgb(&r, &g, &b);\n    map_hsv_to_rgb(&r2, &g2, &b2);\n  }\n\n  // apply the balance between the two colors\n  r = BALANCE(balance, r, r2);\n  g = BALANCE(balance, g, g2);\n  b = BALANCE(balance, b, b2);\n  w = BALANCE(balance, w, w2);\n\n  // scale with the intensity\n  r = intensity * r;\n  g = intensity * g;\n  b = intensity * b;\n  w = intensity * w;\n\n  for (int pixel = 0; pixel < strip.numPixels(); pixel++) {\n    if (RGB)\n      strip.setPixelColor(pixel, r, g, b);\n    else if (RGBW)\n      strip.setPixelColor(pixel, r, g, b, w);\n    yield();\n  }\n  strip.show();\n}\n\n/*\n  mode 3: single uniform color, blinking between the color and black\n  channel 1 = red\n  channel 2 = green\n  channel 3 = blue\n  channel 4 = white\n  channel 5 = intensity\n  channel 6 = speed (number of flashes per unit of time)\n  channel 7 = ramp (whether there is a abrubt or more smooth transition)\n  channel 8 = duty cycle (the time ratio between the color and black)\n*/\n\nvoid mode3(uint16_t universe, uint16_t length, uint8_t sequence, uint8_t * data) {\n  int i = 0, r, g, b, w;\n  float intensity, speed, ramp, duty, phase, balance;\n  if (universe != config.universe)\n    return;\n  if (RGB && (length - config.offset) < (3 + 4) * config.split)\n    return;\n  if (RGBW && (length - config.offset) < (4 + 4) * config.split)\n    return;\n\n  // the code that takes care of the blinking repeats for each of the segments\n  for (int segment = 0; segment < config.split; segment++) {\n    r         = data[config.offset + i++];\n    g         = data[config.offset + i++];\n    b         = data[config.offset + i++];\n    if (RGBW)\n      w       = data[config.offset + i++];\n    intensity = data[config.offset + i++] / 255.;\n    speed     = 1. * data[config.offset + i++] / config.speed;\n    ramp      = 1. * data[config.offset + i++] * 360. / 255.;\n    duty      = 1. * data[config.offset + i++] * 360. / 255.;\n\n    if (config.hsv)\n      map_hsv_to_rgb(&r, &g, &b);\n\n    // the ramp cannot be too wide\n    if (duty < 180)\n      ramp = (ramp < duty ? ramp : duty);\n    else\n      ramp = (ramp < (360 - duty) ? ramp : (360 - duty));\n\n    // determine the current phase in the temporal cycle\n    phase = (speed * millis()) * 360. / 1000.;\n\n    // prevent rolling back\n    // only feasible with a single segment\n    if (config.split == 1 && WRAP180(phase - prev) < 0)\n      phase = prev;\n    else\n      prev = phase;\n\n    phase = WRAP180(phase);\n    phase = ABS(phase);\n\n    if (phase <= (duty / 2 - ramp / 4))\n      balance = 1;\n    else if (phase >= (duty / 2 + ramp / 4))\n      balance = 0;\n    else if (ramp > 0)\n      balance = ((duty / 2 + ramp / 4) - phase) / ( ramp / 2 );\n\n    // scale with the intensity\n    r *= intensity;\n    g *= intensity;\n    b *= intensity;\n    w *= intensity;\n\n    // scale with the balance\n    r *= balance;\n    g *= balance;\n    b *= balance;\n    w *= balance;\n\n    int begpixel = MAX((segment + 0) * strip.numPixels() / config.split, 0);\n    int endpixel = MIN((segment + 1) * strip.numPixels() / config.split, strip.numPixels());\n    for (int pixel = begpixel; pixel < endpixel; pixel++) {\n      if (RGB)\n        strip.setPixelColor(pixel, r, g, b);\n      else if (RGBW)\n        strip.setPixelColor(pixel, r, g, b, w);\n      yield();\n    }\n  }\n  strip.show();\n}\n\n/*\n  mode 4: uniform color, blinking between color 1 and color 2\n  channel 1  = color 1 red\n  channel 2  = color 1 green\n  channel 3  = color 1 blue\n  channel 4  = color 1 white\n  channel 5  = color 2 red\n  channel 6  = color 2 green\n  channel 7  = color 2 blue\n  channel 8  = color 2 white\n  channel 9  = intensity\n  channel 10 = speed\n  channel 11 = ramp\n  channel 12 = duty cycle\n*/\n\nvoid mode4(uint16_t universe, uint16_t length, uint8_t sequence, uint8_t * data) {\n  int i = 0, r, g, b, w, r2, g2, b2, w2;\n  float intensity, speed, ramp, duty, phase, balance;\n  if (universe != config.universe)\n    return;\n  if (RGB && (length - config.offset) < 2 * 3 + 4)\n    return;\n  if (RGBW && (length - config.offset) < 2 * 4 + 4)\n    return;\n  r         = data[config.offset + i++];\n  g         = data[config.offset + i++];\n  b         = data[config.offset + i++];\n  if (RGBW)\n    w       = data[config.offset + i++];\n  r2        = data[config.offset + i++];\n  g2        = data[config.offset + i++];\n  b2        = data[config.offset + i++];\n  if (RGBW)\n    w2      = data[config.offset + i++];\n  intensity = 1. * data[config.offset + i++] / 255.;\n  speed     = 1. * data[config.offset + i++] / config.speed;\n  ramp      = 1. * data[config.offset + i++] * 360. / 255.;\n  duty      = 1. * data[config.offset + i++] * 360. / 255.;\n\n  if (config.hsv) {\n    map_hsv_to_rgb(&r, &g, &b);\n    map_hsv_to_rgb(&r2, &g2, &b2);\n  }\n\n  // the ramp cannot be too wide\n  if (duty < 180)\n    ramp = (ramp < duty ? ramp : duty);\n  else\n    ramp = (ramp < (360 - duty) ? ramp : (360 - duty));\n\n  // determine the current phase in the temporal cycle\n  phase = (speed * millis()) * 360. / 1000.;\n\n  // prevent rolling back\n  if (WRAP180(phase - prev) < 0)\n    phase = prev;\n  else\n    prev = phase;\n\n  phase = WRAP180(phase);\n  phase = ABS(phase);\n\n  if (phase <= (duty / 2 - ramp / 4))\n    balance = 1;\n  else if (phase >= (duty / 2 + ramp / 4))\n    balance = 0;\n  else if (ramp > 0)\n    balance = ((duty / 2 + ramp / 4) - phase) / ( ramp / 2 );\n\n  // apply the balance between the two colors\n  r = BALANCE(balance, r, r2);\n  g = BALANCE(balance, g, g2);\n  b = BALANCE(balance, b, b2);\n  w = BALANCE(balance, w, w2);\n\n  // scale with the intensity\n  r = intensity * r;\n  g = intensity * g;\n  b = intensity * b;\n  w = intensity * w;\n\n  for (int pixel = 0; pixel < strip.numPixels(); pixel++) {\n    if (RGB)\n      strip.setPixelColor(pixel, r, g, b);\n    else if (RGBW)\n      strip.setPixelColor(pixel, r, g, b, w);\n    yield();\n  }\n  strip.show();\n}\n\n/*\n  mode 5: single color slider, segment that can be moved along the array (between the edges)\n  channel 1 = red\n  channel 2 = green\n  channel 3 = blue\n  channel 4 = white\n  channel 5 = intensity\n  channel 6 = position (from 0-255 or 0-360 degrees, relative to the length of the array)\n  channel 7 = width    (from 0-255 or 0-360 degrees, relative to the length of the array)\n*/\n\nvoid mode5(uint16_t universe, uint16_t length, uint8_t sequence, uint8_t * data) {\n  int i = 0, r, g, b, w;\n  float intensity, width, position;\n  if (universe != config.universe)\n    return;\n  if (RGB && (length - config.offset) < 3 + 3)\n    return;\n  if (RGBW && (length - config.offset) < 4 + 3)\n    return;\n  r         = data[config.offset + i++];\n  g         = data[config.offset + i++];\n  b         = data[config.offset + i++];\n  if (RGBW)\n    w       = data[config.offset + i++];\n  intensity = data[config.offset + i++] / 255.;\n  position  = data[config.offset + i++] * (strip.numPixels() - 1) / 255.;\n  width     = data[config.offset + i++] * (strip.numPixels() - 0) / 255.;\n\n  if (config.hsv)\n    map_hsv_to_rgb(&r, &g, &b);\n\n  // scale with the intensity\n  r = intensity * r;\n  g = intensity * g;\n  b = intensity * b;\n  w = intensity * w;\n\n  // the position needs to be corrected for the width\n  position -= strip.numPixels() / 2;\n  position /= strip.numPixels() / 2;\n  position *= (strip.numPixels() - width) / 2;\n  position += strip.numPixels() / 2;\n\n  // express the position and with as phase along the strip\n  position *= 360. / strip.numPixels();\n  width    *= 360. / strip.numPixels();\n\n  for (int pixel = 0; pixel < strip.numPixels(); pixel++) {\n    int flip = (config.reverse ? -1 : 1);\n    float phase, balance;\n\n    phase = WRAP180((360. * flip * pixel / strip.numPixels()) * config.split - position);\n    phase = ABS(phase);\n\n    if (width == 0)\n      balance = 0;\n    else if (phase <= width / 2)\n      balance = 1;\n    else\n      balance = 0;\n\n    if (RGB)\n      strip.setPixelColor(pixel, balance * r, balance * g, balance * b);\n    else if (RGBW)\n      strip.setPixelColor(pixel, balance * r, balance * g, balance * b, balance * w);\n    yield();\n  }\n  strip.show();\n}\n\n/*\n  mode 6: dual color slider, segment can be moved along the array (between the edges)\n  channel 1  = color 1 red\n  channel 2  = color 1 green\n  channel 3  = color 1 blue\n  channel 4  = color 1 white\n  channel 5  = color 2 red\n  channel 6  = color 2 green\n  channel 7  = color 2 blue\n  channel 8  = color 2 white\n  channel 9  = intensity\n  channel 10 = position (from 0-255 or 0-360 degrees, relative to the length of the array)\n  channel 11 = width    (from 0-255 or 0-360 degrees, relative to the length of the array)\n*/\n\nvoid mode6(uint16_t universe, uint16_t length, uint8_t sequence, uint8_t * data) {\n  int i = 0, r, g, b, w, r2, g2, b2, w2;\n  float intensity, width, position;\n  if (universe != config.universe)\n    return;\n  if (RGB && (length - config.offset) < 2 * 3 + 3)\n    return;\n  if (RGBW && (length - config.offset) < 2 * 4 + 3)\n    return;\n  r         = data[config.offset + i++];\n  g         = data[config.offset + i++];\n  b         = data[config.offset + i++];\n  if (RGBW)\n    w       = data[config.offset + i++];\n  r2        = data[config.offset + i++];\n  g2        = data[config.offset + i++];\n  b2        = data[config.offset + i++];\n  if (RGBW)\n    w2      = data[config.offset + i++];\n  intensity = data[config.offset + i++] / 255.;\n  position  = data[config.offset + i++] * (strip.numPixels() - 1) / 255.;\n  width     = data[config.offset + i++] * (strip.numPixels() - 0) / 255.;\n\n  if (config.hsv) {\n    map_hsv_to_rgb(&r, &g, &b);\n    map_hsv_to_rgb(&r2, &g2, &b2);\n  }\n\n  // the position needs to be corrected for the width\n  position -= strip.numPixels() / 2;\n  position /= strip.numPixels() / 2;\n  position *= (strip.numPixels() - width) / 2;\n  position += strip.numPixels() / 2;\n\n  // express the position and with as phase along the strip\n  position *= 360. / strip.numPixels();\n  width    *= 360. / strip.numPixels();\n\n  for (int pixel = 0; pixel < strip.numPixels(); pixel++) {\n    int flip = (config.reverse ? -1 : 1);\n    float phase, balance;\n\n    phase = WRAP180((360. * flip * pixel / (strip.numPixels() - 1)) * config.split - position);\n    phase = ABS(phase);\n\n    if (width == 0)\n      balance = 0;\n    else if (phase <= width / 2)\n      balance = 1;\n    else\n      balance = 0;\n\n    if (RGB)\n      strip.setPixelColor(pixel, intensity * BALANCE(balance, r, r2), intensity * BALANCE(balance, g, g2), intensity * BALANCE(balance, b, b2));\n    else if (RGBW)\n      strip.setPixelColor(pixel, intensity * BALANCE(balance, r, r2), intensity * BALANCE(balance, g, g2), intensity * BALANCE(balance, b, b2), intensity * BALANCE(balance, w, w2));\n    yield();\n  }\n  strip.show();\n}\n\n/*\n  mode 7: single color smooth slider, segment can be moved along the array (continuous over the edge)\n  channel 1 = red\n  channel 2 = green\n  channel 3 = blue\n  channel 4 = white\n  channel 5 = intensity\n  channel 6 = position (from 0-255 or 0-360 degrees, relative to the length of the array)\n  channel 7 = width    (from 0-255 or 0-360 degrees, relative to the length of the array)\n  channel 8 = ramp     (from 0-255 or 0-360 degrees, relative to the length of the array)\n*/\n\nvoid mode7(uint16_t universe, uint16_t length, uint8_t sequence, uint8_t * data) {\n  int i = 0, r, g, b, w;\n  float intensity, position, width, ramp;\n  if (universe != config.universe)\n    return;\n  if (RGB && (length - config.offset) < 3 + 4)\n    return;\n  if (RGBW && (length - config.offset) < 4 + 4)\n    return;\n  r         = data[config.offset + i++];\n  g         = data[config.offset + i++];\n  b         = data[config.offset + i++];\n  if (RGBW)\n    w       = data[config.offset + i++];\n  intensity = data[config.offset + i++] / 255.;\n  position  = data[config.offset + i++] * 360. / 255.;\n  width     = data[config.offset + i++] * 360. / 255.;\n  ramp      = data[config.offset + i++] * 360. / 255.;\n\n  if (config.hsv)\n    map_hsv_to_rgb(&r, &g, &b);\n\n  // the ramp cannot be too wide\n  if (width < 180)\n    ramp = (ramp < width ? ramp : width);\n  else\n    ramp = (ramp < (360 - width) ? ramp : (360 - width));\n\n  // scale with the intensity\n  r = intensity * r;\n  g = intensity * g;\n  b = intensity * b;\n  w = intensity * w;\n\n  for (int pixel = 0; pixel < strip.numPixels(); pixel++) {\n    int flip = (config.reverse ? -1 : 1);\n    float phase, balance;\n\n    phase = WRAP180(360. * flip * pixel / (strip.numPixels() - 1) * config.split - position);\n    phase = ABS(phase);\n\n    if (width == 0)\n      balance = 0;\n    else if (phase < (width / 2. - ramp / 2.))\n      balance = 1;\n    else if (phase > (width / 2. + ramp / 2.))\n      balance = 0;\n    else if (ramp > 0)\n      balance = ((width / 2. + ramp / 2.) - phase) / ramp;\n\n    if (RGB)\n      strip.setPixelColor(pixel, balance * r, balance * g, balance * b);\n    else if (RGBW)\n      strip.setPixelColor(pixel, balance * r, balance * g, balance * b, balance * w);\n    yield();\n  }\n  strip.show();\n}\n\n/*\n  mode 8: dual color smooth slider, segment can be moved along the array (continuous over the edge)\n  channel 1  = color 1 red\n  channel 2  = color 1 green\n  channel 3  = color 1 blue\n  channel 4  = color 1 white\n  channel 5  = color 2 red\n  channel 6  = color 2 green\n  channel 7  = color 2 blue\n  channel 8  = color 2 white\n  channel 9  = intensity\n  channel 10 = position (from 0-255 or 0-360 degrees, relative to the length of the array)\n  channel 11 = width    (from 0-255 or 0-360 degrees, relative to the length of the array)\n  channel 12 = ramp     (from 0-255 or 0-360 degrees, relative to the length of the array)\n*/\n\nvoid mode8(uint16_t universe, uint16_t length, uint8_t sequence, uint8_t * data) {\n  int i = 0, r, g, b, w, r2, g2, b2, w2;\n  float intensity, position, width, ramp;\n  if (universe != config.universe)\n    return;\n  if (RGB && (length - config.offset) < 2 * 3 + 4)\n    return;\n  if (RGBW && (length - config.offset) < 2 * 4 + 4)\n    return;\n  r         = data[config.offset + i++];\n  g         = data[config.offset + i++];\n  b         = data[config.offset + i++];\n  if (RGBW)\n    w       = data[config.offset + i++];\n  r2        = data[config.offset + i++];\n  g2        = data[config.offset + i++];\n  b2        = data[config.offset + i++];\n  if (RGBW)\n    w2      = data[config.offset + i++];\n  intensity = data[config.offset + i++] / 255.;\n  position  = data[config.offset + i++] * 360. / 255.;\n  width     = data[config.offset + i++] * 360. / 255.;\n  ramp      = data[config.offset + i++] * 360. / 255.;\n\n  if (config.hsv) {\n    map_hsv_to_rgb(&r, &g, &b);\n    map_hsv_to_rgb(&r2, &g2, &b2);\n  }\n\n  // the ramp cannot be too wide\n  if (width < 180)\n    ramp = (ramp < width ? ramp : width);\n  else\n    ramp = (ramp < (360 - width) ? ramp : (360 - width));\n\n  for (int pixel = 0; pixel < strip.numPixels(); pixel++) {\n    int flip = (config.reverse ? -1 : 1);\n    float phase, balance;\n\n    phase = WRAP180(360. * flip * pixel / (strip.numPixels() - 1) * config.split - position);\n    phase = ABS(phase);\n\n    if (width == 0)\n      balance = 0;\n    else if (phase < (width / 2. - ramp / 2.))\n      balance = 1;\n    else if (phase > (width / 2. + ramp / 2.))\n      balance = 0;\n    else if (ramp > 0)\n      balance = ((width / 2. + ramp / 2.) - phase) / ramp;\n\n    if (RGB)\n      strip.setPixelColor(pixel, intensity * BALANCE(balance, r, r2), intensity * BALANCE(balance, g, g2), intensity * BALANCE(balance, b, b2));\n    else if (RGBW)\n      strip.setPixelColor(pixel, intensity * BALANCE(balance, r, r2), intensity * BALANCE(balance, g, g2), intensity * BALANCE(balance, b, b2), intensity * BALANCE(balance, w, w2));\n    yield();\n  }\n  strip.show();\n}\n\n/*\n  mode 9: spinning color wheel\n  channel 1 = red\n  channel 2 = green\n  channel 3 = blue\n  channel 4 = white\n  channel 5 = intensity\n  channel 6 = speed\n  channel 7 = width\n  channel 8 = ramp\n*/\n\nvoid mode9(uint16_t universe, uint16_t length, uint8_t sequence, uint8_t * data) {\n  int i = 0, r, g, b, w;\n  float intensity, speed, width, ramp, phase;\n  if (universe != config.universe)\n    return;\n  if (RGB && (length - config.offset) < 3 + 4)\n    return;\n  if (RGBW && (length - config.offset) < 4 + 4)\n    return;\n  r         = data[config.offset + i++];\n  g         = data[config.offset + i++];\n  b         = data[config.offset + i++];\n  if (RGBW)\n    w       = data[config.offset + i++];\n  intensity = 1. * data[config.offset + i++] / 255.;\n  speed     = 1. * data[config.offset + i++] / config.speed;\n  width     = 1. * data[config.offset + i++] * 360. / 255.;\n  ramp      = 1. * data[config.offset + i++] * 360. / 255.;\n\n  if (config.hsv)\n    map_hsv_to_rgb(&r, &g, &b);\n\n  // the ramp cannot be too wide\n  if (width < 180)\n    ramp = (ramp < width ? ramp : width);\n  else\n    ramp = (ramp < (360 - width) ? ramp : (360 - width));\n\n  // scale with the intensity\n  r = intensity * r;\n  g = intensity * g;\n  b = intensity * b;\n  w = intensity * w;\n\n  // determine the current phase in the temporal cycle\n  phase = (speed * millis()) * 360. / 1000.;\n\n  // prevent rolling back\n  if (WRAP180(phase - prev) < 0)\n    phase = prev;\n  else\n    prev = phase;\n\n  for (int pixel = 0; pixel < strip.numPixels(); pixel++) {\n    int flip = (config.reverse ? -1 : 1);\n    float position, balance;\n\n    position = WRAP180(360. * flip * pixel / (strip.numPixels() - 1) * config.split - phase);\n    position = ABS(position);\n\n    if (width == 0)\n      balance = 0;\n    else if (position < (width / 2. - ramp / 2.))\n      balance = 1;\n    else if (position > (width / 2. + ramp / 2.))\n      balance = 0;\n    else if (position > 0)\n      balance = ((width / 2. + ramp / 2.) - position) / ramp;\n\n    if (RGB)\n      strip.setPixelColor(pixel, balance * r, balance * g, balance * b);\n    else if (RGBW)\n      strip.setPixelColor(pixel, balance * r, balance * g, balance * b, balance * w);\n    yield();\n  }\n  strip.show();\n};\n\n/*\n  mode 10: spinning color wheel with color background\n  channel 1  = color 1 red\n  channel 2  = color 1 green\n  channel 3  = color 1 blue\n  channel 4  = color 1 white\n  channel 5  = color 2 red\n  channel 6  = color 2 green\n  channel 7  = color 2 blue\n  channel 8  = color 2 white\n  channel 9  = intensity\n  channel 10 = speed\n  channel 11 = width\n  channel 12 = ramp\n*/\n\nvoid mode10(uint16_t universe, uint16_t length, uint8_t sequence, uint8_t * data) {\n  int i = 0, r, g, b, w, r2, g2, b2, w2;\n  float intensity, speed, width, ramp, phase;\n  if (universe != config.universe)\n    return;\n  if (RGB && (length - config.offset) < 2 * 3 + 4)\n    return;\n  if (RGBW && (length - config.offset) < 2 * 4 + 4)\n    return;\n  r         = data[config.offset + i++];\n  g         = data[config.offset + i++];\n  b         = data[config.offset + i++];\n  if (RGBW)\n    w       = data[config.offset + i++];\n  r2        = data[config.offset + i++];\n  g2        = data[config.offset + i++];\n  b2        = data[config.offset + i++];\n  if (RGBW)\n    w2      = data[config.offset + i++];\n  intensity = 1. * data[config.offset + i++] / 255.;\n  speed     = 1. * data[config.offset + i++] / config.speed;\n  width     = 1. * data[config.offset + i++] * 360. / 255.;\n  ramp      = 1. * data[config.offset + i++] * 360. / 255.;\n\n  if (config.hsv) {\n    map_hsv_to_rgb(&r, &g, &b);\n    map_hsv_to_rgb(&r2, &g2, &b2);\n  }\n\n  // the ramp cannot be too wide\n  if (width < 180)\n    ramp = (ramp < width ? ramp : width);\n  else\n    ramp = (ramp < (360 - width) ? ramp : (360 - width));\n\n  // determine the current phase in the temporal cycle\n  phase = (speed * millis()) * 360. / 1000.;\n\n  // prevent rolling back\n  if (WRAP180(phase - prev) < 0)\n    phase = prev;\n  else\n    prev = phase;\n\n  for (int pixel = 0; pixel < strip.numPixels(); pixel++) {\n    int flip = (config.reverse ? -1 : 1);\n    float position, balance;\n\n    position = WRAP180((360. * flip * pixel / (strip.numPixels() - 1)) * config.split - phase);\n    position = ABS(position);\n\n    if (width == 0)\n      balance = 0;\n    else if (position < (width / 2. - ramp / 2.))\n      balance = 1;\n    else if (position > (width / 2. + ramp / 2.))\n      balance = 0;\n    else if (position > 0)\n      balance = ((width / 2. + ramp / 2.) - position) / ramp;\n\n    if (RGB)\n      strip.setPixelColor(pixel, intensity * BALANCE(balance, r, r2), intensity * BALANCE(balance, g, g2), intensity * BALANCE(balance, b, b2));\n    else if (RGBW)\n      strip.setPixelColor(pixel, intensity * BALANCE(balance, r, r2), intensity * BALANCE(balance, g, g2), intensity * BALANCE(balance, b, b2), intensity * BALANCE(balance, w, w2));\n    yield();\n  }\n  strip.show();\n};\n\n/*\n  mode 11: rainbow slider\n  channel 1 = saturation\n  channel 2 = value\n  channel 3 = position\n*/\n\nvoid mode11(uint16_t universe, uint16_t length, uint8_t sequence, uint8_t * data) {\n  int i = 0;\n  float saturation, value, position;\n\n  if (universe != config.universe)\n    return;\n  if ((length - config.offset) < 3)\n    return;\n  saturation = 1. * data[config.offset + i++];\n  value      = 1. * data[config.offset + i++] ;\n  position   = 1. * data[config.offset + i++] * 360. / 255.;\n\n  for (int pixel = 0; pixel < strip.numPixels(); pixel++) {\n    int flip = (config.reverse ? -1 : 1);\n    float phase = WRAP360((360. * flip * pixel / strip.numPixels()) * config.split - position);\n\n    int r, g, b;\n    r = phase;           // hue, between 0-360\n    g = saturation;      // saturation, between 0-255\n    b = value;           // value, between 0-255\n    map_hsv_to_rgb(&r, &g, &b);\n\n    strip.setPixelColor(pixel, r, g, b);\n    yield();\n  }\n  strip.show();\n};\n\n/*\n  mode 12: rainbow spinner\n  channel 1 = saturation\n  channel 2 = value\n  channel 3 = speed\n*/\n\nvoid mode12(uint16_t universe, uint16_t length, uint8_t sequence, uint8_t * data) {\n  int i = 0;\n  float saturation, value, speed, phase;\n\n  if (universe != config.universe)\n    return;\n  if ((length - config.offset) < 3)\n    return;\n  saturation = 1. * data[config.offset + i++];\n  value      = 1. * data[config.offset + i++] ;\n  speed      = 1. * data[config.offset + i++] / config.speed;\n\n  // determine the current phase in the temporal cycle\n  phase = (speed * millis()) * 360. / 1000.;\n\n  // prevent rolling back\n  if (WRAP180(phase - prev) < 0)\n    phase = prev;\n  else\n    prev = phase;\n\n  for (int pixel = 0; pixel < strip.numPixels(); pixel++) {\n    int flip = (config.reverse ? -1 : 1);\n    float position = WRAP360((360. * flip * pixel / strip.numPixels()) * config.split - phase);\n\n    int r, g, b;\n    r = position;        // hue, between 0-360\n    g = saturation;      // saturation, between 0-255\n    b = value;           // value, between 0-255\n    map_hsv_to_rgb(&r, &g, &b);\n\n    strip.setPixelColor(pixel, r, g, b);\n    yield();\n  }\n  strip.show();\n};\n\n/*\n  mode 13: dual color letter for 8x8 RGBW neopixel array\n  channel 1  = color 1 red\n  channel 2  = color 1 green\n  channel 3  = color 1 blue\n  channel 4  = color 1 white\n  channel 5  = color 2 red\n  channel 6  = color 2 green\n  channel 7  = color 2 blue\n  channel 8  = color 2 white\n  channel 9  = intensity\n  channel 10 = ASCII code\n*/\n\nvoid mode13(uint16_t universe, uint16_t length, uint8_t sequence, uint8_t * data) {\n  int i = 0, r1, g1, b1, w1, r2, g2, b2, w2;\n  byte glyph;\n  float intensity;\n  if (universe != config.universe)\n    return;\n  if (RGB && (length - config.offset) < 3 + 1)\n    return;\n  if (RGBW && (length - config.offset) < 4 + 1)\n    return;\n  r1         = data[config.offset + i++];\n  g1         = data[config.offset + i++];\n  b1         = data[config.offset + i++];\n  if (RGBW)\n    w1       = data[config.offset + i++];\n  r2         = data[config.offset + i++];\n  g2         = data[config.offset + i++];\n  b2         = data[config.offset + i++];\n  if (RGBW)\n    w2       = data[config.offset + i++];\n\n  intensity = 1. * data[config.offset + i++] / 255.;\n  glyph     =      data[config.offset + i++];\n\n  if (glyph > 127)\n    return;\n\n  if (config.hsv) {\n    map_hsv_to_rgb(&r1, &g1, &b1);\n    map_hsv_to_rgb(&r2, &g2, &b2);\n  }\n\n  // scale with the intensity\n  r1 = intensity * r1;\n  g1 = intensity * g1;\n  b1 = intensity * b1;\n  w1 = intensity * w1;\n  r2 = intensity * r2;\n  g2 = intensity * g2;\n  b2 = intensity * b2;\n  w2 = intensity * w2;\n\n  int pixel = 0;\n  for (int row = 0; row < 8; row++) {\n    for (int col = 0; col < 8; col++) {\n      bool toggle = font8x8_basic[glyph][row] & (0x01 << col);\n      if (RGB && toggle)\n        strip.setPixelColor(pixel, r1, g1, b1);\n      else if (RGBW && toggle)\n        strip.setPixelColor(pixel, r1, g1, b1, w1);\n      else if (RGB && !toggle)\n        strip.setPixelColor(pixel, r2, g2, b2);\n      else if (RGBW && !toggle)\n        strip.setPixelColor(pixel, r2, g2, b2, w2);\n      pixel++;\n    }\n    yield();\n  }\n  strip.show();\n};\n\n/************************************************************************************/\n/************************************************************************************/\n/************************************************************************************/\n\nvoid mode14(uint16_t universe, uint16_t length, uint8_t sequence, uint8_t * data) {};\nvoid mode15(uint16_t universe, uint16_t length, uint8_t sequence, uint8_t * data) {};\nvoid mode16(uint16_t universe, uint16_t length, uint8_t sequence, uint8_t * data) {};\n\n/************************************************************************************/\n/************************************************************************************/\n/************************************************************************************/\n\nvoid singleLed(byte r, byte g, byte b, byte w) {\n  fullBlack();\n  strip.setPixelColor(0, strip.Color(r, g, b, w ) );\n  strip.show();\n}\n\nvoid singleRed() {\n  singleLed(128, 0, 0, 0);\n}\nvoid singleGreen() {\n  singleLed(0, 128, 0, 0);\n}\nvoid singleBlue() {\n  singleLed(0, 0, 128, 0);\n}\nvoid singleYellow() {\n  singleLed(128, 128, 0, 0);\n}\nvoid singleCyan() {\n  singleLed(0, 128, 128, 0);\n}\nvoid singleMagenta() {\n  singleLed(128, 0, 128, 0);\n}\n\n// helper functions to convert an integer into individual RGBW values\nuint8_t red(uint32_t c) {\n  return (c >> 8);\n}\nuint8_t green(uint32_t c) {\n  return (c >> 16);\n}\nuint8_t blue(uint32_t c) {\n  return (c);\n}\n\n// The colours transition from r - g - b - back to r.\nuint32_t Wheel(byte WheelPos) {\n  WheelPos = 255 - WheelPos;\n  if (WheelPos < 85) {\n    return strip.Color(255 - WheelPos * 3, 0, WheelPos * 3, 0);\n  }\n  if (WheelPos < 170) {\n    WheelPos -= 85;\n    return strip.Color(0, WheelPos * 3, 255 - WheelPos * 3, 0);\n  }\n  WheelPos -= 170;\n  return strip.Color(WheelPos * 3, 255 - WheelPos * 3, 0, 0);\n}\n\n\nvoid fullRed() {\n  Serial.println(\"fullRed\");\n  for (uint16_t i = 0; i < strip.numPixels(); i++) {\n    strip.setPixelColor(i, strip.Color(255, 0, 0, 0 ) );\n  }\n  strip.show();\n}\n\nvoid fullGreen() {\n  for (uint16_t i = 0; i < strip.numPixels(); i++) {\n    strip.setPixelColor(i, strip.Color(0, 255, 0 ) );\n  }\n  strip.show();\n}\n\nvoid fullBlue() {\n  for (uint16_t i = 0; i < strip.numPixels(); i++) {\n    strip.setPixelColor(i, strip.Color(0, 0, 255, 0 ) );\n  }\n  strip.show();\n}\n\nvoid fullWhite() {\n  for (uint16_t i = 0; i < strip.numPixels(); i++) {\n    strip.setPixelColor(i, strip.Color(0, 0, 0, 255 ) );\n  }\n  strip.show();\n}\n\nvoid fullBlack() {\n  for (uint16_t i = 0; i < strip.numPixels(); i++) {\n    strip.setPixelColor(i, strip.Color(0, 0, 0, 0 ) );\n  }\n  strip.show();\n}\n\n// Fill the dots one after the other with a specific color\nvoid colorWipe(uint8_t wait, uint32_t c) {\n  for (uint16_t i = 0; i < strip.numPixels(); i++) {\n    strip.setPixelColor(i, c);\n    strip.show();\n    delay(wait);\n  }\n}\n\nvoid pulseWhite(uint8_t wait) {\n  for (int j = 0; j < 256 ; j++) {\n    for (uint16_t i = 0; i < strip.numPixels(); i++) {\n      strip.setPixelColor(i, strip.Color(0, 0, 0, gamma_l[j] ) );\n    }\n    delay(wait);\n    strip.show();\n  }\n  for (int j = 255; j >= 0 ; j--) {\n    for (uint16_t i = 0; i < strip.numPixels(); i++) {\n      strip.setPixelColor(i, strip.Color(0, 0, 0, gamma_l[j] ) );\n    }\n    delay(wait);\n    strip.show();\n  }\n}\n\n\nvoid rainbowFade2White(uint8_t wait, int rainbowLoops, int whiteLoops) {\n  float fadeMax = 100.0;\n  int fadeVal = 0;\n  uint32_t wheelVal;\n  int redVal, greenVal, blueVal;\n  for (int k = 0 ; k < rainbowLoops ; k ++) {\n    for (int j = 0; j < 256; j++) { // 5 cycles of all colors on wheel\n      for (int i = 0; i < strip.numPixels(); i++) {\n        wheelVal = Wheel(((i * 256 / strip.numPixels()) + j) & 255);\n        redVal = red(wheelVal) * float(fadeVal / fadeMax);\n        greenVal = green(wheelVal) * float(fadeVal / fadeMax);\n        blueVal = blue(wheelVal) * float(fadeVal / fadeMax);\n        strip.setPixelColor( i, strip.Color( redVal, greenVal, blueVal ) );\n      }\n      //First loop, fade in!\n      if (k == 0 && fadeVal < fadeMax - 1) {\n        fadeVal++;\n      }\n      //Last loop, fade out!\n      else if (k == rainbowLoops - 1 && j > 255 - fadeMax ) {\n        fadeVal--;\n      }\n      strip.show();\n      delay(wait);\n    }\n\n  }\n  /*  delay(500);\n    for (int k = 0 ; k < whiteLoops ; k ++) {\n      for (int j = 0; j < 256 ; j++) {\n        for (uint16_t i = 0; i < strip.numPixels(); i++) {\n          strip.setPixelColor(i, strip.Color(0, 0, 0, gamma_l[j] ) );\n        }\n        strip.show();\n      }\n      delay(2000);\n      for (int j = 255; j >= 0 ; j--) {\n        for (uint16_t i = 0; i < strip.numPixels(); i++) {\n          strip.setPixelColor(i, strip.Color(0, 0, 0, gamma_l[j] ) );\n        }\n        strip.show();\n      }\n    }\n    delay(500);\n  */\n}\n\n\nvoid whiteOverRainbow(uint8_t wait, uint8_t whiteSpeed, uint8_t whiteLength ) {\n  if (whiteLength >= strip.numPixels()) whiteLength = strip.numPixels() - 1;\n  int head = whiteLength - 1;\n  int tail = 0;\n  int loops = 3;\n  int loopNum = 0;\n  static unsigned long lastTime = 0;\n  while (true) {\n    for (int j = 0; j < 256; j++) {\n      for (uint16_t i = 0; i < strip.numPixels(); i++) {\n        if ((i >= tail && i <= head) || (tail > head && i >= tail) || (tail > head && i <= head) ) {\n          strip.setPixelColor(i, strip.Color(0, 0, 0, 255 ) );\n        }\n        else {\n          strip.setPixelColor(i, Wheel(((i * 256 / strip.numPixels()) + j) & 255));\n        }\n      }\n      if (millis() - lastTime > whiteSpeed) {\n        head++;\n        tail++;\n        if (head == strip.numPixels()) {\n          loopNum++;\n        }\n        lastTime = millis();\n      }\n      if (loopNum == loops) return;\n      head %= strip.numPixels();\n      tail %= strip.numPixels();\n      strip.show();\n      delay(wait);\n    }\n  }\n}\n\n\n// Slightly different, this makes the rainbow equally distributed throughout\nvoid rainbowCycle(uint8_t wait) {\n  uint16_t i, j;\n  for (j = 0; j < 256 * 5; j++) { // 5 cycles of all colors on wheel\n    for (i = 0; i < strip.numPixels(); i++) {\n      strip.setPixelColor(i, Wheel(((i * 256 / strip.numPixels()) + j) & 255));\n    }\n    strip.show();\n    delay(wait);\n  }\n}\n\nvoid rainbow(uint8_t wait) {\n  uint16_t i, j;\n  for (j = 0; j < 256; j++) {\n    for (i = 0; i < strip.numPixels(); i++) {\n      strip.setPixelColor(i, Wheel((i + j) & 255));\n    }\n    strip.show();\n    delay(wait);\n  }\n}\n\nvoid map_hsv_to_rgb(int *r, int *g, int *b) {\n  hsv in;\n  rgb out;\n  in.h = 360. * (*r) / 256.;\n  in.s =   1. * (*g) / 255.;\n  in.v =   1. * (*b) / 255.;\n  out = hsv2rgb(in);\n  (*r) = out.r * 255;\n  (*g) = out.g * 255;\n  (*b) = out.b * 255;\n}\n\n"
  },
  {
    "path": "esp8266_artnet_neopixel/neopixel_mode.h",
    "content": "#ifndef _MODE_H_\n#define _MODE_H_\n\n#include <Arduino.h>\n#include <ESP8266WiFi.h>\n#include <WiFiUdp.h>\n#include <ArtnetWifi.h>\n#include <Adafruit_NeoPixel.h>\n\n#define ROUND(x)   (int(x + 0.5))\n#define ABS(x)     (x * (x < 0 ? -1 : 1))\n#define MIN(x, y)  (x < y ? x : y)\n#define MAX(x, y)  (x > y ? x : y)\n#define WRAP360(x) (x > 0 ? (x - int((x)/360)*360) : (x - int((x)/360 - 1)*360))  // between    0 and 360\n#define WRAP180(x) (WRAP360(x) < 180 ? WRAP360(x) : WRAP360(x) - 360)             // between -180 and 180\n#define BALANCE(l, x1, x2)  ((x1) * (1. - l) + (x2) * l)\n\n#ifdef __cplusplus\nextern \"C\" {\n#endif\n\nextern long tic_frame;\n\nvoid map_hsv_to_rgb(int *, int *, int *);\n\nvoid singleRed();\nvoid singleGreen();\nvoid singleBlue();\nvoid singleYellow();\nvoid singleMagenta();\nvoid singleCyan();\nvoid singleWhite();\nvoid fullBlack();\n\nvoid mode0(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode1(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode2(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode3(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode4(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode5(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode6(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode7(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode8(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode9(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode10(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode11(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode12(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode13(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode14(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode15(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode16(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode17(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode18(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode19(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode20(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode21(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode22(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode23(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode24(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode25(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode26(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode27(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode28(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode29(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode30(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode31(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode32(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode33(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode34(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode35(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode36(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode37(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode38(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode39(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode40(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode41(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode42(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode43(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode44(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode45(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode46(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode47(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode48(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode49(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode50(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode51(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode52(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode53(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode54(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode55(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode56(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode57(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode58(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode59(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode60(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode61(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode62(uint16_t, uint16_t, uint8_t, uint8_t *);\nvoid mode63(uint16_t, uint16_t, uint8_t, uint8_t *);\n\n#ifdef __cplusplus\n}\n#endif\n\n#endif\n\n"
  },
  {
    "path": "esp8266_artnet_neopixel/webinterface.cpp",
    "content": "#include \"webinterface.h\"\n\nConfig config;\nextern ESP8266WebServer server;\nextern int packetCounter;\n\n/***************************************************************************/\n\nstatic String getContentType(const String& path) {\n  if (path.endsWith(\".html\"))       return \"text/html\";\n  else if (path.endsWith(\".htm\"))   return \"text/html\";\n  else if (path.endsWith(\".css\"))   return \"text/css\";\n  else if (path.endsWith(\".txt\"))   return \"text/plain\";\n  else if (path.endsWith(\".js\"))    return \"application/javascript\";\n  else if (path.endsWith(\".png\"))   return \"image/png\";\n  else if (path.endsWith(\".gif\"))   return \"image/gif\";\n  else if (path.endsWith(\".jpg\"))   return \"image/jpeg\";\n  else if (path.endsWith(\".jpeg\"))  return \"image/jpeg\";\n  else if (path.endsWith(\".ico\"))   return \"image/x-icon\";\n  else if (path.endsWith(\".svg\"))   return \"image/svg+xml\";\n  else if (path.endsWith(\".xml\"))   return \"text/xml\";\n  else if (path.endsWith(\".pdf\"))   return \"application/pdf\";\n  else if (path.endsWith(\".zip\"))   return \"application/zip\";\n  else if (path.endsWith(\".gz\"))    return \"application/x-gzip\";\n  else if (path.endsWith(\".json\"))  return \"application/json\";\n  return \"application/octet-stream\";\n}\n\n/***************************************************************************/\n\nbool defaultConfig() {\n  Serial.println(\"defaultConfig\");\n  \n  config.universe = 1;\n  config.offset = 0;\n  config.pixels = 12;\n  config.leds = 4;\n  config.white = 0;\n  config.brightness = 255;\n  config.hsv = 0;\n  config.mode = 1;\n  config.reverse = 0;\n  config.speed = 8;\n  config.split = 1;\n  return true;\n}\n\nbool loadConfig() {\n  Serial.println(\"loadConfig\");\n\n  File configFile = SPIFFS.open(\"/config.json\", \"r\");\n  if (!configFile) {\n    Serial.println(\"Failed to open config file\");\n    return false;\n  }\n\n  size_t size = configFile.size();\n  if (size > 1024) {\n    Serial.println(\"Config file size is too large\");\n    return false;\n  }\n\n  std::unique_ptr<char[]> buf(new char[size]);\n  configFile.readBytes(buf.get(), size);\n  configFile.close();\n\n  StaticJsonBuffer<300> jsonBuffer;\n  JsonObject& root = jsonBuffer.parseObject(buf.get());\n\n  if (!root.success()) {\n    Serial.println(\"Failed to parse config file\");\n    return false;\n  }\n\n  N_JSON_TO_CONFIG(universe, \"universe\");\n  N_JSON_TO_CONFIG(offset, \"offset\");\n  N_JSON_TO_CONFIG(pixels, \"pixels\");\n  N_JSON_TO_CONFIG(leds, \"leds\");\n  N_JSON_TO_CONFIG(white, \"white\");\n  N_JSON_TO_CONFIG(brightness, \"brightness\");\n  N_JSON_TO_CONFIG(hsv, \"hsv\");\n  N_JSON_TO_CONFIG(mode, \"mode\");\n  N_JSON_TO_CONFIG(reverse, \"reverse\");\n  N_JSON_TO_CONFIG(speed, \"speed\");\n  N_JSON_TO_CONFIG(split, \"split\");\n\n  return true;\n}\n\nbool saveConfig() {\n  Serial.println(\"saveConfig\");\n  StaticJsonBuffer<300> jsonBuffer;\n  JsonObject& root = jsonBuffer.createObject();\n\n  N_CONFIG_TO_JSON(universe, \"universe\");\n  N_CONFIG_TO_JSON(offset, \"offset\");\n  N_CONFIG_TO_JSON(pixels, \"pixels\");\n  N_CONFIG_TO_JSON(leds, \"leds\");\n  N_CONFIG_TO_JSON(white, \"white\");\n  N_CONFIG_TO_JSON(brightness, \"brightness\");\n  N_CONFIG_TO_JSON(hsv, \"hsv\");\n  N_CONFIG_TO_JSON(mode, \"mode\");\n  N_CONFIG_TO_JSON(reverse, \"reverse\");\n  N_CONFIG_TO_JSON(speed, \"speed\");\n  N_CONFIG_TO_JSON(split, \"split\");\n\n  File configFile = SPIFFS.open(\"/config.json\", \"w\");\n  if (!configFile) {\n    Serial.println(\"Failed to open config file for writing\");\n    return false;\n  }\n  else {\n    Serial.println(\"Writing to config file\");\n    root.printTo(configFile);\n    configFile.close();\n    return true;\n  }\n}\n\nvoid printRequest() {\n  String message = \"HTTP Request\\n\\n\";\n  message += \"URI: \";\n  message += server.uri();\n  message += \"\\nMethod: \";\n  message += (server.method() == HTTP_GET) ? \"GET\" : \"POST\";\n  message += \"\\nHeaders: \";\n  message += server.headers();\n  message += \"\\n\";\n  for (uint8_t i = 0; i < server.headers(); i++ ) {\n    message += \" \" + server.headerName(i) + \": \" + server.header(i) + \"\\n\";\n  }\n  message += \"\\nArguments: \";\n  message += server.args();\n  message += \"\\n\";\n  for (uint8_t i = 0; i < server.args(); i++) {\n    message += \" \" + server.argName(i) + \": \" + server.arg(i) + \"\\n\";\n  }\n  Serial.println(message);\n}\n\nvoid handleUpdate1() {\n  server.sendHeader(\"Connection\", \"close\");\n  server.sendHeader(\"Access-Control-Allow-Origin\", \"*\");\n  server.send(200, \"text/plain\", (Update.hasError()) ? \"FAIL\" : \"OK\");\n  ESP.restart();\n}\n\nvoid handleUpdate2() {\n  HTTPUpload& upload = server.upload();\n  if (upload.status == UPLOAD_FILE_START) {\n    Serial.setDebugOutput(true);\n    WiFiUDP::stopAll();\n    Serial.printf(\"Update: %s\\n\", upload.filename.c_str());\n    uint32_t maxSketchSpace = (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000;\n    if (!Update.begin(maxSketchSpace)) { //start with max available size\n      Update.printError(Serial);\n    }\n  } else if (upload.status == UPLOAD_FILE_WRITE) {\n    if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {\n      Update.printError(Serial);\n    }\n  } else if (upload.status == UPLOAD_FILE_END) {\n    if (Update.end(true)) { //true to set the size to the current progress\n      Serial.printf(\"Update Success: %u\\nRebooting...\\n\", upload.totalSize);\n    } else {\n      Update.printError(Serial);\n    }\n    Serial.setDebugOutput(false);\n  }\n  yield();\n}\n\nvoid handleDirList() {\n  Serial.println(\"handleDirList\");\n  String str = \"\";\n  Dir dir = SPIFFS.openDir(\"/\");\n  while (dir.next()) {\n    str += dir.fileName();\n    str += \" \";\n    str += dir.fileSize();\n    str += \" bytes\\r\\n\";\n  }\n  server.send(200, \"text/plain\", str);\n}\n\nvoid handleNotFound() {\n  Serial.print(\"handleNotFound: \");\n  Serial.println(server.uri());\n  if (SPIFFS.exists(server.uri())) {\n    handleStaticFile(server.uri());\n  }\n  else {\n    String message = \"File Not Found\\n\\n\";\n    message += \"URI: \";\n    message += server.uri();\n    message += \"\\nMethod: \";\n    message += (server.method() == HTTP_GET) ? \"GET\" : \"POST\";\n    message += \"\\nArguments: \";\n    message += server.args();\n    message += \"\\n\";\n    for (uint8_t i = 0; i < server.args(); i++) {\n      message += \" \" + server.argName(i) + \": \" + server.arg(i) + \"\\n\";\n    }\n    server.setContentLength(message.length());\n    server.send(404, \"text/plain\", message);\n  }\n}\n\nvoid handleRedirect(const char * filename) {\n  handleRedirect((String)filename);\n}\n\nvoid handleRedirect(String filename) {\n  Serial.println(\"handleRedirect: \" + filename);\n  server.sendHeader(\"Location\", filename, true);\n  server.setContentLength(0);\n  server.send(302, \"text/plain\", \"\");\n}\n\nbool handleStaticFile(const char * path) {\n  return handleStaticFile((String)path);\n}\n\nbool handleStaticFile(String path) {\n  Serial.println(\"handleStaticFile: \" + path);\n  String contentType = getContentType(path);            // Get the MIME type\n  if (SPIFFS.exists(path)) {                            // If the file exists\n    File file = SPIFFS.open(path, \"r\");                 // Open it\n    server.setContentLength(file.size());\n    server.streamFile(file, contentType);               // And send it to the client\n    file.close();                                       // Then close the file again\n    return true;\n  }\n  Serial.println(\"\\tFile Not Found\");\n  return false;                                         // If the file doesn't exist, return false\n}\n\nvoid handleJSON() {\n  // this gets called in response to either a PUT or a POST\n  Serial.println(\"handleJSON\");\n  printRequest();\n\n  if (server.hasArg(\"universe\") || server.hasArg(\"offset\") || server.hasArg(\"pixels\") || server.hasArg(\"leds\") || server.hasArg(\"white\") || server.hasArg(\"brightness\") || server.hasArg(\"hsv\") || server.hasArg(\"mode\") || server.hasArg(\"reverse\") || server.hasArg(\"speed\") || server.hasArg(\"split\")) {\n    // the body is key1=val1&key2=val2&key3=val3 and the ESP8266Webserver has already parsed it\n    N_KEYVAL_TO_CONFIG(universe, \"universe\");\n    N_KEYVAL_TO_CONFIG(offset, \"offset\");\n    N_KEYVAL_TO_CONFIG(pixels, \"pixels\");\n    N_KEYVAL_TO_CONFIG(leds, \"leds\");\n    N_KEYVAL_TO_CONFIG(white, \"white\");\n    N_KEYVAL_TO_CONFIG(brightness, \"brightness\");\n    N_KEYVAL_TO_CONFIG(hsv, \"hsv\");\n    N_KEYVAL_TO_CONFIG(mode, \"mode\");\n    N_KEYVAL_TO_CONFIG(reverse, \"reverse\");\n    N_KEYVAL_TO_CONFIG(speed, \"speed\");\n    N_KEYVAL_TO_CONFIG(split, \"split\");\n\n    handleStaticFile(\"/reload_success.html\");\n  }\n  else if (server.hasArg(\"plain\")) {\n    // parse the body as JSON object\n    StaticJsonBuffer<300> jsonBuffer;\n    JsonObject& root = jsonBuffer.parseObject(server.arg(\"plain\"));\n    if (!root.success()) {\n      handleStaticFile(\"/reload_failure.html\");\n      return;\n    }\n    N_JSON_TO_CONFIG(universe, \"universe\");\n    N_JSON_TO_CONFIG(offset, \"offset\");\n    N_JSON_TO_CONFIG(pixels, \"pixels\");\n    N_JSON_TO_CONFIG(leds, \"leds\");\n    N_JSON_TO_CONFIG(white, \"white\");\n    N_JSON_TO_CONFIG(brightness, \"brightness\");\n    N_JSON_TO_CONFIG(hsv, \"hsv\");\n    N_JSON_TO_CONFIG(mode, \"mode\");\n    N_JSON_TO_CONFIG(reverse, \"reverse\");\n    N_JSON_TO_CONFIG(speed, \"speed\");\n    N_JSON_TO_CONFIG(split, \"split\");\n    \n    handleStaticFile(\"/reload_success.html\");\n  }\n  else {\n    handleStaticFile(\"/reload_failure.html\");\n    return; // do not save the configuration\n  }\n  \n  saveConfig();\n}\n"
  },
  {
    "path": "esp8266_artnet_neopixel/webinterface.h",
    "content": "#ifndef _WEBINTERFACE_H_\n#define _WEBINTERFACE_H_\n\n#include <Arduino.h>\n#include <ArduinoJson.h>\n#include <string.h>\n#include <ESP8266WebServer.h>\n#include <WiFiUdp.h>\n#include <FS.h>\n\n#ifndef ARDUINOJSON_VERSION\n#error ArduinoJson version 5 not found, please include ArduinoJson.h in your .ino file\n#endif\n\n#if ARDUINOJSON_VERSION_MAJOR != 5\n#error ArduinoJson version 5 is required\n#endif\n\n/* these are for numbers */\n#define N_JSON_TO_CONFIG(x, y)   { if (root.containsKey(y)) { config.x = root[y]; } }\n#define N_CONFIG_TO_JSON(x, y)   { root.set(y, config.x); }\n#define N_KEYVAL_TO_CONFIG(x, y) { if (server.hasArg(y))    { String str = server.arg(y); config.x = str.toFloat(); } }\n\n/* these are for strings */\n#define S_JSON_TO_CONFIG(x, y)   { if (root.containsKey(y)) { strcpy(config.x, root[y]); } }\n#define S_CONFIG_TO_JSON(x, y)   { root.set(y, config.x); }\n#define S_KEYVAL_TO_CONFIG(x, y) { if (server.hasArg(y))    { String str = server.arg(y); strcpy(config.x, str.c_str()); } }\n\nstruct Config {\n  int universe;\n  int offset;\n  int pixels;\n  int leds;\n  int white;\n  int brightness;\n  int hsv;\n  int mode;\n  int reverse;\n  int speed;\n  int split;\n};\n\nextern Config config;\n\nbool defaultConfig(void);\nbool loadConfig(void);\nbool saveConfig(void);\n\nvoid handleUpdate1(void);\nvoid handleUpdate2(void);\nvoid handleDirList(void);\nvoid handleNotFound(void);\nvoid handleRedirect(String);\nvoid handleRedirect(const char *);\nbool handleStaticFile(String);\nbool handleStaticFile(const char *);\nvoid handleJSON();\n\n#endif // _WEBINTERFACE_H_\n"
  },
  {
    "path": "esp8266_config_spiffs/data/config.json",
    "content": "{\n\"var1\":10,\n\"var2\":20,\n\"var3\":30\n}\n"
  },
  {
    "path": "esp8266_config_spiffs/esp8266_config_spiffs.ino",
    "content": "#include <Arduino.h>\n#include <ArduinoJson.h>\n#include <FS.h>\n\n#if defined(ESP32)\n#include <SPIFFS.h>\n#endif\n\n#ifndef ARDUINOJSON_VERSION\n#error ArduinoJson version 5 not found, please include ArduinoJson.h in your .ino file\n#endif\n\n#if ARDUINOJSON_VERSION_MAJOR != 5\n#error ArduinoJson version 5 is required\n#endif\n\nint var1, var2, var3;\n\nbool loadConfig() {\n  File configFile = SPIFFS.open(\"/config.json\", \"r\");\n  if (!configFile) {\n    Serial.println(\"Failed to open config file for reading\");\n    return false;\n  }\n\n  size_t size = configFile.size();\n  if (size > 1024) {\n    Serial.println(\"Config file size is too large\");\n    return false;\n  }\n\n  std::unique_ptr<char[]> buf(new char[size]);\n  configFile.readBytes(buf.get(), size);\n\n  StaticJsonBuffer<200> jsonBuffer;\n  JsonObject& root = jsonBuffer.parseObject(buf.get());\n\n  if (!root.success()) {\n    Serial.println(\"Failed to parse config file\");\n    return false;\n  }\n\n  if (root.containsKey(\"var1\"))\n    var1 = root[\"var1\"];\n  if (root.containsKey(\"var2\"))\n    var2 = root[\"var2\"];\n  if (root.containsKey(\"var3\"))\n    var3 = root[\"var3\"];\n  return true;\n}\n\nbool saveConfig() {\n  StaticJsonBuffer<200> jsonBuffer;\n  JsonObject& root = jsonBuffer.createObject();\n  root[\"var1\"] = var1;\n  root[\"var2\"] = var2;\n  root[\"var3\"] = var3;\n\n  File configFile = SPIFFS.open(\"/config.json\", \"w\");\n  if (!configFile) {\n    Serial.println(\"Failed to open config file for writing\");\n    return false;\n  }\n\n  root.printTo(configFile);\n  return true;\n}\n\nbool printConfig() {\n  Serial.print(\"var1 = \");\n  Serial.println(var1);\n  Serial.print(\"var2 = \");\n  Serial.println(var2);\n  Serial.print(\"var3 = \");\n  Serial.println(var3);\n  return true;\n}\n\nvoid setup() {\n  Serial.begin(115200);\n  while (!Serial) {\n    ;\n  }\n  delay(1000);\n  Serial.println(\"\");\n  Serial.println(\"setup start\");\n\n  if (SPIFFS.begin()) {\n    Serial.println(\"SPIFFS ok\");\n  }\n  else {\n    Serial.println(\"SPIFFS fail\");\n  }\n  \n  if (loadConfig()) {\n    Serial.println(\"loadConfig ok\");\n  }\n  else {\n    Serial.println(\"loadConfig fail\");\n  }\n  \n  if (printConfig()) {\n      Serial.println(\"printConfig ok\");\n  }\n  else {\n    Serial.println(\"printConfig fail\");\n  }\n\n  if (saveConfig()) {\n    Serial.println(\"saveConfig ok\");\n  }\n  else {\n    Serial.println(\"loadConfig fail\");\n  }\n\n  SPIFFS.end();\n  Serial.println(\"setup end\");\n}\n\nvoid loop() {\n  delay(1000);\n  Serial.print(\".\");\n}\n"
  },
  {
    "path": "esp8266_config_webinterface/README.md",
    "content": "# Overview\n\nThis is a demonstration and test sketch for configuring persistent options via the web interface. This strategy is used in a number of my functional esp8266 sketches. It consists of a `settings.json` file in the SPIFFS filesystem and a `settings.html` file with some javascript.\n\nThe settings can be changed in the webbrowser at http://192.168.1.xxx/settings and clicking \"Send\". This results in `ESP8266Webserver` parsing the arguments and placing the variables in the header, but also places the query string in the body.\n\nThe following results in `ESP8266Webserver` parsing the arguments and placing the variables in the header.\n\n    curl -X POST 'http://192.168.1.xxx/json?var1=11&var2=22&var3=33'\n    curl -X PUT  'http://192.168.1.xxx/json?var1=11&var2=22&var3=33'\n\nThe following is not parsed, but only places the JSON data in the body.\n\n    curl -X POST http://192.168.1.xxx/json -d '{\"var1\":10,\"var2\":20,\"var3\":30}'\n    curl -X PUT  http://192.168.1.xxx/json -d '{\"var1\":10,\"var2\":20,\"var3\":30}'\n\n## SPIFFS for static files\n\nYou should not only write the firmware to the ESP8266 module, but also the static content for the web interface. The html, css and javascript files located in the data directory should be written to the SPIFS filesystem on the ESP8266. See for example http://esp8266.github.io/Arduino/versions/2.0.0/doc/filesystem.html and https://www.instructables.com/id/Using-ESP8266-SPIFFS for instructions.\n\nYou will get a \"file not found\" error if the firmware cannot access the data files.\n\n## Arduino ESP8266 filesystem uploader\n\nThis Arduino sketch includes a `data` directory with a number of files that should be uploaded to the ESP8266 using the [SPIFFS filesystem uploader](https://github.com/esp8266/arduino-esp8266fs-plugin) tool. At the moment (Feb 2024) the Arduino 2.x IDE does *not* support the SPIFFS filesystem uploader plugin. You have to use the Arduino 1.8.x IDE (recommended), or the command line utilities for uploading the data.\n"
  },
  {
    "path": "esp8266_config_webinterface/data/config.json",
    "content": "{\n\"var1\":10,\n\"var2\":20,\n\"var3\":30,\n\"var4\":40\n}\n"
  },
  {
    "path": "esp8266_config_webinterface/data/hello.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Hello</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n<p>Hello kitty!</p>\n<img src=\"cat.jpg\">\n</body>\n</html>\n"
  },
  {
    "path": "esp8266_config_webinterface/data/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Index</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n<a href=\"/hello.html\">Hello</a></br>\n<a href=\"/monitor.html\">Monitor</a></br>\n<a href=\"/settings.html\">Change settings</a></br>\n<a href=\"/reconnect\">Reconnect WiFi</a></br>\n<a href=\"/defaults\">Default settings</a></br>\n<a href=\"/update.html\">Update firmware</a></br>\n<a href=\"/restart\">Restart hardware</a></br>\n</body>\n</html>\n"
  },
  {
    "path": "esp8266_config_webinterface/data/monitor.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Monitor</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n\n<h1>Monitor</h1>\n\nFirmware version:\n<div id=\"version\" name=\"version\">?</div>\n\nUptime:\n<div id=\"uptime\" name\"uptime\">?</div>\n\n<script>\n  async function updateContent() {\n    const response = await fetch(\"json\");\n    const data = await response.json();\n    console.log(data);\n    document.getElementById(\"version\").innerHTML = data[\"version\"];\n    document.getElementById(\"uptime\").innerHTML = data[\"uptime\"];\n  }\n  updateContent();\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "esp8266_config_webinterface/data/reload_failure.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Failure</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n<p>Failure</p>\n\n<script type=\"text/javascript\">\n  setTimeout(function(){location=\"/index.html\"},1000);\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "esp8266_config_webinterface/data/reload_success.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Success</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n<p>Success</p>\n\n<script type=\"text/javascript\">\n  setTimeout(function(){location=\"/index.html\"},1000);\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "esp8266_config_webinterface/data/settings.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Settings</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n<h1>Settings</h1>\n\n<form id=\"settings-form\" method=\"post\" action=\"json\">\n    <div class=\"field\">\n        <label for=\"var1\">var1:</label>\n        <input type=\"text\" id=\"var1\" name=\"var1\" value=\"?\" required>\n    </div>\n\n    <div class=\"field\">\n        <label for=\"var2\">var2:</label>\n        <input type=\"text\" id=\"var2\" name=\"var2\" value=\"?\" required>\n    </div>\n\n    <div class=\"field\">\n        <label for=\"var2\">var3:</label>\n        <input type=\"text\" id=\"var3\" name=\"var3\" value=\"?\" required>\n    </div>\n\n    <div class=\"field\">\n        <button type=\"submit\">Save</button>\n    </div>\n</form>\n\n<script>\n  async function updateContent() {\n    const response = await fetch(\"json\");\n    const data = await response.json();\n    console.log(data);\n    document.getElementById(\"var1\").value = data[\"var1\"];\n    document.getElementById(\"var2\").value = data[\"var2\"];\n    document.getElementById(\"var3\").value = data[\"var3\"];\n  }\n  updateContent();\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "esp8266_config_webinterface/data/style.css",
    "content": ".c{\n    text-align: center;\n}\ndiv,input{\n    padding:5px;font-size:1em;\n}\ninput{\n    width:95%;\n}\nbody{\n    text-align: center;font-family:verdana;\n}\nbutton{\n    border:0;border-radius:0.3rem;background-color:#1fa3ec;color:#fff;line-height:2.4rem;font-size:1.2rem;width:100%;\n}\n.q{\n    float: right;width: 64px;text-align: right;\n}\n.l{\n    background: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAALVBMVEX///8EBwfBwsLw8PAzNjaCg4NTVVUjJiZDRUUUFxdiZGSho6OSk5Pg4eFydHTCjaf3AAAAZElEQVQ4je2NSw7AIAhEBamKn97/uMXEGBvozkWb9C2Zx4xzWykBhFAeYp9gkLyZE0zIMno9n4g19hmdY39scwqVkOXaxph0ZCXQcqxSpgQpONa59wkRDOL93eAXvimwlbPbwwVAegLS1HGfZAAAAABJRU5ErkJggg==\") no-repeat left center;background-size: 1em;\n}\n"
  },
  {
    "path": "esp8266_config_webinterface/data/update.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Update</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n<h1>Update the ESP8266 firmware</h1>\n<form method='POST' action='/update' enctype='multipart/form-data'>\n\n<div class=\"field\">\n<input type='file' name='update'>\n</div>\n\n<div class=\"field\">\n<button type=\"submit\">Update</button>\n</div>\n\n</form>\n</body>\n</html>\n"
  },
  {
    "path": "esp8266_config_webinterface/esp8266_config_webinterface.ino",
    "content": "\n#include <Arduino.h>\n#include <ArduinoJson.h>          // https://arduinojson.org\n#include <ESP8266WiFi.h>          // https://github.com/esp8266/Arduino\n#include <WiFiManager.h>          // https://github.com/tzapu/WiFiManager\n#include <WiFiClient.h>\n#include <ESP8266WebServer.h>\n#include <ESP8266mDNS.h>\n#include <FS.h>\n\n#ifndef ARDUINOJSON_VERSION\n#error ArduinoJson version 5 not found, please include ArduinoJson.h in your .ino file\n#endif\n\n#if ARDUINOJSON_VERSION_MAJOR != 5\n#error ArduinoJson version 5 is required\n#endif\n\nESP8266WebServer server(80);\nconst char* host = \"esp8266\";\nconst char* version = __DATE__ \" / \" __TIME__;\nint var1, var2, var3;\n\nstatic String getContentType(const String& path) {\n  if (path.endsWith(\".html\"))       return \"text/html\";\n  else if (path.endsWith(\".htm\"))   return \"text/html\";\n  else if (path.endsWith(\".css\"))   return \"text/css\";\n  else if (path.endsWith(\".txt\"))   return \"text/plain\";\n  else if (path.endsWith(\".js\"))    return \"application/javascript\";\n  else if (path.endsWith(\".png\"))   return \"image/png\";\n  else if (path.endsWith(\".gif\"))   return \"image/gif\";\n  else if (path.endsWith(\".jpg\"))   return \"image/jpeg\";\n  else if (path.endsWith(\".jpeg\"))  return \"image/jpeg\";\n  else if (path.endsWith(\".ico\"))   return \"image/x-icon\";\n  else if (path.endsWith(\".svg\"))   return \"image/svg+xml\";\n  else if (path.endsWith(\".xml\"))   return \"text/xml\";\n  else if (path.endsWith(\".pdf\"))   return \"application/pdf\";\n  else if (path.endsWith(\".zip\"))   return \"application/zip\";\n  else if (path.endsWith(\".gz\"))    return \"application/x-gzip\";\n  else if (path.endsWith(\".json\"))  return \"application/json\";\n  return \"application/octet-stream\";\n}\n\nbool loadConfig() {\n  Serial.println(\"loadConfig\");\n  File configFile = SPIFFS.open(\"/config.json\", \"r\");\n  if (!configFile) {\n    Serial.println(\"Failed to open config file\");\n    return false;\n  }\n\n  size_t size = configFile.size();\n  if (size > 1024) {\n    Serial.println(\"Config file size is too large\");\n    return false;\n  }\n\n  std::unique_ptr<char[]> buf(new char[size]);\n  configFile.readBytes(buf.get(), size);\n\n  StaticJsonBuffer<200> jsonBuffer;\n  JsonObject& root = jsonBuffer.parseObject(buf.get());\n\n  if (!root.success()) {\n    Serial.println(\"Failed to parse config file\");\n    return false;\n  }\n\n  if (root.containsKey(\"var1\"))\n    var1 = root[\"var1\"];\n  if (root.containsKey(\"var2\"))\n    var2 = root[\"var2\"];\n  if (root.containsKey(\"var3\"))\n    var3 = root[\"var3\"];\n\n  printConfig();\n  return true;\n}\n\nbool saveConfig() {\n  Serial.println(\"saveConfig\");\n  printConfig();\n  StaticJsonBuffer<200> jsonBuffer;\n  JsonObject& root = jsonBuffer.createObject();\n  root[\"var1\"] = var1;\n  root[\"var2\"] = var2;\n  root[\"var3\"] = var3;\n\n  File configFile = SPIFFS.open(\"/config.json\", \"w\");\n  if (!configFile) {\n    Serial.println(\"Failed to open config file for writing\");\n    return false;\n  }\n  else {\n    root.printTo(configFile);\n    return true;\n  }\n}\n\nvoid printConfig() {\n  Serial.print(\"var1 = \");\n  Serial.println(var1);\n  Serial.print(\"var2 = \");\n  Serial.println(var2);\n  Serial.print(\"var3 = \");\n  Serial.println(var3);\n}\n\nvoid printRequest() {\n  String message = \"HTTP Request\\n\\n\";\n  message += \"URI: \";\n  message += server.uri();\n  message += \"\\nMethod: \";\n  message += (server.method() == HTTP_GET) ? \"GET\" : \"POST\";\n  message += \"\\nHeaders: \";\n  message += server.headers();\n  message += \"\\n\";\n  for (uint8_t i = 0; i < server.headers(); i++ ) {\n    message += \" \" + server.headerName(i) + \": \" + server.header(i) + \"\\n\";\n  }\n  message += \"\\nArguments: \";\n  message += server.args();\n  message += \"\\n\";\n  for (uint8_t i = 0; i < server.args(); i++) {\n    message += \" \" + server.argName(i) + \": \" + server.arg(i) + \"\\n\";\n  }\n  Serial.println(message);\n}\n\nvoid handleUpdate1() {\n  Serial.println(\"handleUpdate1\");\n  server.sendHeader(\"Connection\", \"close\");\n  server.sendHeader(\"Access-Control-Allow-Origin\", \"*\");\n  server.send(200, \"text/plain\", (Update.hasError()) ? \"FAIL\" : \"OK\");\n  ESP.restart();\n}\n\nvoid handleUpdate2() {\n  Serial.println(\"handleUpdate2\");\n  HTTPUpload& upload = server.upload();\n  if (upload.status == UPLOAD_FILE_START) {\n    Serial.setDebugOutput(true);\n    WiFiUDP::stopAll();\n    Serial.printf(\"Update: %s\\n\", upload.filename.c_str());\n    uint32_t maxSketchSpace = (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000;\n    if (!Update.begin(maxSketchSpace)) { //start with max available size\n      Update.printError(Serial);\n    }\n  } else if (upload.status == UPLOAD_FILE_WRITE) {\n    if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {\n      Update.printError(Serial);\n    }\n  } else if (upload.status == UPLOAD_FILE_END) {\n    if (Update.end(true)) { //true to set the size to the current progress\n      Serial.printf(\"Update Success: %u\\nRebooting...\\n\", upload.totalSize);\n    } else {\n      Update.printError(Serial);\n    }\n    Serial.setDebugOutput(false);\n  }\n  yield();\n}\n\nvoid handleDirList() {\n  Serial.println(\"handleDirList\");\n  String str = \"\";\n  Dir dir = SPIFFS.openDir(\"/\");\n  while (dir.next()) {\n    str += dir.fileName();\n    str += \" \";\n    str += dir.fileSize();\n    str += \" bytes\\r\\n\";\n  }\n  server.send(200, \"text/plain\", str);\n}\n\nvoid handleNotFound() {\n  Serial.print(\"handleNotFound: \");\n  Serial.println(server.uri());\n  if (SPIFFS.exists(server.uri())) {\n    handleStaticFile(server.uri());\n  }\n  else {\n    String message = \"File Not Found\\n\\n\";\n    message += \"URI: \";\n    message += server.uri();\n    message += \"\\nMethod: \";\n    message += (server.method() == HTTP_GET) ? \"GET\" : \"POST\";\n    message += \"\\nArguments: \";\n    message += server.args();\n    message += \"\\n\";\n    for (uint8_t i = 0; i < server.args(); i++) {\n      message += \" \" + server.argName(i) + \": \" + server.arg(i) + \"\\n\";\n    }\n    server.setContentLength(message.length());\n    server.send(404, \"text/plain\", message);\n  }\n}\n\nvoid handleRedirect(const char * filename) {\n  handleRedirect((String)filename);\n}\n\nvoid handleRedirect(String filename) {\n  Serial.println(\"handleRedirect: \" + filename);\n  server.sendHeader(\"Location\", filename, true);\n  server.setContentLength(0);\n  server.send(302, \"text/plain\", \"\");\n}\n\nbool handleStaticFile(const char * path) {\n  return handleStaticFile((String)path);\n}\n\nbool handleStaticFile(String path) {\n  Serial.println(\"handleStaticFile: \" + path);\n  String contentType = getContentType(path);            // Get the MIME type\n  if (SPIFFS.exists(path)) {                            // If the file exists\n    File file = SPIFFS.open(path, \"r\");                 // Open it\n    server.setContentLength(file.size());\n    server.streamFile(file, contentType);               // And send it to the client\n    file.close();                                       // Then close the file again\n    return true;\n  }\n  else {\n    Serial.println(\"\\tFile Not Found\");\n    return false;                                         // If the file doesn't exist, return false\n  }\n}\n\nvoid handleJSON() {\n  // this gets called in response to either a PUT or a POST\n  Serial.println(\"handleJSON\");\n  printRequest();\n\n  if (server.hasArg(\"var1\") || server.hasArg(\"var2\") || server.hasArg(\"var3\")) {\n    // the body is key1=val1&key2=val2&key3=val3 and the ESP8266Webserver has already parsed it\n    String str;\n    if (server.hasArg(\"var1\")) {\n      str = server.arg(\"var1\");\n      var1 = str.toInt();\n    }\n    if (server.hasArg(\"var2\")) {\n      str = server.arg(\"var2\");\n      var2 = str.toInt();\n    }\n    if (server.hasArg(\"var3\")) {\n      str = server.arg(\"var3\");\n      var3 = str.toInt();\n    }\n    handleStaticFile(\"/reload_success.html\");\n  }\n  else if (server.hasArg(\"plain\")) {\n    // parse the body as JSON object\n    String body = server.arg(\"plain\");\n    StaticJsonBuffer<200> jsonBuffer;\n    JsonObject& root = jsonBuffer.parseObject(body);\n    if (!root.success()) {\n      handleStaticFile(\"/reload_failure.html\");\n      return;\n    }\n    if (root.containsKey(\"var1\"))\n      var1 = root[\"var1\"];\n    if (root.containsKey(\"var2\"))\n      var2 = root[\"var2\"];\n    if (root.containsKey(\"var3\"))\n      var3 = root[\"var3\"];\n    handleStaticFile(\"/reload_success.html\");\n  }\n  else {\n      handleStaticFile(\"/reload_failure.html\");\n      return;\n  }\n  saveConfig();\n}\n\nvoid setup() {\n  Serial.begin(115200);\n  while (!Serial) {\n    ;\n  }\n  Serial.println(\"\");\n  Serial.println(\"setup\");\n\n  SPIFFS.begin();\n  loadConfig();\n\n  WiFiManager wifiManager;\n  wifiManager.setAPStaticIPConfig(IPAddress(192, 168, 1, 1), IPAddress(192, 168, 1, 1), IPAddress(255, 255, 255, 0));\n  wifiManager.autoConnect(host);\n  Serial.println(\"connected\");\n\n  // this serves all URIs that can be resolved to a file on the SPIFFS filesystem\n  server.onNotFound(handleNotFound);\n\n  server.on(\"/\", HTTP_GET, []() {\n    handleRedirect(\"/index.html\");\n  });\n\n  server.on(\"/reconnect\", HTTP_GET, []() {\n    Serial.println(\"handleWifi\");\n    handleStaticFile(\"/reload_success.html\");\n    server.close();\n    server.stop();\n    delay(5000);\n    WiFiManager wifiManager;\n    wifiManager.setAPStaticIPConfig(IPAddress(192, 168, 1, 1), IPAddress(192, 168, 1, 1), IPAddress(255, 255, 255, 0));\n    wifiManager.startConfigPortal(host);\n    Serial.println(\"connected\");\n    server.begin();\n  });\n\n  server.on(\"/restart\", HTTP_GET, []() {\n    Serial.println(\"handleRestart\");\n    handleStaticFile(\"/reload_success.html\");\n    server.close();\n    server.stop();\n    SPIFFS.end();\n    delay(5000);\n    ESP.restart();\n  });\n\n  server.on(\"/dir\", HTTP_GET, handleDirList);\n\n  server.on(\"/json\", HTTP_PUT, handleJSON);\n\n  server.on(\"/json\", HTTP_POST, handleJSON);\n\n  server.on(\"/json\", HTTP_GET, [] {\n    StaticJsonBuffer<200> jsonBuffer;\n    JsonObject& root = jsonBuffer.createObject();\n    root[\"var1\"]    = var1;\n    root[\"var2\"]    = var2;\n    root[\"var3\"]    = var3;\n    root[\"version\"] = version;\n    root[\"uptime\"]  = long(millis() / 1000);\n    String content;\n    root.printTo(content);\n    server.send(200, \"application/json\", content);\n  });\n\n  server.on(\"/update\", HTTP_POST, handleUpdate1, handleUpdate2);\n\n  // ask server to track these headers\n  const char * headerkeys[] = {\"User-Agent\",\"Content-Type\"} ;\n  size_t headerkeyssize = sizeof(headerkeys)/sizeof(char*);\n  server.collectHeaders(headerkeys, headerkeyssize );\n\n  server.begin();\n\n  MDNS.begin(host);\n  MDNS.addService(\"http\", \"tcp\", 80);\n}\n\nvoid loop() {\n  // put your main code here, to run repeatedly\n  server.handleClient();\n  delay(10); // in milliseconds\n}\n"
  },
  {
    "path": "esp8266_fan_control/esp8266_fan_control.ino",
    "content": "/*\n  This sketch is for an ESP8266 connected to an ARCTIC BioniX F140\n  140 mm diameter fan with PWM control.\n\n  See https://www.arctic.de/media/55/b2/e4/1690270670/Spec_Sheet_BioniX_F140_EN.pdf\n  \n  The sketch controls the speed of the fan and uses an interrupt \n  to measure the speed. Output is provided on the serial interface.\n\n*/\n\n#include <Arduino.h>\n\nconst int controlPin = D1;\nconst int sensorPin = D2;\n\nvoid ICACHE_RAM_ATTR callback();\n\nconst char* version = __DATE__ \" / \" __TIME__;\n\nvolatile unsigned int counter = 0;\n\nvoid setup() {\n  Serial.begin(115200);\n  Serial.print(\"\\n[esp8266_fan_control / \");\n  Serial.print(version);\n  Serial.println(\"]\");\n\n  pinMode(LED_BUILTIN, OUTPUT);\n  digitalWrite(LED_BUILTIN, HIGH);\n\n  analogWriteFreq(40000);\n  pinMode(controlPin, OUTPUT);\n  analogWrite(controlPin, 255);  // this is where the speed is controlled\n\n  pinMode(sensorPin, INPUT);\n  attachInterrupt(digitalPinToInterrupt(sensorPin), callback, RISING);\n}\n\nvoid loop() {\n  unsigned int rps, rpm;\n\n  noInterrupts();\n  rps = counter;\n  counter = 0;\n  interrupts();\n\n  // convert from counts per second to rotations per minute\n  rpm = rps * 60 / 2;\n\n  Serial.print(\"Counts: \");\n  Serial.print(rps, 1);\n  Serial.print(\" RPM: \");\n  Serial.println(rpm);\n\n  digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));\n\n  delay(1000);  // run once per second\n}\n\nvoid callback() {\n  counter++;\n}\n"
  },
  {
    "path": "esp8266_fieldtrip_buffer/esp8266_fieldtrip_buffer.ino",
    "content": "#include <ESP8266WiFi.h>\n\n#include \"secret.h\"\n#include \"fieldtrip_buffer.h\"\n\nint ftserver = 0, status = 0;\n\n#define NCHANS    1\n#define FSAMPLE   1000\n#define BLOCKSIZE 100\n\n#define FTHOST \"192.168.1.34\"\n#define FTPORT 1972\n\nuint16_t buf[BLOCKSIZE];\n\n/**************************************************************************************************/\n\nvoid setup() {\n  Serial.begin(115200);\n  delay(1000);\n\n  Serial.println();\n  Serial.println();\n  Serial.print(\"Connecting to \");\n  Serial.println(SSID);\n\n  WiFi.mode(WIFI_STA);\n  WiFi.begin(SSID, PASSWORD);\n\n  while (WiFi.status() != WL_CONNECTED) {\n    delay(500);\n    Serial.print(\".\");\n  }\n\n  Serial.println(\"\");\n  Serial.println(\"WiFi connected\");\n  Serial.println(\"IP address: \");\n  Serial.println(WiFi.localIP());\n\n  ftserver = fieldtrip_open_connection(FTHOST, FTPORT);\n  if (ftserver > 0) {\n    Serial.println(\"Connection opened\");\n    status = fieldtrip_write_header(ftserver, DATATYPE_UINT16, NCHANS, FSAMPLE);\n    if (status == 0)\n      Serial.println(\"Wrote header\");\n    else\n      Serial.println(\"Failed writing header\");\n  }\n  else {\n    Serial.println(\"Failed opening connection\");\n  }\n\n  for (int i = 0; i < BLOCKSIZE; i++)\n    buf[i] = 0;\n}\n\n/**************************************************************************************************/\n\nvoid loop() {\n\n  if (ftserver == 0) {\n    ftserver = fieldtrip_open_connection(FTHOST, FTPORT);\n    if (ftserver > 0)\n      Serial.println(\"Connection opened\");\n    else\n      Serial.println(\"Failed opening connection\");\n  }\n\n  if (ftserver > 0) {\n    status = fieldtrip_write_data(ftserver, DATATYPE_UINT16, NCHANS, BLOCKSIZE, (byte *)buf);\n    if (status == 0)\n      Serial.println(\"Wrote data\");\n    else {\n      Serial.println(\"Failed writing data\");\n      status = fieldtrip_close_connection(ftserver);\n      if (status == 0)\n        Serial.println(\"Connection closed\");\n      else\n        Serial.println(\"Failed closing connection\");\n      ftserver = 0;\n    }\n  }\n\n  for (int i = 0; i < BLOCKSIZE; i++)\n    buf[i] += 1;\n\n  delay((1000 * BLOCKSIZE) / FSAMPLE);\n}\n"
  },
  {
    "path": "esp8266_fieldtrip_buffer/fieldtrip_buffer.cpp",
    "content": "#include \"fieldtrip_buffer.h\"\n\nWiFiClient client;\n\n#undef DEBUG\n\n/*******************************************************************************\n   OPEN CONNECTION\n   returns file descriptor that should be >0 on success\n *******************************************************************************/\nint fieldtrip_open_connection(const char *address, int port) {\n  int status;\n  status = client.connect(address, port);\n  if (status == 1)\n    return 1;\n  else\n    return -1;\n};\n\n/*******************************************************************************\n   CLOSE CONNECTION\n   returns 0 on success\n *******************************************************************************/\nint fieldtrip_close_connection(int s) {\n  client.stop();\n  return 0;\n};\n\n/*******************************************************************************\n   WRITE HEADER\n   returns 0 on success\n *******************************************************************************/\nint fieldtrip_write_header(int server, uint32_t datatype, uint32_t nchans, float fsample) {\n  int status;\n  messagedef_t *request  = NULL;\n  messagedef_t *response = NULL;\n  headerdef_t  *header   = NULL;\n\n  byte msg[8+24];\n  for (int i = 0; i < 8+24; i++)\n    msg[i] = 0;\n\n  request = (messagedef_t *)(msg + 0);\n  request->version = VERSION;\n  request->command = PUT_HDR_NORESPONSE;\n  request->bufsize = sizeof(headerdef_t);\n\n  header = (headerdef_t *)(msg + sizeof(messagedef_t)); // the first 8 bytes are version, command and bufsize\n  header->nchans     = nchans;\n  header->nsamples   = 0;\n  header->nevents    = 0;\n  header->fsample    = fsample;\n  header->data_type  = datatype;\n  header->bufsize    = 0;\n\n#ifdef DEBUG\n  Serial.print(\"msg =  \");\n  for (int i = 0; i < sizeof(messagedef_t) + sizeof(headerdef_t); i++) {\n    Serial.print(msg[i]);\n    Serial.print(\" \");\n  }\n  Serial.println();\n#endif\n\n  int n = 0;\n  n += client.write(msg, sizeof(messagedef_t) + sizeof(headerdef_t));\n  client.flush();\n\n#ifdef DEBUG\n  Serial.print(\"Wrote \");\n  Serial.print(n);\n  Serial.println(\" bytes\");\n#endif\n\n  if (n == sizeof(messagedef_t) + sizeof(headerdef_t))\n    return 0;\n  else\n    return -1;\n};\n\n/*******************************************************************************\n   WRITE DATA\n   returns 0 on success\n *******************************************************************************/\nint fieldtrip_write_data(int server, uint32_t datatype, uint32_t nchans, uint32_t nsamples, byte *buffer) {\n  int status;\n  messagedef_t *request  = NULL;\n  messagedef_t *response = NULL;\n  datadef_t    *data   = NULL;\n\n  byte msg[8+16];\n  for (int i = 0; i < 8+16; i++)\n    msg[i] = 0;\n\n  request = (messagedef_t *)(msg + 0);\n  request->version = VERSION;\n  request->command = PUT_DAT_NORESPONSE;\n  request->bufsize = sizeof(datadef_t) + nchans * nsamples * wordsize_from_type(datatype);\n\n  data = (datadef_t *)(msg + sizeof(messagedef_t)); // the first 8 bytes are version, command and bufsize\n  data->nchans     = nchans;\n  data->nsamples   = nsamples;\n  data->data_type  = datatype;\n  data->bufsize    = nchans * nsamples * wordsize_from_type(datatype);\n\n#ifdef DEBUG\n  Serial.print(\"msg =  \");\n  for (int i = 0; i < sizeof(messagedef_t) + sizeof(datadef_t); i++) {\n    Serial.print(msg[i]);\n    Serial.print(\" \");\n  }\n  Serial.println();\n#endif\n\n  int n = 0;\n  n += client.write(msg, sizeof(messagedef_t) + sizeof(datadef_t));\n  client.flush();\n  n += client.write(buffer, nchans * nsamples * wordsize_from_type(datatype));\n  client.flush();\n\n#ifdef DEBUG\n  Serial.print(\"Wrote \");\n  Serial.print(n);\n  Serial.println(\" bytes\");\n#endif\n\n  if (n == sizeof(messagedef_t) + sizeof(datadef_t) + nchans * nsamples * wordsize_from_type(datatype))\n    return 0;\n  else\n    return -1;\n};\n\nint wordsize_from_type(uint32_t datatype) {\n  int wordsize = 0;\n  switch (datatype) {\n    case DATATYPE_CHAR:\n      wordsize = 1;\n      break;\n    case DATATYPE_UINT8:\n      wordsize = 1;\n      break;\n    case DATATYPE_UINT16:\n      wordsize = 2;\n      break;\n    case DATATYPE_UINT32:\n      wordsize = 4;\n      break;\n    case DATATYPE_UINT64:\n      wordsize = 8;\n      break;\n    case DATATYPE_INT8:\n      wordsize = 1;\n      break;\n    case DATATYPE_INT16:\n      wordsize = 2;\n      break;\n    case DATATYPE_INT32:\n      wordsize = 4;\n      break;\n    case DATATYPE_INT64:\n      wordsize = 8;\n      break;\n    case DATATYPE_FLOAT32:\n      wordsize = 4;\n      break;\n    case DATATYPE_FLOAT64:\n      wordsize = 8;\n      break;\n    }\n  return wordsize;\n};\n"
  },
  {
    "path": "esp8266_fieldtrip_buffer/fieldtrip_buffer.h",
    "content": "#ifndef __BUFFER_H_\n#define __BUFFER_H_\n\n#include <WiFiClient.h>\n\n// definition of simplified interface functions, not all of them are implemented\nint fieldtrip_start_server(int port);\nint fieldtrip_open_connection(const char *hostname, int port);\nint fieldtrip_close_connection(int s);\nint fieldtrip_read_header(int server, uint32_t *datatype, uint32_t *nchans, float *fsample, uint32_t *nsamples, uint32_t *nevents);\nint fieldtrip_read_data(int server, uint32_t begsample, uint32_t endsample, byte *buffer);\nint fieldtrip_write_header(int server, uint32_t datatype, uint32_t nchans, float fsample);\nint fieldtrip_write_data(int server, uint32_t datatype, uint32_t nchans, uint32_t nsamples, byte *buffer);\nint fieldtrip_wait_data(int server, uint32_t nsamples, uint32_t nevents, uint32_t milliseconds);\nint wordsize_from_type(uint32_t datatype);\n\n// define the version of the message packet\n#define VERSION    (uint16_t)0x0001\n\n// these define the commands that can be used, which are split over the two available bytes\n#define PUT_HDR    (uint16_t)0x0101 /* decimal 257 */\n#define PUT_DAT    (uint16_t)0x0102 /* decimal 258 */\n#define PUT_EVT    (uint16_t)0x0103 /* decimal 259 */\n#define PUT_OK     (uint16_t)0x0104 /* decimal 260 */\n#define PUT_ERR    (uint16_t)0x0105 /* decimal 261 */\n\n#define GET_HDR    (uint16_t)0x0201 /* decimal 513 */\n#define GET_DAT    (uint16_t)0x0202 /* decimal 514 */\n#define GET_EVT    (uint16_t)0x0203 /* decimal 515 */\n#define GET_OK     (uint16_t)0x0204 /* decimal 516 */\n#define GET_ERR    (uint16_t)0x0205 /* decimal 517 */\n\n#define FLUSH_HDR  (uint16_t)0x0301 /* decimal 769 */\n#define FLUSH_DAT  (uint16_t)0x0302 /* decimal 770 */\n#define FLUSH_EVT  (uint16_t)0x0303 /* decimal 771 */\n#define FLUSH_OK   (uint16_t)0x0304 /* decimal 772 */\n#define FLUSH_ERR  (uint16_t)0x0305 /* decimal 773 */\n\n#define WAIT_DAT   (uint16_t)0x0402 /* decimal 1026 */\n#define WAIT_OK    (uint16_t)0x0404 /* decimal 1027 */\n#define WAIT_ERR   (uint16_t)0x0405 /* decimal 1028 */\n\n#define PUT_HDR_NORESPONSE (uint16_t)0x0501 /* decimal 1281 */\n#define PUT_DAT_NORESPONSE (uint16_t)0x0502 /* decimal 1282 */\n#define PUT_EVT_NORESPONSE (uint16_t)0x0503 /* decimal 1283 */\n\n// these are used in the data_t and event_t structure\n#define DATATYPE_CHAR    (uint32_t)0\n#define DATATYPE_UINT8   (uint32_t)1\n#define DATATYPE_UINT16  (uint32_t)2\n#define DATATYPE_UINT32  (uint32_t)3\n#define DATATYPE_UINT64  (uint32_t)4\n#define DATATYPE_INT8    (uint32_t)5\n#define DATATYPE_INT16   (uint32_t)6\n#define DATATYPE_INT32   (uint32_t)7\n#define DATATYPE_INT64   (uint32_t)8\n#define DATATYPE_FLOAT32 (uint32_t)9\n#define DATATYPE_FLOAT64 (uint32_t)10\n\n// a packet that is sent over the network should contain the following\ntypedef struct {\n  uint16_t version;   // see VERSION\n  uint16_t command;   // see PUT_xxx, GET_xxx and FLUSH_xxx\n  uint32_t bufsize;   // size of the buffer in bytes\n}\nmessagedef_t; // 8 bytes\n\n// the header definition is fixed, it can be followed by additional header chunks\ntypedef struct {\n  uint32_t  nchans;\n  uint32_t  nsamples;\n  uint32_t  nevents;\n  float     fsample;\n  uint32_t  data_type;\n  uint32_t  bufsize;   // size of the buffer in bytes\n}\nheaderdef_t; // 24 bytes\n\n// the data definition is fixed, it should be followed by additional data\ntypedef struct {\n  uint32_t nchans;\n  uint32_t nsamples;\n  uint32_t data_type;\n  uint32_t bufsize;   // size of the buffer in bytes\n}\ndatadef_t; // 16 bytes\n\n// the event definition is fixed, it should be followed by additional data\ntypedef struct {\n  uint32_t type_type;   /* usual would be DATATYPE_CHAR */\n  uint32_t type_numel;  /* length of the type string */\n  uint32_t value_type;\n  uint32_t value_numel;\n  int32_t  sample;\n  int32_t  offset;\n  int32_t  duration;\n  uint32_t bufsize;     /* size of the buffer in bytes */\n} eventdef_t; // 64 bytes\n\n#endif\n"
  },
  {
    "path": "esp8266_imu_osc/I2Cscan.cpp",
    "content": "#include <Arduino.h>\n#include <Wire.h>\n\n#include \"I2Cscan.h\"\n\nvoid I2Cscan()\n{\n  // scan for i2c devices\n  byte error, address;\n  int nDevices;\n\n  Serial.println(\"Scanning...\");\n\n  nDevices = 0;\n  for (address = 1; address < 127; address++ )\n  {\n    // The i2c_scanner uses the return value of\n    // the Write.endTransmisstion to see if\n    // a device did acknowledge to the address.\n    Wire.beginTransmission(address);\n    error = Wire.endTransmission();\n\n    if (error == 0)\n    {\n      Serial.print(\"I2C device found at address 0x\");\n      if (address < 16)\n        Serial.print(\"0\");\n      Serial.print(address, HEX);\n      Serial.println(\"  !\");\n\n      nDevices++;\n    }\n    else if (error == 4)\n    {\n      Serial.print(\"Unknow error at address 0x\");\n      if (address < 16)\n        Serial.print(\"0\");\n      Serial.println(address, HEX);\n    }\n  }\n  if (nDevices == 0)\n    Serial.println(\"No I2C devices found\\n\");\n  else\n    Serial.println(\"done\\n\");\n\n}\n"
  },
  {
    "path": "esp8266_imu_osc/I2Cscan.h",
    "content": "#ifndef __I2CSCAN_H__\n#define __I2CSCAN_H__\n\nvoid I2Cscan();\n\n#endif\n"
  },
  {
    "path": "esp8266_imu_osc/README.md",
    "content": "# Overview\n\nThis is an Arduino sketch for an ESP8266 module that is connected to a number of inertial motion units (IMUs). It sends the data from the IMUs over OSC to a computer.\n\nSee https://robertoostenveld.nl/motion-capture-system/ for more details.\n\n## SPIFFS for static files\n\nYou should not only write the firmware to the ESP8266 module, but also the static content for the web interface. The html, css and javascript files located in the data directory should be written to the SPIFS filesystem on the ESP8266. See for example http://esp8266.github.io/Arduino/versions/2.0.0/doc/filesystem.html and https://www.instructables.com/id/Using-ESP8266-SPIFFS for instructions.\n\nYou will get a \"file not found\" error if the firmware cannot access the data files.\n\n## SPIFFS for static files\n\nYou should not only write the firmware to the ESP8266 module, but also the static content for the web interface. The html, css and javascript files located in the data directory should be written to the SPIFS filesystem on the ESP8266. See for example http://esp8266.github.io/Arduino/versions/2.0.0/doc/filesystem.html and https://www.instructables.com/id/Using-ESP8266-SPIFFS for instructions.\n\nYou will get a \"file not found\" error if the firmware cannot access the data files.\n"
  },
  {
    "path": "esp8266_imu_osc/data/config.json",
    "content": "{\n\t\"sensors\": 8,\n\t\"decimate\": 1,\n\t\"calibrate\": 0,\n\t\"raw\": 1,\n\t\"ahrs\": 0,\n\t\"quaternion\": 0,\n\t\"termperature\": 0,\n\t\"destination\": \"192.168.1.34\",\n\t\"port\": 8000\n}\n"
  },
  {
    "path": "esp8266_imu_osc/data/hello.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Hello</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n<p>Hello rabbit!</p>\n<img src=\"rabbit.jpg\">\n</body>\n</html>\n"
  },
  {
    "path": "esp8266_imu_osc/data/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Index</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n<a href=\"/hello.html\">Hello</a></br>\n<a href=\"/monitor.html\">Monitor</a></br>\n<a href=\"/settings.html\">Change settings</a></br>\n<a href=\"/reconnect\">Reconnect WiFi</a></br>\n<a href=\"/defaults\">Default settings</a></br>\n<a href=\"/update.html\">Update firmware</a></br>\n<a href=\"/restart\">Restart hardware</a></br>\n</body>\n</html>\n"
  },
  {
    "path": "esp8266_imu_osc/data/monitor.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Monitor</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n\n<h1>Monitor</h1>\n\nFirmware version:\n<div id=\"version\" name=\"version\">?</div>\n\nUptime:\n<div id=\"uptime\" name\"uptime\">?</div>\n\nSample rate:\n<div id=\"rate\" name=\"rate\">?</div>\n\n<script>\n  async function updateContent() {\n    const response = await fetch(\"json\");\n    const data = await response.json();\n    console.log(data);\n    document.getElementById(\"version\").innerHTML = data[\"version\"];\n    document.getElementById(\"uptime\").innerHTML = data[\"uptime\"];\n    document.getElementById(\"rate\").innerHTML = data[\"rate\"];\n  }\n  updateContent();\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "esp8266_imu_osc/data/reload_failure.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Failure</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n\n<p>Failure</p>\n\n<script type=\"text/javascript\">\n  setTimeout(function(){location=\"/index.html\"},1000);\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "esp8266_imu_osc/data/reload_success.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Success</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n\n<p>Success</p>\n\n<script type=\"text/javascript\">\n  setTimeout(function(){location=\"/index.html\"},1000);\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "esp8266_imu_osc/data/settings.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Settings</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n\n<h1>Settings</h1>\n<form id=\"settings-form\" method=\"post\" action=\"json\">\n\n    <div class=\"field\">\n        <label for=\"sensors\">sensors:</label>\n        <input type=\"text\" id=\"sensors\" name=\"sensors\" value=\"?\" required>\n    </div>\n\n    <div class=\"field\">\n        <label for=\"decimate\">decimate:</label>\n        <input type=\"text\" id=\"decimate\" name=\"decimate\" value=\"?\" required>\n    </div>\n\n    <div class=\"field\">\n        <label for=\"calibrate\">calibrate:</label>\n        <input type=\"text\" id=\"calibrate\" name=\"calibrate\" value=\"?\" required>\n    </div>\n\n    <div class=\"field\">\n        <label for=\"raw\">raw:</label>\n        <input type=\"text\" id=\"raw\" name=\"raw\" value=\"?\" required>\n    </div>\n\n    <div class=\"field\">\n        <label for=\"ahrs\">ahrs:</label>\n        <input type=\"text\" id=\"ahrs\" name=\"ahrs\" value=\"?\" required>\n    </div>\n\n    <div class=\"field\">\n        <label for=\"quaternion\">quaternion:</label>\n        <input type=\"text\" id=\"quaternion\" name=\"quaternion\" value=\"?\" required>\n    </div>\n\n    <div class=\"field\">\n        <label for=\"temperature\">temperature:</label>\n        <input type=\"text\" id=\"temperature\" name=\"temperature\" value=\"?\" required>\n    </div>\n\n    <div class=\"field\">\n        <label for=\"destination\">destination:</label>\n        <input type=\"text\" id=\"destination\" name=\"destination\" value=\"?\" required>\n    </div>\n\n    <div class=\"field\">\n        <label for=\"port\">port:</label>\n        <input type=\"text\" id=\"port\" name=\"port\" value=\"?\" required>\n    </div>\n\n    <div class=\"field\">\n        <button type=\"submit\">Save</button>\n    </div>\n</form>\n\n<script>\n  async function updateContent() {\n    const response = await fetch(\"json\");\n    const data = await response.json();\n    console.log(data);\n    document.getElementById(\"var1\").value = data[\"var1\"];\n    document.getElementById(\"var2\").value = data[\"var2\"];\n    document.getElementById(\"var3\").value = data[\"var3\"];\n  }\n  updateContent();\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "esp8266_imu_osc/data/style.css",
    "content": ".c{\n    text-align: center;\n}\ndiv,input{\n    padding:5px;font-size:1em;\n}\ninput{\n    width:85%;\n}\nbody{\n    text-align: center;font-family:verdana;\n}\nbutton{\n    border:0;border-radius:0.3rem;background-color:#1fa3ec;color:#fff;line-height:2.4rem;font-size:1.2rem;width:100%;\n}\n.q{\n    float: right;width: 64px;text-align: right;\n}\n.l{\n    background: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAALVBMVEX///8EBwfBwsLw8PAzNjaCg4NTVVUjJiZDRUUUFxdiZGSho6OSk5Pg4eFydHTCjaf3AAAAZElEQVQ4je2NSw7AIAhEBamKn97/uMXEGBvozkWb9C2Zx4xzWykBhFAeYp9gkLyZE0zIMno9n4g19hmdY39scwqVkOXaxph0ZCXQcqxSpgQpONa59wkRDOL93eAXvimwlbPbwwVAegLS1HGfZAAAAABJRU5ErkJggg==\") no-repeat left center;background-size: 1em;\n}\n"
  },
  {
    "path": "esp8266_imu_osc/data/update.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Update</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n\n<h1>Update the ESP8266 firmware</h1>\n<form method='POST' action='/update' enctype='multipart/form-data'>\n\n<div class=\"field\">\n<input type='file' name='update'>\n</div>\n\n<div class=\"field\">\n<button type=\"submit\">Update</button>\n</div>\n\n</form>\n</body>\n</html>\n"
  },
  {
    "path": "esp8266_imu_osc/esp8266_imu_osc.ino",
    "content": "/*\n  This sketch is for an ESP8266 connected to an TCA9548A I2C multiplexer,\n  which in turn connects to multiple MPU9250 inertial motion units (IMUs).\n\n  It reads the acceletometer, gyroscope and magnetometer data from the\n  IMUs and computes AHRS (yaw/pitch/roll) parameters. All data is combined\n  and sent over UDP using the OSC protocol.\n\n  It uses WIFiManager for initial configuration and includes a web-interface\n  that allows to monitor and change parameters.\n\n  The status of the wifi connection, http server and the IMU data transmission\n  is indicated by a RDB led that blinks Red, Green or Blue.\n\n*/\n\n#include <Arduino.h>\n#include <ESP8266WiFi.h>         // https://github.com/esp8266/Arduino\n#include <ESP8266WebServer.h>\n#include <ESP8266mDNS.h>\n#include <WiFiManager.h>         // https://github.com/tzapu/WiFiManager\n#include <WiFiUDP.h>\n#include <OSCBundle.h>\n\n// from https://github.com/robertoostenveld/SparkFun_MPU-9250_Breakout_Arduino_Library\n#include <MPU9250.h>\n#include <quaternionFilters.h>\n\n#include \"webinterface.h\"\n#include \"rgb_led.h\"\n#include \"tca9548a.h\"\n#include \"I2Cscan.h\"\n\n#define debugLevel 2          // 0 = silent, 1 = blink led, 2 = print on serial console\n#define maxSensors 8\n\n// this allows some sections of the code to be disabled for debugging purposes\n#define ENABLE_WEBINTERFACE\n#define ENABLE_MDNS\n#define ENABLE_IMU\n\n#define MIN(x,y) ((x) < (y) ? (x) : (y))\n#define MAX(x,y) ((x) > (y) ? (x) : (y))\n\n// Use either of these to select which I2C address your device is using\n#define MPU9250_ADDRESS MPU9250_ADDRESS_AD0\n// #define MPU9250_ADDRESS MPU9250_ADDRESS_AD1\n\nESP8266WebServer server(80);\nconst char* host = \"IMU-OSC\";\nconst char* version = __DATE__ \" / \" __TIME__;\n\ntca9548a tca;\nMPU9250 mpu[maxSensors];\nString id[maxSensors] = {\"imu1\", \"imu2\", \"imu3\", \"imu4\", \"imu5\", \"imu6\", \"imu7\", \"imu8\"};\n\n// UDP destination address, these will be changed according to the configuration\nIPAddress outIp(192, 168, 1, 100);\nconst unsigned int inPort = 9000, outPort = 8000;\nWiFiUDP Udp;\n\nfloat rate = 0.0f;\nfloat deltat = 0.0f, sum = 0.0f;          // integration interval for both filter schemes\nuint32_t Now = 0, Last[maxSensors];       // used to calculate integration interval\nuint32_t lastDisplay = 0, lastTemperature = 0, lastTransmit = 0;\nunsigned int debugCount = 0, measurementCount = 0;\n\n// For continous calibration\nfloat mag_min[3] = { -1e6, -1e6, -1e6};\nfloat mag_max[3] = {1e6, 1e6, 1e6};\nfloat mag_scale[3] = {1, 1, 1};\n\n// keep track of the timing of the web interface\nlong tic_web = 0;\n\nvoid setup() {\n  Serial.begin(115200);\n  while (!Serial) {\n    ;\n  }\n\n  Serial.print(\"\\n[esp8266_imu_osc / \");\n  Serial.print(__DATE__ \" / \" __TIME__);\n  Serial.println(\"]\");\n\n  WiFi.hostname(host);\n  WiFi.begin();\n\n  pinMode(0, OUTPUT);     // RESET=D3=0\n  digitalWrite(0, LOW);   // reset on\n  delay(10);\n  digitalWrite(0, HIGH);  // reset off\n\n  Wire.begin(5, 4);       // SDA=D1=5, SCL=D2=4\n  Wire.setClock(400000L);\n\n  SPIFFS.begin();\n\n  // Used for OSC\n  Udp.begin(inPort);\n\n  ledInit();\n\n  if (loadConfig()) {\n    ledYellow();\n    delay(1000);\n  }\n  else {\n    ledRed();\n    delay(1000);\n  }\n\n  if (WiFi.status() != WL_CONNECTED)\n    ledRed();\n\n  WiFiManager wifiManager;\n  wifiManager.setAPStaticIPConfig(IPAddress(192, 168, 1, 1), IPAddress(192, 168, 1, 1), IPAddress(255, 255, 255, 0));\n  wifiManager.autoConnect(host);\n  Serial.println(\"connected\");\n\n  if (WiFi.status() == WL_CONNECTED)\n    ledGreen();\n\n#ifdef ENABLE_WEBINTERFACE\n  // this serves all URIs that can be resolved to a file on the SPIFFS filesystem\n  server.onNotFound(handleNotFound);\n\n  server.on(\"/\", HTTP_GET, []() {\n    tic_web = millis();\n    handleRedirect(\"/index.html\");\n  });\n\n  server.on(\"/defaults\", HTTP_GET, []() {\n    tic_web = millis();\n    Serial.println(\"handleDefaults\");\n    handleStaticFile(\"/reload_success.html\");\n    defaultConfig();\n    saveConfig();\n    server.close();\n    server.stop();\n    ESP.restart();\n  });\n\n  server.on(\"/reconnect\", HTTP_GET, []() {\n    tic_web = millis();\n    Serial.println(\"handleReconnect\");\n    handleStaticFile(\"/reload_success.html\");\n    ledRed();\n    server.close();\n    server.stop();\n    delay(5000);\n    WiFiManager wifiManager;\n    wifiManager.resetSettings();\n    wifiManager.setAPStaticIPConfig(IPAddress(192, 168, 1, 1), IPAddress(192, 168, 1, 1), IPAddress(255, 255, 255, 0));\n    wifiManager.startConfigPortal(host);\n    Serial.println(\"connected\");\n    server.begin();\n    if (WiFi.status() == WL_CONNECTED)\n      ledGreen();\n  });\n\n  server.on(\"/restart\", HTTP_GET, []() {\n    tic_web = millis();\n    Serial.println(\"handleRestart\");\n    handleStaticFile(\"/reload_success.html\");\n    ledRed();\n    server.close();\n    server.stop();\n    SPIFFS.end();\n    delay(5000);\n    ESP.restart();\n  });\n\n  server.on(\"/dir\", HTTP_GET, [] {\n    tic_web = millis();\n    handleDirList();\n  });\n\n  server.on(\"/json\", HTTP_PUT, [] {\n    tic_web = millis();\n    handleJSON();\n  });\n\n  server.on(\"/json\", HTTP_POST, [] {\n    tic_web = millis();\n    handleJSON();\n  });\n\n  server.on(\"/json\", HTTP_GET, [] {\n    tic_web = millis();\n    StaticJsonBuffer<300> jsonBuffer;\n    JsonObject& root = jsonBuffer.createObject();\n    N_CONFIG_TO_JSON(sensors, \"sensors\");\n    N_CONFIG_TO_JSON(decimate, \"decimate\");\n    N_CONFIG_TO_JSON(calibrate, \"calibrate\");\n    N_CONFIG_TO_JSON(raw, \"raw\");\n    N_CONFIG_TO_JSON(ahrs, \"ahrs\");\n    N_CONFIG_TO_JSON(quaternion, \"quaternion\");\n    N_CONFIG_TO_JSON(temperature, \"temperature\");\n    S_CONFIG_TO_JSON(destination, \"destination\");\n    N_CONFIG_TO_JSON(port, \"port\");\n    root[\"version\"] = version;\n    root[\"uptime\"]  = long(millis() / 1000);\n    root[\"rate\"]    = rate;\n    String str;\n    root.printTo(str);\n    server.setContentLength(str.length());\n    server.send(200, \"application/json\", str);\n  });\n\n  server.on(\"/update\", HTTP_GET, [] {\n    tic_web = millis();\n    handleStaticFile(\"/update.html\");\n  });\n\n  server.on(\"/update\", HTTP_POST, handleUpdate1, handleUpdate2);\n\n  // start the web server\n  server.begin();\n#endif\n\n#ifdef ENABLE_MDNS\n  // announce the hostname and web server through zeroconf\n  MDNS.begin(host);\n  MDNS.addService(\"http\", \"tcp\", 80);\n#endif\n\n#ifdef ENABLE_IMU\n  ledMagenta();\n\n  // Look for I2C devices on the bus\n  I2Cscan();\n\n  byte status = 0;\n  for (int i = 0; i < config.sensors; i++) {\n    Serial.println(\"====================================================\");\n    Serial.println(String(\"initializing \" + id[i]));\n    tca.select(i);\n\n    // Read the WHO_AM_I register, this is a good test of communication\n    byte c = mpu[i].readByte(MPU9250_ADDRESS, WHO_AM_I_MPU9250);\n    Serial.print(F(\"MPU9250 I am 0x\"));\n    Serial.print(c, HEX);\n    Serial.print(F(\" I should be 0x\"));\n    Serial.println(0x71, HEX);\n\n    // WHO_AM_I should be 0x71 for MPU9250 or 0x73 for MPU9255\n    if ((c == 0x71) || (c == 0x73)) {\n      Serial.println(F(\"MPU9250 is online...\"));\n\n      // Start by performing self test and reporting values\n      mpu[i].MPU9250SelfTest(mpu[i].selfTest);\n      Serial.print(F(\"x-axis self test: acceleration trim within : \"));\n      Serial.print(mpu[i].selfTest[0], 1); Serial.println(\"% of factory value\");\n      Serial.print(F(\"y-axis self test: acceleration trim within : \"));\n      Serial.print(mpu[i].selfTest[1], 1); Serial.println(\"% of factory value\");\n      Serial.print(F(\"z-axis self test: acceleration trim within : \"));\n      Serial.print(mpu[i].selfTest[2], 1); Serial.println(\"% of factory value\");\n      Serial.print(F(\"x-axis self test: gyration trim within : \"));\n      Serial.print(mpu[i].selfTest[3], 1); Serial.println(\"% of factory value\");\n      Serial.print(F(\"y-axis self test: gyration trim within : \"));\n      Serial.print(mpu[i].selfTest[4], 1); Serial.println(\"% of factory value\");\n      Serial.print(F(\"z-axis self test: gyration trim within : \"));\n      Serial.print(mpu[i].selfTest[5], 1); Serial.println(\"% of factory value\");\n\n      // Calibrate gyro and accelerometers, load biases in bias registers\n      mpu[i].calibrateMPU9250(mpu[i].gyroBias, mpu[i].accelBias);\n\n      mpu[i].initMPU9250();\n      // Initialize device for active mode read of acclerometer, gyroscope, and temperature\n      Serial.println(\"MPU9250 initialized for active data mode....\");\n\n      // Read the WHO_AM_I register of the magnetometer, this is a good test of communication\n      byte d = mpu[i].readByte(AK8963_ADDRESS, WHO_AM_I_AK8963);\n      Serial.print(\"AK8963 \");\n      Serial.print(\"I am 0x\");\n      Serial.print(d, HEX);\n      Serial.print(\" I should be 0x\");\n      Serial.println(0x48, HEX);\n\n      if (d != 0x48) {\n        status++;\n      }\n\n      // Get magnetometer calibration from AK8963 ROM\n      mpu[i].initAK8963(mpu[i].factoryMagCalibration);\n      // Initialize device for active mode read of magnetometer\n      Serial.println(\"AK8963 initialized for active data mode....\");\n\n      //  Serial.println(\"Calibration values: \");\n      Serial.print(\"X-Axis factory magnetometer adjustment value \");\n      Serial.println(mpu[i].factoryMagCalibration[0], 2);\n      Serial.print(\"Y-Axis factory magnetometer adjustment value \");\n      Serial.println(mpu[i].factoryMagCalibration[1], 2);\n      Serial.print(\"Z-Axis factory magnetometer adjustment value \");\n      Serial.println(mpu[i].factoryMagCalibration[2], 2);\n\n      // Get sensor resolutions, only need to do this once\n      mpu[i].getAres();\n      mpu[i].getGres();\n      mpu[i].getMres();\n\n      if (config.calibrate == 1) {\n        // The next call delays for 4 seconds, and then records about 15 seconds of data to calculate bias and scale.\n        mpu[i].magCalMPU9250(mpu[i].magBias, mpu[i].magScale);\n\n        Serial.println(\"AK8963 mag biases (mG)\");\n        Serial.println(mpu[i].magBias[0]);\n        Serial.println(mpu[i].magBias[1]);\n        Serial.println(mpu[i].magBias[2]);\n\n        Serial.println(\"AK8963 mag scale (mG)\");\n        Serial.println(mpu[i].magScale[0]);\n        Serial.println(mpu[i].magScale[1]);\n        Serial.println(mpu[i].magScale[2]);\n      }\n\n    } // if (c == 0x71)\n    else {\n      status++;\n    }\n\n    if (status) {\n      Serial.println(\"not present\");\n      config.sensors = i;  // the index starts at 0\n      break;\n    }\n  } // for each sensor\n  Serial.println(\"====================================================\");\n#endif\n\n  Serial.println(\"====================================================\");\n  Serial.println(\"Setup done\");\n  Serial.println(\"====================================================\");\n}\n\nvoid loop() {\n  OSCBundle bundle;\n  char msgId[16];\n\n#ifdef ENABLE_WEBINTERFACE\n  server.handleClient();\n#endif\n\n  if (WiFi.status() != WL_CONNECTED) {\n    ledRed();\n  }\n  else if ((millis() - tic_web) < 3000) {\n    // serving content on the http interface takes a lot of resources and\n    // messes up the regular timing of the acquisition and transmission\n    ledBlue();\n  }\n\n#ifdef ENABLE_IMU\n  for (int i = 0; i < config.sensors; i++) {\n    const float *q;\n    tca.select(i);\n\n    while (!(mpu[i].readByte(MPU9250_ADDRESS, INT_STATUS) & 0x01))\n      delay(1);\n\n    mpu[i].readAccelData(mpu[i].accelCount);  // Read the x/y/z adc values\n    // Now we'll calculate the accleration value into actual g's\n    // This depends on scale being set\n    mpu[i].ax = (float)mpu[i].accelCount[0] * mpu[i].aRes; // - mpu[i].accelBias[0];\n    mpu[i].ay = (float)mpu[i].accelCount[1] * mpu[i].aRes; // - mpu[i].accelBias[1];\n    mpu[i].az = (float)mpu[i].accelCount[2] * mpu[i].aRes; // - mpu[i].accelBias[2];\n\n    mpu[i].readGyroData(mpu[i].gyroCount);  // Read the x/y/z adc values\n    // Calculate the gyro value into actual degrees per second\n    // This depends on scale being set\n    mpu[i].gx = (float)mpu[i].gyroCount[0] * mpu[i].gRes; // - mpu[i].gyroBias[0];\n    mpu[i].gy = (float)mpu[i].gyroCount[1] * mpu[i].gRes; // - mpu[i].gyroBias[1];\n    mpu[i].gz = (float)mpu[i].gyroCount[2] * mpu[i].gRes; // - mpu[i].gyroBias[2];\n\n    mpu[i].readMagData(mpu[i].magCount);  // Read the x/y/z adc values\n    // Calculate the magnetometer values in milliGauss\n    // Get actual magnetometer value, this depends on scale being set\n    // Include factory calibration per data sheet and user environmental corrections\n    mpu[i].mx = (float)mpu[i].magCount[0] * mpu[i].mRes * mpu[i].factoryMagCalibration[0] * mpu[i].magScale[0] - mpu[i].magBias[0];\n    mpu[i].my = (float)mpu[i].magCount[1] * mpu[i].mRes * mpu[i].factoryMagCalibration[1] * mpu[i].magScale[1] - mpu[i].magBias[1];\n    mpu[i].mz = (float)mpu[i].magCount[2] * mpu[i].mRes * mpu[i].factoryMagCalibration[2] * mpu[i].magScale[2] - mpu[i].magBias[2];\n\n    if (config.raw) {\n      String(id[i] + \"/a\").toCharArray(msgId, 16); bundle.add(msgId).add(mpu[i].ax).add(mpu[i].ay).add(mpu[i].az);\n      String(id[i] + \"/g\").toCharArray(msgId, 16); bundle.add(msgId).add(mpu[i].gx).add(mpu[i].gy).add(mpu[i].gz);\n      String(id[i] + \"/m\").toCharArray(msgId, 16); bundle.add(msgId).add(mpu[i].mx).add(mpu[i].my).add(mpu[i].mz);\n    }\n\n    if (config.calibrate > 1) {\n      // calibrate==2 means only for the first 30 seconds\n      // calibrate==3 means continuous\n      // the min value is negative, the max value is positive\n      mag_min[0] = MIN(mpu[i].mx, mag_min[0]);\n      mag_min[1] = MIN(mpu[i].my, mag_min[1]);\n      mag_min[2] = MIN(mpu[i].mz, mag_min[2]);\n      mag_max[0] = MAX(mpu[i].mx, mag_max[0]);\n      mag_max[1] = MAX(mpu[i].my, mag_max[1]);\n      mag_max[2] = MAX(mpu[i].mz, mag_max[2]);\n      // the sum of the min and max should be zero\n      mpu[i].magBias[0] = (mag_max[0] + mag_min[0]) / 2;\n      mpu[i].magBias[1] = (mag_max[1] + mag_min[1]) / 2;\n      mpu[i].magBias[2] = (mag_max[2] + mag_min[2]) / 2;\n      // the min and max themselves should be one\n      mag_scale[0] = (mag_max[0] - mag_min[0]) / 2;\n      mag_scale[1] = (mag_max[1] - mag_min[1]) / 2;\n      mag_scale[2] = (mag_max[2] - mag_min[2]) / 2;\n      mpu[i].magScale[0] = 3 * mag_scale[0] / (mag_scale[0] + mag_scale[1] + mag_scale[2]);\n      mpu[i].magScale[1] = 3 * mag_scale[1] / (mag_scale[0] + mag_scale[1] + mag_scale[2]);\n      mpu[i].magScale[2] = 3 * mag_scale[2] / (mag_scale[0] + mag_scale[1] + mag_scale[2]);\n    }\n\n    if (config.ahrs || config.quaternion) {\n      // Must be called before updating quaternions!\n      mpu[i].updateTime();\n\n      // Sensors x (y)-axis of the accelerometer is aligned with the y (x)-axis of\n      // the magnetometer; the magnetometer z-axis (+ down) is opposite to z-axis\n      // (+ up) of accelerometer and gyro! We have to make some allowance for this\n      // orientationmismatch in feeding the output to the quaternion filter. For the\n      // MPU-9250, we have chosen a magnetic rotation that keeps the sensor forward\n      // along the x-axis just like in the LSM9DS0 sensor. This rotation can be\n      // modified to allow any convenient orientation convention. This is ok by\n      // aircraft orientation standards! Pass gyro rate as rad/s\n\n      // MadgwickQuaternionUpdate(mpu[i].ax, mpu[i].ay, mpu[i].az, mpu[i].gx * DEG_TO_RAD, mpu[i].gy * DEG_TO_RAD, mpu[i].gz * DEG_TO_RAD, mpu[i].my, mpu[i].mx, -mpu[i].mz, mpu[i].deltat);\n      MahonyQuaternionUpdate(mpu[i].ax, mpu[i].ay, mpu[i].az, mpu[i].gx * DEG_TO_RAD, mpu[i].gy * DEG_TO_RAD, mpu[i].gz * DEG_TO_RAD, mpu[i].my, mpu[i].mx, -mpu[i].mz, mpu[i].deltat);\n\n      // Define output variables from updated quaternion---these are Tait-Bryan\n      // angles, commonly used in aircraft orientation. In this coordinate system,\n      // the positive z-axis is down toward Earth. Yaw is the angle between Sensor\n      // x-axis and Earth magnetic North (or true North if corrected for local\n      // declination, looking down on the sensor positive yaw is counterclockwise.\n      // Pitch is angle between sensor x-axis and Earth ground plane, toward the\n      // Earth is positive, up toward the sky is negative. Roll is angle between\n      // sensor y-axis and Earth ground plane, y-axis up is positive roll. These\n      // arise from the definition of the homogeneous rotation matrix constructed\n      // from quaternions. Tait-Bryan angles as well as Euler angles are\n      // non-commutative; that is, the get the correct orientation the rotations\n      // must be applied in the correct order which for this configuration is yaw,\n      // pitch, and then roll.\n      // For more see\n      // http://en.wikipedia.org/wiki/Conversion_between_quaternions_and_Euler_angles\n      // which has additional links.\n\n      q = getQ();\n      mpu[i].yaw    = atan2(2.0f * (q[1] * q[2] + q[0] * q[3]), q[0] * q[0] + q[1] * q[1] - q[2] * q[2] - q[3] * q[3]);\n      mpu[i].pitch  = -asin(2.0f * (q[1] * q[3] - q[0] * q[2]));\n      mpu[i].roll   = atan2(2.0f * (q[0] * q[1] + q[2] * q[3]), q[0] * q[0] - q[1] * q[1] - q[2] * q[2] + q[3] * q[3]);\n      mpu[i].yaw   *= RAD_TO_DEG;\n      mpu[i].pitch *= RAD_TO_DEG;\n      mpu[i].roll  *= RAD_TO_DEG;\n\n      // See http://www.ngdc.noaa.gov/geomag-web/#declination\n      // Declination of SparkFun Electronics (40°05'26.6\"N 105°11'05.9\"W) is 8° 30' E  ± 0° 21' (or 8.5°) on 2016-07-19\n      // mpu[i].yaw  -= 8.5;\n      // Declination at Nijmegen, NL is 1 degrees 31 minutes and 00 seconds on 2017-12-16\n      mpu[i].yaw  -= 1.5167;\n\n      if (config.ahrs) {\n        String(id[i] + \"/roll\").toCharArray(msgId, 16); bundle.add(msgId).add(mpu[i].roll);\n        String(id[i] + \"/yaw\").toCharArray(msgId, 16); bundle.add(msgId).add(mpu[i].yaw);\n        String(id[i] + \"/pitch\").toCharArray(msgId, 16); bundle.add(msgId).add(mpu[i].pitch);\n      }\n      if (config.quaternion) {\n        String(id[i] + \"/q\").toCharArray(msgId, 16); bundle.add(msgId).add(q[0]).add(q[1]).add(q[2]).add(q[3]);\n      }\n    } // if ahrs or quaternion\n\n    if (config.temperature) {\n      mpu[i].tempCount = mpu[i].readTempData();  // Read the adc values\n      mpu[i].temperature = ((float) mpu[i].tempCount) / 333.87 + 21.0;  // Temperature in degrees Centigrade\n      String(id[i] + \"/temp\").toCharArray(msgId, 16); bundle.add(msgId).add(mpu[i].temperature);\n    }\n\n    // this section is in micros\n    Now = micros();\n    rate = 1000000.0f / (Now - Last[i]);\n    String(id[i] + \"/rate\").toCharArray(msgId, 16); bundle.add(msgId).add(rate);\n    String(id[i] + \"/time\").toCharArray(msgId, 16); bundle.add(msgId).add(Now / 1000000.0f);\n    Last[i] = Now;\n\n  } // for each sensor\n\n  // the previous section was in micros, the following section is in millis\n  Now = millis();\n\n  // Update the debug information every second, independent of data rates\n  if ((debugLevel > 0) && (Now - lastDisplay) > 1000) {\n\n    // blink green LED on and off every second\n    if (digitalRead(LED_G)) {\n      ledBlack();\n    }\n    else {\n      ledGreen();\n    }\n\n    if (debugLevel > 1) {\n      for (int i = 0; i < 1; i++) {\n        // Print acceleration values in milligs!\n        Serial.print(\"X-acceleration: \"); Serial.print(1000 * mpu[i].ax);\n        Serial.print(\" mg \");\n        Serial.print(\"Y-acceleration: \"); Serial.print(1000 * mpu[i].ay);\n        Serial.print(\" mg \");\n        Serial.print(\"Z-acceleration: \"); Serial.print(1000 * mpu[i].az);\n        Serial.println(\" mg \");\n\n        // Print gyro values in degree/sec\n        Serial.print(\"X-gyro rate: \"); Serial.print(mpu[i].gx, 3);\n        Serial.print(\" degrees/sec \");\n        Serial.print(\"Y-gyro rate: \"); Serial.print(mpu[i].gy, 3);\n        Serial.print(\" degrees/sec \");\n        Serial.print(\"Z-gyro rate: \"); Serial.print(mpu[i].gz, 3);\n        Serial.println(\" degrees/sec\");\n\n        // Print mag values in milliGauss\n        Serial.print(\"X-mag field: \"); Serial.print(mpu[i].mx);\n        Serial.print(\" mG \");\n        Serial.print(\"Y-mag field: \"); Serial.print(mpu[i].my);\n        Serial.print(\" mG \");\n        Serial.print(\"Z-mag field: \"); Serial.print(mpu[i].mz);\n        Serial.println(\" mG\");\n\n        if (config.ahrs) {\n          // Print AHRS values in degrees\n          Serial.print(\"Yaw, Pitch, Roll: \");\n          Serial.print(mpu[i].yaw, 2);\n          Serial.print(\", \");\n          Serial.print(mpu[i].pitch, 2);\n          Serial.print(\", \");\n          Serial.print(mpu[i].roll, 2);\n          Serial.println(\" degrees\");\n        } // if (config.ahrs)\n\n        if (config.temperature) {\n          Serial.print(\"Temperature: \");\n          Serial.print(mpu[i].temperature, 1);\n          Serial.println(\" degrees C\");\n        }\n      }\n    } // if debugLevel>1\n\n    lastDisplay = millis();\n  } // if debugLevel>0\n\n  measurementCount++;\n  if ((measurementCount % config.decimate) == 0) {\n    // only send every Nth measurement\n    outIp.fromString(config.destination);\n    Udp.beginPacket(outIp, config.port);\n    bundle.send(Udp);\n    Udp.endPacket();\n    bundle.empty();\n    lastTransmit = Now;\n    measurementCount = 0;\n  }\n\n#endif\n\n} // loop\n"
  },
  {
    "path": "esp8266_imu_osc/rgb_led.cpp",
    "content": "#include \"rgb_led.h\"\n\nvoid ledInit() {\n  pinMode(LED_R, OUTPUT);\n  pinMode(LED_G, OUTPUT);\n  pinMode(LED_B, OUTPUT);\n}\n\n#ifdef COMMON_ANODE\n  #define LED_ON  LOW\n  #define LED_OFF HIGH\n#else\n  #define LED_ON  HIGH\n  #define LED_OFF LOW\n#endif\n\nvoid ledRed() {\n  digitalWrite(LED_R, LED_ON);\n  digitalWrite(LED_G, LED_OFF);\n  digitalWrite(LED_B, LED_OFF);\n}\n\nvoid ledGreen() {\n  digitalWrite(LED_R, LED_OFF);\n  digitalWrite(LED_G, LED_ON);\n  digitalWrite(LED_B, LED_OFF);\n}\n\nvoid ledBlue() {\n  digitalWrite(LED_R, LED_OFF);\n  digitalWrite(LED_G, LED_OFF);\n  digitalWrite(LED_B, LED_ON);\n}\n\nvoid ledYellow() {\n  digitalWrite(LED_R, LED_ON);\n  digitalWrite(LED_G, LED_ON);\n  digitalWrite(LED_B, LED_OFF);\n}\n\nvoid ledMagenta() {\n  digitalWrite(LED_R, LED_ON);\n  digitalWrite(LED_G, LED_OFF);\n  digitalWrite(LED_B, LED_ON);\n}\n\nvoid ledCyan() {\n  digitalWrite(LED_R, LED_OFF);\n  digitalWrite(LED_G, LED_ON);\n  digitalWrite(LED_B, LED_ON);\n}\n\nvoid ledBlack() {\n  digitalWrite(LED_R, LED_OFF);\n  digitalWrite(LED_G, LED_OFF);\n  digitalWrite(LED_B, LED_OFF);\n}\n\nvoid ledWhite() {\n  digitalWrite(LED_R, LED_ON);\n  digitalWrite(LED_G, LED_ON);\n  digitalWrite(LED_B, LED_ON);\n}\n"
  },
  {
    "path": "esp8266_imu_osc/rgb_led.h",
    "content": "#ifndef _RGB_LED_H_\n#define _RGB_LED_H_\n\n#include <Arduino.h>\n\n#define LED_R D5\n#define LED_G D6\n#define LED_B D7\n\n// #define COMMON_ANODE\n\nvoid ledInit();\nvoid ledRed();\nvoid ledGreen();\nvoid ledBlue();\nvoid ledYellow();\nvoid ledMagenta();\nvoid ledCyan();\nvoid ledBlack();\nvoid ledWhite();\n\n#endif\n"
  },
  {
    "path": "esp8266_imu_osc/tca9548a.cpp",
    "content": "#include \"tca9548a.h\"\n\ntca9548a::tca9548a(void) {\n}\n\ntca9548a::~tca9548a(void) {\n}\n\nvoid tca9548a::select(uint8_t channel) {\n  if (channel > 7) return;\n  Wire.beginTransmission(TCA9548A_ADDRESS);\n  Wire.write(1 << channel);\n  Wire.endTransmission();\n}\n\nvoid tca9548a::disable(void) {\n  Wire.beginTransmission(TCA9548A_ADDRESS);\n  Wire.write(0);  // no channel selected\n  Wire.endTransmission();\n}\n\n"
  },
  {
    "path": "esp8266_imu_osc/tca9548a.h",
    "content": "#ifndef __TCA9548A_H_\n#define __TCA9548A_H_\n\n#include <Arduino.h>\n#include <Wire.h>\n\n#define TCA9548A_ADDRESS    0x70\n\nclass tca9548a {\n  public:\n    ~tca9548a(void);\n    tca9548a(void);\n    void select(uint8_t);\n    void disable();\n  private:\n    uint8_t _address;\n\n}; // class\n\n#endif\n"
  },
  {
    "path": "esp8266_imu_osc/webinterface.cpp",
    "content": "#include \"webinterface.h\"\n#include \"rgb_led.h\"\n\nConfig config;\nextern ESP8266WebServer server;\nextern unsigned long packetCounter;\n\n/***************************************************************************/\n\nstatic String getContentType(const String& path) {\n  if (path.endsWith(\".html\"))       return \"text/html\";\n  else if (path.endsWith(\".htm\"))   return \"text/html\";\n  else if (path.endsWith(\".css\"))   return \"text/css\";\n  else if (path.endsWith(\".txt\"))   return \"text/plain\";\n  else if (path.endsWith(\".js\"))    return \"application/javascript\";\n  else if (path.endsWith(\".png\"))   return \"image/png\";\n  else if (path.endsWith(\".gif\"))   return \"image/gif\";\n  else if (path.endsWith(\".jpg\"))   return \"image/jpeg\";\n  else if (path.endsWith(\".jpeg\"))  return \"image/jpeg\";\n  else if (path.endsWith(\".ico\"))   return \"image/x-icon\";\n  else if (path.endsWith(\".svg\"))   return \"image/svg+xml\";\n  else if (path.endsWith(\".xml\"))   return \"text/xml\";\n  else if (path.endsWith(\".pdf\"))   return \"application/pdf\";\n  else if (path.endsWith(\".zip\"))   return \"application/zip\";\n  else if (path.endsWith(\".gz\"))    return \"application/x-gzip\";\n  else if (path.endsWith(\".json\"))  return \"application/json\";\n  return \"application/octet-stream\";\n}\n\n/***************************************************************************/\n\nbool defaultConfig() {\n  config.sensors = 8;\n  config.decimate = 1;\n  config.calibrate = 0;\n  config.raw = 1;\n  config.ahrs = 0;\n  config.quaternion = 0;\n  config.temperature = 0;\n  strncpy(config.destination, \"192.168.1.34\", 32);\n  config.port = 8000;\n  return true;\n}\n\nbool loadConfig() {\n  Serial.println(\"loadConfig\");\n\n  File configFile = SPIFFS.open(\"/config.json\", \"r\");\n  if (!configFile) {\n    Serial.println(\"Failed to open config file\");\n    return false;\n  }\n\n  size_t size = configFile.size();\n  if (size > 1024) {\n    Serial.println(\"Config file size is too large\");\n    return false;\n  }\n\n  std::unique_ptr<char[]> buf(new char[size]);\n  configFile.readBytes(buf.get(), size);\n  configFile.close();\n\n  StaticJsonBuffer<300> jsonBuffer;\n  JsonObject& root = jsonBuffer.parseObject(buf.get());\n\n  if (!root.success()) {\n    Serial.println(\"Failed to parse config file\");\n    return false;\n  }\n\n  N_JSON_TO_CONFIG(sensors, \"sensors\");\n  N_JSON_TO_CONFIG(decimate, \"decimate\");\n  N_JSON_TO_CONFIG(calibrate, \"calibrate\");\n  N_JSON_TO_CONFIG(raw, \"raw\");\n  N_JSON_TO_CONFIG(ahrs, \"ahrs\");\n  N_JSON_TO_CONFIG(quaternion, \"quaternion\");\n  N_JSON_TO_CONFIG(temperature, \"temperature\");\n  S_JSON_TO_CONFIG(destination, \"destination\");\n  N_JSON_TO_CONFIG(port, \"port\");\n\n  return true;\n}\n\nbool saveConfig() {\n  Serial.println(\"saveConfig\");\n  StaticJsonBuffer<300> jsonBuffer;\n  JsonObject& root = jsonBuffer.createObject();\n\n  N_CONFIG_TO_JSON(sensors, \"sensors\");\n  N_CONFIG_TO_JSON(decimate, \"decimate\");\n  N_CONFIG_TO_JSON(calibrate, \"calibrate\");\n  N_CONFIG_TO_JSON(raw, \"raw\");\n  N_CONFIG_TO_JSON(ahrs, \"ahrs\");\n  N_CONFIG_TO_JSON(quaternion, \"quaternion\");\n  N_CONFIG_TO_JSON(temperature, \"temperature\");\n  S_CONFIG_TO_JSON(destination, \"destination\");\n  N_CONFIG_TO_JSON(port, \"port\");\n\n  File configFile = SPIFFS.open(\"/config.json\", \"w\");\n  if (!configFile) {\n    Serial.println(\"Failed to open config file for writing\");\n    return false;\n  }\n  else {\n    Serial.println(\"Writing to config file\");\n    root.printTo(configFile);\n    configFile.close();\n    return true;\n  }\n}\n\nvoid printRequest() {\n  String message = \"HTTP Request\\n\\n\";\n  message += \"URI: \";\n  message += server.uri();\n  message += \"\\nMethod: \";\n  message += (server.method() == HTTP_GET) ? \"GET\" : \"POST\";\n  message += \"\\nHeaders: \";\n  message += server.headers();\n  message += \"\\n\";\n  for (uint8_t i = 0; i < server.headers(); i++ ) {\n    message += \" \" + server.headerName(i) + \": \" + server.header(i) + \"\\n\";\n  }\n  message += \"\\nArguments: \";\n  message += server.args();\n  message += \"\\n\";\n  for (uint8_t i = 0; i < server.args(); i++) {\n    message += \" \" + server.argName(i) + \": \" + server.arg(i) + \"\\n\";\n  }\n  Serial.println(message);\n}\n\nvoid handleUpdate1() {\n  server.sendHeader(\"Connection\", \"close\");\n  server.sendHeader(\"Access-Control-Allow-Origin\", \"*\");\n  server.send(200, \"text/plain\", (Update.hasError()) ? \"FAIL\" : \"OK\");\n  ESP.restart();\n}\n\nvoid handleUpdate2() {\n  HTTPUpload& upload = server.upload();\n  if (upload.status == UPLOAD_FILE_START) {\n    Serial.setDebugOutput(true);\n    WiFiUDP::stopAll();\n    Serial.printf(\"Update: %s\\n\", upload.filename.c_str());\n    uint32_t maxSketchSpace = (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000;\n    if (!Update.begin(maxSketchSpace)) { //start with max available size\n      Update.printError(Serial);\n    }\n  } else if (upload.status == UPLOAD_FILE_WRITE) {\n    if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {\n      Update.printError(Serial);\n    }\n  } else if (upload.status == UPLOAD_FILE_END) {\n    if (Update.end(true)) { //true to set the size to the current progress\n      Serial.printf(\"Update Success: %u\\nRebooting...\\n\", upload.totalSize);\n    } else {\n      Update.printError(Serial);\n    }\n    Serial.setDebugOutput(false);\n  }\n  yield();\n}\n\nvoid handleDirList() {\n  Serial.println(\"handleDirList\");\n  String str = \"\";\n  Dir dir = SPIFFS.openDir(\"/\");\n  while (dir.next()) {\n    str += dir.fileName();\n    str += \" \";\n    str += dir.fileSize();\n    str += \" bytes\\r\\n\";\n  }\n  server.send(200, \"text/plain\", str);\n}\n\nvoid handleNotFound() {\n  Serial.print(\"handleNotFound: \");\n  Serial.println(server.uri());\n  if (SPIFFS.exists(server.uri())) {\n    handleStaticFile(server.uri());\n  }\n  else {\n    String message = \"File Not Found\\n\\n\";\n    message += \"URI: \";\n    message += server.uri();\n    message += \"\\nMethod: \";\n    message += (server.method() == HTTP_GET) ? \"GET\" : \"POST\";\n    message += \"\\nArguments: \";\n    message += server.args();\n    message += \"\\n\";\n    for (uint8_t i = 0; i < server.args(); i++) {\n      message += \" \" + server.argName(i) + \": \" + server.arg(i) + \"\\n\";\n    }\n    server.setContentLength(message.length());\n    server.send(404, \"text/plain\", message);\n  }\n}\n\nvoid handleRedirect(const char * filename) {\n  handleRedirect((String)filename);\n}\n\nvoid handleRedirect(String filename) {\n  Serial.println(\"handleRedirect: \" + filename);\n  server.sendHeader(\"Location\", filename, true);\n  server.setContentLength(0);\n  server.send(302, \"text/plain\", \"\");\n}\n\nbool handleStaticFile(const char * path) {\n  return handleStaticFile((String)path);\n}\n\nbool handleStaticFile(String path) {\n  Serial.println(\"handleStaticFile: \" + path);\n  String contentType = getContentType(path);            // Get the MIME type\n  if (SPIFFS.exists(path)) {                            // If the file exists\n    File file = SPIFFS.open(path, \"r\");                 // Open it\n    server.setContentLength(file.size());\n    server.streamFile(file, contentType);               // And send it to the client\n    file.close();                                       // Then close the file again\n    return true;\n  }\n  Serial.println(\"\\tFile Not Found\");\n  return false;                                         // If the file doesn't exist, return false\n}\n\nvoid handleJSON() {\n  // this gets called in response to either a PUT or a POST\n  Serial.println(\"handleJSON\");\n  printRequest();\n\n  if (server.hasArg(\"sensors\") || server.hasArg(\"decimate\") || server.hasArg(\"calibrate\") || server.hasArg(\"raw\") || server.hasArg(\"ahrs\") || server.hasArg(\"quaternion\") || server.hasArg(\"temperature\") || server.hasArg(\"destination\") || server.hasArg(\"port\")) {\n    // the body is key1=val1&key2=val2&key3=val3 and the ESP8266Webserver has already parsed it\n    N_KEYVAL_TO_CONFIG(sensors, \"sensors\");\n    N_KEYVAL_TO_CONFIG(decimate, \"decimate\");\n    N_KEYVAL_TO_CONFIG(calibrate, \"calibrate\");\n    N_KEYVAL_TO_CONFIG(raw, \"raw\");\n    N_KEYVAL_TO_CONFIG(ahrs, \"ahrs\");\n    N_KEYVAL_TO_CONFIG(quaternion, \"quaternion\");\n    N_KEYVAL_TO_CONFIG(temperature, \"temperature\");\n    S_KEYVAL_TO_CONFIG(destination, \"destination\");\n    N_KEYVAL_TO_CONFIG(port, \"port\");\n    handleStaticFile(\"/reload_success.html\");\n  }\n  else if (server.hasArg(\"plain\")) {\n    // parse the body as JSON object\n    StaticJsonBuffer<300> jsonBuffer;\n    JsonObject& root = jsonBuffer.parseObject(server.arg(\"plain\"));\n    if (!root.success()) {\n      handleStaticFile(\"/reload_failure.html\");\n      return;\n    }\n    N_JSON_TO_CONFIG(sensors, \"sensors\");\n    N_JSON_TO_CONFIG(decimate, \"decimate\");\n    N_JSON_TO_CONFIG(calibrate, \"calibrate\");\n    N_JSON_TO_CONFIG(raw, \"raw\");\n    N_JSON_TO_CONFIG(ahrs, \"ahrs\");\n    N_JSON_TO_CONFIG(quaternion, \"quaternion\");\n    N_JSON_TO_CONFIG(temperature, \"temperature\");\n    S_JSON_TO_CONFIG(destination, \"destination\");\n    N_JSON_TO_CONFIG(port, \"port\");\n    handleStaticFile(\"/reload_success.html\");\n  }\n  else {\n    handleStaticFile(\"/reload_failure.html\");\n    return; // do not save the configuration\n  }\n\n  saveConfig();\n\n  // blink five times, this takes 2 seconds\n  for (int i = 0; i < 5; i++) {\n    ledRed();\n    delay(200);\n    ledBlack();\n    delay(200);\n  }\n\n  // some of the settings require re-initialization\n  ESP.restart();\n}\n\nvoid handleFileUpload() { // upload a new file to the SPIFFS\n  File fsUploadFile;  // a File object to temporarily store the received file\n  HTTPUpload& upload = server.upload();\n  if (upload.status == UPLOAD_FILE_START) {\n    String filename = upload.filename;\n    if (!filename.startsWith(\"/\")) filename = \"/\" + filename;\n    Serial.print(\"handleFileUpload Name: \");\n    Serial.println(filename);\n    fsUploadFile = SPIFFS.open(filename, \"w\");            // Open the file for writing in SPIFFS (create if it doesn't exist)\n    filename = String();\n  } else if (upload.status == UPLOAD_FILE_WRITE) {\n    if (fsUploadFile)\n      fsUploadFile.write(upload.buf, upload.currentSize); // Write the received bytes to the file\n  } else if (upload.status == UPLOAD_FILE_END) {\n    if (fsUploadFile) {                                   // If the file was successfully created\n      fsUploadFile.close();                               // Close the file again\n      Serial.print(\"handleFileUpload Size: \");\n      Serial.println(upload.totalSize);\n      server.sendHeader(\"Location\", \"/success.html\");     // Redirect the client to the success page\n      server.send(303);\n    } else {\n      server.send(500, \"text/plain\", \"500: couldn't create file\");\n    }\n  }\n}\n"
  },
  {
    "path": "esp8266_imu_osc/webinterface.h",
    "content": "#ifndef _WEBINTERFACE_H_\n#define _WEBINTERFACE_H_\n\n#include <Arduino.h>\n#include <ArduinoJson.h>\n#include <string.h>\n#include <ESP8266WebServer.h>\n#include <WiFiUdp.h>\n#include <FS.h>\n\n#ifndef ARDUINOJSON_VERSION\n#error ArduinoJson version 5 not found, please include ArduinoJson.h in your .ino file\n#endif\n\n#if ARDUINOJSON_VERSION_MAJOR != 5\n#error ArduinoJson version 5 is required\n#endif\n\n/* these are for numbers */\n#define N_JSON_TO_CONFIG(x, y)   { if (root.containsKey(y)) { config.x = root[y]; } }\n#define N_CONFIG_TO_JSON(x, y)   { root.set(y, config.x); }\n#define N_KEYVAL_TO_CONFIG(x, y) { if (server.hasArg(y))    { String str = server.arg(y); config.x = str.toFloat(); } }\n\n/* these are for strings */\n#define S_JSON_TO_CONFIG(x, y)   { if (root.containsKey(y)) { strcpy(config.x, root[y]); } }\n#define S_CONFIG_TO_JSON(x, y)   { root.set(y, config.x); }\n#define S_KEYVAL_TO_CONFIG(x, y) { if (server.hasArg(y))    { String str = server.arg(y); strcpy(config.x, str.c_str()); } }\n\nstruct Config {\n  int sensors;\n  int decimate;\n  int calibrate;\n  int raw;\n  int ahrs;\n  int quaternion;\n  int temperature;\n  char destination[32];\n  int port;\n};\n\nextern Config config;\n\nbool defaultConfig(void);\nbool loadConfig(void);\nbool saveConfig(void);\n\nvoid handleUpdate1(void);\nvoid handleUpdate2(void);\nvoid handleDirList(void);\nvoid handleNotFound(void);\nvoid handleRedirect(String);\nvoid handleRedirect(const char *);\nbool handleStaticFile(String);\nbool handleStaticFile(const char *);\nvoid handleJSON();\nvoid handleFileUpload();    // https://tttapa.github.io/ESP8266/Chap12%20-%20Uploading%20to%20Server.html\n\n#endif\n"
  },
  {
    "path": "esp8266_inmp411/RunningStat.h",
    "content": "/*\n   See https://www.johndcook.com/blog/standard_deviation/\n*/\n\nclass RunningStat\n{\n  public:\n    RunningStat() : m_n(0) {}\n\n    void Clear()\n    {\n      m_n = 0;\n    }\n\n    void Push(double x)\n    {\n      m_n++;\n\n      // See Knuth TAOCP vol 2, 3rd edition, page 232\n      if (m_n == 1)\n      {\n        m_oldM = m_newM = x;\n        m_oldS = 0.0;\n      }\n      else\n      {\n        m_newM = m_oldM + (x - m_oldM) / m_n;\n        m_newS = m_oldS + (x - m_oldM) * (x - m_newM);\n\n        // set up for next iteration\n        m_oldM = m_newM;\n        m_oldS = m_newS;\n      }\n\n      if (m_n == 1) {\n        m_min = x;\n        m_max = x;\n      }\n      else {\n        m_min = (x < m_min ? x : m_min);\n        m_max = (x > m_max ? x : m_max);\n      }\n    }\n\n    int NumDataValues() const\n    {\n      return m_n;\n    }\n\n    double Mean() const\n    {\n      return (m_n > 0) ? m_newM : 0.0;\n    }\n\n    double Variance() const\n    {\n      return ( (m_n > 1) ? m_newS / (m_n - 1) : 0.0 );\n    }\n\n    double StandardDeviation() const\n    {\n      return sqrt( Variance() );\n    }\n\n    double Min() const\n    {\n      return m_min;\n    }\n\n    double Max() const\n    {\n      return m_max;\n    }\n\n  private:\n    int m_n;\n    double m_oldM, m_newM, m_oldS, m_newS, m_min, m_max;\n};\n"
  },
  {
    "path": "esp8266_inmp411/esp8266_inmp411.ino",
    "content": "/*\n   Sketch for ESP8266 board, like the WEMOS D1 mini pro\n   connected to a INMP411 I2S microphone\n\n   The *internal GPIO number* which is NOT NECESSARIALY the same as the pin numbers, are as follows:\n   I2SI_DATA     = GPIO12\n   IS2I_BCK      = GPIO13\n   I2SI_WS/LRCLK = GPIO14\n\n   See https://github.com/esp8266/Arduino/blob/master/libraries/esp8266/examples/I2SInput/I2SInput.ino\n\n*/\n\n#include <Arduino.h>\n#include <ESP8266WiFi.h>\n#include <I2S.h>\n\n#include \"secret.h\"\n#include \"RunningStat.h\"\n\nIPAddress serverAddress(192, 168, 1, 21);\nconst unsigned int serverPort = 4000;\nconst unsigned int recvPort = 4001;\n\nWiFiClient Tcp;\n\nunsigned long lastBlink = 0;\nunsigned long lastConnect = 0;\nunsigned long blinkTime = 250;\nbool connected = false;\nconst unsigned int sampleRate = 22050;\nconst unsigned int nMessage = 720;\nfloat smoothedMean = 0;\nbool meanInitialized = 0;\nconst float alpha = 10. / sampleRate; // if the sampling time dT is much smaller than the time constant T, then alpha=1/(T*sampleRate) and T=1/(alpha*sampleRate)\n\nstruct message_t {\n  uint32_t version = 1;\n  uint32_t id = 0;\n  uint32_t counter;\n  uint32_t samples;\n  int16_t data[nMessage];\n} message __attribute__((packed));\n\nRunningStat shortstat;\n\nvoid setup() {\n  Serial.begin(115200);\n\n  // initialize the status LED\n  pinMode(LED_BUILTIN, OUTPUT);\n  lastBlink = millis();\n\n  // set A0 LOW, this is connected to the L/R selection\n  pinMode(A0, OUTPUT);\n  digitalWrite(A0, LOW);\n\n  // set GPIO16 LOW and use that as GND, rather than the GND pin\n  // this allows for a cleaner routing of the wires\n  pinMode(16, OUTPUT);\n  digitalWrite(16, LOW);\n\n  Serial.println();\n  Serial.println();\n  Serial.print(\"Connecting to \");\n  Serial.println(ssid);\n\n  /* Explicitly set the ESP8266 to be a WiFi-client, otherwise, it by default,\n     would try to act as both a client and an access-point and could cause\n     network-issues with your other WiFi-devices on your WiFi-network. */\n  WiFi.mode(WIFI_STA);\n  WiFi.begin(ssid, password);\n\n  while (WiFi.status() != WL_CONNECTED) {\n    delay(500);\n    Serial.print(\".\");\n  }\n\n  Serial.println(\"\");\n  Serial.println(\"WiFi connected\");\n  Serial.println(\"IP address: \");\n  Serial.println(WiFi.localIP());\n\n  i2s_rxtx_begin(true, false); // Enable I2S RX\n  i2s_set_rate(sampleRate);\n\n  // use the last part of the IP address as identifier\n  message.id = WiFi.localIP()[3];\n\n} // setup\n\nvoid loop() {\n  int16_t l, r;\n\n  if ((millis() - lastBlink) > 2 * blinkTime) {\n    digitalWrite(LED_BUILTIN, LOW); // status LED on\n    lastBlink = millis();\n  }\n  else if ((millis() - lastBlink) > 1 * blinkTime) {\n    digitalWrite(LED_BUILTIN, HIGH); // status LED off\n  }\n\n  while (i2s_rx_available() && message.samples < nMessage) {\n    i2s_read_sample(&l, &r, true);\n    float value = l;\n\n    // compute a smooth running mean with https://en.wikipedia.org/wiki/Exponential_smoothing\n    if (meanInitialized) {\n      smoothedMean = alpha * value + (1 - alpha) * smoothedMean;\n    }\n    else {\n      smoothedMean = value;\n      meanInitialized = 1;\n    }\n    value -= smoothedMean;\n\n    /*\n           Serial.print(value);\n           Serial.println();\n    */\n\n    message.data[message.samples] = value; // this casts the value back to an int16\n    message.samples++;\n    shortstat.Push(value);\n  }\n\n  if (message.samples == nMessage) {\n\n    Serial.print(shortstat.Min());\n    Serial.print(\", \");\n    Serial.print(shortstat.Mean());\n    Serial.print(\", \");\n    Serial.println(shortstat.Max());\n    /*\n      Serial.print(message.version); Serial.print(\", \");\n      Serial.print(message.counter); Serial.print(\", \");\n      Serial.print(message.samples); Serial.print(\", \");\n      Serial.print(message.data[0]); Serial.println();\n    */\n\n    if  ((millis() - lastConnect) > 10000) {\n      // reconnect, but don't try to reconnect too often\n      lastConnect = millis();\n\n      // turn the status LED on\n      digitalWrite(LED_BUILTIN, LOW);\n      lastBlink = millis();\n\n      if (!connected) {\n        connected = Tcp.connect(serverAddress, serverPort);\n        if (connected) {\n          Serial.print(\"Connected to \");\n          Serial.println(serverAddress);\n        }\n        else {\n          Serial.print(\"Failed to connect to \");\n          Serial.println(serverAddress);\n        }\n      }\n    }\n\n    if (connected) {\n      blinkTime = 1000;\n      int count = Tcp.write((uint8_t *)(&message), sizeof(message));\n      connected = (count == sizeof(message));\n    }\n    else {\n      blinkTime = 250;\n    }\n\n    message.counter++;\n    message.samples = 0;\n    shortstat.Clear();\n  }\n\n} // loop\n"
  },
  {
    "path": "esp8266_p1_thingspeak/esp8266_p1_thingspeak.ino",
    "content": "/*\n   This sketch is designed for the P1 port of an ISKRA AM500 smart energy meter.\n   It runs on a Wemos D1 mini PRO that reads and parses the energy usage data and\n   subsequently sends it to http://thingspeak.com/\n\n   Pin 5 appears to behave as an https://en.wikipedia.org/wiki/Open_collector and\n   hence requires to be connected over a ~1kOhm resistor  to VCC of the ESP8266.\n\n   The Wemos D1 mini PRO includes a lithium battery charging circuit, which I \n   connected to a small LiPo battery. This allows running it from the low power that\n   the P1 port provides, as the battery takes care of the current spikes during \n   wifi transmission. The battery needed to be fully charged prior to connecting it \n   to the P1 port, otherwise too much current would be drawn and the P1 port would \n   switch off and on repeatedly.\n\n   See http://domoticx.com/p1-poort-slimme-meter-hardware/ and\n   http://domoticx.com/p1-poort-slimme-meter-hardware/\n\n   The schematic below shows how I wired the 6 pins of the RJ12 connector (on the left) \n   to the Wemos D1 mini PRO (on the right).\n\n   1---PWR_VCC-------------------5V\n   2-------RTS-------------------D1\n   3---DAT_GND-------------------GND\n   4--------NC-       +----------D2 = SoftwareSerial RX\n   5--------TX--------|---R670---3V3\n   6---PWR_GND-------------------GND\n\n*/\n\n#include <ESP8266WiFi.h>\n#include <SoftwareSerial.h>   // https://github.com/plerup/espsoftwareserial/\n#include <PubSubClient.h>     // https://github.com/knolleary/pubsubclient\n#include <dsmr.h>             // https://github.com/matthijskooijman/arduino-dsmr\n#include \"secret.h\"\n\n/* these are defined in secret.h \n  for the wifi\n    #define SSID    \"XXXXXXXXXXXXXXXX\"\n    #define PASS    \"XXXXXXXXXXXXXXXX\"\n  for thingspeak mqtt\n    #define CLIENTID \"XXXXXXXXXXXXXXXX\"\n    #define USERNAME \"XXXXXXXXXXXXXXXX\"\n    #define PASSWORD \"XXXXXXXXXXXXXXXX\"\n  for thingspeak http\n    #define CHANNEL \"XXXXXXXXXXXXXXXX\"\n    #define APIKEY  \"XXXXXXXXXXXXXXXX\"\n*/\n\nconst char* version    = __DATE__ \" / \" __TIME__;\nconst int8_t rxPin = D2;\nconst int8_t txPin = -1;\nconst int8_t requestPin = D1;\n\n#define BUFSIZE 1024\n\n// one of these options is to be selected: LOCAL is a local mqtt broker, MQTT is thingspeak mqtt, REST is thingspeak http\n// #define USE_LOCAL\n#define USE_MQTT\n// #define USE_REST\n\nSoftwareSerial MySerial;\nP1Reader reader(&MySerial, requestPin);\nWiFiClient wifi_client;\nPubSubClient mqtt_client(wifi_client);\n\n#if defined USE_LOCAL\nconst char* mqtt_server = \"192.168.1.16\";\nconst int mqtt_port = 1883;\n#elif defined USE_MQTT\nconst char* mqtt_server = \"mqtt3.thingspeak.com\";\nconst int mqtt_port = 1883;\n#elif defined USE_REST\n// this uses the server and port defined below\n#endif\n\nconst char* server = \"api.thingspeak.com\";\nconst int port = 80;\n\nconst unsigned long intervalTime = 20 * 1000; // post data every 20 seconds\nconst unsigned long durationLed = 1000;       // the status LED remains on for 1 second\nunsigned long resetCounter = 0, lastLed = 0, lastTime = 0;\n\nusing MyData = ParsedData <\n               /* String */ identification,\n               /* String */ p1_version,\n               /* String */ timestamp,\n               /* String */ equipment_id,\n               /* FixedValue */ energy_delivered_tariff1,\n               /* FixedValue */ energy_delivered_tariff2,\n               /* FixedValue */ energy_returned_tariff1,\n               /* FixedValue */ energy_returned_tariff2,\n               /* String */ electricity_tariff,\n               /* FixedValue */ power_delivered,\n               /* FixedValue */ power_returned,\n               /* FixedValue */ electricity_threshold,\n               /* uint8_t */ electricity_switch_position,\n               /* uint32_t */ electricity_failures,\n               /* uint32_t */ electricity_long_failures,\n               /* String */ electricity_failure_log,\n               /* uint32_t */ electricity_sags_l1,\n               /* uint32_t */ electricity_sags_l2,\n               /* uint32_t */ electricity_sags_l3,\n               /* uint32_t */ electricity_swells_l1,\n               /* uint32_t */ electricity_swells_l2,\n               /* uint32_t */ electricity_swells_l3,\n               /* String */ message_short,\n               /* String */ message_long,\n               /* FixedValue */ voltage_l1,\n               /* FixedValue */ voltage_l2,\n               /* FixedValue */ voltage_l3,\n               /* FixedValue */ current_l1,\n               /* FixedValue */ current_l2,\n               /* FixedValue */ current_l3,\n               /* FixedValue */ power_delivered_l1,\n               /* FixedValue */ power_delivered_l2,\n               /* FixedValue */ power_delivered_l3,\n               /* FixedValue */ power_returned_l1,\n               /* FixedValue */ power_returned_l2,\n               /* FixedValue */ power_returned_l3,\n               /* uint16_t */ gas_device_type,\n               /* String */ gas_equipment_id,\n               /* uint8_t */ gas_valve_position,\n               /* TimestampedFixedValue */ gas_delivered,\n               /* uint16_t /*/ thermal_device_type,\n               /* String */ thermal_equipment_id,\n               /* uint8_t */ thermal_valve_position,\n               /* TimestampedFixedValue */ thermal_delivered,\n               /* uint16_t */ water_device_type,\n               /* String */ water_equipment_id,\n               /* uint8_t */ water_valve_position,\n               /* TimestampedFixedValue */ water_delivered,\n               /* uint16_t */ slave_device_type,\n               /* String */ slave_equipment_id,\n               /* uint8_t */ slave_valve_position,\n               /* TimestampedFixedValue */ slave_delivered\n               >;\n\nMyData *globaldata;\n\n/*****************************************************************************/\n\nstruct Printer {\n  template<typename Item>\n  void apply(Item &i) {\n    if (i.present()) {\n      Serial.print(Item::name);\n      Serial.print(F(\": \"));\n      Serial.print(i.val());\n      Serial.print(Item::unit());\n      Serial.println();\n    }\n  }\n};\n\n/*****************************************************************************/\n\nvoid setup() {\n  Serial.begin(115200);\n  MySerial.begin(115200, SWSERIAL_8N1, rxPin, txPin, true, BUFSIZE);\n\n  pinMode(LED_BUILTIN, OUTPUT);\n\n  Serial.println();\n  Serial.print(\"\\n[esp2866_p1_thingspeak / \");\n  Serial.print(version);\n  Serial.println(\"]\");\n\n  Serial.println();\n  Serial.print(\"Connecting to \");\n  Serial.println(SSID);\n\n  WiFi.mode(WIFI_STA);\n  WiFi.begin(SSID, PASS);\n  delay(500);\n\n  while (WiFi.status() != WL_CONNECTED) {\n    Serial.print(\".\");\n    delay(500);\n  }\n\n  Serial.println();\n  Serial.println(\"WiFi connected\");\n  Serial.println(\"IP address: \");\n  Serial.println(WiFi.localIP());\n\n#if defined USE_LOCAL\n  mqtt_client.setServer(mqtt_server, mqtt_port);\n#elif defined USE_MQTT\n  mqtt_client.setServer(mqtt_server, mqtt_port);\n#endif\n\n  //Upon restart, fire off a one-off reading\n  reader.enable(true);\n  lastTime = millis();\n}\n\n/*****************************************************************************/\n\nvoid loop () {\n  // this object should be recreated on every call, otherwise the parsing fails\n  MyData localdata;\n  // share a pointer to the object to the sendThingspeak functions\n  globaldata = &localdata;\n\n  // Allow the reader to check the serial buffer regularly\n  reader.loop();\n\n  // Allow the MQTT client to do its things\n  mqtt_client.loop();\n\n  // Fire off a one-off reading if there is no message for some time\n  unsigned long now = millis();\n  if (now - lastTime > intervalTime) {\n    reader.enable(true);\n    lastTime = now;\n  }\n\n  // Switch the LED off, it was switched on when sending a HTTP or MQTT message\n  if (now - lastLed > durationLed) {\n    digitalWrite(LED_BUILTIN, HIGH);  // it is inverted\n    lastLed = now;\n  }\n\n  if (reader.available()) {\n    // A complete telegram has arrived\n    Serial.println(\"---raw telegram-------------------------------------------------\");\n    Serial.println(reader.raw());\n    lastTime = now;\n\n    String err;\n    if (reader.parse(&localdata, &err)) {\n      // Parse succesful, print result\n      Serial.println(\"---parse succesful---------------------------------------------\");\n      localdata.applyEach(Printer());\n#if defined USE_LOCAL\n      sendLOCALMQTT();\n#elif defined USE_MQTT\n      sendThingspeakMQTT();\n#elif defined USE_REST\n      sendThingspeakREST();\n#endif\n    }\n    else {\n      // Parser error, print error\n      Serial.println(\"---parse error-------------------------------------------------\");\n      Serial.println(err);\n    } // parse result ok\n  } // full telegram available\n} // loop\n\n/*****************************************************************************/\n\nvoid sendLOCALMQTT() {\n  while (!mqtt_client.connected()) {\n    Serial.print(\"Reconnecting to MQTT...\");\n    if (mqtt_client.connect(CLIENTID)) {\n      Serial.println(\" done!\");\n    }\n    else {\n      Serial.println(\" failed.\");\n      resetCounter++;\n      if (resetCounter >= 5 )\n        ESP.restart();\n      delay(5000);\n    }\n  }\n\n  if (mqtt_client.connected()) {\n    digitalWrite(LED_BUILTIN, LOW);  // it is inverted\n    lastLed = millis();\n\n    mqtt_client.publish (\"p1/power_delivered\",          String(globaldata->power_delivered).c_str());\n    mqtt_client.publish (\"p1/power_returned\",           String(globaldata->power_returned).c_str());\n    mqtt_client.publish (\"p1/gas_delivered\",            String(globaldata->gas_delivered).c_str());\n    mqtt_client.publish (\"p1/energy_delivered_tariff1\", String(globaldata->energy_delivered_tariff1).c_str());\n    mqtt_client.publish (\"p1/energy_delivered_tariff2\", String(globaldata->energy_delivered_tariff2).c_str());\n    mqtt_client.publish (\"p1/energy_returned_tariff1\",  String(globaldata->energy_returned_tariff1).c_str());\n    mqtt_client.publish (\"p1/energy_returned_tariff2\",  String(globaldata->energy_returned_tariff2).c_str());\n  }\n} // sendLOCALMQTT\n\n/*****************************************************************************/\n\nvoid sendThingspeakMQTT() {\n  while (!mqtt_client.connected()) {\n    Serial.print(\"Reconnecting to MQTT...\");\n    if (mqtt_client.connect(CLIENTID, USERNAME, PASSWORD)) {\n      Serial.println(\" done!\");\n    }\n    else {\n      Serial.println(\" failed.\");\n      resetCounter++;\n      if (resetCounter >= 5 )\n        ESP.restart();\n      delay(5000);\n    }\n  }\n\n  if (mqtt_client.connected()) {\n    digitalWrite(LED_BUILTIN, LOW);  // it is inverted\n    lastLed = millis();\n\n    // Construct MQTT message\n    String body = \"field1=\";\n    body += String(globaldata->power_delivered);\n    body += String(\"&field2=\");\n    body += String(globaldata->power_returned);\n    body += String(\"&field3=\");\n    body += String(globaldata->gas_delivered);\n    body += String(\"&field4=\");\n    body += String(globaldata->energy_delivered_tariff1);\n    body += String(\"&field5=\");\n    body += String(globaldata->energy_delivered_tariff2);\n    body += String(\"&field6=\");\n    body += String(globaldata->energy_returned_tariff1);\n    body += String(\"&field7=\");\n    body += String(globaldata->energy_returned_tariff2);\n\n    Serial.println(\"---mqtt message------------------------------------------------\");\n    Serial.println(body);\n    if (mqtt_client.publish(\"channels/\" CHANNEL \"/publish\", body.c_str()))\n      resetCounter = 0;\n  }\n} // sendThingspeakMQTT\n\n/*****************************************************************************/\n\nvoid sendThingspeakREST() {\n  if (wifi_client.connect(server, port)) {\n    digitalWrite(LED_BUILTIN, LOW);  // it is inverted\n    lastLed = millis();\n\n    // Construct request body\n    String body = \"field1=\";\n    body += String(globaldata->power_delivered);\n    body += String(\"&field2=\");\n    body += String(globaldata->power_returned);\n    body += String(\"&field3=\");\n    body += String(globaldata->gas_delivered);\n    body += String(\"&field4=\");\n    body += String(globaldata->energy_delivered_tariff1);\n    body += String(\"&field5=\");\n    body += String(globaldata->energy_delivered_tariff2);\n    body += String(\"&field6=\");\n    body += String(globaldata->energy_returned_tariff1);\n    body += String(\"&field7=\");\n    body += String(globaldata->energy_returned_tariff2);\n\n    Serial.println(\"---post url----------------------------------------------------\");\n    Serial.println(body);\n\n    // Send message to thingspeak\n    wifi_client.print(\"POST /update HTTP/1.1\\n\");\n    wifi_client.print(\"Host: \" + String(server) + \"\\n\");\n    wifi_client.print(\"Connection: close\\n\");\n    wifi_client.print(\"X-THINGSPEAKAPIKEY: \" + String(APIKEY) + \"\\n\");\n    wifi_client.print(\"Content-Type: application/x-www-form-urlencoded\\n\");\n    wifi_client.print(\"Content-Length: \" + String(body.length()) + \"\\n\");\n    wifi_client.print(\"\\n\");\n    wifi_client.print(body);\n    wifi_client.print(\"\\n\");\n\n    Serial.println(\"---server response---------------------------------------------\");\n    while (wifi_client.available()) {\n      char c = wifi_client.read();\n      Serial.print(c);\n    }\n\n    wifi_client.stop();\n    resetCounter = 0;\n  }\n  else {\n    Serial.println(\"REST connection Failed.\");\n    Serial.println();\n\n    resetCounter++;\n    if (resetCounter >= 5 ) {\n      ESP.restart();\n    }\n  }\n} // sendThingspeakREST\n"
  },
  {
    "path": "esp8266_polar_wearlink/README.md",
    "content": "# Overview\n\nThis is designed to link https://www.adafruit.com/product/1077 to http://www.eegsynth.org\n\n## SPIFFS for static files\n\nYou should not only write the firmware to the ESP8266 module, but also the static content for the web interface. The html, css and javascript files located in the data directory should be written to the SPIFS filesystem on the ESP8266. See for example http://esp8266.github.io/Arduino/versions/2.0.0/doc/filesystem.html and https://www.instructables.com/id/Using-ESP8266-SPIFFS for instructions.\n\nYou will get a \"file not found\" error if the firmware cannot access the data files.\n\n## Arduino ESP8266 filesystem uploader\n\nThis Arduino sketch includes a `data` directory with a number of files that should be uploaded to the ESP8266 using the [SPIFFS filesystem uploader](https://github.com/esp8266/arduino-esp8266fs-plugin) tool. At the moment (Feb 2024) the Arduino 2.x IDE does *not* support the SPIFFS filesystem uploader plugin. You have to use the Arduino 1.8.x IDE (recommended), or the command line utilities for uploading the data.\n"
  },
  {
    "path": "esp8266_polar_wearlink/data/config.json",
    "content": "{\n\t\"redis\": \"192.168.1.34\",\n\t\"port\": 6379,\n\t\"duration\": 100\n}\n"
  },
  {
    "path": "esp8266_polar_wearlink/data/hello.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Hello</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n<p>Hello kangaroo!</p>\n<img src=\"kangaroo.jpg\">\n</body>\n</html>\n"
  },
  {
    "path": "esp8266_polar_wearlink/data/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Index</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n<a href=\"/hello.html\">Hello</a></br>\n<a href=\"/monitor.html\">Monitor</a></br>\n<a href=\"/settings.html\">Change settings</a></br>\n<a href=\"/reconnect\">Reconnect WiFi</a></br>\n<a href=\"/defaults\">Default settings</a></br>\n<a href=\"/update.html\">Update firmware</a></br>\n<a href=\"/restart\">Restart hardware</a></br>\n</body>\n</html>\n"
  },
  {
    "path": "esp8266_polar_wearlink/data/monitor.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Monitor</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n\n<h1>Monitor</h1>\n\nFirmware version:\n<div id=\"version\" name=\"version\">?</div>\n\nUptime:\n<div id=\"uptime\" name\"uptime\">?</div>\n\nHeart rate:\n<div id=\"heartrate\" name=\"heartrate\">?</div>\n\n<script>\n  async function updateContent() {\n    const response = await fetch(\"json\");\n    const data = await response.json();\n    console.log(data);\n    document.getElementById(\"version\").innerHTML = data[\"version\"];\n    document.getElementById(\"uptime\").innerHTML = data[\"uptime\"];\n    document.getElementById(\"heartrate\").innerHTML = data[\"heartrate\"];\n  }\n  updateContent();\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "esp8266_polar_wearlink/data/reload_failure.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Failure</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n\n<p>Failure</p>\n\n<script type=\"text/javascript\">\n  setTimeout(function(){location=\"/index.html\"},1000);\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "esp8266_polar_wearlink/data/reload_success.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Success</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n\n<p>Success</p>\n\n<script type=\"text/javascript\">\n  setTimeout(function(){location=\"/index.html\"},1000);\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "esp8266_polar_wearlink/data/settings.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Settings</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n\n<h1>Settings</h1>\n<form id=\"settings-form\" method=\"post\" action=\"json\">\n\n    <div class=\"field\">\n        <label for=\"redis\">redis:</label>\n        <input type=\"text\" id=\"redis\" name=\"redis\" value=\"?\" required>\n    </div>\n\n    <div class=\"field\">\n        <label for=\"port\">port:</label>\n        <input type=\"text\" id=\"port\" name=\"port\" value=\"?\" required>\n    </div>\n\n    <div class=\"field\">\n        <label for=\"duration\">duration:</label>\n        <input type=\"text\" id=\"duration\" name=\"duration\" value=\"?\" required>\n    </div>\n\n    <div class=\"field\">\n        <button type=\"submit\">Save</button>\n    </div>\n</form>\n\n<script>\n  async function updateContent() {\n    const response = await fetch(\"json\");\n    const data = await response.json();\n    console.log(data);\n    document.getElementById(\"var1\").value = data[\"var1\"];\n    document.getElementById(\"var2\").value = data[\"var2\"];\n    document.getElementById(\"var3\").value = data[\"var3\"];\n  }\n  updateContent();\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "esp8266_polar_wearlink/data/style.css",
    "content": ".c{\n    text-align: center;\n}\ndiv,input{\n    padding:5px;font-size:1em;\n}\ninput{\n    width:85%;\n}\nbody{\n    text-align: center;font-family:verdana;\n}\nbutton{\n    border:0;border-radius:0.3rem;background-color:#1fa3ec;color:#fff;line-height:2.4rem;font-size:1.2rem;width:100%;\n}\n.q{\n    float: right;width: 64px;text-align: right;\n}\n.l{\n    background: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAALVBMVEX///8EBwfBwsLw8PAzNjaCg4NTVVUjJiZDRUUUFxdiZGSho6OSk5Pg4eFydHTCjaf3AAAAZElEQVQ4je2NSw7AIAhEBamKn97/uMXEGBvozkWb9C2Zx4xzWykBhFAeYp9gkLyZE0zIMno9n4g19hmdY39scwqVkOXaxph0ZCXQcqxSpgQpONa59wkRDOL93eAXvimwlbPbwwVAegLS1HGfZAAAAABJRU5ErkJggg==\") no-repeat left center;background-size: 1em;\n}\n"
  },
  {
    "path": "esp8266_polar_wearlink/data/update.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Update</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n\n<h1>Update the ESP8266 firmware</h1>\n<form method='POST' action='/update' enctype='multipart/form-data'>\n\n<div class=\"field\">\n<input type='file' name='update'>\n</div>\n\n<div class=\"field\">\n<button type=\"submit\">Update</button>\n</div>\n\n</form>\n</body>\n</html>\n"
  },
  {
    "path": "esp8266_polar_wearlink/esp8266_polar_wearlink.ino",
    "content": "/*\n  This sketch is for an ESP8266 connected to the Polar WearLink+\n  receiver that comes with the Adafruit Polar T34 starter pack\n  https://www.adafruit.com/product/1077.\n\n  It uses WIFiManager for initial configuration and includes a web-interface\n  that allows to monitor and change parameters.\n\n  The status of the wifi connection, http server and received data\n  is indicated by an RDB led that blinks Red, Green or Blue.\n*/\n\n#include <Arduino.h>\n#include <ESP8266WiFi.h>         // https://github.com/esp8266/Arduino\n#include <ESP8266WebServer.h>\n#include <ESP8266mDNS.h>\n#include <WiFiManager.h>         // https://github.com/tzapu/WiFiManager\n#include <Wire.h>\n#include <Redis.h>               // https://github.com/remicaumette/esp8266-redis\n\n#include \"webinterface.h\"\n#include \"rgb_led.h\"\n\n// this allows some sections of the code to be disabled for debugging purposes\n#define ENABLE_WEBINTERFACE\n#define ENABLE_MDNS\n#define ENABLE_REDIS\n#define ENABLE_INTERRUPT\n\n// pointer to object is needed because the initialization is delayed\nRedis *redis_p = NULL;\n\n// this port and address are not used, they are taken from the config instead\n#define REDIS_ADDR      \"127.0.0.1\"\n#define REDIS_PORT      6379\n#define REDIS_PASSWORD  \"\"\n\nESP8266WebServer server(80);\nconst char* host = \"POLAR-WEARLINK\";\nconst char* version = __DATE__ \" / \" __TIME__;\n\n// keep track of the timing of the web interface\nlong tic_web = 0;\nlong tic_redis = 0;\nlong tic_heartbeat = 0;\n\nbool pulse = 0;\nfloat bpm = 0;\n\n#define INTERRUPT_PIN       13  // GPIO13 maps to pin D7\n#define INTERRUPT_DEBOUNCE  200 // milliseconds\nvolatile byte interruptCounter = 0;\n\n/************************************************************************************************/\n\nvoid interruptHandler() {\n  // prevent roll-around\n  if (interruptCounter < 255)\n    interruptCounter++;\n}\n\n/************************************************************************************************/\n\nvoid setup() {\n  Serial.begin(115200);\n  while (!Serial) {\n    ;\n  }\n\n  Serial.print(\"\\n[esp8266_polar_wearlink / \");\n  Serial.print(__DATE__ \" / \" __TIME__);\n  Serial.println(\"]\");\n\n  WiFi.hostname(host);\n  WiFi.begin();\n\n  SPIFFS.begin();\n\n  ledInit();\n\n#ifdef ENABLE_INTERRUPT\n  pinMode(INTERRUPT_PIN, INPUT_PULLUP);\n  attachInterrupt(digitalPinToInterrupt(INTERRUPT_PIN), interruptHandler, RISING);\n#endif\n\n  if (loadConfig()) {\n    ledYellow();\n    delay(1000);\n  }\n  else {\n    ledRed();\n    delay(1000);\n  }\n\n  if (WiFi.status() != WL_CONNECTED)\n    ledRed();\n\n  WiFiManager wifiManager;\n  wifiManager.setAPStaticIPConfig(IPAddress(192, 168, 1, 1), IPAddress(192, 168, 1, 1), IPAddress(255, 255, 255, 0));\n  wifiManager.autoConnect(host);\n  Serial.println(\"connected\");\n\n  if (WiFi.status() == WL_CONNECTED)\n    ledGreen();\n\n#ifdef ENABLE_WEBINTERFACE\n  // this serves all URIs that cannot be resolved to a file on the SPIFFS filesystem\n  server.onNotFound(handleNotFound);\n\n  server.on(\"/\", HTTP_GET, []() {\n    tic_web = millis();\n    handleRedirect(\"/index.html\");\n  });\n\n  server.on(\"/defaults\", HTTP_GET, []() {\n    tic_web = millis();\n    Serial.println(\"handleDefaults\");\n    handleStaticFile(\"/reload_success.html\");\n    defaultConfig();\n    saveConfig();\n    server.close();\n    server.stop();\n    ESP.restart();\n  });\n\n  server.on(\"/reconnect\", HTTP_GET, []() {\n    tic_web = millis();\n    Serial.println(\"handleReconnect\");\n    handleStaticFile(\"/reload_success.html\");\n    ledRed();\n    server.close();\n    server.stop();\n    delay(5000);\n    WiFiManager wifiManager;\n    wifiManager.resetSettings();\n    wifiManager.setAPStaticIPConfig(IPAddress(192, 168, 1, 1), IPAddress(192, 168, 1, 1), IPAddress(255, 255, 255, 0));\n    wifiManager.startConfigPortal(host);\n    Serial.println(\"connected\");\n    server.begin();\n    if (WiFi.status() == WL_CONNECTED)\n      ledGreen();\n  });\n\n  server.on(\"/restart\", HTTP_GET, []() {\n    tic_web = millis();\n    Serial.println(\"handleRestart\");\n    handleStaticFile(\"/reload_success.html\");\n    ledRed();\n    server.close();\n    server.stop();\n    SPIFFS.end();\n    delay(5000);\n    ESP.restart();\n  });\n\n  server.on(\"/dir\", HTTP_GET, [] {\n    tic_web = millis();\n    handleDirList();\n  });\n\n  server.on(\"/json\", HTTP_PUT, [] {\n    tic_web = millis();\n    handleJSON();\n  });\n\n  server.on(\"/json\", HTTP_POST, [] {\n    tic_web = millis();\n    handleJSON();\n  });\n\n  server.on(\"/json\", HTTP_GET, [] {\n    tic_web = millis();\n    StaticJsonBuffer<300> jsonBuffer;\n    JsonObject& root = jsonBuffer.createObject();\n    S_CONFIG_TO_JSON(redis, \"redis\");\n    N_CONFIG_TO_JSON(port, \"port\");\n    N_CONFIG_TO_JSON(duration, \"duration\");\n    root[\"heartrate\"] = bpm;\n    root[\"version\"] = version;\n    root[\"uptime\"]  = long(millis() / 1000);\n    String str;\n    root.printTo(str);\n    server.setContentLength(str.length());\n    server.send(200, \"application/json\", str);\n  });\n\n  server.on(\"/update\", HTTP_GET, [] {\n    tic_web = millis();\n    handleStaticFile(\"/update.html\");\n  });\n\n  server.on(\"/update\", HTTP_POST, handleUpdate1, handleUpdate2);\n\n  // start the web server\n  server.begin();\n#endif\n\n#ifdef ENABLE_MDNS\n  // announce the hostname and web server through zeroconf\n  MDNS.begin(host);\n  MDNS.addService(\"http\", \"tcp\", 80);\n#endif\n\n#ifdef ENABLE_REDIS\n  // pointer to object is needed because the initialization is delayed\n  Serial.print(\"redis = \");\n  Serial.println(config.redis);\n  Serial.print(\"port = \");\n  Serial.println(config.port);\n  Serial.print(\"duration = \");\n  Serial.println(config.duration);\n\n  Redis redis(config.redis, config.port);\n  redis.setNoDelay(false);\n  redis.setTimeout(100);\n\n  if (redis.begin(REDIS_PASSWORD)) {\n    Serial.println(\"Connected to the Redis server!\");\n    redis_p = &redis;\n  }\n  else {\n    Serial.println(\"Failed to connect to the Redis server!\");\n    redis_p = NULL;\n  }\n#endif\n\n  Serial.println(\"====================================================\");\n  Serial.println(\"Setup done\");\n  Serial.println(\"====================================================\");\n\n  while (true) {\n    // somehow the redis connection fails when control moves from setup() to loop()\n    // hence remain in the setup() function\n    loop();\n  }\n}\n\n/************************************************************************************************/\n\nvoid loop() {\n#ifdef ENABLE_WEBINTERFACE\n  server.handleClient();\n#endif\n\n#ifdef ENABLE_REDIS\n  if ((millis() - tic_redis) > 5000) {\n    tic_redis = millis();\n    String str(tic_redis);\n    char buf[32];\n    str.toCharArray(buf, 32);\n\n    if (redis_p != NULL)\n      redis_p->set(\"polar.millis\", buf);\n  }\n#endif\n\n#ifdef ENABLE_INTERRUPT\n  if (interruptCounter) {\n    if ((millis() - tic_heartbeat) > INTERRUPT_DEBOUNCE) {\n      long now = millis();\n      ledMagenta();\n      // skip the first beat, since the BPM cannot be computed\n      if (tic_heartbeat > 0) {\n        bpm = 60000. / (now - tic_heartbeat);\n        String str(bpm);\n        char buf[32];\n        str.toCharArray(buf, 32);\n\n        if (redis_p != NULL) {\n          redis_p->publish(\"polar.heartbeat\", buf);\n          redis_p->set(\"polar.heartrate\", buf);\n          redis_p->set(\"polar.heartbeat\", \"1\");\n          pulse = 1;\n        }\n      }\n      Serial.print(\"Heartbeat, BPM = \");\n      Serial.println(bpm);\n      tic_heartbeat = now;\n    } // if debounce\n    interruptCounter = 0;\n  } // if interruptCounter\n\n  if (pulse && ((millis() - tic_heartbeat) > config.duration)) {\n    pulse = 0;\n    if (redis_p != NULL) {\n      redis_p->set(\"polar.heartbeat\", \"0\");\n    }\n  }\n\n  if ((millis() - tic_heartbeat) > 100) {\n    // switch the LED back to the normal status color\n    if ((millis() - tic_web) < 5000)\n      ledBlue();\n    else if ((WiFi.status() == WL_CONNECTED) && (redis_p != NULL))\n      ledGreen();\n    else if ((WiFi.status() == WL_CONNECTED) && (redis_p == NULL))\n      ledCyan();\n    else\n      ledRed();\n  }\n#endif\n\n}\n"
  },
  {
    "path": "esp8266_polar_wearlink/rgb_led.cpp",
    "content": "#include \"rgb_led.h\"\n\nvoid ledInit() {\n  pinMode(LED_R, OUTPUT);\n  pinMode(LED_G, OUTPUT);\n  pinMode(LED_B, OUTPUT);\n}\n\n#ifdef COMMON_ANODE\n\nvoid ledRed() {\n  digitalWrite(LED_R, LOW);\n  digitalWrite(LED_G, HIGH);\n  digitalWrite(LED_B, HIGH);\n}\n\nvoid ledGreen() {\n  digitalWrite(LED_R, HIGH);\n  digitalWrite(LED_G, LOW);\n  digitalWrite(LED_B, HIGH);\n}\n\nvoid ledBlue() {\n  digitalWrite(LED_R, HIGH);\n  digitalWrite(LED_G, HIGH);\n  digitalWrite(LED_B, LOW);\n}\n\nvoid ledYellow() {\n  digitalWrite(LED_R, LOW);\n  digitalWrite(LED_G, LOW);\n  digitalWrite(LED_B, HIGH);\n}\n\nvoid ledMagenta() {\n  digitalWrite(LED_R, LOW);\n  digitalWrite(LED_G, HIGH);\n  digitalWrite(LED_B, LOW);\n}\n\nvoid ledCyan() {\n  digitalWrite(LED_R, HIGH);\n  digitalWrite(LED_G, LOW);\n  digitalWrite(LED_B, LOW);\n}\n\nvoid ledBlack() {\n  digitalWrite(LED_R, HIGH);\n  digitalWrite(LED_G, HIGH);\n  digitalWrite(LED_B, HIGH);\n}\n\nvoid ledWhite() {\n  digitalWrite(LED_R, LOW);\n  digitalWrite(LED_G, LOW);\n  digitalWrite(LED_B, LOW);\n}\n\n#else\n\nvoid ledRed() {\n  digitalWrite(LED_R, HIGH);\n  digitalWrite(LED_G, LOW);\n  digitalWrite(LED_B, LOW);\n}\n\nvoid ledGreen() {\n  digitalWrite(LED_R, LOW);\n  digitalWrite(LED_G, HIGH);\n  digitalWrite(LED_B, LOW);\n}\n\nvoid ledBlue() {\n  digitalWrite(LED_R, LOW);\n  digitalWrite(LED_G, LOW);\n  digitalWrite(LED_B, HIGH);\n}\n\nvoid ledYellow() {\n  digitalWrite(LED_R, HIGH);\n  digitalWrite(LED_G, HIGH);\n  digitalWrite(LED_B, LOW);\n}\n\nvoid ledMagenta() {\n  digitalWrite(LED_R, HIGH);\n  digitalWrite(LED_G, LOW);\n  digitalWrite(LED_B, HIGH);\n}\n\nvoid ledCyan() {\n  digitalWrite(LED_R, LOW);\n  digitalWrite(LED_G, HIGH);\n  digitalWrite(LED_B, HIGH);\n}\n\nvoid ledBlack() {\n  digitalWrite(LED_R, LOW);\n  digitalWrite(LED_G, LOW);\n  digitalWrite(LED_B, LOW);\n}\n\nvoid ledWhite() {\n  digitalWrite(LED_R, HIGH);\n  digitalWrite(LED_G, HIGH);\n  digitalWrite(LED_B, HIGH);\n}\n\n#endif\n"
  },
  {
    "path": "esp8266_polar_wearlink/rgb_led.h",
    "content": "#ifndef _RGB_LED_H_\n#define _RGB_LED_H_\n\n#include <Arduino.h>\n\n#define LED_R D1\n#define LED_G D2\n#define LED_B D3\n\n// #define COMMON_ANODE\n\nvoid ledInit();\nvoid ledRed();\nvoid ledGreen();\nvoid ledBlue();\nvoid ledYellow();\nvoid ledMagenta();\nvoid ledCyan();\nvoid ledBlack();\nvoid ledWhite();\n\n#endif\n"
  },
  {
    "path": "esp8266_polar_wearlink/webinterface.cpp",
    "content": "#include \"webinterface.h\"\n#include \"rgb_led.h\"\n\nConfig config;\nextern ESP8266WebServer server;\nextern unsigned long packetCounter;\n\n/***************************************************************************/\n\nstatic String getContentType(const String& path) {\n  if (path.endsWith(\".html\"))       return \"text/html\";\n  else if (path.endsWith(\".htm\"))   return \"text/html\";\n  else if (path.endsWith(\".css\"))   return \"text/css\";\n  else if (path.endsWith(\".txt\"))   return \"text/plain\";\n  else if (path.endsWith(\".js\"))    return \"application/javascript\";\n  else if (path.endsWith(\".png\"))   return \"image/png\";\n  else if (path.endsWith(\".gif\"))   return \"image/gif\";\n  else if (path.endsWith(\".jpg\"))   return \"image/jpeg\";\n  else if (path.endsWith(\".jpeg\"))  return \"image/jpeg\";\n  else if (path.endsWith(\".ico\"))   return \"image/x-icon\";\n  else if (path.endsWith(\".svg\"))   return \"image/svg+xml\";\n  else if (path.endsWith(\".xml\"))   return \"text/xml\";\n  else if (path.endsWith(\".pdf\"))   return \"application/pdf\";\n  else if (path.endsWith(\".zip\"))   return \"application/zip\";\n  else if (path.endsWith(\".gz\"))    return \"application/x-gzip\";\n  else if (path.endsWith(\".json\"))  return \"application/json\";\n  return \"application/octet-stream\";\n}\n\n/***************************************************************************/\n\nbool defaultConfig() {\n  Serial.println(\"loadConfig\");\n\n  strncpy(config.redis, \"192.168.1.34\", 32);\n  config.port = 6379;\n  config.duration = 100;\n  return true;\n}\n\nbool loadConfig() {\n  Serial.println(\"loadConfig\");\n\n  File configFile = SPIFFS.open(\"/config.json\", \"r\");\n  if (!configFile) {\n    Serial.println(\"Failed to open config file\");\n    return false;\n  }\n\n  size_t size = configFile.size();\n  if (size > 1024) {\n    Serial.println(\"Config file size is too large\");\n    return false;\n  }\n\n  std::unique_ptr<char[]> buf(new char[size]);\n  configFile.readBytes(buf.get(), size);\n  configFile.close();\n\n  StaticJsonBuffer<300> jsonBuffer;\n  JsonObject& root = jsonBuffer.parseObject(buf.get());\n\n  if (!root.success()) {\n    Serial.println(\"Failed to parse config file\");\n    return false;\n  }\n\n  S_JSON_TO_CONFIG(redis, \"redis\");\n  N_JSON_TO_CONFIG(port, \"port\");\n  N_JSON_TO_CONFIG(duration, \"duration\");\n\n  return true;\n}\n\nbool saveConfig() {\n  Serial.println(\"saveConfig\");\n  StaticJsonBuffer<300> jsonBuffer;\n  JsonObject& root = jsonBuffer.createObject();\n\n  S_CONFIG_TO_JSON(redis, \"redis\");\n  N_CONFIG_TO_JSON(port, \"port\");\n  N_CONFIG_TO_JSON(duration, \"duration\");\n\n  File configFile = SPIFFS.open(\"/config.json\", \"w\");\n  if (!configFile) {\n    Serial.println(\"Failed to open config file for writing\");\n    return false;\n  }\n  else {\n    Serial.println(\"Writing to config file\");\n    root.printTo(configFile);\n    configFile.close();\n    return true;\n  }\n}\n\nvoid printRequest() {\n  String message = \"HTTP Request\\n\\n\";\n  message += \"URI: \";\n  message += server.uri();\n  message += \"\\nMethod: \";\n  message += (server.method() == HTTP_GET) ? \"GET\" : \"POST\";\n  message += \"\\nHeaders: \";\n  message += server.headers();\n  message += \"\\n\";\n  for (uint8_t i = 0; i < server.headers(); i++ ) {\n    message += \" \" + server.headerName(i) + \": \" + server.header(i) + \"\\n\";\n  }\n  message += \"\\nArguments: \";\n  message += server.args();\n  message += \"\\n\";\n  for (uint8_t i = 0; i < server.args(); i++) {\n    message += \" \" + server.argName(i) + \": \" + server.arg(i) + \"\\n\";\n  }\n  Serial.println(message);\n}\n\nvoid handleUpdate1() {\n  server.sendHeader(\"Connection\", \"close\");\n  server.sendHeader(\"Access-Control-Allow-Origin\", \"*\");\n  server.send(200, \"text/plain\", (Update.hasError()) ? \"FAIL\" : \"OK\");\n  ESP.restart();\n}\n\nvoid handleUpdate2() {\n  HTTPUpload& upload = server.upload();\n  if (upload.status == UPLOAD_FILE_START) {\n    Serial.setDebugOutput(true);\n    WiFiUDP::stopAll();\n    Serial.printf(\"Update: %s\\n\", upload.filename.c_str());\n    uint32_t maxSketchSpace = (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000;\n    if (!Update.begin(maxSketchSpace)) { //start with max available size\n      Update.printError(Serial);\n    }\n  } else if (upload.status == UPLOAD_FILE_WRITE) {\n    if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {\n      Update.printError(Serial);\n    }\n  } else if (upload.status == UPLOAD_FILE_END) {\n    if (Update.end(true)) { //true to set the size to the current progress\n      Serial.printf(\"Update Success: %u\\nRebooting...\\n\", upload.totalSize);\n    } else {\n      Update.printError(Serial);\n    }\n    Serial.setDebugOutput(false);\n  }\n  yield();\n}\n\nvoid handleDirList() {\n  Serial.println(\"handleDirList\");\n  String str = \"\";\n  Dir dir = SPIFFS.openDir(\"/\");\n  while (dir.next()) {\n    str += dir.fileName();\n    str += \" \";\n    str += dir.fileSize();\n    str += \" bytes\\r\\n\";\n  }\n  server.send(200, \"text/plain\", str);\n}\n\nvoid handleNotFound() {\n  Serial.print(\"handleNotFound: \");\n  Serial.println(server.uri());\n  if (SPIFFS.exists(server.uri())) {\n    handleStaticFile(server.uri());\n  }\n  else {\n    String message = \"File Not Found\\n\\n\";\n    message += \"URI: \";\n    message += server.uri();\n    message += \"\\nMethod: \";\n    message += (server.method() == HTTP_GET) ? \"GET\" : \"POST\";\n    message += \"\\nArguments: \";\n    message += server.args();\n    message += \"\\n\";\n    for (uint8_t i = 0; i < server.args(); i++) {\n      message += \" \" + server.argName(i) + \": \" + server.arg(i) + \"\\n\";\n    }\n    server.setContentLength(message.length());\n    server.send(404, \"text/plain\", message);\n  }\n}\n\nvoid handleRedirect(const char * filename) {\n  handleRedirect((String)filename);\n}\n\nvoid handleRedirect(String filename) {\n  Serial.println(\"handleRedirect: \" + filename);\n  server.sendHeader(\"Location\", filename, true);\n  server.setContentLength(0);\n  server.send(302, \"text/plain\", \"\");\n}\n\nbool handleStaticFile(const char * path) {\n  return handleStaticFile((String)path);\n}\n\nbool handleStaticFile(String path) {\n  Serial.println(\"handleStaticFile: \" + path);\n  String contentType = getContentType(path);            // Get the MIME type\n  if (SPIFFS.exists(path)) {                            // If the file exists\n    File file = SPIFFS.open(path, \"r\");                 // Open it\n    server.setContentLength(file.size());\n    server.streamFile(file, contentType);               // And send it to the client\n    file.close();                                       // Then close the file again\n    return true;\n  }\n  Serial.println(\"\\tFile Not Found\");\n  return false;                                         // If the file doesn't exist, return false\n}\n\nvoid handleJSON() {\n  // this gets called in response to either a PUT or a POST\n  Serial.println(\"handleJSON\");\n  printRequest();\n\n  if (server.hasArg(\"redis\") || server.hasArg(\"port\") || server.hasArg(\"duration\")) {\n    // the body is key1=val1&key2=val2&key3=val3 and the ESP8266Webserver has already parsed it\n    S_KEYVAL_TO_CONFIG(redis, \"redis\");\n    N_KEYVAL_TO_CONFIG(port, \"port\");\n    N_KEYVAL_TO_CONFIG(duration, \"duration\");\n    handleStaticFile(\"/reload_success.html\");\n  }\n  else if (server.hasArg(\"plain\")) {\n    // parse the body as JSON object\n    StaticJsonBuffer<300> jsonBuffer;\n    JsonObject& root = jsonBuffer.parseObject(server.arg(\"plain\"));\n    if (!root.success()) {\n      handleStaticFile(\"/reload_failure.html\");\n      return;\n    }\n    S_JSON_TO_CONFIG(redis, \"redis\");\n    N_JSON_TO_CONFIG(port, \"port\");\n    N_JSON_TO_CONFIG(duration, \"duration\");\n    handleStaticFile(\"/reload_success.html\");\n  }\n  else {\n    handleStaticFile(\"/reload_failure.html\");\n    return; // do not save the configuration\n  }\n\n  saveConfig();\n\n  // blink five times\n  for (int i = 0; i < 5; i++) {\n    ledRed();\n    delay(200);\n    ledBlack();\n    delay(200);\n  }\n\n  // some of the settings require re-initialization\n  ESP.restart();\n}\n"
  },
  {
    "path": "esp8266_polar_wearlink/webinterface.h",
    "content": "#ifndef _WEBINTERFACE_H_\n#define _WEBINTERFACE_H_\n\n#include <Arduino.h>\n#include <ArduinoJson.h>\n#include <string.h>\n#include <ESP8266WebServer.h>\n#include <WiFiUdp.h>\n#include <FS.h>\n\n#ifndef ARDUINOJSON_VERSION\n#error ArduinoJson version 5 not found, please include ArduinoJson.h in your .ino file\n#endif\n\n#if ARDUINOJSON_VERSION_MAJOR != 5\n#error ArduinoJson version 5 is required\n#endif\n\n/* these are for numbers */\n#define N_JSON_TO_CONFIG(x, y)   { if (root.containsKey(y)) { config.x = root[y]; } }\n#define N_CONFIG_TO_JSON(x, y)   { root.set(y, config.x); }\n#define N_KEYVAL_TO_CONFIG(x, y) { if (server.hasArg(y))    { String str = server.arg(y); config.x = str.toFloat(); } }\n\n/* these are for strings */\n#define S_JSON_TO_CONFIG(x, y)   { if (root.containsKey(y)) { strcpy(config.x, root[y]); } }\n#define S_CONFIG_TO_JSON(x, y)   { root.set(y, config.x); }\n#define S_KEYVAL_TO_CONFIG(x, y) { if (server.hasArg(y))    { String str = server.arg(y); strcpy(config.x, str.c_str()); } }\n\nstruct Config {\n  char redis[32];\n  int port;\n  int duration; // in milliseconds\n};\n\nextern Config config;\n\nbool defaultConfig(void);\nbool loadConfig(void);\nbool saveConfig(void);\n\nvoid handleUpdate1(void);\nvoid handleUpdate2(void);\nvoid handleDirList(void);\nvoid handleNotFound(void);\nvoid handleRedirect(String);\nvoid handleRedirect(const char *);\nbool handleStaticFile(String);\nbool handleStaticFile(const char *);\nvoid handleJSON();\n\n#endif\n"
  },
  {
    "path": "esp8266_pulse_sensor/README.md",
    "content": "# Overview\n\nThis is an Arduino sketch for an ESP8266 module connected to a [PulseSensor](http://pulsesensor.com) heart beat sensor. It uses the ADC to sample the data and writes it as a continuous stream to a computer that is running a FieldTrip buffer.\n\nYou can use the webinterface to set the IP address and port of the receiving computer.\n\n## SPIFFS for static files\n\nYou should not only write the firmware to the ESP8266 module, but also the static content for the web interface. The html, css and javascript files located in the data directory should be written to the SPIFS filesystem on the ESP8266. See for example http://esp8266.github.io/Arduino/versions/2.0.0/doc/filesystem.html and https://www.instructables.com/id/Using-ESP8266-SPIFFS for instructions.\n\nYou will get a \"file not found\" error if the firmware cannot access the data files.\n\n## Arduino ESP8266 filesystem uploader\n\nThis Arduino sketch includes a `data` directory with a number of files that should be uploaded to the ESP8266 using the [SPIFFS filesystem uploader](https://github.com/esp8266/arduino-esp8266fs-plugin) tool. At the moment (Feb 2024) the Arduino 2.x IDE does *not* support the SPIFFS filesystem uploader plugin. You have to use the Arduino 1.8.x IDE (recommended), or the command line utilities for uploading the data.\n"
  },
  {
    "path": "esp8266_pulse_sensor/blink_led.cpp",
    "content": "#include \"blink_led.h\"\n\nTicker blinker;\n\nenum {\n  LED_ON,\n  LED_OFF,\n  LED_SLOW,\n  LED_MEDIUM,\n  LED_FAST,\n} ledState;\n\nvoid changeState() {\n  digitalWrite(LED, !(digitalRead(LED)));  // Invert the current state of the LED\n}\n\nvoid ledInit() {\n  pinMode(LED, OUTPUT);\n}\n\nvoid ledOn() {\n  if (ledState != LED_ON) {\n    ledState = LED_ON;\n    blinker.detach();\n    digitalWrite(LED, LOW);\n  }\n}\n\nvoid ledOff() {\n  if (ledState != LED_OFF) {\n    ledState = LED_OFF;\n    blinker.detach();\n    digitalWrite(LED, HIGH);\n  }\n}\n\nvoid ledSlow() {\n  if (ledState != LED_SLOW) {\n    ledState = LED_SLOW;\n    blinker.detach();\n    blinker.attach_ms(1000, changeState);\n  }\n}\n\nvoid ledMedium() {\n  if (ledState != LED_MEDIUM) {\n    ledState = LED_MEDIUM;\n    blinker.detach();\n    blinker.attach_ms(250, changeState);\n  }\n}\n\nvoid ledFast() {\n  if (ledState != LED_FAST) {\n    ledState = LED_FAST;\n    blinker.detach();\n    blinker.attach_ms(100, changeState);\n  }\n}\n"
  },
  {
    "path": "esp8266_pulse_sensor/blink_led.h",
    "content": "#ifndef _BLINK_LED_H_\n#define _BLINK_LED_H_\n\n#include <Arduino.h>\n#include <Ticker.h>\n\n#define LED 2 // GPIO2 is connected to the builtin LED\n\nvoid ledInit(void);\nvoid ledOn(void);\nvoid ledOff(void);\nvoid ledSlow(void);\nvoid ledMedium(void);\nvoid ledFast(void);\n\n#endif\n"
  },
  {
    "path": "esp8266_pulse_sensor/data/config.json",
    "content": "{\n  \"address\": \"192.168.1.34\",\n  \"port\": 1972\n}\n"
  },
  {
    "path": "esp8266_pulse_sensor/data/hello.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Hello</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n<p>Hello capibara!</p>\n<img src=\"capibara.png\">\n</body>\n</html>\n"
  },
  {
    "path": "esp8266_pulse_sensor/data/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Index</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n<a href=\"/hello.html\">Hello</a></br>\n<a href=\"/monitor.html\">Monitor</a></br>\n<a href=\"/settings.html\">Change settings</a></br>\n<a href=\"/reconnect\">Reconnect WiFi</a></br>\n<a href=\"/defaults\">Default settings</a></br>\n<a href=\"/update.html\">Update firmware</a></br>\n<a href=\"/restart\">Restart hardware</a></br>\n</body>\n</html>\n"
  },
  {
    "path": "esp8266_pulse_sensor/data/monitor.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Monitor</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n\n<h1>Monitor</h1>\n\nFirmware version:\n<div id=\"version\" name=\"version\">?</div>\n\nUptime:\n<div id=\"uptime\" name\"uptime\">?</div>\n\n<script>\n  async function updateContent() {\n    const response = await fetch(\"json\");\n    const data = await response.json();\n    console.log(data);\n    document.getElementById(\"version\").innerHTML = data[\"version\"];\n    document.getElementById(\"uptime\").innerHTML = data[\"uptime\"];\n  }\n  updateContent();\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "esp8266_pulse_sensor/data/reload_failure.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Failure</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n\n<p>Failure</p>\n\n<script type=\"text/javascript\">\n  setTimeout(function(){location=\"/index.html\"},1000);\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "esp8266_pulse_sensor/data/reload_success.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Success</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n\n<p>Success</p>\n\n<script type=\"text/javascript\">\n  setTimeout(function(){location=\"/index.html\"},1000);\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "esp8266_pulse_sensor/data/settings.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Settings</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n\n<h1>Settings</h1>\n<form id=\"settings-form\" method=\"post\" action=\"json\">\n\n    <div class=\"field\">\n        <label for=\"address\">address:</label>\n        <input type=\"text\" id=\"address\" name=\"address\" value=\"?\" required>\n    </div>\n\n    <div class=\"field\">\n        <label for=\"port\">port:</label>\n        <input type=\"text\" id=\"port\" name=\"port\" value=\"?\" required>\n    </div>\n\n    <div class=\"field\">\n        <button type=\"submit\">Save</button>\n    </div>\n</form>\n\n<script>\n  async function updateContent() {\n    const response = await fetch(\"json\");\n    const data = await response.json();\n    console.log(data);\n    document.getElementById(\"address\").value = data[\"address\"];\n    document.getElementById(\"port\").value = data[\"port\"];\n  }\n  updateContent();\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "esp8266_pulse_sensor/data/style.css",
    "content": ".c{\n    text-align: center;\n}\ndiv,input{\n    padding:5px;font-size:1em;\n}\ninput{\n    width:85%;\n}\nbody{\n    text-align: center;font-family:verdana;\n}\nbutton{\n    border:0;border-radius:0.3rem;background-color:#1fa3ec;color:#fff;line-height:2.4rem;font-size:1.2rem;width:100%;\n}\n.q{\n    float: right;width: 64px;text-align: right;\n}\n.l{\n    background: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAALVBMVEX///8EBwfBwsLw8PAzNjaCg4NTVVUjJiZDRUUUFxdiZGSho6OSk5Pg4eFydHTCjaf3AAAAZElEQVQ4je2NSw7AIAhEBamKn97/uMXEGBvozkWb9C2Zx4xzWykBhFAeYp9gkLyZE0zIMno9n4g19hmdY39scwqVkOXaxph0ZCXQcqxSpgQpONa59wkRDOL93eAXvimwlbPbwwVAegLS1HGfZAAAAABJRU5ErkJggg==\") no-repeat left center;background-size: 1em;\n}\n"
  },
  {
    "path": "esp8266_pulse_sensor/data/update.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>Update</title>\n<meta charset=\"utf-8\">\n<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n\n<h1>Update the ESP8266 firmware</h1>\n<form method='POST' action='/update' enctype='multipart/form-data'>\n\n<div class=\"field\">\n<input type='file' name='update'>\n</div>\n\n<div class=\"field\">\n<button type=\"submit\">Update</button>\n</div>\n\n</form>\n</body>\n</html>\n"
  },
  {
    "path": "esp8266_pulse_sensor/esp8266_pulse_sensor.ino",
    "content": "/*\n  This sketch is for an ESP8266 connected to an optical Pulse Sensor like this https://pulsesensor.com.\n  It sends the continuous signal with the Fieldtrip buffer protocol to a server.\n\n  It uses WIFiManager for initial configuration and includes a web-interface\n  that allows to monitor and change parameters.\n\n  The status of the wifi connection, http server and received data\n  is indicated by the builtin LED.\n*/\n\n#include <Arduino.h>\n#include <ESP8266WiFi.h>         // https://github.com/esp8266/Arduino\n#include <ESP8266WebServer.h>\n#include <ESP8266mDNS.h>\n#include <WiFiManager.h>         // https://github.com/tzapu/WiFiManager\n#include <Ticker.h>\n\n#include \"webinterface.h\"\n#include \"blink_led.h\"\n#include \"fieldtrip_buffer.h\"\n\n// this allows some sections of the code to be disabled for debugging purposes\n#define ENABLE_WEBINTERFACE\n#define ENABLE_MDNS\n#define ENABLE_BUFFER\n\nconst char* host = \"PULSE-SENSOR\";\nconst char* version = __DATE__ \" / \" __TIME__;\n\nESP8266WebServer server(80);\nTicker sampler;\n\n#define ADC       A0\n#define NCHANS    1\n#define FSAMPLE   200\n#define BLOCKSIZE (FSAMPLE/10)\n\nlong tic_web = 0;\nint sample = 0, block = 0;\nbool flush0 = false, flush1 = false;\nuint16_t block0[BLOCKSIZE], block1[BLOCKSIZE];\nint ftserver = 0, status = 0;\n\n/************************************************************************************************/\n\nvoid getSample() {\n  if (sample == BLOCKSIZE) {\n    // switch to the start of the other block\n    switch (block) {\n      case 0:\n        sample = 0;\n        flush0 = true;\n        block = 1;\n        break;\n      case 1:\n        sample = 0;\n        flush1 = true;\n        block = 0;\n        break;\n    }\n  }\n\n  // get the current ECG value, it is from a 10-bits ADC, value between 0 and 1023\n  uint16_t value = analogRead(ADC);\n\n  // store it in the active block\n  switch (block) {\n    case 0:\n      block0[sample++] = value;\n      break;\n    case 1:\n      block1[sample++] = value;\n      break;\n  }\n}\n\n/************************************************************************************************/\n\nvoid setup() {\n  Serial.begin(115200);\n  while (!Serial) {\n    ;\n  }\n\n  Serial.print(\"\\n[esp8266_pulse_sensor / \");\n  Serial.print(__DATE__ \" / \" __TIME__);\n  Serial.println(\"]\");\n\n  ledInit();\n\n  WiFi.hostname(host);\n  WiFi.begin();\n\n  SPIFFS.begin();\n\n  if (loadConfig()) {\n    ledSlow();\n    delay(1000);\n  }\n  else {\n    ledFast();\n    delay(1000);\n  }\n  Serial.println(config.address);\n  Serial.println(config.port);\n\n  if (WiFi.status() != WL_CONNECTED)\n    ledFast();\n\n  WiFiManager wifiManager;\n  wifiManager.setAPStaticIPConfig(IPAddress(192, 168, 1, 1), IPAddress(192, 168, 1, 1), IPAddress(255, 255, 255, 0));\n  wifiManager.autoConnect(host);\n  Serial.println(\"connected\");\n\n  if (WiFi.status() == WL_CONNECTED)\n    ledSlow();\n\n#ifdef ENABLE_WEBINTERFACE\n  // this serves all URIs that cannot be resolved to a file on the SPIFFS filesystem\n  server.onNotFound(handleNotFound);\n\n  server.on(\"/\", HTTP_GET, []() {\n    tic_web = millis();\n    handleRedirect(\"/index.html\");\n  });\n\n  server.on(\"/defaults\", HTTP_GET, []() {\n    tic_web = millis();\n    Serial.println(\"handleDefaults\");\n    handleStaticFile(\"/reload_success.html\");\n    defaultConfig();\n    saveConfig();\n    server.close();\n    server.stop();\n    ESP.restart();\n  });\n\n  server.on(\"/reconnect\", HTTP_GET, []() {\n    tic_web = millis();\n    Serial.println(\"handleReconnect\");\n    handleStaticFile(\"/reload_success.html\");\n    ledFast();\n    server.close();\n    server.stop();\n    delay(5000);\n    WiFiManager wifiManager;\n    wifiManager.resetSettings();\n    wifiManager.setAPStaticIPConfig(IPAddress(192, 168, 1, 1), IPAddress(192, 168, 1, 1), IPAddress(255, 255, 255, 0));\n    wifiManager.startConfigPortal(host);\n    Serial.println(\"connected\");\n    server.begin();\n    if (WiFi.status() == WL_CONNECTED)\n      ledSlow();\n  });\n\n  server.on(\"/restart\", HTTP_GET, []() {\n    tic_web = millis();\n    Serial.println(\"handleRestart\");\n    handleStaticFile(\"/reload_success.html\");\n    ledFast();\n    server.close();\n    server.stop();\n    SPIFFS.end();\n    delay(5000);\n    ESP.restart();\n  });\n\n  server.on(\"/dir\", HTTP_GET, [] {\n    tic_web = millis();\n    handleDirList();\n  });\n\n  server.on(\"/json\", HTTP_PUT, [] {\n    tic_web = millis();\n    handleJSON();\n  });\n\n  server.on(\"/json\", HTTP_POST, [] {\n    tic_web = millis();\n    handleJSON();\n  });\n\n  server.on(\"/json\", HTTP_GET, [] {\n    tic_web = millis();\n    StaticJsonBuffer<300> jsonBuffer;\n    JsonObject& root = jsonBuffer.createObject();\n    S_CONFIG_TO_JSON(address, \"address\");\n    N_CONFIG_TO_JSON(port, \"port\");\n    root[\"version\"] = version;\n    root[\"uptime\"]  = long(millis() / 1000);\n    String str;\n    root.printTo(str);\n    server.setContentLength(str.length());\n    server.send(200, \"application/json\", str);\n  });\n\n  server.on(\"/update\", HTTP_GET, [] {\n    tic_web = millis();\n    handleStaticFile(\"/update.html\");\n  });\n\n  server.on(\"/update\", HTTP_POST, handleUpdate1, handleUpdate2);\n\n  // start the web server\n  server.begin();\n#endif\n\n#ifdef ENABLE_MDNS\n  // announce the hostname and web server through zeroconf\n  MDNS.begin(host);\n  MDNS.addService(\"http\", \"tcp\", 80);\n#endif\n\n#ifdef ENABLE_BUFFER\n  Serial.print(\"Connecting to \");\n  Serial.print(config.address);\n  Serial.print(\" on port \");\n  Serial.println(config.port);\n\n  ftserver = fieldtrip_open_connection(config.address, config.port);\n  if (ftserver > 0) {\n    Serial.println(\"Connection opened\");\n    status = fieldtrip_write_header(ftserver, DATATYPE_UINT16, NCHANS, FSAMPLE);\n    if (status == 0)\n      Serial.println(\"Wrote header\");\n    else\n      Serial.println(\"Failed writing header\");\n  }\n  else {\n    Serial.println(\"Failed opening connection\");\n  }\n\n#endif\n\n  // start sampling\n  sampler.attach_ms(1000 / FSAMPLE, getSample);\n\n  Serial.println(\"====================================================\");\n  Serial.println(\"Setup done\");\n  Serial.println(\"====================================================\");\n\n  return;\n} // setup\n\n/************************************************************************************************/\n\nvoid loop() {\n\n#ifdef ENABLE_WEBINTERFACE\n  server.handleClient();\n#endif\n\n#ifdef ENABLE_BUFFER\n  byte *ptr = NULL;\n\n  if (flush0) {\n    ptr = (byte *)block0;\n    flush0 = false;\n  }\n  else if (flush1) {\n    ptr = (byte *)block1;\n    flush1 = false;\n  }\n\n  if (ptr) {\n    if (ftserver == 0) {\n      ftserver = fieldtrip_open_connection(config.address, config.port);\n      if (ftserver > 0)\n        Serial.println(\"Connection opened\");\n      else\n        Serial.println(\"Failed opening connection\");\n    }\n\n    if (ftserver > 0) {\n      status = fieldtrip_write_data(ftserver, DATATYPE_UINT16, NCHANS, BLOCKSIZE, ptr);\n      if (status == 0)\n        Serial.println(\"Wrote data\");\n      else {\n        Serial.println(\"Failed writing data\");\n        status = fieldtrip_close_connection(ftserver);\n        if (status == 0)\n          Serial.println(\"Connection closed\");\n        else\n          Serial.println(\"Failed closing connection\");\n        ftserver = 0;\n      }\n    }\n  }\n\n#endif\n\n  delay(10);\n  return;\n} // loop\n"
  },
  {
    "path": "esp8266_pulse_sensor/fieldtrip_buffer.cpp",
    "content": "#include \"fieldtrip_buffer.h\"\n\nWiFiClient client;\n\n#undef DEBUG\n\n/*******************************************************************************\n   OPEN CONNECTION\n   returns file descriptor that should be >0 on success\n *******************************************************************************/\nint fieldtrip_open_connection(const char *address, int port) {\n#ifdef DEBUG\n  Serial.println(\"fieldtrip_open_connection\");\n#endif\n  if (client.connect(address, port))\n    return 1;\n  else\n    return -1;\n};\n\n/*******************************************************************************\n   CLOSE CONNECTION\n   returns 0 on success\n *******************************************************************************/\nint fieldtrip_close_connection(int s) {\n#ifdef DEBUG\n  Serial.println(\"fieldtrip_close_connection\");\n#endif\n  client.stop();\n  return 0;\n};\n\n/*******************************************************************************\n   WRITE HEADER\n   returns 0 on success\n *******************************************************************************/\nint fieldtrip_write_header(int server, uint32_t datatype, uint32_t nchans, float fsample) {\n  int status;\n  messagedef_t *request  = NULL;\n  messagedef_t *response = NULL;\n  headerdef_t  *header   = NULL;\n\n#ifdef DEBUG\n  Serial.println(\"fieldtrip_write_header\");\n#endif\n\n  byte msg[8+24];\n  for (int i = 0; i < 8+24; i++)\n    msg[i] = 0;\n\n  request = (messagedef_t *)(msg + 0);\n  request->version = VERSION;\n  request->command = PUT_HDR;\n  request->bufsize = sizeof(headerdef_t);\n\n  header = (headerdef_t *)(msg + sizeof(messagedef_t)); // the first 8 bytes are version, command and bufsize\n  header->nchans     = nchans;\n  header->nsamples   = 0;\n  header->nevents    = 0;\n  header->fsample    = fsample;\n  header->data_type  = datatype;\n  header->bufsize    = 0;\n\n#ifdef DEBUG\n  Serial.print(\"msg =  \");\n  for (int i = 0; i < sizeof(messagedef_t) + sizeof(headerdef_t); i++) {\n    Serial.print(msg[i]);\n    Serial.print(\" \");\n  }\n  Serial.println();\n#endif\n\n  int n = 0;\n  n += client.write(msg, sizeof(messagedef_t) + sizeof(headerdef_t));\n  client.flush();\n\n#ifdef DEBUG\n  Serial.print(\"Wrote \");\n  Serial.print(n);\n  Serial.println(\" bytes\");\n#endif\n\n  if (n != sizeof(messagedef_t) + sizeof(headerdef_t))\n    status++;\n\n  // read and ignore whatever response we get\n  while (client.available())\n    client.read();\n\n  return status;\n};\n\n/*******************************************************************************\n   WRITE DATA\n   returns 0 on success\n *******************************************************************************/\nint fieldtrip_write_data(int server, uint32_t datatype, uint32_t nchans, uint32_t nsamples, byte *buffer) {\n  int status = 0;\n  messagedef_t *request  = NULL;\n  messagedef_t *response = NULL;\n  datadef_t    *data   = NULL;\n\n#ifdef DEBUG\n  Serial.println(\"fieldtrip_write_data\");\n#endif\n\n  byte msg[8+16];\n  for (int i = 0; i < 8+16; i++)\n    msg[i] = 0;\n\n  request = (messagedef_t *)(msg + 0);\n  request->version = VERSION;\n  request->command = PUT_DAT;\n  request->bufsize = sizeof(datadef_t) + nchans * nsamples * wordsize_from_type(datatype);\n\n  data = (datadef_t *)(msg + sizeof(messagedef_t)); // the first 8 bytes are version, command and bufsize\n  data->nchans     = nchans;\n  data->nsamples   = nsamples;\n  data->data_type  = datatype;\n  data->bufsize    = nchans * nsamples * wordsize_from_type(datatype);\n\n#ifdef DEBUG\n  Serial.print(\"msg =  \");\n  for (int i = 0; i < sizeof(messagedef_t) + sizeof(datadef_t); i++) {\n    Serial.print(msg[i]);\n    Serial.print(\" \");\n  }\n  Serial.println();\n#endif\n\n  int n = 0;\n  n += client.write(msg, sizeof(messagedef_t) + sizeof(datadef_t));\n  client.flush();\n  n += client.write(buffer, nchans * nsamples * wordsize_from_type(datatype));\n  client.flush();\n\n#ifdef DEBUG\n  Serial.print(\"Wrote \");\n  Serial.print(n);\n  Serial.println(\" bytes\");\n#endif\n\n  if (n != sizeof(messagedef_t) + sizeof(datadef_t) + nchans * nsamples * wordsize_from_type(datatype))\n    status++;\n\n  // read and ignore whatever response we get\n  while (client.available())\n    client.read();\n\n  return status;\n};\n\nint wordsize_from_type(uint32_t datatype) {\n  int wordsize = 0;\n  switch (datatype) {\n    case DATATYPE_CHAR:\n      wordsize = 1;\n      break;\n    case DATATYPE_UINT8:\n      wordsize = 1;\n      break;\n    case DATATYPE_UINT16:\n      wordsize = 2;\n      break;\n    case DATATYPE_UINT32:\n      wordsize = 4;\n      break;\n    case DATATYPE_UINT64:\n      wordsize = 8;\n      break;\n    case DATATYPE_INT8:\n      wordsize = 1;\n      break;\n    case DATATYPE_INT16:\n      wordsize = 2;\n      break;\n    case DATATYPE_INT32:\n      wordsize = 4;\n      break;\n    case DATATYPE_INT64:\n      wordsize = 8;\n      break;\n    case DATATYPE_FLOAT32:\n      wordsize = 4;\n      break;\n    case DATATYPE_FLOAT64:\n      wordsize = 8;\n      break;\n    }\n  return wordsize;\n};\n"
  },
  {
    "path": "esp8266_pulse_sensor/fieldtrip_buffer.h",
    "content": "#ifndef __BUFFER_H_\n#define __BUFFER_H_\n\n#include <WiFiClient.h>\n\n// definition of simplified interface functions, not all of them are implemented\nint fieldtrip_start_server(int port);\nint fieldtrip_open_connection(const char *hostname, int port);\nint fieldtrip_close_connection(int s);\nint fieldtrip_read_header(int server, uint32_t *datatype, uint32_t *nchans, float *fsample, uint32_t *nsamples, uint32_t *nevents);\nint fieldtrip_read_data(int server, uint32_t begsample, uint32_t endsample, byte *buffer);\nint fieldtrip_write_header(int server, uint32_t datatype, uint32_t nchans, float fsample);\nint fieldtrip_write_data(int server, uint32_t datatype, uint32_t nchans, uint32_t nsamples, byte *buffer);\nint fieldtrip_wait_data(int server, uint32_t nsamples, uint32_t nevents, uint32_t milliseconds);\nint wordsize_from_type(uint32_t datatype);\n\n// define the version of the message packet\n#define VERSION    (uint16_t)0x0001\n\n// these define the commands that can be used, which are split over the two available bytes\n#define PUT_HDR    (uint16_t)0x0101 /* decimal 257 */\n#define PUT_DAT    (uint16_t)0x0102 /* decimal 258 */\n#define PUT_EVT    (uint16_t)0x0103 /* decimal 259 */\n#define PUT_OK     (uint16_t)0x0104 /* decimal 260 */\n#define PUT_ERR    (uint16_t)0x0105 /* decimal 261 */\n\n#define GET_HDR    (uint16_t)0x0201 /* decimal 513 */\n#define GET_DAT    (uint16_t)0x0202 /* decimal 514 */\n#define GET_EVT    (uint16_t)0x0203 /* decimal 515 */\n#define GET_OK     (uint16_t)0x0204 /* decimal 516 */\n#define GET_ERR    (uint16_t)0x0205 /* decimal 517 */\n\n#define FLUSH_HDR  (uint16_t)0x0301 /* decimal 769 */\n#define FLUSH_DAT  (uint16_t)0x0302 /* decimal 770 */\n#define FLUSH_EVT  (uint16_t)0x0303 /* decimal 771 */\n#define FLUSH_OK   (uint16_t)0x0304 /* decimal 772 */\n#define FLUSH_ERR  (uint16_t)0x0305 /* decimal 773 */\n\n#define WAIT_DAT   (uint16_t)0x0402 /* decimal 1026 */\n#define WAIT_OK    (uint16_t)0x0404 /* decimal 1027 */\n#define WAIT_ERR   (uint16_t)0x0405 /* decimal 1028 */\n\n#define PUT_HDR_NORESPONSE (uint16_t)0x0501 /* decimal 1281 */\n#define PUT_DAT_NORESPONSE (uint16_t)0x0502 /* decimal 1282 */\n#define PUT_EVT_NORESPONSE (uint16_t)0x0503 /* decimal 1283 */\n\n// these are used in the data_t and event_t structure\n#define DATATYPE_CHAR    (uint32_t)0\n#define DATATYPE_UINT8   (uint32_t)1\n#define DATATYPE_UINT16  (uint32_t)2\n#define DATATYPE_UINT32  (uint32_t)3\n#define DATATYPE_UINT64  (uint32_t)4\n#define DATATYPE_INT8    (uint32_t)5\n#define DATATYPE_INT16   (uint32_t)6\n#define DATATYPE_INT32   (uint32_t)7\n#define DATATYPE_INT64   (uint32_t)8\n#define DATATYPE_FLOAT32 (uint32_t)9\n#define DATATYPE_FLOAT64 (uint32_t)10\n\n// a packet that is sent over the network should contain the following\ntypedef struct {\n  uint16_t version;   // see VERSION\n  uint16_t command;   // see PUT_xxx, GET_xxx and FLUSH_xxx\n  uint32_t bufsize;   // size of the buffer in bytes\n}\nmessagedef_t; // 8 bytes\n\n// the header definition is fixed, it can be followed by additional header chunks\ntypedef struct {\n  uint32_t  nchans;\n  uint32_t  nsamples;\n  uint32_t  nevents;\n  float     fsample;\n  uint32_t  data_type;\n  uint32_t  bufsize;   // size of the buffer in bytes\n}\nheaderdef_t; // 24 bytes\n\n// the data definition is fixed, it should be followed by additional data\ntypedef struct {\n  uint32_t nchans;\n  uint32_t nsamples;\n  uint32_t data_type;\n  uint32_t bufsize;   // size of the buffer in bytes\n}\ndatadef_t; // 16 bytes\n\n// the event definition is fixed, it should be followed by additional data\ntypedef struct {\n  uint32_t type_type;   /* usual would be DATATYPE_CHAR */\n  uint32_t type_numel;  /* length of the type string */\n  uint32_t value_type;\n  uint32_t value_numel;\n  int32_t  sample;\n  int32_t  offset;\n  int32_t  duration;\n  uint32_t bufsize;     /* size of the buffer in bytes */\n} eventdef_t; // 64 bytes\n\n#endif _BUFFER_H_\n"
  },
  {
    "path": "esp8266_pulse_sensor/webinterface.cpp",
    "content": "#include \"webinterface.h\"\n#include \"blink_led.h\"\n\nConfig config;\nextern ESP8266WebServer server;\n\n/***************************************************************************/\n\nstatic String getContentType(const String& path) {\n  if (path.endsWith(\".html\"))       return \"text/html\";\n  else if (path.endsWith(\".htm\"))   return \"text/html\";\n  else if (path.endsWith(\".css\"))   return \"text/css\";\n  else if (path.endsWith(\".txt\"))   return \"text/plain\";\n  else if (path.endsWith(\".js\"))    return \"application/javascript\";\n  else if (path.endsWith(\".png\"))   return \"image/png\";\n  else if (path.endsWith(\".gif\"))   return \"image/gif\";\n  else if (path.endsWith(\".jpg\"))   return \"image/jpeg\";\n  else if (path.endsWith(\".jpeg\"))  return \"image/jpeg\";\n  else if (path.endsWith(\".ico\"))   return \"image/x-icon\";\n  else if (path.endsWith(\".svg\"))   return \"image/svg+xml\";\n  else if (path.endsWith(\".xml\"))   return \"text/xml\";\n  else if (path.endsWith(\".pdf\"))   return \"application/pdf\";\n  else if (path.endsWith(\".zip\"))   return \"application/zip\";\n  else if (path.endsWith(\".gz\"))    return \"application/x-gzip\";\n  else if (path.endsWith(\".json\"))  return \"application/json\";\n  return \"application/octet-stream\";\n}\n\n/***************************************************************************/\n\nbool defaultConfig() {\n  Serial.println(\"defaultConfig\");\n  \n  strncpy(config.address, \"192.168.1.34\", 32);\n  config.port = 1972;\n  return true;\n}\n\nbool loadConfig() {\n  Serial.println(\"loadConfig\");\n\n  File configFile = SPIFFS.open(\"/config.json\", \"r\");\n  if (!configFile) {\n    Serial.println(\"Failed to open config file\");\n    return false;\n  }\n\n  size_t size = configFile.size();\n  if (size > 1024) {\n    Serial.println(\"Config file size is too large\");\n    return false;\n  }\n\n  std::unique_ptr<char[]> buf(new char[size]);\n  configFile.readBytes(buf.get(), size);\n  configFile.close();\n\n  StaticJsonBuffer<300> jsonBuffer;\n  JsonObject& root = jsonBuffer.parseObject(buf.get());\n\n  if (!root.success()) {\n    Serial.println(\"Failed to parse config file\");\n    return false;\n  }\n\n  S_JSON_TO_CONFIG(address, \"address\");\n  N_JSON_TO_CONFIG(port, \"port\");\n\n  return true;\n}\n\nbool saveConfig() {\n  Serial.println(\"saveConfig\");\n  \n  StaticJsonBuffer<300> jsonBuffer;\n  JsonObject& root = jsonBuffer.createObject();\n\n  S_CONFIG_TO_JSON(address, \"address\");\n  N_CONFIG_TO_JSON(port, \"port\");\n\n  File configFile = SPIFFS.open(\"/config.json\", \"w\");\n  if (!configFile) {\n    Serial.println(\"Failed to open config file for writing\");\n    return false;\n  }\n  else {\n    Serial.println(\"Writing to config file\");\n    root.printTo(configFile);\n    configFile.close();\n    return true;\n  }\n}\n\nvoid printRequest() {\n  String message = \"HTTP Request\\n\\n\";\n  message += \"URI: \";\n  message += server.uri();\n  message += \"\\nMethod: \";\n  message += (server.method() == HTTP_GET) ? \"GET\" : \"POST\";\n  message += \"\\nHeaders: \";\n  message += server.headers();\n  message += \"\\n\";\n  for (uint8_t i = 0; i < server.headers(); i++ ) {\n    message += \" \" + server.headerName(i) + \": \" + server.header(i) + \"\\n\";\n  }\n  message += \"\\nArguments: \";\n  message += server.args();\n  message += \"\\n\";\n  for (uint8_t i = 0; i < server.args(); i++) {\n    message += \" \" + server.argName(i) + \": \" + server.arg(i) + \"\\n\";\n  }\n  Serial.println(message);\n}\n\nvoid handleUpdate1() {\n  server.sendHeader(\"Connection\", \"close\");\n  server.sendHeader(\"Access-Control-Allow-Origin\", \"*\");\n  server.send(200, \"text/plain\", (Update.hasError()) ? \"FAIL\" : \"OK\");\n  ESP.restart();\n}\n\nvoid handleUpdate2() {\n  HTTPUpload& upload = server.upload();\n  if (upload.status == UPLOAD_FILE_START) {\n    Serial.setDebugOutput(true);\n    WiFiUDP::stopAll();\n    Serial.printf(\"Update: %s\\n\", upload.filename.c_str());\n    uint32_t maxSketchSpace = (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000;\n    if (!Update.begin(maxSketchSpace)) { //start with max available size\n      Update.printError(Serial);\n    }\n  } else if (upload.status == UPLOAD_FILE_WRITE) {\n    if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {\n      Update.printError(Serial);\n    }\n  } else if (upload.status == UPLOAD_FILE_END) {\n    if (Update.end(true)) { //true to set the size to the current progress\n      Serial.printf(\"Update Success: %u\\nRebooting...\\n\", upload.totalSize);\n    } else {\n      Update.printError(Serial);\n    }\n    Serial.setDebugOutput(false);\n  }\n  yield();\n}\n\nvoid handleDirList() {\n  Serial.println(\"handleDirList\");\n  String str = \"\";\n  Dir dir = SPIFFS.openDir(\"/\");\n  while (dir.next()) {\n    str += dir.fileName();\n    str += \" \";\n    str += dir.fileSize();\n    str += \" bytes\\r\\n\";\n  }\n  server.send(200, \"text/plain\", str);\n}\n\nvoid handleNotFound() {\n  Serial.print(\"handleNotFound: \");\n  Serial.println(server.uri());\n  if (SPIFFS.exists(server.uri())) {\n    handleStaticFile(server.uri());\n  }\n  else {\n    String message = \"File Not Found\\n\\n\";\n    message += \"URI: \";\n    message += server.uri();\n    message += \"\\nMethod: \";\n    message += (server.method() == HTTP_GET) ? \"GET\" : \"POST\";\n    message += \"\\nArguments: \";\n    message += server.args();\n    message += \"\\n\";\n    for (uint8_t i = 0; i < server.args(); i++) {\n      message += \" \" + server.argName(i) + \": \" + server.arg(i) + \"\\n\";\n    }\n    server.setContentLength(message.length());\n    server.send(404, \"text/plain\", message);\n  }\n}\n\nvoid handleRedirect(const char * filename) {\n  handleRedirect((String)filename);\n}\n\nvoid handleRedirect(String filename) {\n  Serial.println(\"handleRedirect: \" + filename);\n  server.sendHeader(\"Location\", filename, true);\n  server.setContentLength(0);\n  server.send(302, \"text/plain\", \"\");\n}\n\nbool handleStaticFile(const char * path) {\n  return handleStaticFile((String)path);\n}\n\nbool handleStaticFile(String path) {\n  Serial.println(\"handleStaticFile: \" + path);\n  String contentType = getContentType(path);            // Get the MIME type\n  if (SPIFFS.exists(path)) {                            // If the file exists\n    File file = SPIFFS.open(path, \"r\");                 // Open it\n    server.setContentLength(file.size());\n    server.streamFile(file, contentType);               // And send it to the client\n    file.close();                                       // Then close the file again\n    return true;\n  }\n  Serial.println(\"\\tFile Not Found\");\n  return false;                                         // If the file doesn't exist, return false\n}\n\nvoid handleJSON() {\n  // this gets called in response to either a PUT or a POST\n  Serial.println(\"handleJSON\");\n  printRequest();\n\n  if (server.hasArg(\"address\") || server.hasArg(\"port\")) {\n    // the body is key1=val1&key2=val2&key3=val3 and the ESP8266Webserver has already parsed it\n    S_KEYVAL_TO_CONFIG(address, \"address\");\n    N_KEYVAL_TO_CONFIG(port, \"port\");\n\n    handleStaticFile(\"/reload_success.html\");\n  }\n  else if (server.hasArg(\"plain\")) {\n    // parse the body as JSON object\n    StaticJsonBuffer<300> jsonBuffer;\n    JsonObject& root = jsonBuffer.parseObject(server.arg(\"plain\"));\n    if (!root.success()) {\n      handleStaticFile(\"/reload_failure.html\");\n      return;\n    }\n    S_JSON_TO_CONFIG(address, \"address\");\n    N_JSON_TO_CONFIG(port, \"port\");\n\n    handleStaticFile(\"/reload_success.html\");\n  }\n  else {\n    handleStaticFile(\"/reload_failure.html\");\n    return; // do not save the configuration\n  }\n \n  saveConfig();\n\n  // blink five times\n  for (int i = 0; i < 5; i++) {\n    ledOn();\n    delay(200);\n    ledOff();\n    delay(200);\n  }\n\n  // some of the settings require re-initialization\n  ESP.restart();\n}\n"
  },
  {
    "path": "esp8266_pulse_sensor/webinterface.h",
    "content": "#ifndef _WEBINTERFACE_H_\n#define _WEBINTERFACE_H_\n\n#include <Arduino.h>\n#include <ArduinoJson.h>\n#include <string.h>\n#include <ESP8266WebServer.h>\n#include <WiFiUdp.h>\n#include <FS.h>\n\n#ifndef ARDUINOJSON_VERSION\n#error ArduinoJson version 5 not found, please include ArduinoJson.h in your .ino file\n#endif\n\n#if ARDUINOJSON_VERSION_MAJOR != 5\n#error ArduinoJson version 5 is required\n#endif\n\n/* these are for numbers */\n#define N_JSON_TO_CONFIG(x, y)   { if (root.containsKey(y)) { config.x = root[y]; } }\n#define N_CONFIG_TO_JSON(x, y)   { root.set(y, config.x); }\n#define N_KEYVAL_TO_CONFIG(x, y) { if (server.hasArg(y))    { String str = server.arg(y); config.x = str.toFloat(); } }\n\n/* these are for strings */\n#define S_JSON_TO_CONFIG(x, y)   { if (root.containsKey(y)) { strcpy(config.x, root[y]); } }\n#define S_CONFIG_TO_JSON(x, y)   { root.set(y, config.x); }\n#define S_KEYVAL_TO_CONFIG(x, y) { if (server.hasArg(y))    { String str = server.arg(y); strcpy(config.x, str.c_str()); } }\n\nstruct Config {\n  char address[32];\n  int port;\n};\n\nextern Config config;\n\nbool defaultConfig(void);\nbool loadConfig(void);\nbool saveConfig(void);\n\nvoid handleUpdate1(void);\nvoid handleUpdate2(void);\nvoid handleDirList(void);\nvoid handleNotFound(void);\nvoid handleRedirect(String);\nvoid handleRedirect(const char *);\nbool handleStaticFile(String);\nbool handleStaticFile(const char *);\nvoid handleJSON();\n\n#endif\n"
  },
  {
    "path": "esp8266_thingspeak/esp8266_thingspeak.ino",
    "content": "#include <Arduino.h>\n#include <ESP8266WiFi.h>\n#include \"secret.h\"\n\n/* these are defined in secret.h\n   const char* SSID   = \"XXXXXXXXXXXXXXXX\";\n   const char* PASS   = \"XXXXXXXXXXXXXXXX\";\n   const char* APIKEY = \"XXXXXXXXXXXXXXXX\"; // Write API Key for my ThingSpeak Channel\n*/\n\nconst char* version    = __DATE__ \" / \" __TIME__;\n\nWiFiClient client;\n\nconst char* server = \"api.thingspeak.com\";\nconst int postingInterval = 20 * 1000; // post data every 20 seconds\nunsigned int resetCounter = 0;\n\n/*****************************************************************************/\n\nvoid setup () {\n  Serial.begin(115200);\n  Serial.print(\"\\n[esp2866_thingspeak / \");\n  Serial.print(version);\n  Serial.println(\"]\");\n\n  WiFi.mode(WIFI_STA);\n  WiFi.begin(SSID, PASS);\n  Serial.println(\"\");\n\n  // Wait for connection\n  int count = 0;\n  while (WiFi.status() != WL_CONNECTED) {\n    delay(1000);\n    if (count++ > 20) {\n      Serial.println(\"No WiFi connection, restart...\");\n      ESP.restart();\n    }\n    else\n      Serial.print(\".\");\n  }\n  Serial.println(\"\");\n  Serial.print(\"Connected to \");\n  Serial.println(SSID);\n  Serial.print(\"IP address: \");\n  Serial.println(WiFi.localIP());\n} // setup\n\n/*****************************************************************************/\n\nvoid loop() {\n  if (client.connect(server, 80)) {\n\n    // Measure Signal Strength (RSSI) of Wi-Fi connection\n    long rssi = WiFi.RSSI();\n    Serial.print(\"RSSI: \");\n    Serial.println(rssi);\n\n    // Construct API request body\n    String body = \"field1=\";\n    body += String(rssi);\n\n    client.print(\"POST /update HTTP/1.1\\n\");\n    client.print(\"Host: \" + String(server) + \"\\n\");\n    client.print(\"Connection: close\\n\");\n    client.print(\"X-THINGSPEAKAPIKEY: \" + String(APIKEY) + \"\\n\");\n    client.print(\"Content-Type: application/x-www-form-urlencoded\\n\");\n    client.print(\"Content-Length: \" + String(body.length()) + \"\\n\");\n    client.print(\"\\n\");\n    client.print(body);\n    client.print(\"\\n\");\n\n    Serial.println(\"------------------------------------------------------------\");\n    while (client.available()) {\n      char c = client.read();\n      Serial.print(c);\n    }\n    Serial.println(\"------------------------------------------------------------\");\n\n    client.stop();\n\n    // wait and then post again\n    delay(postingInterval);\n    resetCounter = 0;\n\n  }\n  else {\n    Serial.println(\"Connection Failed.\");\n    Serial.println();\n\n    resetCounter++;\n\n    if (resetCounter >= 5 ) {\n      ESP.restart();\n    }\n  }\n} // loop\n"
  },
  {
    "path": "m5dial_midi/colormap.h",
    "content": "#ifndef _COLORMAP_H_\n#define _COLORMAP_H_\n\n// This is the colorcet \"r2\" or \"rainbow2\" colormap, which goes from blue via green and orange to to red.\n// https://cmap-docs.readthedocs.io/en/latest/catalog/miscellaneous/colorcet:cet_r2/\n\nuint8_t r[128] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 20, 29, 36, 41, 46, 49, 52, 55, 57, 59, 60, 61, 62, 63, 63, 63, 63, 63, 63, 63, 65, 67, 70, 75, 80, 86, 91, 97, 102, 107, 112, 117, 122, 127, 132, 137, 141, 146, 151, 155, 160, 164, 169, 173, 178, 182, 186, 191, 195, 199, 204, 208, 212, 217, 221, 225, 229, 234, 238, 241, 244, 247, 249, 250, 251, 252, 253, 253, 253, 254, 254, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 254, 254, 254, 254, 253, 253, 253, 253};\nuint8_t g[128] = {52, 58, 63, 68, 72, 77, 81, 85, 89, 92, 96, 100, 103, 106, 109, 112, 115, 117, 120, 122, 124, 126, 128, 130, 132, 134, 136, 138, 140, 142, 144, 147, 149, 151, 153, 155, 157, 159, 161, 163, 165, 167, 168, 169, 171, 172, 173, 174, 176, 177, 178, 179, 180, 181, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 205, 205, 205, 204, 202, 200, 198, 195, 193, 190, 187, 185, 182, 179, 176, 174, 171, 168, 165, 162, 159, 157, 154, 151, 148, 145, 142, 139, 136, 133, 130, 126, 123, 120, 117, 113, 110, 106, 103, 99, 96, 92, 88, 84, 80, 75, 70, 66, 60, 55, 48};\nuint8_t b[128] = {245, 240, 234, 228, 223, 217, 212, 206, 200, 195, 189, 184, 178, 173, 167, 162, 157, 152, 147, 142, 137, 132, 128, 123, 118, 113, 108, 103, 98, 93, 88, 82, 77, 71, 65, 59, 52, 46, 39, 32, 26, 21, 17, 15, 15, 15, 16, 17, 18, 19, 20, 21, 22, 23, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 42, 43, 44, 44, 44, 44, 44, 43, 42, 41, 40, 39, 38, 37, 36, 34, 33, 32, 31, 30, 29, 28, 27, 25, 24, 23, 22, 21, 20, 19, 17, 16, 15, 14, 13, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 2, 1, 0, 0, 0, 0, 0, 0};\n\n#endif\n"
  },
  {
    "path": "m5dial_midi/m5dial_midi.ino",
    "content": "#include <M5Dial.h>\n//#include <Control_Surface.h>\n#include \"colormap.h\"\n\n#define clip(x, lo, hi) (x < lo ? lo : (x > hi ? hi : x))\n\nlong chan = 0;\nconst int NCHAN = 16;\nlong value[NCHAN];\nlong oldPosition;\n\n//USBMIDI_Interface midi;\n\nenum { CHAN,\n       VALUE } mode = CHAN;\n\nvoid updateMIDI() {\n//    MIDIAddress controller = {chan, Channel_1};\n//    midi.sendControlChange(controller, value[chan]);\n}\n\nvoid updateScreen() {\n  String s;\n  if (mode == CHAN) {\n    s = String(\"c \") + String(chan);\n  } else {\n    s = String(\"v \") + String(value[chan]);\n  }\n  Serial.println(s);\n  M5Dial.Display.clear();\n  M5Dial.Display.drawString(s,\n                            M5Dial.Display.width() / 2,\n                            M5Dial.Display.height() / 2);\n}\n\nvoid setup() {\n  auto cfg = M5.config();\n  Control_Surface.begin();  // Initialize the Control Surface\n  M5Dial.begin(cfg, true, false);\n  M5Dial.Display.setTextColor(GREEN);\n  M5Dial.Display.setTextDatum(middle_center);\n  M5Dial.Display.setTextFont(&fonts::Orbitron_Light_32);\n  M5Dial.Display.setTextSize(1);\n  oldPosition = M5Dial.Encoder.read();\n  for (int chan = 0; chan < NCHAN; chan++)\n    value[chan] = 0;\n}\n\nvoid loop() {\n  M5Dial.update();\n  Control_Surface.loop();  // Update the Control Surface\n\n  if (M5Dial.BtnA.wasPressed()) {\n    if (mode == CHAN)\n      mode = VALUE;\n    else\n      mode = CHAN;\n    updateScreen();\n  }\n\n  long newPosition = M5Dial.Encoder.read();\n  if (newPosition != oldPosition) {\n    if (mode == CHAN) {\n      chan += (newPosition - oldPosition);\n      chan = clip(chan, 0, NCHAN - 1);\n    } else {\n      value[chan] += (newPosition - oldPosition);\n      value[chan] = clip(value[chan], 0, 127);\n      updateMIDI();\n    }\n    updateScreen();\n    oldPosition = newPosition;\n  }\n}"
  },
  {
    "path": "m5nanoc6_angle8_midi/colormap.h",
    "content": "#ifndef _COLORMAP_H_\n#define _COLORMAP_H_\n\n// This is the colorcet \"r2\" or \"rainbow2\" colormap, which goes from blue via green and orange to red.\n// https://cmap-docs.readthedocs.io/en/latest/catalog/miscellaneous/colorcet:cet_r2/\n\nuint8_t r[128] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 20, 29, 36, 41, 46, 49, 52, 55, 57, 59, 60, 61, 62, 63, 63, 63, 63, 63, 63, 63, 65, 67, 70, 75, 80, 86, 91, 97, 102, 107, 112, 117, 122, 127, 132, 137, 141, 146, 151, 155, 160, 164, 169, 173, 178, 182, 186, 191, 195, 199, 204, 208, 212, 217, 221, 225, 229, 234, 238, 241, 244, 247, 249, 250, 251, 252, 253, 253, 253, 254, 254, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 254, 254, 254, 254, 253, 253, 253, 253};\nuint8_t g[128] = {52, 58, 63, 68, 72, 77, 81, 85, 89, 92, 96, 100, 103, 106, 109, 112, 115, 117, 120, 122, 124, 126, 128, 130, 132, 134, 136, 138, 140, 142, 144, 147, 149, 151, 153, 155, 157, 159, 161, 163, 165, 167, 168, 169, 171, 172, 173, 174, 176, 177, 178, 179, 180, 181, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 205, 205, 205, 204, 202, 200, 198, 195, 193, 190, 187, 185, 182, 179, 176, 174, 171, 168, 165, 162, 159, 157, 154, 151, 148, 145, 142, 139, 136, 133, 130, 126, 123, 120, 117, 113, 110, 106, 103, 99, 96, 92, 88, 84, 80, 75, 70, 66, 60, 55, 48};\nuint8_t b[128] = {245, 240, 234, 228, 223, 217, 212, 206, 200, 195, 189, 184, 178, 173, 167, 162, 157, 152, 147, 142, 137, 132, 128, 123, 118, 113, 108, 103, 98, 93, 88, 82, 77, 71, 65, 59, 52, 46, 39, 32, 26, 21, 17, 15, 15, 15, 16, 17, 18, 19, 20, 21, 22, 23, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 42, 43, 44, 44, 44, 44, 44, 43, 42, 41, 40, 39, 38, 37, 36, 34, 33, 32, 31, 30, 29, 28, 27, 25, 24, 23, 22, 21, 20, 19, 17, 16, 15, 14, 13, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 2, 1, 0, 0, 0, 0, 0, 0};\n\n#endif\n"
  },
  {
    "path": "m5nanoc6_angle8_midi/m5nanoc6_angle8_midi.ino",
    "content": "/*\n   This is a sketch for an M5NanoC6 or M5Dial controller connected to an 8Angle input module.\n   It provides MIDI control change (CC) messages over Bluetooth.\n\n   See https://robertoostenveld.nl/low-cost-8-channel-midi-controller/\n*/\n\n#include <M5Unified.h>        // https://github.com/m5stack/M5Unified\n#include <M5_ANGLE8.h>        // https://github.com/m5stack/M5Unit-8Angle\n#include <Control_Surface.h>  // https://github.com/tttapa/Control-Surface\n#include \"colormap.h\"\n\n// #define I2C_SDA G13 // for the M5Dial\n// #define I2C_SCL G15\n#define I2C_SDA 2  // for the M5NanoC6, they are listed as G1 and G2\n#define I2C_SCL 1\n#define I2C_ADDR 0x43\n#define I2C_SPEED 4000000L\n#define MIDI_DELAY 10  // do not send the MIDI messages too fast/frequent\n\nBluetoothMIDI_Interface midi;\nM5_ANGLE8 angle8;\n\nbool sw;\nuint16_t value[8];\n\nvoid updateSwitch(bool sw) {\n  uint32_t rgb = 0x0000FF00 * sw;  // green or black\n  angle8.setLEDColor(8, rgb, 64);\n}\n\nvoid updateValue(bool sw, uint8_t knob, uint8_t value) {\n  uint32_t rgb = (r[value] << 16) | (g[value] << 8) | (b[value] << 0);\n  angle8.setLEDColor(knob, rgb, 64);\n\n  // the switch allows selecting MIDI channel 1 or 2\n  if (sw == 0) {\n    MIDIAddress controller = { knob, Channel_1 };\n    midi.sendControlChange(controller, value);\n  } else {\n    MIDIAddress controller = { knob, Channel_2 };\n    midi.sendControlChange(controller, value);\n  }\n\n  Serial.print(sw);\n  Serial.print(\" \");\n  Serial.print(knob);\n  Serial.print(\" \");\n  Serial.println(value);\n  delay(MIDI_DELAY);\n}\n\nvoid setup() {\n  auto cfg = M5.config();\n  cfg.serial_baudrate = 115200;\n  M5.begin();\n  Serial.println(\"setup start\");\n\n  while (!angle8.begin(I2C_ADDR, I2C_SDA, I2C_SCL, I2C_SPEED)) {\n    Serial.println(\"angle8 connect error\");\n    delay(100);\n  }\n  Serial.println(\"angle8 connect OK\");\n\n  Control_Surface.begin();\n  esp_log_level_set(\"i2c.master\", ESP_LOG_NONE);  // see https://github.com/espressif/arduino-esp32/issues/11787\n\n  sw = angle8.getDigitalInput();\n  updateSwitch(sw);\n\n  for (uint8_t knob = 0; knob < 8; knob++) {\n    value[knob] = (255 - angle8.getAnalogInput(knob, _8bit)) / 2;\n    updateValue(sw, knob, value[knob]);\n  }\n\n  Serial.println(\"setup done\");\n}\n\nvoid loop() {\n  M5.update();\n  Control_Surface.loop();\n\n  bool newsw = angle8.getDigitalInput();\n  if (newsw != sw) {\n    updateSwitch(newsw);\n    sw = newsw;\n  }\n\n  for (uint8_t knob = 0; knob < ANGLE8_TOTAL_ADC; knob++) {\n    uint16_t newvalue = (255 - angle8.getAnalogInput(knob, _8bit)) / 2;\n    if (value[knob] != newvalue) {\n      value[knob] = newvalue;\n      updateValue(sw, knob, value[knob]);\n    }\n  }\n}\n"
  },
  {
    "path": "m5nanoc6_encoder8_midi/colormap.h",
    "content": "#ifndef _COLORMAP_H_\n#define _COLORMAP_H_\n\n// This is the colorcet \"r2\" or \"rainbow2\" colormap, which goes from blue via green and orange to red.\n// https://cmap-docs.readthedocs.io/en/latest/catalog/miscellaneous/colorcet:cet_r2/\n\nuint8_t r[128] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 20, 29, 36, 41, 46, 49, 52, 55, 57, 59, 60, 61, 62, 63, 63, 63, 63, 63, 63, 63, 65, 67, 70, 75, 80, 86, 91, 97, 102, 107, 112, 117, 122, 127, 132, 137, 141, 146, 151, 155, 160, 164, 169, 173, 178, 182, 186, 191, 195, 199, 204, 208, 212, 217, 221, 225, 229, 234, 238, 241, 244, 247, 249, 250, 251, 252, 253, 253, 253, 254, 254, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 254, 254, 254, 254, 253, 253, 253, 253};\nuint8_t g[128] = {52, 58, 63, 68, 72, 77, 81, 85, 89, 92, 96, 100, 103, 106, 109, 112, 115, 117, 120, 122, 124, 126, 128, 130, 132, 134, 136, 138, 140, 142, 144, 147, 149, 151, 153, 155, 157, 159, 161, 163, 165, 167, 168, 169, 171, 172, 173, 174, 176, 177, 178, 179, 180, 181, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 205, 205, 205, 204, 202, 200, 198, 195, 193, 190, 187, 185, 182, 179, 176, 174, 171, 168, 165, 162, 159, 157, 154, 151, 148, 145, 142, 139, 136, 133, 130, 126, 123, 120, 117, 113, 110, 106, 103, 99, 96, 92, 88, 84, 80, 75, 70, 66, 60, 55, 48};\nuint8_t b[128] = {245, 240, 234, 228, 223, 217, 212, 206, 200, 195, 189, 184, 178, 173, 167, 162, 157, 152, 147, 142, 137, 132, 128, 123, 118, 113, 108, 103, 98, 93, 88, 82, 77, 71, 65, 59, 52, 46, 39, 32, 26, 21, 17, 15, 15, 15, 16, 17, 18, 19, 20, 21, 22, 23, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 42, 43, 44, 44, 44, 44, 44, 43, 42, 41, 40, 39, 38, 37, 36, 34, 33, 32, 31, 30, 29, 28, 27, 25, 24, 23, 22, 21, 20, 19, 17, 16, 15, 14, 13, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 2, 1, 0, 0, 0, 0, 0, 0};\n\n#endif\n"
  },
  {
    "path": "m5nanoc6_encoder8_midi/m5nanoc6_encoder8_midi.ino",
    "content": "/*\n   This is a sketch for an M5NanoC6 or M5Dial controller connected to an 8Encoder input module.\n   It provides MIDI control change (CC) messages over Bluetooth.\n\n  See https://robertoostenveld.nl/low-cost-8-channel-midi-controller/\n*/\n\n#include <M5Unified.h>        // https://github.com/m5stack/M5Unified\n#include <UNIT_8ENCODER.h>    // https://github.com/m5stack/M5Unit-8Encoder\n#include <Control_Surface.h>  // https://github.com/tttapa/Control-Surface\n#include \"colormap.h\"\n\n// #define I2C_SDA G13 // for the M5Dial\n// #define I2C_SCL G15\n#define I2C_SDA 2  // for the M5NanoC6, they are listed as G1 and G2\n#define I2C_SCL 1\n#define I2C_ADDR 0x41\n#define I2C_SPEED 4000000L\n#define MIDI_DELAY 10  // do not send the MIDI messages too fast/frequent\n\nBluetoothMIDI_Interface midi;\nUNIT_8ENCODER encoder8;\n\nbool sw;\nint32_t value[8];\nbool button[8];\n\nvoid updateSwitch(bool sw) {\n  uint32_t rgb = 0x0000FF00 * sw;  // green or black\n  encoder8.setLEDColor(8, rgb);\n}\n\nvoid updateValue(bool sw, uint8_t knob, int32_t value, bool button) {\n  uint8_t rb = (r[value] / 4), gb = g[value] / 4, bb = b[value] / 4;  // reduce the brightness\n  uint32_t rgb = (rb << 16) | (gb << 8) | (bb << 0);\n  encoder8.setLEDColor(knob, rgb);\n\n  // the switch allows selecting MIDI channel 1 or 2\n  if (sw == 0) {\n    MIDIAddress controller = { knob, Channel_1 };\n    midi.sendControlChange(controller, value);\n  } else {\n    MIDIAddress controller = { knob, Channel_2 };\n    midi.sendControlChange(controller, value);\n  }\n\n  Serial.print(sw);\n  Serial.print(\" \");\n  Serial.print(knob);\n  Serial.print(\" \");\n  Serial.print(value);\n  Serial.print(\" \");\n  Serial.println(button);\n  delay(MIDI_DELAY);\n}\n\nvoid setup() {\n  auto cfg = M5.config();\n  cfg.serial_baudrate = 115200;\n  M5.begin();\n\n  Serial.println(\"setup start\");\n  while (!encoder8.begin(&Wire, I2C_ADDR, I2C_SDA, I2C_SCL, I2C_SPEED)) {\n    Serial.println(\"encoder8 connect error\");\n    delay(100);\n  }\n  Serial.println(\"encoder8 connect OK\");\n\n  Control_Surface.begin();\n  esp_log_level_set(\"i2c.master\", ESP_LOG_NONE);  // see https://github.com/espressif/arduino-esp32/issues/11787\n\n  sw = encoder8.getSwitchStatus();\n  updateSwitch(sw);\n\n  for (uint8_t knob = 0; knob < 8; knob++) {\n    value[knob] = 0;\n    button[knob] = false;\n    encoder8.setEncoderValue(knob, value[knob]);\n    updateValue(sw, knob, value[knob], button[knob]);\n  }\n\n  Serial.println(\"setup done\");\n}\n\nvoid loop() {\n  M5.update();\n  Control_Surface.loop();\n\n  bool newsw = encoder8.getSwitchStatus();\n  if (newsw != sw) {\n    updateSwitch(newsw);\n    sw = newsw;\n  }\n\n  for (uint8_t knob = 0; knob < 8; knob++) {\n    bool newbutton = !encoder8.getButtonStatus(knob);  // the pushbutton seems to be inverted\n    if (button[knob] != newbutton) {\n      button[knob] = newbutton;\n      updateValue(sw, knob, value[knob], button[knob]);\n    }\n  }\n\n  for (uint8_t knob = 0; knob < 8; knob++) {\n    int32_t newvalue = encoder8.getEncoderValue(knob);\n    if (value[knob] != newvalue) {\n      if (newvalue < 0) {\n        newvalue = 0;\n        encoder8.setEncoderValue(knob, newvalue);\n      } else if (newvalue > 127) {\n        newvalue = 127;\n        encoder8.setEncoderValue(knob, newvalue);\n      }\n      value[knob] = newvalue;\n      updateValue(sw, knob, value[knob], button[knob]);\n    }\n  }\n}\n"
  },
  {
    "path": "rfm12b_recv_xxxx/README.md",
    "content": "# Arduino relay between RFM12b and ThingSpeak\n\nThis is a sketch for an Arduino pro mini attached to an RFM12b module. It operates in conjunction with the \"rfm12b_thingspeak\" sketch, which is running on an Arduino Uno with an Ethershield. The two Arduino's exchange sensor data over a i2c connection.\n\nSee http://robertoostenveld.nl/arduino-relay-rfm12b-thingspeak/ for details.\n"
  },
  {
    "path": "rfm12b_recv_xxxx/rfm12b_recv_xxxx.ino",
    "content": "#include <RF12.h>          // https://github.com/jcw/jeelib\n#include <Wire.h>\n\n#define BLIP_DEBUG\n#define BLIP_NODE 1  // set this to a unique ID to disambiguate multiple nodes\n#define BLIP_GRP  17 // wireless net group to use for sending blips\n\n#define RF_CHIPSELECT   10\n\n#define getbyte1(x) (x>> 0 & 0xff)\n#define getbyte2(x) (x>> 8 & 0xff)\n#define getbyte3(x) (x>>16 & 0xff)\n#define getbyte4(x) (x>>24 & 0xff)\n\ntypedef struct payload_t {\n  unsigned long id;\n  unsigned long counter;\n  float value1;\n  float value2;\n  float value3;\n  float value4;\n  float value5;\n  unsigned long crc;\n};\n\npayload_t payload;\n\n/*******************************************************************************************************************/\n\nvoid setup () {\n#ifdef BLIP_DEBUG\n  Serial.begin(57600);\n  Serial.print(\"\\n[rfm12b_recv_xxxx / \");\n  Serial.print(__DATE__);\n  Serial.print(\" / \");\n  Serial.print(__TIME__);\n  Serial.println(\"]\");\n#endif\n\n  Wire.begin(); // Start I2C Bus as Master\n\n  pinMode(RF_CHIPSELECT, OUTPUT);\n  rf12_set_cs(RF_CHIPSELECT);\n  rf12_initialize(BLIP_NODE, RF12_868MHZ, BLIP_GRP);\n}\n\n/*******************************************************************************************************************/\n\nvoid loop () {\n\n  // volatile byte rf12_hdr  - Contains the header byte of the received packet - with flag bits and node ID of either the sender or the receiver.\n  // volatile byte rf12_len  - The number of data bytes in the packet. A value in the range 0 .. 66.\n  // volatile byte rf12_data - A pointer to the received data.\n  // volatile byte rf12_crc  - CRC of the received packet, zero indicates correct reception. If != 0 then rf12_hdr, rf12_len, and rf12_data should not be relied upon.\n\n  if (rf12_recvDone()) {\n    if (RF12_WANTS_ACK) {\n      rf12_sendStart(RF12_ACK_REPLY, 0, 0);\n    }\n\n    if (rf12_crc != 0) {\n      Serial.println(\"Invalid crc\");\n    }\n    else if (rf12_len != sizeof payload) {\n      Serial.println(\"Invalid len\");\n    }\n    else {\n      memcpy(&payload, (void *)rf12_data, sizeof payload);\n\n      unsigned long check = crc_buf((char *)&payload, sizeof(payload_t) - sizeof(unsigned long));\n      \n      if (payload.crc == check) {\n        write_i2c((byte *)&payload.id,      sizeof(unsigned long));\n        write_i2c((byte *)&payload.counter, sizeof(unsigned long));\n        write_i2c((byte *)&payload.value1,  sizeof(float));\n        write_i2c((byte *)&payload.value2,  sizeof(float));\n        write_i2c((byte *)&payload.value3,  sizeof(float));\n        write_i2c((byte *)&payload.value4,  sizeof(float));\n        write_i2c((byte *)&payload.value5,  sizeof(float));\n        write_i2c((byte *)&payload.crc,     sizeof(unsigned long));\n      }\n      else {\n        Serial.print(\"checksum mismatch \");\n        Serial.print(payload.crc);\n        Serial.print(\" != \");\n        Serial.println(check);\n      }\n\n      // DISPLAY DATA\n      Serial.print(payload.id);\n      Serial.print(\",\\t\");\n      Serial.print(payload.counter);\n      Serial.print(\",\\t\");\n      Serial.print(payload.value1, 2);\n      Serial.print(\",\\t\");\n      Serial.print(payload.value2, 2);\n      Serial.print(\",\\t\");\n      Serial.print(payload.value3, 2);\n      Serial.print(\",\\t\");\n      Serial.print(payload.value4, 2);\n      Serial.print(\",\\t\");\n      Serial.print(payload.value5, 2);\n      Serial.print(\",\\t\");\n      Serial.println(payload.crc);\n    }\n  }\n} // loop\n\n/*******************************************************************************************************************/\n\nvoid write_i2c(byte *b, int n) {\n  Wire.beginTransmission(9); // transmit to device #9\n  for (i = 0; i < n; i++)\n    Wire.write(b[i]);\n  Wire.endTransmission();    // stop transmitting\n  delay(5); // otherwise the next transmission hangs\n}\n\n/*******************************************************************************************************************/\n\nstatic PROGMEM prog_uint32_t crc_table[16] = {\n  0x00000000, 0x1db71064, 0x3b6e20c8, 0x26d930ac,\n  0x76dc4190, 0x6b6b51f4, 0x4db26158, 0x5005713c,\n  0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c,\n  0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c\n};\n\nunsigned long crc_update(unsigned long crc, byte data)\n{\n  byte tbl_idx;\n  tbl_idx = crc ^ (data >> (0 * 4));\n  crc = pgm_read_dword_near(crc_table + (tbl_idx & 0x0f)) ^ (crc >> 4);\n  tbl_idx = crc ^ (data >> (1 * 4));\n  crc = pgm_read_dword_near(crc_table + (tbl_idx & 0x0f)) ^ (crc >> 4);\n  return crc;\n}\n\nunsigned long crc_buf(char *b, long l)\n{\n  unsigned long crc = ~0L;\n  for (unsigned long i = 0; i < l; i++)\n    crc = crc_update(crc, ((char *)b)[i]);\n  crc = ~crc;\n  return crc;\n}\n\nunsigned long crc_string(char *s)\n{\n  unsigned long crc = ~0L;\n  while (*s)\n    crc = crc_update(crc, *s++);\n  crc = ~crc;\n  return crc;\n}\n"
  },
  {
    "path": "rfm12b_send_am2301/README.md",
    "content": "# Arduino with AM2301 temperature and humidity sensor\n\nSee http://robertoostenveld.nl/arduino-am2301/ for details.\n"
  },
  {
    "path": "rfm12b_send_am2301/rfm12b_send_am2301.ino",
    "content": "#include <JeeLib.h>        // https://github.com/jcw/jeelib\n#include <dht.h>           // http://arduino.cc/playground/Main/DHTLib\n\n#define BLIP_DEBUG\n#define BLIP_NODE x   // set this to a unique ID to disambiguate multiple nodes\n#define BLIP_GRP  17  // wireless net group to use for sending blips\n\n#define DHT21_PIN     3\n#define SEND_MODE     2   // set to 3 if fuses are e=06/h=DE/l=CE, else set to 2\n\n//#define RF_CHIPSELECT   9\n#define RF_CHIPSELECT   10\n\n// this must be defined since we're using the watchdog for low-power waiting\nISR(WDT_vect) {\n  Sleepy::watchdogEvent();\n}\n\ndht DHT;\n\ntypedef struct message_t {\n  unsigned long id;\n  unsigned long counter;\n  float value1;\n  float value2;\n  float value3;\n  float value4;\n  float value5;\n  unsigned long crc;\n};\n\nmessage_t message;\nfloat previous1 = 0, previous2 = 0, previous3 = 0;\n\n/*****************************************************************************/\n\nvoid setup () {\n#ifdef BLIP_DEBUG\n  Serial.begin(57600);\n  Serial.print(\"\\n[rfm12b_send_am2301 / \");\n  Serial.print(__DATE__);\n  Serial.print(\" / \");\n  Serial.print(__TIME__);\n  Serial.println(\"]\");\n#endif\n\n  pinMode(RF_CHIPSELECT, OUTPUT);\n  rf12_set_cs(RF_CHIPSELECT);\n  rf12_initialize(BLIP_NODE, RF12_868MHZ, BLIP_GRP);\n  rf12_sleep(RF12_SLEEP);\n\n  delay(100); // give it some time to send before falling asleep\n\n  message.id          = BLIP_NODE;\n  message.counter     = 0;\n  message.value1      = NAN;\n  message.value2      = NAN;\n  message.value3      = NAN;\n  message.value4      = NAN;\n  message.value5      = NAN;\n}\n\n/*****************************************************************************/\n\nvoid loop () {\n  boolean stable = false;\n\n  while (!stable) {\n\n    // READ DATA\n    int chk = DHT.read21(DHT21_PIN);\n#ifdef BLIP_DEBUG\n    Serial.print(\"DHT21, \\t\");\n    switch (chk)\n    {\n    case DHTLIB_OK:\n      Serial.println(\"OK\\t\");\n      break;\n    case DHTLIB_ERROR_CHECKSUM:\n      Serial.println(\"Checksum error\\t\");\n      break;\n    case DHTLIB_ERROR_TIMEOUT:\n      Serial.println(\"Time out error\\t\");\n      break;\n    default:\n      Serial.println(\"Unknown error\\t\");\n      break;\n    }\n#endif\n\n    message.value1    = 0.001*readVcc(); // this is in mV, we want V\n    message.value2    = DHT.temperature; // this is in Celcius\n    message.value3    = DHT.humidity;    // this is in %\n\n    // compute the difference with the previous values\n    previous1 -= message.value1;\n    previous1 = abs(previous1);\n    previous2 -= message.value2;\n    previous2 = abs(previous2);\n    previous3 -= message.value3;\n    previous3 = abs(previous3);\n\n    // the voltage should be stable within 0.1, the temperature within 1, the humidity within 5\n    stable = (previous1<0.1) && (previous2<1) && (previous3<5);\n\n    // update the previous values\n    previous1 = message.value1;\n    previous2 = message.value2;\n    previous3 = message.value3;\n\n    if (!stable) {\n      // the sensor is sluggish, you can't query it more than once every second or two https://learn.adafruit.com/dht\n      // wait for three seconds before trying again\n      delay(3000);\n    }\n\n  } // if stable\n\n  message.counter    += 1;\n  message.crc         = crc_buf((char *)&message, sizeof(message_t) - sizeof(unsigned long));\n\n  rf12_sleep(RF12_WAKEUP);\n  rf12_sendNow(0, &message, sizeof message);\n  rf12_sendWait(SEND_MODE);\n  rf12_sleep(RF12_SLEEP);\n\n  // DISPLAY DATA\n#ifdef BLIP_DEBUG\n  Serial.print(message.id);\n  Serial.print(\",\\t\");\n  Serial.print(message.counter);\n  Serial.print(\",\\t\");\n  Serial.print(message.value1, 2);\n  Serial.print(\",\\t\");\n  Serial.print(message.value2, 2);\n  Serial.print(\",\\t\");\n  Serial.print(message.value3, 2);\n  Serial.print(\",\\t\");\n  Serial.print(message.value4, 2);\n  Serial.print(\",\\t\");\n  Serial.print(message.value5, 2);\n  Serial.print(\",\\t\");\n  Serial.println(message.crc);\n#endif\n\n  delay(100); // give it some time to send before falling asleep\n\n  Sleepy::loseSomeTime(63000);\n}\n\n/*****************************************************************************/\n\nlong readVcc() {\n  // Read 1.1V reference against AVcc\n  // set the reference to Vcc and the measurement to the internal 1.1V reference\n#if defined(__AVR_ATmega32U4__) || defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__)\n  ADMUX = _BV(REFS0) | _BV(MUX4) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);\n#elif defined (__AVR_ATtiny24__) || defined(__AVR_ATtiny44__) || defined(__AVR_ATtiny84__)\n  ADMUX = _BV(MUX5) | _BV(MUX0);\n#elif defined (__AVR_ATtiny25__) || defined(__AVR_ATtiny45__) || defined(__AVR_ATtiny85__)\n  ADMUX = _BV(MUX3) | _BV(MUX2);\n#else\n  ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);\n#endif\n\n  delay(2); // Wait for Vref to settle\n  ADCSRA |= _BV(ADSC); // Start conversion\n  while (bit_is_set(ADCSRA,ADSC)); // measuring\n\n  uint8_t low  = ADCL; // must read ADCL first - it then locks ADCH\n  uint8_t high = ADCH; // unlocks both\n\n  long result = (high<<8) | low;\n\n  result = 1134050 / result; // Calculate Vcc (in mV); 1125300 = 1.1*1023*1000\n  return result; // Vcc in millivolts\n}\n\n/*****************************************************************************/\n\nPROGMEM const uint32_t crc_table[16] = {\n  0x00000000, 0x1db71064, 0x3b6e20c8, 0x26d930ac,\n  0x76dc4190, 0x6b6b51f4, 0x4db26158, 0x5005713c,\n  0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c,\n  0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c\n};\n\nunsigned long crc_update(unsigned long crc, byte data)\n{\n  byte tbl_idx;\n  tbl_idx = crc ^ (data >> (0 * 4));\n  crc = pgm_read_dword_near(crc_table + (tbl_idx & 0x0f)) ^ (crc >> 4);\n  tbl_idx = crc ^ (data >> (1 * 4));\n  crc = pgm_read_dword_near(crc_table + (tbl_idx & 0x0f)) ^ (crc >> 4);\n  return crc;\n}\n\nunsigned long crc_buf(char *b, long l)\n{\n  unsigned long crc = ~0L;\n  for (unsigned long i=0; i<l; i++)\n    crc = crc_update(crc, ((char *)b)[i]);\n  crc = ~crc;\n  return crc;\n}\n\nunsigned long crc_string(char *s)\n{\n  unsigned long crc = ~0L;\n  while (*s)\n    crc = crc_update(crc, *s++);\n  crc = ~crc;\n  return crc;\n}\n"
  },
  {
    "path": "rfm12b_send_bmp085/README.md",
    "content": "# Arduino with BMP085 temperature and barometric pressure sensor\n\nSee http://robertoostenveld.nl/arduino-bmp085/ for details.\n"
  },
  {
    "path": "rfm12b_send_bmp085/rfm12b_send_bmp085.ino",
    "content": "#include <Wire.h>\n#include <Adafruit_Sensor.h>\n#include <Adafruit_BMP085_U.h>\n#include <JeeLib.h>        // https://github.com/jcw/jeelib\n#include <Ports.h>         // https://github.com/jcw/jeelib\n#include <avr/pgmspace.h>  // http://excamera.com/sphinx/article-crc.html\n#include <avr/sleep.h>\n\n//#define BLIP_DEBUG\n#define BLIP_NODE 4   // set this to a unique ID to disambiguate multiple nodes\n#define BLIP_GRP  17  // wireless net group to use for sending blips\n\n#define SEND_MODE     2   // set to 3 if fuses are e=06/h=DE/l=CE, else set to 2\n#define RF_CHIPSELECT 10\n\n// this must be defined since we're using the watchdog for low-power waiting\nISR(WDT_vect) {\n  Sleepy::watchdogEvent();\n}\n\nAdafruit_BMP085_Unified bmp = Adafruit_BMP085_Unified(10085);\n\ntypedef struct payload_t {\n  unsigned long id;\n  unsigned long counter;\n  float value1;\n  float value2;\n  float value3;\n  float value4;\n  float value5;\n  unsigned long crc;\n};\n\npayload_t payload;\nfloat previous1 = 0, previous2 = 0, previous3 = 0;\n\n/**************************************************************************/\nvoid displaySensorDetails(void)\n{\n  sensor_t sensor;\n  bmp.getSensor(&sensor);\n  Serial.println(\"------------------------------------\");\n  Serial.print  (\"Sensor:       \");\n  Serial.println(sensor.name);\n  Serial.print  (\"Driver Ver:   \");\n  Serial.println(sensor.version);\n  Serial.print  (\"Unique ID:    \");\n  Serial.println(sensor.sensor_id);\n  Serial.print  (\"Max Value:    \");\n  Serial.print(sensor.max_value);\n  Serial.println(\" hPa\");\n  Serial.print  (\"Min Value:    \");\n  Serial.print(sensor.min_value);\n  Serial.println(\" hPa\");\n  Serial.print  (\"Resolution:   \");\n  Serial.print(sensor.resolution);\n  Serial.println(\" hPa\");\n  Serial.println(\"------------------------------------\");\n  Serial.println(\"\");\n  delay(500);\n}\n\n/**************************************************************************/\nvoid setup(void)\n{\n#ifdef BLIP_DEBUG\n  Serial.begin(57600);\n  Serial.print(\"\\n[rfm12b_send_bmp085 / \");\n  Serial.print(__DATE__);\n  Serial.print(\" / \");\n  Serial.print(__TIME__);\n  Serial.println(\"]\");\n#endif\n\n  pinMode(RF_CHIPSELECT, OUTPUT);\n  rf12_set_cs(RF_CHIPSELECT);\n  rf12_initialize(BLIP_NODE, RF12_868MHZ, BLIP_GRP);\n  rf12_sleep(RF12_SLEEP);\n\n  delay(100); // give it some time to send before falling asleep\n\n  /* Initialise the sensor */\n  if (!bmp.begin()) {\n    /* There was a problem detecting the BMP085 ... check your connections */\n    Serial.print(\"Ooops, no BMP085 detected ... Check your wiring or I2C ADDR!\");\n    while (1);\n  }\n  else {\n    Serial.print(\"BMP085 detected.\");\n  }\n\n  /* Display some basic information on this sensor */\n  displaySensorDetails();\n\n  payload.id          = BLIP_NODE;\n  payload.counter     = 0;\n  payload.value1      = NAN;\n  payload.value2      = NAN;\n  payload.value3      = NAN;\n  payload.value4      = NAN;\n  payload.value5      = NAN;\n}\n\n/**************************************************************************/\nvoid loop(void)\n{\n  boolean stable = false;\n  float pressure, temperature;\n\n  while (!stable) {\n    bmp.getTemperature(&temperature);\n    bmp.getPressure(&pressure);\n\n    payload.value1      = 0.001 * readVcc(); // this is in mV, we want V\n    payload.value2      = temperature;     // this is in Celcius\n    payload.value3      = 0.01 * pressure; // this is in Pa, we want hPa, i.e. mbar\n\n    // compute the difference with the previous values\n    previous1 -= payload.value1;\n    previous1 = abs(previous1);\n    previous2 -= payload.value2;\n    previous2 = abs(previous2);\n    previous3 -= payload.value3;\n    previous3 = abs(previous3);\n\n    // the voltage should be stable within 0.1, the temperature withing 1, the pressure within 1\n    stable = (previous1 < 0.1) && (previous2 < 1) && (previous3 < 1);\n\n#ifdef BLIP_DEBUG\n    // DISPLAY DATA\n    Serial.print(\"BMP085,\\t\");\n    Serial.print(payload.id);\n    Serial.print(\",\\t\");\n    Serial.print(payload.counter);\n    Serial.print(\",\\t\");\n    Serial.print(payload.value1, 2);\n    Serial.print(\",\\t\");\n    Serial.print(payload.value2, 2);\n    Serial.print(\",\\t\");\n    Serial.print(payload.value3, 2);\n    Serial.print(\",\\t\");\n    Serial.print(payload.value4, 2);\n    Serial.print(\",\\t\");\n    Serial.print(payload.value5, 2);\n    Serial.print(\",\\t\");\n    Serial.println(payload.crc);\n    if (!stable)\n      Serial.println(\"Not stable\");\n    else\n      Serial.println(\"Stable\");\n#endif\n    // update the previous values\n    previous1 = payload.value1;\n    previous2 = payload.value2;\n    previous3 = payload.value3;\n\n    delay(100);\n  } // if stable\n\n  payload.counter    += 1;\n  payload.crc         = crc_buf((char *)&payload, sizeof(payload_t) - sizeof(unsigned long));\n\n#ifdef BLIP_DEBUG\n  Serial.println(\"Sending...\");\n  delay(100);\n#endif\n\n  rf12_sleep(RF12_WAKEUP);\n  rf12_sendNow(0, &payload, sizeof payload);\n  rf12_sendWait(SEND_MODE);\n  rf12_sleep(RF12_SLEEP);\n\n  delay(100); // give it some time to send before falling asleep\n\n  Sleepy::loseSomeTime(64000);\n}\n\n/*****************************************************************************/\n\nlong readVcc() {\n  // Read 1.1V reference against AVcc\n  // set the reference to Vcc and the measurement to the internal 1.1V reference\n#if defined(__AVR_ATmega32U4__) || defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__)\n  ADMUX = _BV(REFS0) | _BV(MUX4) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);\n#elif defined (__AVR_ATtiny24__) || defined(__AVR_ATtiny44__) || defined(__AVR_ATtiny84__)\n  ADMUX = _BV(MUX5) | _BV(MUX0);\n#elif defined (__AVR_ATtiny25__) || defined(__AVR_ATtiny45__) || defined(__AVR_ATtiny85__)\n  ADMUX = _BV(MUX3) | _BV(MUX2);\n#else\n  ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);\n#endif\n\n  delay(2); // Wait for Vref to settle\n  ADCSRA |= _BV(ADSC); // Start conversion\n  while (bit_is_set(ADCSRA, ADSC)); // measuring\n\n  uint8_t low  = ADCL; // must read ADCL first - it then locks ADCH\n  uint8_t high = ADCH; // unlocks both\n\n  long result = (high << 8) | low;\n\n  result = 1155700L / result; // Calculate Vcc (in mV); 1125300 = 1.1*1023*1000\n  return result; // Vcc in millivolts\n}\n\n/*****************************************************************************/\n\nPROGMEM const uint32_t crc_table[16] = {\n  0x00000000, 0x1db71064, 0x3b6e20c8, 0x26d930ac,\n  0x76dc4190, 0x6b6b51f4, 0x4db26158, 0x5005713c,\n  0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c,\n  0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c\n};\n\nunsigned long crc_update(unsigned long crc, byte data)\n{\n  byte tbl_idx;\n  tbl_idx = crc ^ (data >> (0 * 4));\n  crc = pgm_read_dword_near(crc_table + (tbl_idx & 0x0f)) ^ (crc >> 4);\n  tbl_idx = crc ^ (data >> (1 * 4));\n  crc = pgm_read_dword_near(crc_table + (tbl_idx & 0x0f)) ^ (crc >> 4);\n  return crc;\n}\n\nunsigned long crc_buf(char *b, long l)\n{\n  unsigned long crc = ~0L;\n  for (unsigned long i = 0; i < l; i++)\n    crc = crc_update(crc, ((char *)b)[i]);\n  crc = ~crc;\n  return crc;\n}\n\nunsigned long crc_string(char *s)\n{\n  unsigned long crc = ~0L;\n  while (*s)\n    crc = crc_update(crc, *s++);\n  crc = ~crc;\n  return crc;\n}\n\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "rfm12b_send_cny70/README.md",
    "content": "# Arduino with CNY70 reflective optical sensor.\n\nSee http://robertoostenveld.nl/arduino-cny70/ for details.\n"
  },
  {
    "path": "rfm12b_send_cny70/rfm12b_send_cny70.ino",
    "content": "#include <JeeLib.h>        // https://github.com/jcw/jeelib\n#include <Ports.h>         // https://github.com/jcw/jeelib\n#include <avr/pgmspace.h>  // http://excamera.com/sphinx/article-crc.html\n\n// RFM12demo for node N should be configured as\n// 1 B iN g17 @868.0000 MHz 0x3BB\n\n#define BLIP_DEBUG\n#define BLIP_NODE     5   // set this to a unique NODE to disambiguate multiple nodes\n#define BLIP_GRP      17  // wireless net group to use for sending blips\n\n#define SEND_MODE     2   // set to 3 if fuses are e=06/h=DE/l=CE, else set to 2\n#define RF_CHIPSELECT 10\n\n#define LED           9\n#define KWH_DETECTOR  A1   // pin number for the CNY50 sensor\n#define GAS_DETECTOR  A2   // pin number for the CNY50 sensor\n\ntypedef struct message_t {\n  unsigned long id;\n  unsigned long counter;\n  float value1;\n  float value2;\n  float value3;\n  float value4;\n  float value5;\n  unsigned long crc;\n};\n\nmessage_t message;\nunsigned int sensorValue   = 0;           // present sensor value\nunsigned int sensorMin     = 1023;        // minimum sensor value, start at max\nunsigned int sensorMax     = 0;           // maximum sensor value, start at min\nunsigned int prevValue     = 0;\nunsigned long prevTime     = 0;\nunsigned long currTime     = 0;\nunsigned long diffTime     = 0;\nunsigned long prevSend     = 0;\nunsigned int pulseCount    = 0;\n\n/*****************************************************************************/\n\nvoid setup() {\n\n#ifdef BLIP_DEBUG\n  Serial.begin(57600);\n  while (!Serial);\n  Serial.print(\"\\n[rfm12b_send_cny70 / \");\n  Serial.print(__DATE__);\n  Serial.print(\" / \");\n  Serial.print(__TIME__);\n  Serial.println(\"]\");\n#endif\n\n  pinMode(KWH_DETECTOR,INPUT);\n  pinMode(GAS_DETECTOR,INPUT);\n  pinMode(LED, OUTPUT);\n\n  pinMode(RF_CHIPSELECT, OUTPUT);\n  rf12_set_cs(RF_CHIPSELECT);\n  rf12_initialize(BLIP_NODE, RF12_868MHZ, BLIP_GRP);\n  rf12_sleep(RF12_SLEEP);\n\n  message.id          = BLIP_NODE;\n  message.counter     = 0;\n  message.value1      = NAN;\n  message.value2      = NAN;\n  message.value3      = NAN;\n  message.value4      = NAN;\n  message.value5      = NAN;\n}\n\n/*****************************************************************************/\n\nvoid loop() {\n  // read the sensor, apply an IIR filter\n  sensorValue = (5*analogRead(KWH_DETECTOR) + 5*sensorValue)/10;\n\n  if (sensorValue<sensorMin)\n    sensorMin = sensorValue;\n  if (sensorValue>sensorMax)\n    sensorMax = sensorValue;\n\n  Serial.print(\"val = \");\n  Serial.print(sensorValue);\n  Serial.print(\" min = \");\n  Serial.print(sensorMin);\n  Serial.print(\" max = \");\n  Serial.print(sensorMax);\n  Serial.println();\n\n  // threshold the sensor value somewhere between the min and max\n  boolean pulse = sensorValue>((8*sensorMax+2*sensorMin)/10);\n\n  if (pulse)  \n    digitalWrite(LED, HIGH);\n  else\n    digitalWrite(LED, LOW);\n\n  // compute the time to the previous event, deal with integer overflow\n  currTime = millis();\n  diffTime = currTime - prevTime;  \n\n  // detect the upgoing flank, debounce it with 100 ms\n  if (pulse && prevValue==0 && diffTime>100) {\n    prevTime  = currTime;\n    prevValue = 1;\n    pulseCount++;\n#ifdef BLIP_DEBUG\n    float watt = 6000000.0/diffTime;\n    Serial.print(\"instantaneous watt = \");\n    Serial.println(watt);\n#endif\n  }\n  else {\n    prevValue = 0;\n  }\n\n  if ((currTime-prevSend)>65000) {\n    // compute average power usage since last transmission\n    float watt = (float)pulseCount * 6000000.0/(currTime-prevSend);\n    prevSend   = currTime;\n    pulseCount = 0;\n\n    message.value1      = 0.001*readVcc(); // this is in mV, we want V\n    message.value2      = watt;\n    message.counter    += 1;\n    message.crc         = crc_buf((char *)&message, sizeof(message_t) - sizeof(unsigned long));\n\n    rf12_sleep(RF12_WAKEUP);\n    rf12_sendNow(0, &message, sizeof message);\n    rf12_sendWait(SEND_MODE);\n    rf12_sleep(RF12_SLEEP);\n\n#ifdef BLIP_DEBUG\n    // DISPLAY DATA\n    Serial.print(\"CNY70 \\t\");\n    Serial.print(message.id);\n    Serial.print(\",\\t\");\n    Serial.print(message.counter);\n    Serial.print(\",\\t\");\n    Serial.print(message.value1, 2);\n    Serial.print(\",\\t\");\n    Serial.print(message.value2, 2);\n    Serial.print(\",\\t\");\n    Serial.print(message.value3, 2);\n    Serial.print(\",\\t\");\n    Serial.print(message.value4, 2);\n    Serial.print(\",\\t\");\n    Serial.print(message.value5, 2);\n    Serial.print(\",\\t\");\n    Serial.println(message.crc);\n#endif\n  }\n\n\n  if (sensorMin<1023)\n    sensorMin++;\n//  if (sensorMax>0)\n//    sensorMax--;\n\n  // sample the CNY50 sensor approximately at 100 Hz\n  delay(10);\n}\n\n\n\n/*****************************************************************************/\n\nlong readVcc() {\n  // Read 1.1V reference against AVcc\n  // set the reference to Vcc and the measurement to the internal 1.1V reference\n#if defined(__AVR_ATmega32U4__) || defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__)\n  ADMUX = _BV(REFS0) | _BV(MUX4) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);\n#elif defined (__AVR_ATtiny24__) || defined(__AVR_ATtiny44__) || defined(__AVR_ATtiny84__)\n  ADMUX = _BV(MUX5) | _BV(MUX0);\n#elif defined (__AVR_ATtiny25__) || defined(__AVR_ATtiny45__) || defined(__AVR_ATtiny85__)\n  ADMUX = _BV(MUX3) | _BV(MUX2);\n#else\n  ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);\n#endif  \n\n  delay(2); // Wait for Vref to settle\n  ADCSRA |= _BV(ADSC); // Start conversion\n  while (bit_is_set(ADCSRA,ADSC)); // measuring\n\n  uint8_t low  = ADCL; // must read ADCL first - it then locks ADCH  \n  uint8_t high = ADCH; // unlocks both\n\n  long result = (high<<8) | low;\n\n  result = 1108848L / result; // Calculate Vcc (in mV); 1125300 = 1.1*1023*1000\n  return result; // Vcc in millivolts\n}\n\n/*****************************************************************************/\n\nstatic PROGMEM prog_uint32_t crc_table[16] = {\n  0x00000000, 0x1db71064, 0x3b6e20c8, 0x26d930ac,\n  0x76dc4190, 0x6b6b51f4, 0x4db26158, 0x5005713c,\n  0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c,\n  0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c\n};\n\nunsigned long crc_update(unsigned long crc, byte data)\n{\n  byte tbl_NODEx;\n  tbl_NODEx = crc ^ (data >> (0 * 4));\n  crc = pgm_read_dword_near(crc_table + (tbl_NODEx & 0x0f)) ^ (crc >> 4);\n  tbl_NODEx = crc ^ (data >> (1 * 4));\n  crc = pgm_read_dword_near(crc_table + (tbl_NODEx & 0x0f)) ^ (crc >> 4);\n  return crc;\n}\n\nunsigned long crc_buf(char *b, long l)\n{\n  unsigned long crc = ~0L;\n  for (unsigned long i=0; i<l; i++)\n    crc = crc_update(crc, ((char *)b)[i]);\n  crc = ~crc;\n  return crc;\n}\n\nunsigned long crc_string(char *s)\n{\n  unsigned long crc = ~0L;\n  while (*s)\n    crc = crc_update(crc, *s++);\n  crc = ~crc;\n  return crc;\n}\n\n\n\n\n\n\n"
  },
  {
    "path": "rfm12b_send_ds18b20/README.md",
    "content": "# Arduino with DS18B20 temperature sensor\n\nSee http://robertoostenveld.nl/arduino-ds18b20/ for details.\n"
  },
  {
    "path": "rfm12b_send_ds18b20/rfm12b_send_ds18b20.ino",
    "content": "#include <OneWire.h>       // http://www.pjrc.com/teensy/td_libs_OneWire.html\n#include <JeeLib.h>        // https://github.com/jcw/jeelib\n#include <Ports.h>         // https://github.com/jcw/jeelib\n#include <avr/pgmspace.h>  // http://excamera.com/sphinx/article-crc.html\n#include <avr/sleep.h>\n\n//#define BLIP_DEBUG\n//#define USE_SLEEPY\n#define BLIP_NODE 6   // set this to a unique NODE to disambiguate multiple nodes\n#define BLIP_GRP  17  // wireless net group to use for sending blips\n\n#define SEND_MODE     2   // set to 3 if fuses are e=06/h=DE/l=CE, else set to 2\n#define RF_CHIPSELECT 10\n\nOneWire ds(9);        // two ds18b20's are connected to this pin \n\n// this must be defined since we're using the watchdog for low-power waiting\nISR(WDT_vect) { \n  Sleepy::watchdogEvent(); \n}\n\ntypedef struct message_t {\n  unsigned long id;\n  unsigned long counter;\n  float value1;\n  float value2;\n  float value3;\n  float value4;\n  float value5;\n  unsigned long crc;\n};\n\nmessage_t message;\n\n/*****************************************************************************/\n\nvoid setup () {\n#ifdef BLIP_DEBUG\n  Serial.begin(57600);\n  Serial.print(\"\\n[rfm12b_send_ds18b20 / \");\n  Serial.print(__DATE__);\n  Serial.print(\" / \");\n  Serial.print(__TIME__);\n  Serial.println(\"]\");\n#endif\n\n  pinMode(RF_CHIPSELECT, OUTPUT);\n  rf12_set_cs(RF_CHIPSELECT);\n  rf12_initialize(BLIP_NODE, RF12_868MHZ, BLIP_GRP);\n  rf12_sleep(RF12_SLEEP);\n\n  delay(100); // give it some time to send before falling asleep\n\n  message.id          = BLIP_NODE;\n  message.counter     = 0;\n  message.value1      = NAN;\n  message.value2      = NAN;\n  message.value3      = NAN;\n  message.value4      = NAN;\n  message.value5      = NAN;\n}\n\n/*****************************************************************************/\n\nvoid loop () {\n\n  byte addr1[8] = {\n    0x28, 0x50, 0x49, 0x6B, 0x05, 0x00, 0x00, 0x1B          };\n  byte addr2[8] = {\n    0x28, 0x07, 0x61, 0x98, 0x06, 0x00, 0x00, 0x6B          };\n\n  ds.reset();\n  ds.select(addr1);\n  ds.write(0x44, 1);        // start conversion, with parasite power on at the end\n\n  ds.reset();\n  ds.select(addr2);\n  ds.write(0x44, 1);        // start conversion, with parasite power on at the end\n\n  delay(1000);              // maybe 750ms is enough, maybe not\n\n  message.value1      = 0.001*readVcc(); // this is in mV, we want V\n  message.value2      = readTemp(addr1);\n  message.value3      = readTemp(addr2);\n  message.counter    += 1;\n  message.crc         = crc_buf((char *)&message, sizeof(message_t) - sizeof(unsigned long));\n\n  rf12_sleep(RF12_WAKEUP);\n  rf12_sendNow(0, &message, sizeof message);\n  rf12_sendWait(SEND_MODE);\n  rf12_sleep(RF12_SLEEP);\n\n#ifdef BLIP_DEBUG\n  // DISPLAY DATA\n  Serial.print(\"DS18B20,\\t\");\n  Serial.print(message.id);\n  Serial.print(\",\\t\");\n  Serial.print(message.counter);\n  Serial.print(\",\\t\");\n  Serial.print(message.value1, 2);\n  Serial.print(\",\\t\");\n  Serial.print(message.value2, 2);\n  Serial.print(\",\\t\");\n  Serial.print(message.value3, 2);\n  Serial.print(\",\\t\");\n  Serial.print(message.value4, 2);\n  Serial.print(\",\\t\");\n  Serial.print(message.value5, 2);\n  Serial.print(\",\\t\");\n  Serial.println(message.crc);\n#endif\n\n  delay(100); // give it some time to send before falling asleep\n\n#ifdef USE_SLEEPY\n  // this does not work in combination with the OneWire library\n  Sleepy::loseSomeTime(66000);\n#else\n  delay(66000);\n#endif\n}\n\n\n/*****************************************************************************/\n\nfloat readTemp(byte addr[]) {\n  byte data[9];\n  float celsius, fahrenheit;\n\n  ds.reset();\n  ds.select(addr);    \n  ds.write(0xBE);         // Read Scratchpad\n\n  for ( int i = 0; i < 9; i++) {           // we need 9 bytes\n    data[i] = ds.read();\n  }\n\n  // Convert the data to actual temperature\n  // because the result is a 16 bit signed integer, it should\n  // be stored to an \"int16_t\" type, which is always 16 bits\n  // even when compiled on a 32 bit processor.\n  int16_t raw = (data[1] << 8) | data[0];\n\n  byte cfg = (data[4] & 0x60);\n  // at lower res, the low bits are undefined, so let's zero them\n  if      (cfg == 0x00) raw = raw & ~7; //  9 bit resolution, 93.75 ms\n  else if (cfg == 0x20) raw = raw & ~3; // 10 bit resolution, 187.5 ms\n  else if (cfg == 0x40) raw = raw & ~1; // 11 bit resolution, 375 ms\n  //// default is 12 bit resolution, 750 ms conversion time\n\n  celsius    = (float)raw / 16.0;\n  fahrenheit = celsius * 1.8 + 32.0;\n\n  return celsius;\n}\n\n/*****************************************************************************/\n\nlong readVcc() {\n  // Read 1.1V reference against AVcc\n  // set the reference to Vcc and the measurement to the internal 1.1V reference\n#if defined(__AVR_ATmega32U4__) || defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__)\n  ADMUX = _BV(REFS0) | _BV(MUX4) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);\n#elif defined (__AVR_ATtiny24__) || defined(__AVR_ATtiny44__) || defined(__AVR_ATtiny84__)\n  ADMUX = _BV(MUX5) | _BV(MUX0);\n#elif defined (__AVR_ATtiny25__) || defined(__AVR_ATtiny45__) || defined(__AVR_ATtiny85__)\n  ADMUX = _BV(MUX3) | _BV(MUX2);\n#else\n  ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);\n#endif  \n\n  delay(2); // Wait for Vref to settle\n  ADCSRA |= _BV(ADSC); // Start conversion\n  while (bit_is_set(ADCSRA,ADSC)); // measuring\n\n  uint8_t low  = ADCL; // must read ADCL first - it then locks ADCH  \n  uint8_t high = ADCH; // unlocks both\n\n  long result = (high<<8) | low;\n\n  result = 1108848L / result; // Calculate Vcc (in mV); 1125300 = 1.1*1023*1000\n  return result; // Vcc in millivolts\n}\n\n/*****************************************************************************/\n\nstatic PROGMEM prog_uint32_t crc_table[16] = {\n  0x00000000, 0x1db71064, 0x3b6e20c8, 0x26d930ac,\n  0x76dc4190, 0x6b6b51f4, 0x4db26158, 0x5005713c,\n  0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c,\n  0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c\n};\n\nunsigned long crc_update(unsigned long crc, byte data)\n{\n  byte tbl_NODEx;\n  tbl_NODEx = crc ^ (data >> (0 * 4));\n  crc = pgm_read_dword_near(crc_table + (tbl_NODEx & 0x0f)) ^ (crc >> 4);\n  tbl_NODEx = crc ^ (data >> (1 * 4));\n  crc = pgm_read_dword_near(crc_table + (tbl_NODEx & 0x0f)) ^ (crc >> 4);\n  return crc;\n}\n\nunsigned long crc_buf(char *b, long l)\n{\n  unsigned long crc = ~0L;\n  for (unsigned long i=0; i<l; i++)\n    crc = crc_update(crc, ((char *)b)[i]);\n  crc = ~crc;\n  return crc;\n}\n\nunsigned long crc_string(char *s)\n{\n  unsigned long crc = ~0L;\n  while (*s)\n    crc = crc_update(crc, *s++);\n  crc = ~crc;\n  return crc;\n}\n\n\n\n\n\n"
  },
  {
    "path": "rfm12b_send_lm35/README.md",
    "content": "# Arduino with LM35 temperature sensor\n\nSee http://robertoostenveld.nl/arduino-lm35/ for details.\n"
  },
  {
    "path": "rfm12b_send_lm35/rfm12b_send_lm35.ino",
    "content": "#include <JeeLib.h>        // https://github.com/jcw/jeelib\n#include <Ports.h>         // https://github.com/jcw/jeelib\n#include <avr/pgmspace.h>  // http://excamera.com/sphinx/article-crc.html\n#include <avr/sleep.h>\n\n#define BLIP_DEBUG\n#define BLIP_NODE 2   // set this to a unique NODE to disambiguate multiple nodes\n#define BLIP_GRP  17  // wireless net group to use for sending blips\n\n#define LM35_PIN      A2\n#define SEND_MODE     2   // set to 3 if fuses are e=06/h=DE/l=CE, else set to 2\n#define RF_CHIPSELECT 10\n\n// this must be defined since we're using the watchdog for low-power waiting\nISR(WDT_vect) { \n  Sleepy::watchdogEvent(); \n}\n\ntypedef struct message_t {\n  unsigned long id;\n  unsigned long counter;\n  float value1;\n  float value2;\n  float value3;\n  float value4;\n  float value5;\n  unsigned long crc;\n};\n\nmessage_t message;\nfloat previous1 = 0, previous2 = 0;\n\n/*****************************************************************************/\n\nvoid setup () {\n#ifdef BLIP_DEBUG\n  Serial.begin(57600);\n  Serial.print(\"\\n[rfm12b_send_lm35 / \");\n  Serial.print(__DATE__);\n  Serial.print(\" / \");\n  Serial.print(__TIME__);\n  Serial.println(\"]\");\n#endif\n\n  pinMode(LM35_PIN, INPUT);\n\n  pinMode(RF_CHIPSELECT, OUTPUT);\n  rf12_set_cs(RF_CHIPSELECT);\n  rf12_initialize(BLIP_NODE, RF12_868MHZ, BLIP_GRP);\n  rf12_sleep(RF12_SLEEP);\n\n  delay(100); // give it some time to send before falling asleep\n\n  message.id          = BLIP_NODE;\n  message.counter     = 0;\n  message.value1      = NAN;\n  message.value2      = NAN;\n  message.value3      = NAN;\n  message.value4      = NAN;\n  message.value5      = NAN;\n}\n\n/*****************************************************************************/\n\nvoid loop () {\n  boolean stable = false;\n\n  while (!stable) {\n    long vcc = readVcc();\n    float sensorValue = analogRead(LM35_PIN);    \n    sensorValue *= vcc;\n    sensorValue /= 10230;\n\n    message.value1      = 0.001*readVcc(); // this is in mV, we want V\n    message.value2      = sensorValue;     // this is Celcius\n\n    // compute the difference with the previous values\n    previous1 -= message.value1;\n    previous1 = abs(previous1);\n    previous2 -= message.value2;\n    previous2 = abs(previous2);\n\n    // the voltage should be stable within 0.1, the temperature within 1\n    stable = (previous1<0.1) && (previous2<1);\n\n    // update the previous values\n    previous1 = message.value1;\n    previous2 = message.value2;\n\n  } // if stable \n\n  message.counter    += 1;\n  message.crc         = crc_buf((char *)&message, sizeof(message_t) - sizeof(unsigned long));\n\n  rf12_sleep(RF12_WAKEUP);\n  rf12_sendNow(0, &message, sizeof message);\n  rf12_sendWait(SEND_MODE);\n  rf12_sleep(RF12_SLEEP);\n\n#ifdef BLIP_DEBUG\n  // DISPLAY DATA\n  Serial.print(\"LM35,\\t\");\n  Serial.print(message.id);\n  Serial.print(\",\\t\");\n  Serial.print(message.counter);\n  Serial.print(\",\\t\");\n  Serial.print(message.value1, 2);\n  Serial.print(\",\\t\");\n  Serial.print(message.value2, 2);\n  Serial.print(\",\\t\");\n  Serial.print(message.value3, 2);\n  Serial.print(\",\\t\");\n  Serial.print(message.value4, 2);\n  Serial.print(\",\\t\");\n  Serial.print(message.value5, 2);\n  Serial.print(\",\\t\");\n  Serial.println(message.crc);\n  delay(100); // give it some time to send before falling asleep\n#endif\n\n  Sleepy::loseSomeTime(62000);\n}\n\n/*****************************************************************************/\n\nlong readVcc() {\n  // Read 1.1V reference against AVcc\n  // set the reference to Vcc and the measurement to the internal 1.1V reference\n#if defined(__AVR_ATmega32U4__) || defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__)\n  ADMUX = _BV(REFS0) | _BV(MUX4) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);\n#elif defined (__AVR_ATtiny24__) || defined(__AVR_ATtiny44__) || defined(__AVR_ATtiny84__)\n  ADMUX = _BV(MUX5) | _BV(MUX0);\n#elif defined (__AVR_ATtiny25__) || defined(__AVR_ATtiny45__) || defined(__AVR_ATtiny85__)\n  ADMUX = _BV(MUX3) | _BV(MUX2);\n#else\n  ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);\n#endif  \n\n  delay(2); // Wait for Vref to settle\n  ADCSRA |= _BV(ADSC); // Start conversion\n  while (bit_is_set(ADCSRA,ADSC)); // measuring\n\n  uint8_t low  = ADCL; // must read ADCL first - it then locks ADCH  \n  uint8_t high = ADCH; // unlocks both\n\n  long result = (high<<8) | low;\n\n  result = 1108848L / result; // Calculate Vcc (in mV); 1125300 = 1.1*1023*1000\n  return result; // Vcc in millivolts\n}\n\n/*****************************************************************************/\n\nstatic PROGMEM prog_uint32_t crc_table[16] = {\n  0x00000000, 0x1db71064, 0x3b6e20c8, 0x26d930ac,\n  0x76dc4190, 0x6b6b51f4, 0x4db26158, 0x5005713c,\n  0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c,\n  0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c\n};\n\nunsigned long crc_update(unsigned long crc, byte data)\n{\n  byte tbl_NODEx;\n  tbl_NODEx = crc ^ (data >> (0 * 4));\n  crc = pgm_read_dword_near(crc_table + (tbl_NODEx & 0x0f)) ^ (crc >> 4);\n  tbl_NODEx = crc ^ (data >> (1 * 4));\n  crc = pgm_read_dword_near(crc_table + (tbl_NODEx & 0x0f)) ^ (crc >> 4);\n  return crc;\n}\n\nunsigned long crc_buf(char *b, long l)\n{\n  unsigned long crc = ~0L;\n  for (unsigned long i=0; i<l; i++)\n    crc = crc_update(crc, ((char *)b)[i]);\n  crc = ~crc;\n  return crc;\n}\n\nunsigned long crc_string(char *s)\n{\n  unsigned long crc = ~0L;\n  while (*s)\n    crc = crc_update(crc, *s++);\n  crc = ~crc;\n  return crc;\n}\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "rfm12b_send_random/README.md",
    "content": "# Arduino with RFM12B that sends random numbers to the ThingSpeak relay.\n\nSee http://robertoostenveld.nl/arduino-based-energy-and-climate-monitor-version-2/\n"
  },
  {
    "path": "rfm12b_send_random/rfm12b_send_random.ino",
    "content": "#include <JeeLib.h>        // https://github.com/jcw/jeelib\n#include <Ports.h>         // https://github.com/jcw/jeelib\n#include <avr/pgmspace.h>  // http://excamera.com/sphinx/article-crc.html\n#include <avr/sleep.h>\n\n#define BLIP_DEBUG\n#define BLIP_NODE 7   // set this to a unique NODE to disambiguate multiple nodes\n#define BLIP_GRP  17  // wireless net group to use for sending blips\n\n#define SEND_MODE     2   // set to 3 if fuses are e=06/h=DE/l=CE, else set to 2\n#define RF_CHIPSELECT 10\n\n// this must be defined since we're using the watchdog for low-power waiting\nISR(WDT_vect) {\n  Sleepy::watchdogEvent();\n}\n\ntypedef struct message_t {\n  unsigned long id;\n  unsigned long counter;\n  float value1;\n  float value2;\n  float value3;\n  float value4;\n  float value5;\n  unsigned long crc;\n};\n\nmessage_t message;\n\n/*****************************************************************************/\n\nvoid setup () {\n#ifdef BLIP_DEBUG\n  Serial.begin(57600);\n  Serial.print(\"\\n[rfm12b_send_random / \");\n  Serial.print(__DATE__);\n  Serial.print(\" / \");\n  Serial.print(__TIME__);\n  Serial.println(\"]\");\n#endif\n\n  pinMode(RF_CHIPSELECT, OUTPUT);\n  rf12_set_cs(RF_CHIPSELECT);\n  rf12_initialize(BLIP_NODE, RF12_868MHZ, BLIP_GRP);\n  rf12_sleep(RF12_SLEEP);\n\n  delay(100); // give it some time to send before falling asleep\n\n  message.id          = BLIP_NODE;\n  message.counter     = 0;\n  message.value1      = NAN;\n  message.value2      = NAN;\n  message.value3      = NAN;\n  message.value4      = NAN;\n  message.value5      = NAN;\n}\n\n/*****************************************************************************/\n\nvoid loop () {\n  boolean stable = false;\n\n  long vcc = readVcc();\n  message.value1  = 0.001*readVcc(); // this is in mV, we want V\n  message.value2  = (float)(random(2147483647L))/(2147483647L);\n  message.value3  = (float)(random(2147483647L))/(2147483647L);\n  message.value4  = (float)(random(2147483647L))/(2147483647L);\n  message.value5  = (float)(random(2147483647L))/(2147483647L);\n  message.counter += 1;\n  message.crc     = crc_buf((char *)&message, sizeof(message_t) - sizeof(unsigned long));\n\n  rf12_sleep(RF12_WAKEUP);\n  rf12_sendNow(0, &message, sizeof message);\n  rf12_sendWait(SEND_MODE);\n  rf12_sleep(RF12_SLEEP);\n\n#ifdef BLIP_DEBUG\n  // DISPLAY DATA\n  Serial.print(\"RANDOM,\\t\");\n  Serial.print(message.id);\n  Serial.print(\",\\t\");\n  Serial.print(message.counter);\n  Serial.print(\",\\t\");\n  Serial.print(message.value1, 2);\n  Serial.print(\",\\t\");\n  Serial.print(message.value2, 2);\n  Serial.print(\",\\t\");\n  Serial.print(message.value3, 2);\n  Serial.print(\",\\t\");\n  Serial.print(message.value4, 2);\n  Serial.print(\",\\t\");\n  Serial.print(message.value5, 2);\n  Serial.print(\",\\t\");\n  Serial.println(message.crc);\n  delay(100); // give it some time to send before falling asleep\n#endif\n\n  Sleepy::loseSomeTime(67000);\n}\n\n/*****************************************************************************/\n\nlong readVcc() {\n  // Read 1.1V reference against AVcc\n  // set the reference to Vcc and the measurement to the internal 1.1V reference\n#if defined(__AVR_ATmega32U4__) || defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__)\n  ADMUX = _BV(REFS0) | _BV(MUX4) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);\n#elif defined (__AVR_ATtiny24__) || defined(__AVR_ATtiny44__) || defined(__AVR_ATtiny84__)\n  ADMUX = _BV(MUX5) | _BV(MUX0);\n#elif defined (__AVR_ATtiny25__) || defined(__AVR_ATtiny45__) || defined(__AVR_ATtiny85__)\n  ADMUX = _BV(MUX3) | _BV(MUX2);\n#else\n  ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);\n#endif\n\n  delay(2); // Wait for Vref to settle\n  ADCSRA |= _BV(ADSC); // Start conversion\n  while (bit_is_set(ADCSRA,ADSC)); // measuring\n\n  uint8_t low  = ADCL; // must read ADCL first - it then locks ADCH\n  uint8_t high = ADCH; // unlocks both\n\n  long result = (high<<8) | low;\n\n  result = 1108848L / result; // Calculate Vcc (in mV); 1125300 = 1.1*1023*1000\n  return result; // Vcc in millivolts\n}\n\n/*****************************************************************************/\n\nPROGMEM const uint32_t crc_table[16] = {\n  0x00000000, 0x1db71064, 0x3b6e20c8, 0x26d930ac,\n  0x76dc4190, 0x6b6b51f4, 0x4db26158, 0x5005713c,\n  0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c,\n  0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c\n};\n\nunsigned long crc_update(unsigned long crc, byte data)\n{\n  byte tbl_NODEx;\n  tbl_NODEx = crc ^ (data >> (0 * 4));\n  crc = pgm_read_dword_near(crc_table + (tbl_NODEx & 0x0f)) ^ (crc >> 4);\n  tbl_NODEx = crc ^ (data >> (1 * 4));\n  crc = pgm_read_dword_near(crc_table + (tbl_NODEx & 0x0f)) ^ (crc >> 4);\n  return crc;\n}\n\nunsigned long crc_buf(char *b, long l)\n{\n  unsigned long crc = ~0L;\n  for (unsigned long i=0; i<l; i++)\n    crc = crc_update(crc, ((char *)b)[i]);\n  crc = ~crc;\n  return crc;\n}\n\nunsigned long crc_string(char *s)\n{\n  unsigned long crc = ~0L;\n  while (*s)\n    crc = crc_update(crc, *s++);\n  crc = ~crc;\n  return crc;\n}\n"
  },
  {
    "path": "rfm12b_thingspeak/README.md",
    "content": "# Arduino relay between RFM12b and ThingSpeak\n\nThis is a sketch for an Arduino Uno with an Ethershield. It operates in conjunction with the \"rfm12b_recv_xxxx\" sketch, which is running on an Arduino pro mini with a RFM12b module attached. The two Arduino's exchange sensor data over a i2c connection.\n\nSee http://robertoostenveld.nl/arduino-relay-rfm12b-thingspeak/ for details.\n"
  },
  {
    "path": "rfm12b_thingspeak/rfm12b_thingspeak.ino",
    "content": "\n// ethernet starts off from http://arduino.cc/en/Tutorial/DhcpAddressPrinter\n// thingspeak starts off from http://community.thingspeak.com/arduino/ThingSpeakClient.pde\n\n#include <SPI.h>\n#include <Ethernet.h>\n#include <Wire.h>\n#include <string.h>\n\n#include \"secret.h\"\n\n/*\n    The write API keys for ThingSpeak are defined in secret.h like this\n\n   #define APIKeyChannel2 \"XXXXXXXXXXXXXXXX\"\n   #define APIKeyChannel3 \"XXXXXXXXXXXXXXXX\"\n   #define APIKeyChannel4 \"XXXXXXXXXXXXXXXX\"\n   #define APIKeyChannel5 \"XXXXXXXXXXXXXXXX\"\n   #define APIKeyChannel6 \"XXXXXXXXXXXXXXXX\"\n\n*/\n\n#ifndef DEBUG\n#define DEBUG_PRINT(x)\n#define DEBUG_PRINT2(x, y)\n#define DEBUG_PRINTLN(x)\n#elif DEBUG == SERIAL\n#define DEBUG_PRINT(x)     Serial.print (x)\n#define DEBUG_PRINT2(x, y) Serial.print (x, y)\n#define DEBUG_PRINTLN(x)   Serial.println (x)\n#elif DEBUG == LOGGER\n#include <Syslog.h>\n#define DEBUG_PRINT(x)        Syslog.logger(1, 6, \"rfm12b_thingspeak:\", \"\", x);\n#define DEBUG_PRINT2(x, y)    Syslog.logger(1, 6, \"rfm12b_thingspeak:\", \"\", x);\n#define DEBUG_PRINTLN(x)      Syslog.logger(1, 6, \"rfm12b_thingspeak:\", \"\", x);\n#else\n#error \"Unexpected value of DEBUG.\"\n#endif\n\nbyte buf[32]; // ring buffer\nunsigned int bufptr = 0; // pointer into ring buffer\nunsigned int bufblk = 0; // boolean to block buffer updates\n\n// ThingSpeak Settings\nIPAddress server(184, 106, 153, 149);          // IP Address for the ThingSpeak API\nEthernetClient client;\n\nbyte mac[] = { 0x90, 0xA2, 0xDA, 0x0D, 0x2A, 0x4A };\nbyte loghost[] = { 192, 168, 1, 7 };\n\ntypedef struct message_t {\n  unsigned long id;\n  unsigned long counter;\n  float value1;\n  float value2;\n  float value3;\n  float value4;\n  float value5;\n  unsigned long crc;\n};\n\n// Variable Setup\nunsigned long lastConnectionTime = 0;\nboolean       lastConnectionStatus = false;\nunsigned int  resetCounter = 0;\n\n/*******************************************************************************************************************/\n\nvoid(* resetArduino) (void) = 0; // declare reset function at address 0\n\n/*******************************************************************************************************************/\n\nvoid setup() {\n  Serial.begin(57600);\n  Serial.println(\"\\n[rfm12b_thingspeak / \" __DATE__ \" / \" __TIME__ \"]\");\n\n  // start the Ethernet connection:\n  while (Ethernet.begin(mac) == 0) {\n    Serial.println(\"Failed to configure Ethernet using DHCP\");\n    delay(5000);\n  }\n  // print your local IP address:\n  Serial.print(\"My IP address: \");\n  for (byte thisByte = 0; thisByte < 4; thisByte++) {\n    // print the value of each byte of the IP address:\n    Serial.print(Ethernet.localIP()[thisByte], DEC);\n    Serial.print(\".\");\n  }\n  Serial.println();\n\n  for (int i; i < 32; i++) {\n    buf[i] = 0;\n  }\n\n  Wire.begin(9);                // Start I2C Bus as a Slave (Device Number 9)\n  Wire.onReceive(receiveEvent); // register event\n\n#if defined(DEBUG) && DEBUG == LOGGER\n  Syslog.setLoghost(loghost);\n  Syslog.logger(1, 5, \"\", \"rfm12b_thingspeak:\", \"setup finished\");\n#endif\n\n} // setup\n\n/*******************************************************************************************************************/\n\nvoid loop() {\n\n  // Clock rollover, reset after 49 days\n  if (millis() < lastConnectionTime)\n    resetArduino();\n\n  // There is a problem with ethernet, restart the shield\n  if (resetCounter >= 5 ) {\n    resetEthernetShield();\n    resetCounter = 0;\n  }\n\n  if (bufblk) {\n    // the I2C buffer is full and needs to be processed\n    message_t *message = (message_t *)buf;\n\n    DEBUG_PRINTLN(\"packet received\");\n    DEBUG_PRINT(message->id);\n    DEBUG_PRINT(\",\\t\");\n    DEBUG_PRINT(message->counter);\n    DEBUG_PRINT(\",\\t\");\n    DEBUG_PRINT2(message->value1, 2);\n    DEBUG_PRINT(\",\\t\");\n    DEBUG_PRINT2(message->value2, 2);\n    DEBUG_PRINT(\",\\t\");\n    DEBUG_PRINT2(message->value3, 2);\n    DEBUG_PRINT(\",\\t\");\n    DEBUG_PRINT2(message->value4, 2);\n    DEBUG_PRINT(\",\\t\");\n    DEBUG_PRINT2(message->value5, 2);\n    DEBUG_PRINT(\",\\t\");\n    DEBUG_PRINTLN(message->crc);\n\n    if (message->crc == crc_buf((char *)message, sizeof(message_t) - sizeof(unsigned long))) {\n\n      DEBUG_PRINTLN(\"CRC valid.\");\n\n      String postString = \"&field1=\" + String(message->value1) + \"&field2=\" + String(message->value2) + \"&field3=\" + String(message->value3) + \"&field4=\" + String(message->value4) + \"&field5=\" + String(message->value5) + \"&field6=\" + String(message->counter);\n      DEBUG_PRINTLN(postString);\n\n      // connect to ThingSpeak and forward the received message\n      switch (message->id) {\n        case 1:\n          updateThingSpeak(postString, APIKeyChannel1);\n          break;\n        case 2:\n          updateThingSpeak(postString, APIKeyChannel2);\n          break;\n        case 3:\n          updateThingSpeak(postString, APIKeyChannel3);\n          break;\n        case 4:\n          updateThingSpeak(postString, APIKeyChannel4);\n          break;\n        case 5:\n          updateThingSpeak(postString, APIKeyChannel5);\n          break;\n        case 6:\n          updateThingSpeak(postString, APIKeyChannel6);\n          break;\n      } // switch\n\n    } // if message with correct CRC\n    else {\n      DEBUG_PRINTLN(\"CRC mismatch.\\n\");\n    }\n\n    bufblk = 0; // release buffer\n  } // if the I2C buffer is full\n\n} // loop\n\n/*******************************************************************************************************************/\n\nvoid updateThingSpeak(const String tsData, const String writeAPIKey) {\n  if (client.connected() || client.connect(server, 80)) {\n    DEBUG_PRINTLN(\"Connected to ThingSpeak...\\n\");\n    lastConnectionTime = millis();\n    resetCounter = 0;\n\n    client.print(F(\"POST /update HTTP/1.1\\n\"));\n    client.print(F(\"Host: api.thingspeak.com\\n\"));\n    client.print(F(\"Connection: close\\n\"));\n    client.print(F(\"X-THINGSPEAKAPIKEY: \"));\n    client.print(writeAPIKey);\n    client.print(F(\"\\n\"));\n    client.print(F(\"Content-Type: application/x-www-form-urlencoded\\n\"));\n    client.print(F(\"Content-Length: \"));\n    client.print(tsData.length());\n    client.print(F(\"\\n\\n\"));\n    client.print(tsData);\n\n    // Print Update Response to Serial Monitor\n    while (client.available()) {\n      char c = client.read();\n      DEBUG_PRINT(c);\n    }\n\n    // disconnect from ThingSpeak\n    client.stop();\n\n  }\n  else {\n    DEBUG_PRINTLN(\"Not connected.\\n\");\n    client.stop();\n    resetCounter++;\n  }\n} // updateThingSpeak\n\nvoid resetEthernetShield() {\n  DEBUG_PRINTLN(\"Resetting Ethernet Shield.\\n\");\n\n  client.stop();\n  delay(1000);\n\n  while (Ethernet.begin(mac) == 0) {\n    DEBUG_PRINTLN(\"Failed to configure Ethernet using DHCP\");\n    delay(5000);\n  }\n} // resetEthernetShield\n\nvoid receiveEvent(int howMany) {\n  //DEBUG_PRINT(\"receiveEvent \");\n  //DEBUG_PRINT(howMany);\n  //DEBUG_PRINT(\" : \");\n\n  while (howMany-- > 0) {\n    int x = Wire.read(); // receive byte as an integer\n    //DEBUG_PRINT(x);\n    //DEBUG_PRINT(\" \");\n\n    if (!bufblk)\n      buf[bufptr++] = x; // insert into buffer\n\n    if ((bufptr % 32) == 0) {\n      bufptr = 0; // point to the first element\n      bufblk = 1; // block buffer\n    }\n  }\n  //DEBUG_PRINTLN(\"\");\n\n} // receiveEvent\n\n/*******************************************************************************************************************/\n\nstatic const uint32_t PROGMEM crc_table[16] = {\n  0x00000000, 0x1db71064, 0x3b6e20c8, 0x26d930ac,\n  0x76dc4190, 0x6b6b51f4, 0x4db26158, 0x5005713c,\n  0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c,\n  0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c\n};\n\nunsigned long crc_update(unsigned long crc, byte data) {\n  byte tbl_idx;\n  tbl_idx = crc ^ (data >> (0 * 4));\n  crc = pgm_read_dword_near(crc_table + (tbl_idx & 0x0f)) ^ (crc >> 4);\n  tbl_idx = crc ^ (data >> (1 * 4));\n  crc = pgm_read_dword_near(crc_table + (tbl_idx & 0x0f)) ^ (crc >> 4);\n  return crc;\n}\n\nunsigned long crc_buf(char *b, long l) {\n  unsigned long crc = ~0L;\n  for (unsigned long i = 0; i < l; i++)\n    crc = crc_update(crc, ((char *)b)[i]);\n  crc = ~crc;\n  return crc;\n}\n\nunsigned long crc_string(char *s) {\n  unsigned long crc = ~0L;\n  while (*s)\n    crc = crc_update(crc, *s++);\n  crc = ~crc;\n  return crc;\n}\n"
  },
  {
    "path": "rp2040_dac7578/blink_led.cpp",
    "content": "#include \"blink_led.h\"\n\nTicker blinker;\n\nenum {\n  LED_ON,\n  LED_OFF,\n  LED_SLOW,\n  LED_MEDIUM,\n  LED_FAST,\n} ledState;\n\nvoid changeState() {\n  digitalWrite(LED, !(digitalRead(LED)));  // Invert the current state of the LED\n}\n\nvoid ledInit() {\n  pinMode(LED, OUTPUT);\n}\n\nvoid ledOn() {\n  if (ledState != LED_ON) {\n    ledState = LED_ON;\n    blinker.detach();\n    digitalWrite(LED, LOW);\n  }\n}\n\nvoid ledOff() {\n  if (ledState != LED_OFF) {\n    ledState = LED_OFF;\n    blinker.detach();\n    digitalWrite(LED, HIGH);\n  }\n}\n\nvoid ledSlow() {\n  if (ledState != LED_SLOW) {\n    ledState = LED_SLOW;\n    blinker.detach();\n    blinker.attach_ms(1000, changeState);\n  }\n}\n\nvoid ledMedium() {\n  if (ledState != LED_MEDIUM) {\n    ledState = LED_MEDIUM;\n    blinker.detach();\n    blinker.attach_ms(250, changeState);\n  }\n}\n\nvoid ledFast() {\n  if (ledState != LED_FAST) {\n    ledState = LED_FAST;\n    blinker.detach();\n    blinker.attach_ms(100, changeState);\n  }\n}"
  },
  {
    "path": "rp2040_dac7578/blink_led.h",
    "content": "#ifndef _BLINK_LED_H_\n#define _BLINK_LED_H_\n\n#include <Arduino.h>\n#include <Ticker.h>\n\n#define LED D13 //connected to the builtin LED on the Adafruit Feather RP2040\n\nvoid ledInit(void);\nvoid ledOn(void);\nvoid ledOff(void);\nvoid ledSlow(void);\nvoid ledMedium(void);\nvoid ledFast(void);\n\n#endif"
  },
  {
    "path": "rp2040_dac7578/rp2040_dac7578.ino",
    "content": "/* \n * Sketch for an Adafruit RP2040 feather connected to two Adafruit DAC7578 modules. The purpose is to \n * act as a MEG phantom driver, controlling a small current through up to 16 magnetic dipole or \n * equivalent current dipole sources, thereby generating small magnetic fields that can be recorded \n * using an OPM- or SQUID-based MEG system.\n *\n * See \n *   https://learn.adafruit.com/adafruit-feather-rp2040-pico\n *   https://learn.adafruit.com/adafruit-dac7578-8-x-channel-12-bit-i2c-dac\n */\n\n#include <Adafruit_DACX578.h>\n#include <Adafruit_I2CDevice.h>\n#include <math.h>\n\n#include \"blink_led.h\"\n\nAdafruit_DACX578 dac0(12);\nAdafruit_DACX578 dac1(12);\nAdafruit_DACX578 dac2(12);\n\nconst uint8_t nchannel = 24;\n\nconst uint8_t addr0 = 0x4C;  // open pads\nconst uint8_t addr1 = 0x48;  // left pads\nconst uint8_t addr2 = 0x4A;  // right pads\n\nfloat frequency[24] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24 };\nfloat phase[24] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };\nconst float freqshift = 0;\n\nconst uint16_t amplitude = 2048;                                                                 // Half of full scale for 12-bit dac0 (0 to 4095)\nconst uint16_t offset = 2048;                                                                    // DC offset to keep sine wave positive\nconst uint32_t sampleTime = 1000;                                                                // Approximate time between samples, in microseconds\nconst uint32_t feedbackTime = -1;                                                                // Time between serial feedback, in microseconds\n\nvoid setup() {\n  // blink rapidly, this will continue indefinitely when the setup fails\n  ledInit();\n  ledFast();\n\n  Serial.begin(115200);\n  while (!Serial && (millis() < 5000)) {\n    // wait for the serial interface to start, but not longer than 5 seconds\n    delay(100);\n  }\n\n  Serial.println(\"-------------------------------\");\n  Serial.println(\"rp2040 dac7578 sine wave generator\");\n  Serial.print(\"firmware \");\n  Serial.print(__TIME__);\n  Serial.print(\" / \");\n  Serial.println(__DATE__);\n\n  if (nchannel > 0) {\n    // initialize the first DACX578\n    while (!dac0.begin(addr0, &Wire)) {\n      Serial.println(\"Failed to find dac0\");\n      delay(1000);\n    }\n    Serial.println(\"dac0 initialized\");\n  }\n\n  if (nchannel > 8) {\n    // initialize the second DACX578\n    while (!dac1.begin(addr1, &Wire)) {\n      Serial.println(\"Failed to find dac1\");\n      delay(1000);\n    }\n    Serial.println(\"dac1 initialized\");\n  }\n\n  if (nchannel > 16) {\n    // initialize the third DACX578\n    while (!dac2.begin(addr2, &Wire)) {\n      Serial.println(\"Failed to find dac2\");\n      delay(1000);\n    }\n    Serial.println(\"dac2 initialized\");\n  }\n\n  for (uint8_t channel = 0; channel < nchannel; channel++) {\n    // shift the frequency\n    frequency[channel] += freqshift;\n    Serial.print(\"channel \");\n    Serial.print(channel + 1);\n    Serial.print(\" = \");\n    Serial.print(frequency[channel]);\n    Serial.println(\" Hz\");\n  }\n\n  // set I2C frequency to 3.4 MHz for faster communication\n  Wire.setClock(3400000);\n\n  // blink slowly for the rest of the time\n  ledSlow();\n}\n\nvoid loop() {\n  static uint32_t lastSample = micros();\n  static uint32_t lastFeedback = micros();\n\n  uint32_t currentTime = micros();\n  uint32_t deltaTime = currentTime - lastSample;\n\n  if (deltaTime >= sampleTime) {\n\n    for (uint8_t channel = 0; channel < nchannel; channel++) {\n      // increment the phase according to the channels-specific frequency\n      phase[channel] += 2.0 * M_PI * frequency[channel] * ((float)deltaTime / 1000000);\n      if (phase[channel] >= 2.0 * M_PI) {\n        phase[channel] -= 2.0 * M_PI;\n      }\n\n      // compute the new value\n      float sineValue = sin(phase[channel]) * amplitude + offset;\n\n      // set the output to the new value\n      if (channel < 8)\n        // channels 0-7 on the first DAC\n        dac0.writeAndUpdateChannelValue(channel, (uint16_t)sineValue);\n      else if (channel < 16)\n        // channels 8-15 correspond to channels 0-7 on the second DAC\n        dac1.writeAndUpdateChannelValue(channel - 8, (uint16_t)sineValue);\n      else if (channel < 24)\n        // channels 16-23 correspond to channels 0-7 on the third DAC\n        dac2.writeAndUpdateChannelValue(channel - 16, (uint16_t)sineValue);\n    }\n    lastSample = currentTime;\n\n    if ((currentTime - lastFeedback) > feedbackTime) {\n      Serial.println(deltaTime);\n      lastFeedback = currentTime;\n    }\n\n  }  // if deltaTime\n\n}  // loop\n"
  },
  {
    "path": "teensy_cvgate_mcp4725_neopixel/colormap.h",
    "content": "/*\n * This is the \"turbo\" colormap, which resembles the MATLAB \"jet\" colormap\n * See https://ai.googleblog.com/2019/08/turbo-improved-rainbow-colormap-for.html\n * \n * It consists of 256 RGB triplets with values between 0 and 1.\n */\n\nfloat R[] = {0.18995,0.19483,0.19956,0.20415,0.20860,0.21291,0.21708,0.22111,0.22500,0.22875,0.23236,0.23582,0.23915,0.24234,0.24539,0.24830,0.25107,0.25369,0.25618,0.25853,0.26074,0.26280,0.26473,0.26652,0.26816,0.26967,0.27103,0.27226,0.27334,0.27429,0.27509,0.27576,0.27628,0.27667,0.27691,0.27701,0.27698,0.27680,0.27648,0.27603,0.27543,0.27469,0.27381,0.27273,0.27106,0.26878,0.26592,0.26252,0.25862,0.25425,0.24946,0.24427,0.23874,0.23288,0.22676,0.22039,0.21382,0.20708,0.20021,0.19326,0.18625,0.17923,0.17223,0.16529,0.15844,0.15173,0.14519,0.13886,0.13278,0.12698,0.12151,0.11639,0.11167,0.10738,0.10357,0.10026,0.09750,0.09532,0.09377,0.09287,0.09267,0.09320,0.09451,0.09662,0.09958,0.10342,0.10815,0.11374,0.12014,0.12733,0.13526,0.14391,0.15323,0.16319,0.17377,0.18491,0.19659,0.20877,0.22142,0.23449,0.24797,0.26180,0.27597,0.29042,0.30513,0.32006,0.33517,0.35043,0.36581,0.38127,0.39678,0.41229,0.42778,0.44321,0.45854,0.47375,0.48879,0.50362,0.51822,0.53255,0.54658,0.56026,0.57357,0.58646,0.59891,0.61088,0.62233,0.63323,0.64362,0.65394,0.66428,0.67462,0.68494,0.69525,0.70553,0.71577,0.72596,0.73610,0.74617,0.75617,0.76608,0.77591,0.78563,0.79524,0.80473,0.81410,0.82333,0.83241,0.84133,0.85010,0.85868,0.86709,0.87530,0.88331,0.89112,0.89870,0.90605,0.91317,0.92004,0.92666,0.93301,0.93909,0.94489,0.95039,0.95560,0.96049,0.96507,0.96931,0.97323,0.97679,0.98000,0.98289,0.98549,0.98781,0.98986,0.99163,0.99314,0.99438,0.99535,0.99607,0.99654,0.99675,0.99672,0.99644,0.99593,0.99517,0.99419,0.99297,0.99153,0.98987,0.98799,0.98590,0.98360,0.98108,0.97837,0.97545,0.97234,0.96904,0.96555,0.96187,0.95801,0.95398,0.94977,0.94538,0.94084,0.93612,0.93125,0.92623,0.92105,0.91572,0.91024,0.90463,0.89888,0.89298,0.88691,0.88066,0.87422,0.86760,0.86079,0.85380,0.84662,0.83926,0.83172,0.82399,0.81608,0.80799,0.79971,0.79125,0.78260,0.77377,0.76476,0.75556,0.74617,0.73661,0.72686,0.71692,0.70680,0.69650,0.68602,0.67535,0.66449,0.65345,0.64223,0.63082,0.61923,0.60746,0.59550,0.58336,0.57103,0.55852,0.54583,0.53295,0.51989,0.50664,0.49321,0.47960};\nfloat G[] = {0.07176,0.08339,0.09498,0.10652,0.11802,0.12947,0.14087,0.15223,0.16354,0.17481,0.18603,0.19720,0.20833,0.21941,0.23044,0.24143,0.25237,0.26327,0.27412,0.28492,0.29568,0.30639,0.31706,0.32768,0.33825,0.34878,0.35926,0.36970,0.38008,0.39043,0.40072,0.41097,0.42118,0.43134,0.44145,0.45152,0.46153,0.47151,0.48144,0.49132,0.50115,0.51094,0.52069,0.53040,0.54015,0.54995,0.55979,0.56967,0.57958,0.58950,0.59943,0.60937,0.61931,0.62923,0.63913,0.64901,0.65886,0.66866,0.67842,0.68812,0.69775,0.70732,0.71680,0.72620,0.73551,0.74472,0.75381,0.76279,0.77165,0.78037,0.78896,0.79740,0.80569,0.81381,0.82177,0.82955,0.83714,0.84455,0.85175,0.85875,0.86554,0.87211,0.87844,0.88454,0.89040,0.89600,0.90142,0.90673,0.91193,0.91701,0.92197,0.92680,0.93151,0.93609,0.94053,0.94484,0.94901,0.95304,0.95692,0.96065,0.96423,0.96765,0.97092,0.97403,0.97697,0.97974,0.98234,0.98477,0.98702,0.98909,0.99098,0.99268,0.99419,0.99551,0.99663,0.99755,0.99828,0.99879,0.99910,0.99919,0.99907,0.99873,0.99817,0.99739,0.99638,0.99514,0.99366,0.99195,0.98999,0.98775,0.98524,0.98246,0.97941,0.97610,0.97255,0.96875,0.96470,0.96043,0.95593,0.95121,0.94627,0.94113,0.93579,0.93025,0.92452,0.91861,0.91253,0.90627,0.89986,0.89328,0.88655,0.87968,0.87267,0.86553,0.85826,0.85087,0.84337,0.83576,0.82806,0.82025,0.81236,0.80439,0.79634,0.78823,0.78005,0.77181,0.76352,0.75519,0.74682,0.73842,0.73000,0.72140,0.71250,0.70330,0.69382,0.68408,0.67408,0.66386,0.65341,0.64277,0.63193,0.62093,0.60977,0.59846,0.58703,0.57549,0.56386,0.55214,0.54036,0.52854,0.51667,0.50479,0.49291,0.48104,0.46920,0.45740,0.44565,0.43399,0.42241,0.41093,0.39958,0.38836,0.37729,0.36638,0.35566,0.34513,0.33482,0.32473,0.31489,0.30530,0.29599,0.28696,0.27824,0.26981,0.26152,0.25334,0.24526,0.23730,0.22945,0.22170,0.21407,0.20654,0.19912,0.19182,0.18462,0.17753,0.17055,0.16368,0.15693,0.15028,0.14374,0.13731,0.13098,0.12477,0.11867,0.11268,0.10680,0.10102,0.09536,0.08980,0.08436,0.07902,0.07380,0.06868,0.06367,0.05878,0.05399,0.04931,0.04474,0.04028,0.03593,0.03169,0.02756,0.02354,0.01963,0.01583};\nfloat B[] = {0.23217,0.26149,0.29024,0.31844,0.34607,0.37314,0.39964,0.42558,0.45096,0.47578,0.50004,0.52373,0.54686,0.56942,0.59142,0.61286,0.63374,0.65406,0.67381,0.69300,0.71162,0.72968,0.74718,0.76412,0.78050,0.79631,0.81156,0.82624,0.84037,0.85393,0.86692,0.87936,0.89123,0.90254,0.91328,0.92347,0.93309,0.94214,0.95064,0.95857,0.96594,0.97275,0.97899,0.98461,0.98930,0.99303,0.99583,0.99773,0.99876,0.99896,0.99835,0.99697,0.99485,0.99202,0.98851,0.98436,0.97959,0.97423,0.96833,0.96190,0.95498,0.94761,0.93981,0.93161,0.92305,0.91416,0.90496,0.89550,0.88580,0.87590,0.86581,0.85559,0.84525,0.83484,0.82437,0.81389,0.80342,0.79299,0.78264,0.77240,0.76230,0.75237,0.74265,0.73316,0.72393,0.71500,0.70599,0.69651,0.68660,0.67627,0.66556,0.65448,0.64308,0.63137,0.61938,0.60713,0.59466,0.58199,0.56914,0.55614,0.54303,0.52981,0.51653,0.50321,0.48987,0.47654,0.46325,0.45002,0.43688,0.42386,0.41098,0.39826,0.38575,0.37345,0.36140,0.34963,0.33816,0.32701,0.31622,0.30581,0.29581,0.28623,0.27712,0.26849,0.26038,0.25280,0.24579,0.23937,0.23356,0.22835,0.22370,0.21960,0.21602,0.21294,0.21032,0.20815,0.20640,0.20504,0.20406,0.20343,0.20311,0.20310,0.20336,0.20386,0.20459,0.20552,0.20663,0.20788,0.20926,0.21074,0.21230,0.21391,0.21555,0.21719,0.21880,0.22038,0.22188,0.22328,0.22456,0.22570,0.22667,0.22744,0.22800,0.22831,0.22836,0.22811,0.22754,0.22663,0.22536,0.22369,0.22161,0.21918,0.21650,0.21358,0.21043,0.20706,0.20348,0.19971,0.19577,0.19165,0.18738,0.18297,0.17842,0.17376,0.16899,0.16412,0.15918,0.15417,0.14910,0.14398,0.13883,0.13367,0.12849,0.12332,0.11817,0.11305,0.10797,0.10294,0.09798,0.09310,0.08831,0.08362,0.07905,0.07461,0.07031,0.06616,0.06218,0.05837,0.05475,0.05134,0.04814,0.04516,0.04243,0.03993,0.03753,0.03521,0.03297,0.03082,0.02875,0.02677,0.02487,0.02305,0.02131,0.01966,0.01809,0.01660,0.01520,0.01387,0.01264,0.01148,0.01041,0.00942,0.00851,0.00769,0.00695,0.00629,0.00571,0.00522,0.00481,0.00449,0.00424,0.00408,0.00401,0.00401,0.00410,0.00427,0.00453,0.00486,0.00529,0.00579,0.00638,0.00705,0.00780,0.00863,0.00955,0.01055};\n"
  },
  {
    "path": "teensy_cvgate_mcp4725_neopixel/teensy_cvgate_mcp4725_neopixel.ino",
    "content": "/*\n* EEGSynth Teensy based CV/Gate controller. This sketch allows\n* one to use control voltages and gates to interface a computer\n* with an analog synthesizer. The hardware comprises a Teensy \n* with two MCP4725 12-bit DAC breakout boards.\n*\n* Some example sequences of characters are\n*   *c1v1024#  control 1 voltage 5*1024/4095 = 1.25 V\n*   *g1v1#     gate 1 value ON\n*\n* This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.\n* See http://creativecommons.org/licenses/by-sa/4.0/\n*\n* Copyright (C) 2020, Robert Oostenveld, http://www.eegsynth.org/\n*/\n\n#include <Wire.h>//Include the Wire library to talk I2C\n#include <Adafruit_NeoPixel.h>\n\n#include \"colormap.h\"\n\n#define NEOPIXEL_PIN 14\n#define GATE1_PIN    15\n#define GATE2_PIN    16\n#define WIRE_SDAPIN  18\n#define WIRE_SCLPIN  19\n\n// 0x60 is the I2C Address of the MCP4725, by default (A0 pulled to GND).\n// Please note that this breakout is for the MCP4725A0.\n// For devices with A0 pulled HIGH, use 0x61\n#define address1 0x60\n#define address2 0x61\n\n#define MCP4726_CMD_WRITEDAC            (0x40)  // Writes data to the DAC\n#define MCP4726_CMD_WRITEDACEEPROM      (0x60)  // Writes data to the DAC and the EEPROM (persisting the assigned value after reset)\n\n// the values of the DAC range from 0 to 4095 (12 bits)\n#define MAXVALUE  4095.\n\n// these are used to interpret the commands over the serial interface\n#define NONE      0\n#define VOLTAGE   1\n#define GATE      2\n\n// this is the status after parsing the commands over the serial interface\n#define OK       0\n#define ERROR   -1\n\n#define NUMPIXELS 4\n#define BRIGHTNESS 0.3\n\nAdafruit_NeoPixel pixels(NUMPIXELS, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800);\n\n// tghese are the initial values of the output control voltages and gates\nint voltage1 = 0, voltage2 = 0;\nint gate1 = 0, gate2 = 0;\n\nvoid setColor(int led, float value) {\n  byte r, g, b;\n  int index = value*255.;\n  index = (index > 255 ? 255 : index);  // must be between 0 and 255\n  r = 255*R[index]*BRIGHTNESS;\n  g = 255*G[index]*BRIGHTNESS;\n  b = 255*B[index]*BRIGHTNESS;\n  pixels.setPixelColor(led, pixels.Color(r, g, b));\n  pixels.show();\n  return;\n}\n\nvoid setValue(uint16_t address, uint16_t value) {\n  uint8_t msb, lsb;\n  msb = (value / 16);\n  lsb = (value % 16) << 4;\n  Wire.beginTransmission(address);\n  Wire.write(MCP4726_CMD_WRITEDAC);   // cmd to update the DAC\n  Wire.write(msb);                    // the 8 most significant bits...\n  Wire.write(lsb);                    // the 4 least significant bits...\n  Wire.endTransmission();\n  return;\n}\n\nvoid setup() {\n  // initialize the serial communication:\n  while (!Serial) {;}\n  Serial.begin(115200);\n  Serial.print(\"\\n[teensy_cvgate_mcp4725_neopixel/ \");\n  Serial.print(__DATE__);\n  Serial.print(\" / \");\n  Serial.print(__TIME__);\n  Serial.println(\"]\");\n  Serial.setTimeout(1000);\n\n  Wire.begin();\n  Wire.setSDA(WIRE_SDAPIN);\n  Wire.setSCL(WIRE_SCLPIN);\n\n  // initialize the gate pins as output:\n  pinMode(GATE1_PIN, OUTPUT);\n  pinMode(GATE2_PIN, OUTPUT);\n\n  // INITIALIZE NeoPixel strip object\n  pixels.begin(); \n  delay(1000);\n  \n  // Set all pixel colors to 'off'\n  pixels.clear(); \n\n  // Set the pixels to RGBW to indicate that setup is done\n  pixels.setPixelColor(0, pixels.Color(128, 0, 0));\n  pixels.show();\n  delay(250);\n  pixels.setPixelColor(1, pixels.Color(0, 128, 0));\n  pixels.show();\n  delay(250);\n  pixels.setPixelColor(2, pixels.Color(0, 0, 128));\n  pixels.show();\n  delay(250);\n  pixels.setPixelColor(3, pixels.Color(128, 128, 128));\n  pixels.show();\n  delay(250);\n\n  Serial.println(\"Setup done.\");\n  return;\n}\n\nvoid loop() {\n  byte b, channel = 0, command = NONE, status = OK;\n  int value = 0;\n\n  if (Serial.available()) {\n\n    // parse the input over the serial connection\n    b = Serial.read();\n    if (b == '*') {\n      Serial.readBytes(&b, 1);\n      if (b == 'c') {\n        command = VOLTAGE;\n        value = 0;\n        Serial.readBytes(&b, 1); channel = b - 48; // character '1' is ascii value 49\n        Serial.readBytes(&b, 1); // 'v'\n        Serial.readBytes(&b, 1); value  = (b - 48) * 1000;\n        Serial.readBytes(&b, 1); value += (b - 48) * 100;\n        Serial.readBytes(&b, 1); value += (b - 48) * 10;\n        Serial.readBytes(&b, 1); value += (b - 48) * 1;\n        Serial.readBytes(&b, 1); command = (b == '#' ? command : NONE);\n      }\n      else if (b == 'g') {\n        command = GATE;\n        Serial.readBytes(&b, 1); channel = b - 48; // character '1' is ascii value 49\n        Serial.readBytes(&b, 1); // 'v'\n        Serial.readBytes(&b, 1); value  = (b == '1');\n        Serial.readBytes(&b, 1); command = (b == '#' ? command : NONE);\n      }\n      else {\n        command = NONE;\n      }\n    }\n    else {\n      command = NONE;\n    }\n\n    // update the internal state of all output channels\n    if (command == VOLTAGE) {\n      switch (channel) {\n        case 1:\n          voltage1 = (value > MAXVALUE ? MAXVALUE : value);\n          status = OK;\n          break;\n        case 2:\n          voltage2 = (value > MAXVALUE ? MAXVALUE : value);\n          status = OK;\n          break;\n        default:\n          status = ERROR;\n      }\n\n    }\n    else if (command == GATE) {\n      switch (channel) {\n        case 1:\n          gate1 = (value != 0);\n          status = OK;\n          break;\n        case 2:\n          gate2 = (value != 0);\n          status = OK;\n          break;\n        default:\n          status = ERROR;\n      }\n    }\n    else {\n      status = ERROR;\n    }\n    if (status == OK)\n      Serial.println(\"OK\");\n    else if (status == ERROR)\n      Serial.println(\"error\");\n  }\n  else {\n    // update the output cointrol voltages and gates\n      setValue(address1, voltage1);\n      setValue(address2, voltage2);\n      digitalWrite(GATE1_PIN, gate1);\n      digitalWrite(GATE2_PIN, gate2);\n\n    // update the Neopixels by mapping a value between 0 and 1 onto the colormap\n    // note that they are mounted in the opposite order than the 3.5 mm jacks\n\n    if (!Serial) {\n      // switch all pixels off when there is no serial connection\n      pixels.clear(); \n      pixels.show(); \n    }\n    else {\n      // the integer representation of the control voltage is represented between 0 and 4095\n      setColor(3, voltage1/MAXVALUE);\n      setColor(2, voltage2/MAXVALUE); \n      // the integer representation of the gate voltage is either 0 or 1\n      setColor(1, gate1); \n      setColor(0, gate2); \n    }\n  }\n} //main\n"
  },
  {
    "path": "teensy_gps_temp_ttn/payload.h",
    "content": "\n// This is identical to the messages I send with my RFM12b nodes\nstruct payload_t {\n  unsigned int id;\n  unsigned int counter;\n  float value1;\n  float value2;\n  float value3;\n  float value4;\n  float value5;\n  unsigned int crc;\n};\n\n"
  },
  {
    "path": "teensy_gps_temp_ttn/sensor.cpp",
    "content": "// Include the libraries we need\n#include <OneWire.h>\n#include <DallasTemperature.h>\n#include <TinyGPS++.h>\n\n#include \"payload.h\"\nextern payload_t payload;\n\n// Data wire is plugged into port 15 on the Teensy\n#define ONE_WIRE_BUS 15\n\n// pin 13 corresponds to the onboard LED, but it is already used for SPI\n// hence we are using an external LED\n#define LED 14 \n\n// Setup a oneWire instance to communicate with any OneWire devices (not just Maxim/Dallas temperature ICs)\nOneWire oneWire(ONE_WIRE_BUS);\n\n// Pass our oneWire reference to Dallas Temperature.\nDallasTemperature dallasTemperature(&oneWire);\n\n// TinyGPS++ requires the use of SoftwareSerial, and assumes that you have a\n// 4800-baud serial GPS device hooked up on pins 4(rx) and 3(tx).\nstatic const int RXPin = 4, TXPin = 3;\nstatic const uint32_t GPSBaud = 9600;\n\n// The TinyGPS++ object\nTinyGPSPlus gps;\n\n// The serial connection to the GPS device\n#define ss Serial1\n\n// These functions are defined further down\nuint32_t readVcc();\nunsigned long crc_buf(char *b, unsigned long l);\n\n/*****************************************************************************/\n\nvoid init_sensor() {\n    pinMode(LED, OUTPUT);\n  \n  // Start up the GPS serial port\n  ss.begin(GPSBaud);\n\n  // Start up the Wire interface\n  dallasTemperature.begin();\n\n  // Start up the internal voltage measurement\n  analogReference(EXTERNAL);\n  analogReadResolution(12);\n  analogReadAveraging(32); // this one is optional\n}\n\n/*****************************************************************************/\n\nvoid read_sensor() {\n  float lat = 0, lng = 0, alt = 0, temp = 0, volt = 0;\n  int verbose = 1;\n  \n  digitalWrite(LED, HIGH);\n\n  volt = 0.001 * readVcc(); // this is in mV, we want V\n\n  dallasTemperature.requestTemperatures();\n  temp = dallasTemperature.getTempCByIndex(0);\n\n  unsigned long timeout = millis() + 1000;\n  while (millis() < timeout) {\n    // keep reading until a full sentence is correctly encoded\n    if (ss.available() > 0) {\n      if (gps.encode(ss.read()) || millis() > timeout) {\n        timeout = millis();\n        if (gps.location.isValid()) {\n          lat = gps.location.lat();\n          lng = gps.location.lng();\n          alt = gps.altitude.meters();\n        }\n      }\n    }\n    else {\n      // wait for some new data to come in from the GPS\n      delay(10);\n    }\n  }\n\n  payload.value1      = volt;\n  payload.value2      = temp;\n  payload.value3      = lat;\n  payload.value4      = lng;\n  payload.value5      = alt;\n  payload.counter    += 1;\n  payload.crc         = crc_buf((char *)&payload, sizeof(payload_t) - sizeof(unsigned long));\n\n  if (verbose) {\n    Serial.print(payload.id);\n    Serial.print(\",\\t\");\n    Serial.print(payload.counter);\n    Serial.print(\",\\t\");\n    Serial.print(payload.value1, 2);\n    Serial.print(\",\\t\");\n    Serial.print(payload.value2, 2);\n    Serial.print(\",\\t\");\n    Serial.print(payload.value3, 6);\n    Serial.print(\",\\t\");\n    Serial.print(payload.value4, 6);\n    Serial.print(\",\\t\");\n    Serial.print(payload.value5, 2);\n    Serial.print(\",\\t\");\n    Serial.println(payload.crc);\n  }\n    digitalWrite(LED, LOW);\n}\n\n/*****************************************************************************/\n\n// see https://forum.pjrc.com/threads/26117-Teensy-3-1-Voltage-sensing-and-low-battery-alert\n// for Teensy 3.0, only valid between 2.0V and 3.5V. Returns in millivolts.\nuint32_t readVcc() {\n  uint32_t x = analogRead(39);\n  return (178 * x * x + 2688757565 - 1184375 * x) / 372346;\n};\n\n/*****************************************************************************/\n\nstatic PROGMEM prog_uint32_t crc_table[16] = {\n  0x00000000, 0x1db71064, 0x3b6e20c8, 0x26d930ac,\n  0x76dc4190, 0x6b6b51f4, 0x4db26158, 0x5005713c,\n  0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c,\n  0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c\n};\n\n/*****************************************************************************/\n\nunsigned long crc_update(unsigned long crc, byte data)\n{\n  byte tbl_NODEx;\n  tbl_NODEx = crc ^ (data >> (0 * 4));\n  crc = pgm_read_dword_near(crc_table + (tbl_NODEx & 0x0f)) ^ (crc >> 4);\n  tbl_NODEx = crc ^ (data >> (1 * 4));\n  crc = pgm_read_dword_near(crc_table + (tbl_NODEx & 0x0f)) ^ (crc >> 4);\n  return crc;\n}\n\n/*****************************************************************************/\n\nunsigned long crc_buf(char *b, unsigned long l)\n{\n  unsigned long crc = ~0L;\n  for (unsigned long i = 0; i < l; i++)\n    crc = crc_update(crc, ((char *)b)[i]);\n  crc = ~crc;\n  return crc;\n}\n\n/*****************************************************************************/\n\nunsigned long crc_string(char *s)\n{\n  unsigned long crc = ~0L;\n  while (*s)\n    crc = crc_update(crc, *s++);\n  crc = ~crc;\n  return crc;\n}\n\n"
  },
  {
    "path": "teensy_gps_temp_ttn/teensy_gps_temp_ttn.ino",
    "content": "#include <lmic.h>\n#include <hal/hal.h>\n#include <SPI.h>\n\n// the LoRa keys and IDs are secret and should not be disclosed\n#include \"secret.h\"\n//static const PROGMEM u1_t NWKSKEY[16] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };\n//static const u1_t PROGMEM APPSKEY[16] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };\n//static const u4_t DEVADDR = 0x00000000;\n\n#include \"payload.h\"\npayload_t payload;\n\nvoid init_sensor();\nvoid read_sensor();\n\n// These callbacks are only used in over-the-air activation, so they are\n// left empty here (we cannot leave them out completely unless\n// DISABLE_JOIN is set in config.h, otherwise the linker will complain).\nvoid os_getArtEui (u1_t* buf) { }\nvoid os_getDevEui (u1_t* buf) { }\nvoid os_getDevKey (u1_t* buf) { }\n\nstatic osjob_t sendjob;\n\n// Schedule TX every this many seconds (might become longer due to duty cycle limitations).\nconst unsigned TX_INTERVAL = 10;\n\n// Pin mapping\nconst lmic_pinmap lmic_pins = {\n  .nss = 10,\n  .rxtx = LMIC_UNUSED_PIN,\n  .rst = 9,\n  .dio = {2, 5, 6},\n};\n\nvoid onEvent (ev_t ev) {\n  Serial.print(os_getTime());\n  Serial.print(\": \");\n  switch (ev) {\n    case EV_SCAN_TIMEOUT:\n      Serial.println(F(\"EV_SCAN_TIMEOUT\"));\n      break;\n    case EV_BEACON_FOUND:\n      Serial.println(F(\"EV_BEACON_FOUND\"));\n      break;\n    case EV_BEACON_MISSED:\n      Serial.println(F(\"EV_BEACON_MISSED\"));\n      break;\n    case EV_BEACON_TRACKED:\n      Serial.println(F(\"EV_BEACON_TRACKED\"));\n      break;\n    case EV_JOINING:\n      Serial.println(F(\"EV_JOINING\"));\n      break;\n    case EV_JOINED:\n      Serial.println(F(\"EV_JOINED\"));\n      break;\n    case EV_RFU1:\n      Serial.println(F(\"EV_RFU1\"));\n      break;\n    case EV_JOIN_FAILED:\n      Serial.println(F(\"EV_JOIN_FAILED\"));\n      break;\n    case EV_REJOIN_FAILED:\n      Serial.println(F(\"EV_REJOIN_FAILED\"));\n      break;\n    case EV_TXCOMPLETE:\n      Serial.println(F(\"EV_TXCOMPLETE (includes waiting for RX windows)\"));\n      if (LMIC.dataLen) {\n        // data received in rx slot after tx\n        Serial.print(F(\"Data Received: \"));\n        Serial.write(LMIC.frame + LMIC.dataBeg, LMIC.dataLen);\n        Serial.println();\n      }\n      // Schedule next transmission\n      os_setTimedCallback(&sendjob, os_getTime() + sec2osticks(TX_INTERVAL), do_send);\n      break;\n    case EV_LOST_TSYNC:\n      Serial.println(F(\"EV_LOST_TSYNC\"));\n      break;\n    case EV_RESET:\n      Serial.println(F(\"EV_RESET\"));\n      break;\n    case EV_RXCOMPLETE:\n      // data received in ping slot\n      Serial.println(F(\"EV_RXCOMPLETE\"));\n      break;\n    case EV_LINK_DEAD:\n      Serial.println(F(\"EV_LINK_DEAD\"));\n      break;\n    case EV_LINK_ALIVE:\n      Serial.println(F(\"EV_LINK_ALIVE\"));\n      break;\n    default:\n      Serial.println(F(\"Unknown event\"));\n      break;\n  }\n}\n\nvoid do_send(osjob_t* j) {\n  // Check if there is not a current TX/RX job running\n  if (LMIC.opmode & OP_TXRXPEND) {\n    Serial.println(F(\"OP_TXRXPEND, not sending\"));\n  } else {\n    // get the sensor data\n    read_sensor();\n    // Prepare upstream data transmission at the next possible time.\n    LMIC_setTxData2(1, (unsigned char *)&payload, sizeof(payload_t), 0);\n    Serial.println(F(\"Packet queued\"));\n  }\n  // Next TX is scheduled after TX_COMPLETE event.\n}\n\nvoid setup() {\n  Serial.begin(115200);\n\n  unsigned long timeout = millis() + 1000;\n  while (!Serial && millis() < timeout) {\n    ; // wait for serial port to connect. Needed for native USB\n  }\n  Serial.println(F(\"Starting\"));\n\n  // initialize the sensors\n  init_sensor();\n\n  // Start with an empty packet\n  payload.id = 1;\n  payload.counter = 0;\n\n  // LMIC init\n  os_init();\n\n  // Reset the MAC state. Session and pending data transfers will be discarded.\n  LMIC_reset();\n\n  // Set static session parameters. Instead of dynamically establishing a session\n  // by joining the network, precomputed session parameters are be provided.\n#ifdef PROGMEM\n  // On AVR, these values are stored in flash and only copied to RAM\n  // once. Copy them to a temporary buffer here, LMIC_setSession will\n  // copy them into a buffer of its own again.\n  uint8_t appskey[sizeof(APPSKEY)];\n  uint8_t nwkskey[sizeof(NWKSKEY)];\n  memcpy_P(appskey, APPSKEY, sizeof(APPSKEY));\n  memcpy_P(nwkskey, NWKSKEY, sizeof(NWKSKEY));\n  LMIC_setSession (0x1, DEVADDR, nwkskey, appskey);\n#else\n  // If not running an AVR with PROGMEM, just use the arrays directly\n  LMIC_setSession (0x1, DEVADDR, NWKSKEY, APPSKEY);\n#endif\n\n  // Set up the channels used by the Things Network, which corresponds\n  // to the defaults of most gateways. Without this, only three base\n  // channels from the LoRaWAN specification are used, which certainly\n  // works, so it is good for debugging, but can overload those\n  // frequencies, so be sure to configure the full frequency range of\n  // your network here (unless your network autoconfigures them).\n  // Setting up channels should happen after LMIC_setSession, as that\n  // configures the minimal channel set.\n  LMIC_setupChannel(0, 868100000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band\n  LMIC_setupChannel(1, 868300000, DR_RANGE_MAP(DR_SF12, DR_SF7B), BAND_CENTI);      // g-band\n  LMIC_setupChannel(2, 868500000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band\n  LMIC_setupChannel(3, 867100000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band\n  LMIC_setupChannel(4, 867300000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band\n  LMIC_setupChannel(5, 867500000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band\n  LMIC_setupChannel(6, 867700000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band\n  LMIC_setupChannel(7, 867900000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band\n  LMIC_setupChannel(8, 868800000, DR_RANGE_MAP(DR_FSK,  DR_FSK),  BAND_MILLI);      // g2-band\n  // TTN defines an additional channel at 869.525Mhz using SF9 for class B\n  // devices' ping slots. LMIC does not have an easy way to define set this\n  // frequency and support for class B is spotty and untested, so this\n  // frequency is not configured here.\n\n  // Disable link check validation\n  LMIC_setLinkCheckMode(0);\n\n  // Set data rate and transmit power (note: txpow seems to be ignored by the library)\n  LMIC_setDrTxpow(DR_SF7, 14);\n\n  // Start job\n  do_send(&sendjob);\n}\n\nvoid loop() {\n  os_runloop_once();\n}\n"
  },
  {
    "path": "teensy_midifilter/midiname.c",
    "content": "// To give your project a unique name, this code must be\n// placed into a .c file (its own tab).  It can not be in\n// a .cpp file or your main sketch (the .ino file).\n\n#include \"usb_names.h\"\n\n// Edit these lines to create your own name.  The length must\n// match the number of characters in your custom name.\n\n#define MIDI_NAME   {'T','e','e','n','s','y'}\n#define MIDI_NAME_LEN  6\n\n// Do not change this part.  This exact format is required by USB.\n\nstruct usb_string_descriptor_struct usb_string_product_name = {\n        2 + MIDI_NAME_LEN * 2,\n        3,\n        MIDI_NAME\n};\n"
  },
  {
    "path": "teensy_midifilter/teensy_midifilter.ino",
    "content": "#include <MIDI.h>\n\n// select \"tools\"->\"usb type\"->\"serial + MIDI\" to instantiate the usbMIDI device\nMIDI_CREATE_INSTANCE(HardwareSerial, Serial1, inMIDI);\nMIDI_CREATE_INSTANCE(HardwareSerial, Serial2, outMIDI);\n\n// #define DEBUG_NOTE\n// #define DEBUG_SERIAL\n\n/************************************************************************************************************/\n\nvoid handleNoteOff(byte Channel, byte NoteNumber, byte Velocity) {\n  outMIDI.sendNoteOff(NoteNumber, Velocity, Channel);\n};\n\nvoid handleNoteOn(byte Channel, byte NoteNumber, byte Velocity) {\n  outMIDI.sendNoteOn(NoteNumber, Velocity, Channel);\n};\n\nvoid handleAfterTouchPoly(byte Channel, byte NoteNumber, byte Pressure) {\n  outMIDI.sendPolyPressure(NoteNumber, Pressure, Channel);\n};\n\nvoid handleControlChange(byte Channel, byte ControlNumber, byte ControlValue) {\n  outMIDI.sendControlChange(ControlNumber, ControlValue, Channel);\n};\n\nvoid handleProgramChange(byte Channel, byte ProgramNumber) {\n  outMIDI.sendProgramChange(ProgramNumber, Channel);\n};\n\nvoid handleAfterTouchChannel(byte Channel, byte Pressure) {\n  outMIDI.sendAfterTouch(Pressure, Channel);\n};\n\nvoid handlePitchBend(byte Channel, int PitchValue) {\n  outMIDI.sendPitchBend(PitchValue, Channel);\n};\n\nvoid handleSystemExclusive(byte* Array, unsigned Size) {\n  outMIDI.sendSysEx(Size, Array);\n};\n\nvoid handleTimeCodeQuarterFrame(byte Data) {\n  outMIDI.sendTimeCodeQuarterFrame(Data);\n};\n\nvoid handleSongPosition(unsigned int Beats) {\n  outMIDI.sendSongPosition(Beats);\n};\n\nvoid handleSongSelect(byte SongNumber) {\n  outMIDI.sendSongSelect(SongNumber);\n};\n\nvoid handleTuneRequest(void) {\n  outMIDI.sendTuneRequest();\n};\n\nvoid handleClock(void) {\n  outMIDI.sendRealTime(midi::Clock);\n};\n\nvoid handleStart(void) {\n  outMIDI.sendRealTime(midi::Start);\n};\n\nvoid handleContinue(void) {\n  outMIDI.sendRealTime(midi::Continue);\n};\n\nvoid handleStop(void) {\n  outMIDI.sendRealTime(midi::Stop);\n};\n\nvoid handleActiveSensing(void) {\n  outMIDI.sendRealTime(midi::ActiveSensing);\n};\n\nvoid handleSystemReset(void) {\n  outMIDI.sendRealTime(midi::SystemReset);\n};\n\n/************************************************************************************************************/\n\nlong prev = 0;\nconst int led = 13;\n\n#ifdef DEBUG_NOTE\nconst int channel = 1;\nconst int note = 42;\nconst int velocity = 127;\n#endif\n\n/************************************************************************************************************/\n\nvoid setup() {\n  \n  Serial.print(\"\\n[teensy_midifilter / \");\n  Serial.print(__DATE__);\n  Serial.print(\" / \");\n  Serial.print(__TIME__);\n  Serial.println(\"]\");\n    \n  pinMode(led, OUTPUT);\n\n  outMIDI.begin(MIDI_CHANNEL_OMNI);\n  inMIDI.begin(MIDI_CHANNEL_OMNI);\n\n  inMIDI.setHandleNoteOff(handleNoteOff);\n  inMIDI.setHandleNoteOn(handleNoteOn);\n  inMIDI.setHandleAfterTouchPoly(handleAfterTouchPoly);\n  inMIDI.setHandleControlChange(handleControlChange);\n  inMIDI.setHandleProgramChange(handleProgramChange);\n  inMIDI.setHandleAfterTouchChannel(handleAfterTouchChannel);\n  inMIDI.setHandlePitchBend(handlePitchBend);\n  inMIDI.setHandleSystemExclusive(handleSystemExclusive);\n  inMIDI.setHandleTimeCodeQuarterFrame(handleTimeCodeQuarterFrame);\n  inMIDI.setHandleSongPosition(handleSongPosition);\n  inMIDI.setHandleSongSelect(handleSongSelect);\n  inMIDI.setHandleTuneRequest(handleTuneRequest);\n  inMIDI.setHandleClock(handleClock);\n  //inMIDI.setHandleStart(handleStart);\n  //inMIDI.setHandleContinue(handleContinue);\n  //inMIDI.setHandleStop(handleStop);\n  inMIDI.setHandleActiveSensing(handleActiveSensing);\n  inMIDI.setHandleSystemReset(handleSystemReset);\n}\n\n/************************************************************************************************************/\n\nvoid loop() {\n\n  if ((millis() - prev) > 1000) {\n    // blink every second\n    digitalWrite(led, !digitalRead(led));\n#ifdef DEBUG_NOTE\n    // send a MIDI note every second\n    outMIDI.sendNoteOn(note, velocity, channel);\n    usbMIDI.sendNoteOn(note, velocity, channel);\n#endif\n    prev = millis();\n  }\n\n  while (inMIDI.read()) {\n#ifdef DEBUG_SERIAL\n    Serial.print(inMIDI.getType());\n    Serial.print(\" \");\n    switch (inMIDI.getType()) {\n      case midi::ActiveSensing:\n        Serial.println(\"ActiveSensing\");\n        break;\n      case midi::AfterTouchChannel:\n        Serial.println(\"AfterTouchChannel\");\n        break;\n      case midi::AfterTouchPoly:\n        Serial.println(\"AfterTouchPoly\");\n        break;\n      case midi::Clock:\n        Serial.println(\"Clock\");\n        break;\n      case midi::Continue:\n        Serial.println(\"Continue\");\n        break;\n      case midi::ControlChange:\n        Serial.println(\"ControlChange\");\n        break;\n      case midi::InvalidType:\n        Serial.println(\"InvalidType\");\n        break;\n      case midi::NoteOff:\n        Serial.println(\"NoteOff\");\n        break;\n      case midi::NoteOn:\n        Serial.println(\"NoteOn\");\n        break;\n      case midi::PitchBend:\n        Serial.println(\"PitchBend\");\n        break;\n      case midi::ProgramChange:\n        Serial.println(\"ProgramChange\");\n        break;\n      case midi::SongPosition:\n        Serial.println(\"SongPosition\");\n        break;\n      case midi::SongSelect:\n        Serial.println(\"SongSelect\");\n        break;\n      case midi::Start:\n        Serial.println(\"Start\");\n        break;\n      case midi::Stop:\n        Serial.println(\"Stop\");\n        break;\n      case midi::SystemExclusive:\n        Serial.println(\"SystemExclusive\");\n        break;\n      case midi::SystemReset:\n        Serial.println(\"SystemReset\");\n        break;\n      case midi::TimeCodeQuarterFrame:\n        Serial.println(\"TimeCodeQuarterFrame\");\n        break;\n      case midi::TuneRequest:\n        Serial.println(\"TuneRequest\");\n      default:\n        Serial.println(\"Other\");\n        break;\n    }\n#endif\n  }\n}\n"
  },
  {
    "path": "teensy_ttn1/teensy_ttn1.ino",
    "content": "#include <lmic.h>\n#include <hal/hal.h>\n#include <SPI.h>\n\n// the LoRa keys and IDs are secret and should not be disclosed\n#include \"secret.h\"\n//static const PROGMEM u1_t NWKSKEY[16] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };\n//static const u1_t PROGMEM APPSKEY[16] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };\n//static const u4_t DEVADDR = 0x00000000;\n\n// These callbacks are only used in over-the-air activation, so they are\n// left empty here (we cannot leave them out completely unless\n// DISABLE_JOIN is set in config.h, otherwise the linker will complain).\nvoid os_getArtEui (u1_t* buf) { }\nvoid os_getDevEui (u1_t* buf) { }\nvoid os_getDevKey (u1_t* buf) { }\n\nstatic uint8_t mydata[] = \"Hello, world!\";\nstatic osjob_t sendjob;\n\n// Schedule TX every this many seconds (might become longer due to duty cycle limitations).\nconst unsigned TX_INTERVAL = 15;\n\n// Pin mapping\nconst lmic_pinmap lmic_pins = {\n    .nss = 10,\n    .rxtx = LMIC_UNUSED_PIN,\n    .rst = 9,\n    .dio = {2, 5, 6},\n};\n\nvoid onEvent (ev_t ev) {\n    Serial.print(os_getTime());\n    Serial.print(\": \");\n    switch(ev) {\n        case EV_SCAN_TIMEOUT:\n            Serial.println(F(\"EV_SCAN_TIMEOUT\"));\n            break;\n        case EV_BEACON_FOUND:\n            Serial.println(F(\"EV_BEACON_FOUND\"));\n            break;\n        case EV_BEACON_MISSED:\n            Serial.println(F(\"EV_BEACON_MISSED\"));\n            break;\n        case EV_BEACON_TRACKED:\n            Serial.println(F(\"EV_BEACON_TRACKED\"));\n            break;\n        case EV_JOINING:\n            Serial.println(F(\"EV_JOINING\"));\n            break;\n        case EV_JOINED:\n            Serial.println(F(\"EV_JOINED\"));\n            break;\n        case EV_RFU1:\n            Serial.println(F(\"EV_RFU1\"));\n            break;\n        case EV_JOIN_FAILED:\n            Serial.println(F(\"EV_JOIN_FAILED\"));\n            break;\n        case EV_REJOIN_FAILED:\n            Serial.println(F(\"EV_REJOIN_FAILED\"));\n            break;\n        case EV_TXCOMPLETE:\n            Serial.println(F(\"EV_TXCOMPLETE (includes waiting for RX windows)\"));\n            if(LMIC.dataLen) {\n                // data received in rx slot after tx\n                Serial.print(F(\"Data Received: \"));\n                Serial.write(LMIC.frame+LMIC.dataBeg, LMIC.dataLen);\n                Serial.println();\n            }\n            // Schedule next transmission\n            os_setTimedCallback(&sendjob, os_getTime()+sec2osticks(TX_INTERVAL), do_send);\n            break;\n        case EV_LOST_TSYNC:\n            Serial.println(F(\"EV_LOST_TSYNC\"));\n            break;\n        case EV_RESET:\n            Serial.println(F(\"EV_RESET\"));\n            break;\n        case EV_RXCOMPLETE:\n            // data received in ping slot\n            Serial.println(F(\"EV_RXCOMPLETE\"));\n            break;\n        case EV_LINK_DEAD:\n            Serial.println(F(\"EV_LINK_DEAD\"));\n            break;\n        case EV_LINK_ALIVE:\n            Serial.println(F(\"EV_LINK_ALIVE\"));\n            break;\n         default:\n            Serial.println(F(\"Unknown event\"));\n            break;\n    }\n}\n\nvoid do_send(osjob_t* j){\n    // Check if there is not a current TX/RX job running\n    if (LMIC.opmode & OP_TXRXPEND) {\n        Serial.println(F(\"OP_TXRXPEND, not sending\"));\n    } else {\n        // Prepare upstream data transmission at the next possible time.\n        LMIC_setTxData2(1, mydata, sizeof(mydata)-1, 0);\n        Serial.println(F(\"Packet queued\"));\n    }\n    // Next TX is scheduled after TX_COMPLETE event.\n}\n\nvoid setup() {\n    Serial.begin(115200);\n    while (!Serial) {\n      ; // wait for serial port to connect. Needed for native USB\n    }\n    Serial.println(F(\"Starting\"));\n\n    #ifdef VCC_ENABLE\n    // For Pinoccio Scout boards\n    pinMode(VCC_ENABLE, OUTPUT);\n    digitalWrite(VCC_ENABLE, HIGH);\n    delay(1000);\n    #endif\n\n    // LMIC init\n    os_init();\n    // Reset the MAC state. Session and pending data transfers will be discarded.\n    LMIC_reset();\n\n    // Set static session parameters. Instead of dynamically establishing a session\n    // by joining the network, precomputed session parameters are be provided.\n    #ifdef PROGMEM\n    // On AVR, these values are stored in flash and only copied to RAM\n    // once. Copy them to a temporary buffer here, LMIC_setSession will\n    // copy them into a buffer of its own again.\n    uint8_t appskey[sizeof(APPSKEY)];\n    uint8_t nwkskey[sizeof(NWKSKEY)];\n    memcpy_P(appskey, APPSKEY, sizeof(APPSKEY));\n    memcpy_P(nwkskey, NWKSKEY, sizeof(NWKSKEY));\n    LMIC_setSession (0x1, DEVADDR, nwkskey, appskey);\n    #else\n    // If not running an AVR with PROGMEM, just use the arrays directly\n    LMIC_setSession (0x1, DEVADDR, NWKSKEY, APPSKEY);\n    #endif\n\n    // Set up the channels used by the Things Network, which corresponds\n    // to the defaults of most gateways. Without this, only three base\n    // channels from the LoRaWAN specification are used, which certainly\n    // works, so it is good for debugging, but can overload those\n    // frequencies, so be sure to configure the full frequency range of\n    // your network here (unless your network autoconfigures them).\n    // Setting up channels should happen after LMIC_setSession, as that\n    // configures the minimal channel set.\n    LMIC_setupChannel(0, 868100000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band\n    LMIC_setupChannel(1, 868300000, DR_RANGE_MAP(DR_SF12, DR_SF7B), BAND_CENTI);      // g-band\n    LMIC_setupChannel(2, 868500000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band\n    LMIC_setupChannel(3, 867100000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band\n    LMIC_setupChannel(4, 867300000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band\n    LMIC_setupChannel(5, 867500000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band\n    LMIC_setupChannel(6, 867700000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band\n    LMIC_setupChannel(7, 867900000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band\n    LMIC_setupChannel(8, 868800000, DR_RANGE_MAP(DR_FSK,  DR_FSK),  BAND_MILLI);      // g2-band\n    // TTN defines an additional channel at 869.525Mhz using SF9 for class B\n    // devices' ping slots. LMIC does not have an easy way to define set this\n    // frequency and support for class B is spotty and untested, so this\n    // frequency is not configured here.\n\n    // Disable link check validation\n    LMIC_setLinkCheckMode(0);\n\n    // Set data rate and transmit power (note: txpow seems to be ignored by the library)\n    LMIC_setDrTxpow(DR_SF7,14);\n\n    // Start job\n    do_send(&sendjob);\n}\n\nvoid loop() {\n    os_runloop_once();\n}\n"
  },
  {
    "path": "teensy_ttn2/teensy_ttn2.ino",
    "content": "#include <lmic.h>\n#include <hal/hal.h>\n#include <SPI.h>\n#include <OneWire.h>\n#include <DallasTemperature.h>\n#include <string.h>\n\n// the LoRa keys and IDs are secret and should not be disclosed\n#include \"secret.h\"\n//static const PROGMEM u1_t NWKSKEY[16] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };\n//static const u1_t PROGMEM APPSKEY[16] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };\n//static const u4_t DEVADDR = 0x00000000;\n\n#define BUTTON_PIN 20\nint button = 0;\n\n#define LED_R 21\n#define LED_G 22\n#define LED_B 23\nint color = NULL;\n\n#define ONE_WIRE_BUS 15\nOneWire oneWire(ONE_WIRE_BUS);\nDallasTemperature dallasTemperature(&oneWire);\nfloat temp = 0.0;\n\n// These callbacks are only used in over-the-air activation, so they are left empty here (we cannot\n// leave them out completely unless DISABLE_JOIN is set in config.h, otherwise the linker will complain).\nvoid os_getArtEui (u1_t* buf) { }\nvoid os_getDevEui (u1_t* buf) { }\nvoid os_getDevKey (u1_t* buf) { }\n\n#define DATALEN 32\n// static uint8_t datatx[] = \"Hello, world!\";\nstatic uint8_t datatx[DATALEN]; // transmit\nstatic uint8_t datarx[DATALEN]; // receive\nstatic osjob_t sendjob;\nstatic osjob_t recvjob;\nstatic osjob_t tempjob;\nstatic osjob_t ledjob;\n\n// schedule some functions to execute every this many seconds (might become longer due to duty cycle limitations)\nconst unsigned TX_INTERVAL = 10;\nconst unsigned RX_INTERVAL = 2;\nconst unsigned TEMP_INTERVAL = 2;\nconst unsigned LED_INTERVAL  = 150;  // in miliseconds\n\n// Pin mapping\nconst lmic_pinmap lmic_pins = {\n  .nss = 10,\n  .rxtx = LMIC_UNUSED_PIN,\n  .rst = 9,\n  .dio = {2, 5, 6},\n};\n\nvoid onEvent (ev_t ev) {\n  Serial.print(os_getTime());\n  Serial.print(\": \");\n  switch (ev) {\n    case EV_SCAN_TIMEOUT:\n      Serial.println(F(\"EV_SCAN_TIMEOUT\"));\n      break;\n    case EV_BEACON_FOUND:\n      Serial.println(F(\"EV_BEACON_FOUND\"));\n      break;\n    case EV_BEACON_MISSED:\n      Serial.println(F(\"EV_BEACON_MISSED\"));\n      break;\n    case EV_BEACON_TRACKED:\n      Serial.println(F(\"EV_BEACON_TRACKED\"));\n      break;\n    case EV_JOINING:\n      Serial.println(F(\"EV_JOINING\"));\n      break;\n    case EV_JOINED:\n      Serial.println(F(\"EV_JOINED\"));\n      break;\n    case EV_RFU1:\n      Serial.println(F(\"EV_RFU1\"));\n      break;\n    case EV_JOIN_FAILED:\n      Serial.println(F(\"EV_JOIN_FAILED\"));\n      break;\n    case EV_REJOIN_FAILED:\n      Serial.println(F(\"EV_REJOIN_FAILED\"));\n      break;\n    case EV_TXCOMPLETE:\n      Serial.println(F(\"EV_TXCOMPLETE (includes waiting for RX windows)\"));\n      if (LMIC.dataLen) {\n        // data received in rx slot after tx\n        Serial.print(F(\"Data Received: \"));\n        Serial.write(LMIC.frame + LMIC.dataBeg, LMIC.dataLen);\n        Serial.println();\n        // another scheduled function will deal with the received data\n        bzero(datarx, DATALEN);\n        memcpy(datarx, LMIC.frame + LMIC.dataBeg, min(LMIC.dataLen, DATALEN - 1));\n      }\n      // schedule next transmission\n      os_setTimedCallback(&sendjob, os_getTime() + ms2osticks(TX_INTERVAL), send_message);\n      break;\n    case EV_LOST_TSYNC:\n      Serial.println(F(\"EV_LOST_TSYNC\"));\n      break;\n    case EV_RESET:\n      Serial.println(F(\"EV_RESET\"));\n      break;\n    case EV_RXCOMPLETE:\n      // data received in ping slot\n      Serial.println(F(\"EV_RXCOMPLETE\"));\n      break;\n    case EV_LINK_DEAD:\n      Serial.println(F(\"EV_LINK_DEAD\"));\n      break;\n    case EV_LINK_ALIVE:\n      Serial.println(F(\"EV_LINK_ALIVE\"));\n      break;\n    default:\n      Serial.println(F(\"Unknown event\"));\n      break;\n  }\n}\n\nvoid switch_led() {\n  // Serial.print(\"color = \");\n  // Serial.println(color);\n  digitalWrite(LED_R, LOW);    // turn the LED off by making the voltage LOW\n  digitalWrite(LED_G, LOW);    // turn the LED off by making the voltage LOW\n  digitalWrite(LED_B, LOW);    // turn the LED off by making the voltage LOW\n  switch (color) {\n    case LED_R:\n      digitalWrite(LED_R, HIGH);   // turn the LED on (HIGH is the voltage level)\n      break;\n    case LED_G:\n      digitalWrite(LED_G, HIGH);   // turn the LED on (HIGH is the voltage level)\n      break;\n    case LED_B:\n      digitalWrite(LED_B, HIGH);   // turn the LED on (HIGH is the voltage level)\n      break;\n    default:\n      break;\n  }\n}\n\nvoid blink_led(osjob_t* j) {\n  switch_led();\n  if (color) {\n    // schedule the LED to switch off\n    color = NULL;\n    os_setTimedCallback(j, os_getTime() + ms2osticks(LED_INTERVAL), blink_led);\n  }\n}\n\nvoid update_button() {\n  // this function is triggered by an interrupt\n  delay(50); // debounce\n  if (digitalRead(BUTTON_PIN) == HIGH) {\n    color = LED_B;\n    blink_led(&ledjob);\n    button = 1;\n    Serial.print(\"button = \");\n    Serial.println(button);\n  }\n}\n\nvoid receive_message(osjob_t* j) {\n  if (datarx[0]) {\n    Serial.println(\"message = [\");\n    unsigned int i = 0;\n    while (datarx[i])\n      Serial.println((int)datarx[i++]);\n    Serial.println(\"]\");\n    bzero(datarx, DATALEN);\n    // blink\n    color = LED_B;\n    switch_led();\n  }\n  os_setTimedCallback(j, os_getTime() + sec2osticks(RX_INTERVAL), receive_message);\n}\n\nvoid update_temp(osjob_t* j) {\n  dallasTemperature.requestTemperatures();     // Send the command to get temperatures\n  temp = dallasTemperature.getTempCByIndex(0); // Get the temperature of the first sensor\n  // Serial.print(\"temp = \");\n  // Serial.println(temp);\n  // the measurement takes quite some time, hence we blink afterwards (otherwise the blink duration would be incorrect)\n  color = LED_G;\n  blink_led(&ledjob);\n  // schedule the next measurement\n  os_setTimedCallback(j, os_getTime() + sec2osticks(TEMP_INTERVAL), update_temp);\n}\n\nvoid send_message(osjob_t* j) {\n  color = LED_R;\n  blink_led(&ledjob);\n\n  // determine whether to send the temperature or the button press\n  if (button) {\n    String str = String(\"Button pressed\");\n    str.toCharArray((char *)datatx, DATALEN);\n  }\n  else {\n    String str = String(\"Temperature = \");\n    str += String(temp);\n    str.toCharArray((char *)datatx, DATALEN);\n  }\n\n  // Check if there is not a current TX/RX job running\n  if (LMIC.opmode & OP_TXRXPEND) {\n    Serial.println(F(\"OP_TXRXPEND, not sending\"));\n  } else {\n    // reset the button status\n    button = 0;\n    // Prepare upstream data transmission at the next possible time.\n    LMIC_setTxData2(1, datatx, sizeof(datatx) - 1, 0);\n    Serial.println(F(\"Packet queued\"));\n  }\n  // Next TX is scheduled after TX_COMPLETE event.\n}\n\nvoid setup() {\n  // setup the serial port for debugging\n  Serial.begin(115200);\n  while (!Serial) {\n    ; // wait for serial port to connect. Needed for native USB\n  }\n  Serial.println(F(\"Starting\"));\n\n  // setup the button\n  pinMode(BUTTON_PIN, INPUT_PULLDOWN);\n  attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), update_button, RISING);\n\n  // setup the temperature sensor\n  dallasTemperature.begin();\n\n  // setup the message content\n  bzero(datatx, DATALEN);\n  bzero(datarx, DATALEN);\n\n  // setup the RGB led\n  pinMode(LED_R, OUTPUT);\n  pinMode(LED_G, OUTPUT);\n  pinMode(LED_B, OUTPUT);\n\n#ifdef VCC_ENABLE\n  // For Pinoccio Scout boards\n  pinMode(VCC_ENABLE, OUTPUT);\n  digitalWrite(VCC_ENABLE, HIGH);\n  delay(1000);\n#endif\n\n  // LMIC init\n  os_init();\n  // Reset the MAC state. Session and pending data transfers will be discarded.\n  LMIC_reset();\n\n  // Set static session parameters. Instead of dynamically establishing a session\n  // by joining the network, precomputed session parameters are to be provided.\n#ifdef PROGMEM\n  // On AVR, these values are stored in flash and only copied to RAM\n  // once. Copy them to a temporary buffer here, LMIC_setSession will\n  // copy them into a buffer of its own again.\n  uint8_t appskey[sizeof(APPSKEY)];\n  uint8_t nwkskey[sizeof(NWKSKEY)];\n  memcpy_P(appskey, APPSKEY, sizeof(APPSKEY));\n  memcpy_P(nwkskey, NWKSKEY, sizeof(NWKSKEY));\n  LMIC_setSession (0x1, DEVADDR, nwkskey, appskey);\n#else\n  // If not running an AVR with PROGMEM, just use the arrays directly\n  LMIC_setSession (0x1, DEVADDR, NWKSKEY, APPSKEY);\n#endif\n\n  // Set up the channels used by the Things Network, which corresponds\n  // to the defaults of most gateways. Without this, only three base\n  // channels from the LoRaWAN specification are used, which certainly\n  // works, so it is good for debugging, but can overload those\n  // frequencies, so be sure to configure the full frequency range of\n  // your network here (unless your network autoconfigures them).\n  // Setting up channels should happen after LMIC_setSession, as that\n  // configures the minimal channel set.\n  LMIC_setupChannel(0, 868100000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band\n  LMIC_setupChannel(1, 868300000, DR_RANGE_MAP(DR_SF12, DR_SF7B), BAND_CENTI);      // g-band\n  LMIC_setupChannel(2, 868500000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band\n  LMIC_setupChannel(3, 867100000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band\n  LMIC_setupChannel(4, 867300000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band\n  LMIC_setupChannel(5, 867500000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band\n  LMIC_setupChannel(6, 867700000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band\n  LMIC_setupChannel(7, 867900000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band\n  LMIC_setupChannel(8, 868800000, DR_RANGE_MAP(DR_FSK,  DR_FSK),  BAND_MILLI);      // g2-band\n  // TTN defines an additional channel at 869.525Mhz using SF9 for class B\n  // devices' ping slots. LMIC does not have an easy way to define set this\n  // frequency and support for class B is spotty and untested, so this\n  // frequency is not configured here.\n\n  // Disable link check validation\n  LMIC_setLinkCheckMode(FALSE);\n\n  // Set adaptive data rate\n  LMIC_setAdrMode(FALSE);\n  // Set data rate and transmit power (note: txpow seems to be ignored by the library)\n  LMIC_setDrTxpow(DR_SF7, 20);\n\n  // Start the periodic jobs\n  update_temp(&tempjob);\n  send_message(&sendjob);\n  receive_message(&recvjob);\n}\n\nvoid loop() {\n  // all functionality is implemented in scheduled function calls\n  os_runloop();\n}\n"
  },
  {
    "path": "uno_dac7578/blink_led.cpp",
    "content": "#include \"blink_led.h\"\n\nTicker blink(ledFlip, 1000, 0, MILLIS);\n\nenum {\n  LED_ON,\n  LED_OFF,\n  LED_SLOW,\n  LED_MEDIUM,\n  LED_FAST,\n} ledState;\n\nvoid ledInit() {\n  pinMode(LED_BUILTIN, OUTPUT);\n  blink.pause();\n}\n\nvoid ledOn() {\n  if (ledState != LED_ON) {\n    ledState = LED_ON;\n    blink.pause();\n    digitalWrite(LED_BUILTIN, LOW);\n  }\n}\n\nvoid ledOff() {\n  if (ledState != LED_OFF) {\n    ledState = LED_OFF;\n    blink.pause();\n    digitalWrite(LED_BUILTIN, HIGH);\n  }\n}\n\nvoid ledSlow() {\n  if (ledState != LED_SLOW) {\n    ledState = LED_SLOW;\n    blink.interval(1000);\n    blink.resume();\n  }\n}\n\nvoid ledMedium() {\n  if (ledState != LED_MEDIUM) {\n    ledState = LED_MEDIUM;\n    blink.interval(250);\n    blink.resume();\n  }\n}\n\nvoid ledFast() {\n  if (ledState != LED_FAST) {\n    ledState = LED_FAST;\n    blink.interval(100);\n    blink.resume();\n  }\n}\n\nvoid ledFlip() {\n  // Invert the current state of the LED_BUILTIN\n  digitalWrite(LED_BUILTIN, !(digitalRead(LED_BUILTIN)));  \n}\n"
  },
  {
    "path": "uno_dac7578/blink_led.h",
    "content": "#ifndef _BLINK_LED_H_\n#define _BLINK_LED_H_\n\n#include <Arduino.h>\n#include <Ticker.h>  // see https://github.com/sstaub/Ticker\n\nextern Ticker blink;\n\nvoid ledInit(void);\nvoid ledOn(void);\nvoid ledOff(void);\nvoid ledSlow(void);\nvoid ledMedium(void);\nvoid ledFast(void);\nvoid ledFlip(void);\n\n#endif"
  },
  {
    "path": "uno_dac7578/uno_dac7578.ino",
    "content": "/* \n * Sketch for an Arduino Uno or Leonardo connected to two Adafruit DAC7578 modules. The purpose is to \n * act as a MEG phantom driver, controlling a small current through up to 16 magnetic dipole or \n * equivalent current dipole sources, thereby generating small magnetic fields that can be recorded \n * using an OPM or SQUID-based MEG system.\n *\n * See \n *   https://store.arduino.cc/products/arduino-uno-rev3-smd\n *   https://store.arduino.cc/products/arduino-leonardo-with-headers\n *   https://learn.adafruit.com/adafruit-dac7578-8-x-channel-12-bit-i2c-dac\n */\n\n#include <Adafruit_DACX578.h>\n#include <Adafruit_I2CDevice.h>\n#include <math.h>\n\n#include \"blink_led.h\"\n\nAdafruit_DACX578 dac0(12);\nAdafruit_DACX578 dac1(12);\nAdafruit_DACX578 dac2(12);\n\nbool dac0Found = false;\nbool dac1Found = false;\nbool dac2Found = false;\n\nconst uint8_t addr0 = 0x4C;  // open pads\nconst uint8_t addr1 = 0x48;  // left pads\nconst uint8_t addr2 = 0x4A;  // right pads\n\n// these will be updated according to the number of DACs found\nint nchannels = 24;\nint dacBoard[24] = { 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2 };\nint dacChannel[24] = { 0, 1, 2, 3, 4, 5, 6, 7, 0, 1, 2, 3, 4, 5, 6, 7, 0, 1, 2, 3, 4, 5, 6, 7 };\nfloat frequency[24] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };\nfloat phase[24] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };\nfloat freqoffset = 5;\nfloat freqstepsize = 0.2;\n\nconst float offset = 2048;          // Offset for the 12-bit DAC output (0 to 4095)\nconst float amplitude = 2047;       // Half of the full scale for 12-bit DAC (0 to 4095)\nconst uint32_t sampleTime = 10000;  // Approximate time between samples, in microseconds\nconst uint32_t feedbackTime = -1;   // Time between serial feedback, in microseconds (-1 for no feedback)\n\nvoid blinkdelay(uint32_t duration) {\n  // wait for some time, but keep the LED blinking\n  uint32_t start = millis();\n  while ((millis() - start) < duration) {\n    blink.update();\n    delay(10);\n  }\n};\n\nvoid setup() {\n  // blink rapidly, this will continue indefinitely when the setup fails\n  ledInit();\n  ledFast();\n\n  Serial.begin(115200);\n  while (!Serial && (millis() < 3000)) {\n    // wait for the serial interface to start, but not longer than 3 seconds\n    blinkdelay(200);\n  }\n\n  Serial.println(\"-------------------------------\");\n  Serial.println(\"Arduino dac7578 sine wave generator\");\n  Serial.print(\"Firmware version \");\n  Serial.print(__TIME__);\n  Serial.print(\" / \");\n  Serial.println(__DATE__);\n\n  // initialize the first DAC7578\n  dac0Found = dac0.begin(addr0, &Wire);\n  long initstart = millis();\n  while (!dac0Found && (millis() - initstart < 2000)) {\n    blinkdelay(200);\n    dac0Found = dac0.begin(addr0, &Wire);\n  }\n  if (!dac0Found) {\n    Serial.print(\"Failed to find dac0 at address \");\n    Serial.println(addr0, HEX);\n  } else {\n    Serial.print(\"Initialized dac0 at address \");\n    Serial.println(addr0, HEX);\n  }\n\n  // initialize the second DACC7578\n  dac1Found = dac1.begin(addr1, &Wire);\n  initstart = millis();\n  while (!dac1Found && (millis() - initstart < 2000)) {\n    blinkdelay(200);\n    dac1Found = dac1.begin(addr1, &Wire);\n  }\n  if (!dac1Found) {\n    Serial.print(\"Failed to find dac1 at address \");\n    Serial.println(addr1, HEX);\n  } else {\n    Serial.print(\"Initialized dac1 at address \");\n    Serial.println(addr1, HEX);\n  }\n\n  // initialize the third DACC7578\n  dac2Found = dac2.begin(addr2, &Wire);\n  initstart = millis();\n  while (!dac2Found && (millis() - initstart < 2000)) {\n    blinkdelay(200);\n    dac2Found = dac2.begin(addr2, &Wire);\n  }\n  if (!dac2Found) {\n    Serial.print(\"Failed to find dac2 at address \");\n    Serial.println(addr2, HEX);\n  } else {\n    Serial.print(\"Initialized dac2 at address \");\n    Serial.println(addr2, HEX);\n  }\n\n  // update the channel-to-board mapping\n  for (uint8_t channel = 0; channel < 24; channel++) {\n    // shift each channel to the next board if the current board is not found\n    if (!dac0Found && dacBoard[channel] == 0) {\n      dacBoard[channel]++;\n    }\n    if (!dac1Found && dacBoard[channel] == 1) {\n      dacBoard[channel]++;\n    }\n    if (!dac2Found && dacBoard[channel] == 2) {\n      dacBoard[channel]++;\n    }\n  }\n\n  // update the number of channels according to the number of DAC boards that were found\n  nchannels = 8 * dac0Found + 8 * dac1Found + 8 * dac2Found;\n\n  // set the frequency for each channel\n  for (uint8_t channel = 0; channel < nchannels; channel++) {\n    frequency[channel] = freqoffset + freqstepsize * channel;\n    Serial.print(\"channel \");\n    Serial.print(channel + 1);\n    Serial.print(\", board \");\n    Serial.print(dacBoard[channel] + 1);\n    Serial.print(\", frequency \");\n    Serial.print(frequency[channel]);\n    Serial.println(\" Hz\");\n  }\n\n  // The Arduino Leonardo (based on the ATmega32u4) defaults to an I2C clock speed of 100 kHz and supports a maximum clock speed of 400 kHz\n  Wire.setClock(400000);\n\n  // blink slowly for the rest of the time\n  if (nchannels > 0) {\n    ledSlow();\n  }\n\n}  // setup\n\nvoid loop() {\n  static uint32_t lastSample = micros();\n  static uint32_t lastFeedback = micros();\n\n  // this is needed to keep the LED blinking\n  blink.update();\n\n  uint32_t currentTime = micros();\n  uint32_t deltaTime = currentTime - lastSample;\n\n  if (deltaTime >= sampleTime) {\n\n    for (uint8_t channel = 0; channel < nchannels; channel++) {\n      // increment the phase according to the channels-specific frequency\n      phase[channel] += 2.0 * M_PI * frequency[channel] * ((float)deltaTime / 1000000);\n      if (phase[channel] >= 2.0 * M_PI) {\n        phase[channel] -= 2.0 * M_PI;\n      }\n\n      // compute the new value\n      float sineValue = offset + amplitude * cos(phase[channel]);\n\n      // write the value to the corresponding channel of the corresponding dacBoard\n      if (dacBoard[channel] == 0) {\n        dac0.writeAndUpdateChannelValue(dacChannel[channel], (uint16_t)sineValue);\n      } else if (dacBoard[channel] == 1) {\n        dac1.writeAndUpdateChannelValue(dacChannel[channel], (uint16_t)sineValue);\n      } else if (dacBoard[channel] == 2) {\n        dac2.writeAndUpdateChannelValue(dacChannel[channel], (uint16_t)sineValue);\n      }\n    }\n    lastSample = currentTime;\n\n    if ((currentTime - lastFeedback) > feedbackTime) {\n      Serial.println(deltaTime);\n      lastFeedback = currentTime;\n    }\n\n  }  // if deltaTime\n\n}  // loop\n"
  }
]