/*
 *  ReceiverTimingAnalysis.cpp
 *
 *  This program enables the pin change interrupt at pin 3 and waits for NEC (or other Pulse-Distance-Coding) IR Signal.
 *  It measures the pulse and pause times of the incoming signal and computes some statistics for it.
 *
 *  Observed values:
 *  Delta of each signal type is around 50 up to 100 and at low signals up to 200. TSOP is better, especially at low IR signal level.
 *  VS1838      Mark Excess -50 to +50 us
 *  TSOP31238   Mark Excess 0 to +50
 *
 *
 *  Copyright (C) 2019-2020  Armin Joachimsmeyer
 *  armin.joachimsmeyer@gmail.com
 *
 *  This file is part of IRMP https://github.com/IRMP-org/IRMP.
 *  This file is part of Arduino-IRremote https://github.com/Arduino-IRremote/Arduino-IRremote.
 *
 *  IRMP is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 *  See the GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program. If not, see <http://www.gnu.org/licenses/gpl.html>.
 *
 */

#include <Arduino.h>

#define IR_RECEIVE_PIN    2
//#define IR_RECEIVE_PIN    3

/*
 * Helper macro for getting a macro definition as string
 */
#define STR_HELPER(x) #x
#define STR(x) STR_HELPER(x)

#if !(defined(EICRA) && defined(EIFR) && defined(EIMSK))
void measureTimingISR(void);
#endif

void setup() {
    pinMode(LED_BUILTIN, OUTPUT);
    Serial.begin(115200);
    while (!Serial)
        ; // Wait for Serial to become available. Is optimized away for some cores.

#if defined(__AVR_ATmega32U4__) || defined(SERIAL_PORT_USBVIRTUAL) || defined(SERIAL_USB) /*stm32duino*/|| defined(USBCON) /*STM32_stm32*/ \
    || defined(SERIALUSB_PID)  || defined(ARDUINO_ARCH_RP2040) || defined(ARDUINO_attiny3217)
    delay(4000); // To be able to connect Serial monitor after reset or power up and before first print out. Do not wait for an attached Serial Monitor!
#endif
    // Just to know which program is running on my Arduino
    Serial.println(F("START " __FILE__ " from " __DATE__));

#if defined(EICRA) && defined(EIFR) && defined(EIMSK)
#  if (IR_RECEIVE_PIN == 2)
    EICRA |= _BV(ISC00);  // interrupt on any logical change
    EIFR |= _BV(INTF0);     // clear interrupt bit
    EIMSK |= _BV(INT0);     // enable interrupt on next change
#  elif (IR_RECEIVE_PIN == 3)
    EICRA |= _BV(ISC10);    // enable interrupt on pin3 on both edges for ATmega328
    EIFR |= _BV(INTF1);     // clear interrupt bit
    EIMSK |= _BV(INT1);     // enable interrupt on next change
#  endif
#else
    attachInterrupt(digitalPinToInterrupt(IR_RECEIVE_PIN), measureTimingISR, CHANGE);
#endif
    Serial.println(F("Ready to analyze NEC IR signal at pin " STR(IR_RECEIVE_PIN)));
    Serial.println();
}

uint8_t ISREdgeCounter = 0;
volatile uint32_t LastMicros;
struct timingStruct {
    uint16_t minimum;
    uint8_t indexOfMinimum;
    uint16_t maximum;
    uint8_t indexOfMaximum;
    uint16_t average;

    uint16_t SumForAverage;
    uint8_t SampleCount;
//    uint8_t LastPrintedCount;
};

struct timingStruct Mark;
struct timingStruct ShortSpace;
struct timingStruct LongSpace;

/*
 * Compute minimum, maximum and average
 */
void processTmingValue(struct timingStruct *aTimingStruct, uint16_t aValue) {
    if (aTimingStruct->SampleCount == 0) {
        // initialize values
        aTimingStruct->minimum = UINT16_MAX;
        aTimingStruct->maximum = 0;
        aTimingStruct->SumForAverage = 0;
    }

    if (aTimingStruct->minimum > aValue) {
        aTimingStruct->minimum = aValue;
        aTimingStruct->indexOfMinimum = aTimingStruct->SampleCount;
    }
    if (aTimingStruct->maximum < aValue) {
        aTimingStruct->maximum = aValue;
        aTimingStruct->indexOfMaximum = aTimingStruct->SampleCount;
    }

    aTimingStruct->SampleCount++;
    aTimingStruct->SumForAverage += aValue;
    aTimingStruct->average = (aTimingStruct->SumForAverage + (aTimingStruct->SampleCount / 2)) / aTimingStruct->SampleCount;

}

