commit 62a5597ae07b6f2c3acda3bda09d19b28b07f8da Author: iGoX Date: Mon Dec 23 14:27:47 2024 +0100 Initial commit 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/3D-files-to-print/Busylight v2.f3d b/3D-files-to-print/Busylight v2.f3d new file mode 100644 index 0000000..8178d66 Binary files /dev/null and b/3D-files-to-print/Busylight v2.f3d differ diff --git a/3D-files-to-print/LED cover.stl b/3D-files-to-print/LED cover.stl new file mode 100644 index 0000000..b5d4c6a Binary files /dev/null and b/3D-files-to-print/LED cover.stl differ diff --git a/3D-files-to-print/LED support bottom.stl b/3D-files-to-print/LED support bottom.stl new file mode 100644 index 0000000..8819d45 Binary files /dev/null and b/3D-files-to-print/LED support bottom.stl differ diff --git a/3D-files-to-print/LED support top.stl b/3D-files-to-print/LED support top.stl new file mode 100644 index 0000000..da6783b Binary files /dev/null and b/3D-files-to-print/LED support top.stl differ diff --git a/3D-files-to-print/README.md b/3D-files-to-print/README.md new file mode 100644 index 0000000..4a0a30b --- /dev/null +++ b/3D-files-to-print/README.md @@ -0,0 +1,5 @@ +Loctite glue and hot glue gun are you friends :smile: + +All files can be printed in PLA. \ +To have a nice light diffusion, I printed the `LED cover.stl` file in [_vase mode_](https://all3dp.com/2/cura-vase-mode-all-you-need-to-know/). +![2D files](../img/busyligh-3d.png) \ No newline at end of file diff --git a/3D-files-to-print/Raiser.stl b/3D-files-to-print/Raiser.stl new file mode 100644 index 0000000..04179c6 Binary files /dev/null and b/3D-files-to-print/Raiser.stl differ diff --git a/3D-files-to-print/USB holder.stl b/3D-files-to-print/USB holder.stl new file mode 100644 index 0000000..270cab2 Binary files /dev/null and b/3D-files-to-print/USB holder.stl differ diff --git a/3D-files-to-print/USB plate.stl b/3D-files-to-print/USB plate.stl new file mode 100644 index 0000000..7bdb84a Binary files /dev/null and b/3D-files-to-print/USB plate.stl differ diff --git a/ESP32/boot.py b/ESP32/boot.py new file mode 100644 index 0000000..993205a --- /dev/null +++ b/ESP32/boot.py @@ -0,0 +1,34 @@ +# This file is executed on every boot (including wake-boot from deepsleep) +# It is executed after boot.py, before main.py +import os, machine +import network +import gc + +gc.collect() + +# Disable Access Point +ap_if = network.WLAN(network.AP_IF) +ap_if.active(False) + +# Connect to the WIFI when booting +SSID = '' # Set the WIFI network SSID +PASSWORD = '' # Set the WIFI network password +network.country('') # Set the country code for the WIFI (ISO 3166-1 Alpha-2 country code) +network.hostname('busylight-esp32') # Hostname that will identify this device on the network + +def boot_wifi_connect(): + wlan = network.WLAN(network.STA_IF) + if not wlan.isconnected(): + print('connecting to network...') + wlan.active(True) + wlan.connect(SSID, PASSWORD) + while not wlan.isconnected(): + pass + ip, mask, gateway, dns = wlan.ifconfig() + print('\nNetwork config:') + print('- IP address: ' + ip) + print('- Network mask: ' + mask) + print('- Network gateway: ' + gateway) + print('- DNS server: ' + dns + '\n') + +boot_wifi_connect() \ No newline at end of file diff --git a/ESP32/main.py b/ESP32/main.py new file mode 100644 index 0000000..faaf24b --- /dev/null +++ b/ESP32/main.py @@ -0,0 +1,219 @@ +from microdot import Microdot, send_file +import machine, sys, neopixel, time + +app = Microdot() + +# Difine NeoPixel object +nbPixels = 12*2 +pinPixelStrip = 16 # ESP32 D1 Mini +neoPixelStrip = neopixel.NeoPixel(machine.Pin(pinPixelStrip), nbPixels) + +# Define status colors +statusColors = { + 'BUSY': (255,0,0), # red + 'AVAILABLE': (0,255,0), # green + 'AWAY': (246,190,0), # cyan + 'OFF': (0, 0, 0), # off + 'ON': (255, 255, 255) # white + } + +# Store BusyLight default global status +blColor = statusColors.get('OFF') +blStatus = 'off' +blPreviousStatus='off' +blBrightness = 0.1 # Adjust the brightness (0.0 - 1.0) + +def __setColor(color): + r, g , b = color + r = int(r * blBrightness) + g = int(g * blBrightness) + b = int(b * blBrightness) + return (r, g, b) + +def __setBusyLightColor(color, brightness): + global blBrightness + blBrightness = brightness + global blColor + blColor = color + neoPixelStrip.fill(__setColor(color)) + neoPixelStrip.write() + + global blStatus + blStatus = 'colored' + +def __setBusyLightStatus(status): + status = status.upper() + color = statusColors.get(status) + __setBusyLightColor(color, blBrightness) + + global blStatus + blStatus = status.lower() + +# Microdot APP routes + +@app.get('/static/') +async def staticRoutes(request, path): + if '..' in path: + # directory traversal is not allowed + return {'error': '4040 Not found'}, 404 + return send_file('static/' + path) + +@app.get('/') +async def getIndex(request): + return send_file('static/index.html') + +@app.get('/api/brightness') +async def getBrightness(request): + return {'brightness': blBrightness} + +@app.post('/api/brightness') +async def setBrightness(request): + brightness = request.json.get("brightness") + + if brightness is None: + return {'error': 'missing brightness parameter'}, 400 + + if type(brightness) is float \ + or type(brightness) is int: + if brightness < 0 or brightness > 1: + return {'error': 'brigthness out of bound (0.0 - 1.0)'}, 400 + else: + return {'error': 'wrong brigthness type (float)'}, 400 + + # Save blStatus + global blStatus + status = blStatus + + # Apply new brightness to current color + color = blColor + __setBusyLightColor(color, brightness) + + # Restore global status + blStatus = status + + global blBrightness + blBrightness = brightness + + return {'brightness': blBrightness} + + +@app.post('/api/color') +async def setColor(request): + + r = request.json.get("r") + g = request.json.get("g") + b = request.json.get("b") + + if bool(r is None or g is None or b is None): + return {'error': 'missing color'}, 400 + else: + if type(r) is int \ + and type(g) is int \ + and type(b) is int: + color = (r, g, b) + else: + return {'error': 'wrong color type (int)'}, 400 + + if (r < 0 or r > 255) \ + or (g < 0 or g > 255) \ + or (b < 0 or b > 255): + return {'error': 'color out of bound (0 - 255)'}, 400 + + brightness = request.json.get("brightness") + + if not brightness is None: + if type(brightness) is float \ + or type(brightness) is int: + if brightness < 0 or brightness > 1: + return {'error': 'brightness out of bound (0.0 - 1.0)'}, 400 + else: + return {'error': 'wrong brightness type (float)'}, 400 + __setBusyLightColor(color, brightness) + + __setBusyLightColor(color, blBrightness) + + return {'status': blStatus} + +@app.route('/api/status/', methods=['GET', 'POST']) +async def setStatus(request, status): + lStatus = status.lower() + if lStatus == 'on': + __setBusyLightStatus('ON') + elif lStatus == 'off': + __setBusyLightStatus('OFF') + elif lStatus == 'available': + __setBusyLightStatus('AVAILABLE') + elif lStatus == 'away': + __setBusyLightStatus('AWAY') + elif lStatus == 'busy': + __setBusyLightStatus('BUSY') + else: + return {'error': 'unknown /api/status/' + lStatus + ' route'}, 404 + + return {'status': blStatus} + +@app.get('/api/color') +async def getColor(request): + r, g, b = neoPixelStrip.__getitem__(0) + return {'color': {'r': r, 'g': g, 'b': b}} + +@app.get('/api/status') +async def getStatus(request): + return {'status': blStatus} + +@app.get('/api/debug') +async def getDebugInfo(request): + r, g, b = blColor + dr, dg, db = neoPixelStrip.__getitem__(0) + return {'status': blStatus, 'brightness': blBrightness, 'color': {'r': r, 'g': g, 'b': b}, 'dimColor': {'r': dr, 'g': dg, 'b': db}} + +@app.post('/api/mutedeck-webhook') +async def mutedeckWebhook(request): + + if request.json.get('control') != 'system': + if request.json.get('call') == 'active': + if request.json.get('mute') == 'active': + isMuted = True + else: + isMuted = False + + if request.json.get('video') == 'active': + isVideoOn = True + else: + isVideoOn = False + + if isMuted: + __setBusyLightStatus('away') + else: + __setBusyLightStatus('busy') + else: + __setBusyLightStatus('available') + + return {'status': blStatus} + + +@app.post('/shutdown') +async def shutdown(request): + request.app.shutdown() + return 'The server is shutting down...' + +# Startup effect +def startUpSeq(): + print('Start seq begins') + __setBusyLightStatus('OFF') + time.sleep_ms(100) + __setBusyLightStatus('BUSY') + time.sleep_ms(200) + __setBusyLightStatus('AWAY') + time.sleep_ms(300) + __setBusyLightStatus('AVAILABLE') + time.sleep_ms(500) + __setBusyLightStatus('OFF') + print('Start seq is ended') + __setBusyLightColor(statusColors.get('OFF'), 0.4) + +startUpSeq() + +# Start API webserver +if __name__ == '__main__': + app.run(port=80, debug=True) \ No newline at end of file diff --git a/ESP32/static/busylight-api.js b/ESP32/static/busylight-api.js new file mode 100644 index 0000000..de96039 --- /dev/null +++ b/ESP32/static/busylight-api.js @@ -0,0 +1,70 @@ +async function setStatus(status = '') { + // Les options par défaut sont indiquées par * + const response = await fetch(`/api/status/${status}`, { + method: "POST", // *GET, POST, PUT, DELETE, etc. + cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached + headers: { + "Content-Type": "application/json" + } + }); + return response.json(); // transforme la réponse JSON reçue en objet JavaScript natif + } + +async function getStatus() { + // Les options par défaut sont indiquées par * + const response = await fetch(`/api/status`, { + method: "GET", // *GET, POST, PUT, DELETE, etc. + cache: "no-cache" // *default, no-cache, reload, force-cache, only-if-cached + }); + return response.json(); // transforme la réponse JSON reçue en objet JavaScript natif + } + +async function setColor(color = {}) { + // Les options par défaut sont indiquées par * + const response = await fetch(`/api/color`, { + method: "POST", // *GET, POST, PUT, DELETE, etc. + cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(color) // le type utilisé pour le corps doit correspondre à l'en-tête "Content-Type" + }); + return response.json(); // transforme la réponse JSON reçue en objet JavaScript natif +} + +async function getColor() { + // Les options par défaut sont indiquées par * + const response = await fetch(`/api/color`, { + method: "GET", // *GET, POST, PUT, DELETE, etc. + cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached + headers: { + "Content-Type": "application/json" + } + }); + return response.json(); // transforme la réponse JSON reçue en objet JavaScript natif +} + +async function setBrightness(brightness = {}) { + // Les options par défaut sont indiquées par * + const response = await fetch(`/api/brightness`, { + method: "POST", // *GET, POST, PUT, DELETE, etc. + cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(brightness) // le type utilisé pour le corps doit correspondre à l'en-tête "Content-Type" + }); + return response.json(); // transforme la réponse JSON reçue en objet JavaScript natif +} + +async function getBrightness() { + // Les options par défaut sont indiquées par * + const response = await fetch(`/api/brightness`, { + method: "GET", // *GET, POST, PUT, DELETE, etc. + cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached + headers: { + "Content-Type": "application/json" + } + }); + return response.json(); // transforme la réponse JSON reçue en objet JavaScript natif +} \ No newline at end of file diff --git a/ESP32/static/index.html b/ESP32/static/index.html new file mode 100644 index 0000000..d73884b --- /dev/null +++ b/ESP32/static/index.html @@ -0,0 +1,136 @@ + + + + BusyLight + + + + + +

Status

+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + + +
+ +
+ + +
+
+ + + + + \ No newline at end of file diff --git a/ESP32/static/jscolor.min.js b/ESP32/static/jscolor.min.js new file mode 100644 index 0000000..48dd2b1 --- /dev/null +++ b/ESP32/static/jscolor.min.js @@ -0,0 +1 @@ +(function(global,factory){"use strict";if(typeof module==="object"&&typeof module.exports==="object"){module.exports=global.document?factory(global):function(win){if(!win.document){throw new Error("jscolor needs a window with document")}return factory(win)};return}factory(global)})(typeof window!=="undefined"?window:this,function(window){"use strict";var jscolor=function(){var jsc={initialized:false,instances:[],readyQueue:[],register:function(){if(typeof window!=="undefined"&&window.document){if(window.document.readyState!=="loading"){jsc.pub.init()}else{window.document.addEventListener("DOMContentLoaded",jsc.pub.init,false)}}},installBySelector:function(selector,rootNode){rootNode=rootNode?jsc.node(rootNode):window.document;if(!rootNode){throw new Error("Missing root node")}var elms=rootNode.querySelectorAll(selector);var matchClass=new RegExp("(^|\\s)("+jsc.pub.lookupClass+")(\\s*(\\{[^}]*\\})|\\s|$)","i");for(var i=0;i-1},isButtonEmpty:function(el){switch(jsc.nodeName(el)){case"input":return!el.value||el.value.trim()==="";case"button":return el.textContent.trim()===""}return null},isPassiveEventSupported:function(){var supported=false;try{var opts=Object.defineProperty({},"passive",{get:function(){supported=true}});window.addEventListener("testPassive",null,opts);window.removeEventListener("testPassive",null,opts)}catch(e){}return supported}(),isColorAttrSupported:function(){var elm=window.document.createElement("input");if(elm.setAttribute){elm.setAttribute("type","color");if(elm.type.toLowerCase()=="color"){return true}}return false}(),dataProp:"_data_jscolor",setData:function(){var obj=arguments[0];if(arguments.length===3){var data=obj.hasOwnProperty(jsc.dataProp)?obj[jsc.dataProp]:obj[jsc.dataProp]={};var prop=arguments[1];var value=arguments[2];data[prop]=value;return true}else if(arguments.length===2&&typeof arguments[1]==="object"){var data=obj.hasOwnProperty(jsc.dataProp)?obj[jsc.dataProp]:obj[jsc.dataProp]={};var map=arguments[1];for(var prop in map){if(map.hasOwnProperty(prop)){data[prop]=map[prop]}}return true}throw new Error("Invalid arguments")},removeData:function(){var obj=arguments[0];if(!obj.hasOwnProperty(jsc.dataProp)){return true}for(var i=1;i=3&&(mR=par[0].match(re))&&(mG=par[1].match(re))&&(mB=par[2].match(re))){ret.format="rgb";ret.rgba=[parseFloat(mR[1])||0,parseFloat(mG[1])||0,parseFloat(mB[1])||0,null];if(par.length>=4&&(mA=par[3].match(re))){ret.format="rgba";ret.rgba[3]=parseFloat(mA[1])||0}return ret}}return false},parsePaletteValue:function(mixed){var vals=[];if(typeof mixed==="string"){mixed.replace(/#[0-9A-F]{3}\b|#[0-9A-F]{6}([0-9A-F]{2})?\b|rgba?\(([^)]*)\)/gi,function(val){vals.push(val)})}else if(Array.isArray(mixed)){vals=mixed}var colors=[];for(var i=0;ivs[a]?-vp[a]+tp[a]+ts[a]/2>vs[a]/2&&tp[a]+ts[a]-ps[a]>=0?tp[a]+ts[a]-ps[a]:tp[a]:tp[a],-vp[b]+tp[b]+ts[b]+ps[b]-l+l*c>vs[b]?-vp[b]+tp[b]+ts[b]/2>vs[b]/2&&tp[b]+ts[b]-l-l*c>=0?tp[b]+ts[b]-l-l*c:tp[b]+ts[b]-l+l*c:tp[b]+ts[b]-l+l*c>=0?tp[b]+ts[b]-l+l*c:tp[b]+ts[b]-l-l*c]}var x=pp[a];var y=pp[b];var positionValue=thisObj.fixed?"fixed":"absolute";var contractShadow=(pp[0]+ps[0]>tp[0]||pp[0]0?Math.ceil(sampleCount/cols):0;cellW=Math.max(1,Math.floor((width-(cols-1)*thisObj.paletteSpacing)/cols));cellH=thisObj.paletteHeight?Math.min(thisObj.paletteHeight,cellW):cellW}if(rows){height=rows*cellH+(rows-1)*thisObj.paletteSpacing}return{cols:cols,rows:rows,cellW:cellW,cellH:cellH,width:width,height:height}},getControlPadding:function(thisObj){return Math.max(thisObj.padding/2,2*thisObj.pointerBorderWidth+thisObj.pointerThickness-thisObj.controlBorderWidth)},getPadYChannel:function(thisObj){switch(thisObj.mode.charAt(1).toLowerCase()){case"v":return"v";break}return"s"},getSliderChannel:function(thisObj){if(thisObj.mode.length>2){switch(thisObj.mode.charAt(2).toLowerCase()){case"s":return"s";break;case"v":return"v";break}}return null},triggerCallback:function(thisObj,prop){if(!thisObj[prop]){return}var callback=null;if(typeof thisObj[prop]==="string"){try{callback=new Function(thisObj[prop])}catch(e){console.error(e)}}else{callback=thisObj[prop]}if(callback){callback.call(thisObj)}},triggerGlobal:function(eventNames){var inst=jsc.getInstances();for(var i=0;i0){for(var y=0;y=2&&typeof arguments[0]==="string"){try{if(!setOption(arguments[0],arguments[1])){return false}}catch(e){console.warn(e);return false}this.redraw();this.exposeColor();return true}else if(arguments.length===1&&typeof arguments[0]==="object"){var opts=arguments[0];var success=true;for(var opt in opts){if(opts.hasOwnProperty(opt)){try{if(!setOption(opt,opts[opt])){success=false}}catch(e){console.warn(e);success=false}}}this.redraw();this.exposeColor();return success}throw new Error("Invalid arguments")};this.channel=function(name,value){if(typeof name!=="string"){throw new Error("Invalid value for channel name: "+name)}if(value===undefined){if(!this.channels.hasOwnProperty(name.toLowerCase())){console.warn("Getting unknown channel: "+name);return false}return this.channels[name.toLowerCase()]}else{var res=false;switch(name.toLowerCase()){case"r":res=this.fromRGBA(value,null,null,null);break;case"g":res=this.fromRGBA(null,value,null,null);break;case"b":res=this.fromRGBA(null,null,value,null);break;case"h":res=this.fromHSVA(value,null,null,null);break;case"s":res=this.fromHSVA(null,value,null,null);break;case"v":res=this.fromHSVA(null,null,value,null);break;case"a":res=this.fromHSVA(null,null,null,value);break;default:console.warn("Setting unknown channel: "+name);return false}if(res){this.redraw();return true}}return false};this.trigger=function(eventNames){var evs=jsc.strList(eventNames);for(var i=0;i255/2};this.hide=function(){if(isPickerOwner()){detachPicker()}};this.show=function(){drawPicker()};this.redraw=function(){if(isPickerOwner()){drawPicker()}};this.getFormat=function(){return this._currentFormat};this._setFormat=function(format){this._currentFormat=format.toLowerCase()};this.hasAlphaChannel=function(){if(this.alphaChannel==="auto"){return this.format.toLowerCase()==="any"||jsc.isAlphaFormat(this.getFormat())||this.alpha!==undefined||this.alphaElement!==undefined}return this.alphaChannel};this.processValueInput=function(str){if(!this.fromString(str)){this.exposeColor()}};this.processAlphaInput=function(str){if(!this.fromHSVA(null,null,null,parseFloat(str))){this.exposeColor()}};this.exposeColor=function(flags){var colorStr=this.toString();var fmt=this.getFormat();jsc.setDataAttr(this.targetElement,"current-color",colorStr);if(!(flags&jsc.flags.leaveValue)&&this.valueElement){if(fmt==="hex"||fmt==="hexa"){if(!this.uppercase){colorStr=colorStr.toLowerCase()}if(!this.hash){colorStr=colorStr.replace(/^#/,"")}}this.setValueElementValue(colorStr)}if(!(flags&jsc.flags.leaveAlpha)&&this.alphaElement){var alphaVal=Math.round(this.channels.a*100)/100;this.setAlphaElementValue(alphaVal)}if(!(flags&jsc.flags.leavePreview)&&this.previewElement){var previewPos=null;if(jsc.isTextInput(this.previewElement)||jsc.isButton(this.previewElement)&&!jsc.isButtonEmpty(this.previewElement)){previewPos=this.previewPosition}this.setPreviewElementBg(this.toRGBAString())}if(isPickerOwner()){redrawPad();redrawSld();redrawASld()}};this.setPreviewElementBg=function(color){if(!this.previewElement){return}var position=null;var width=null;if(jsc.isTextInput(this.previewElement)||jsc.isButton(this.previewElement)&&!jsc.isButtonEmpty(this.previewElement)){position=this.previewPosition;width=this.previewSize}var backgrounds=[];if(!color){backgrounds.push({image:"none",position:"left top",size:"auto",repeat:"no-repeat",origin:"padding-box"})}else{backgrounds.push({image:jsc.genColorPreviewGradient(color,position,width?width-jsc.pub.previewSeparator.length:null),position:"left top",size:"auto",repeat:position?"repeat-y":"repeat",origin:"padding-box"});var preview=jsc.genColorPreviewCanvas("rgba(0,0,0,0)",position?{left:"right",right:"left"}[position]:null,width,true);backgrounds.push({image:"url('"+preview.canvas.toDataURL()+"')",position:(position||"left")+" top",size:preview.width+"px "+preview.height+"px",repeat:position?"repeat-y":"repeat",origin:"padding-box"})}var bg={image:[],position:[],size:[],repeat:[],origin:[]};for(var i=0;i=0;i-=1){var pres=presetsArr[i];if(!pres){continue}if(!jsc.pub.presets.hasOwnProperty(pres)){console.warn("Unknown preset: %s",pres);continue}for(var opt in jsc.pub.presets[pres]){if(jsc.pub.presets[pres].hasOwnProperty(opt)){try{setOption(opt,jsc.pub.presets[pres][opt])}catch(e){console.warn(e)}}}}var nonProperties=["preset"];for(var opt in opts){if(opts.hasOwnProperty(opt)){if(nonProperties.indexOf(opt)===-1){try{setOption(opt,opts[opt])}catch(e){console.warn(e)}}}}if(this.container===undefined){this.container=window.document.body}else{this.container=jsc.node(this.container)}if(!this.container){throw new Error("Cannot instantiate color picker without a container element")}this.targetElement=jsc.node(targetElement);if(!this.targetElement){if(typeof targetElement==="string"&&/^[a-zA-Z][\w:.-]*$/.test(targetElement)){var possiblyId=targetElement;throw new Error("If '"+possiblyId+"' is supposed to be an ID, please use '#"+possiblyId+"' or any valid CSS selector.")}throw new Error("Cannot instantiate color picker without a target element")}if(this.targetElement.jscolor&&this.targetElement.jscolor instanceof jsc.pub){throw new Error("Color picker already installed on this element")}this.targetElement.jscolor=this;jsc.addClass(this.targetElement,jsc.pub.className);jsc.instances.push(this);if(jsc.isButton(this.targetElement)){if(this.targetElement.type.toLowerCase()!=="button"){this.targetElement.type="button"}if(jsc.isButtonEmpty(this.targetElement)){jsc.removeChildren(this.targetElement);this.targetElement.appendChild(window.document.createTextNode(" "));var compStyle=jsc.getCompStyle(this.targetElement);var currMinWidth=parseFloat(compStyle["min-width"])||0;if(currMinWidth-1){var color=jsc.parseColorString(initValue);this._currentFormat=color?color.format:"hex"}else{this._currentFormat=this.format.toLowerCase()}this.processValueInput(initValue);if(initAlpha!==undefined){this.processAlphaInput(initAlpha)}if(this.random){this.randomize.apply(this,Array.isArray(this.random)?this.random:[])}}};jsc.pub.className="jscolor";jsc.pub.activeClassName="jscolor-active";jsc.pub.looseJSON=true;jsc.pub.presets={};jsc.pub.presets["default"]={};jsc.pub.presets["light"]={backgroundColor:"rgba(255,255,255,1)",controlBorderColor:"rgba(187,187,187,1)",buttonColor:"rgba(0,0,0,1)"};jsc.pub.presets["dark"]={backgroundColor:"rgba(51,51,51,1)",controlBorderColor:"rgba(153,153,153,1)",buttonColor:"rgba(240,240,240,1)"};jsc.pub.presets["small"]={width:101,height:101,padding:10,sliderSize:14,paletteCols:8};jsc.pub.presets["medium"]={width:181,height:101,padding:12,sliderSize:16,paletteCols:10};jsc.pub.presets["large"]={width:271,height:151,padding:12,sliderSize:24,paletteCols:15};jsc.pub.presets["thin"]={borderWidth:1,controlBorderWidth:1,pointerBorderWidth:1};jsc.pub.presets["thick"]={borderWidth:2,controlBorderWidth:2,pointerBorderWidth:2};jsc.pub.sliderInnerSpace=3;jsc.pub.chessboardSize=8;jsc.pub.chessboardColor1="#666666";jsc.pub.chessboardColor2="#999999";jsc.pub.previewSeparator=["rgba(255,255,255,.65)","rgba(128,128,128,.65)"];jsc.pub.init=function(){if(jsc.initialized){return}window.document.addEventListener("mousedown",jsc.onDocumentMouseDown,false);window.document.addEventListener("keyup",jsc.onDocumentKeyUp,false);window.addEventListener("resize",jsc.onWindowResize,false);window.addEventListener("scroll",jsc.onWindowScroll,false);jsc.appendDefaultCss();jsc.pub.install();jsc.initialized=true;while(jsc.readyQueue.length){var func=jsc.readyQueue.shift();func()}};jsc.pub.install=function(rootNode){var success=true;try{jsc.installBySelector("[data-jscolor]",rootNode)}catch(e){success=false;console.warn(e)}if(jsc.pub.lookupClass){try{jsc.installBySelector("input."+jsc.pub.lookupClass+", "+"button."+jsc.pub.lookupClass,rootNode)}catch(e){}}return success};jsc.pub.ready=function(func){if(typeof func!=="function"){console.warn("Passed value is not a function");return false}if(jsc.initialized){func()}else{jsc.readyQueue.push(func)}return true};jsc.pub.trigger=function(eventNames){var triggerNow=function(){jsc.triggerGlobal(eventNames)};if(jsc.initialized){triggerNow()}else{jsc.pub.ready(triggerNow)}};jsc.pub.hide=function(){if(jsc.picker&&jsc.picker.owner){jsc.picker.owner.hide()}};jsc.pub.chessboard=function(color){if(!color){color="rgba(0,0,0,0)"}var preview=jsc.genColorPreviewCanvas(color);return preview.canvas.toDataURL()};jsc.pub.background=function(color){var backgrounds=[];backgrounds.push(jsc.genColorPreviewGradient(color));var preview=jsc.genColorPreviewCanvas();backgrounds.push(["url('"+preview.canvas.toDataURL()+"')","left top","repeat"].join(" "));return backgrounds.join(", ")};jsc.pub.options={};jsc.pub.lookupClass="jscolor";jsc.pub.installByClassName=function(){console.error('jscolor.installByClassName() is DEPRECATED. Use data-jscolor="" attribute instead of a class name.'+jsc.docsRef);return false};jsc.register();return jsc.pub}();if(typeof window.jscolor==="undefined"){window.jscolor=window.JSColor=jscolor}return jscolor}); diff --git a/README.md b/README.md new file mode 100644 index 0000000..d3ae533 --- /dev/null +++ b/README.md @@ -0,0 +1,128 @@ +# What's this project? + +A simple to build, nice looking, portable DIY **Busy Light**. + +It comes with a with a rudimentary (and ugly) **Web UI** and a simple (but hopefully convenient) **Rest API**. + +![busylight](img/busylight.png) + +# Web UI +A very simplistic (and ugly) UI is available on port `80`. \ +Default hostname is `busylight-esp32`. + +You can try to reach the web UI @ `http://busylight-esp32.local`. + +| What an ugly UI ! :smile: | +|---------------------------| +| ![Web UI](img/web-ui.png) | + +# 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/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. | +| /api/status/available | POST / GET | n/a | Set the BusyLight in `available` mode. Green 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/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. | + +## JSON objects +### `color` object + +``` +{ + "r": 255, + "g": 0, + "b": 127, + "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] + +### `brightness` object + +``` +{ + "brightness": 0.5 +} +``` +`brightness`: LED brighness | float | [0.0 .. 1.0] + +### `status` object + +``` +{ + "status": "" +} +``` + +\ : `on` | `off` | `available` | `away` | `busy` | `colored` + + +# 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 | +|---------------------------------------------------| +| ![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) | +| 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) | + +# Installation + +**(1)** Flash your ESP32 with [Micropython](https://micropython.org/download/ESP32_GENERIC/). + +**(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): + +| Tools > Manage plug-ins | lib selection | +|-------------------------------|-------------------------------------| +| ![lib-menu](img/lib-menu.png) | ![lib-install](img/lib-install.png) | + +**(3)** Drop the content of [ESP32](ESP32/) folder to the root directory of your ESP32 file system. Again, can easily be done using [Thonny](https://thonny.org): + +![file-copy](img/file-copy.png) + +**Done!** + +# 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. + +# Wiring + +To be done + +# 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 diff --git a/img/busyligh-3d.png b/img/busyligh-3d.png new file mode 100644 index 0000000..7110e33 Binary files /dev/null and b/img/busyligh-3d.png differ diff --git a/img/busylight.png b/img/busylight.png new file mode 100644 index 0000000..7dc0c3f Binary files /dev/null and b/img/busylight.png differ diff --git a/img/file-copy.png b/img/file-copy.png new file mode 100644 index 0000000..d447c83 Binary files /dev/null and b/img/file-copy.png differ diff --git a/img/lib-install.png b/img/lib-install.png new file mode 100644 index 0000000..2c2b820 Binary files /dev/null and b/img/lib-install.png differ diff --git a/img/lib-menu.png b/img/lib-menu.png new file mode 100644 index 0000000..fccffb9 Binary files /dev/null and b/img/lib-menu.png differ diff --git a/img/mutedeck-webhook-conf.png b/img/mutedeck-webhook-conf.png new file mode 100644 index 0000000..10dfdc7 Binary files /dev/null and b/img/mutedeck-webhook-conf.png differ diff --git a/img/web-ui.png b/img/web-ui.png new file mode 100644 index 0000000..e5193f0 Binary files /dev/null and b/img/web-ui.png differ