Jeff Sawyer

EARNING Trust and unlocking HUMAN potential


Bar BEE Tender: From Cocktail Napkin Idea to Smart Device

Every great project starts with a simple idea. For me, it was about blending classic mixology with interactive technology. I wanted to create more than just a drink dispenser; I wanted to create an experience. The result was Bar BEE Tender, a smart robotic bartender that crafts the perfect Old Fashioned, but only after you prove your worth in a classic game of “Simon.”

The concept was simple: the better you play, the more you get. As the evening progresses and more drinks are poured, the game’s memory sequence gets longer, adding a fun, challenging twist to a social gathering. It was an idea born from a desire to make technology fun, engaging, and a true centerpiece for connection.

The Concept: A Gamified Mixology Experience

Bar BEE Tender is an automated cocktail machine designed to pour a perfect Old Fashioned. The user interacts with a sleek touchscreen interface to start a game of “Simon.” Upon successfully completing the sequence, the machine precisely dispenses the ingredients—whiskey, bitters, and sugar—directly into your glass. With each drink poured, the challenge intensifies as the sequence length increases.

Core Features:

  • Gamified Dispensing: A “Simon” memory game that increases in difficulty.
  • Precision Mixology: Utilizes precise pumps and measurements for a consistently perfect Old Fashioned every time.
  • Smart Connectivity: Wi-Fi enabled for IoT integration, including voice commands via Amazon Alexa.
  • Mobile Management: A companion Android app for controlling the device, tracking usage, and managing settings.

The Technology Stack: Bringing Bar BEE Tender to Life

This project was a deep dive into full-stack product development. I personally designed, built, and coded every component, from the physical hardware to the cloud-based services.

Hardware: The heart of Bar BEE Tender is a custom-designed system built around a ESP32 microcontroller that controls a series of peristaltic pumps for accurate liquid dispensing. THe ESP32 was selected for the balance between cost and functions, including embedded WiFI, Bluetooth and its multithreading capabilities. The user interface is a responsive touchscreen display, all housed in a prototype chassis designed for both functionality and aesthetic appeal.

Hardware Block Diagram of Prototype

Firmware & Software: The firmware was written to manage the hardware operations—reading touchscreen input, running the game logic, and controlling the motors and pumps with millisecond precision. The system’s software layer, mainly in the AWS cloud included:

  • Wi-Fi Connectivity: Allowing the device to connect to a local network.
  • Amazon Alexa Integration: I developed a custom Alexa Skill enabling users to start the machine with voice commands like, “Alexa, ask Bar BEE Tender to make me a drink.”
  • Android Application: A prototype mobile app was created to remotely control the device, monitor ingredient levels, and customize drink recipes.

Firmware: The Brains of the Operation

The heart of the Bar BEE Tender’s intelligence lies in its firmware. Written in Arduino (C++), this code is the bridge between the physical hardware and the user experience. Running directly on the device’s microcontroller, the firmware is responsible for orchestrating every action, from the flashing lights of the “Simon” game to the precise activation of the pumps that pour the drink.

My approach was to build a robust, state-driven program that could reliably manage the machine’s different modes: waiting for a user (Idle), running the game (Gameplay), dispensing the cocktail (Pouring), cleaning and storage (Clean), and handling any potential issues (Error). This ensures the device operates smoothly and predictably.

Key Firmware Responsibilities:

  • Hardware Abstraction: Writing low-level drivers to control the touchscreen, addressable LEDs, audio buzzer, and the high-precision peristaltic pumps.
  • Game Logic: Implementing the core “Simon” game, including generating random sequences, capturing user input from the touchscreen, and validating the player’s memory. The difficulty (sequence length) was programmed to increase incrementally with each drink served.
  • Precision Pouring: Calibrating and controlling the pump motors with exact timing to dispense the precise volume of each ingredient—the key to a perfect Old Fashioned, every time.
  • State Management: A finite state machine (FSM) ensures the device is always in a known state, preventing conflicts like trying to pour a drink while a game is in progress.
  • Communication with AWS IOT: Syncing of local state and remote state using the AWS IOT framework.

Below is the full firmware.

