Цифровой бармен. Arduino проект для совершеннолетних начинающих электронщиков. Часть 1
У меня много друзей. Молодые парни, мужчины средних лет и конечно дамы всех возрастов. Наверное всех. Трудно определить возраст современной женщины. Да и не очень хочется.
Так вот. Детей моих друзей и знакомых в силу своих возможностей я увлекаю электроникой. Строим маленьких роботов, жучков светлячков всяких и даже световые мечи. У детей почти всегда всё получается и они, конечно, бегут хвастаться ожившей электроникой родителям. И так раз за разом. Но однажды одна из моих знакомых, глядя на очередной всплеск гордости у своей дочери, говорит — я тоже хочу вникнуть в эту электронику, и программирование, и может даже что-то спаять. Не вопрос. Давай покажу, как собрать робота на колесной платформе. Будет ездить по полоске на полу. Или игру сделаем, питона например, или просто поморгаем светодиодами, или … Перебрали множество примеров. Получается если для начинающих, то всё какое-то детское, а если не детское, то совсем уже не для начинающих. Что-то не так! Нужны уроки для НАЧИНАЮЩИХ, ВЗРОСЛЫХ электронщиков. Пусть это будет — цифровой бармен или машина для смешивания коктейлей.
Проект на сегодняшнюю дату не закончен. Если что нужно добавить пишите прямо в комментариях. На фото стенд, на котором можно рассмотреть конструкцию этого бармена.
А это схема в стиле Arduino проектов.
Теперь конструкция и логика работы. По схеме и фото.
В четыре бутылки опущены принимающие трубки четырех микронасосов. Эти микронасосы подключены к четырем ключам собранных на полевых транзисторах MOSFET. Ключи в свою очередь подключены к выводам 3,5,6,9 любой Ардуины (дальше если написано к выводу **, это значит вывод Ардуины). Это исполнительная часть. Логика работы исполнительной части задается пятью потенциометрами. По порядку: самым левым потенциометром, подключенным к выводу A4, выставляем объем бокала, в котором будем готовить коктейль. Над ним вы видите шкалу из 16-ти RGB светодиодов с так называемой пиксельной адресацией WS2812b. Когда мы крутим первый потенциометр на шкале последовательно зажигаются светодиоды, условно показывающие объем бокала. Эта шкала подключена к выводу 11.
Следующими четырьмя ползунковыми потенциометрами (подключенными к выводам A0, A1, A2, A3) устанавливаем ту пропорцию от общего объема напитка, который нам нужно налить из конкретной бутылки. Над ползунками маленькие шкалы из 8 светодиодов каждая (подключены последовательно к длинной). Двигая потенциометрами, выбираем пропорцию и маленькие шкалы, снизу вверх, закрашиваются каждая своим цветом. Одновременно левая длинная шкала, вернее её первоначально зажженная часть раскрашивается в эти же цвета пропорционально маленьким.
Сразу видим состав коктейля. Нажимаем кнопку (вывод 7), и насосы последовательно слева направо включаются, подгоняя напитки в бокал.
В скетче можно настроить:
1. Активные цвета всех шкал, их фоновую подсветку.
2. Скорость работы помп, путем широтно-импульсной модуляции. Поэтому насосы включают не реле, а транзисторные ключи. Это нужно если вы, например, будете использовать автомобильную помпу омывателя стекла. Она очень производительная.
3. Максимальный физический объем бокала. По умолчанию 750 мл.
4. Время подсоса жидкости до включения отсчета и индикации. Это для того чтобы заполнить опустевшую трубку во время паузы.
5. Паузу перед подачей из следующей бутылки.
6. Размер шкал в светодиодах. По умолчанию установлено 16 светодиодов в большой шкале и по восемь светодиодов в маленьких. Можно изменить, как хотите. Будет красиво.
Как-то так.
Это ролик с демонстрацией.
Предлагаю собрать эту конструкцию начинающим электронщикам. Залить в Ардуину скетч и проиграться настройками бармена. Когда глаза привыкнут к коду и вы будете ориентироваться в строчках можно будет добавлять новые функции, например кнопку Стоп или кнопку Лёд. Последняя может автоматически уменьшать общий объем напитка пропорционально объему кубика льда.
Я надеюсь, что подобные проекты увлекут взрослых начинающих электронщиков-программистов. И они потихоньку разобравшись в конструкции и коде вместе со своими детьми поднимут уважение к себе ещё на несколько уровней.
Хороших выходных!
А в заключении также как и в ролике, я порошу сделать замечания и предложения. Я учту это в продолжении и в реально действующем проекте.
Код (скетч) можно скачать здесь.
Или посмотреть:
Понадобится Adafruit_NeoPixel.h — библиотека для светодиодов и PinChangeInt.h — библиотека для прерываний.
#include
#include
#define START_PIN 7
#define LEDS_PIN 11
#define VOLUME_1_PIN A0
#define VOLUME_2_PIN A1
#define VOLUME_3_PIN A2
#define VOLUME_4_PIN A3
#define TOTAL_VOLUME_PIN A4
#define ACT_1_PIN 3
#define ACT_2_PIN 5
#define ACT_3_PIN 6
#define ACT_4_PIN 9
#define DRINKS_NUM 4
#define PIXEL_IN_STICK 8
#define PIXEL_IN_DRINK PIXEL_IN_STICK
#define PIXEL_IN_VOLUME (2 * PIXEL_IN_STICK)
#define PIXEL_NUM (DRINKS_NUM * PIXEL_IN_DRINK + PIXEL_IN_VOLUME)
#define VOLUME_START_PIXEL 0
#define DRINKS_START_PIXEL (VOLUME_START_PIXEL + PIXEL_IN_VOLUME)
#define DRINK_START_PIXEL(DRINK) (DRINKS_START_PIXEL + DRINK * PIXEL_IN_DRINK)
#define BACKGROUND_COLOUR ((uint32_t) 0x000001)
#define SHADOW_1_COLOUR ((uint32_t) 0x000100)
#define SHADOW_2_COLOUR ((uint32_t) 0x000100)
#define SHADOW_3_COLOUR ((uint32_t) 0x000100)
#define SHADOW_4_COLOUR ((uint32_t) 0x000100)
#define PROCESS_1_COLOUR ((uint32_t) 0xFF0000)
#define PROCESS_2_COLOUR ((uint32_t) 0x0100ff)
#define PROCESS_3_COLOUR ((uint32_t) 0x111100)
#define PROCESS_4_COLOUR ((uint32_t) 0xFF00FF)
#define VOLUME_PROCESS_COLOUR ((uint32_t) 0x888888)
#define DataThreshold ((uint16_t) (1024/PIXEL_IN_DRINK))
#define DataThresholdVol ((uint16_t) (1024/PIXEL_IN_VOLUME))
#define PROCESS (1 << 0)
#define mlToTimeCoef 10 //время на подачу 1 мл жидксти, мс
#define MIN_VOLUME 1 //минимальный учитываемый объем, мл
#define MAX_VOLUME ((uint32_t) 750) //мл
#define mlForLED ((float)((float)MAX_VOLUME / (float)PIXEL_IN_VOLUME))
#define PREPROCESS_DELAY ((uint32_t) 2000) //мс
#define PUMP_POWER ((uint16_t) 255) //от 0 (выключен) до 255 (максимум)
#define WaitShowDelay ((uint16_t) 300) //2 * WaitShowDelay - период моргания ожидающей жидкости
#define WaitCycle 3 //циклы ожидания время ожидания = WaitCycle * 2 * WaitShowDelay
#define EndDelay ((uint16_t) 2500) //пауза в конце
typedef struct
{
uint8_t VolPin;
uint8_t ActPin;
uint16_t Volume; //показания АЦП
uint32_t ProcColour;
uint32_t ShadColour;
float mlVol; //мл в зависимости от общего объема и доли напитка
uint8_t LEDsNum;
uint8_t LEDsPos;
} DrinkType;
Adafruit_NeoPixel LEDS = Adafruit_NeoPixel(PIXEL_NUM, LEDS_PIN, NEO_GRB + NEO_KHZ800);
uint8_t State = 0;
uint16_t TotalVolume = 0;
float TotalVolml = 0;
uint8_t TotVolActLEDs = 0;
bool ColourMix = false;
uint32_t NewColour = 0;
bool SystemChange = false;
DrinkType Drinks[DRINKS_NUM];
void setup()
{
digitalWrite(START_PIN, HIGH);
uint8_t VolPINs[DRINKS_NUM] = {VOLUME_1_PIN, VOLUME_2_PIN, VOLUME_3_PIN, VOLUME_4_PIN};
uint8_t ActPINs[DRINKS_NUM] = {ACT_1_PIN, ACT_2_PIN, ACT_3_PIN, ACT_4_PIN};
uint32_t ProcCOLOURs[DRINKS_NUM] = {PROCESS_1_COLOUR, PROCESS_2_COLOUR, PROCESS_3_COLOUR, PROCESS_4_COLOUR};
uint32_t ShadCOLOURs[DRINKS_NUM] = {SHADOW_1_COLOUR, SHADOW_2_COLOUR, SHADOW_3_COLOUR, SHADOW_4_COLOUR};
for(uint8_t i = 0; i < DRINKS_NUM; i++)
{
Drinks[i].VolPin = VolPINs[i];
Drinks[i].ActPin = ActPINs[i];
pinMode(Drinks[i].ActPin, OUTPUT);
digitalWrite(Drinks[i].ActPin, LOW);
Drinks[i].Volume = 0;
Drinks[i].ProcColour = ProcCOLOURs[i];
Drinks[i].ShadColour = ShadCOLOURs[i];
Drinks[i].LEDsPos = 0;
}
PCintPort::attachInterrupt(START_PIN, &START_ISR, FALLING);
LEDS.begin();
for(byte i=0; i < PIXEL_NUM; i++)
LEDS.setPixelColor(i, BACKGROUND_COLOUR);
LEDS.show();
}
//----------------------------------------
void loop()
{
if ((State & PROCESS) != PROCESS)
{
uint16_t Data;
for(uint8_t cup = 0; cup < DRINKS_NUM; cup++)
{
Data = analogRead(Drinks[cup].VolPin);
if (abs(Data - Drinks[cup].Volume) >= DataThreshold)
{
Drinks[cup].Volume = Data;
if (Drinks[cup].Volume > 975)
Drinks[cup].Volume = 1024;
uint8_t StartPixel = DRINK_START_PIXEL(cup);
for(byte i = StartPixel; i < (StartPixel + PIXEL_IN_DRINK); i++)
LEDS.setPixelColor(i, Drinks[cup].ShadColour);
for(byte i = StartPixel; i < (StartPixel + (Drinks[cup].Volume / DataThreshold)); i++)
LEDS.setPixelColor(i, Drinks[cup].ProcColour);
SystemChange = true;
}
}
Data = analogRead(TOTAL_VOLUME_PIN);
if (abs(Data - TotalVolume) >= DataThresholdVol)
{
TotalVolume = Data;
if (TotalVolume > 975)
TotalVolume = 1024;
TotVolActLEDs = TotalVolume / DataThresholdVol;
for(byte i = VOLUME_START_PIXEL; i < (VOLUME_START_PIXEL + PIXEL_IN_VOLUME); i++)
LEDS.setPixelColor(i, BACKGROUND_COLOUR);
SystemChange = true;
}
if (SystemChange)
{
TotalVolml = (float)((TotalVolume * MAX_VOLUME) / 1024);
uint16_t MinVol = 1025;
for(uint8_t cup = 0; cup < DRINKS_NUM; cup++)
{
if ((Drinks[cup].Volume < MinVol) && (Drinks[cup].Volume != 0))
MinVol = Drinks[cup].Volume;
}
float OnePartVol = 0;
for(uint8_t cup = 0; cup < DRINKS_NUM; cup++)
{
Drinks[cup].mlVol = (float)Drinks[cup].Volume / (float)MinVol;
OnePartVol += Drinks[cup].mlVol;
}
OnePartVol = TotalVolml / OnePartVol;
for(uint8_t cup = 0; cup < DRINKS_NUM; cup++)
{
Drinks[cup].mlVol *= OnePartVol;
Drinks[cup].LEDsNum = Drinks[cup].mlVol / mlForLED;
if ((Drinks[cup].mlVol > 0) && (Drinks[cup].LEDsNum < 1))
Drinks[cup].LEDsNum = 1;
}
uint8_t LEDsSum = 0;
for(uint8_t cup = 0; cup < DRINKS_NUM; cup++)
LEDsSum += Drinks[cup].LEDsNum;
if ((LEDsSum > 0) && (LEDsSum <= TotVolActLEDs))
{
uint8_t LedsNumMAX = Drinks[0].LEDsNum;
uint8_t LedsNumMAXPos = 0;
for(uint8_t cup = 1; cup < DRINKS_NUM; cup++)
{
if (Drinks[cup].LEDsNum > LedsNumMAX)
{
LedsNumMAX = Drinks[cup].LEDsNum;
LedsNumMAXPos = cup;
}
}
Drinks[LedsNumMAXPos].LEDsNum += TotVolActLEDs - LEDsSum;
Drinks[0].LEDsPos = VOLUME_START_PIXEL;
for(uint8_t cup = 1; cup < DRINKS_NUM; cup++)
Drinks[cup].LEDsPos = Drinks[cup - 1].LEDsPos + Drinks[cup - 1].LEDsNum;
ColourMix = false;
}
else if (LEDsSum > TotVolActLEDs)
{
for(uint8_t cup = 0; cup < DRINKS_NUM; cup++)
{
Drinks[cup].LEDsNum = TotVolActLEDs;
Drinks[cup].LEDsPos = VOLUME_START_PIXEL;
}
ColourMix = true;
NewColour = 0;
for(uint8_t cup = 0; cup < DRINKS_NUM; cup++)
NewColour |= Drinks[cup].ProcColour;
}
bool EmptyCup = true;
for(uint8_t cup = 0; cup < DRINKS_NUM; cup++)
{
if (Drinks[cup].LEDsNum != 0)
{
EmptyCup = false;
break;
}
}
if (EmptyCup)
{
for(byte i = VOLUME_START_PIXEL; i < (VOLUME_START_PIXEL + TotVolActLEDs); i++)
LEDS.setPixelColor(i, VOLUME_PROCESS_COLOUR);
}
else
{
if (ColourMix)
{
for(byte i = VOLUME_START_PIXEL; i < (VOLUME_START_PIXEL + TotVolActLEDs); i++)
LEDS.setPixelColor(i, NewColour);
}
else
{
for(uint8_t cup = 0; cup < DRINKS_NUM; cup++)
{
if (Drinks[cup].LEDsNum != 0)
{
for(byte i = Drinks[cup].LEDsPos; i < (Drinks[cup].LEDsPos + Drinks[cup].LEDsNum); i++)
LEDS.setPixelColor(i, Drinks[cup].ProcColour);
}
}
}
}
SystemChange = false;
}
LEDS.show();
}
else
{
uint8_t LEDSPos[DRINKS_NUM];
uint8_t LEDSNum[DRINKS_NUM];
for(uint8_t cup = 0; cup < DRINKS_NUM; cup++)
{
LEDSPos[cup] = Drinks[cup].LEDsPos;
LEDSNum[cup] = Drinks[cup].LEDsNum;
}
for(uint8_t cup = 0; cup < DRINKS_NUM; cup++)
{
if (Drinks[cup].LEDsNum != 0)
{
float Volume = Drinks[cup].mlVol;
uint16_t VolCnt = 0;
uint16_t mlPerLEDCoef = Volume / LEDSNum[cup];
analogWrite(Drinks[cup].ActPin, PUMP_POWER);//вкл
delay(PREPROCESS_DELAY);
while (Volume >= MIN_VOLUME)
{
delay(mlToTimeCoef * MIN_VOLUME);
Volume -= MIN_VOLUME;
VolCnt += MIN_VOLUME;
{
if (VolCnt >= mlPerLEDCoef)
{
if (ColourMix)
{
}
else
{
if (LEDSNum[cup] != 0)
{
for(byte i = VOLUME_START_PIXEL; i < (VOLUME_START_PIXEL + PIXEL_IN_VOLUME); i++)
LEDS.setPixelColor(i, BACKGROUND_COLOUR);
if (LEDSNum[cup] > 0)
LEDSNum[cup]--;
for(uint8_t i = 0; i < DRINKS_NUM; i++)
{
if (LEDSPos[i] > 0)
LEDSPos[i]--;
}
for(uint8_t i = 0; i < DRINKS_NUM; i++)
{
if (Drinks[i].LEDsNum != 0)
{
for(byte j = LEDSPos[i]; j < (LEDSPos[i] + LEDSNum[i]); j++)
LEDS.setPixelColor(j, Drinks[i].ProcColour);
}
}
}
}
LEDS.show();
VolCnt = 0;
}
}
}
analogWrite(Drinks[cup].ActPin, 0);//выкл
if (cup < (DRINKS_NUM - 1))
{
uint8_t StickNum = cup + 1;
uint8_t StartPixel = DRINK_START_PIXEL(StickNum);
for (uint8_t BlinkTime = 0; BlinkTime < WaitCycle; BlinkTime++)
{
for(byte i = StartPixel; i < (StartPixel + PIXEL_IN_DRINK); i++)
LEDS.setPixelColor(i, Drinks[cup+1].ShadColour);
LEDS.show();
delay(WaitShowDelay);
for(byte i = StartPixel; i < (StartPixel + (Drinks[cup+1].Volume / DataThreshold)); i++)
LEDS.setPixelColor(i, Drinks[cup+1].ProcColour);
LEDS.show();
delay(WaitShowDelay);
}
}
}
}
delay(EndDelay);
if (ColourMix)
{
for(byte i = VOLUME_START_PIXEL; i < (VOLUME_START_PIXEL + TotVolActLEDs); i++)
LEDS.setPixelColor(i, NewColour);
}
else
{
for(uint8_t cup = 0; cup < DRINKS_NUM; cup++)
{
if (Drinks[cup].LEDsNum != 0)
{
for(byte i = Drinks[cup].LEDsPos; i < (Drinks[cup].LEDsPos + Drinks[cup].LEDsNum); i++)
LEDS.setPixelColor(i, Drinks[cup].ProcColour);
}
}
}
State &= ~PROCESS;
}
}
//----------------------------------------
void START_ISR()
{
State |= PROCESS;
}