For this final project, I was debating among a few project ideas that seemed both interesting and relevant to me. I wrote an email to my instructor for this course, Dr. Richard Huntrods, with a basic summary of what I wanted to do for each of the possible projects. There was however one project that stood out to me as being particularly interesting and exciting to work on. A smart mask! After receiving approval for the project I was eager to get started on building my own version of a smart mask prototype that Razer announce around a year ago, called “Project Hazel”.
*This blog post got a bit long… if you want you can skip to the end where I have a video that shows my final creation and attempts to walk you through some of the features that I was able to build in. I also briefly discuss some plans that I have for future versions of this mask.
Table of Contents
Introduction
We are still impacted by the Covid-19 pandemic, and with the rise now of the omicron variant there is no clear end to our response to the virus. Masks have been and will continue to be an important part of how we protect each other and ourselves. As such, masks have become a normal part of our daily lives, and project hazel was interesting to me for a number of reasons. First of all, it looks cool. It looks like a piece of future technology that one might find in a sci-fi, or cyberpunk universe. Secondly, it tries to alleviate a few problems that have arisen as part of the current masks we wear. There is a clear window so that facial expressions can still be read by others. When we briefly thought the pandemic was under control and we relaxed the mask regulations this past summer, one of the things that struck me most deeply was the feeling and joy of being able to smile at others. Facial expressions are such an integral part of our biological wiring that having a mask which allows for non-verbal communication seems like a wonderful thing. Next, the mask actually made it easier to brreathe then most of the masks we use now. It accomplishes this through the use of fans hidden behind the filters which allow for easier and more comfortable airflow. Finally, the prototype mask made use of a simple microphone and speaker system to get around the problem of muffled speech while wearing a mask.
So, I have a few objectives from the Razer prototype that I would like to accomplish with the mask I am attempting to build:
- I want a sensor so that the mask can detect if it is being worn, or if it should be in a standby mode. There will also need to be a hardwired power switch for longer storage.
- I want to build the mask so that it can use high-quality off-the-shelf cartridge filters with a fan behind them which should make it easy to breathe with.
- I want the final mask to have a clear window so that facial expressions can easily be seen.
- I want to integrate lights into the mask that can change their pattern/lighting based upon if someone is talking or just breathing.
- I would like to integrate a microphone into the mask, this mic will be used to adjust the lighting of the mask.
- If I have time, I would also like to integrate speakers into the mask so that the user can be heard clearly by others.
- I want to check into the possibility of adding some voice modulation to the system, if I can get the speakers working (it could be fun to sound like you are talking from a space suit!)
There are a lot of steps and parts to this project, but I am going to begin by getting the electrical and programming challenges taken care of in discrete chunks. Once I have some functional components I will then attempt to bring them all together and get them working in concert. After the initial prototype is working on my breadboard and desk I will spend some time designing, printing and refining a way to effectively attach everything to my face.
Prototyping
So in the next couple of sections I will begin implementations of the various steps that I outlined above. I am writing this up as a bit of a walkthrough or how-to, so that I will have a good reference if I want to build additional Cyber Masks or if I want to refine the approach in the future.
*Just a note, my initial prototype will be created with the RedBoard Qwiic from the Sparkfun Inventor’s Kit. Later I plan to do the final implementation with an Arduino Nano, so as to save space and weight. If you need help connecting to the Redboard, there is a guide available here: https://learn.sparkfun.com/tutorials/redboard-qwiic-hookup-guide?_ga=2.253475465.1033689899.1639719047-924208707.1633384134
Wear Sensor
If you have had the chance to use a modern VR headset, you may have noticed that the displays turn off if you take the headset off. I want to replicate this behaviour and have my smart mask go into a stand-by mode if it is not being worn. That would mean the lights turn off, the fans power down and the mic is muted. My first thought on how I might do this is to use an IR led, and an IR receiver. My thinking is that I can have the IR LED illuminated (the user won’t see this light) and the receiver calibrated so that if there is a reflective surface (say skin) close to the receiver/emitter combo, then the controller will know that the headset is being worn and will enter the active state.
For this part of the project I ordered the following pack of IR emitters and receivers from amazon: https://www.amazon.ca/gp/product/B07FFQ9B9H/ref=ppx_yo_dt_b_asin_title_o00_s00?ie=UTF8&psc=1
Now that I have my IR Emitter, I can see in the specs that the LED is rated for:
- Forward Voltage: 0.9-1.3V
- Current: 30 mA
Now before I can use the LED I have to determine the appropriate resistor to use. This can be done with the following equation:
[latexpage] \[ \quicklatex{color=”#00ff00″ size=25} \boxed{R = \frac{V-V_{LED}}{I}} \]Now I know the voltage of the LED, it is 0.9-1.3 V and the current through the LED is 0.03 A. I am planning to power the project with 4 x AA or 4 x AAA batteries, which should mean that the controller should be able to to provide 6V of output. Before I move on with my implementation, I am going to confirm this. I will also be able to confirm the voltage available if the controller is powered by USB, which I believe should be 5V. For this test, I will use the following simple code to activate power output from pin 13. (This will also be the same code that I use to test the IR emitter after I determine the correct resistor to use.)
#define IR 13 // The pin the IR-LED is connected to
void setup() {
pinMode(IR, OUTPUT); // Declare the IR-LED as an output
}
void loop() {
digitalWrite(IR, HIGH); // Turn the IR-LED on
}
A note about power
When powered via USB, this results in pin 13 outputting 4.95 V, and when powered from a battery pack we get 3.91V. Which was lower than I expected, So I swapped out the batteries for fresh batteries and got 3.97 V. Again much lower than I expected. Now I mainly use rechargeable batteries, and these were brand new, so they are probably not fully charged. I’ve thrown some on the charger and will check again after they are fully charged. (After charging my batteries, I am getting a reading of 4.61V)
This leads to an interesting segue into batteries and voltage. After coming across this unexpected voltage I got looking into battery voltage. First of all looking at my rechargeable batteries, they are Ni-MH rechargeables, with expected output of 1.2V not 1.5V as I assumed; and secondly, it is expected for batteries to experience a voltage drop as they are used. This drop in voltage is actually how devices can tell if their battery is getting low. With some quick google-fu I found a handbook from Panasonic regarding Ni-MH batteries with some interesting information: https://www.mouser.com/pdfdocs/PanasonicBatteries_NI-MH_Handbook.pdf. Batteries have a discharge curve, but Ni-MH batteries tend to have a very flat curve for most of their usable capacity.
According to their handbook, most Ni-MH batteries will output around 1.2 V throughout their usable capacity, and once that voltage drops down to a certain level the device using that battery is supposed to shut down so as to protect the battery. For four batteries arranged serially this is at 4.0 V total output. Based on this, it does look like the batteries I was attempting to use were “dead”. This leads me to two conclusions. First of all I should design and plan my circuits to operate with 4.8V of power; and secondly, I should look into whether the Arduino has a way to measure input voltage. If I can track input voltage, I can add a special lighting mode to warn the user that the batteries are low and will need to be recharged soon.
At this point, it seems that my control board can expect to output between 4.0 V at low battery, and 4.8 V at full capacity. I am not planning to power the device with USB, so I am going to design my circuits for 4.8 V. This may mean that performance drops a bit (LED’s dim, fans slow, speaker gets quieter) as the the battery capacity discharges. I am not currently sure if these changes will be noticeable to the user though.
Back to the Wear Sensor
So back to our resistance formula. We know V = 4.8V, V_LED = 1.3V, and I = 0.03A. This gives us a desired resistance of 116.67 ohms. I could wire a 100 ohm and two 10 ohm resistors in sequence, but that is going to result in extra work and will take up more space inside my mask. It seems that it is possible to choose the next resistance value up and things should work out. So I will use a 220 ohm resistor, which I happen to have some spares of.
After wiring everything up, I cannot see any indication that things are working…excellent! If I examine the system with a camera however I can see that there is definitely light coming from the IR-LED. We are now ready to begin working on the receiver portion of our wear detection sensor.
Now originally, my intention was to use the IR receiver from that kit I ordered earlier to read the brightness level of the IR emitter. I figured that if there was a surface near the receiver and the emitter, so much light would bounce back that the receiver would essentially become saturated and know that someone was wearing the headset. Unfortunately, I seem to have run into a problem with this plan, as the IR receiver is not intended for measuring IR light levels, and instead interprets a modulated signal as a hexadecimal code. So, I won’t be able to use the IR-Receiver as a wear sensor, but on the bright side I should be able to set it up to allow the user to control the mask on the go! I’ll come back to this idea after I have the RGB LEDS Set up on the mask.
I am now going to test using the photoresistor from the Sparkfun kit as my wear sensor. It is my hope that the sensor will respond to IR light effectively and let me get a reading that I can use. I will be referring back to Circuit 1C in the SIK Guidebook for this implementation: https://github.com/sparkfun/SIK_Guide/raw/master/English/SIK%20v4.1%20Book%202019%20WEB.pdf
#define IR 13 // The pin the IR-LED is connected to
#define PHOTORESISTOR A0// The pin the Photoresistor is connected to
int IRLevel = 0; // this variable will hold the valu returned by the photoresistor
int IRThreshold = 900; // if the reading is above this the user is wearing the mask and it should turn on.
void setup() {
Serial.begin(9600); // Start a serial connection with the computer
pinMode(IR, OUTPUT); // Declare the IR-LED as an output
}
void loop() {
digitalWrite(IR, HIGH); // Turn the IR-LED on
IRLevel = analogRead(PHOTORESISTOR);
Serial.println(IRLevel);
if(IRLevel > IRThreshold)
{
// mask should be active
//Serial.println("Active Mode");
} else {
// mask should be in standby
//Serial.println("Standby Mode");
}
delay(100); // short delay between loops
}
Unfortunately, this combination does not seem to be viable. The photoresistor is picking up too much light from the ambient environment, and the IR LED doesn’t seem to be bright enough to meaningfully trigger the photoresistor. I think a visible light filter (would block visible light while allowing IR through) might work to fix the problem, but I can’t seem to get a hold of one. This leaves me with the following options:
- I could attempt to use a visible LED and continue with my reflected light threshold idea, this is likely to be prone to false active states from lighting in the environment.
- I could use a contact switch, so that when the user is wearing the mask the switch would be triggered and the mask would enter the active state.
- I could use a regular switch so that the user could turn on or off the mask.
- I could use a an IR receiver as a power button for the mask, this would allow the user to turn the mask on with a remote control. I would probably need to include a timer to enter standby after the mask has been on for a set period of time.
- I could attempt to obtain an IR collision sensor which is rated to detect the presence of things within 2-30cm of the sensor.
Visible LED Wear Sensor
So let’s check out option 1. I will simply replace the IR LED in my current prototype and code with a visible light LED. For this to be viable, I need to have the bounced light from an object ~1-2 cm away be considerably brighter than lights in the environment.
Bright Ambient | Dim Ambient | Reflected Light – Bright | Reflected Light – Dim | |
Red | 880 | 615 | 787 | 763 |
Green | 873 | 565 | 583 | 515 |
Blue | 879 | 615 | 740 | 703 |
Yellow | 871 | 590 | 645 | 605 |
While these are not rigorously tested and verified results, they do give me a quick guideline as to how my wear sensor might perform. Red light was the best performer, and it might be okay in most cases, but there is definitely a chance that ambient light might create a false positive which would result in the mask turning on when it should not. I liked the idea of an IR emitter and sensor, as I thought it would avoid issues with false positives from the environment, but without a proper receiver and visible light filter, I really don’t think this is a robust enough solution to be viable.
Contact Switch Wear Sensor
Implementing a contact switch in an Arduino system is fairly simple. I have some spare microswitches from my 3D printer that I could use and the switch can be polled to return whether it is either open or closed. The biggest challenge of using a microswitch would be the placement of the switch inside the mask. I would want to avoid actually having the switch in contact with the user and so would want to either place it between the mask shell and the face seal, or between the the shell and the strap so that when the user was wearing the mask the switch would be triggered. In both cases, this is going to require some additional design work and I worry that mechanical interactions like this might be a point where things could fail. I think the switch itself would likely be okay, but I would be depending on two parts squishing together and then springing apart for the switch to be reliable. This seems like it would be doable, but could be a failure point and it could take a lot of tweaking to get the feel and pressure right. Again this does not seem like a great solution, but it would likely be better then the visible light sensor.
#define WEARSWITCH 12 // the pin the contact switch is connected to
void setup() {
Serial.begin(9600); // Start a serial connection with the computer
}
void loop() {
if(digitalRead(WEARSWITCH) == LOW)
{
// mask should be active
Serial.println("Active Mode");
} else {
// mask should be in standby
Serial.println("Standby Mode");
}
delay(100); // short delay between loops
}
Toggle Switch
The implementation from a code perspective is virtually the same for this as it was for the contact switch. It is again very simple to implement. There are however two issues with this approach. First of all, it requires manual input and deliberate manipulation from the user. Secondly, it seems almost barbaric to expect a user of a sci-fi mask to need to use a toggle switch for normal operations. While it would be functional, aesthetically and from a user experience point of view I don’t think its a good option. A toggle switch for a smart device feels very… outdated. So I’m going to pretend this option doesn’t exist unless I actually need it for some reason. (I do plan to use a toggle as an override for the battery, so that the mask can be left in storage without draining the batteries though, but I will be hiding the switch as best as I can.)
IR Receiver
Now we return to the IR receiver from my initial attempt, but this time instead of trying to read a light level from the IR receiver, I will be attempting to receive input from a remote control. The remote I will be using came with a set of RGB string lights I bought for decorating a long time ago and looks like this:
There are 44 buttons on this remote, so I should have access to many more buttons than I imagine I will need for this project. At the moment, the only button I am interested in receiving is the power button.
To receive input from the remote, I used a library from Ken Shirriff, which allows me to receive the input from a button press as a six digit hexadecimal code. There is a simple intro to the library on Ken’s blog at: http://www.righto.com/2009/08/multi-protocol-infrared-remote-library.html. Using his IR receiver library I was able to learn that the code produced by my remote when the power button is pushed is FF02FD. Using this I can toggle a boolean and let the mask know if it should be in the active or standby mode.
#include <IRremote.hpp>
#define IRReceiver 11 // the pin the IR Receiver is connected to
IRrecv irrecv(IRReceiver);
decode_results results;
bool bActive = false;
void setup() {
Serial.begin(9600); // Start a serial connection with the computer
irrecv.enableIRIn(); // start the receiver
}
void loop() {
if(irrecv.decode(&results))
{
Serial.println(results.value, HEX);
//if the power button was pushed
if(results.value == 0xFF02FD)
{
//toggle the bActive boolean
if(bActive){
bActive = false;
Serial.println("Setting bActive to false.");
} else {
bActive = true;
Serial.println("Setting bActive to true.");
}
}
irrecv.resume();
}
delay(100); // short delay between loops
}
This is working, and I will likely use this in the final iteration of the project, as well as many of the other buttons, but I am still not happy with this as a potential replacement for the wear sensor. It is a good thing that I was able to figure out how to set up and read input from a remote control; but, like the toggle switch above, I do not like the idea that the user must remember to turn on or off the mask for it to function. The mask should be “smart” enough to automatically turn itself on or off as the user puts on or takes off the mask. This remote power button should be more for demo or override purposes.
So I plan to use this remote power button, but I am not satisfied with it as a replacement for the wear sensor I am trying to implement.
IR Collision Detector
After my disappointment and frustration with my initial attempt to create a wear sensor, I found that there is an arduino sensor called an IR collision sensor. According to the specs, it should be able to detect an object within 2-30cm and has a potentiometer to manually adjust the activation distance. I ordered a five pack of these from Amazon: https://www.amazon.ca/gp/product/B07D3PHQT8/ref=ppx_yo_dt_b_asin_title_o00_s00?ie=UTF8&psc=1.
After implementing this sensor in my project, I can happily say that it accomplishes my goal for a wear sensor. I am pretty sure that it operates upon the same principle that I was originally wanting to create too, where there is an IR LED and then a sensor that will trigger when the IR receiver reaches a certain saturation. I suspect the potentiometer on the board adjusts the resistance to the led and in doing so adjusts the brightness of the IR LED which determines how far away the object can be before the reflected LED light is bright enough to trigger the IR detector.
#define IRCollision 10 // the pin the IR Collision Sensor is connected to
bool bActive = false;
int CollisionCurrent = HIGH; //High indicates no collision, Low indicates Collision
int CollisionPrevious = HIGH;
void setup() {
Serial.begin(9600); // Start a serial connection with the computer
pinMode(IRCollision, INPUT); // Declare the IRCollision sensor as an input device
}
void loop() {
// ***************************************************************
// Check wear sensor for state change
CollisionCurrent = digitalRead(IRCollision);
// Check wear sensor for activation
if(CollisionCurrent == LOW && CollisionPrevious == HIGH){
bActive = true;
Serial.println("IR Collision - Setting bActive to true.");
}
// check wear sensor for standby
else if(CollisionCurrent == HIGH && CollisionPrevious == LOW){
bActive = false;
Serial.println("IR Collision - Setting bActive to false.");
}
CollisionPrevious = CollisionCurrent;
delay(100); // short delay between loops
}
I was curious if this was how the sensor worked, so I set up a camera above the sensor and started playing with the pot to adjust the activation distance. It does not appear that the led brightness changed with my manipulation of the control. There is a small chip on the board, so now I wonder if the pot adjusts a threshold value that the chip is comparing against, instead of adjusting the brightness of the LED. The sensor outputs a high voltage signal if there is no collision, and a LOW voltage signal if the sensor is triggered.
I am quite happy with this sensor, and I expect that it will work well as my wear sensor. I plan to build the mask so that the chip is hidden behind some shrouding with only the led and receiver exposed. I also plan to use the IR Remote control sensor, and between the wear sensor and the IR remote control I think I will have a good amount of control of the device.
Fan Control
The primary function of a mask is filtration. It is to protect both the user and others form airborne contaminants. So now that I have some options for how to turn on the mask and control it, I want to begin working on the air flow. For this I bought two little 4010 5V blower fans from amazon: https://www.amazon.ca/gp/product/B07PM2QVZ8/ref=ppx_yo_dt_b_asin_title_o00_s01?ie=UTF8&psc=1.
Excited for this project I ordered these fans and went on my merry way thinking that they would be easy to use with the arduino as they were 5v and the arduino works with 5v. Unfortunately, I forgot to consider the current. These fans use 150 mA each at 5V, while the arduino can only handle a maximum of 40 mA per gpio pin. Never fear however, for there is a solution! Transistors. I have a couple of PN2222 transistors leftover from a previous project. These little guys are able to handle up to 40V at a peak current of 1A, which should be more than enough for my needs. I was able to get the fans working with the transistor, and I kept the IR collision sensor in this step, so that I could turn the fans on by triggering the IR collision detector. Now that the fans are working the next logical step is to get the lights glowing.
#define IRCollision 10 // the pin the IR Collision Sensor is connected to
#define Fan 9 // The pin that will control the left fan
bool bActive = false;
bool bActivePrevious = false;
int CollisionCurrent = HIGH; //High indicates no collision, Low indicates Collision
int CollisionPrevious = HIGH;
int FanSpeed = 0;
void setup() {
Serial.begin(9600); // Start a serial connection with the computer
pinMode(IRCollision, INPUT); // Declare the IRCollision sensor as an input device
pinMode(Fan, OUTPUT); // declare the left fan pin for output control
analogWrite(Fan, FanSpeed); // initialize the fan speed to 0
}
void loop() {
// ***************************************************************
// Check wear sensor for state change
CollisionCurrent = digitalRead(IRCollision);
// Check wear sensor for activation
if(CollisionCurrent == LOW && CollisionPrevious == HIGH){
bActive = true;
Serial.println("IR Collision - Setting bActive to true.");
}
// check wear sensor for standby
else if(CollisionCurrent == HIGH && CollisionPrevious == LOW){
bActive = false;
Serial.println("IR Collision - Setting bActive to false.");
}
CollisionPrevious = CollisionCurrent;
// ***************************************************************
// Active Functions
if(bActive){
//check if the device just activated
if(!bActivePrevious){
Serial.println("Activating the mask!");
FanSpeed = 255;
analogWrite(Fan, FanSpeed);
bActivePrevious = true; // set the previous activity to true so that we can skip the initialization steps
}else{
//the device can continue operating
}
}
// ***************************************************************
// Standing by functions
else if(!bActive){
//check if the device just deactivated
if(bActivePrevious){
Serial.println("Entering Standby");
FanSpeed = 0;
analogWrite(Fan, FanSpeed);
bActivePrevious = false; // set the previous active state to false so we can skip adjustments.
}else{
//should add a short sleep here to reduce power usage.
}
}
delay(100); // short delay between loops
}
Addressable RGB LEDS
If you have ever worked with a Razer product before, you might have noticed that they love to pack RGB LEDs into their devices wherever possible. Having colourful lights at this point almost seems to scream Gamers! and High-Tech! So if I want to build a cyber mask, RGB’s are pretty much a requirement. For this project I got a strip of 5V addressable RGB’s: https://www.amazon.ca/gp/product/B07R6LR7DK/ref=ppx_yo_dt_b_asin_title_o01_s00?ie=UTF8&psc=1. I’ve done a few projects with LED strips before, but I have never been able to work with addressable RGB’s. So far they have been a lot of fun! To get the LED’s working I used a library called FastLED: https://fastled.io. FastLED provided an easy way to setup my LED’s and then to control their colour and brightness in a way that even allowed me to setup some simple animations. After getting the LED’s functioning, I spent some additional time bringing all of my existing code together that I thought would be relevant to making my mask. I also added more triggers from the IR remote so that I can easily switch between the 16 colours on the remote.
// ***************************************************************
// DEPENDENCIES
#include <IRremote.hpp>
#define FASTLED_INTERNAL //disables a warning from the FastLED Library
#include <FastLED.h>
// ***************************************************************
// PIN DECLARATIONS
#define IRReceiver 11 // the pin the IR Receiver is connected to
#define IRCollision 10 // the pin the IR Collision Sensor is connected to
#define Fan 9 // The pin that will control the left fan
#define RGBLED 8 // The pin that will control the addressable LEDs
#define RGBLEDNum 10 //the number of RGD LEDS to control via the RGBLED pin
// ***************************************************************
// GLOBAL VARIABLES
IRrecv irrecv(IRReceiver);
decode_results results;
CRGB leds[RGBLEDNum]; //declare a string of leds for controlling with fastLED
CHSV RGBColour = CHSV(0,0,255);
bool RGBColourChanged = false;
int RGBBrightness = 4; //Allows control of the brightness of an LED 0-0%, 1-25%, 2-50%, 3-75%, 4-100%
int RGBDelay = 250;
bool bActive = false;
bool bActivePrevious = false;
int CollisionCurrent = HIGH; //High indicates no collision, Low indicates Collision
int CollisionPrevious = HIGH;
int FanSpeed = 0;
//int IRLevel = 0; // this variable will hold the valu returned by the photoresistor
//int IRThreshold = 900; // if the reading is above this the user is wearing the mask and it should turn on.
void UpdateRGBLEDs(){
Serial.println("Updating RGB LEDs");
RGBColour.value = RGBBrightness * 63.75;
for(int i = 0; i < RGBLEDNum; i++){
leds[i]=RGBColour;
FastLED.show();
delay(RGBDelay);
}
RGBColourChanged = false;
}
void StopRGBLEDs(int delayTime){
Serial.println("Stopping RGB LEDs");
for(int i = RGBLEDNum - 1; i >= 0; i--){
leds[i]=CRGB::Black;
FastLED.show();
delay(delayTime);
}
}
void ProcessIRCommand(){
uint32_t * IRCommand = &irrecv.decodedIRData.decodedRawData;
Serial.print("Attemting to process IR Command: ");
Serial.println(*IRCommand, HEX);
//Exit if input is Scrambled
if (*IRCommand == 0x0){
return;
}
//if the power button was pushed
else if(*IRCommand == 0xBF40FF00){
//toggle the bActive boolean
if(bActive){
Serial.println("IR Remote - Setting bActive to false.");
bActive = false;
} else {
Serial.println("IR Remote - Setting bActive to true.");
bActive = true;
}
}
//Process the Colour choices
else if (*IRCommand == 0xBB44FF00){RGBColour = CHSV(0,0,255); RGBColourChanged = true;}
else if (*IRCommand == 0xA758FF00){RGBColour = CHSV(0,255,255); RGBColourChanged = true;}
else if (*IRCommand == 0xA659FF00){RGBColour = CHSV(85,255,255); RGBColourChanged = true;}
else if (*IRCommand == 0xBA45FF00){RGBColour = CHSV(170,255,255); RGBColourChanged = true;}
else if (*IRCommand == 0xAB54FF00){RGBColour = CHSV(6,255,255); RGBColourChanged = true;}
else if (*IRCommand == 0xAA55FF00){RGBColour = CHSV(99,255,255); RGBColourChanged = true;}
else if (*IRCommand == 0xB649FF00){RGBColour = CHSV(143,255,255); RGBColourChanged = true;}
else if (*IRCommand == 0xAF50FF00){RGBColour = CHSV(11,255,255); RGBColourChanged = true;}
else if (*IRCommand == 0xAE51FF00){RGBColour = CHSV(128,255,255); RGBColourChanged = true;}
else if (*IRCommand == 0xB24DFF00){RGBColour = CHSV(208,255,255); RGBColourChanged = true;}
else if (*IRCommand == 0xE31CFF00){RGBColour = CHSV(26,255,255); RGBColourChanged = true;}
else if (*IRCommand == 0xE21DFF00){RGBColour = CHSV(183,255,255); RGBColourChanged = true;}
else if (*IRCommand == 0xE11EFF00){RGBColour = CHSV(224,255,255); RGBColourChanged = true;}
else if (*IRCommand == 0xE718FF00){RGBColour = CHSV(38,255,255); RGBColourChanged = true;}
else if (*IRCommand == 0xE619FF00){RGBColour = CHSV(125,255,255); RGBColourChanged = true;}
else if (*IRCommand == 0xE51AFF00){RGBColour = CHSV(213,255,255); RGBColourChanged = true;}
//Increase Brightness
else if (*IRCommand == 0xA35CFF00){
if(RGBBrightness < 4){
RGBBrightness++;
RGBColourChanged = true;
Serial.print("Increasing Brightness: ");
Serial.println(RGBBrightness);
}
}
//Decrease Brightness
else if (*IRCommand == 0xA25DFF00){
if(RGBBrightness > 0){
RGBBrightness--;
RGBColourChanged = true;
Serial.print("Decreasing Brightness: ");
Serial.println(RGBBrightness);
}
}
return;
}
void setup() {
Serial.begin(9600); // Start a serial connection with the computer
irrecv.enableIRIn(); // start the receiver
FastLED.addLeds<WS2812, RGBLED, GRB>(leds, RGBLEDNum);
pinMode(IRCollision, INPUT); // Declare the IRCollision sensor as an input device
pinMode(Fan, OUTPUT); // declare the left fan pin for output control
analogWrite(Fan, FanSpeed); // initialize the fan speed to 0
StopRGBLEDs(0);
}
void loop() {
// ***************************************************************
//Receive IR Input from remote
if(irrecv.decode())
{
//Serial.println(results.value, HEX);
//The function decode(&results)) is deprecated and may not work as expected! Just use decode() without a parameter and IrReceiver.decodedIRData.<fieldname> .
ProcessIRCommand();
irrecv.resume();
}
// ***************************************************************
// Check wear sensor for state change
CollisionCurrent = digitalRead(IRCollision);
// Check wear sensor for activation
if(CollisionCurrent == LOW && CollisionPrevious == HIGH){
bActive = true;
Serial.println("IR Collision - Setting bActive to true.");
}
// check wear sensor for standby
else if(CollisionCurrent == HIGH && CollisionPrevious == LOW){
bActive = false;
Serial.println("IR Collision - Setting bActive to false.");
}
CollisionPrevious = CollisionCurrent;
// ***************************************************************
// Active Functions
if(bActive){
//check if the device just activated
if(!bActivePrevious){
Serial.println("Activating the mask!");
FanSpeed = 255;
analogWrite(Fan, FanSpeed);
UpdateRGBLEDs();
bActivePrevious = true; // set the previous activity to true so that we can skip the initialization steps
}else{
//the device can continue operating
if(RGBColourChanged){
UpdateRGBLEDs();
}
}
}
// ***************************************************************
// Standing by functions
else if(!bActive){
//check if the device just deactivated
if(bActivePrevious){
Serial.println("Entering Standby");
FanSpeed = 0;
analogWrite(Fan, FanSpeed);
StopRGBLEDs(RGBDelay);
bActivePrevious = false; // set the previous active state to false so we can skip adjustments.
}else{
//should add a short sleep here to reduce power usage.
}
}
delay(100); // short delay between loops
}
Before I get to the microphone implementation, I wanted to add a bit more functionality to the lighting of the RGB LED’s. I spent some more time working with the lights, and aside from being able to choose any of the colours on the remote, I also added the ability to adjust the brightness of the lights. I also added several lighting modes to the project. First I added a travelling rainbow LED effect which was fairly simple as the FastLED library included a function specifically for this purpose. I then also implemented a breathing light, which glows and then fades at roughly the same rate at which a normal human breathes. To this breathing light I also tied in control of the fans so that the fans would speed up and then slow down a little. This causes the mask to “breathe” both visually and physically. Finally, I also added a “bounce” or “fade” light mode which smoothly transitions from one colour to the colour that was previously chosen. I am really happy with how the lighting is working at this point, and I still have one more button that I hope to add an additional light mode to, when I figure out what I would like to put there.
Microphone Input
The next step in my plan is to add a microphone to the system. The first stage of this implementation will use the mic to change the lighting mode, making it easier to view facial expressions, and slow the fans when someone is speaking. This will cause the mask to enter “speaking” mode with a slight delay before returning to normal lighting and fan modes. At this stage, I am also planning to add a headphone jack so that external speakers can be used to amplify/broadcast the output of the microphone. If I can, my next step will to be to add voice modulation to the system so that the voice output will sound more sci-fi and techno. Finally, the last thing I would like to try implementing would be integrated speakers. However, the amplifier board I ordered for this purpose is quite a bit larger than I expected and I am not sure if I will be able to implement it in the final mask design.
I was able to implement the basic microphone input as I desired. The mask can detect when there is input from the mic over a threshold value and will adjust the fans and lighting for “speaking” mode when input is detected. There is then a 3 second delay after speaking before the mask returns to normal operation. I can adjust the threshold and the delay as desired by adjusting two variables in my code. At the moment, I am using a headphone out jack to external speakers to broadcast the audio that the mic is picking up. The biggest issue that I have with this, is that I want to mute the speakers if the mask is not in speaking mode, and this currently does not seem to be doable. I am hoping that the audio amplifier that I will be using for the built in speakers will be able to mute audio output.
// ***************************************************************
// DEPENDENCIES
#include <IRremote.hpp>
#define FASTLED_INTERNAL //disables a warning from the FastLED Library
#include <FastLED.h>
// ***************************************************************
// PIN DECLARATIONS
//#define IR 13 // The pin the IR-LED is connected to
//#define PHOTORESISTOR A0// The pin the Photoresistor is connected to
//#define WEARSWITCH 12 // the pin the contact switch is connected to
#define IRReceiver 11 // the pin the IR Receiver is connected to
#define IRCollision 10 // the pin the IR Collision Sensor is connected to
#define Fan 9 // The pin that will control the left fan
#define RGBLED 8 // The pin that will control the addressable LEDs
#define RGBLEDNum 20 //the number of RGD LEDS to control via the RGBLED pin
#define MicInput A0 // The pin for the microphone input
// ***************************************************************
// GLOBAL VARIABLES
IRrecv irrecv(IRReceiver);
decode_results results;
CRGB leds[RGBLEDNum]; //declare a string of leds for controlling with fastLED
CHSV RGBColourTemp = CHSV(0,0,0);
CHSV RGBColour = CHSV(0,0,255); // holds the current LED Colour
CHSV RGBColour2 = CHSV(224,255,255); //holds the previous LED colour for the bounce effect
CHSV RGBOff = CHSV(0,0,0);
bool bRGBColourChanged = false; // tracks if the leds need an update
bool bRGBRainbow = false; // controls if the rainbow effect is on
bool bRGBBreathe = false; //controls if the breathe effect is on
bool bRGBBounce = false; // controls if the bounce effect is on
int RGBBrightness = 4; //Allows control of the brightness of an LED 0-0%, 1-25%, 2-50%, 3-75%, 4-100%
int RGBDelay = 50; // controls delay between updating LEDs for animated effects
bool bActive = false; //Tracks if the Mask is in standby or active mode
bool bActivePrevious = false; // allows active state changes only if there is a change in wear or power state
int CollisionCurrent = HIGH; //High indicates no collision, Low indicates Collision
int CollisionPrevious = HIGH; // Used so that the collision sensor only adjusts active state if there is a change
float FanSpeed = 0;
float FanMax = 255.0;
float FanMin = 128.0;
float PulseSpeed = 0.5;
float DeltaMin = 60;
float DeltaMax = 255.0;
float DeltaTime = 2000;
float DeltaValue = (DeltaMax - DeltaMin) / 2.35040238;
const int MicSampleWindow = 50;
unsigned int sample;
float MicSpeakingThreshold = 1.7;
bool bMicMute = false;
bool bSpeaking = false;
int SpeakingTimeDelay = 3000;
unsigned long LastSpeakingTime;
double volts;
CHSV RGBSpeaking = CHSV(0,0,255);
//int IRLevel = 0; // this variable will hold the value returned by the photoresistor
//int IRThreshold = 900; // if the reading is above this the user is wearing the mask and it should turn on.
void UpdateRGBLEDs(uint8_t hue, uint8_t sat, float brightness, int delayTime){
//Serial.println("Updating RGB LEDs");
//RGBColour.value = RGBBrightness * 63.75;
RGBColourTemp.hue = hue;
RGBColourTemp.sat = sat;
RGBColourTemp.value = brightness;
for(int i = 0; i < RGBLEDNum; i++){
leds[i]=RGBColourTemp;
FastLED.show();
delay(delayTime);
}
bRGBColourChanged = false;
}
void UpdateRGBLEDsReverse(uint8_t hue, uint8_t sat, float brightness, int delayTime){
//Serial.println("Updating RGB LEDs");
//RGBColour.value = RGBBrightness * 63.75;
RGBColourTemp.hue = hue;
RGBColourTemp.sat = sat;
RGBColourTemp.value = brightness;
for(int i = RGBLEDNum - 1; i >= 0; i--){
leds[i]=RGBColourTemp;
FastLED.show();
delay(delayTime);
}
bRGBColourChanged = false;
}
void UpdateRGBColour2(){
RGBColour2 = RGBColour;
}
/*
void StopRGBLEDs(int delayTime){
Serial.println("Stopping RGB LEDs");
for(int i = RGBLEDNum - 1; i >= 0; i--){
leds[i]=CRGB::Black;
FastLED.show();
delay(delayTime);
}
}
*/
void ProcessIRCommand(){
uint32_t * IRCommand = &irrecv.decodedIRData.decodedRawData;
Serial.print("Attempting to process IR Command: ");
Serial.println(*IRCommand, HEX);
//Exit if input is Scrambled
if (*IRCommand == 0x0){
return;
}
//if the power button was pushed
else if(*IRCommand == 0xBF40FF00){
//toggle the bActive boolean
if(bActive){
Serial.println("IR Remote - Setting bActive to false.");
bActive = false;
} else {
Serial.println("IR Remote - Setting bActive to true.");
bActive = true;
}
}
//if the Mic button was pushed
else if(*IRCommand == 0xBE41FF00){
//toggle the bMicMute boolean
if(bMicMute){
Serial.println("IR Remote - Setting bMicMute to false.");
bMicMute = false;
} else {
Serial.println("IR Remote - Setting bMicMute to true.");
bMicMute = true;
}
}
//Process the Colour choices
else if (*IRCommand == 0xBB44FF00){UpdateRGBColour2(); RGBColour = CHSV(0,0,255); bRGBColourChanged = true;}
else if (*IRCommand == 0xA758FF00){UpdateRGBColour2(); RGBColour = CHSV(0,255,255); bRGBColourChanged = true;}
else if (*IRCommand == 0xA659FF00){UpdateRGBColour2(); RGBColour = CHSV(85,255,255); bRGBColourChanged = true;}
else if (*IRCommand == 0xBA45FF00){UpdateRGBColour2(); RGBColour = CHSV(170,255,255); bRGBColourChanged = true;}
else if (*IRCommand == 0xAB54FF00){UpdateRGBColour2(); RGBColour = CHSV(6,255,255); bRGBColourChanged = true;}
else if (*IRCommand == 0xAA55FF00){UpdateRGBColour2(); RGBColour = CHSV(99,255,255); bRGBColourChanged = true;}
else if (*IRCommand == 0xB649FF00){UpdateRGBColour2(); RGBColour = CHSV(143,255,255); bRGBColourChanged = true;}
else if (*IRCommand == 0xAF50FF00){UpdateRGBColour2(); RGBColour = CHSV(11,255,255); bRGBColourChanged = true;}
else if (*IRCommand == 0xAE51FF00){UpdateRGBColour2(); RGBColour = CHSV(128,255,255); bRGBColourChanged = true;}
else if (*IRCommand == 0xB24DFF00){UpdateRGBColour2(); RGBColour = CHSV(208,255,255); bRGBColourChanged = true;}
else if (*IRCommand == 0xE31CFF00){UpdateRGBColour2(); RGBColour = CHSV(26,255,255); bRGBColourChanged = true;}
else if (*IRCommand == 0xE21DFF00){UpdateRGBColour2(); RGBColour = CHSV(130,255,255); bRGBColourChanged = true;}
else if (*IRCommand == 0xE11EFF00){UpdateRGBColour2(); RGBColour = CHSV(224,255,255); bRGBColourChanged = true;}
else if (*IRCommand == 0xE718FF00){UpdateRGBColour2(); RGBColour = CHSV(38,255,255); bRGBColourChanged = true;}
else if (*IRCommand == 0xE619FF00){UpdateRGBColour2(); RGBColour = CHSV(125,255,255); bRGBColourChanged = true;}
else if (*IRCommand == 0xE51AFF00){UpdateRGBColour2(); RGBColour = CHSV(213,255,255); bRGBColourChanged = true;}
//Increase Brightness
else if (*IRCommand == 0xA35CFF00){
if(RGBBrightness < 4){
RGBBrightness++;
bRGBColourChanged = true;
Serial.print("Increasing Brightness: ");
Serial.println(RGBBrightness);
}
}
//Decrease Brightness
else if (*IRCommand == 0xA25DFF00){
if(RGBBrightness > 0){
RGBBrightness--;
bRGBColourChanged = true;
Serial.print("Decreasing Brightness: ");
Serial.println(RGBBrightness);
}
}
//Activate Rainbow Mode
else if (*IRCommand == 0xB748FF00){
Serial.println("Taste the Rainbow!");
bRGBRainbow = true;
bRGBBreathe = false;
bRGBBounce = false;
}
//Activate Breathe Mode
else if (*IRCommand == 0xB34CFF00){
Serial.println("Take a deep breathe!");
bRGBBreathe = true;
bRGBRainbow = false;
bRGBBounce = false;
}
//Activate Bounce mode
else if (*IRCommand == 0xE01FFF00){
Serial.println("Which is better, this or that?");
bRGBBreathe = false;
bRGBRainbow = false;
bRGBBounce = true;
}
return;
}
//The Rainbow wave function is based on code from https://pastebin.com/4V5xkXey
void rainbow_wave(uint8_t thisSpeed, uint8_t deltaHue) { // The fill_rainbow call doesn't support brightness levels.
uint8_t thisHue = beat8(thisSpeed,255); // A simple rainbow march.
fill_rainbow(leds, RGBLEDNum, thisHue, deltaHue); // Use FastLED's fill_rainbow routine.
FastLED.show();
}
float CalculateDelta(){
return DeltaMin + ((exp(sin(PulseSpeed * millis()/DeltaTime*PI)) - 0.36787944) * DeltaValue);
}
//This function will pulse the leds like the lights are breathing
//Inspired by https://github.com/marmilicious/FastLED_examples/blob/master/breath_effect_v2.ino
void UpdateRGBBreath(){
float BreathBrightness = CalculateDelta();
//Serial.print("Setting Brightness: ");
//Serial.println(BreathBrightness);
UpdateRGBLEDs(RGBColour.hue, RGBColour.sat, BreathBrightness, 0);
FastLED.show();
analogWrite(Fan, BreathBrightness);
}
/*
void UpdateRGBBounce(){
float delta = CalculateDelta();
uint8_t tempHue = map(delta, DeltaMin, DeltaMax, RGBColour.hue, RGBColour2.hue);
uint8_t tempSat = map(delta, DeltaMin, DeltaMax, RGBColour.sat, RGBColour2.sat);
UpdateRGBLEDs(tempHue, tempSat, DeltaMax, 0);
FastLED.show();
//analogWrite(Fan, delta);
}
*/
//Referenced from https://gist.github.com/atuline/b279fda278417f581773
void UpdateRGBBounce(){
uint8_t speed = beatsin8(6,0,255);
RGBColourTemp = blend(RGBColour, RGBColour2, speed);
UpdateRGBLEDs(RGBColourTemp.hue, RGBColourTemp.sat, DeltaMax, 0);
FastLED.show();
}
//Referenced from https://learn.adafruit.com/adafruit-microphone-amplifier-breakout/measuring-sound-levels
void CollectMicSample(){
unsigned long startMillis= millis(); // Start of sample window
unsigned int peakToPeak = 0; // peak-to-peak level
unsigned int signalMax = 0;
unsigned int signalMin = 1024;
// collect data for 50 mS
while (millis() - startMillis < MicSampleWindow)
{
sample = analogRead(MicInput);
if (sample < 1024) // toss out spurious readings
{
if (sample > signalMax)
{
signalMax = sample; // save just the max levels
}
else if (sample < signalMin)
{
signalMin = sample; // save just the min levels
}
}
}
peakToPeak = signalMax - signalMin; // max - min = peak-peak amplitude
volts = (peakToPeak * 5.0) / 1024; // convert to volts
//Serial.println(volts);
if(volts > MicSpeakingThreshold){
Serial.print("Audio Input Detected:");
Serial.println(volts);
bSpeaking = true;
LastSpeakingTime = millis();
UpdateRGBLEDsReverse(RGBSpeaking.hue, RGBSpeaking.sat, RGBSpeaking.val, RGBDelay/2);
analogWrite(Fan, FanMax/2);
}
}
void setup() {
Serial.begin(9600); // Start a serial connection with the computer
irrecv.enableIRIn(); // start the receiver
FastLED.addLeds<WS2812, RGBLED, GRB>(leds, RGBLEDNum);
pinMode(IRCollision, INPUT); // Declare the IRCollision sensor as an input device
pinMode(Fan, OUTPUT); // declare the left fan pin for output control
analogWrite(Fan, FanSpeed); // initialize the fan speed to 0
//pinMode(IR, OUTPUT); // Declare the IR-LED as an output
//pinMode(WEARSWITCH, INPUT_PULLUP);
UpdateRGBLEDsReverse(RGBOff.hue, RGBOff.sat, RGBOff.val, RGBDelay);
//StopRGBLEDs(0);
}
void loop() {
// ***************************************************************
//Receive IR Input from remote
if(irrecv.decode())
{
//Serial.println(results.value, HEX);
//The function decode(&results)) is deprecated and may not work as expected! Just use decode() without a parameter and IrReceiver.decodedIRData.<fieldname> .
ProcessIRCommand();
irrecv.resume();
}
// ***************************************************************
// Check wear sensor for state change
CollisionCurrent = digitalRead(IRCollision);
// Check wear sensor for activation
if(CollisionCurrent == LOW && CollisionPrevious == HIGH){
bActive = true;
Serial.println("IR Collision - Setting bActive to true.");
}
// check wear sensor for standby
else if(CollisionCurrent == HIGH && CollisionPrevious == LOW){
bActive = false;
Serial.println("IR Collision - Setting bActive to false.");
}
CollisionPrevious = CollisionCurrent;
// ***************************************************************
// Active Functions
if(bActive){
if(!bMicMute){
CollectMicSample();
}
if(bSpeaking){
if(LastSpeakingTime + SpeakingTimeDelay < millis()){
Serial.println("Mic Standing by.");
bSpeaking = false;
analogWrite(Fan, FanMax);
UpdateRGBLEDs(RGBColour.hue, RGBColour.sat, RGBBrightness * 63.75, RGBDelay/2);
}
}
//check if the device just activated
else if(!bActivePrevious){
Serial.println("Activating the mask!");
analogWrite(Fan, FanMax);
UpdateRGBLEDs(RGBColour.hue, RGBColour.sat, RGBBrightness * 63.75, RGBDelay);
bActivePrevious = true; // set the previous activity to true so that we can skip the initialization steps
}else{
//the device can continue operating
if(bRGBColourChanged){
bRGBRainbow = false;
bRGBBreathe = false;
bRGBBounce = false;
analogWrite(Fan, FanMax);
UpdateRGBLEDs(RGBColour.hue, RGBColour.sat, RGBBrightness * 63.75, RGBDelay);
}
if(bRGBRainbow){
bRGBBreathe = false;
bRGBBounce = false;
analogWrite(Fan, FanMax);
rainbow_wave(10,10);
}
if(bRGBBreathe){
bRGBBounce - false;
bRGBRainbow = false;
UpdateRGBBreath();
}
if(bRGBBounce){
bRGBBreathe = false;
bRGBRainbow = false;
UpdateRGBBounce();
}
}
}
// ***************************************************************
// Standing by functions
else if(!bActive){
//check if the device just deactivated
if(bActivePrevious){
Serial.println("Entering Standby");
FanSpeed = 0;
analogWrite(Fan, FanSpeed);
UpdateRGBLEDsReverse(RGBOff.hue, RGBOff.sat, RGBOff.val, RGBDelay);
//StopRGBLEDs(RGBDelay);
bActivePrevious = false; // set the previous active state to false so we can skip adjustments.
}else{
//should add a short sleep here to reduce power usage.
}
}
/*
// ***************************************************************
// LED Level Wear Activation
digitalWrite(IR, HIGH); // Turn the IR-LED on
IRLevel = analogRead(PHOTORESISTOR);
Serial.println(IRLevel);
if(IRLevel > IRThreshold)
{
// mask should be active
Serial.println("Active Mode");
} else {
// mask should be in standby
Serial.println("Standby Mode");
}
*/
/*
// ***************************************************************
// Switch based Activation
if(digitalRead(WEARSWITCH) == LOW)
{
// mask should be active
Serial.println("Active Mode");
} else {
// mask should be in standby
Serial.println("Standby Mode");
}
*/
// ***************************************************************
// RGB LED
/*
leds[0]=RGBColour;
FastLED.show();
delay(500);
leds[1]=RGBColour;
FastLED.show();
delay(500);
leds[2]=RGBColour;
FastLED.show();
delay(500);
leds[3]=RGBColour;
FastLED.show();
delay(500);
*/
delay(100); // short delay between loops
}
Voice Modulation
I thought it would be fun to add some circuitry to allow for voice modulation. To that end I wanted to repurpose the troopduino for my project, https://github.com/BipeFlyer/Troopduino. The troopduino was created for star wars stormtrooper costumes to make the speaker sound like they are talking over the radio and adds a bunch of clicks and noises to the mic input.
Unfortunately, I did not have access to the specialty PCB for this project. So I spent some time working through the diagrams and I was able to piece together a working facsimile with the parts that I had available to me. It took far more time than I expected to get this working, and ultimately it required so much extra wiring for additional resistors and capacitors, that I don’t think its going to be reasonable to try to fit this into the mask. Additionally, the Troopduino code, was rather extensive and trying to refactor it to fit into my code base would be a bit of a nightmare. So at this point I am going to scratch voice modulation from my list of features for my mask.
Mask Design
With functional code and some working circuits I was finally ready to try bringing all the pieces together to create my mask. I expected this to be a fairly straightforward 3D-modelling project that I should be able to punch out in a day or two. I have a previous degree in game art where I specialized in environments and hard-surface objects, so I figured that my skills would be well suited to the task at hand. Unfortunately, the years have flown by and my skills have clearly stagnated. What I expected to be a two day project wound up being closer to two week, I ran through many different iterations, and had to restart the process several times as I dusted off some rather rusty skills. In the end though I was able to create a mask that I am delighted with and which I look forward to wearing in public.
When creating the mask there were a lot of practical problems that arose where I faced problems with the 3d printing process. Ultimately I decided to break the mask into separate parts so that it would be easier to print and only take a little bit of assembly. This meant that I was able to print the underlying frame from a flexible TPU plastic. This allows the mask to flex and form to the wearers face. I then printed off all the “mounts” in a more rigid plastic that could easily be swapped out if a mount was damaged. These mounts were printed in PLA-F and at a much higher resolution so that they would better conform to the parts they were to support. Finally, to attach the various components to the mask, I fused some brass inserts into the frame which the mounts could then screw into.
The final step was to assemble the mask. The assembly was fairly straightforward, but the soldering took a bit to get done. I am quite happy with the final mask.
Mask in action
I attempted to create a little video to showcase the mask and briefly review the project. I had a blast with this project, and while I do see some more points that I could improve on, I am quite happy with where things are and I look forward to using this mask whenever I go out now.
I was able to accomplish almost everything I set out to do with this mask. The filtration system works very well, and the mask is quite comfortable to wear and to breathe in. I love the way things look, and the lighting and lighting effects are a lot of fun. While I was able to create a voice modulator, I was unable to adapt it to fit into the form factor of the mask and I was unable to integrate the code into my project code, so I decided to cut voice modulation from this iteration of the mask. I was able to get the microphone and speaker system working, but I ran into problems with the microphone gain control where if I stopped talking there would be a very loud feedback loop. I believe that this is due to the microphone that I used which features automatic gain control, I would like to test a more basic microphone with a manually controlled gain and see if that solves the problem. The wear sensor is very effective at determining if someone is wearing the mask or not, and the mask seems to go for a very long time on a charge of the battery that I am using(so far it is greater than 24hrs in standby, and several hours in active mode). I enjoy being able to control the lighting, microphone and effects with the remote control but I am also happy with how I have the default behavior set if I don’t have the remote on me. In a future version, I think it would be worth switching to a Bluetooth module for controlling the device and outputting the microphone audio. This would however require more extensive work for driver implementation and creating a control app, so is not currently achievable. Lastly, I am fairly happy with the design, however I would like to make two important changes to any future versions of the mask: first I would like to add some additional length to the bottom so that I could have an attachment point for the wiring, and secondly I would like to shift the window down just a bit. The window is intended to show facial expressions, and so is intended primarily for the mouth area, the current implementation shows a bit of the nose and that just seems weird. The only other thing I would further do with the mask is to also print out the outer shell, which would serve to protect the electronics and wiring, and which could be processed to a much nicer finish then the flexible frame can be.
Thank you for joining me on this journey and I hope you have a fantastic new year!
Shawn Ritter
January 8th, 2022
Final Code and Diagram
*Just as a last note I am going to include the circuit diagram and pin allocation that I wound up using for the final implementation as well as the final code base.
// ***************************************************************
// DEPENDENCIES
#include <IRremote.hpp>
#define FASTLED_INTERNAL //disables a warning from the FastLED Library
#include <FastLED.h>
// ***************************************************************
// Arduino Uno PIN DECLARATIONS
//#define IR 13 // The pin the IR-LED is connected to
//#define PHOTORESISTOR A0// The pin the Photoresistor is connected to
//#define WEARSWITCH 12 // the pin the contact switch is connected to
//#define IRReceiver 11 // the pin the IR Receiver is connected to
//#define IRCollision 10 // the pin the IR Collision Sensor is connected to
//#define Fan 9 // The pin that will control the left fan
//#define RGBLED 8 // The pin that will control the addressable LEDs
//#define RGBLEDNum 20 //the number of RGD LEDS to control via the RGBLED pin
//#define MicInput A0 // The pin for the microphone input
//#define SpeakerControl 12 // the pin used to shutdown the speakers
// ***************************************************************
// ARDUINO NANO PIN DECLARATIONS
//#define IR 13 // The pin the IR-LED is connected to
//#define PHOTORESISTOR A0// The pin the Photoresistor is connected to
//#define WEARSWITCH 12 // the pin the contact switch is connected to
#define IRReceiver 11 // the pin the IR Receiver is connected to
#define IRCollision 7 // the pin the IR Collision Sensor is connected to
#define Fan 9 // The pin that will control the left fan
#define RGBLED 3 // The pin that will control the addressable LEDs
#define RGBLEDNum 14 //the number of RGD LEDS to control via the RGBLED pin
#define MicInput A0 // The pin for the microphone input
#define SpeakerControl 5 // the pin used to shutdown the speakers
// ***************************************************************
// GLOBAL VARIABLES
IRrecv irrecv(IRReceiver);
decode_results results;
CRGB leds[RGBLEDNum]; //declare a string of leds for controlling with fastLED
CHSV RGBColourTemp = CHSV(0,0,0);
CHSV RGBColour = CHSV(0,0,255); // holds the current LED Colour
CHSV RGBColour2 = CHSV(224,255,255); //holds the previous LED colour for the bounce effect
CHSV RGBOff = CHSV(0,0,0);
bool bRGBColourChanged = false; // tracks if the leds need an update
bool bRGBRainbow = true; // controls if the rainbow effect is on
bool bRGBBreathe = false; //controls if the breathe effect is on
bool bRGBBounce = false; // controls if the bounce effect is on
int RGBBrightness = 4; //Allows control of the brightness of an LED 0-0%, 1-25%, 2-50%, 3-75%, 4-100%
int RGBDelay = 50; // controls delay between updating LEDs for animated effects
bool bActive = false; //Tracks if the Mask is in standby or active mode
bool bActivePrevious = false; // allows active state changes only if there is a change in wear or power state
int CollisionCurrent = HIGH; //High indicates no collision, Low indicates Collision
int CollisionPrevious = HIGH; // Used so that the collision sensor only adjusts active state if there is a change
float FanSpeed = 0;
float FanMax = 255.0;
float FanMin = 128.0;
float PulseSpeed = 0.5;
float DeltaMin = 60;
float DeltaMax = 255.0;
float DeltaTime = 2000;
float DeltaValue = (DeltaMax - DeltaMin) / 2.35040238;
const int MicSampleWindow = 50;
unsigned int sample;
float MicSpeakingThreshold = 1.9;
bool bMicMute = false;
bool bSpeaking = false;
int SpeakingTimeDelay = 3000;
float SpeakingFanSpeed = FanMax;
unsigned long LastSpeakingTime;
double volts;
CHSV RGBSpeaking = CHSV(0,0,255);
//int IRLevel = 0; // this variable will hold the value returned by the photoresistor
//int IRThreshold = 900; // if the reading is above this the user is wearing the mask and it should turn on.
void UpdateRGBLEDs(uint8_t hue, uint8_t sat, float brightness, int delayTime){
//Serial.println("Updating RGB LEDs");
//RGBColour.value = RGBBrightness * 63.75;
RGBColourTemp.hue = hue;
RGBColourTemp.sat = sat;
RGBColourTemp.value = brightness;
for(int i = 0; i < RGBLEDNum; i++){
leds[i]=RGBColourTemp;
FastLED.show();
delay(delayTime);
}
bRGBColourChanged = false;
}
void UpdateRGBLEDsReverse(uint8_t hue, uint8_t sat, float brightness, int delayTime){
//Serial.println("Updating RGB LEDs");
//RGBColour.value = RGBBrightness * 63.75;
RGBColourTemp.hue = hue;
RGBColourTemp.sat = sat;
RGBColourTemp.value = brightness;
for(int i = RGBLEDNum - 1; i >= 0; i--){
leds[i]=RGBColourTemp;
FastLED.show();
delay(delayTime);
}
bRGBColourChanged = false;
}
void UpdateRGBColour2(){
RGBColour2 = RGBColour;
}
/*
void StopRGBLEDs(int delayTime){
Serial.println("Stopping RGB LEDs");
for(int i = RGBLEDNum - 1; i >= 0; i--){
leds[i]=CRGB::Black;
FastLED.show();
delay(delayTime);
}
}
*/
void ProcessIRCommand(){
uint32_t * IRCommand = &irrecv.decodedIRData.decodedRawData;
Serial.print("Attempting to process IR Command: ");
Serial.println(*IRCommand, HEX);
//Exit if input is Scrambled
if (*IRCommand == 0x0){
return;
}
//if the power button was pushed
else if(*IRCommand == 0xBF40FF00){
//toggle the bActive boolean
if(bActive){
Serial.println("IR Remote - Setting bActive to false.");
bActive = false;
} else {
Serial.println("IR Remote - Setting bActive to true.");
bActive = true;
}
}
//if the Mic button was pushed
else if(*IRCommand == 0xBE41FF00){
//toggle the bMicMute boolean
if(bMicMute){
Serial.println("IR Remote - Setting bMicMute to false.");
bMicMute = false;
} else {
Serial.println("IR Remote - Setting bMicMute to true.");
bMicMute = true;
}
}
//Process the Colour choices
else if (*IRCommand == 0xBB44FF00){UpdateRGBColour2(); RGBColour = CHSV(0,0,255); bRGBColourChanged = true;}
else if (*IRCommand == 0xA758FF00){UpdateRGBColour2(); RGBColour = CHSV(0,255,255); bRGBColourChanged = true;}
else if (*IRCommand == 0xA659FF00){UpdateRGBColour2(); RGBColour = CHSV(85,255,255); bRGBColourChanged = true;}
else if (*IRCommand == 0xBA45FF00){UpdateRGBColour2(); RGBColour = CHSV(170,255,255); bRGBColourChanged = true;}
else if (*IRCommand == 0xAB54FF00){UpdateRGBColour2(); RGBColour = CHSV(6,255,255); bRGBColourChanged = true;}
else if (*IRCommand == 0xAA55FF00){UpdateRGBColour2(); RGBColour = CHSV(99,255,255); bRGBColourChanged = true;}
else if (*IRCommand == 0xB649FF00){UpdateRGBColour2(); RGBColour = CHSV(143,255,255); bRGBColourChanged = true;}
else if (*IRCommand == 0xAF50FF00){UpdateRGBColour2(); RGBColour = CHSV(11,255,255); bRGBColourChanged = true;}
else if (*IRCommand == 0xAE51FF00){UpdateRGBColour2(); RGBColour = CHSV(128,255,255); bRGBColourChanged = true;}
else if (*IRCommand == 0xB24DFF00){UpdateRGBColour2(); RGBColour = CHSV(208,255,255); bRGBColourChanged = true;}
else if (*IRCommand == 0xE31CFF00){UpdateRGBColour2(); RGBColour = CHSV(26,255,255); bRGBColourChanged = true;}
else if (*IRCommand == 0xE21DFF00){UpdateRGBColour2(); RGBColour = CHSV(130,255,255); bRGBColourChanged = true;}
else if (*IRCommand == 0xE11EFF00){UpdateRGBColour2(); RGBColour = CHSV(224,255,255); bRGBColourChanged = true;}
else if (*IRCommand == 0xE718FF00){UpdateRGBColour2(); RGBColour = CHSV(38,255,255); bRGBColourChanged = true;}
else if (*IRCommand == 0xE619FF00){UpdateRGBColour2(); RGBColour = CHSV(125,255,255); bRGBColourChanged = true;}
else if (*IRCommand == 0xE51AFF00){UpdateRGBColour2(); RGBColour = CHSV(213,255,255); bRGBColourChanged = true;}
//Increase Brightness
else if (*IRCommand == 0xA35CFF00){
if(RGBBrightness < 4){
RGBBrightness++;
bRGBColourChanged = true;
Serial.print("Increasing Brightness: ");
Serial.println(RGBBrightness);
}
}
//Decrease Brightness
else if (*IRCommand == 0xA25DFF00){
if(RGBBrightness > 0){
RGBBrightness--;
bRGBColourChanged = true;
Serial.print("Decreasing Brightness: ");
Serial.println(RGBBrightness);
}
}
//Activate Rainbow Mode
else if (*IRCommand == 0xB748FF00){
Serial.println("Taste the Rainbow!");
bRGBRainbow = true;
bRGBBreathe = false;
bRGBBounce = false;
}
//Activate Breathe Mode
else if (*IRCommand == 0xB34CFF00){
Serial.println("Take a deep breathe!");
bRGBBreathe = true;
bRGBRainbow = false;
bRGBBounce = false;
}
//Activate Bounce mode
else if (*IRCommand == 0xE01FFF00){
Serial.println("Which is better, this or that?");
bRGBBreathe = false;
bRGBRainbow = false;
bRGBBounce = true;
}
return;
}
//The Rainbow wave function is based on code from https://pastebin.com/4V5xkXey
void rainbow_wave(uint8_t thisSpeed, uint8_t deltaHue) { // The fill_rainbow call doesn't support brightness levels.
uint8_t thisHue = beat8(thisSpeed,255); // A simple rainbow march.
fill_rainbow(leds, RGBLEDNum, thisHue, deltaHue); // Use FastLED's fill_rainbow routine.
FastLED.show();
}
float CalculateDelta(){
return DeltaMin + ((exp(sin(PulseSpeed * millis()/DeltaTime*PI)) - 0.36787944) * DeltaValue);
}
//This function will pulse the leds like the lights are breathing
//Inspired by https://github.com/marmilicious/FastLED_examples/blob/master/breath_effect_v2.ino
void UpdateRGBBreath(){
float BreathBrightness = CalculateDelta();
//Serial.print("Setting Brightness: ");
//Serial.println(BreathBrightness);
UpdateRGBLEDs(RGBColour.hue, RGBColour.sat, BreathBrightness, 0);
FastLED.show();
analogWrite(Fan, BreathBrightness);
}
/*
void UpdateRGBBounce(){
float delta = CalculateDelta();
uint8_t tempHue = map(delta, DeltaMin, DeltaMax, RGBColour.hue, RGBColour2.hue);
uint8_t tempSat = map(delta, DeltaMin, DeltaMax, RGBColour.sat, RGBColour2.sat);
UpdateRGBLEDs(tempHue, tempSat, DeltaMax, 0);
FastLED.show();
//analogWrite(Fan, delta);
}
*/
//Referenced from https://gist.github.com/atuline/b279fda278417f581773
void UpdateRGBBounce(){
uint8_t speed = beatsin8(6,0,255);
RGBColourTemp = blend(RGBColour, RGBColour2, speed);
UpdateRGBLEDs(RGBColourTemp.hue, RGBColourTemp.sat, DeltaMax, 0);
FastLED.show();
}
//Referenced from https://learn.adafruit.com/adafruit-microphone-amplifier-breakout/measuring-sound-levels
void CollectMicSample(){
unsigned long startMillis= millis(); // Start of sample window
unsigned int peakToPeak = 0; // peak-to-peak level
unsigned int signalMax = 0;
unsigned int signalMin = 1024;
// collect data for 50 mS
while (millis() - startMillis < MicSampleWindow)
{
sample = analogRead(MicInput);
if (sample < 1024) // toss out spurious readings
{
if (sample > signalMax)
{
signalMax = sample; // save just the max levels
}
else if (sample < signalMin)
{
signalMin = sample; // save just the min levels
}
}
}
peakToPeak = signalMax - signalMin; // max - min = peak-peak amplitude
volts = (peakToPeak * 5.0) / 1024; // convert to volts
//Serial.println(volts);
if(volts > MicSpeakingThreshold){
Serial.print("Audio Input Detected:");
Serial.println(volts);
bSpeaking = true;
LastSpeakingTime = millis();
UpdateRGBLEDsReverse(RGBSpeaking.hue, RGBSpeaking.sat, RGBSpeaking.val, RGBDelay/2);
analogWrite(Fan, SpeakingFanSpeed); // throttle the fans
digitalWrite(SpeakerControl, HIGH); //turn on the speakers
}
}
void setup() {
Serial.begin(9600); // Start a serial connection with the computer
irrecv.enableIRIn(); // start the receiver
FastLED.addLeds<WS2812, RGBLED, GRB>(leds, RGBLEDNum);
pinMode(IRCollision, INPUT); // Declare the IRCollision sensor as an input device
pinMode(Fan, OUTPUT); // declare the left fan pin for output control
analogWrite(Fan, FanSpeed); // initialize the fan speed to 0
pinMode(SpeakerControl, OUTPUT);
digitalWrite(SpeakerControl, LOW); //Put speakers on standby
//pinMode(IR, OUTPUT); // Declare the IR-LED as an output
//pinMode(WEARSWITCH, INPUT_PULLUP);
UpdateRGBLEDsReverse(RGBOff.hue, RGBOff.sat, RGBOff.val, RGBDelay);
//StopRGBLEDs(0);
}
void loop() {
// ***************************************************************
//Receive IR Input from remote
if(irrecv.decode())
{
//Serial.println(results.value, HEX);
//The function decode(&results)) is deprecated and may not work as expected! Just use decode() without a parameter and IrReceiver.decodedIRData.<fieldname> .
ProcessIRCommand();
irrecv.resume();
}
// ***************************************************************
// Check wear sensor for state change
CollisionCurrent = digitalRead(IRCollision);
// Check wear sensor for activation
if(CollisionCurrent == LOW && CollisionPrevious == HIGH){
bActive = true;
Serial.println("IR Collision - Setting bActive to true.");
}
// check wear sensor for standby
else if(CollisionCurrent == HIGH && CollisionPrevious == LOW){
bActive = false;
Serial.println("IR Collision - Setting bActive to false.");
}
CollisionPrevious = CollisionCurrent;
// ***************************************************************
// Active Functions
if(bActive){
if(!bMicMute){
CollectMicSample();
}
if(bSpeaking){
if(LastSpeakingTime + SpeakingTimeDelay < millis() || bMicMute){
Serial.println("Mic Standing by.");
bSpeaking = false;
analogWrite(Fan, FanMax);
digitalWrite(SpeakerControl, LOW);
UpdateRGBLEDs(RGBColour.hue, RGBColour.sat, RGBBrightness * 63.75, RGBDelay/2);
}
}
//check if the device just activated
else if(!bActivePrevious){
Serial.println("Activating the mask!");
analogWrite(Fan, FanMax);
UpdateRGBLEDs(RGBColour.hue, RGBColour.sat, RGBBrightness * 63.75, RGBDelay);
bActivePrevious = true; // set the previous activity to true so that we can skip the initialization steps
}else{
//the device can continue operating
if(bRGBColourChanged){
bRGBRainbow = false;
bRGBBreathe = false;
bRGBBounce = false;
analogWrite(Fan, FanMax);
UpdateRGBLEDs(RGBColour.hue, RGBColour.sat, RGBBrightness * 63.75, RGBDelay);
}
if(bRGBRainbow){
bRGBBreathe = false;
bRGBBounce = false;
analogWrite(Fan, FanMax);
rainbow_wave(10,10);
}
if(bRGBBreathe){
bRGBBounce - false;
bRGBRainbow = false;
UpdateRGBBreath();
}
if(bRGBBounce){
bRGBBreathe = false;
bRGBRainbow = false;
UpdateRGBBounce();
}
}
}
// ***************************************************************
// Standing by functions
else if(!bActive){
//check if the device just deactivated
if(bActivePrevious){
Serial.println("Entering Standby");
FanSpeed = 0;
analogWrite(Fan, FanSpeed);
UpdateRGBLEDsReverse(RGBOff.hue, RGBOff.sat, RGBOff.val, RGBDelay);
//StopRGBLEDs(RGBDelay);
bActivePrevious = false; // set the previous active state to false so we can skip adjustments.
}else{
//should add a short sleep here to reduce power usage.
}
}
/*
// ***************************************************************
// LED Level Wear Activation
digitalWrite(IR, HIGH); // Turn the IR-LED on
IRLevel = analogRead(PHOTORESISTOR);
Serial.println(IRLevel);
if(IRLevel > IRThreshold)
{
// mask should be active
Serial.println("Active Mode");
} else {
// mask should be in standby
Serial.println("Standby Mode");
}
*/
/*
// ***************************************************************
// Switch based Activation
if(digitalRead(WEARSWITCH) == LOW)
{
// mask should be active
Serial.println("Active Mode");
} else {
// mask should be in standby
Serial.println("Standby Mode");
}
*/
// ***************************************************************
// RGB LED
/*
leds[0]=RGBColour;
FastLED.show();
delay(500);
leds[1]=RGBColour;
FastLED.show();
delay(500);
leds[2]=RGBColour;
FastLED.show();
delay(500);
leds[3]=RGBColour;
FastLED.show();
delay(500);
*/
delay(100); // short delay between loops
}
What an awesome mask, when you are able I want to order one.