Add blink feature
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
.DS_Store
|
||||||
|
**/Bruno/
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
from microdot import Microdot, send_file
|
from microdot import Microdot, send_file
|
||||||
import machine, sys, neopixel, time
|
import machine, sys, neopixel, time, asyncio
|
||||||
|
|
||||||
app = Microdot()
|
app = Microdot()
|
||||||
|
|
||||||
@@ -22,6 +22,10 @@ blColor = statusColors.get('OFF')
|
|||||||
blStatus = 'off'
|
blStatus = 'off'
|
||||||
blPreviousStatus='off'
|
blPreviousStatus='off'
|
||||||
blBrightness = 0.1 # Adjust the brightness (0.0 - 1.0)
|
blBrightness = 0.1 # Adjust the brightness (0.0 - 1.0)
|
||||||
|
blBlinking = False
|
||||||
|
blBlinkFrequency = 0
|
||||||
|
blBlinkDuration = 0
|
||||||
|
blBlinkStartTime = 0
|
||||||
|
|
||||||
def __setColor(color):
|
def __setColor(color):
|
||||||
r, g , b = color
|
r, g , b = color
|
||||||
@@ -152,6 +156,82 @@ async def setStatus(request, status):
|
|||||||
|
|
||||||
return {'status': blStatus}
|
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')
|
@app.get('/api/color')
|
||||||
async def getColor(request):
|
async def getColor(request):
|
||||||
r, g, b = neoPixelStrip.__getitem__(0)
|
r, g, b = neoPixelStrip.__getitem__(0)
|
||||||
|
|||||||
155
README.md
155
README.md
@@ -1,16 +1,16 @@
|
|||||||
# Table of Content
|
# Table of Content
|
||||||
|
|
||||||
1. [What's this project?](#whats-this-project)
|
1. [What's this project?](#user-content-whats-this-project)
|
||||||
2. [Web UI](#web-ui)
|
2. [Web UI](#user-content-web-ui)
|
||||||
3. [Stream Deck plug-in](#stream-deck-plug-in)
|
3. [BusyLight Buddy companion app](#user-content-busylight-buddy-companion-app)
|
||||||
4. [BusyLight API](#busylight-api)
|
4. [Stream Deck plug-in](#user-content-stream-deck-plug-in)
|
||||||
5. [MuteDeck integration](#mutedeck-integration)
|
5. [BusyLight API](#user-content-busylight-api)
|
||||||
6. [Electronic parts](#electronic-parts)
|
6. [MuteDeck integration](#user-content-mutedeck-integration)
|
||||||
7. [Firmware installation](#firmware-installation)
|
7. [Electronic parts](#user-content-electronic-parts)
|
||||||
8. [3D files - Enclosure](#3d-files---enclosure)
|
8. [Firmware installation](#user-content-firmware-installation)
|
||||||
9. [Wiring / Soldering](#wiring--soldering)
|
9. [3D files - Enclosure](#user-content-3d-files---enclosure)
|
||||||
10. [Tools & libs](#tools--libs)
|
10. [Wiring / Soldering](#user-content-wiring--soldering)
|
||||||
|
11. [Tools & libs](#user-content-tools--libs)
|
||||||
|
|
||||||
# What's this project?
|
# 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**.
|
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 |
|
| Controlled by Stream Deck with REST API | Light roll |
|
||||||
|-------------------------------------------------|---------------------------------------|
|
| --- | --- |
|
||||||
|  |  |
|
|  |  |
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
# Web UI
|
# 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`.
|
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 @ <http://igox-busylight.local>.
|
||||||
|
|
||||||
| What an neat UI ! :smile: |
|
| What an neat UI ! 😄 |
|
||||||
|---------------------------|
|
| --- |
|
||||||
|  |
|
|  |
|
||||||
|
|
||||||
|
# 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
|
# Stream Deck plug-in
|
||||||
|
|
||||||
You can download a Stream Deck plugin to control your BusyLight:
|
You can download a Stream Deck plugin to control your BusyLight:
|
||||||
|
|
||||||
[](https://marketplace.elgato.com/product/igox-busylight-7448a0be-6dd6-4711-ba0d-86c52b9075b9)
|
|
||||||
|
|
||||||
|
|
||||||
Or directly from [here](streamdeck-plugin/README.md).
|
Or directly from [here](streamdeck-plugin/README.md).
|
||||||
|
|
||||||
# BusyLight API
|
# BusyLight API
|
||||||
|
|
||||||
## End points
|
## End points
|
||||||
|
|
||||||
| Path | Method | Parameter | Description |
|
| 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 | 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 | 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/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. |
|
| /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/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/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/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/status | GET | n/a | Retreive the current BusyLight status. Return a `status` object. |
|
||||||
| /api/debug | GET | n/a | Retreive the full BusyLight status. |
|
| /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
|
## JSON objects
|
||||||
|
|
||||||
### `color` object
|
### `color` object
|
||||||
|
|
||||||
```
|
```json
|
||||||
{
|
{
|
||||||
"r": 255,
|
"r": 255,
|
||||||
"g": 0,
|
"g": 0,
|
||||||
@@ -71,65 +82,87 @@ Or directly from [here](streamdeck-plugin/README.md).
|
|||||||
"brightness": 0.5
|
"brightness": 0.5
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
`r`: RED color | integer | [0 .. 255]\
|
|
||||||
`g`: GREEN color | integer | [0 .. 255]\
|
`r`: RED color | integer | [0 .. 255]
|
||||||
`b`: BLUE color | integer | [0 .. 255]\
|
`g`: GREEN color | integer | [0 .. 255]
|
||||||
`brightness`: LED brighness (optional) | float | [0.0 .. 1.0]
|
`b`: BLUE color | integer | [0 .. 255]
|
||||||
|
`brightness`: LED brightness (optional) | float | [0.0 .. 1.0]
|
||||||
|
|
||||||
### `brightness` object
|
### `brightness` object
|
||||||
|
|
||||||
```
|
```json
|
||||||
{
|
{
|
||||||
"brightness": 0.5
|
"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
|
### `status` object
|
||||||
|
|
||||||
```
|
```json
|
||||||
{
|
{
|
||||||
"status": "<STATUS>"
|
"status": "<STATUS>"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
\<STATUS\> : `on` | `off` | `available` | `away` | `busy` | `colored`
|
`<STATUS>` : `on` | `off` | `available` | `away` | `busy` | `colored` | `blinking`
|
||||||
|
|
||||||
|
|
||||||
# MuteDeck integration
|
# 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.
|
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:
|
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 |
|
||||||
|
| --- |
|
||||||
|  |
|
|  |
|
||||||
|
|
||||||
# Electronic parts
|
# Electronic parts
|
||||||
| Parts | Links (Amazon - not affiliated) |
|
|
||||||
|-------------------------|-----------------------------------------------------------------------------------------------------------|
|
| 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) |
|
| Micro-controler | [D1 ESP32 Mini NodeMCU](https://www.amazon.fr/dp/B0CDXB48DZ) - USB C version |
|
||||||
| Battery | [Anker PowerCore 5000mAh](https://www.amazon.fr/dp/B01CU1EC6Y) |
|
| Led rings (x2) | [AZDelivery 5 x LED Ring 5V RGB compatible avec WS2812B 12-Bit 38mm](https://www.amazon.fr/dp/B07V1GGKHV) |
|
||||||
| USB A to USB C adapter | [USB A to USB C adapter](https://www.amazon.fr/dp/B0BYK917NM) |
|
| 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
|
# 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 |
|
||||||
|-------------------------------|-------------------------------------|
|
| --- | --- |
|
||||||
|  |  |
|
|  |  |
|
||||||
|
|
||||||
|
> ⚠️ **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.
|
**(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):
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -137,7 +170,7 @@ It will automatically switch to the BusyLight in:
|
|||||||
|
|
||||||
# 3D files - Enclosure
|
# 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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -150,13 +183,17 @@ You can see a final assembly image [here](3D-files-to-print/README.md).
|
|||||||
# Tools & libs
|
# Tools & libs
|
||||||
|
|
||||||
## Thonny
|
## Thonny
|
||||||
https://thonny.org
|
|
||||||
|
<https://thonny.org>
|
||||||
|
|
||||||
## Micropython
|
## Micropython
|
||||||
https://micropython.org
|
|
||||||
|
<https://micropython.org>
|
||||||
|
|
||||||
## Microdot
|
## Microdot
|
||||||
https://microdot.readthedocs.io/en/latest/index.html
|
|
||||||
|
<https://microdot.readthedocs.io/en/latest/index.html>
|
||||||
|
|
||||||
## JSColor
|
## JSColor
|
||||||
https://jscolor.com
|
|
||||||
|
<https://jscolor.com>
|
||||||
Reference in New Issue
Block a user