This Alexa-automated pill dispenser dispenses pills from a "pill box" when you prompt Alexa to dispense your pills for a certain day.
The idea for my project came to fruition on one random morning in the kitchen. As I was eating my breakfast, I watched my dad struggle to fish his daily pills out of the tiny weekly pill box he stored in the medicine cabinet. With a long-standing family history of heart problems, both my mom and dad have to take multiple pills every single day. I wanted to be able to relieve that small part of their already busy day to day lives by making a pill dispenser that could be activated via Amazon Alexa.
MaterialsRequired materials:
- Particle Photon
- Jumper wires
- 28BYJ-48 Stepper Motor
- Potentiometer
- Amazon Alexa (I have an Echo show, but any kind works).
Before beginning the design process, I had to connect my Particle Photon to my Alexa such that she can call a cloud function on the IoT microcontroller. I did this by using Amazon's Alexa Developer Console, which allowed me to create a custom Alexa skill that communicated with my Photon directly through the Particle Cloud.
Step 1:Configuring the Alexa Skill
In the Developer Console, I created a skill called "Motor Controller." This skill would allow me to give commands to Alexa that would rotate the Stepper motor.
When creating the skill, the configurations I used were other for the type of experience (as it's a unique skill), custom for the model (which means you start from scratch), and Alexa-hosted [node.js] for the hosting service (since I planned to use JavaScript for the program).
Each skill allows me to create intents, which are basically the commands that you physically say to Alexa.
For example:
- "rotate motor 90"
- "give me Monday pills"
Each phrase activates the specific intents that I created, which then runs code in the cloud based on your prompt.
These are the intents that I created for my Motor Controller skill.
This intent rotates the motor with a specified number of degrees.
This intent uses the RotateMotorIntent, but maps it such that when you prompt it with a certain day, it will rotate a certain amount of degrees.
Step 2:Coding theSkill
Every skill needs a "brain" so that it knows what to do when you speak. In short, the AWS Lambda (a server-less compute service developed by Amazon Web Services) is this brain, where the code written in Node.js (the JavaScript file that I selected as my hosting service) determines what to do with the prompts given from the intents.
How the Lambda works:
- Listens for the voice commands that you give it
I programmed an intent handler for each intent that I created (RotateMotor and DispenseDay)
Example:
- Processes the request from the intent
When Alexa recognizes one of the intents, such as "rotate motor 90, " she inputs the value 90 for {StepCount}
- Contacts the Particle Cloud and calls the cloud function rotate on my Python with the parameter being determined by the previous steps
After determining what to do with the command I gave Alexa, it sends an HTTPS POST request to the Particle API to call my cloud function named "rotate" with the determined argument (ex: 90, 45, 360).
TheCode:
#include "Particle.h"
#include <Stepper.h>
SYSTEM_MODE(AUTOMATIC);
SerialLogHandler logHandler(LOG_LEVEL_INFO);
const int stepsPerRevolution = 2048; // 28BYJ-48
#define IN1 D2
#define IN2 D3
#define IN3 D4
#define IN4 D5
#define POT A5
Stepper myStepper(stepsPerRevolution, IN1, IN3, IN2, IN4);
volatile bool busy = false;
volatile long pendingSteps = 0; // queued by cloud
int lastAngle = 0; // our current target angle [0..360)
unsigned long potFreezeUntil = 0; // ignore pot until this time (ms) after cloud cmd
const unsigned long potFreezeMs = 1200;
// simple low-pass filter for pot
float filteredPot = 0.0f; // 0..4095
const float potAlpha = 0.15f; // smoothing (0=noise, 1=no smoothing)
int rotateHandler(String arg);
void runSteps(long steps);
static inline int wrap360(int a);
void setup() {
myStepper.setSpeed(17); // keep modest to avoid stalls
pinMode(POT, INPUT);
// Start filteredPot near real value to avoid a big jump on boot
int raw = analogRead(POT);
filteredPot = (float)raw;
lastAngle = map(raw, 0, 4095, 0, 360);
Particle.function("rotate", rotateHandler);
Serial.begin(9600);
delay(500);
Log.info("Ready — function 'rotate' (degrees) is online.");
}
void loop() {
// 1) Service any queued cloud movement first
if (!busy && pendingSteps != 0) {
long s = pendingSteps;
pendingSteps = 0;
runSteps(s);
// keep lastAngle in sync with actual movement, so the pot won't "snap back"
long movedDeg = (long)((s * 360L) / stepsPerRevolution);
lastAngle = wrap360(lastAngle + (int)movedDeg);
// freeze the pot briefly after cloud moves
potFreezeUntil = millis() + potFreezeMs;
}
// 2) Pot control (only when not busy and not frozen)
if (!busy && (long)(millis() - potFreezeUntil) >= 0) {
int raw = analogRead(POT); // 0..4095
int newAngle = map((int)raw, 0, 4095, 0, 360);
int rot = newAngle - lastAngle;
// choose shortest path
if (rot > 180) rot -= 360;
if (rot < -180) rot += 360;
if (abs(rot) > 4) { // deadband so it doesn't chatter
long steps = (long)stepsPerRevolution * rot / 360;
runSteps(steps);
lastAngle = wrap360(newAngle);
}
}
delay(50);
}
// helper functions
void runSteps(long steps) {
busy = true;
if (steps != 0) {
myStepper.step((int)steps);
}
busy = false;
}
static inline int wrap360(int a) {
a %= 360;
if (a < 0) a += 360;
return a;
}
// cloud function
// arg is a number that represents the degrees to be rotated
int rotateHandler(String arg) {
arg.trim();
long degrees = arg.toInt(); // user enters DEGREES
long steps = (long)stepsPerRevolution * degrees / 360;
pendingSteps = steps;
return (int)degrees;
}Step1:ConnectingToParticleCloud
Particle.function("rotate", rotateHandler);This line of code registers a cloud function on the Particle cloud called rotate(). Any time either I or Alexa call the rotate() function on the cloud, the rotateHandler() function in my code runs with the inputted value.
Step2:SettinguptheStepperMotor
In my project, I used a 28BYJ-48 Stepper Motor connected with a ULN2003 driver board.
#define IN1 D2
#define IN2 D3
#define IN3 D4
#define IN4 D5
Stepper myStepper(stepsPerRevolution, IN1, IN3, IN2, IN4);With these lines of code, I created a Stepper object that represented my motor and connected the input pins on the driver board(INS1, 2, 3, 4) with the pins on my Photon (D2, 3, 4, 5). [Important! Make sure you have the Stepper library imported into your Particle Web IDE in order to make sure that you have access to the Stepper class.]
Then in the setup() of the code, I set the speed of the motor to 17 revolutions per minute (RPM). You can choose any value around ~15-20, but I chose 17 since it best prevented the motor from stalling
myStepper.setSpeed(17);Step3:HandlingAlexaCommands
The main function of my code is the function that Alexa is able to call:
int rotateHandler(String arg) {
arg.trim();
long degrees = arg.toInt(); // user enters DEGREES
long steps = (long)stepsPerRevolution * degrees / 360;
pendingSteps = steps;
return (int)degrees;
}Basically, the flow of code when ran through Alexa goes,
Prompt Alexa (ex: rotate motor 90) --> code in the Lambda runs with the RotateMotorIntentHandler --> Lambda connects to the Particle Cloud and calls rotateHandler() function on my Photon with the prompted value --> value is converted into steps (the unit for the motor's rotations) and stored into pendingSteps --> pendingSteps is later used in the loop function of the code.
Step4:HelperFunctions
In my code, I created two helper functions to help with the logic of the motor.
void runSteps(long steps) {
busy = true;
if (steps != 0) {
myStepper.step((int)steps);
}
busy = false;
}
static inline int wrap360(int a) {
a %= 360;
if (a < 0) a += 360;
return a;
}The runSteps function is what actually physically rotates the motor. The busy boolean ensures that the motor doesn't execute a new command while it's still rotating.
The wrap360 function keeps the angle "wrapped" between 0 and 360 degrees since the motor rotates within those boundaaries. I did this to make sure if a degree greater than 360 was inputted, it would loop back around.
Step5:LoopandPotentiometerControl
Since I was using two methods to rotate the motor (Potentiometer as a "homing device" so that I could manually move it for troubleshooting purposes, and via Alexa commands), I wanted to make sure that they didn't interfere with each other.
To handle cloud commands to actually rotate the motor, I wrote this:
// 1) Service any queued cloud movement first
if (!busy && pendingSteps != 0) {
long s = pendingSteps;
pendingSteps = 0;
runSteps(s);
// keep lastAngle in sync with actual movement, so the pot won't "snap back"
// steps -> degrees
long movedDeg = (long)((s * 360L) / stepsPerRevolution);
lastAngle = wrap360(lastAngle + (int)movedDeg);
// freeze the pot briefly after cloud moves
potFreezeUntil = millis() + potFreezeMs;
} Here, I'm now using the pendingSteps variable that was stored through the rotateHandler() function that Alexa called. The busy boolean ensures that the motor isn't mid-rotation when it's prompted to start a new one. The runSteps() function is called (a helper function that I created that actually does the rotating) with the amount of pending steps.
Then, I create potFreezeUntil in order to stop the potentiometer from rotating any steps it inputs until the motor is done rotating.
Now, to handle the potentiometer, I wrote this:
// 2) Pot control (only when not busy and not frozen)
if (!busy && (long)(millis() - potFreezeUntil) >= 0) {
int raw = analogRead(POT); // 0..4095
int newAngle = map((int)raw, 0, 4095, 0, 360);
int rot = newAngle - lastAngle;
// choose shortest path
if (rot > 180) rot -= 360;
if (rot < -180) rot += 360;
if (abs(rot) > 4) { // deadband so it doesn't chatter
long steps = (long)stepsPerRevolution * rot / 360;
runSteps(steps);
lastAngle = wrap360(newAngle);
}
}The code waits for the freeze period to end, preventing the potentiometer from interfering with Alexa. It then reads the values of the potentiometer and maps it to a value between 0 and 360, representing degrees of rotation. It then compares that value to the last known angle in order to make sure the motor doesn't wrap around instead of going the shortest rotation path--I ignored tiny changes in the potentiometer (4 deg) in order to prevent the motor from jittering from misread pot values. Then, the angle difference is converted into steps and calls the runSteps function.
Creating the DispenserMy proposal for the project was this:
- The motor would be connected to a disk with a sector cut out, which would act as a hole for the pills to drop into
- The disk would sit underneath a carousel that acts as the pillbox, with dividers that create 8 45 degree sections for the pills to sit in.
- When the disk rotates, the pills rotate with the disk but are blocked by the dividers in the carousel (the carousel isn't attached to the disk) and fall.
- There is a funnel underneath the disk that catches the pills and dispenses them onto a tray.
Here was the prototype that I designed for the project.
CreatingthePillBoxCarousel:
The first structural component that I decided to make was the carousel that would act as a "pill box." To do this, I folded a long sheet of cardboard into a cylindrical shape that would act as the outer shell of the carousel by hot gluing the ends together. I then cut out 8 small identical rectangles of cardboard that would act as the dividers, and hot glued them to the sides of the carousel.
CreatingTheDisk:
I then made the disk that would sit underneath the carousel by tracing out a circle with the same diameter as it onto a thin sheet of cardboard. I drew it into 8 45 degree sections that would line up with the dividers in the carousel, and cut one of them out in order to create the hole for the pills to drop into.
CreatingtheDispenseTray:
Next, I cut out the tray for the pills to dispense into. I did this by first creating a "slide" for the pills to fall down under the funnel. This slide would then lead to a wider piece of flat cardboard for the pills to slide on to. The diagonal borders connected to the contraption are to prevent the pills from flying out to the sides when falling down the slides, ensuring that they fall into the designated tray.
PiecingitallTogether:
I chose to place my project underneath the cabinet containing all of our medicine so that it would be easy to reload the pill box. I did this by attaching a tall, wide sheet of key board to the wall that would serve as the wall for the contraption, since my wall wasn't level. First, I placed the dispense tray on the bottom. Then, I placed the motor with the desk onto a platform a few inches above the funnel so that the pills would fall into the funnel. I created a platform for the motor the sit on because I wasn't able to connect the motor to the wall due to the large disk that sat on it's shaft.
Finally, I glued the carousel just above the disk-motor contraption (~3 mm), ensuring that it wasn't touching the disk to make sure that the disk would actually rotate.
and that's it! Here is a final demonstration of the Pill Dispenser working















Comments