├── best_friend_lamp_arduino.ino ├── config.h ├── doc ├── bflamp.gif └── inside.jpeg └── readme.md /best_friend_lamp_arduino.ino: -------------------------------------------------------------------------------- 1 | //import the libraries 2 | 3 | #include "config.h" 4 | #include 5 | #include 6 | 7 | #define N_LEDS 12 8 | #define LED_PIN 3 9 | #define BOT 4 // capacitive sensor pin 10 | 11 | //////////////////LAMP ID//////////////////////////////////////////////////////////// 12 | int lampID = 2; 13 | ///////////////////////////////////////////////////////////////////////////////////// 14 | 15 | NeoPixelBrightnessBus < NeoGrbFeature, NeoEsp8266Dma800KbpsMethod > strip(N_LEDS, LED_PIN); 16 | 17 | // Adafruit inicialization 18 | AdafruitIO_Feed * lamp = io.feed("Lampara"); // Change to your feed 19 | 20 | int recVal {0}; 21 | int sendVal {0}; 22 | 23 | const int max_intensity = 255; // Max intensity 24 | 25 | int selected_color = 0; // Index for color vector 26 | 27 | int i_breath; 28 | 29 | char msg[50]; // Custom messages for Adafruit IO 30 | 31 | // Color definitions 32 | RgbColor red(max_intensity, 0, 0); 33 | RgbColor green(0, max_intensity, 0); 34 | RgbColor blue(0, 0, max_intensity); 35 | RgbColor purple(200, 0, max_intensity); 36 | RgbColor cian(0, max_intensity, max_intensity); 37 | RgbColor yellow(max_intensity, 200, 0); 38 | RgbColor white(max_intensity, max_intensity, max_intensity); 39 | RgbColor pink(255, 20, 30); 40 | RgbColor orange(max_intensity, 50, 0); 41 | RgbColor black(0, 0, 0); 42 | 43 | RgbColor colors[] = { 44 | red, 45 | orange, 46 | yellow, 47 | green, 48 | cian, 49 | blue, 50 | purple, 51 | pink, 52 | white, 53 | black 54 | }; 55 | 56 | int state = 0; 57 | 58 | // Time vars 59 | unsigned long RefMillis; 60 | unsigned long ActMillis; 61 | const int send_selected_color_time = 4000; 62 | const int answer_time_out = 900000; 63 | const int on_time = 900000; 64 | 65 | // Disconection timeout 66 | unsigned long currentMillis; 67 | unsigned long previousMillis = 0; 68 | const unsigned long conection_time_out = 300000; // 5 minutos 69 | 70 | // Long press detection 71 | const int long_press_time = 2000; 72 | int lastState = LOW; // the previous state from the input pin 73 | int currentState; // the current reading from the input pin 74 | unsigned long pressedTime = 0; 75 | unsigned long releasedTime = 0; 76 | 77 | void setup() { 78 | //Start the serial monitor for debugging and status 79 | Serial.begin(115200); 80 | 81 | // Activate neopixels 82 | strip.Begin(); 83 | strip.Show(); // Initialize all pixels to 'off' 84 | 85 | wificonfig(); 86 | 87 | pinMode(BOT, INPUT); 88 | 89 | // Set ID values 90 | if (lampID == 1) { 91 | recVal = 20; 92 | sendVal = 10; 93 | } 94 | else if (lampID == 2) { 95 | recVal = 10; 96 | sendVal = 20; 97 | } 98 | 99 | //start connecting to Adafruit IO 100 | Serial.printf("\nConnecting to Adafruit IO with User: %s Key: %s.\n", IO_USERNAME, IO_KEY); 101 | AdafruitIO_WiFi io(IO_USERNAME, IO_KEY, "", ""); 102 | io.connect(); 103 | 104 | lamp -> onMessage(handle_message); 105 | 106 | while (io.status() < AIO_CONNECTED) { 107 | Serial.print("."); 108 | spin(6); 109 | delay(500); 110 | } 111 | turn_off(); 112 | Serial.println(); 113 | Serial.println(io.statusText()); 114 | // Animation 115 | spin(3); turn_off(); delay(50); 116 | flash(8); turn_off(); delay(100); 117 | flash(8); turn_off(); delay(50); 118 | 119 | //get the status of our value in Adafruit IO 120 | lamp -> get(); 121 | sprintf(msg, "L%d: connected", lampID); 122 | lamp -> save(msg); 123 | } 124 | 125 | void loop() { 126 | currentMillis = millis(); 127 | io.run(); 128 | // State machine 129 | switch (state) { 130 | // Wait 131 | case 0: 132 | currentState = digitalRead(BOT); 133 | if(lastState == LOW && currentState == HIGH) // Button is pressed 134 | { 135 | pressedTime = millis(); 136 | } 137 | else if(currentState == HIGH) { 138 | releasedTime = millis(); 139 | long pressDuration = releasedTime - pressedTime; 140 | if( pressDuration > long_press_time ) 141 | { 142 | state = 1; 143 | } 144 | } 145 | lastState = currentState; 146 | break; 147 | // Wait for button release 148 | case 1: 149 | selected_color = 0; 150 | light_half_intensity(selected_color); 151 | state = 2; 152 | RefMillis = millis(); 153 | while(digitalRead(BOT) == HIGH){} 154 | break; 155 | // Color selector 156 | case 2: 157 | if (digitalRead(BOT) == HIGH) { 158 | selected_color++; 159 | if (selected_color > 9) 160 | selected_color = 0; 161 | while (digitalRead(BOT) == HIGH) { 162 | delay(50); 163 | } 164 | light_half_intensity(selected_color); 165 | // Reset timer each time it is touched 166 | RefMillis = millis(); 167 | } 168 | // If a color is selected more than a time, it is interpreted as the one selected 169 | ActMillis = millis(); 170 | if (ActMillis - RefMillis > send_selected_color_time) { 171 | if (selected_color == 9) // Cancel msg 172 | state = 8; 173 | else 174 | state = 3; 175 | } 176 | break; 177 | // Publish msg 178 | case 3: 179 | sprintf(msg, "L%d: color send", lampID); 180 | lamp -> save(msg); 181 | lamp -> save(selected_color + sendVal); 182 | Serial.print(selected_color + sendVal); 183 | state = 4; 184 | flash(selected_color); 185 | light_half_intensity(selected_color); 186 | delay(100); 187 | flash(selected_color); 188 | light_half_intensity(selected_color); 189 | break; 190 | // Set timer 191 | case 4: 192 | RefMillis = millis(); 193 | state = 5; 194 | i_breath = 0; 195 | break; 196 | // Wait for answer 197 | case 5: 198 | for (i_breath = 0; i_breath <= 314; i_breath++) { 199 | breath(selected_color, i_breath); 200 | ActMillis = millis(); 201 | if (ActMillis - RefMillis > answer_time_out) { 202 | turn_off(); 203 | lamp -> save("L%d: Answer time out", lampID); 204 | lamp -> save(0); 205 | state = 8; 206 | break; 207 | } 208 | } 209 | break; 210 | // Answer received 211 | case 6: 212 | Serial.println("Answer received"); 213 | light_full_intensity(selected_color); 214 | RefMillis = millis(); 215 | sprintf(msg, "L%d: connected", lampID); 216 | lamp -> save(msg); 217 | lamp -> save(0); 218 | state = 7; 219 | break; 220 | // Turned on 221 | case 7: 222 | ActMillis = millis(); 223 | // Send pulse 224 | if (digitalRead(BOT) == HIGH) { 225 | lamp -> save(420 + sendVal); 226 | pulse(selected_color); 227 | } 228 | if (ActMillis - RefMillis > on_time) { 229 | turn_off(); 230 | lamp -> save(0); 231 | state = 8; 232 | } 233 | break; 234 | // Reset before state 0 235 | case 8: 236 | turn_off(); 237 | state = 0; 238 | break; 239 | // Msg received 240 | case 9: 241 | sprintf(msg, "L%d: msg received", lampID); 242 | lamp -> save(msg); 243 | RefMillis = millis(); 244 | state = 10; 245 | break; 246 | // Send answer wait 247 | case 10: 248 | for (i_breath = 236; i_breath <= 549; i_breath++) { 249 | breath(selected_color, i_breath); 250 | if (digitalRead(BOT) == HIGH) { 251 | state = 11; 252 | break; 253 | } 254 | ActMillis = millis(); 255 | if (ActMillis - RefMillis > answer_time_out) { 256 | turn_off(); 257 | sprintf(msg, "L%d: answer time out", lampID); 258 | lamp -> save(msg); 259 | state = 8; 260 | break; 261 | } 262 | } 263 | break; 264 | // Send answer 265 | case 11: 266 | light_full_intensity(selected_color); 267 | RefMillis = millis(); 268 | sprintf(msg, "L%d: answer sent", lampID); 269 | lamp -> save(msg); 270 | lamp -> save(1); 271 | state = 7; 272 | break; 273 | default: 274 | state = 0; 275 | break; 276 | } 277 | if ((currentMillis - previousMillis >= conection_time_out)) { 278 | if (WiFi.status() != WL_CONNECTED) 279 | ESP.restart(); 280 | previousMillis = currentMillis; 281 | } 282 | } 283 | 284 | //code that tells the ESP8266 what to do when it recieves new data from the Adafruit IO feed 285 | void handle_message(AdafruitIO_Data * data) { 286 | 287 | //convert the recieved data to an INT 288 | int reading = data -> toInt(); 289 | if (reading == 66) { 290 | sprintf(msg, "L%d: rebooting", lampID); 291 | lamp -> save(msg); 292 | lamp -> save(0); 293 | ESP.restart(); 294 | } else if (reading == 100) { 295 | sprintf(msg, "L%d: ping", lampID); 296 | lamp -> save(msg); 297 | lamp -> save(0); 298 | } else if (reading == 420 + recVal) { 299 | sprintf(msg, "L%d: pulse received", lampID); 300 | lamp -> save(msg); 301 | lamp -> save(0); 302 | pulse(selected_color); 303 | } else if (reading != 0 && reading / 10 != lampID) { 304 | // Is it a color msg? 305 | if (state == 0 && reading != 1) { 306 | state = 9; 307 | selected_color = reading - recVal; 308 | } 309 | // Is it an answer? 310 | if (state == 5 && reading == 1) 311 | state = 6; 312 | } 313 | } 314 | 315 | void turn_off() { 316 | strip.SetBrightness(max_intensity); 317 | for (int i = 0; i < N_LEDS; i++) { 318 | strip.SetPixelColor(i, black); 319 | } 320 | strip.Show(); 321 | } 322 | 323 | // 50% intensity 324 | void light_half_intensity(int ind) { 325 | strip.SetBrightness(max_intensity / 2); 326 | for (int i = 0; i < N_LEDS; i++) { 327 | strip.SetPixelColor(i, colors[ind]); 328 | } 329 | strip.Show(); 330 | } 331 | 332 | // 100% intensity 333 | void light_full_intensity(int ind) { 334 | strip.SetBrightness(max_intensity); 335 | for (int i = 0; i < N_LEDS; i++) { 336 | strip.SetPixelColor(i, colors[ind]); 337 | } 338 | strip.Show(); 339 | } 340 | 341 | void pulse(int ind) { 342 | int i; 343 | int i_step = 5; 344 | for (i = max_intensity; i > 80; i -= i_step) { 345 | strip.SetBrightness(i); 346 | for (int i = 0; i < N_LEDS; i++) { 347 | strip.SetPixelColor(i, colors[ind]); 348 | strip.Show(); 349 | delay(1); 350 | } 351 | } 352 | delay(20); 353 | for (i = 80; i < max_intensity; i += i_step) { 354 | strip.SetBrightness(i); 355 | for (int i = 0; i < N_LEDS; i++) { 356 | strip.SetPixelColor(i, colors[ind]); 357 | strip.Show(); 358 | delay(1); 359 | } 360 | } 361 | } 362 | 363 | //The code that creates the gradual color change animation in the Neopixels (thank you to Adafruit for this!!) 364 | void spin(int ind) { 365 | strip.SetBrightness(max_intensity); 366 | for (int i = 0; i < N_LEDS; i++) { 367 | strip.SetPixelColor(i, colors[ind]); 368 | strip.Show(); 369 | delay(30); 370 | } 371 | for (int i = 0; i < N_LEDS; i++) { 372 | strip.SetPixelColor(i, black); 373 | strip.Show(); 374 | delay(30); 375 | } 376 | } 377 | 378 | // Inspired by Jason Yandell 379 | void breath(int ind, int i) { 380 | float MaximumBrightness = max_intensity / 2; 381 | float SpeedFactor = 0.02; 382 | float intensity; 383 | if (state == 5) 384 | intensity = MaximumBrightness / 2.0 * (1 + cos(SpeedFactor * i)); 385 | else 386 | intensity = MaximumBrightness / 2.0 * (1 + sin(SpeedFactor * i)); 387 | strip.SetBrightness(intensity); 388 | for (int ledNumber = 0; ledNumber < N_LEDS; ledNumber++) { 389 | strip.SetPixelColor(ledNumber, colors[ind]); 390 | strip.Show(); 391 | delay(1); 392 | } 393 | } 394 | 395 | //code to flash the Neopixels when a stable connection to Adafruit IO is made 396 | void flash(int ind) { 397 | strip.SetBrightness(max_intensity); 398 | for (int i = 0; i < N_LEDS; i++) { 399 | strip.SetPixelColor(i, colors[ind]); 400 | } 401 | strip.Show(); 402 | 403 | delay(200); 404 | 405 | } 406 | 407 | // Waiting connection led setup 408 | void wait_connection() { 409 | strip.SetBrightness(max_intensity); 410 | for (int i = 0; i < 3; i++) { 411 | strip.SetPixelColor(i, yellow); 412 | } 413 | strip.Show(); 414 | delay(50); 415 | for (int i = 3; i < 6; i++) { 416 | strip.SetPixelColor(i, red); 417 | } 418 | strip.Show(); 419 | delay(50); 420 | for (int i = 6; i < 9; i++) { 421 | strip.SetPixelColor(i, blue); 422 | } 423 | strip.Show(); 424 | delay(50); 425 | for (int i = 9; i < 12; i++) { 426 | strip.SetPixelColor(i, green); 427 | } 428 | strip.Show(); 429 | delay(50); 430 | } 431 | 432 | void configModeCallback(WiFiManager * myWiFiManager) { 433 | Serial.println("Entered config mode"); 434 | wait_connection(); 435 | } 436 | 437 | void wificonfig() { 438 | WiFi.mode(WIFI_STA); 439 | WiFiManager wifiManager; 440 | 441 | std::vector menu = {"wifi","info"}; 442 | wifiManager.setMenu(menu); 443 | // set dark theme 444 | wifiManager.setClass("invert"); 445 | 446 | bool res; 447 | wifiManager.setAPCallback(configModeCallback); 448 | res = wifiManager.autoConnect("Lamp", "password"); // password protected ap 449 | 450 | if (!res) { 451 | spin(0); 452 | delay(50); 453 | turn_off(); 454 | } else { 455 | //if you get here you have connected to the WiFi 456 | spin(3); 457 | delay(50); 458 | turn_off(); 459 | } 460 | Serial.println("Ready"); 461 | Serial.print("IP address: "); 462 | Serial.println(WiFi.localIP()); 463 | } 464 | -------------------------------------------------------------------------------- /config.h: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | //get this info from the "My Key" section of the Adafruit IO website 5 | #define IO_USERNAME "" 6 | #define IO_KEY "" 7 | 8 | //Input your WiFi username and password here so the ESP8266 can connect to it 9 | #define WIFI_SSID "none" 10 | #define WIFI_PASS "none" 11 | 12 | 13 | 14 | // ***************************IGNORE ALL BELOW THIS********************************** 15 | 16 | 17 | 18 | 19 | #include "AdafruitIO_WiFi.h" 20 | 21 | #if defined(USE_AIRLIFT) || defined(ADAFRUIT_METRO_M4_AIRLIFT_LITE) || \ 22 | defined(ADAFRUIT_PYPORTAL) 23 | // Configure the pins used for the ESP32 connection 24 | #if !defined(SPIWIFI_SS) // if the wifi definition isnt in the board variant 25 | // Don't change the names of these #define's! they match the variant ones 26 | #define SPIWIFI SPI 27 | #define SPIWIFI_SS 10 // Chip select pin 28 | #define NINA_ACK 9 // a.k.a BUSY or READY pin 29 | #define NINA_RESETN 6 // Reset pin 30 | #define NINA_GPIO0 -1 // Not connected 31 | #endif 32 | AdafruitIO_WiFi io(IO_USERNAME, IO_KEY, WIFI_SSID, WIFI_PASS, SPIWIFI_SS, 33 | NINA_ACK, NINA_RESETN, NINA_GPIO0, &SPIWIFI); 34 | #else 35 | AdafruitIO_WiFi io(IO_USERNAME, IO_KEY, WIFI_SSID, WIFI_PASS); 36 | #endif 37 | -------------------------------------------------------------------------------- /doc/bflamp.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevJav/best_friend_lamp_arduino/11aab07df398f4ce0d712bfb37fc06233de593cc/doc/bflamp.gif -------------------------------------------------------------------------------- /doc/inside.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevJav/best_friend_lamp_arduino/11aab07df398f4ce0d712bfb37fc06233de593cc/doc/inside.jpeg -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 | # Arduino Best Friend Lamps 3 | 4 | ![lamp-gif](doc/bflamp.gif) 5 | 6 | 7 | This project objective is to make a cheap, open source version of what's commomnly called a best friend/friendship/long distance lamp, 8 | like [this one](https://www.friendlamps.com/) or [this one](https://bit.ly/3GN3ZJa). 9 | 10 | The idea is having two (or more) lamps connected so as when one of them is turned on, the others will turn on. 11 | 12 | This project is a modification and ampliation of [CoronaLamps](https://www.instructables.com/CoronaLamps-Simple-Friendship-Lamps-Anyone-Can-Mak/), 13 | so thanks to the authors for making it public. 14 | 15 | ## Material used (per lamp) 16 | - Microcontroller esp8266 17 | - NeoPixel Ring 12 LEDs 18 | - TTP223 touch sensor 19 | - [IKEA TOKABO lamp](https://www.ikea.com/es/es/p/tokabo-lampara-mesa-vidrio-blanco-opalo-40357998/) 20 | - Male micro USB to male USB 21 | - Welder, glue, wires... 22 | 23 | These components could be easily replaced for por example another microcontroller like ESP32, different LED type (but directonal) 24 | or even a different lamp. 25 | 26 | All together looks like this: 27 | 28 | drawing 29 | 30 | The touch sensor has a wire connected so it will cast less shadows inside the lamp, however this might be what makes the sensor trigger by itself sometimes :( 31 | 32 | ## Adafruit IO setup 33 | [Adafruit IO](https://io.adafruit.com/) is a free use server that we will use to communicate the lamps. 34 | You must create an account and create a new feed with whatever name you want. This created feed is where the messages from the 35 | lamps will be published. 36 | ## Code setup 37 | First of all, we need to have [Arduino IDE](https://www.arduino.cc/en/software) installed and the ESP8266 configuration added. 38 | You can follow [this tutorial](https://randomnerdtutorials.com/how-to-install-esp8266-board-arduino-ide/) to achieve that. 39 | 40 | We also need to install some libraries: 41 | - NeoPixelBus: to control de LED ring. 42 | - WifiManager: used to create a WiFi connection to set the WiFi id and password. 43 | - Adafruit IO Arduino: this library provides the tools for connecting to Adafruit IO. Once installed, we must make some changes in "AdafruitIO_ESP8266.cpp" in order to 44 | be able to use with WifiManager. Those changes are provided by [wyojustin](https://github.com/wyojustin) in 45 | [this comment](https://github.com/tzapu/WiFiManager/issues/243#issuecomment-364804188) (so thanks to him too). 46 | 47 | #### Changes in best_friend_lamp_arduino.ino: 48 | - Change the defines: 49 | ``` 50 | #define N_LEDS // number of leds used 51 | #define LED_PIN // pin connected to leds, must be an RX pin 52 | #define BOT // pin connected to capacitive sensor 53 | ``` 54 | - Change lamp id, one lamp must be se to 1 and the other to 2: 55 | ``` 56 | int lampID = 1; 57 | ``` 58 | - Change adafruit feed name to the one you created before: 59 | ``` 60 | AdafruitIO_Feed * lamp = io.feed("your_feed"); 61 | ``` 62 | 63 | #### Changes in config.h: 64 | Here we will add our login credentials to Adafruit IO. In the web page, go to 65 | "My Key" and copy the Arduino section. Then it should be paste on the config file: 66 | ``` 67 | #define IO_USERNAME "user" 68 | #define IO_KEY "key" 69 | ``` 70 | 71 | After making this changes, the program can be uploaded to both lamps, just remember 72 | to change the lampID for each one. 73 | 74 | ## User manual 75 | The first time the lamp is connected, or if the Wifi has changed, the lamp will light 76 | 4 different colors, indicating that it is not connected to internet. The micro will 77 | create a Wifi connection called "Lamp" and with password "password" (can be 78 | changed if you're afraid to be hacked). Connect to it (from your 79 | phone for example) and navigate to "192.168.4.1". From there, you can 80 | configure the connection. 81 | 82 | Once the internet setup is done, it will connect to Adafruit IO. During this process 83 | light will spin on green, and when its done, it will flash. 84 | 85 | Now it's ready to use. To send a message, touch the lamp for 2 seconds and it will light 86 | up on red. Touch again and again and the color will change. When you decide the one you 87 | like, wait some time and after a flash, the message is sent. Now, both lamps will make a 88 | breath animation until the one who received the message is touched (considered an answer) 89 | and will turn on at 100% for 15 minutes, or until 15 minutes have passed without response 90 | and it will turn off. 91 | 92 | ## Known issues 93 | - In my experience with TTP223 touch sensor, it may start tiggering randomly by itself. I haven't found a way to fix this apart from incresing the time needed to recognise the first touch. 94 | --------------------------------------------------------------------------------