/*
Copyright (C) 2023 Jeffrey Sawyer - All Rights Reserved
Project:      Bar BEE Tender Firmware
Created by:   Jeff Sawyer on 6/30/23
*/
#include "secrets.h"
#include <WiFiClientSecure.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include "WiFi.h"
const byte dwinrxPin = 16;  //rx2
const byte dwintxPin = 17;  //tx2
HardwareSerial dwin(1);
#include <Arduino.h>
#include <Wire.h>
#include <PCF8575.h>  //https://github.com/xreef/PCF8575_library
#include <EEPROM.h>
#define EEPROM_SIZE 1028
#define GPIO1_Addr 0x20
PCF8575 pcf8575_1(GPIO1_Addr);
#define STATE_INIT 0
#define STATE_OLDFASHIONED 1
#define STATE_BOURBONSHOT 2
#define STATE_VODKASHOT 3
#define STATE_SETUP 4
#define STATE_MAX 4
// the main switch
#define rtsw 5
// define the pins that will be used for the pumps
#define pump1_p P0
#define pump1_n P1
#define pump2_p P2
#define pump2_n P3
#define pump3_p P4
#define pump3_n P5
#define pump4_p P6
#define pump4_n P7
#define debug_pin 16
#define DRINKS_COUNTER_ADDRESS 0
TaskHandle_t Task1;
boolean TurnDetected;
boolean up;
boolean insleep = 0;
boolean triggersleep = 0;
boolean entersleep = 0;
boolean exitsleep = 0;
boolean aborttask = 0;
bool use_lcd = 0;
bool SS_enabled = 1;
// Assign pins for buttons, leds and buzzer for simon
const int buttonPins[] = { P9, P11, P13, P15 };
const int ledPins[] = { P8, P10, P12, P14 };
const int buzzer = 26;  // TODO this needs to be assigned to the correct pin
// These are the tones used for the buzzer using Hertz (Hz).
const int tones[] = { 1900, 1600, 1300, 1000, 3200 };
const int clean_steps = 5;
const int clean_time = 10;
const int soak_time = 50;
const int purge_time = 10;
const int prime_time = 1;
int wifi_timeout = 10;
boolean wifi_found = 0;
int wifi_networkid = 1;
char* wifi_hostname = "BarBeeTender";
char* wifi_ssid0 = "cackandballs";
char* wifi_password0 = "*********";
char* wifi_ssid1 = "Anteater";
char* wifi_password1 = "*********";
char* wifi_ssid2 = "pabsthouse";
char* wifi_password2 = ""*********";
char* wifi_ssid = wifi_ssid0;
char* wifi_password = wifi_password0;
// SS veriables
int buttonState[] = { 0, 0, 0, 0 };      //  Current state of button.
int lastButtonState[] = { 0, 0, 0, 0 };  // Previous state of button.
int buttonCounter[] = { 0, 0, 0, 0 };    // This array holds 4 values - 1 (pressed) / 0 (not pressed).
int gameOn = 0;                          // A new game or level starts when gameOn is 0.
int wait = 0;                            // This is used to tell the game to wait until the player inputs a pattern.
const int n_levels = 100;                // Length of series per level and number of levels until game is won.
int currentLevel = 1;                    // Current game level and length of sequence to make it to the next level.
int dlay = 500;                          // This is the amount of time to wait for the next button press (0.5 seconds).
int ledTime = 500;                       // Delay time of each LED flash when the correct button is pressed (0.5s).
int pinAndTone = 0;                      // Variable used to determine which LED to turn on and its buzzer tone.
int correct = 0;                         // This value must become 1 to go to the next level.
int speedFactor = 5;                     // This is the speed of the game. It increases every time a level is beaten.
int ledDelay = 200;                      // Delay before next LED lights up (0.2s). Decreases when level is completed.
int SS_level = 0;
int SS_unlocked = 0;
char temp_status_message[50];
int n_array[n_levels];  // n_array will store the randomized game pattern.
int u_array[n_levels];  // u_array will store the pattern input by the player.
int connectedfl = 0;
int trigger_pour = 0;
int trigger_pour_source = 0;
String drinktype = "";
int pouring = 0;
int cleaning = 0;
int purging = 0;
int priming = 0;
char AWS_PUBLISH_TOPIC[] = "$aws/things/" THINGNAME "/shadow/update";
char AWS_GET_TOPIC[] = "$aws/things/" THINGNAME "/shadow/get";
char* AWS_SUBSCRIBE_TOPIC[5] = {
"$aws/things/" THINGNAME "/shadow/get/accepted",
"$aws/things/" THINGNAME "/shadow/get/rejected",
"$aws/things/" THINGNAME "/shadow/update/accepted",
"$aws/things/" THINGNAME "/shadow/update/rejected",
"$aws/things/" THINGNAME "/shadow/update/delta"
};
char publishPayload[MQTT_MAX_PACKET_SIZE];
// interrupt handler for the turn detection of the rotary input device
void isr1() {
if (pouring || cleaning || purging || priming) {  // if (pouring || cleaning) {
aborttask = 1;
} else {
//log_d("button pressed STATE_INIT insleep=%d, entersleep=%d, exitsleep=%d", insleep, entersleep, exitsleep);
if (insleep == 0) {
//log_d("Enter sleep");
triggersleep = 1;
entersleep = 1;
} else {
//log_d("Exit sleep");
triggersleep = 0;
exitsleep = 1;
}
}
}
// return the number of drinks that have been made which is stored in the eeprom
byte get_lifetime_drinks() {
byte lifetime_drinks;
EEPROM.get(DRINKS_COUNTER_ADDRESS, lifetime_drinks);
set_touch_numdrinks(lifetime_drinks);
return lifetime_drinks;
}
// reset the number of drinks eeprom value
void reset_lifetime_drinks() {
EEPROM.write(DRINKS_COUNTER_ADDRESS, 0);
EEPROM.commit();
set_touch_numdrinks(0);
}
// increment the number ofr drinks counter and update the eeprom
void increment_drinks() {
byte lifetime_drinks;
EEPROM.get(DRINKS_COUNTER_ADDRESS, lifetime_drinks);
lifetime_drinks = lifetime_drinks + 1;
EEPROM.write(DRINKS_COUNTER_ADDRESS, lifetime_drinks);
EEPROM.commit();
set_touch_numdrinks(lifetime_drinks);
}
WiFiClientSecure net = WiFiClientSecure();
PubSubClient PSclient(net);
void getState() {
if (PSclient.publish(AWS_GET_TOPIC, "{}")) {
log_d("Getting Something from AWS_GET_TOPIC");
PSclient.loop();
} else {
log_d("Not Getting Something from AWS_GET_TOPIC");
};
}
void connectAWS() {
int wifi_trycounter = 0;
WiFi.mode(WIFI_STA);
WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE, INADDR_NONE);
WiFi.setHostname(wifi_hostname);  //define hostname
wifi_found = (WiFi.status() == WL_CONNECTED);
while (!wifi_found) {
wifi_trycounter = 0;
connectedfl = 0;
WiFi.begin(wifi_ssid, wifi_password);
log_d("Connecting to Wi-Fi %s %s ", wifi_ssid, wifi_password);
while (WiFi.status() != WL_CONNECTED && wifi_trycounter < wifi_timeout) {
delay(500);
log_d(".");
wifi_trycounter++;
}
if (WiFi.status() == WL_CONNECTED) {
wifi_found = 1;
log_d("Connected to Wi-Fi %s %s ", wifi_ssid, wifi_password);
set_top_icon(0, 1);
} else {
wifi_found = 0;
set_top_icon(0, 0);
wifi_networkid++;
if (wifi_networkid > 2) {
wifi_networkid = 0;
}
if (wifi_networkid == 0) {
wifi_ssid = wifi_ssid0;
wifi_password = wifi_password0;
} else if (wifi_networkid == 1) {
wifi_ssid = wifi_ssid1;
wifi_password = wifi_password1;
} else {
wifi_ssid = wifi_ssid2;
wifi_password = wifi_password2;
}
log_d("Failed t find Wi-Fi, switching to %s %s wifi_networkid=%d", wifi_ssid, wifi_password, wifi_networkid);
}
}
// Configure WiFiClientSecure to use the AWS IoT device credentials
net.setCACert(AWS_CERT_CA);
net.setCertificate(AWS_CERT_CRT);
net.setPrivateKey(AWS_CERT_PRIVATE);
// Connect to the MQTT broker on the AWS endpoint we defined earlier
PSclient.setBufferSize(1024);
PSclient.setServer(AWS_IOT_ENDPOINT, 8883);
// Create a message handler
PSclient.setCallback(callback);
log_d("Connecting to AWS IOT");
while (!PSclient.connect(THINGNAME)) {
log_d(".");
delay(100);
}
if (!PSclient.connected()) {
log_d("AWS IoT Timeout!");
return;
}
// Subscribe to a topic
for (int i = 0; i < 5; i++) {
if (PSclient.subscribe(AWS_SUBSCRIBE_TOPIC[i])) {
log_d("Connected to %s\n", AWS_SUBSCRIBE_TOPIC[i]);
} else {
log_d("Not connected to %s\n", AWS_SUBSCRIBE_TOPIC[i]);
}
}
log_d("AWS IoT Connected!");
}
void updateIOTState(String istate_type, int itrigger_pour) {
const char* temp_drinktype = drinktype.c_str();
sprintf(publishPayload, "{\"state\":{\"%s\":{\"connectedfl\":%d,\"trigger_pour\":%d,\"drinktype\":\"%s\",\"pouring\":%d,\"cleaning\":%d,\"purging\":%d,\"priming\":%d,\"num_drinks\":%d,\"localip\":\"%s\",\"subnetmask\":\"%s\",\"wifi\":\"%s\"}}}",
istate_type, connectedfl, itrigger_pour, temp_drinktype, pouring, cleaning, purging, priming, get_lifetime_drinks(), String(WiFi.localIP()), WiFi.subnetMask().toString(), wifi_ssid);
PSclient.publish(AWS_PUBLISH_TOPIC, publishPayload);
log_d("Publish [%s] %s  \ndrinktype:%s\n", AWS_PUBLISH_TOPIC, publishPayload, temp_drinktype);
}
void callback(char* topic, byte* payload, unsigned int length) {
//char buf[MQTT_MAX_PACKET_SIZE];
// Create a buffer for the incoming payload
char message[length + 1];
strncpy(message, (char*)payload, length);
message[length] = '\0';
printf("Message arrived [%s] %s\r\n", topic, message);
if ((strstr(topic, "/shadow/get/accepted") != NULL) || (strstr(topic, "/shadow/update/accepted") != NULL)) {
// Parse JSON
DynamicJsonDocument doc(1024);
DeserializationError error = deserializeJson(doc, message);
if (error) {
Serial.print("deserializeJson() failed: ");
Serial.println(error.f_str());
return;
}
// Extract values (modify according to your JSON structure)
//const char* state = doc["state"]["desired"]["yourPropertyName"];
//Serial.println(state);
JsonVariant exists_n = doc["state"]["desired"];
if (!exists_n.isNull()) {
// connectedfl = doc["state"]["desired"]["connectedfl"];
trigger_pour = doc["state"]["desired"]["trigger_pour"];
trigger_pour_source = 1;
const char* temp_drinktype = doc["state"]["desired"]["drinktype"];
//String temp_drinktype = sprintf("%s", obj["state"]["desired"]["drinktype"]);
// pouring = doc["state"]["desired"]["pouring"];
drinktype = String(temp_drinktype);
log_d("callback connectedfl=%d trigger_pour=%d drinktype=%s pouring=%d", connectedfl, trigger_pour, drinktype, pouring);
}
}
}
// this is the Simmon Says test routine, called when the user wants to make a drink and SS is enabled
void SS_test() {
// wait for unlock
int SS_unlocked = 0;
int i;
int fail_count = 0;
String locked_str = "";
if (SS_enabled && !trigger_pour_source) {
currentLevel = get_lifetime_drinks() + 1;
set_touch_page(52);
sprintf(temp_status_message, "Game Level: %d:  Good Luck!.", currentLevel);
set_status_text(temp_status_message);
// play the game
while (SS_unlocked == 0) {
if (wait == 0) {  // Triggers if no action is required from the player.
i = 0;
for (i = 0; i < currentLevel; i = i + 1) {  // This ‘for’ loop cycles the current game pattern.
ledDelay = ledTime / (1 + (speedFactor / n_levels) * (currentLevel - 1));
pinAndTone = n_array[i];
//pcf8575_1.digitalWrite(ledPins[pinAndTone], HIGH);
set_game_light(pinAndTone, 1);
playTone(tones[pinAndTone], ledDelay);
//pcf8575_1.digitalWrite(ledPins[pinAndTone], LOW);
set_game_light(pinAndTone, 0);
delay(100);
log_d("Show user: %i : %i", i, pinAndTone);
}
wait = 1;  // This puts the game on hold until the player enters a pattern.
}
set_touch_page(2);
i = 0;
int buttonChange = 0;
// buttonChange will be used to detect when a button is pressed.
int j = 0;  // ‘j’ is the current position in the pattern.
while (j < currentLevel) {
while (buttonChange == 0) {
unsigned char button_value = get_button_press();
for (i = 0; i < 4; i = i + 1) {  // This loop determines which button is pressed by the player.
// pcf8575_1.digitalWrite(buttonPins[i], HIGH);
buttonState[i] = bitRead(button_value, i);
buttonChange += buttonState[i];
if (buttonChange > 0) {
log_d("Buttons: buttonState[%i]=%i buttonChange=%i", i, buttonState[i], buttonChange);
}
}
delay(10);  // this delay has to be here to allow for the read,   should try and do the read in a single call
}
// This turns on the corresponding LED to the button pressed, and stores it in the array.
for (i = 0; i < 4; i = i + 1) {
if (buttonState[i] == 1) {
set_game_light(i, 1);
playTone(tones[i], ledTime);  // Calls function playTone, plays corresponding tone on the buzzer.
set_game_light(i, 0);
wait = 0;
u_array[j] = i;  // This stores the player’s input to be matched against the game pattern.
buttonState[i] = LOW;
buttonChange = 0;  // This resets the button.
}
}
if (u_array[j] == n_array[j]) {
correct = 1;
log_d("Correct: %i", n_array[j]);
j++;
}
// This section checks if the button pressed by the player matches the game pattern.
else {
correct = 0;
i = 4;
j = currentLevel;
wait = 0;
log_d("Incorrect: expected: %i received %i", n_array[j], u_array[j]);
}
}
// If the player makes a mistake, these variables will be reset so that the game starts over.
if (correct == 0) {
set_touch_page(52);
fail_count = fail_count + 1;
if (fail_count > 1) {
int randomNumber = random(4);
switch (randomNumber) {
case 0:
sprintf(temp_status_message, "Game Level: %d:  Loser you failed again.", currentLevel);
break;
case 1:
sprintf(temp_status_message, "Game Level: %d:  Are you kidding me?", currentLevel);
break;
case 2:
sprintf(temp_status_message, "Game Level: %d:  What a shit play", currentLevel);
break;
case 3:
sprintf(temp_status_message, "Game Level: %d:  Did your mother drop you?", currentLevel);
break;
}
} else {
sprintf(temp_status_message, "Game Level: %d:  You are a failure!", currentLevel);
}
set_status_text(temp_status_message);
delay(300);
i = 0;
gameOn = 0;
// this needs to be rewritten to write all the bits at the same time.
for (i = 0; i < 4; i = i + 1) {
set_game_light(i, 1);
}
playTone(tones[4], ledTime);
for (i = 0; i < 4; i = i + 1) {
set_game_light(i, 0);
}
delay(200);
for (i = 0; i < 4; i = i + 1) {
set_game_light(i, 1);
}
playTone(tones[4], ledTime);
// This "for loop" makes all of the LEDs blink twice and the buzzer beep twice when the player makes a mistake and loses the game.
for (i = 0; i < 4; i = i + 1) {
set_game_light(i, 0);
}
delay(500);
gameOn = 1;
SS_unlocked = 0;
log_d("failed, still locked");
for (i = 0; i < n_levels; i = i + 1) {
u_array[i] = 0;
}
}
if (correct == 1) {  // If the player gets the sequence right, the game goes up one level.
wait = 0;
set_touch_page(52);
SS_unlocked = 1;
Serial.println("unlocked");
}
}
}
sprintf(temp_status_message, "");
set_status_text(temp_status_message);
}
// control the motors, either forward or reverse
void control_motor(int motor_num, int motor_direction) {
if (motor_direction == 1) {  // forward
if (motor_num == 1) {
log_d("Motor 1 ON FORWARD");
pcf8575_1.digitalWrite(pump1_p, LOW);
pcf8575_1.digitalWrite(pump1_n, HIGH);
}
if (motor_num == 2) {
log_d("Motor 2 ON FORWARD");
pcf8575_1.digitalWrite(pump2_p, LOW);
pcf8575_1.digitalWrite(pump2_n, HIGH);
}
if (motor_num == 3) {
log_d("Motor 3 ON FORWARD");
pcf8575_1.digitalWrite(pump3_p, LOW);
pcf8575_1.digitalWrite(pump3_n, HIGH);
}
if (motor_num == 4) {
log_d("Motor 4 ON FORWARD");
pcf8575_1.digitalWrite(pump4_p, LOW);
pcf8575_1.digitalWrite(pump4_n, HIGH);
}
} else if (motor_direction == -1) {
if (motor_num == 1) {
log_d("Motor 1 ON REVERSE");
pcf8575_1.digitalWrite(pump1_p, HIGH);
pcf8575_1.digitalWrite(pump1_n, LOW);
}
if (motor_num == 2) {
log_d("Motor 2 ON REVERSE");
pcf8575_1.digitalWrite(pump2_p, HIGH);
pcf8575_1.digitalWrite(pump2_n, LOW);
}
if (motor_num == 3) {
log_d("Motor 3 ON REVERSE");
pcf8575_1.digitalWrite(pump3_p, HIGH);
pcf8575_1.digitalWrite(pump3_n, LOW);
}
if (motor_num == 4) {
log_d("Motor 4 ON REVERSE");
pcf8575_1.digitalWrite(pump4_p, HIGH);
pcf8575_1.digitalWrite(pump4_n, LOW);
}
} else {
if (motor_num == 1) {
log_d("Motor 1 OFF");
pcf8575_1.digitalWrite(pump1_p, LOW);
pcf8575_1.digitalWrite(pump1_n, LOW);
}
if (motor_num == 2) {
log_d("Motor 2 OFF");
pcf8575_1.digitalWrite(pump2_p, LOW);
pcf8575_1.digitalWrite(pump2_n, LOW);
}
if (motor_num == 3) {
log_d("Motor 3 OFF");
pcf8575_1.digitalWrite(pump3_p, LOW);
pcf8575_1.digitalWrite(pump3_n, LOW);
}
if (motor_num == 4) {
log_d("Motor 4 OFF");
pcf8575_1.digitalWrite(pump4_p, LOW);
pcf8575_1.digitalWrite(pump4_n, LOW);
}
}
}
void prime() {
log_i("Priming");
priming = 1;
set_touch_page(51);
updateIOTState("reported", 0);
sprintf(temp_status_message, "Prime");
set_status_text(temp_status_message);
control_motor(1, 1);
control_motor(2, 1);
control_motor(3, 1);
control_motor(4, 1);
for (int i = 0; i <= (prime_time * 10); i++) {
delay(100);
int percent_complete = int((float(i) / (prime_time * 10)) * 100);
set_touch_progress(percent_complete);
}
control_motor(1, 0);
control_motor(2, 0);
control_motor(3, 0);
control_motor(4, 0);
delay(100);
set_touch_progress(0);
priming = 0;
updateIOTState("reported", 0);
sprintf(temp_status_message, "");
set_status_text(temp_status_message);
set_touch_page(1);
}
void purge() {
log_i("Purging");
purging = 1;
set_touch_page(51);
updateIOTState("reported", 0);
sprintf(temp_status_message, "Purging");
set_status_text(temp_status_message);
control_motor(1, -1);
control_motor(2, -1);
control_motor(3, -1);
control_motor(4, -1);
for (int i = 0; i <= (purge_time * 10); i++) {
delay(100);
int percent_complete = int((float(i) / (purge_time * 10)) * 100);
set_touch_progress(percent_complete);
}
control_motor(1, 0);
control_motor(2, 0);
control_motor(3, 0);
control_motor(4, 0);
delay(100);
set_touch_progress(0);
purging = 0;
updateIOTState("reported", 0);
sprintf(temp_status_message, "");
set_status_text(temp_status_message);
set_touch_page(1);
}
void reset_counters() {
log_i("Resetting counters");
set_touch_page(51);
reset_lifetime_drinks();
delay(100);
set_touch_progress(0);
set_touch_page(1);
}
void clean() {
log_i("Cleaning");
delay(10);
cleaning = 1;
aborttask = 0;
updateIOTState("reported", 0);
int total_time = ((clean_time + soak_time) * clean_steps) + purge_time;
int completed_time = 0;
set_touch_page(51);
for (int s = 1; s <= clean_steps; s++) {
control_motor(1, 1);
control_motor(2, 1);
control_motor(3, 1);
control_motor(4, 1);
sprintf(temp_status_message, "Cleaning: Phase %d of %d Pump", s, clean_steps);
set_status_text(temp_status_message);
for (int i = 0; i <= clean_time; i++) {
int percent_complete = int((float(i + completed_time) / total_time) * 100);
set_touch_progress(percent_complete);
delay(1000);
if (aborttask) {
delay(500);
if (digitalRead(rtsw) == LOW) {
control_motor(1, 0);
control_motor(2, 0);
control_motor(3, 0);
control_motor(4, 0);
log_d("Abort Cleaning Pump");
set_touch_page(1);
delay(1000);
set_touch_progress(0);
cleaning = 0;
aborttask = 0;
updateIOTState("reported", 0);
return;
}
aborttask = 0;
}
}
control_motor(1, 0);
control_motor(2, 0);
control_motor(3, 0);
control_motor(4, 0);
sprintf(temp_status_message, "Cleaning: Phase %d of %d Soak", s, clean_steps);
set_status_text(temp_status_message);
for (int i = 0; i <= soak_time; i++) {
int percent_complete = int((float((i + clean_time) + completed_time) / total_time) * 100);
set_touch_progress(percent_complete);
delay(1000);
if (aborttask) {
delay(500);
if (digitalRead(rtsw) == LOW) {
control_motor(1, 0);
control_motor(2, 0);
control_motor(3, 0);
control_motor(4, 0);
log_d("Abort Cleaning Soak");
set_touch_page(1);
delay(1000);
set_touch_progress(0);
cleaning = 0;
aborttask = 0;
updateIOTState("reported", 0);
return;
}
aborttask = 0;
}
}
completed_time = completed_time + clean_time + soak_time;
}
control_motor(1, -1);
control_motor(2, -1);
control_motor(3, -1);
control_motor(4, -1);
sprintf(temp_status_message, "Cleaning: Purge");
set_status_text(temp_status_message);
for (int i = 0; i <= (purge_time); i++) {
delay(1000);
int percent_complete = int((float((i + purge_time) + completed_time) / total_time) * 100);
set_touch_progress(percent_complete);
}
control_motor(1, 0);
control_motor(2, 0);
control_motor(3, 0);
control_motor(4, 0);
delay(1000);
set_touch_progress(0);
set_touch_page(1);
cleaning = 0;
updateIOTState("reported", 0);
sprintf(temp_status_message, "");
set_status_text(temp_status_message);
}
// pour a drink
void pour(int m1_time, int m2_time, int m3_time, int m4_time) {
delay(10);
pouring = 1;
updateIOTState("reported", trigger_pour);
aborttask = 0;
if (m1_time > 0) {
control_motor(1, 1);
}
if (m2_time > 0) {
control_motor(2, 1);
}
if (m3_time > 0) {
control_motor(3, 1);
}
if (m4_time > 0) {
control_motor(4, 1);
}
int max_time = m1_time;
if (m2_time > max_time) {
max_time = m2_time;
}
if (m3_time > max_time) {
max_time = m3_time;
}
if (m4_time > max_time) {
max_time = m4_time;
}
increment_drinks();
log_d("max_time %i", max_time);
for (int i = 0; i <= max_time; i++) {
int percent_complete = int((float(i) / max_time) * 100);
int percent_complete_bar = int((float(i) / max_time) * 18);
String percent_complete_time_str = "";
percent_complete_time_str += String(i);
percent_complete_time_str += " of ";
percent_complete_time_str += String(max_time);
percent_complete_time_str += " seconds  ";
String percent_complete_str = "";
percent_complete_str += String(percent_complete);
percent_complete_str += "% complete";
String percent_complete_bar_str = "8";
// percent_complete_str += String(percent_complete);
// percent_complete_str += "% complete     ";
for (int i = 0; i < 17; i = i + 1) {
if (i < percent_complete_bar) {
percent_complete_bar_str += "=";
} else if (i == percent_complete_bar) {
percent_complete_bar_str += "D";
} else {
percent_complete_bar_str += " ";
}
}
percent_complete_bar_str += "O:";
log_d("Percent complete: %i", percent_complete);
set_touch_progress(percent_complete);
if (i == m1_time) {
control_motor(1, 0);
}
if (i == m2_time) {
control_motor(2, 0);
}
if (i == m3_time) {
control_motor(3, 0);
}
if (i == m4_time) {
control_motor(4, 0);
}
delay(1000);
if (aborttask) {
delay(500);
if (digitalRead(rtsw) == LOW) {
control_motor(1, 0);
control_motor(2, 0);
control_motor(3, 0);
control_motor(4, 0);
set_touch_page(10);
log_d("Abort Pouring");
delay(1000);
set_touch_progress(0);
pouring = 0;
aborttask = 0;
updateIOTState("reported", 0);
return;
}
aborttask = 0;
}
}
pouring = 0;
updateIOTState("reported", trigger_pour);
}
// play tones on the buzzer
void playTone(int tone, int duration) {
for (long i = 0; i < duration * 1000L; i += tone * 2) {
digitalWrite(buzzer, HIGH);  // Turns the buzzer on.
delayMicroseconds(tone);     // Creates the tone of the buzzer.
digitalWrite(buzzer, LOW);   // Turns the buzzer off.
delayMicroseconds(tone);
}
}
void SS_init_game_pattern() {
int i;
for (i = 0; i < n_levels; i = i + 1) {
// Saves the number in n_array to generate a random pattern.
n_array[i] = 0;
u_array[i] = 0;
n_array[i] = random(0, 4);
log_d("n_arry[%i]=%i", i, n_array[i]);
}
}
// setup
void setup() {
// set the debug serial port baud rate
Serial.begin(115200);
dwin.begin(115200, SERIAL_8N1, dwinrxPin, dwintxPin);
// display debug information to the debug serial port
log_d("Total heap: %d", ESP.getHeapSize());
log_d("Free heap: %d", ESP.getFreeHeap());
log_d("Total PSRAM: %d", ESP.getPsramSize());
log_d("Free PSRAM: %d", ESP.getFreePsram());
log_d("Bar-BEE setup started");
// make sure the eeprom is working
if (!EEPROM.begin(EEPROM_SIZE)) {
log_d("EEPROM failed to initialize");
while (true)
;
} else {
log_d("EEPROM initialized");
}
// setup the rotary input interface
pinMode(rtsw, INPUT_PULLUP);
// set the the motor pins
pcf8575_1.pinMode(pump1_p, OUTPUT);
pcf8575_1.pinMode(pump1_n, OUTPUT);
pcf8575_1.pinMode(pump2_p, OUTPUT);
pcf8575_1.pinMode(pump2_n, OUTPUT);
pcf8575_1.pinMode(pump3_p, OUTPUT);
pcf8575_1.pinMode(pump3_n, OUTPUT);
pcf8575_1.pinMode(pump4_p, OUTPUT);
pcf8575_1.pinMode(pump4_n, OUTPUT);
// this is to set the random see for the SS game
randomSeed(analogRead(0));  // Used to generate random numbers.
// Initialize inputs.
reset_touch_screen();
for (int i = 0; i < 4; i = i + 1) {
pcf8575_1.pinMode(ledPins[i], OUTPUT);
pcf8575_1.pinMode(buttonPins[i], INPUT);
}
//  connect to the external GPIO chip(s)
pcf8575_1.begin();
pinMode(buzzer, OUTPUT);
SS_init_game_pattern();
// go into a debug mode where the LCD screen is not working
// by holding the rotary inout switch down when the unit boots the
// lcd screen will be disabled and the led attached to the
// debug pin will be flashed
// wait for the wsitch to be relased before proceeding
if (digitalRead(rtsw) == LOW) {
use_lcd = 0;
log_d("Debug mode, turn off LCD");
digitalWrite(debug_pin, HIGH);
delay(500);
digitalWrite(debug_pin, LOW);
delay(500);
digitalWrite(debug_pin, HIGH);
delay(500);
digitalWrite(debug_pin, LOW);
delay(500);
while (digitalRead(rtsw) == LOW) {
delay(100);
}
}
// test is the external GPIO deviceis availible
// if not flash the debug pin and stall the
// program
int GPIO_found = 0;
while (!GPIO_found) {
Wire.beginTransmission(GPIO1_Addr);
log_d("Testing address for GPIO I2C chip");
int error = Wire.endTransmission();
if (error == 0) {
log_d("GPIO Found");
GPIO_found = 1;
} else {
log_d("GPIO Not found, retry");
GPIO_found = 0;
delay(1000);
}
}
// turn all the motors off
control_motor(1, 0);
control_motor(2, 0);
control_motor(3, 0);
control_motor(4, 0);
attachInterrupt(digitalPinToInterrupt(rtsw), isr1, FALLING);
TurnDetected = false;
// create a task that will be executed in the Task1code() function, with priority 1 and executed on core 0
xTaskCreatePinnedToCore(
Task1code, /* Task function. */
"Task1",   /* name of task. */
10000,     /* Stack size of task */
NULL,      /* parameter of the task */
1,         /* priority of the task */
&Task1,    /* Task handle to keep track of created task */
0);        /* pin task to core 0 */
delay(500);
log_d("Bar-BEE setup completed");
log_d("Night drinks made:  %i", get_lifetime_drinks());
SS_enabled = 1;
if (SS_enabled) {
set_top_icon(1, 2);
} else {
set_top_icon(1, 3);
}
delay(1000);
set_touch_page(10);
}
// Task1code: Stay connected to AWS
void Task1code(void* pvParameters) {
Serial.print("Network running on core ");
Serial.println(xPortGetCoreID());
for (;;) {
if (!PSclient.connected()) {
connectAWS();
// getState();
}
PSclient.loop();
if (connectedfl == 0) {
connectedfl = 1;
//TODO this should not be hard coded,  it should grab from the current vars,   this could crash the running drink like this
updateIOTState("reported", 0);
}
delay(100);
}
}
// the main loop of the program
void loop() {
// always clear this flag
aborttask = 0;
// if the isr triggered sleep by the user pressing a button or we are in sleep
if (triggersleep || insleep) {
if (entersleep) {
// do stuff to enter sleep
log_d("Entering Sleep");
set_touch_page(00);
delay(1000);
entersleep = 0;
set_touch_page(99);
insleep = 1;
} else if (exitsleep) {
// do stuff on exit sleep
log_d("Exiting Sleep");
set_touch_page(00);
delay(1000);
set_touch_page(10);
exitsleep = 0;
insleep = 0;
} else {
// do nothing
}
} else {
// if not entering of exiting of in sleep then check if we need to pour a drink
// check if we have anything to do from the screen
// make sure that we did not get a triffer from aws first
if (!trigger_pour) {
trigger_pour = check_touch_screen();
trigger_pour_source = 0;
}
if (trigger_pour) {
// we can get here from the screen trggering this of AWS IOT
trigger_pour = 0;
//drinktype.trim();
log_d("Pouring - triggered by remote ** %s ** ", drinktype);
String drinktype_display = "";
if (drinktype.equals("oldfashioned")) {
// old fashioned
drinktype_display = "Old Fashioned";
} else if (drinktype.equals("bourbonshot") || drinktype.equals("bourbon")) {
// bourbon shot
drinktype_display = "Bourbon Shot";
} else if (drinktype.equals("vodkashot") || drinktype.equals("vodka")) {
// vodka shot
drinktype_display = "Vodka Shot";
} else {
drinktype_display = "unknown drink";
}
if (drinktype.equals("oldfashioned")) {
// old fashioned
SS_test();
set_touch_page(200);
pour(16, 60, 0, 2);
} else if (drinktype.equals("bourbonshot") || drinktype.equals("bourbon")) {
// bourbon shot
SS_test();
set_touch_page(201);
pour(0, 60, 0, 0);
} else if (drinktype.equals("vodkashot") || drinktype.equals("vodka")) {
// vodka shot
SS_test();
set_touch_page(202);
pour(0, 0, 60, 0);
} else {
drinktype = "error";
delay(3000);
}
drinktype = "";
updateIOTState("reported", trigger_pour);
clear_touch_serial_buffer();
set_touch_page(10);
set_touch_progress(0);
}
}
}
int check_touch_screen() {
int return_code = 0;
unsigned char Buffer[9];
if (dwin.available()) {
for (int i = 0; i <= 8; i++)  //this loop will store whole frame in buffer array.
{
Buffer[i] = dwin.read();
}
if (Buffer[0] == 0X5A && Buffer[1] == 0XA5 && Buffer[3] == 0X83 && Buffer[4] == 0x20 && Buffer[5] == 0x00)
// if (Buffer[0] == 0X5A )
{
for (int i = 0; i <= 8; i++)  //this loop will store whole frame in buffer array.
{
Serial.print(" 0x");
Serial.print(Buffer[i], HEX);
}
Serial.println("");
switch (Buffer[7]) {
case 0x01:  //for pour
if (Buffer[8] == 0x00) {
Serial.println("Pour: Old Fashioned");
drinktype = "oldfashioned";
return_code = 1;
} else if (Buffer[8] == 0x01) {
Serial.println("Pour: Bourbon Shot");
drinktype = "bourbonshot";
return_code = 1;
} else if (Buffer[8] == 0x02) {
Serial.println("Pour: Vodka Shot");
drinktype = "vodkashot";
return_code = 1;
} else {
Serial.println("Pour: Unknown Drink");
}
break;
case 0x02:  //for setup
if (Buffer[8] == 0x00) {
Serial.println("Setup: Purge");
purge();
} else if (Buffer[8] == 0x01) {
Serial.println("Setup: Clean");
clean();
} else if (Buffer[8] == 0x02) {
Serial.println("Setup: Prime");
prime();
} else if (Buffer[8] == 0x03) {
Serial.println("Setup: Resert Counters");
reset_counters();
} else if (Buffer[8] == 0x04) {
Serial.println("Setup: Toggle Game");
SS_enabled = !SS_enabled;
if (SS_enabled) {
set_top_icon(1, 2);
} else {
set_top_icon(1, 3);
}
} else {
Serial.println("Setup: Unknown Requestd");
}
break;
default:
Serial.println("Unknown Command Code");
}
}
}
return return_code;
}
unsigned char get_button_press() {
unsigned char Buffer[9];
if (dwin.available()) {
for (int i = 0; i <= 8; i++)  //this loop will store whole frame in buffer array.
{
Buffer[i] = dwin.read();
}
if (Buffer[0] == 0X5A && Buffer[1] == 0XA5 && Buffer[3] == 0X83 && Buffer[4] == 0x22 && Buffer[5] == 0x10)
// if (Buffer[0] == 0X5A )
{
for (int i = 0; i <= 8; i++)  //this loop will store whole frame in buffer array.
{
Serial.print(" 0x");
Serial.print(Buffer[i], HEX);
}
Serial.println("");
unsigned char ClearButton[8] = { 0x5a, 0xa5, 0x05, 0x82, 0x22, 0x10, 0x00, 0x00 };
dwin.write(ClearButton, 8);
return Buffer[8];
}
}
return 0x00;
}
void clear_touch_serial_buffer() {
unsigned char Buffer[9];
if (dwin.available()) {
for (int i = 0; i <= 8; i++)  //this loop will store whole frame in buffer array.
{
Buffer[i] = dwin.read();
}
}
}
void set_touch_progress(unsigned char progress_percent) {
unsigned char ProgressBar[8] = { 0x5a, 0xa5, 0x05, 0x82, 0x20, 0x04, 0x00, 0x00 };
ProgressBar[6] = 0x00;
ProgressBar[7] = progress_percent;
dwin.write(ProgressBar, 8);
}
void set_touch_numdrinks(unsigned char num_drinks) {
unsigned char NumDrinks[8] = { 0x5a, 0xa5, 0x05, 0x82, 0x20, 0x08, 0x00, 0x00 };
NumDrinks[6] = 0x00;
NumDrinks[7] = num_drinks;
dwin.write(NumDrinks, 8);
}
void set_touch_page(unsigned char page) {
unsigned char Page[10] = { 0x5a, 0xa5, 0x07, 0x82, 0x00, 0x84, 0x5a, 0x01, 0x00, 0x00 };
Page[8] = 0x00;
Page[9] = page;
dwin.write(Page, 10);
}
void reset_touch_screen() {
unsigned char ResetCommand[10] = { 0x5a, 0xa5, 0x07, 0x82, 0x00, 0x04, 0x55, 0xaa, 0x5a, 0xa5 };
unsigned char ProgressBar[8] = { 0x5a, 0xa5, 0x05, 0x82, 0x20, 0x04, 0x00, 0x00 };
dwin.write(ResetCommand, 10);
dwin.write(ProgressBar, 8);
}
void set_top_icon(unsigned char id, unsigned char value) {
unsigned char Icon[8] = { 0x5a, 0xa5, 0x05, 0x82, 0x21, 0x00, 0x00, 0x00 };
Icon[5] = Icon[5] + (id * 4);
Icon[6] = 0x00;
Icon[7] = value;
dwin.write(Icon, 8);
}
void set_game_light(unsigned char id, unsigned char value) {
unsigned char Icon[8] = { 0x5a, 0xa5, 0x05, 0x82, 0x22, 0x00, 0x00, 0x00 };
Icon[5] = Icon[5] + (id * 4);
Icon[6] = 0x00;
Icon[7] = value;
dwin.write(Icon, 8);
}
void set_status_text(char text[]) {
//6  + 25
unsigned char Status[56] = { 0x5a, 0xa5, 53, 0x82, 0x40, 0x00 };
int hit_null = 0;
for (int i = 0; i < 50; i++) {
if (hit_null) {
Status[6 + i] = 0;
} else {
Status[6 + i] = text[i];
if (text[i] == 0) {
hit_null = 1;
}
}
}
//for (int i = 0; i < 31; i++) {
//  char  temp_string[3];
//  sprintf(temp_string, "%02x ", Status[i]);
//  Serial.print( temp_string );
// }
//log_d("");
dwin.write(Status, 56);
}