void printTimingValues(struct timingStruct *aTimingStruct, const char *aCaption) {
//    if (aTimingStruct->LastPrintedCount != aTimingStruct->SampleCount)
//    {
//        aTimingStruct->LastPrintedCount = aTimingStruct->SampleCount;
    Serial.print(aCaption);
    Serial.print(F(": SampleCount="));
    Serial.print(aTimingStruct->SampleCount);
    Serial.print(F(" Minimum="));
    Serial.print(aTimingStruct->minimum);
    Serial.print(F(" @"));
    Serial.print(aTimingStruct->indexOfMinimum);
    Serial.print(F(" Maximum="));
    Serial.print(aTimingStruct->maximum);
    Serial.print(F(" @"));
    Serial.print(aTimingStruct->indexOfMaximum);
    Serial.print(F(" Delta="));
    Serial.print(aTimingStruct->maximum - aTimingStruct->minimum);
    Serial.print(F("   Average="));
    Serial.print(aTimingStruct->average);

    Serial.println();
//    }
}

void loop() {
    if (Mark.SampleCount >= 32) {
        /*
         * This check enables statistics for longer protocols like Kaseikyo/Panasonics
         */
#if !defined(ARDUINO_ARCH_MBED)
        noInterrupts();
#endif
        uint32_t tLastMicros = LastMicros;
#if !defined(ARDUINO_ARCH_MBED)
        interrupts();
#endif
        uint32_t tMicrosDelta = micros() - tLastMicros;

        if (tMicrosDelta > 10000) {
            // NEC signal ended just now
            Serial.println();
            printTimingValues(&Mark, "Mark      ");
            printTimingValues(&ShortSpace, "ShortSpace");
            printTimingValues(&LongSpace, "LongSpace ");

            /*
             * Print analysis of mark and short spaces
             */
            Serial.println(F("Analysis  :"));
            Serial.print(F(" (Average of mark + short space)/2 = "));
            int16_t MarkAndShortSpaceAverage = (Mark.average + ShortSpace.average) / 2;
            Serial.print(MarkAndShortSpaceAverage);
            Serial.print(F(" us\r\n Delta (to NEC standard 560) = "));
            Serial.print(MarkAndShortSpaceAverage - 560);
            Serial.print(F("us\r\n MARK_EXCESS_MICROS = (Average of mark - Average of mark and short space) = "));
            Serial.print((int16_t) Mark.average - MarkAndShortSpaceAverage);
            Serial.print(F("us"));
            Serial.println();
            Serial.println();

            Mark.SampleCount = 0; // used as flag for not printing the results more than once
        }
    }
}

/*
 * The interrupt handler.
 * Just add to the appropriate timing structure.
 */
#if defined(ESP8266) || defined(ESP32)
void IRAM_ATTR measureTimingISR()
#else
#  if defined(EICRA) && defined(EIFR) && defined(EIMSK)
#    if (IR_RECEIVE_PIN == 2)
ISR(INT0_vect)
#    elif (IR_RECEIVE_PIN == 3)
ISR(INT1_vect)
#    endif
#  else
void measureTimingISR()
#  endif
#endif
{
    uint32_t tMicros = micros();
    uint32_t tMicrosDelta = tMicros - LastMicros;
    LastMicros = tMicros;
    /*
     * read level and give feedback
     */
    uint8_t tInputLevel = digitalRead(IR_RECEIVE_PIN);
    digitalWrite(LED_BUILTIN, !tInputLevel);

    if (tMicrosDelta > 10000) {
        // gap > 10 ms detected, reset counter to first detected edge and initialize timing structures
        ISREdgeCounter = 1;
        LongSpace.SampleCount = 0;
        ShortSpace.SampleCount = 0;
        Mark.SampleCount = 0;
    } else {
        ISREdgeCounter++;
    }

    /*
     * Skip header mark and space and first bit mark and space
     */
    if (ISREdgeCounter > 4) {
        if (tInputLevel != LOW) {
            // Mark ended
            processTmingValue(&Mark, tMicrosDelta);
//            Serial.print('M');
        } else {
            // Space ended
            if (tMicrosDelta > 1000) {
                // long space - logical 1
                processTmingValue(&LongSpace, tMicrosDelta);
                Serial.print('1');
            } else {
                // short space - logical 0
                processTmingValue(&ShortSpace, tMicrosDelta);
                Serial.print('0');
            }
        }
    }
}
