From 4bcda62d9758f990f4376dcf968b1ca0f5bfcced Mon Sep 17 00:00:00 2001 From: iGoX Date: Sat, 21 Mar 2026 05:57:45 +0100 Subject: [PATCH] Add blink feature --- .gitignore | 2 + ESP32/main.py | 82 +++++++++++++++++++++++++- README.md | 155 +++++++++++++++++++++++++++++++------------------- 3 files changed, 179 insertions(+), 60 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..28c8c57 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.DS_Store +**/Bruno/ \ No newline at end of file diff --git a/ESP32/main.py b/ESP32/main.py index 1433e87..89bd3fb 100644 --- a/ESP32/main.py +++ b/ESP32/main.py @@ -1,5 +1,5 @@ from microdot import Microdot, send_file -import machine, sys, neopixel, time +import machine, sys, neopixel, time, asyncio app = Microdot() @@ -22,6 +22,10 @@ blColor = statusColors.get('OFF') blStatus = 'off' blPreviousStatus='off' blBrightness = 0.1 # Adjust the brightness (0.0 - 1.0) +blBlinking = False +blBlinkFrequency = 0 +blBlinkDuration = 0 +blBlinkStartTime = 0 def __setColor(color): r, g , b = color @@ -152,6 +156,82 @@ async def setStatus(request, status): return {'status': blStatus} +async def __blinkTask(color, brightness, frequency, duration): + global blBlinking, blStatus, blColor, blBrightness + blBlinking = True + interval = 1.0 / (frequency * 2) # half period in seconds + elapsed = 0.0 + while blBlinking: + neoPixelStrip.fill(__setColor(color)) + neoPixelStrip.write() + await asyncio.sleep(interval) + neoPixelStrip.fill((0, 0, 0)) + neoPixelStrip.write() + await asyncio.sleep(interval) + if duration > 0: + elapsed += interval * 2 + if elapsed >= duration: + break + # Restore previous state + blBlinking = False + __setBusyLightColor(color, brightness) + blStatus = blPreviousStatus + +@app.post('/api/blink') +async def setBlink(request): + frequency = request.json.get('frequency') + duration = request.json.get('duration') + + if frequency is None or duration is None: + return {'error': 'missing frequency or duration parameter'}, 400 + + if not isinstance(frequency, int) or frequency <= 0: + return {'error': 'frequency must be a positive integer (Hz)'}, 400 + + if not (isinstance(duration, (int, float))) or duration < 0: + return {'error': 'duration must be a positive float in seconds (0 = endless)'}, 400 + + # Save current state + global blPreviousStatus, blBlinking, blBlinkFrequency, blBlinkDuration, blBlinkStartTime + blPreviousStatus = blStatus + savedColor = blColor + savedBrightness = blBrightness + + # Stop any ongoing blink + blBlinking = False + await asyncio.sleep(0) + + # Save blink params + blBlinkFrequency = frequency + blBlinkDuration = duration + blBlinkStartTime = time.ticks_ms() + + # Launch blink task + asyncio.create_task(__blinkTask(savedColor, savedBrightness, frequency, duration)) + + return {'status': 'blinking', 'frequency': frequency, 'duration': duration} + +@app.get('/api/blink') +async def getBlink(request): + if blBlinking and blBlinkDuration > 0: + elapsed = time.ticks_diff(time.ticks_ms(), blBlinkStartTime) / 1000.0 + remains = max(0.0, blBlinkDuration - elapsed) + else: + remains = 0.0 + + return { + 'isblinking': blBlinking, + 'frequency': blBlinkFrequency, + 'duration': blBlinkDuration, + 'remains': remains + } + +@app.post('/api/blink/stop') +async def blinkStop(request): + global blBlinking + blBlinking = False + return {'status': blStatus} + @app.get('/api/color') async def getColor(request): r, g, b = neoPixelStrip.__getitem__(0) diff --git a/README.md b/README.md index 15ee8e6..888c385 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,16 @@ # Table of Content -1. [What's this project?](#whats-this-project) -2. [Web UI](#web-ui) -3. [Stream Deck plug-in](#stream-deck-plug-in) -4. [BusyLight API](#busylight-api) -5. [MuteDeck integration](#mutedeck-integration) -6. [Electronic parts](#electronic-parts) -7. [Firmware installation](#firmware-installation) -8. [3D files - Enclosure](#3d-files---enclosure) -9. [Wiring / Soldering](#wiring--soldering) -10. [Tools & libs](#tools--libs) - +1. [What's this project?](#user-content-whats-this-project) +2. [Web UI](#user-content-web-ui) +3. [BusyLight Buddy companion app](#user-content-busylight-buddy-companion-app) +4. [Stream Deck plug-in](#user-content-stream-deck-plug-in) +5. [BusyLight API](#user-content-busylight-api) +6. [MuteDeck integration](#user-content-mutedeck-integration) +7. [Electronic parts](#user-content-electronic-parts) +8. [Firmware installation](#user-content-firmware-installation) +9. [3D files - Enclosure](#user-content-3d-files---enclosure) +10. [Wiring / Soldering](#user-content-wiring--soldering) +11. [Tools & libs](#user-content-tools--libs) # What's this project? @@ -20,36 +20,43 @@ A cheap, simple to build, nice looking and portable DIY **Busy Light**. It comes with a with a simplistic but neat **Web UI** and a simple (but hopefully convenient) **Rest API**. -| Controlled by Stream Deck with REST API | Light roll | -|-------------------------------------------------|---------------------------------------| -| ![busylight and stream deck](img/busylight.jpg) | ![busylight roll](img/busylight.gif) | - -![showoff video](img/busylight-showoff.mp4) +| Controlled by Stream Deck with REST API | Light roll | +| --- | --- | +| ![busylight and stream deck](img/busylight.jpg) | ![busylight roll](img/busylight.gif) | # Web UI -A very simplistic but neat UI is available on port `80` (thanks @nicolaeser). \ + +A very simplistic but neat UI is available on port `80` (thanks @nicolaeser). Default hostname is `igox-busylight`. -You can try to reach the web UI @ [http://igox-busylight.local](http://igox-busylight.local). +You can try to reach the web UI @ . -| What an neat UI ! :smile: | -|---------------------------| +| What an neat UI ! 😄 | +| --- | | ![Web UI](img/web-ui.png) | +# BusyLight Buddy companion app + +[BusyLight Buddy](https://code.igox.org/iGoX/busylight-buddy) is a free, open-source multiplatform companion app to control your BusyLight from your phone or computer — no browser needed. + +Available for **iOS**, **iPadOS**, **Android**, **macOS**, and **Windows**. + +It features quick status presets, a custom color picker with saveable presets, a brightness slider, and background polling to keep the UI in sync with the device. + # Stream Deck plug-in + You can download a Stream Deck plugin to control your BusyLight: -[![get it on marketplace](img/elgato-marketplace.png "Get iGoX BusyLight on Marketplace")](https://marketplace.elgato.com/product/igox-busylight-7448a0be-6dd6-4711-ba0d-86c52b9075b9) - - Or directly from [here](streamdeck-plugin/README.md). # BusyLight API + ## End points + | Path | Method | Parameter | Description | -|--------|------|-----------|-------------| +| --- | --- | --- | --- | | /api/color | POST | `color` JSON object | Set the BusyLight color according to the `color` object passed in the request body. Return a `status` object. | -| /api/color | GET | n/a | Retreive the color currently displyed by the BusyLight. Return a `color` object. | +| /api/color | GET | n/a | Retreive the color currently displayed by the BusyLight. Return a `color` object. | | /api/brightness | POST | `brightness` JSON object | Set the BusyLight brightness according to the `brightness` object passed in the request body. Return a `status` object. | | /api/brightness | GET | n/a | Retreive the BusyLight brightness. Return a `brightness` object. | | /api/status/on | POST / GET | n/a | Light up the BusyLight. White color. Return a `status` object. | @@ -57,13 +64,17 @@ Or directly from [here](streamdeck-plugin/README.md). | /api/status/away | POST / GET | n/a | Set the BusyLight in `away` mode. Yellow color. Return a `status` object. | | /api/status/busy | POST / GET | n/a | Set the BusyLight in `busy` mode. Red color. Return a `status` object. | | /api/status/off | POST / GET | n/a | Shutdown the BusyLight. Return a `status` object. | -| /api/status | GET | n/a | Retreive the current BusyLight status. Return a `status` object. | -| /api/debug | GET | n/a | Retreive the full BusyLight status. | +| /api/status | GET | n/a | Retreive the current BusyLight status. Return a `status` object. | +| /api/blink | POST | `blink` JSON object | Make the BusyLight blink. Preserves current color, status and brightness. Return a `status` object. | +| /api/blink | GET | n/a | Retrieve the current BusyLight blink status. Return a `blink` object. | +| /api/blink/stop | POST | n/a | Stop the BusyLight blinking and restore previous state. Return a `status` object. | +| /api/debug | GET | n/a | Retreive the full BusyLight status. | ## JSON objects + ### `color` object -``` +```json { "r": 255, "g": 0, @@ -71,65 +82,87 @@ Or directly from [here](streamdeck-plugin/README.md). "brightness": 0.5 } ``` -`r`: RED color | integer | [0 .. 255]\ -`g`: GREEN color | integer | [0 .. 255]\ -`b`: BLUE color | integer | [0 .. 255]\ -`brightness`: LED brighness (optional) | float | [0.0 .. 1.0] + +`r`: RED color | integer | [0 .. 255] +`g`: GREEN color | integer | [0 .. 255] +`b`: BLUE color | integer | [0 .. 255] +`brightness`: LED brightness (optional) | float | [0.0 .. 1.0] ### `brightness` object -``` +```json { "brightness": 0.5 } ``` -`brightness`: LED brighness | float | [0.0 .. 1.0] + +`brightness`: LED brightness | float | [0.0 .. 1.0] + +### `blink` object + +```json +{ + "isblinking": true, + "frequency": 2, + "duration": 5.0, + "remains": 3.2 +} +``` + +`isblinking`: whether the BusyLight is currently blinking | boolean +`frequency`: blink frequency | integer | Hz +`duration`: total blink duration | float | seconds | 0 = endless +`remains`: remaining blink time | float | seconds | 0 if endless ### `status` object -``` +```json { "status": "" } ``` -\ : `on` | `off` | `available` | `away` | `busy` | `colored` - +`` : `on` | `off` | `available` | `away` | `busy` | `colored` | `blinking` # MuteDeck integration + The `POST api/mutedeck-webhook` endpoint aims to collect [MuteDeck](https://mutedeck.com/help/docs/notifications.html#enabling-the-webhook-integration) webhook callbacks. It will automatically switch to the BusyLight in: -- busy mode (Red color) when entering a meeting with Mic `ON` **and** camera `ON`. -- away mode (Yellow color) when entering a meeting with Mic `OFF` **and** camera `ON`. -- away mode (Yellow color) if the mic is muted during a meeting. -- available mode (Green color) when exiting/closing a meeting. -| MuteDeck Configuration | -|---------------------------------------------------| +* busy mode (Red color) when entering a meeting with Mic `ON` **and** camera `ON`. +* away mode (Yellow color) when entering a meeting with Mic `OFF` **and** camera `ON`. +* away mode (Yellow color) if the mic is muted during a meeting. +* available mode (Green color) when exiting/closing a meeting. + +| MuteDeck Configuration | +| --- | | ![MuteDeck config](img/mutedeck-webhook-conf.png) | # Electronic parts -| Parts | Links (Amazon - not affiliated) | -|-------------------------|-----------------------------------------------------------------------------------------------------------| -| Micro-controler | [D1 ESP32 Mini NodeMCU](https://www.amazon.fr/dp/B0CDXB48DZ) - USB C version | -| Led rings (x2) | [AZDelivery 5 x LED Ring 5V RGB compatible avec WS2812B 12-Bit 38mm](https://www.amazon.fr/dp/B07V1GGKHV) | -| Battery | [Anker PowerCore 5000mAh](https://www.amazon.fr/dp/B01CU1EC6Y) | -| USB A to USB C adapter | [USB A to USB C adapter](https://www.amazon.fr/dp/B0BYK917NM) | + +| Parts | Links (Amazon - not affiliated) | +| --- | --- | +| Micro-controler | [D1 ESP32 Mini NodeMCU](https://www.amazon.fr/dp/B0CDXB48DZ) - USB C version | +| Led rings (x2) | [AZDelivery 5 x LED Ring 5V RGB compatible avec WS2812B 12-Bit 38mm](https://www.amazon.fr/dp/B07V1GGKHV) | +| Battery | [Anker PowerCore 5000mAh](https://www.amazon.fr/dp/B01CU1EC6Y) | +| USB A to USB C adapter | [USB A to USB C adapter](https://www.amazon.fr/dp/B0BYK917NM) | # Firmware installation -**(1)** Flash your ESP32 with [Micropython](https://micropython.org/download/ESP32_GENERIC/). +**(1)** Flash your ESP32 with [Micropython](https://micropython.org/download/ESP32_GENERIC/) **v1.24.1**. -**(2)** Install [microdot](https://microdot.readthedocs.io/en/latest/index.html) library on the ESP32. This can easily be done using [Thonny](https://thonny.org): +**(2)** Install [microdot](https://microdot.readthedocs.io/en/latest/index.html) **v2.0.6** library on the ESP32. This can easily be done using [Thonny](https://thonny.org): -| Tools > Manage plug-ins | lib selection | -|-------------------------------|-------------------------------------| +| Tools > Manage plug-ins | lib selection | +| --- | --- | | ![lib-menu](img/lib-menu.png) | ![lib-install](img/lib-install.png) | +> ⚠️ **Compatibility note:** MicroPython v1.27 is **not compatible** with Microdot due to breaking changes in the underlying ESP-IDF v5.5. Use MicroPython **v1.24.1** with Microdot **v2.0.6**. + **(3)** Edit the `WIFI Configuration` section in the [boot.py](ESP32/boot.py) file. -**(4)** Copy the content of [ESP32](ESP32/) folder with the modified `boot.py` file to the root of your ESP32 file system. Again, can easily be done using [Thonny](https://thonny.org): +**(4)** Copy the content of [ESP32](ESP32) folder with the modified `boot.py` file to the root of your ESP32 file system. Again, can easily be done using [Thonny](https://thonny.org): ![file-copy](img/file-copy.png) @@ -137,7 +170,7 @@ It will automatically switch to the BusyLight in: # 3D files - Enclosure -All the required 3D files (STLs and f3d project) to 3D print the enclosure are available in the [3D-files-to-print](3D-files-to-print/) folder. +All the required 3D files (STLs and f3d project) to 3D print the enclosure are available in the [3D-files-to-print](3D-files-to-print) folder. ![3D files](3D-files-to-print/img/busylight-3d.jpg) @@ -150,13 +183,17 @@ You can see a final assembly image [here](3D-files-to-print/README.md). # Tools & libs ## Thonny -https://thonny.org + + ## Micropython -https://micropython.org + + ## Microdot -https://microdot.readthedocs.io/en/latest/index.html + + ## JSColor -https://jscolor.com + + \ No newline at end of file -- 2.49.1