/* Наливатор 0.9
*
* Dodato je čuvanje parametara u EEPROM-u prilikom kalibracije
* Dodata je mogućnost vraćanja na fabrička podešavanja (reset to factory)
* Dodat je ekran dobrodošlice !!! izazvalo probleme, trenutno onemogućeno (HELLO_SCREEN) !!!
*
* Upravljanje:
* Dugme "start":
* pritiskanje - pokretanje točenja u ručnom režimu
* zadržavanje 3 sekunde - promena režima (ručni/automatski)
* zadržavanje više od 1 sekunde, ali manje od 3 sekunde - otkazivanje točenja
* Enkoder:
* zadržavanje 1 sekunde - promena zapremine točenja
* zadržavanje 5 sekundi - režim kalibracije pozicija (automatsko čuvanje parametara u EEPROM)
* zadržavanje 10 sekundi - vraćanje parametara na fabrička podešavanja.
* * u režimu promene zapremine:
* * rotacija - promena zapremine od 15 do 75 ml sa korakom od 5 ml
* * pritiskanje - izlaz iz režima
* * u režimu kalibracije pozicija
* * rotacija - promena pozicije od 3 do 177 stepeni sa korakom od 1 stepen
* * pritiskanje - prelazak na sledeću poziciju, nakon podešavanja poslednje pozicije - automatsko čuvanje u EEPROM (pozicije, zapremina) i izlazak iz režima kalibracije
*
* (c) Maks Selivanov aka Vector
* e-mail: [email protected]
*
*/
#define MX1508 // если определено - используется драйвер MX1508, иначе - реле.
//define HELLO_SCREEN
#include <Wire.h>
#include <Adafruit_SSD1306.h>
#include <Fonts/FreeSans9pt7b.h>
#include <Servo.h>
#include <Adafruit_NeoPixel.h>
#include <EEPROM.h>
// Основные константы
#define TOTAL_GLASSES 5 // количество рюмок
#define MIN_VOLUME 15 // минимальный объём, мл
#define MAX_VOLUME 75 // максимальный объём, мл
#define STEP_VOLUME 5 // шаг изменения объёма, мл
#define BASE_VOLUME 30 // базовый объём
#define BASE_FILLING_TIME 3000 // время налива рюмки базового объёма, мс
#define SERVO_MIN_POSITION 3 // минимально возможное положение сервопривода
#define SERVO_MAX_POSITION 177 // максимально возможное положение сервопривода
#define SETTING_TIME 1000 // время удержания кнопки энкодера для изменения объёма налива, мс
#define CALIBRATE_TIME 5000 // время удержания кнопки энкодера для входа в режим калибровкии позиций рюмок, мс
#define RESET_TIME 10000 // время удержания кнопки энкодера для сброса настроек, мс
#define RELEASE_TIME 1000 // время удержания кноки "Старт" до сброса цикла, мс
#define MODE_TIME 3000 // время удержания кнопки "Старт" для смены режима работы (автоматический/ручной)
#define DOT_PRINTING_TIME 40 // время отображения 1 сегмента прогресс-бара, мс
#define DEBOUNCE_TIME 50 // время контроля дребезка кнопки
#define CLOCKWISE -1 // направление вращения энкодера (по часовой стрелке)
#define COUNTERCLOCKWISE 1 // направление вращения энкодера (против часовой стрелки)
#define NO_ROTATION 0 // энкодер не вращается
#define NO_VOLUME false // Отображение режима калибровки на экране
#define PARAM_ADDR 0 // Адрес хранения параметров
#define INIT_ADDR 1023 // Адрес ячейки ключа инициализации
#define INIT_KEY 0 // Ключ инициализации (0..254)
#define READ true // Параметр функции paramEEPROM - прочитать данные
#define WRITE false // ----------- // ----------- - записать данные
#define INIT true // ----------- // ----------- - признак инициализации
#define RED 1
#define YELLOW 2
#define GREEN 3
#define BLUE 4
// Распиновка (Arduino UNO)
#define ENC_CLK_PIN 2 // CLK энкодера
#define ENC_DT_PIN 3 // DT энкодера
#define ENC_BUTTON_PIN 4 // кнопка энкодера
#define SERVO_PIN 9 // сервопривод (кран)
#define START_BUTTON_PIN 10 // кнопка "Старт"
#define POMP_PIN 11 // помпа
#define LEDS_PIN A2 // индикация
const int glassPin[TOTAL_GLASSES] = {5, 6, 7, 8, 12}; // рюмки
// Параметры дисплея
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
// Базовые настройки
const int baseGlassPosition[TOTAL_GLASSES] = {15, 52.5, 90, 127.5, 160};
const int baseServoStartPosition = 0;
const int baseVolume = BASE_VOLUME;
// Константы и переменные
int glassPosition[TOTAL_GLASSES]; // позиции рюмок (*)
int currentVolume = BASE_VOLUME; // текущий объём, мл (*)
int currentFillingTime = BASE_FILLING_TIME; // текущее время наполнения рюмки (текущего объёма), мс
int servoStartPosition = baseServoStartPosition; // стартовая позиция сервопривода (*)
int startButtonState = HIGH; // чтение состояния кнопки "Старт"
bool startButtonFlag = false; // состояние кнопки "Старт" (false - отпущена)
unsigned long startButtonTimer = 0; // таймер удержания кнопки "Старт"
int encoderButtonState = HIGH; // чтение кнопки энкодера
bool encoderButtonFlag = false; // состояние кнопки энкодера (false - отпущена)
unsigned long encoderButtonTimer = 0; // таймер удержания кнопки энкодера
int sensorGlass[TOTAL_GLASSES] = {HIGH, HIGH, HIGH}; // состояние датчика рюмки
bool isGlass = false; // признак наличия хотя бы одной рюмки
enum MODE {AUTO, MANUAL, SETTINGS, CALIBRATE, RESET}; // режимы работы
MODE workMode;
enum STATES {NOGLASS, EMPTY, FILLING, FULL}; // состояния рюмок
STATES stateGlass[TOTAL_GLASSES];
STATES stateGlassPrev[TOTAL_GLASSES];
struct {
int servo;
int volume;
int glass[TOTAL_GLASSES];
} saveData; // запись параметров к EEPROM
// Макросы
#ifdef MX1508
#define _POMP_ON digitalWrite(POMP_PIN, HIGH)
#define _POMP_OFF digitalWrite(POMP_PIN, LOW)
#else
#define _POMP_ON digitalWrite(POMP_PIN, LOW)
#define _POMP_OFF digitalWrite(POMP_PIN, HIGH)
#endif
#define _sign(x) ((x < 0) ? -1 : ((x > 0) ? 1 : 0))
// Создание объектов
Servo srv;
Adafruit_NeoPixel leds(TOTAL_GLASSES, LEDS_PIN);
Adafruit_SSD1306 oled(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
// Запись/чтение параметров из EEPRPOM
void paramEEPROM(unsigned int addr, byte direction, byte init = false){
if (direction == WRITE){
if (init){
saveData.servo = baseServoStartPosition;
saveData.volume = baseVolume;
for (int i = 0; i < TOTAL_GLASSES; i++){
saveData.glass[i] = baseGlassPosition[i];
}
} else {
saveData.servo = servoStartPosition;
saveData.volume = currentVolume;
for (int i = 0; i < TOTAL_GLASSES; i++){
saveData.glass[i] = glassPosition[i];
}
}
EEPROM.put(addr, saveData);
} else {
EEPROM.get(addr, saveData);
servoStartPosition = saveData.servo;
currentVolume = saveData.volume;
for (int i = 0; i < TOTAL_GLASSES; i++){
glassPosition[i] = saveData.glass[i];
}
}
}
// Вращение серво
void goServo(int srvDest){
int srvPos, srvMove;
srv.attach(SERVO_PIN);
srvPos = srv.read();
srvMove = srvDest - srvPos;
for (int srvStep = 0; srvStep < abs(srvMove); srvStep++){
srv.write(srvPos + srvStep * _sign(srvMove));
delay(5);
}
delay(50);
srv.detach();
}
// Изменение статуса рюмки
void changeGlassState(int glass, STATES state){
stateGlassPrev[glass] = stateGlass[glass];
stateGlass[glass] = state;
ledsGlassControl();
}
// Изменение индикации
void setLedColor(int ledNumber, int color = 0){
int r, g, b;
switch (color){
case RED:
leds.setPixelColor(ledNumber, leds.Color(32, 0, 0));
break;
case YELLOW:
leds.setPixelColor(ledNumber, leds.Color(127, 127, 0));
break;
case GREEN:
leds.setPixelColor(ledNumber, leds.Color(0, 127, 0));
break;
case BLUE:
leds.setPixelColor(ledNumber, leds.Color(0, 0, 127));
break;
default:
leds.setPixelColor(ledNumber, leds.Color(0, 0, 0));
}
leds.show();
}
// Установка цвета в зависимости от статуса рюмки
void ledsGlassControl(){
int newColor = 0;
for (int i = 0; i < TOTAL_GLASSES; i++){
switch(stateGlass[i]){
case NOGLASS:
newColor = RED; // или BLUE?
break;
case EMPTY:
newColor = YELLOW;
break;
case FILLING:
newColor = BLUE;
break;
case FULL:
newColor = GREEN;
break;
default:
newColor = 0;
}
setLedColor(i, newColor);
}
}
// Изменение заданного объёма
void changeVol(int encDirection){
currentVolume += STEP_VOLUME*encDirection;
currentVolume = constrain(currentVolume, MIN_VOLUME, MAX_VOLUME);
currentFillingTime = currentVolume * BASE_FILLING_TIME / BASE_VOLUME;
}
// Отображение общего экрана
void oledMainDisplay(bool dispVolume = true){
oled.clearDisplay();
oled.setCursor(15,15);
oled.println("Mod:");
oled.setCursor(55,15);
switch (workMode){
case AUTO:
oled.println("auto");
break;
case MANUAL:
oled.println("rucno");
break;
case SETTINGS:
// oled.println("settings");
oled.println("kolicina");
break;
case CALIBRATE:
oled.println("podes");
break;
case RESET:
oled.println("reset");
}
oled.drawLine(0, 20, 127, 20, WHITE);
if (dispVolume){
oled.setCursor(20, 40);
oled.println("Kol.: " + String(currentVolume) + " ml.");
oled.display();
}
}
void oledCalibrateDisplay(int glassNum){
oledMainDisplay(NO_VOLUME);
oled.setCursor(15, 40);
oled.println("Casa: " + String(glassNum + 1));
oled.setCursor(15, 60);
oled.println("Pozic: " + String(glassPosition[glassNum]));
oled.display();
}
#ifdef HELLO_SCREEN
// Приветствие
void helloScreen(){
oled.clearDisplay();
oled.setCursor(30,14);
oled.println("POMAZE BOG!");
oled.drawLine(0, 20, 127, 20, WHITE);
oled.setCursor(18, 40);
oled.println("Let's drink!!!");
oled.display();
delay(3000);
}
#endif
// Отображение прогресс-бара во время налива рюмки
void showProgressBar (int prog){
oled.drawRoundRect(0, 47, 127, 13, 2, WHITE);
oled.fillRect(0, 48, prog-1, 11, WHITE);
oled.display();
}
// Режим настроек
void settings(){
int encClk;
int encClkPrev = HIGH;
int encDt;
int encDirection;
bool encFlag;
MODE workModePrev = workMode;
workMode = SETTINGS;
oledMainDisplay();
while (digitalRead(ENC_BUTTON_PIN) == HIGH){
encClk = digitalRead(ENC_CLK_PIN);
if (encClk != encClkPrev){
encFlag = !encFlag;
if (encFlag){
encDt = digitalRead(ENC_DT_PIN);
if (encClk == LOW && encDt == HIGH){
// Поворот по часовой стрелке
encDirection = COUNTERCLOCKWISE;
}
if (encClk == LOW && encDt == LOW){
// Поворот против часовой стрелки
encDirection = CLOCKWISE;
}
encClkPrev = encClk;
changeVol(encDirection);
encDirection = NO_ROTATION;
oledMainDisplay();
}
}
}
workMode = workModePrev;
oledMainDisplay();
}
// Калибровка позиций сервы (крана) относительно рюмок
void calibrate(){
int encClk;
int encClkPrev = HIGH;
int encDt;
int encDirection;
bool encFlag;
MODE workModePrev = workMode;
workMode = CALIBRATE;
srv.attach(SERVO_PIN); // Serva se pomera prema enkoderu
for (int i = 0; i < TOTAL_GLASSES; i++){
srv.write(glassPosition[i]);
oledCalibrateDisplay(i);
while (digitalRead(ENC_BUTTON_PIN) == HIGH){
encClk = digitalRead(ENC_CLK_PIN);
if (encClk != encClkPrev){
encFlag = !encFlag;
if (encFlag){
encDt = digitalRead(ENC_DT_PIN);
if (encClk == LOW && encDt == HIGH){
encDirection = COUNTERCLOCKWISE;
}
if (encClk == LOW && encDt == LOW){
encDirection = CLOCKWISE;
}
encClkPrev = encClk;
glassPosition[i] += encDirection;
glassPosition[i] = constrain(glassPosition[i], SERVO_MIN_POSITION, SERVO_MAX_POSITION);
srv.write(glassPosition[i]);
encDirection = NO_ROTATION;
oledCalibrateDisplay(i);
}
}
}
delay(100);
}
srv.write(servoStartPosition);
delay(100);
srv.detach();
paramEEPROM(PARAM_ADDR, WRITE);
workMode = workModePrev;
oledMainDisplay();
}
// Наливаем рюмку
void fillGlass(int glassNum){
unsigned long timeOfFilling = 0; // таймер времени налива
bool fillingFlag = true; // флаг прекращения налива
int progrDotPrintTime = currentFillingTime / (SCREEN_WIDTH - 2); // интервал увеличения прогресс-бара
int progrDots = currentFillingTime / DOT_PRINTING_TIME; // количество сегментов прогресс-бара
int progrDotCounter = 0; // счётчик прогресс-бара
changeGlassState(glassNum, FILLING);
delay(250);
_POMP_ON; // включаем помпу
timeOfFilling = millis();
while (fillingFlag){
// Отображение прогресса налива
if ((millis() - timeOfFilling) > (progrDotPrintTime * progrDotCounter)){
showProgressBar(map(progrDotCounter, 0, progrDots, 0, SCREEN_WIDTH - 2));
progrDotCounter++;
}
// Контроль времени налива
if (millis() - timeOfFilling > currentFillingTime){
fillingFlag = false;
}
// Контроль убранной рюмки
checkGlasses();
if (stateGlass[glassNum] == NOGLASS){
fillingFlag = false;
}
}
_POMP_OFF; // выключаем помпу
if (stateGlass[glassNum] == FILLING){
changeGlassState(glassNum, FULL);
}
oledMainDisplay();
}
// Основной цикл налива напитков (для ручного режима)
void pourDrink(){
for (int i = 0; i < TOTAL_GLASSES; i++){
if (stateGlass[i] == EMPTY){
goServo(glassPosition[i]);
fillGlass(i);
}
}
goServo(servoStartPosition);
}
// проверка наличия рюмки, возвращает true, если установлена хотя бы одна рюмка
bool checkGlasses(){
bool result = false;
for (int i = 0; i < TOTAL_GLASSES; i++){
sensorGlass[i] = digitalRead(glassPin[i]);
if (sensorGlass[i] == LOW){
if (stateGlassPrev[i] == NOGLASS){
changeGlassState(i, EMPTY);
result = true;
}
} else {
changeGlassState(i, NOGLASS);
}
}
return result;
}
// Изменяем режим работы
void changeMode(){
workMode = (workMode == MANUAL) ? AUTO : MANUAL;
}
// Возврат к базовым настройкам
void resetToFactory(){
MODE workModePrev = workMode;
workMode = RESET;
oledMainDisplay(NO_VOLUME);
oled.setCursor(40, 50);
oled.println("Wait...");
oled.display();
paramEEPROM(PARAM_ADDR, WRITE, INIT);
paramEEPROM(PARAM_ADDR, READ);
delay(2000);
oledMainDisplay(NO_VOLUME);
oled.setCursor(35, 50);
oled.println("Done!!!");
oled.display();
delay(1000);
workMode = workModePrev;
oledMainDisplay();
}
// Настройки запуска устройства
void setup() {
Serial.begin(9600);
// Проверка, если первый запуск - записываем стандратные значений в память
if (EEPROM.read(INIT_ADDR) != INIT_KEY){
EEPROM.write(INIT_ADDR, INIT_KEY);
paramEEPROM(PARAM_ADDR, WRITE, INIT);
}
// Считываем параметры из памяти
paramEEPROM(PARAM_ADDR, READ);
// Настройка пинов
for (int i = 0; i < TOTAL_GLASSES; i++){
pinMode(glassPin[i], INPUT_PULLUP);
}
pinMode(START_BUTTON_PIN, INPUT_PULLUP);
pinMode(POMP_PIN, OUTPUT);
pinMode(SERVO_PIN, OUTPUT);
pinMode(LEDS_PIN, OUTPUT);
pinMode(ENC_CLK_PIN, INPUT);
pinMode(ENC_DT_PIN, INPUT);
pinMode(ENC_BUTTON_PIN, INPUT_PULLUP);
// Режим работы
workMode = MANUAL;
// Инициализация дисплея
oled.begin(SSD1306_SWITCHCAPVCC, 0x3C);
// delay(1000);
oled.setFont(&FreeSans9pt7b);
oled.setTextColor(WHITE);
#ifdef HELLO_SCREEN
helloScreen();
#endif
oledMainDisplay();
// Установка сервопривода в стартовую позиция
goServo(servoStartPosition);
#ifndef MX1508
// Отключение реле помпы
_POMP_OFF;
#endif
// Инициализация индикации
leds.begin();
// Статусы рюмок
for (int i = 0; i < TOTAL_GLASSES; i++){
stateGlass[i] = NOGLASS;
stateGlassPrev[i] = NOGLASS;
}
}
// Основной цикл работы
void loop() {
// Если есть рюмка - включим подсветку
checkGlasses();
// Проверка нажатия кнопки "Старт"
startButtonState = digitalRead(START_BUTTON_PIN);
// Определяем продолжительность нажатия кнопки "Старт"...
if (startButtonState == LOW){
startButtonTimer = millis();
startButtonFlag = true;
while (startButtonFlag){
if (digitalRead(START_BUTTON_PIN) == HIGH){
startButtonFlag = false;
startButtonTimer = millis() - startButtonTimer;
}
}
if (startButtonTimer > MODE_TIME){ // ...если больше MODE_TIME - меняем режим работы
changeMode();
oledMainDisplay();
} else if (startButtonTimer < RELEASE_TIME) { // ...если меньше RELEASE_TIME и в ручном режиме -наливаем, иниаче ничего не делаем
if (workMode == MANUAL){
pourDrink();
}
}
}
// Автоматический режим - ожидание установки рюмки
if (workMode == AUTO){
isGlass = checkGlasses();
if (isGlass){
pourDrink();
isGlass = false;
}
}
// Проверка нажатия кнопки энкодера
encoderButtonState = digitalRead(ENC_BUTTON_PIN);
// Определяем продолжительность нажатия кнопки "Старт"
if (encoderButtonState == LOW){
encoderButtonTimer = millis();
encoderButtonFlag = true;
while (encoderButtonFlag){
encoderButtonState = digitalRead(ENC_BUTTON_PIN);
if (encoderButtonState == HIGH){
encoderButtonTimer = millis() - encoderButtonTimer;
encoderButtonFlag = false;
}
}
if (encoderButtonTimer > RESET_TIME){ // если больше RESET_TIME - сбрасываем настройки в стандартные
resetToFactory();
} else if (encoderButtonTimer > CALIBRATE_TIME){ // если больше CALIBRATE_TIME - идёв в режим калибровки
calibrate();
} else if (encoderButtonTimer > SETTING_TIME){ // если больше SETTING_TIME - идём в настройки
settings();
}
}
}
/*
**************************************************************************
TODO LIST
????? 1. Режим "русской рулетки":
- наливает случайную рюмку + визуальные эффекты
3. Режим "прокачки":
- Стартовый - при включении устройства:
При наличии в определённой позиции рюмки - осуществить прокачку,
до нажатия кнопки
- "смена бутылки" - в любой момент времени при определённом нажатии кнопки
(в режиме настроек?)
**************************************************************************
*/