The Business Journey: From Prototype to Production-Ready

Bar BEE Tender was more than just a technical challenge; it was an exercise in entrepreneurship. I treated this project as a startup, navigating every stage of the business development lifecycle.

Intellectual Property: To protect the unique concept, I successfully filed for:

  • A Provisional Patent with the U.S. Patent and Trademark Office (USPTO), detailing the novel integration of a memory game with an automated dispensing system.
  • A Trademark for the “Bar BEE Tender” name and brand.

Go-to-Market Strategy: My goal was to launch Bar BEE Tender on Kickstarter. To prepare for this, I developed a comprehensive business plan and marketing collateral, outlining the target market, competitive landscape, and financial projections.

Manufacturing & Supply Chain: I initiated discussions with a manufacturing broker in China to source components and plan for mass production. This involved creating a Bill of Materials (BOM), estimating costs, and beginning the design for manufacturing (DFM) process for the first production unit. Since this was a at home applicate reliably was critical to ensure that the brand was not tarnished by defective product. In addition to the physical hardware I started to build a team to take my prototype and harden the cloud infrastructure and mobile app. I told the distributed team that we need it so simple that my grandmother can set it up and use it. I was working with a US based distributor to provide drop shipping services and product support.

The Vision for the Future 🚀

While the prototype focused on perfecting the core experience, there was a robust roadmap to evolve Bar BEE Tender from a smart cocktail machine into a fully interactive entertainment platform. The goal was to deepen user engagement through software updates and expanded connectivity.

