ARDUINO Как победить лень с Arduino и наукой

ununnamed

★✩✩✩✩✩✩
18 Авг 2020
75
20
Финальная версия кода, прошедшая месяцы полировки:
Безымянный1.png

Изначальный пост (сохранён для истории):

Ежедневная лень мешает жить?

Нужны всего одна кнопка и дисплей

scheme.pnggeneralschemeturned.png
(Ножки кнопки разогнуты и воткнуты в порты. Дисплей с I2C модулем впаян прямо в Ардуино в подходящие отверстия, которые даже располагаются в нужном порядке. Пришлось выпаять аналоговые порты Ардуино, чтобы дисплей поместился)
generalscheme.png

Что делает устройство: Когда нажимаешь на кнопку, выбирается случайное задание и запускается таймер на 25 минут. После выполнения задания на счёт прибавляются деньги (и загорается лампочка, но пока ещё не реализовал), и начинается 5-минутный перерыв.

Я с нуля учил Java, купил Arduino, изучал его синтаксис, провёл недели, пытаясь написать код, и ещё месяцы, пытаясь понять, почему не компилируется. Я читал научные статьи по теме прокрастинации, изучал существующие методы борьбы с ленью, сам боролся с желанием всё бросить, но, пройдя через весь этот ад, создал этот проект.

За основу взял Метод Помидора. Умные дядьки доказали, что наиболее продуктивно человек работает в таком режиме: 25 минут работы, 5 минут перерыва. Один такой цикл называется "Помидор". Каждые 4 "Помидора" нужно делать 30-минутный перерыв.

Чтобы замотивировать, я прочитал с десяток научных статей, и объединил их советы по-своему: за выполнение заданий начисляются деньги. Тут магии нет, деньги берутся из копилки, но с одним условием. Те деньги, которые заработаны на устройстве, можно потратить на ВСЁ, ЧТО УГОДНО. Это уточнение очень важно. Хоть на премиум аккаунт, хоть на алкоголь, хоть на подписку на Спотифай - на любую "хрень" (или не хрень, а реально полезную вещь), на которую в обычной жизни жалко потратить деньги. Тут всё просто: деньги, которые тут можно заработать, имеют небольшую ценность (ну около 250 рублей в день), но когда ты понимаешь, что на эти деньги ты можешь купить ВООБЩЕ всё, что хочешь, без малейшего зазрения совести, то они обретают огромную ценность. Да даже элементарно Киндер Сюрприз я не ел 10 лет, потому что жаба душила потратить 60 рублей на 10 грамм шоколада.
Также тут работает математика. Чтобы получить бесплатно премиумный танк в условной игре про танки, авиацию и флот нужно неделю играть в игру. Тут же ты получишь его за 1 день, так ещё и не будешь гореть с команды, не будешь нервничать, не будешь портить зрение и позвоночник, ещё и саморазвитием занимаешься.
А если собрать это устройство для ребёнка - это будет учить его ценить своё время, и ценить деньги (вот это почти дословная цитата из научного журнала). Ребёнок 1 час делает задания и получает 50 рублей - ребёнок может потратить эти 50 рублей на час игры на компьютере, а может поучиться ещё 1 час и на 125 рублей купить игрушку или сходить в Макдак.

Ещё одна важная черта устройства: задания из списка выбираются в случайном порядке. Нигде нет таких аналогов с такой функцией, а она очень важна. Ну ну могу я выбирать из 10 скучных заданий то, которое менее скучное. А тратя даже по 25 минут на задание, ты приближаешься к его завершению.

И финальная функция (которая содержится в улучшенной версии проекта) - это световая индикация. В проекте есть один RGD светодиод. Когда ты не сделал ни одного задания светится красный светодиод. Он и светит, и бесит. Когда выполнил одно задание, цвет меняется. Даже выбор цветов основан на литературных данных. Световая индикация один из лучших способов наградить психику человека. Так это ещё не всё: когда выполнены 4 задания, загорается красивый зелёный цвет, и за крайнее выполненное задание даётся в 2 раза больше денег.

Как работать с этим устройством:
scheme_infa.png
0) Собираете устройство по схеме. Подключаете плату к компьютеру. Загружаете скетч на Ардуино. Не отключаете плату от компьютера.
1) Добавить задание. Открываете консоль (монитор COM порта, ну лупа справа вверху в окне с кодом Ардуино) и печатаете туда на английском языке задание, можно использовать пробелы. В длину оно должно быть не более 13 символов. (И отключите в низу в окне консоли добавление новой линии в конце строки.) (Задание можно добавлять только когда НЕ запущен таймер)
2) Нажимаете на кнопку
3) Программа выбирает задание. Вы видите его название на экране. 25 минут усердно выполняете задание. Если в это время нажать кнопку, то задание прервётся. Если нажать 3 раза кнопку, то текущее задание удалится из памяти
4) После выполнения задания вам зачисляется зарплата и начинается отдых (я назвал его "REST"). Если нажать кнопку, то отдых закончится
5) Если хотите выполнить ещё задание - нажимаете на кнопку (пункт 2)
-) Если, во время того, как таймер выключен (другими словами, когда на экране отображается только количество денег) нажать 3 раза кнопку, то количество денег обнулится. Это нужно в тех случаях, когда вы тратите эти деньги в реальном мире

Код программы: (обратите внимание на верхние строки - в них можно выбирать параметры под свой вкус, но научные исследования не советуют это делать)
POMODORO:
#include <LiquidCrystal_I2C.h>
#include <GyverButton.h>
#include <EEPROM.h>
LiquidCrystal_I2C lcd(0x27, 16, 2);
GButton startButt(2); //кнопка подключена на 9 пин одной ногой, и на землю (Ground, GND) другой ногой
int workTime = 25; //время одного задания в минутах (по умолчанию: 25)
int idleTime = 5; //время отдыха в минутах (по умолчанию: 5)
byte tasksBeforeBigIdle = 4; //сколько нужно выполнить заданий до большого перерыва (по умолчанию: 4)
int bigIdleTime = 30; //время большого перерыва в минутах (по умолчанию 30);
bool doubleMoney = true; //давать ли двойные деньги за каждое выполненное четвёртое задание (по умолчанию: true) (на 20% повышает средний доход)
int goldPerMinute = 1; //денег за минуту выполнения задания (по умолчанию: 1)
int gold;
int tasksCompleted;
int selectedTaskNumber = 1;
int previouslySelectedTaskNumber = 11; // 11 - код в первый прогон выбросит из выдачи 11 задание (но у нас их всего 10, поэтому мы не заметим этого). Это костыль
bool countDownCompleted;

char selectedTaskName[18];
int selectedTaskAddress;

byte Filler1[] = { //закрашенный на 1/5 прямоугольник (нужен для красивого заполнения шкалы в таймере)
  B10000,
  B10000,
  B10000,
  B10000,
  B10000,
  B10000,
  B10000,
  B10000
};
byte Filler2[] = { //закрашенный на 2/5 прямоугольник (нужен для красивого заполнения шкалы в таймере)
  B11000,
  B11000,
  B11000,
  B11000,
  B11000,
  B11000,
  B11000,
  B11000
};
byte Filler3[] = { //закрашенный на 3/5 прямоугольник (нужен для красивого заполнения шкалы в таймере)
  B11100,
  B11100,
  B11100,
  B11100,
  B11100,
  B11100,
  B11100,
  B11100
};
byte Filler4[] = { //закрашенный на 4/5 прямоугольник (нужен для красивого заполнения шкалы в таймере)
  B11110,
  B11110,
  B11110,
  B11110,
  B11110,
  B11110,
  B11110,
  B11110
};
byte Filler5[] = { //закрашенный на 5/5 прямоугольник (нужен для красивого заполнения шкалы в таймере)
  B11111,
  B11111,
  B11111,
  B11111,
  B11111,
  B11111,
  B11111,
  B11111
};

