1
\$\begingroup\$

I am using my HD44780 based 20x4 LCD with the famous I2C backpack, AKA the I2C I/O expander module, the PCF8547, the I2C I/O expander module is connected to the 20x4 LCD and is connected via the I2C bus to my 8-bit PIC microcontroller, PIC18F4620.

Now what am I trying to do is have different menus on the LCD, each menu utilizes all the 80 characters on the display, and I use a rotary encoder to move between the menus.

Now the code for all of this is written and is indeed working. My problem is a different story, my problem is that when I run the program, the first time I try to scroll through all the menus, there is a noticeable delay in printing the lines, to be very specific, sometimes there is a delay in the printing starting from the second line (counting from one not from zero as the LCD usually counts), and sometimes there is a delay in the printing starting from the third line. Now when I try to scroll again through all the menus, this noticeable delay is vanished, literally vanished, there is no delay, as it should be. I've attached a URL to an unlisted YouTube video uploaded by me showing this full behavior from complete power-off state, I hope this is okay with the policy of the website. https://youtu.be/ajXUXeMd_7U

As for the code, I will indeed post it, but I do not think there is a problem with the code at all, this is an interrupt driven code, with around 1k lines of code in the main file, I will only post the main function, the ISR, and the functions called in both of them which are related to this matter, if you need anything else please feel free to ask me, I am trying to ease things for you guys. :)