An Arcade of Bar Games

The “Simon” game was just the beginning. The next step was to build a library of games that could be selected via the touchscreen or mobile app.

  • Cocktail Trivia: Imagine this: the screen presents a trivia question about spirits or cocktail history. Players would submit their answers through their smartphones. A correct answer would be rewarded with the machine pouring a drink.
  • Reaction Challenges: A fast-paced game where users have to tap colors or shapes on the screen as quickly as possible. Faster times would unlock a “perfect pour.”

Networked & Social Gameplay

The true centerpiece of the future vision was multi-device connectivity. By enabling two or more Bar BEE Tender machines on the same Wi-Fi network to communicate, we could have created truly social, competitive experiences.

  • Head-to-Head “Simon”: Two players could compete simultaneously on separate devices, racing to see who could complete the longest sequence first.
  • Team Trivia: A group could be split into two teams, with each Bar BEE Tender representing a team’s station. Correct answers from the team would contribute to earning the next round of drinks.

A Customizable Digital Mixologist

The most significant planned upgrade was a comprehensive recipe management system powered by the companion mobile app. This would have transformed the device from a single-drink dispenser into a versatile robotic bartender.

  • Recipe Creator & Browser: Users could use the app to browse a library of classic cocktail recipes or create and save their own custom concoctions by specifying the ingredients and their exact ratios.
  • “Upload to Bar BEE”: With a single tap, a user could send any recipe from their phone to the machine. Bar BEE Tender would then automatically adjust its pouring logic to craft the new drink perfectly.
  • Smart Inventory: The app would also track the volume of each ingredient, sending a notification when a bottle was running low and even suggesting cocktails that could be made with the remaining ingredients.

Project Conclusion: A Journey Paused

Due to a major life-changing event, I was unfortunately forced to put the Bar BEE Tender project on hold before its planned Kickstarter launch. While it was a difficult decision, the journey was an invaluable experience.

This project demonstrates my ability to take a complex idea from a simple concept to a functional hardware prototype, a polished software experience, and a viable business plan. It showcases my skills in hardware engineering, software development, IoT integration, mobile app creation, and the strategic planning required to bring a product to market.

Though Bar BEE Tender never made its public debut, it stands as a testament to my passion for innovation, my technical capabilities, and my drive to build something truly unique.