void setup() {
  //EEPROM.write(555, 1); //заскомментируй, если зачем-то хочешь полностью очистить память. После загрузки на плату закомментируй и загрузи заново.
  pinMode(8, OUTPUT);
  digitalWrite(8, HIGH);

  if (EEPROM.read(555) != 2) { //проверяем, происходит ли запуск в первый раз
    for (int i = 1; i <= 20; i++) {
      EEPROM.put(i * 20, ""); //очищаем ячейки, в которых будут храниться задания
    }
    EEPROM.put(1000, ""); //очищаем ячейку хранения годы
    EEPROM.put(555, 2); //записываем в проверяемую ячейку заданное число. Если прога увидит это число, она будет знать, что программу уже запускали
  }
  Serial.begin(9600); //включаем доступ к консоли компа
  lcd.init(); //запускаем дисплей
  lcd.backlight(); //включаем подсветку
  lcd.createChar(1, Filler1); //создаём символы прямоугольников, чтобы они заполняли шкалу таймера
  lcd.createChar(2, Filler2);
  lcd.createChar(3, Filler3);
  lcd.createChar(4, Filler4);
  lcd.createChar(5, Filler5);
  randomSeed(millis()); //создаём новое зерно для рандома, чтобы задания каждый раз выбирались в новом порядке
  gold = EEPROM.read(1000); //загружаем значение голды из внутренней памяти ардуино (1000 - это адрес значения голды в памяти. Выбрал число от балды)
  printGold(); //выводим на экран деньги
  lcd.backlight();
}

void loop() {
  importDataFromConsole(); //если в консоль пишем слово, оно добавляется в список заданий
  startButt.tick(); //опрашиваем кнопку СТАРТ
  if (startButt.isSingle()) { //если кнопка нажата 1 раз
    selectNewTask(); //выбрать новое задание
    startNewTask(); //начать новое задание //если нажать СТАРТ, то таймер выключится
    if (countDownCompleted == 1) { //если время задания подошло к концу
      giveGold(); //дать деньги
      countDownCompleted = 0; //костыль, потому что countDown() после выполнения переключает флаг countDownCompleted = 1
      ++tasksCompleted; //увеличить счетчик выполненных заданий
      if (tasksCompleted < tasksBeforeBigIdle) { //если выполнено меньше 4 заданий
        lcd.setCursor(13, 0);
        lcd.print(gold); //вывести на экран обновленную сумму денег
        startIdle(idleTime); //начать отдых //если нажать СТАРТ, то таймер выключится
        countDownCompleted = 0;
      }
      else       { //если выполнены 4 задания, то начинается большой отдых
        if (doubleMoney) {
          giveGold();
        }; //если включены двойные деньги за серию заданий - дать деньги ещё раз
        tasksCompleted = 0; //обнуляем число выполненных заданий
        lcd.setCursor(13, 0);
        lcd.print(gold); //вывести на экран обновленную сумму денег
        startIdle(bigIdleTime); //начать большой отдых //если нажать СТАРТ, то таймер выключится
        countDownCompleted = 0;
      }
    }
  };
  if (startButt.isTriple()) { //если кнопка старт нажата 3 раза, то обнуляем деньги
    gold = 0;
    EEPROM.put(1000, gold); //записываем голд в энергонезависимую память;
    printGold();
  }
}

void importDataFromConsole() {
  if (Serial.available() > 0) { //если в консоли есть текст
    // тогда читаем этот текст:
    String incomingText = Serial.readString();
    for (int i = 1; i <= 20; i++) { //максимум можно записать 20 заданий
      int firstLetter = EEPROM.read((i * 20));
      if (firstLetter == 0) { //если ячейка пустая, то импортируем текст
        char incomingTask[18];
        strcpy(incomingTask, incomingText.c_str());
        EEPROM.put((i * 20), incomingTask);
        lcd.setCursor(0, 0);
        lcd.backlight();
        lcd.print(incomingText); //на одну секунду выводим на экран название нового задания
        delay(1000);
        lcd.noBacklight();
        lcd.clear();

        //часть кода, которая пишет в консоль список заданий в памяти устройства, и сколько осталось свободного места
        int freeSpaceOfTasks = 20;

        for (int j = 1; j <= 20; j++) {
          if (EEPROM.read(j * 20) != 0) {
            char existingTask[18];
            EEPROM.get((j * 20), existingTask);
            Serial.print(j);
            Serial.print(" задание: ");
            Serial.println(existingTask);
            freeSpaceOfTasks = freeSpaceOfTasks - 1;
          }
        }
        Serial.print("Можно добавить ещё ");
        Serial.print(freeSpaceOfTasks);
        Serial.println(" заданий"); //окончание кода вывода списка заданий в консоль
        break;
      }
    }
  }
}

void selectNewTask() {
  int i = 0;
  while ((EEPROM.read(selectedTaskNumber * 20) == 0) || selectedTaskNumber == previouslySelectedTaskNumber) { //если выбралось пустое задание, или то, которое уже было, то заново "бросаем кубик"
    selectedTaskNumber = random(1, 11); //от 1(включительно) до 21(не включительно)
    ++i;
    if (i > 40) { //если в списке всего одно задание, то выберется оно (по сути, если код выполнится "впустую" более 40 раз)
      selectedTaskNumber = previouslySelectedTaskNumber;
      break;
    }
  }

  previouslySelectedTaskNumber = selectedTaskNumber;
  selectedTaskAddress = selectedTaskNumber * 20;
  EEPROM.get(selectedTaskAddress, selectedTaskName);

}

void startNewTask() {
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print(selectedTaskName); //выводим на дисплей название задания
  lcd.setCursor(12, 0);
  lcd.print("P");
  lcd.setCursor(13, 0);
  lcd.print(gold); //выводим на дисплей деньги
  countDown(workTime, 5, 1, selectedTaskName);
}

void countDown(int Time, byte cursorX, byte cursorY, char printedText[18]) { //таймер обратного отсчёта с записью данных на LCD
  lcd.backlight();
  lcd.setCursor(0, 0);
  lcd.print(printedText);
  long  startTime = millis();
  while (((Time * 60000) + startTime) > millis()) {
    long      TimeLeft = (((Time * 60000) + startTime) - millis()) / 1000; //сколько осталось времени в секундах
    drawLowLine(Time, TimeLeft);
    lcd.setCursor(cursorX, cursorY);
    if (TimeLeft / 60 < 10) {
      lcd.print("0");
      lcd.setCursor(cursorX + 1, cursorY);
      lcd.print(TimeLeft / 60); //перевод в минуты
    } else {
      lcd.print(TimeLeft / 60);
    };
    lcd.setCursor(cursorX + 2, cursorY);
    lcd.print(":");
    lcd.setCursor(cursorX + 3, cursorY);
    if (TimeLeft % 60 < 10) { //перевод в секунды
      lcd.print("0");
      lcd.setCursor(cursorX + 4, cursorY);
      lcd.print(TimeLeft % 60);
    } else {
      lcd.print(TimeLeft % 60);
    };
    if (TimeLeft <= 0) {
      countDownCompleted = 1;
    }; //флаг, позволяет отследить, прерывался ли таймер. Деньги выдаются только если он не прерывался
    startButt.tick();
    if (startButt.isSingle()) { //если кнопка СТАРТ нажата, то таймер прерывается...
      break;
    }
    else if (startButt.isTriple()) { //если кнопка СТАРТ нажата 3 раза, то это задание удаляется
      EEPROM.put((previouslySelectedTaskNumber * 20), 0);
      break;
    }
  };
  printGold();
}