Constant Definitions (#define)

#define _XTAL_FREQ                  16e6
#define _INPUT                      1
#define _OUTPUT                     0
#define _HIGH                       1
#define _LOW                        0
#define _RISING_EDGE                1
#define _FALLING_EDGE               0
#define _MAX_NUMBER_OF_IDLE_SECONDS 30

#define _RTC_SQW_DIR                TRISBbits.TRISB0
#define _RTC_SQW_INTERRUPT          INT0IE
#define _RTC_SQW_INTERRUPT_FLAG     INT0IF

#define _ROTARY_SW_DIR              TRISBbits.TRISB1
#define _ROTARY_SW_INTERRUPT        INT1IE
#define _ROTARY_SW_INTERRUPT_FLAG   INT1IF
#define _ROTARY_CLK_DIR             TRISBbits.TRISB2
#define _ROTARY_CLK_INTERRUPT       INT2IE
#define _ROTARY_CLK_INTERRUPT_FLAG  INT2IF
#define _ROTARY_CLK_PIN_READ        PORTBbits.RB2
#define _ROTARY_CLK_INTERRUPT_ON    INTEDG2
#define _ROTARY_DT_DIR              TRISBbits.TRISB3
#define _ROTARY_DT_PIN_READ         PORTBbits.RB3

#define testLED_DIR                 TRISBbits.TRISB4
#define testLED_PIN_WRITE           LATBbits.LATB4

Main Routine:

void main(void)
{
// Inputs initialization
Inputs_Init();

// Outputs initialization
Outputs_Init();

// Peripherals initialization from in to out of the MCU.
I2C_Master_Init();
I2C_LCD_Init(LCD1_WRITE);
I2C_LCD_Clear(LCD1_WRITE);
DS3231_Init();
DHT22_Init();

// EEPROM Retrieval

// Interrupts initialization
Interrupts_Init();
while (1)
{
    // Section: Input


    // Section: Inputs Processing and Decision Taking 


    // Section: Output to User Interface
}
return;
}

ISR Routine:

void __interrupt() ISR(void)
{
if (_ROTARY_CLK_INTERRUPT_FLAG)
{
    testLED_PIN_WRITE = _HIGH;
    if (_ROTARY_CLK_PIN_READ != _ROTARY_DT_PIN_READ)
    {
        // CW ==> ++
        if (rotarySetToChange)
        {
            cursorIncreaseValue();
        }
        else
        {
            cursorMoveToTheRight();
        }
    }
    else
    {
        // CCW ==> --
        if (rotarySetToChange)
        {
            cursorDecreaseValue();
        }
        else
        {
            cursorMoveToTheLeft();
        }
    }
    cursorGoToCorrectPosition();
    testLED_PIN_WRITE = _LOW;
    idleSeconds = 0;
    _ROTARY_CLK_INTERRUPT_ON = ~_ROTARY_CLK_INTERRUPT_ON; // Adjust next interrupt edge.
    _ROTARY_CLK_INTERRUPT_FLAG = _LOW;
}

if (_ROTARY_SW_INTERRUPT_FLAG)
{
    rotarySetToChange = ~rotarySetToChange;
    if (menu != 0)
    {
        if (rotarySetToChange)
        {
            I2C_LCD_BlinkON(LCD1_WRITE);
        }
        else
        {
            I2C_LCD_CursorON(LCD1_WRITE);
        }
    }
    idleSeconds = 0;
    _ROTARY_SW_INTERRUPT_FLAG = _LOW;
}
}

Used functions in the main and ISR routines

void Inputs_Init(void)
{
_RTC_SQW_DIR = _INPUT;

_ROTARY_SW_DIR = _INPUT;
_ROTARY_CLK_DIR = _INPUT;
_ROTARY_DT_DIR = _INPUT;
}

void Outputs_Init(void)
{
testLED_PIN_WRITE = _LOW;
testLED_DIR = _OUTPUT;
}

void Interrupts_Init(void)
{
GIE = 1; // General Interrupt Enable.
PEIE = 1; // Peripheral Interrupt Enable.

_RTC_SQW_INTERRUPT = 1; // RB0 Interrupt Enable, on rising edge by default. (INTEDG0 = 1)

rotaryAdjustNextInterruptEdge();
_ROTARY_CLK_INTERRUPT = 1; // Rotary Interrupt Enable.

_ROTARY_SW_INTERRUPT = 1; // Rotary Switch Interrupt Enable, on rising edge by default. 
 (INTEDG2 = 1)
 }

void cursorMoveToTheRight(void)
{
switch (menu)
{
case 0: // General Menu
    menu = 1;
    cursorRow = 3;
    cursorCol = 9;
    printStrings();
    break;
case 1: // Pigeons Door Time Menu
    cursorMoveRightInMenus_1234(2);
    break;
case 2: // Pigeons Food Time Menu
    cursorMoveRightInMenus_1234(3);
    break;
case 3: // Watering 1 Menu
    cursorMoveRightInMenus_1234(4);
    break;
case 4: // Watering 2 Menu
    cursorMoveRightInMenus_1234(5);
    break;
case 5: // Spraying Menu
    cursorMoveRightInMenus_5();
    break;
case 6: // Air Conditioner Menu
    cursorMoveRightInMenus_6();
    break;
case 7: // Temperature-Humidity Menu
    cursorMoveRightInMenus_7();
    break;
}
}

void cursorMoveRightInMenus_1234(unsigned char newMenu)
{
switch (cursorRow)
{
case 3:
    switch (cursorCol)
    {
    case 9:
        cursorRow = 3;
        cursorCol = 12;
        break;
    case 12:
        cursorRow = 4;
        cursorCol = 9;
        break;
    }
    break;
case 4:
    switch (cursorCol)
    {
    case 9:
        cursorRow = 4;
        cursorCol = 12;
        break;
    case 12:
        menu = newMenu;
        if (menu == 5)
        {
            cursorRow = 2;
            cursorCol = 9;
        }
        else
        {
            cursorRow = 3;
            cursorCol = 9;
        }
        printStrings();
        break;
    }
    break;
}
}

void cursorMoveRightInMenus_5(void)
{
switch (cursorRow)
{
case 2:
    switch (cursorCol)
    {
    case 9:
        cursorRow = 2;
        cursorCol = 12;
        break;
    case 12:
        cursorRow = 3;
        cursorCol = 9;
        break;
    }
    break;
case 3:
    switch (cursorCol)
    {
    case 9:
        cursorRow = 3;
        cursorCol = 12;
        break;
    case 12:
        cursorRow = 4;
        cursorCol = 7;
        break;
    }
    break;
case 4:
    switch (cursorCol)
    {
    case 7:
        cursorRow = 4;
        cursorCol = 17;
        break;
    case 17:
        menu = 6;
        cursorRow = 2;
        cursorCol = 9;
        printStrings();
        break;
    }
    break;
}
}

void cursorMoveRightInMenus_6(void)
{
switch (cursorRow)
{
case 2:
    switch (cursorCol)
    {
    case 9:
        cursorRow = 2;
        cursorCol = 12;
        break;
    case 12:
        cursorRow = 3;
        cursorCol = 9;
        break;
    }
    break;
case 3:
    switch (cursorCol)
    {
    case 9:
        cursorRow = 3;
        cursorCol = 12;
        break;
    case 12:
        cursorRow = 4;
        cursorCol = 6;
        break;
    }
    break;
case 4:
    switch (cursorCol)
    {
    case 6:
        cursorRow = 4;
        cursorCol = 17;
        break;
    case 17:
        menu = 7;
        cursorRow = 3;
        cursorCol = 7;
        printStrings();
        break;
    }
    break;
}
}

void cursorMoveRightInMenus_7(void)
{
switch (cursorRow)
{
case 3:
    switch (cursorCol)
    {
    case 7:
        cursorRow = 3;
        cursorCol = 19;
        break;
    case 19:
        cursorRow = 4;
        cursorCol = 7;
        break;
    }
    break;
case 4:
    switch (cursorCol)
    {
    case 7:
        cursorRow = 4;
        cursorCol = 19;
        break;
    case 19:
        menu = 0;
        cursorRow = 3;
        cursorCol = 9;
        printStrings();
        break;
    }
    break;
}
}

void cursorMoveToTheLeft(void)
{
switch (menu)
{
case 0: // General Menu
    menu = 7;
    cursorRow = 4;
    cursorCol = 19;
    printStrings();
    break;
case 1: // Pigeons Door Time Menu
    cursorMoveLeftInMenus_1234(1);
    break;
case 2: // Pigeons Food Time Menu
    cursorMoveLeftInMenus_1234(2);
    break;
case 3: // Watering 1 Menu
    cursorMoveLeftInMenus_1234(3);
    break;
case 4: // Watering 2 Menu
    cursorMoveLeftInMenus_1234(4);
    break;
case 5: // Spraying Menu
    cursorMoveLeftInMenus_5();
    break;
case 6: // Air Conditioner Menu
    cursorMoveLeftInMenus_6();
    break;
case 7: // Temperature-Humidity Menu
    cursorMoveLeftInMenus_7();
    break;
}
}

void cursorMoveLeftInMenus_1234(unsigned char newMenu)
{
switch (cursorRow)
{
case 3:
    switch (cursorCol)
    {
    case 9:
        menu = newMenu - 1;
        cursorRow = 4;
        cursorCol = 12;
        printStrings();
        break;
    case 12:
        cursorRow = 3;
        cursorCol = 9;
        break;
    }
    break;
case 4:
    switch (cursorCol)
    {
    case 9:
        cursorRow = 3;
        cursorCol = 12;
        break;
    case 12:
        cursorRow = 4;
        cursorCol = 9;
        break;
    }
    break;
}
}

void cursorMoveLeftInMenus_5(void)
{
switch (cursorRow)
{
case 2:
    switch (cursorCol)
    {
    case 9:
        menu = 4;
        cursorRow = 4;
        cursorCol = 12;
        printStrings();
        break;
    case 12:
        cursorRow = 2;
        cursorCol = 9;
        break;
    }
    break;
case 3:
    switch (cursorCol)
    {
    case 9:
        cursorRow = 2;
        cursorCol = 12;
        break;
    case 12:
        cursorRow = 3;
        cursorCol = 9;
        break;
    }
    break;
case 4:
    switch (cursorCol)
    {
    case 7:
        cursorRow = 3;
        cursorCol = 12;
        break;
    case 17:
        cursorRow = 4;
        cursorCol = 7;
        break;
    }
    break;
}
}

void cursorMoveLeftInMenus_6(void)
{
switch (cursorRow)
{
case 2:
    switch (cursorCol)
    {
    case 9:
        menu = 5;
        cursorRow = 4;
        cursorCol = 17;
        printStrings();
        break;
    case 12:
        cursorRow = 2;
        cursorCol = 9;
        break;
    }
    break;
case 3:
    switch (cursorCol)
    {
    case 9:
        cursorRow = 2;
        cursorCol = 12;
        break;
    case 12:
        cursorRow = 3;
        cursorCol = 9;
        break;
    }
    break;
case 4:
    switch (cursorCol)
    {
    case 6:
        cursorRow = 3;
        cursorCol = 12;
        break;
    case 17:
        cursorRow = 4;
        cursorCol = 6;
        break;
    }
    break;
}
}

void cursorMoveLeftInMenus_7(void)
{
switch (cursorRow)
{
case 3:
    switch (cursorCol)
    {
    case 7:
        menu = 6;
        cursorRow = 4;
        cursorCol = 17;
        printStrings();
        break;
    case 19:
        cursorRow = 3;
        cursorCol = 7;
        break;
    }
    break;
case 4:
    switch (cursorCol)
    {
    case 7:
        cursorRow = 3;
        cursorCol = 19;
        break;
    case 19:
        cursorRow = 4;
        cursorCol = 7;
        break;
    }
    break;
}
}

void rotaryAdjustNextInterruptEdge(void)
{
if (_ROTARY_CLK_PIN_READ == _HIGH)
{
    _ROTARY_CLK_INTERRUPT_ON = _FALLING_EDGE;
}
else
{
    _ROTARY_CLK_INTERRUPT_ON = _RISING_EDGE;
}
}

void printStrings(void)
{
I2C_LCD_CursorOFF(LCD1_WRITE);

switch (menu)
{
case 0: // General Menu
    I2C_LCD_Goto(1, 4, LCD1_WRITE);
    I2C_LCD_Print(LCD1_WRITE, humidityString);
    I2C_LCD_Goto(1, 3, LCD1_WRITE);
    I2C_LCD_Print(LCD1_WRITE, temperatureString);
    I2C_LCD_Goto(1, 2, LCD1_WRITE);
    I2C_LCD_Print(LCD1_WRITE, timeString);
    I2C_LCD_Goto(1, 1, LCD1_WRITE);
    I2C_LCD_Print(LCD1_WRITE, "   [General Menu]   ");
    break;
case 1: // Pigeons Door Time Menu
    I2C_LCD_Goto(1, 4, LCD1_WRITE);
    I2C_LCD_Print(LCD1_WRITE, pigeonsDoorTimeCloseString);
    I2C_LCD_Goto(1, 3, LCD1_WRITE);
    I2C_LCD_Print(LCD1_WRITE, pigeonsDoorTimeOpenString);
    I2C_LCD_Goto(1, 2, LCD1_WRITE);
    I2C_LCD_Print(LCD1_WRITE, "    [Door  Time]    ");
    I2C_LCD_Goto(1, 1, LCD1_WRITE);
    I2C_LCD_Print(LCD1_WRITE, "   [Pigeons Menu]   ");
    break;
case 2: // Pigeons Food Time Menu
    I2C_LCD_Goto(1, 4, LCD1_WRITE);
    I2C_LCD_Print(LCD1_WRITE, pigeonsFoodTimeCloseString);
    I2C_LCD_Goto(1, 3, LCD1_WRITE);
    I2C_LCD_Print(LCD1_WRITE, pigeonsFoodTimeOpenString);
    I2C_LCD_Goto(1, 2, LCD1_WRITE);
    I2C_LCD_Print(LCD1_WRITE, "    [Food  Time]    ");
    I2C_LCD_Goto(1, 1, LCD1_WRITE);
    I2C_LCD_Print(LCD1_WRITE, "   [Pigeons Menu]   ");
    break;
case 3: // Watering 1 Menu
    I2C_LCD_Goto(1, 4, LCD1_WRITE);
    I2C_LCD_Print(LCD1_WRITE, watering1ForString);
    I2C_LCD_Goto(1, 3, LCD1_WRITE);
    I2C_LCD_Print(LCD1_WRITE, watering1OpenString);
    I2C_LCD_Goto(1, 2, LCD1_WRITE);
    I2C_LCD_Print(LCD1_WRITE, "        [1]         ");
    I2C_LCD_Goto(1, 1, LCD1_WRITE);
    I2C_LCD_Print(LCD1_WRITE, "     [Watering]     ");
    break;
case 4: // Watering 2 Menu
    I2C_LCD_Goto(1, 4, LCD1_WRITE);
    I2C_LCD_Print(LCD1_WRITE, watering2ForString);
    I2C_LCD_Goto(1, 3, LCD1_WRITE);
    I2C_LCD_Print(LCD1_WRITE, watering2OpenString);
    I2C_LCD_Goto(1, 2, LCD1_WRITE);
    I2C_LCD_Print(LCD1_WRITE, "        [2]         ");
    I2C_LCD_Goto(1, 1, LCD1_WRITE);
    I2C_LCD_Print(LCD1_WRITE, "     [Watering]     ");
    break;
case 5: // Spraying Menu
    I2C_LCD_Goto(1, 4, LCD1_WRITE);
    I2C_LCD_Print(LCD1_WRITE, sprayingIntervalString);
    I2C_LCD_Goto(1, 3, LCD1_WRITE);
    I2C_LCD_Print(LCD1_WRITE, sprayingToString);
    I2C_LCD_Goto(1, 2, LCD1_WRITE);
    I2C_LCD_Print(LCD1_WRITE, sprayingFromString);
    I2C_LCD_Goto(1, 1, LCD1_WRITE);
    I2C_LCD_Print(LCD1_WRITE, "     [Spraying]     ");
    break;
case 6: // Air Conditioner Menu
    I2C_LCD_Goto(1, 4, LCD1_WRITE);
    I2C_LCD_Print(LCD1_WRITE, conditionerIntervalString);
    I2C_LCD_Goto(1, 3, LCD1_WRITE);
    I2C_LCD_Print(LCD1_WRITE, conditionerToString);
    I2C_LCD_Goto(1, 2, LCD1_WRITE);
    I2C_LCD_Print(LCD1_WRITE, conditionerFromString);
    I2C_LCD_Goto(1, 1, LCD1_WRITE);
    I2C_LCD_Print(LCD1_WRITE, "  [Air Conditioner] ");
    break;
case 7: // Temperature-Humidity Menu
    I2C_LCD_Goto(1, 4, LCD1_WRITE);
    I2C_LCD_Print(LCD1_WRITE, temperatureAndHumidityMaxString);
    I2C_LCD_Goto(1, 3, LCD1_WRITE);
    I2C_LCD_Print(LCD1_WRITE, temperatureAndHumidityMinString);
    I2C_LCD_Goto(1, 2, LCD1_WRITE);
    I2C_LCD_Print(LCD1_WRITE, "--------------------");
    I2C_LCD_Goto(1, 1, LCD1_WRITE);
    I2C_LCD_Print(LCD1_WRITE, "Temperature-Humidity");
    break;
}
}

void cursorGoToCorrectPosition(void)
{
switch (menu)
{
case 0: // General Menu, no Cursor
    break;
default:
    I2C_LCD_Goto(cursorCol, cursorRow, LCD1_WRITE);
    if (rotarySetToChange)
    {
        I2C_LCD_BlinkON(LCD1_WRITE);
    }
    else
    {

        I2C_LCD_CursorON(LCD1_WRITE);
    }
    break;
}
}

I do not want to go any further with the code, this is already exhausting, unless you want to.

A few things to summarize my thoughts, please note that the printStrings() function is called before the cursorGoToCorrrectPosition() function, so I am sure it is a hardware problem, maybe in the LCD it self?, because it prints a line or two out of four lines of strings, then it updates the cursor position correctly then it returns to print the remaining lines of strings.

My question is, why is there a delay at all? why is the delay only in the first scroll? how could I solve this delay?

Thank you all in advance.

\$\endgroup\$
17
  • 2
    \$\begingroup\$ Did you write every line of code? Or are you using any library routines between your code and the display hardware? \$\endgroup\$
    – jonk
    Aug 19, 2021 at 0:19
  • 2
    \$\begingroup\$ Thanks. I think that's an important addition. Your writing was informed and I was almost sure you'd say that you wrote it all. But I wasn't completely sure. So I asked. The thought that crossed my mind was initialization (such as a buffer pool) which might occur early and then not later on. But since you wrote it, then I think you'd know where to look for things like that. So I'll table that idea, now. \$\endgroup\$
    – jonk
    Aug 19, 2021 at 0:32
  • 2
    \$\begingroup\$ I think it is time you "instrumented" your code. Allocate an I/O pin for this and hook it to a scope. Use special sequential but fast pulses (make them up as you go) to identify where you are. Or if you have lots of I/O, you can be easier on yourself and just stuff a binary number out. Back in the day, we just laid an AM radio nearby and would learn to "hear" the music of different areas of the code running. Regardless, you need to narrow the turf. (Or someone here has to read your posted code better than I may.) \$\endgroup\$
    – jonk
    Aug 19, 2021 at 0:35
  • 2
    \$\begingroup\$ What do you know about the expander, itself? Is it possible that this is your problem? (I'm just trying to imagine anything that you aren't already in control of -- and that seems like the only thing.) Does it have any software (I've not looked up the device code, yet, sorry.) Or is it a pure hardware implementation IC? \$\endgroup\$
    – jonk
    Aug 19, 2021 at 0:45
  • 2
    \$\begingroup\$ Most of those devices have an IC on them -- I think it's the one you named, or similar. (You can buy just the glass, but then you have to do a whole heck of a lot more work. So it's just not often found that way.) But I've used them. Both the 2 and the 4 line versions. I have a bunch of them in a box here, too. I've never experienced what you are talking about. Not once. They "just work." However, I've never tried to use them via I2C. So that's a difference. \$\endgroup\$
    – jonk
    Aug 19, 2021 at 1:05

2 Answers 2

0
\$\begingroup\$

If your lcd has some obscure defect, we’re not going to be able to help you. You could try another lcd to see if it behaves the same. My money would be on the code causing the issue. Strip your code down to the bare minimum that exhibits the problem.

As a side note, having your program logic in an isr is bad juju. As well, having a rotary encoder generate interrupts is not good practice as well. Have a timer isr read the encoder pins and decode the state. Communicate the encoder position down to the main line code. Do the bare minimum in the isr. Try to minimise the number of interrupts and sources where possible. This is general advice - you can write your code anyway you like, but some ways are better than others.

\$\endgroup\$
6
  • \$\begingroup\$ I will replace the LCD tomorrow morning first thing, and will report back as well, I know you would bet your money on the code, it is a common mistake in the newbie's side :), but I am consider my self to be a beginner not a complete newbie :). I broke down the code into blocks, I developed and tested each block alone to make sure it is running fine, and indeed it is, until that problem appeared, only when I developed and began to test the menus' code. As for the side note, I thank you for that as well, but I have to clarify my idea and vision, maybe I am correct this time.. I hope so :) \$\endgroup\$ Aug 19, 2021 at 1:44
  • \$\begingroup\$ The idea behind my project is to have a good user interface, by good I mean, easy and simple (its relative of course) and fast, and by fast I do mean literal fast, you push something you see the change immediately, you can't wait for the loop in while(1) to finish, I have developed indeed two versions before in this project, one with interrupt driven program and the other with the super loop program, the super loop program was definitely much slower and did not provide good user experience at all, that's why I moved over to the other program. \$\endgroup\$ Aug 19, 2021 at 1:47
  • \$\begingroup\$ Please note that I am not implementing the program logic in the ISR, the logic is to be implemented in the main loop, for ISR is to handle the user interface tasks as fast as possible. \$\endgroup\$ Aug 19, 2021 at 1:47
  • \$\begingroup\$ As for the rotary encoder, I do have a side problem with it yes, it reads sometimes backward for one turn, but I am leaving this problem for now, I know I could solve it with hardware, like tuning the resistors/capacitors values in the RC filter I implemented, or even use your approach of using a timer interrupt. Once again I thank you very much for your advice and I do not mean any offense against you, I am merely expressing my opinion on why do I think my approach is better and I am very open to corrections from anyone. We are here to learn mainly and I thank you all for your time. \$\endgroup\$ Aug 19, 2021 at 1:49
  • \$\begingroup\$ The lcd has a response time of 100ms and you want microsecond response using an interrupt? Realistically you should be able to use no interrupts! Your initial poor response was due to poor implementation not the technique. \$\endgroup\$
    – Kartman
    Aug 19, 2021 at 5:59
0
\$\begingroup\$

The solution is simple, like any other solution :)

I simply forgot to format the strings before printing them.

I have character arrays that hold each line to be printed on the LCD for each menu, and I do format them with sprintf, as it is the easiest way, but since I do have many menus and hence many lines and hence again many character arrays variables, I do not format them all, I only format the ones I need. The problem was I forgot to call this function, so I print empty strings, so literally no change on the LCD happens, until an interrupt coming from the DS3231 RTC each second which formats the strings before printing them. Now after I have scrolled through the whole menu, they are all formatted and ready to use already.

This has nothing to do with my code being bad, or my implementation being poor, it is simply that I forgot to call a function, my implementation is very user friendly and acts very quickly to the user interaction, and this is the main point in the project. I think my implementation is really good, it has proven itself to work flawlessly and quickly. If you have any other opinion, please share with details =)

Thank you all.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.