void drawLowLine(int Time, long TimeLeft) {
  float accurateLowLine = (((Time * 60) - TimeLeft) * 16) / (Time * 60)  ; //16 - это количество символов на дисплее;
  int lowLine = floor(accurateLowLine);
  lcd.setCursor(lowLine, 1);
  float accurateLittleLowLine = ((((Time * 60) - TimeLeft) * 16 * 10) / (Time * 60)) % 10  ; //16 - это количество символов на дисплее;
  int littleLowLine = ceil(accurateLittleLowLine);
  if (littleLowLine >= 0 && littleLowLine < 2) {
    lcd.write(1);
  }
  else if (littleLowLine >= 2 && littleLowLine < 4) {
    lcd.write(2);
  }
  else if (littleLowLine >= 4 && littleLowLine < 6) {
    lcd.write(3);
  }
  else if (littleLowLine >= 6 && littleLowLine < 8) {
    lcd.write(4);
  }
  else if (littleLowLine >= 8 && littleLowLine < 10) {
    lcd.write(5);
  };
}

void giveGold() {
  gold = gold + goldPerMinute * workTime;
  EEPROM.put(1000, gold); //записываем голд в энергонезависимую память
}

void startIdle(int idleTimeVal) {
  char rest[18] = "REST";
  countDown(idleTimeVal, 5, 0, rest);
}

void printGold() {
  lcd.clear(); //стирается всё на экране
  lcd.setCursor(12, 0); //и печатаются деньги
  lcd.print("P");
  lcd.setCursor(13, 0);
  lcd.print(gold);
  lcd.noBacklight();
}

}
Это реально мой первый написанный выстраданный код. Если честно, я пишу этот пост только чтобы не потерять волшебные функции записи String в EEPROM.

Комментарии по коду: все данные записываются в EEPROM - поэтому список заданий и денег не удаляется при отключении от USB. Код закомментирован по максимуму, чтобы даже я вспомнил, как что работает.

Обратите внимание на первую закомментированную строку в void setup() - используйте, если вдруг пригодится сбросить устройство до заводских настроек (хотя не должно).

Если захотите добавить зажигание светодиодов с каждым выполненным заданием, для вас переменная tasksCompleted . Она принимает значения от 0 до 4. При 4 сразу же обнуляется. (Как придут детали, я допишу эту часть кода и удалю этот комментарий)

Отдельно скопирую фрагмент кода, который я месяц искал, чтобы всё заработало. Запись String в EEPROM:
String to EEPROM:
#include <EEPROM.h>

// Constants:-
String myString="Arduino UNO";

void setup()
{
    Serial.begin(9600);
    char myBoard[15];
    strcpy(myBoard,myString.c_str());
    EEPROM.put(0,myBoard);

    char theBoard[15];
    EEPROM.get(0,theBoard);
    Serial.println(theBoard);
}
Схема стала намного сложнее, и требует пайки, либо макетной платы, но только так можно оценить самый полный и лучший опыт использования устройства

complexScheme.png
Световая индикация помогает следить, сколько заданий выполнено. Когда заканчивается таймер, светодиод несколько раз мигает - и это удобно даёт понять, что начался перерыв.

POMODORO_PRO:
#include <LiquidCrystal_I2C.h>
#include <GyverButton.h>
#include <EEPROM.h>
#include <GyverRGB.h>
GRGB diode(10, 9, 6);  // куда подключены цвета светодиода (R, G, B)
LiquidCrystal_I2C lcd(0x27, 16, 2);
GButton startButt(2); //кнопка подключена на 9 пин одной ногой, и на землю (Ground, GND) другой ногой
int workTime = 25; //время одного задания в минутах (по умолчанию: 25)
int idleTime = 5; //время отдыха в минутах (по умолчанию: 5)
byte tasksBeforeBigIdle = 4; //сколько нужно выполнить заданий до большого перерыва (по умолчанию: 4)
int bigIdleTime = 30; //время большого перерыва в минутах (по умолчанию 30);
bool doubleMoney = true; //давать ли двойные деньги за каждое выполненное четвёртое задание (по умолчанию: true) (на 20% повышает средний доход)
int goldPerMinute = 1; //денег за минуту выполнения задания (по умолчанию: 1)
int maxDelay = 2; //через сколько часов после выполнения последнего задания счётчик заданий сбросится (по умолчанию: 2) (например, чтобы через 2 часа отдыха снова загорелась красная лампочка, намекая, что нужно поработать)
int gold;
int tasksCompleted;
int selectedTaskNumber = 1;
int previouslySelectedTaskNumber = 11; // 11 - код в первый прогон выбросит из выдачи 11 задание (но у нас их максимум 20, а обычно не более 5, поэтому мы не заметим этого). Это костыль
bool countDownCompleted;
long timeOfLastTask;

char selectedTaskName[18];
int selectedTaskAddress;

byte Filler1[] = { //закрашенный на 1/5 прямоугольник (нужен для красивого заполнения шкалы в таймере)
  B10000,
  B10000,
  B10000,
  B10000,
  B10000,
  B10000,
  B10000,
  B10000
};
byte Filler2[] = { //закрашенный на 2/5 прямоугольник (нужен для красивого заполнения шкалы в таймере)
  B11000,
  B11000,
  B11000,
  B11000,
  B11000,
  B11000,
  B11000,
  B11000
};
byte Filler3[] = { //закрашенный на 3/5 прямоугольник (нужен для красивого заполнения шкалы в таймере)
  B11100,
  B11100,
  B11100,
  B11100,
  B11100,
  B11100,
  B11100,
  B11100
};
byte Filler4[] = { //закрашенный на 4/5 прямоугольник (нужен для красивого заполнения шкалы в таймере)
  B11110,
  B11110,
  B11110,
  B11110,
  B11110,
  B11110,
  B11110,
  B11110
};
byte Filler5[] = { //закрашенный на 5/5 прямоугольник (нужен для красивого заполнения шкалы в таймере)
  B11111,
  B11111,
  B11111,
  B11111,
  B11111,
  B11111,
  B11111,
  B11111
};

void setup() {
  //EEPROM.write(555, 1); //заскомментируй, если зачем-то хочешь полностью очистить память. После загрузки на плату закомментируй и загрузи заново.
  pinMode(8, OUTPUT);
  digitalWrite(8, HIGH);

  if (EEPROM.read(555) != 2) { //проверяем, происходит ли запуск в первый раз
    for (int i = 1; i <= 20; i++) {
      EEPROM.put(i * 20, ""); //очищаем ячейки, в которых будут храниться задания
    }
    EEPROM.put(1000, ""); //очищаем ячейку хранения годы
    EEPROM.put(555, 2); //записываем в проверяемую ячейку заданное число. Если прога увидит это число, она будет знать, что программу уже запускали
  }
  Serial.begin(9600); //включаем доступ к консоли компа
  lcd.init(); //запускаем дисплей
  lcd.backlight(); //включаем подсветку
  lcd.createChar(1, Filler1); //создаём символы прямоугольников, чтобы они заполняли шкалу таймера
  lcd.createChar(2, Filler2);
  lcd.createChar(3, Filler3);
  lcd.createChar(4, Filler4);
  lcd.createChar(5, Filler5);
  randomSeed(millis()); //создаём новое зерно для рандома, чтобы задания каждый раз выбирались в новом порядке
  gold = EEPROM.read(1000); //загружаем значение голды из внутренней памяти ардуино (1000 - это адрес значения голды в памяти. Выбрал число от балды)
  printGold(); //выводим на экран деньги
  lcd.backlight();
  diode.setRGB(2, 0, 0); //пока задания не начались, светодиод горит красным
}

void loop() {
  importDataFromConsole(); //если в консоль пишем слово, оно добавляется в список заданий
  startButt.tick(); //опрашиваем кнопку СТАРТ
  if (startButt.isSingle()) { //если кнопка нажата 1 раз
    selectNewTask(); //выбрать новое задание
    startNewTask(); //начать новое задание //если нажать СТАРТ, то таймер выключится
    if (countDownCompleted == 1) { //если время задания подошло к концу
      giveGold(); //дать деньги
      countDownCompleted = 0; //костыль, потому что countDown() после выполнения переключает флаг countDownCompleted = 1
      ++tasksCompleted; //увеличить счетчик выполненных заданий
      lcdBlink(); //мигаем светодиодом, когда задание закончено
      startLCD(); //переключить цвет светодиода
      if ((tasksCompleted % tasksBeforeBigIdle) > 0) { //если выполнено меньше 4 заданий
        lcd.setCursor(13, 0);
        lcd.print(gold); //вывести на экран обновленную сумму денег
        startIdle(idleTime); //начать отдых //если нажать СТАРТ, то таймер выключится
        countDownCompleted = 0;
        timeOfLastTask = millis(); //начинаем отсчёт со времени последнего задания
      }
      else       { //если выполнены 4 задания, то начинается большой отдых
        if (doubleMoney) {
          giveGold();
        }; //если включены двойные деньги за серию заданий - дать деньги ещё раз
        tasksCompleted = 0; //обнуляем число выполненных заданий
        lcd.setCursor(13, 0);
        lcd.print(gold); //вывести на экран обновленную сумму денег
        startIdle(bigIdleTime); //начать большой отдых //если нажать СТАРТ, то таймер выключится
        countDownCompleted = 0;
        timeOfLastTask = millis(); //начинаем отсчёт со времени последнего задания
      }
    }
  };
  if (startButt.isTriple()) { //если кнопка старт нажата 3 раза, то обнуляем деньги
    gold = 0;
    EEPROM.put(1000, gold); //записываем голд в энергонезависимую память;
    printGold();
  };
  if ((millis() - timeOfLastTask) >= (maxDelay * 3600000)) { //если с последнего задания прошло очень много времени
    tasksCompleted = 0; //обнуляем количество выполненых заданий
    diode.setRGB(2, 0, 0); //включаем красный светодиод
  };
}

void importDataFromConsole() {
  if (Serial.available() > 0) { //если в консоли есть текст
    // тогда читаем этот текст:
    String incomingText = Serial.readString();
    for (int i = 1; i <= 20; i++) { //максимум можно записать 20 заданий
      int firstLetter = EEPROM.read((i * 20));
      if (firstLetter == 0) { //если ячейка пустая, то импортируем текст
        char incomingTask[18];
        strcpy(incomingTask, incomingText.c_str());
        EEPROM.put((i * 20), incomingTask);
        lcd.setCursor(0, 0);
        lcd.backlight();
        lcd.print(incomingText); //на одну секунду выводим на экран название нового задания
        delay(1000);
        lcd.noBacklight();
        lcd.clear();

        //часть кода, которая пишет в консоль список заданий в памяти устройства, и сколько осталось свободного места
        int freeSpaceOfTasks = 20;

        for (int j = 1; j <= 20; j++) {
          if (EEPROM.read(j * 20) != 0) {
            char existingTask[18];
            EEPROM.get((j * 20), existingTask);
            Serial.print(j);
            Serial.print(" задание: ");
            Serial.println(existingTask);
            freeSpaceOfTasks = freeSpaceOfTasks - 1;
          }
        }
        Serial.print("Можно добавить ещё ");
        Serial.print(freeSpaceOfTasks);
        Serial.println(" заданий"); //окончание кода вывода списка заданий в консоль
        break;
      }
    }
  }
}

void selectNewTask() {
  int i = 0;
  while ((EEPROM.read(selectedTaskNumber * 20) == 0) || selectedTaskNumber == previouslySelectedTaskNumber) { //если выбралось пустое задание, или то, которое уже было, то заново "бросаем кубик"
    selectedTaskNumber = random(1, 11); //от 1(включительно) до 21(не включительно)
    ++i;
    if (i > 40) { //если в списке всего одно задание, то выберется оно (по сути, если код выполнится "впустую" более 40 раз)
      selectedTaskNumber = previouslySelectedTaskNumber;
      break;
    }
  }

  previouslySelectedTaskNumber = selectedTaskNumber;
  selectedTaskAddress = selectedTaskNumber * 20;
  EEPROM.get(selectedTaskAddress, selectedTaskName);

}

void startNewTask() {
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print(selectedTaskName); //выводим на дисплей название задания
  lcd.setCursor(12, 0);
  lcd.print("P");
  lcd.setCursor(13, 0);
  lcd.print(gold); //выводим на дисплей деньги
  countDown(workTime, 5, 1, selectedTaskName);
}

void countDown(int Time, byte cursorX, byte cursorY, char printedText[18]) { //таймер обратного отсчёта с записью данных на LCD
  lcd.backlight();
  lcd.setCursor(0, 0);
  lcd.print(printedText);
  long  startTime = millis();
  while (((Time * 60000) + startTime) > millis()) {
    long      TimeLeft = (((Time * 60000) + startTime) - millis()) / 1000; //сколько осталось времени в секундах
    drawLowLine(Time, TimeLeft);
    lcd.setCursor(cursorX, cursorY);
    if (TimeLeft / 60 < 10) {
      lcd.print("0");
      lcd.setCursor(cursorX + 1, cursorY);
      lcd.print(TimeLeft / 60); //перевод в минуты
    } else {
      lcd.print(TimeLeft / 60);
    };
    lcd.setCursor(cursorX + 2, cursorY);
    lcd.print(":");
    lcd.setCursor(cursorX + 3, cursorY);
    if (TimeLeft % 60 < 10) { //перевод в секунды
      lcd.print("0");
      lcd.setCursor(cursorX + 4, cursorY);
      lcd.print(TimeLeft % 60);
    } else {
      lcd.print(TimeLeft % 60);
    };
    if (TimeLeft <= 0) {
      countDownCompleted = 1;
    }; //флаг, позволяет отследить, прерывался ли таймер. Деньги выдаются только если он не прерывался
    startButt.tick();
    if (startButt.isSingle()) { //если кнопка СТАРТ нажата, то таймер прерывается...
      break;
    }
    else if (startButt.isTriple()) { //если кнопка СТАРТ нажата 3 раза, то это задание удаляется
      EEPROM.put((previouslySelectedTaskNumber * 20), 0);
      break;
    }
  };
  printGold();
}

void drawLowLine(int Time, long TimeLeft) {
  float accurateLowLine = (((Time * 60) - TimeLeft) * 16) / (Time * 60)  ; //16 - это количество символов на дисплее;
  int lowLine = floor(accurateLowLine);
  lcd.setCursor(lowLine, 1);
  float accurateLittleLowLine = ((((Time * 60) - TimeLeft) * 16 * 10) / (Time * 60)) % 10  ; //16 - это количество символов на дисплее;
  int littleLowLine = ceil(accurateLittleLowLine);
  if (littleLowLine >= 0 && littleLowLine < 2) {
    lcd.write(1);
  }
  else if (littleLowLine >= 2 && littleLowLine < 4) {
    lcd.write(2);
  }
  else if (littleLowLine >= 4 && littleLowLine < 6) {
    lcd.write(3);
  }
  else if (littleLowLine >= 6 && littleLowLine < 8) {
    lcd.write(4);
  }
  else if (littleLowLine >= 8 && littleLowLine < 10) {
    lcd.write(5);
  };
}

void giveGold() {
  gold = gold + goldPerMinute * workTime;
  EEPROM.put(1000, gold); //записываем голд в энергонезависимую память
}

void startIdle(int idleTimeVal) {
  char rest[18] = "REST";
  countDown(idleTimeVal, 5, 0, rest);
}

void printGold() {
  lcd.clear(); //стирается всё на экране
  lcd.setCursor(12, 0); //и печатаются деньги
  lcd.print("P");
  lcd.setCursor(13, 0);
  lcd.print(gold);
  lcd.noBacklight();
}

void lcdBlink() { //мигаем светодиодом, когда задание закончено
  diode.setRGB(255, 255, 100);
  delay(200);
  diode.setRGB(0, 0, 0);
  delay(200);
  diode.setRGB(255, 255, 100);
  delay(200);
  diode.setRGB(0, 0, 0);
  delay(200);
  diode.setRGB(255, 255, 100);
  delay(200);
  diode.setRGB(0, 0, 0);
  delay(200);
  diode.setRGB(255, 255, 100);
  delay(200);
  diode.setRGB(0, 0, 0);
  delay(200);
}

void startLCD() { //меняем цвет светодиода в зависимости от количества выполненых заданий
  int i = (tasksCompleted % tasksBeforeBigIdle) ;
  switch (i) {
    case 0:
      diode.setRGB(0, 2, 0); //ставим зеленый
      break;
    case 1:
      diode.setRGB(3, 1, 0); //ставим жёлтый
      break;
    case 2:
      diode.setRGB(1, 0, 1); //ставим фиолетовый
      break;
    case 3:
      diode.setRGB(0, 0, 2); //ставим синий
      break;
  }

}
Еле уместил всё на макетной плате. Когда спаял - вспомнил, что не выпаял из ардуино встроенные светодиоды, пришлось заклеивать изолентой. Не забудьте.

Буду очень благодарен вашим отзывам по использованию, авось и научную статейку напишем
 

Вложения

Изменено:

b707_2

★★✩✩✩✩✩
22 Июл 2020
182
51
психология - тонкая штука :) перекладывать СВОИ деньги из одного кармана в другой и считать это "зарабатыванием" - это сильно :)
 
  • Лойс +1
Реакции: Mikhail1302 и SashaPetrov

ununnamed

★✩✩✩✩✩✩
18 Авг 2020
75
20
@b707_2,
а это, кстати, хорошее наблюдение. Люди бросают самообразование, спорт, прочие дела, потому что не видят сиюминутного результата. Тут же мы преобразуем далеко идущие выгоды в мгновенную награду и закрепляем удовольствие от процесса
 

Эдуард Анисимов

★★★★★★✩
23 Сен 2019
2,265
944
58
Марий-Эл
@ununnamed, Это тупик.
Человек привыкает к сиюминутной выгоде и не будет работать на будущее.
В начале 90 весь бизнес так строился. Урвать и бежать.
Сейчас с этим конечно получше. Но зачем воспитывать рвачей.
 

ununnamed

★✩✩✩✩✩✩
18 Авг 2020
75
20
@Эдуард Анисимов,
Ахах, спасибо, попробую когда-нибудь на полном серьёзе сравнить психологию с Россией 90-х, интересно будет понаблюдать за реакцией собеседника

А так, это доказанная вещь. Да и мне даже интересно, как выглядит человек, который поставил себе задачу "25 минут в день заниматься спортом", который занимается ради сиюминутной выгоды и не работает на будущее. Ну как бы осознанно поставил задачу заниматься спортом, но не чтобы быть здоровым, а чтобы 25 рублей в программе получать. Так ещё и который что-то должен делать, чтобы ежедневные тренировки ни в коем случае не приводили к долговременным результатам.

То же самое про изучение языков, обучение рисованию, крафту

А вообще, мне очень нравится фраза, которую я бы сделал девизом этого устройства:

К цели движется даже тот, кто хотя бы ползёт
 
Изменено:

ununnamed

★✩✩✩✩✩✩
18 Авг 2020
75
20
@Эдуард Анисимов,
тоже верно... В статьях про синтез по сей день правды нет
впрочем вопрос мотивации - это по сути развитие работ Павлова, и за полвека пока не нашли противоречий, только подтверждение эффективности
 

ununnamed

★✩✩✩✩✩✩
18 Авг 2020
75
20
Первые результаты - за эту неделю сделал дел больше, чем за месяц. Случайный выбор задания и деньги стимулируют. Интересно, что будет через месяц
 

PiratFox

★★★★★✩✩
13 Фев 2020
1,694
472
А по мне - если у человека силы воли нет, чтоб себя заставить делать нужные дела, так его хоть ардуиной, хоть шокером долбай. Горбатого могила исправит, да. Впрочем, это моё мнение, основанное на личном жизненном опыте. Никому не навязываю.
 

PiratFox

★★★★★✩✩
13 Фев 2020
1,694
472
Случайный выбор задания и деньги стимулируют.
Наверное, я слишком стар, или не понимаю чего... Как самого себя можно стимулировать своими же деньгами, которые уже есть. Мдааа... Запущено всё. А вообще, чтобы всё успевать делать, так нужно научиться планировать и правильно расставлять приоритеты. Случайный выбор - это изначально тупик.
 

ununnamed

★✩✩✩✩✩✩
18 Авг 2020
75
20
@Wan-Derer,
как же JR сжигает время. Просто впустую, на задалбливание элементарнейших задач. Но даже там есть психологический "пинок" - тёмная материя ;)
Наука, так её
 

Wan-Derer

★★★★★✩✩
Команда форума
31 Июл 2018
2,002
406
Москва
wan-derer.ru
@ununnamed, Я делаю все задачи и не считаю это бесполезным. Какие-то вещи запоминаются на уровне пальцев, просто начинаешь хреначить "аккордами". Если учишься не с нуля, часть задач можно пропускать. "Тёмная материя" не самоцель. Она даётся за решение задач и нужна для открытия следующих разделов. Через какое-то время её становится с избытком, т.о. становится возможным пропускать неинтересные задачи.
Там ещё медальки раздают, но мериться медальками с другими участниками интересно только в самом начале. Потом само по себе решение сложной или непонятной задачи греет больше чем выданное "достижение".
Там есть другая хрень. Курс немножко платный и денежку берут не за уровни, а за время. А сколько уровней ты пройдёшь за это время (точнее сколько решишь задач, лекции-то бесплатные все) зависит от тебя. Как бы провоцируют сидеть на курсе плотнее.
Хотя я пока (18 уровень) умудряюсь проходить бесплатно :)
 

ununnamed

★✩✩✩✩✩✩
18 Авг 2020
75
20
night.pngvis.png
Довёл дело до конца. Впаял RGB светодиод, и собрал всё намертво на макетной плате. Отказался от UNO, взял Nano. Казалось бы, добавить светодиод - но как же усложнилась схема, добавились резисторы, провода. Зато всё теперь работает.

В режиме ожидания горит красный светодиод. По мере выполнения заданий он меняет цвет (и мигает, когда начинается перерыв).
Добавил последнюю функцию - счётчик заданий сбрасывается, если ничего не было сделано в течение 2 часов (легко отключается прописыванием, например, 100 часов вверху кода).

Оставляю тему для архива, буду лишь делиться опытом использования

Спасибо за поддержку и дельные советы, друзья! Без вас бы это не получилось!
 
  • Лойс +1
Реакции: b707_2

b707_2

★★✩✩✩✩✩
22 Июл 2020
182
51
Поздравляю, что преодолел лень и довел проект до завершения :)
 

ununnamed

★✩✩✩✩✩✩
18 Авг 2020
75
20
@b707_2, да тут дело не про лень - хотел было сначала использовать готовую планку из 8 светодиодов, но она уже второй месяц идёт из Китая. И потом призадумался, если кто-то вдруг захочет повторить, зачем мне ему жизнь портить - вот и решил взять вместо планки - самый распространённый вариант - RGB светодиод. Так ещё и всё на плату красиво поместилось
 

ununnamed

★✩✩✩✩✩✩
18 Авг 2020
75
20
Прошло ещё 2 недели. Дописал 2 статьи, подготовил планы, вдвое сократил список дел из ежедневника. В качестве награды купил себе крутые бронированные очки, о которых давно мечтал, но душила жаба (1400 за пластиковые очки!).
На сегодняшний день свободного времени нет, ардуинка стоит без дела
 
Изменено:
  • Лойс +1
Реакции: Wan-Derer

ununnamed

★✩✩✩✩✩✩
18 Авг 2020
75
20
Новая версия кода. ОП пост нельзя дополнить - он слишком большой.
Вдохновился я идеей оригинальной разработки с известного канала:

В новой версии:
  • Добавлен подсчёт времени безделья
  • Добавлен счётчик упущенных из-за безделья денег
  • Счётчик упущенных денег содержит небольшую фору, чтобы его можно было обогнать
  • Двойное нажатие теперь включает\отключает подсветку
  • Счётчики упущенных денег сбрасывается и подсветка выключается через 3 часа после безделья (по сути - это автоматический спящий режим, если лёг спать или ушёл на работу)

Также прикрепляю архив с используемыми библиотеками

C++:
#include <LiquidCrystal_I2C.h>
#include <GyverButton.h>
#include <EEPROM.h>
#include <GyverRGB.h>
GRGB diode(10, 9, 6);  // куда подключены цвета светодиода (R, G, B)
LiquidCrystal_I2C lcd(0x27, 16, 2);
GButton startButt(2); //кнопка подключена на 9 пин одной ногой, и на землю (Ground, GND) другой ногой
int workTime = 25; //время одного задания в минутах (по умолчанию: 25)
int idleTime = 5; //время отдыха в минутах (по умолчанию: 5)
byte tasksBeforeBigIdle = 4; //сколько нужно выполнить заданий до большого перерыва (по умолчанию: 4)
int bigIdleTime = 30; //время большого перерыва в минутах (по умолчанию 30);
bool doubleMoney = true; //давать ли двойные деньги за каждое выполненное четвёртое задание (по умолчанию: true) (на 20% повышает средний доход)
int goldPerMinute = 1; //денег за минуту выполнения задания (по умолчанию: 1)
int maxDelay = 3; //через сколько часов после выполнения последнего задания счётчик заданий сбросится (по умолчанию: 2) (например, чтобы через 2 часа отдыха снова загорелась красная лампочка, намекая, что нужно поработать)
int gold;
int tasksCompleted;
int selectedTaskNumber = 1;
int previouslySelectedTaskNumber = 11; // 11 - код в первый прогон выбросит из выдачи 11 задание (но у нас их максимум 20, а обычно не более 5, поэтому мы не заметим этого). Это костыль
bool countDownCompleted;
long timeOfLastTask;
long lazyTime;
int lostGold;
long startingTime;

char selectedTaskName[18];
int selectedTaskAddress;
int backLight = 1;

byte Filler1[] = { //закрашенный на 1/5 прямоугольник (нужен для красивого заполнения шкалы в таймере)
  B10000,
  B10000,
  B10000,
  B10000,
  B10000,
  B10000,
  B10000,
  B10000
};
byte Filler2[] = { //закрашенный на 2/5 прямоугольник (нужен для красивого заполнения шкалы в таймере)
  B11000,
  B11000,
  B11000,
  B11000,
  B11000,
  B11000,
  B11000,
  B11000
};
byte Filler3[] = { //закрашенный на 3/5 прямоугольник (нужен для красивого заполнения шкалы в таймере)
  B11100,
  B11100,
  B11100,
  B11100,
  B11100,
  B11100,
  B11100,
  B11100
};
byte Filler4[] = { //закрашенный на 4/5 прямоугольник (нужен для красивого заполнения шкалы в таймере)
  B11110,
  B11110,
  B11110,
  B11110,
  B11110,
  B11110,
  B11110,
  B11110
};
byte Filler5[] = { //закрашенный на 5/5 прямоугольник (нужен для красивого заполнения шкалы в таймере)
  B11111,
  B11111,
  B11111,
  B11111,
  B11111,
  B11111,
  B11111,
  B11111
};

void setup() {
  //EEPROM.write(555, 1); //заскомментируй, если зачем-то хочешь полностью очистить память. После загрузки скетча на плату закомментируй и загрузи заново.
  pinMode(8, OUTPUT);
  digitalWrite(8, HIGH);

  if (EEPROM.read(555) != 2) { //проверяем, происходит ли запуск в первый раз
    for (int i = 1; i <= 20; i++) {
      EEPROM.put(i * 20, ""); //очищаем ячейки, в которых будут храниться задания
    }
    EEPROM.put(1000, ""); //очищаем ячейку хранения годы
    EEPROM.put(555, 2); //записываем в проверяемую ячейку заданное число. Если прога увидит это число, она будет знать, что программу уже запускали
  }
  Serial.begin(9600); //включаем доступ к консоли компа
  lcd.init(); //запускаем дисплей
  lcd.backlight(); //включаем подсветку
  lcd.createChar(1, Filler1); //создаём символы прямоугольников, чтобы они заполняли шкалу таймера
  lcd.createChar(2, Filler2);
  lcd.createChar(3, Filler3);
  lcd.createChar(4, Filler4);
  lcd.createChar(5, Filler5);
  randomSeed(millis()); //создаём новое зерно для рандома, чтобы задания каждый раз выбирались в новом порядке
  gold = EEPROM.read(1000); //загружаем значение голды из внутренней памяти ардуино (1000 - это адрес значения голды в памяти. Выбрал число от балды)
  printGold(); //выводим на экран деньги
  diode.setRGB(2, 0, 0); //пока задания не начались, светодиод горит красным
  timeOfLastTask = millis(); //обнуляем время ленности
  startingTime = millis(); //начинаем общий отсчёт времени
}

void loop() {
  importDataFromConsole(); //если в консоль пишем слово, оно добавляется в список заданий
  startButt.tick(); //опрашиваем кнопку СТАРТ
  if (startButt.isDouble()) { //если кнопка нажата 2 раза, то подсветка выключается
    backLight = backLight * (-1);
  }

  if (backLight == 1) {
    lcd.backlight();
  } else {
    lcd.noBacklight();
  }

  if (startButt.isSingle()) { //если кнопка нажата 1 раз
    if (startingTime == 0) {
      startingTime = millis();
    }
    selectNewTask(); //выбрать новое задание
    startNewTask(); //начать новое задание //если нажать СТАРТ, то таймер выключится
    if (countDownCompleted == 1) { //если время задания подошло к концу
      giveGold(); //дать деньги
      countDownCompleted = 0; //костыль, потому что countDown() после выполнения переключает флаг countDownCompleted = 1
      ++tasksCompleted; //увеличить счетчик выполненных заданий
      lcdBlink(); //мигаем светодиодом, когда задание закончено
      startLCD(); //переключить цвет светодиода
      if ((tasksCompleted % tasksBeforeBigIdle) > 0) { //если выполнено меньше 4 заданий
        lcd.setCursor(13, 0);
        lcd.print(gold); //вывести на экран обновленную сумму денег
        startIdle(idleTime); //начать отдых //если нажать СТАРТ, то таймер выключится
        countDownCompleted = 0;
        timeOfLastTask = millis(); //начинаем отсчёт со времени последнего задания
      }
      else       { //если выполнены 4 задания, то начинается большой отдых
        if (doubleMoney) {
          giveGold();
        }; //если включены двойные деньги за серию заданий - дать деньги ещё раз
        tasksCompleted = 0; //обнуляем число выполненных заданий
        lcd.setCursor(13, 0);
        lcd.print(gold); //вывести на экран обновленную сумму денег
        startIdle(bigIdleTime); //начать большой отдых //если нажать СТАРТ, то таймер выключится
        countDownCompleted = 0;
        timeOfLastTask = millis(); //начинаем отсчёт со времени последнего задания
      }
    }
  };
  if (startButt.isTriple()) { //если кнопка старт нажата 3 раза, то обнуляем деньги
    gold = 0;
    EEPROM.put(1000, gold); //записываем голд в энергонезависимую память;
    printGold();
  };
  counter(); //считаем и пишем на экран время безделья
  if (((millis() - timeOfLastTask) < (maxDelay * 3600000)) && (startingTime > 0)) { //если с последнего задания прошло НЕ очень много времени и секундомер не останавливался
    LostGold(); //считаем и пишем упущенную прибыль
  };
  if (((millis() - timeOfLastTask) >= (maxDelay * 3600000)) && ((millis() - timeOfLastTask) <= ((maxDelay * 3600000)+1000))) { //если с последнего задания прошло очень много времени
    tasksCompleted = 0; //обнуляем количество выполненых заданий
    diode.setRGB(2, 0, 0); //включаем обратно красный светодиод
    lostGold = -1;
    startingTime = 0; //обнуляем начальное время для безделья
    printGold();
    backLight = -1; //выключаем экран. Ну зачем ему ночью светить?
  };


}

void importDataFromConsole() {
  if (Serial.available() > 0) { //если в консоли есть текст
    // тогда читаем этот текст:
    String incomingText = Serial.readString();
    for (int i = 1; i <= 20; i++) { //максимум можно записать 20 заданий
      int firstLetter = EEPROM.read((i * 20));
      if (firstLetter == 0) { //если ячейка пустая, то импортируем текст
        char incomingTask[18];
        strcpy(incomingTask, incomingText.c_str());
        EEPROM.put((i * 20), incomingTask);
        lcd.setCursor(0, 0);
        backLight = 1;
        lcd.print(incomingText); //на одну секунду выводим на экран название нового задания
        delay(2000);
        printGold();

        //часть кода, которая пишет в консоль список заданий в памяти устройства, и сколько осталось свободного места
        int freeSpaceOfTasks = 20;

        for (int j = 1; j <= 20; j++) {
          if (EEPROM.read(j * 20) != 0) {
            char existingTask[18];
            EEPROM.get((j * 20), existingTask);
            Serial.print(j);
            Serial.print(" задание: ");
            Serial.println(existingTask);
            freeSpaceOfTasks = freeSpaceOfTasks - 1;
          }
        }
        Serial.print("Можно добавить ещё ");
        Serial.print(freeSpaceOfTasks);
        Serial.println(" заданий"); //окончание кода вывода списка заданий в консоль
        break;
      }
    }
  }
}

void selectNewTask() {
  int i = 0;
  while ((EEPROM.read(selectedTaskNumber * 20) == 0) || selectedTaskNumber == previouslySelectedTaskNumber) { //если выбралось пустое задание, или то, которое уже было, то заново "бросаем кубик"
    selectedTaskNumber = random(1, 11); //от 1(включительно) до 21(не включительно)
    ++i;
    if (i > 40) { //если в списке всего одно задание, то выберется оно (по сути, если код выполнится "впустую" более 40 раз)
      selectedTaskNumber = previouslySelectedTaskNumber;
      break;
    }
  }

  previouslySelectedTaskNumber = selectedTaskNumber;
  selectedTaskAddress = selectedTaskNumber * 20;
  EEPROM.get(selectedTaskAddress, selectedTaskName);

}

void startNewTask() {
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print(selectedTaskName); //выводим на дисплей название задания
  lcd.setCursor(12, 0);
  lcd.print("P");
  lcd.setCursor(13, 0);
  lcd.print(gold); //выводим на дисплей деньги
  countDown(workTime, 5, 1, selectedTaskName);
}

void countDown(int Time, byte cursorX, byte cursorY, char printedText[18]) { //таймер обратного отсчёта с записью данных на LCD
  backLight = 1;
  lcd.setCursor(0, 0);
  lcd.print(printedText);
  long  startTime = millis();
  while (((Time * 60000) + startTime) > millis()) {
    long      TimeLeft = (((Time * 60000) + startTime) - millis()) / 1000; //сколько осталось времени в секундах
    drawLowLine(Time, TimeLeft);
    lcd.setCursor(cursorX, cursorY);
    if (TimeLeft / 60 < 10) {
      lcd.print("0");
      lcd.setCursor(cursorX + 1, cursorY);
      lcd.print(TimeLeft / 60); //перевод в минуты
    } else {
      lcd.print(TimeLeft / 60);
    };
    lcd.setCursor(cursorX + 2, cursorY);
    lcd.print(":");
    lcd.setCursor(cursorX + 3, cursorY);
    if (TimeLeft % 60 < 10) { //перевод в секунды
      lcd.print("0");
      lcd.setCursor(cursorX + 4, cursorY);
      lcd.print(TimeLeft % 60);
    } else {
      lcd.print(TimeLeft % 60);
    };
    if (TimeLeft <= 0) {
      countDownCompleted = 1;
    }; //флаг, позволяет отследить, прерывался ли таймер. Деньги выдаются только если он не прерывался
    startButt.tick();
    if (startButt.isDouble()) { //если кнопка нажата 2 раза, то подсветка выключается
      backLight = backLight * (-1);
    }
    if (backLight == 1) {
      lcd.backlight();
    } else {
      lcd.noBacklight();
    }
    if (startButt.isSingle()) { //если кнопка СТАРТ нажата, то таймер прерывается...
      break;
    }
    else if (startButt.isTriple()) { //если кнопка СТАРТ нажата 3 раза, то это задание удаляется
      EEPROM.put((previouslySelectedTaskNumber * 20), 0);
      break;
    }
  };
  printGold();
}

void drawLowLine(int Time, long TimeLeft) {
  float accurateLowLine = (((Time * 60) - TimeLeft) * 16) / (Time * 60)  ; //16 - это количество символов на дисплее;
  int lowLine = floor(accurateLowLine);
  lcd.setCursor(lowLine, 1);
  float accurateLittleLowLine = ((((Time * 60) - TimeLeft) * 16 * 10) / (Time * 60)) % 10  ; //16 - это количество символов на дисплее;
  int littleLowLine = ceil(accurateLittleLowLine);
  if (littleLowLine >= 0 && littleLowLine < 2) {
    lcd.write(1);
  }
  else if (littleLowLine >= 2 && littleLowLine < 4) {
    lcd.write(2);
  }
  else if (littleLowLine >= 4 && littleLowLine < 6) {
    lcd.write(3);
  }
  else if (littleLowLine >= 6 && littleLowLine < 8) {
    lcd.write(4);
  }
  else if (littleLowLine >= 8 && littleLowLine < 10) {
    lcd.write(5);
  };
}

void giveGold() {
  gold = gold + goldPerMinute * workTime;
  EEPROM.put(1000, gold); //записываем голд в энергонезависимую память
}

void startIdle(int idleTimeVal) {
  char rest[18] = "REST";
  countDown(idleTimeVal, 5, 0, rest);
}

void printGold() {
  lcd.clear(); //стирается всё на экране
  lcd.setCursor(12, 0); //и печатаются деньги
  lcd.print("P");
  lcd.setCursor(13, 0);
  lcd.print(gold);
}

void lcdBlink() { //мигаем светодиодом, когда задание закончено
  diode.setRGB(255, 255, 100);
  delay(200);
  diode.setRGB(0, 0, 0);
  delay(200);
  diode.setRGB(255, 255, 100);
  delay(200);
  diode.setRGB(0, 0, 0);
  delay(200);
  diode.setRGB(255, 255, 100);
  delay(200);
  diode.setRGB(0, 0, 0);
  delay(200);
  diode.setRGB(255, 255, 100);
  delay(200);
  diode.setRGB(0, 0, 0);
  delay(200);
}

void startLCD() { //меняем цвет светодиода в зависимости от количества выполненых заданий
  int i = (tasksCompleted % tasksBeforeBigIdle) ;
  switch (i) {
    case 0:
      diode.setRGB(0, 2, 0); //ставим зеленый
      break;
    case 1:
      diode.setRGB(3, 1, 0); //ставим жёлтый
      break;
    case 2:
      diode.setRGB(1, 0, 1); //ставим фиолетовый
      break;
    case 3:
      diode.setRGB(0, 0, 2); //ставим синий
      break;
  }
}

void counter() { //счётчик времени лени
  lazyTime = (millis() - timeOfLastTask) / 1000; //время лени в секундах

  lcd.setCursor(2, 1); //пишем сотни часов
  if (lazyTime / 3600 < 100) { //если время меньше 100 часов - пропустить клетку
    //lcd.print("0");
  } else {
    lcd.print(lazyTime / 3600 / 100); //пишем число сотен часов
  };

  lcd.setCursor(3, 1); //переходим в следующую клетку //пишем десятки часов
  if (lazyTime / 3600 < 10) { //если время меньше 10 часов - пропустить клетку
    lcd.print("0");
  } else {
    lcd.print((lazyTime / 3600) % 100 / 10);
  };

  lcd.setCursor(4, 1); //пишем часы
  if (lazyTime / 3600 < 1) { //если время меньше 1 часа - пропустить клетку
    lcd.print("0");
  } else {
    lcd.print((lazyTime / 3600) % 10);
  };

  lcd.setCursor(5, 1); //двоеточие между часами и минутами
  if (lazyTime / 3600 < 1) { //если время меньше 1 часа - пропустить клетку
    lcd.print(":");
  } else {
    lcd.print(":");
  };

  lcd.setCursor(6, 1); //пишем десятки минут
  if (lazyTime / 60 < 10) { //если время меньше 10 минут - пропустить клетку
    lcd.print("0");
  } else {
    lcd.print((lazyTime / 60) % 60 / 10);
  };

  lcd.setCursor(7, 1); //пишем минуты
  if (lazyTime / 60 < 1) { //если время меньше 1 минуты - пропустить клетку
    lcd.print("0");
  } else {
    lcd.print((lazyTime / 60) % 10);
  };

  lcd.setCursor(8, 1); //двоеточие между минутами и секундами
  if (lazyTime < 60) { //если время меньше 60 секунд - пропустить клетку
    lcd.print(":");
  } else {
    lcd.print(":");
  };

  lcd.setCursor(9, 1); //пишем десятки секунд
  if (lazyTime < 10) { //если время меньше 10 секунд - пропустить клетку
    lcd.print("0");
  } else {
    lcd.print((lazyTime) % 60 / 10);
  };

  lcd.setCursor(10, 1); //пишем секунды
  if (lazyTime < 1) { //если время меньше 10 секунд - пропустить клетку
    lcd.print("0");
  } else {
    lcd.print((lazyTime) % 10);
  };
}

void LostGold() { //счётчик упущенного золота
  long delayTime;
  if (lostGold == -1) {
   delayTime = (millis() - startingTime + (workTime - idleTime) * 60000) / 1000; //учитываем одно рабочее время для точности (просто таймер не идёт, пока lostGold = -1, а когда он начинает идти, то уже одно задание завершено)
  } else {
    delayTime = (millis() - startingTime) / 1000; //время с начала таймера в секундах
  }



  int longBreaks = delayTime / (((((workTime + idleTime)) * tasksBeforeBigIdle + bigIdleTime)) * 60); // сколько за это время произошло больших перерывов (один в каждые 150 минут)
  lostGold = (delayTime * goldPerMinute * workTime / ((workTime + idleTime * 3) * 60)) - (longBreaks * (bigIdleTime - idleTime) * goldPerMinute); //из-за того, что каждое задание включает после себя неоплачиваемый отдых, то приходится домножать на такой вот поправочный коэффициент
  //idleTime*3 - это для того, чтобы у человека была фора перед бездушным счётчиком
  //workTime/(workTime+idleTime) - поправочный коэффициент учёта времени отдыха в общую плату за задание
  //(longBreaks * (bigIdleTime - idleTime) * goldPerMinute) - учли отсутствие получения денег за большой перерыв
  if (doubleMoney == true) {
    lostGold = lostGold + longBreaks * goldPerMinute * workTime;
  };
  lcd.setCursor(12, 1);
  lcd.print("-");
  lcd.setCursor(13, 1);
  lcd.print(lostGold);
  ;
}
 

Вложения

Изменено:

ununnamed

★✩✩✩✩✩✩
18 Авг 2020
75
20
Финальная версия.
Что нового:
Перепаял экран. С белым экраном стало пользоваться намного удобнее.
Переписана логика начисления бонусов.
Появился бонус за первое выполненное задание в день
Появился бонус за первое выполненное задание
Появился бонус за выполнение заданий без перерывов
Большой перерыв стал обязательным
Добавлена полная защита от перезагрузок. Reset не даст преимуществ
Максимальное количество денег в виртуальном кошельке теперь 9999 (раньше было 256)
Добавлена функция снятия денег со счёта - достаточно прописать в консоль сколько нужно вычесть
Отполировал все возможные баги
Переписал кучу кода, чтобы уместить на Atmega168

Логика финального подсчёта награды:
Выполнил первое задание и отдохнул 5 минут - получаю 25 (за 25 минут работы) + 10 (за первое выполненное задание в день) (итого: 35)
Если начинаю следующее задание менее чем через минуту, то получаю бонус +5, который начислится только после 4 выполненных заданий и весь сгорит, если буду бездельничать больше 1 часа.
В общем, выполняю второе задание, получаю 25 (за 25 минут работы). 5 идут в бонусный счёт (итого 60 (5 на бонусном счету)).
Быстро запускаю и выполняю третье задание. Получаю 25 (за 25 минут работы). 5 идут в бонусный счёт (итого 85 (10 на бонусном счету)).
Быстро запускаю и выполняю четвёртое задание. Получаю 25 (за 25 минут работы) + 25 за четвёртое выполненное задание, 5 идут в бонусный счёт и бонусный счёт, наконец, выдаётся на руки (итого 150 (0 на бонусном счету)).
И начинается перерыв 30 минут, который нельзя пропустить (это важное нововведение, потому что есть соблазн продолжать и продолжать работу, но потом глаза болят)

Код прикрепляю в виде текстового файла.

По поводу пайки. Я пересобрал всё на красной плате Arduino с micro usb с aliexpress, потому что такой разъем распространён больше. При сборке я не забыл выпаять светодиоды питания ардуино и модуля lcd дисплея. Спаял схему так, что все провода сверху макетной платы, и поэтому из платы торчат только ноги ардуино. Просто взял и воткнул эти ноги в хлебную доску. Держится удобно.

И да, результат работы таймера сложно переоценить. Перерывы делаю постоянно, купил себе уже кучу вещей, о которых мечтал, но жалко было денег. Ну и главное, теперь нет невыполненной работы, делаю всё вовремя, жить стало спокойнее. И всего лишь за 60 руб/ч.
 

Вложения

Изменено:
  • Лойс +1
Реакции: Un_ka

ununnamed

★✩✩✩✩✩✩
18 Авг 2020
75
20
@Un_ka,
Все провода залиты клеем БФ-6. Считай, заизолированы

Включена подсветка, запущено задание. Сверху название задания и текущий счёт. Снизу заполняется шкала таймера:
Безымянный1.png

Включена подсветка, задание выключено. Снизу считается время безделья, справа снизу бонус, который получу, если начну делать задание прямо сейчас:
Безымянный2.png

С изначальным синим дисплеем вообще ничего не было видно без подсветки. Тут же всё видно (а ночью не слепит):
Безымянный3.png

Фото со вспышкой, чтобы лучше видеть пайку:
Безымянный4.png
 
  • Лойс +1
Реакции: Un_ka

ununnamed

★✩✩✩✩✩✩
18 Авг 2020
75
20
Это медицинский клей. Используется для заклеивания мелких ран. Малопрочный и маловлагостойкий.
Для таких целей лучше не использовать.
Спасибо за информацию. Для чего то, что 100% не буду распаивать, буду использовать термоклей