diff --git a/.gitignore b/.gitignore index 894a44c..b4929a5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,10 @@ +.DS_Store + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class - +.vscode/ # C extensions *.so diff --git a/README.md b/README.md index 0968eb1..8130ec9 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,11 @@ - [Schematic](#schematic) - [Setup](#setup) - [Quick Start](#quick-start) +- [REST API Reference](#rest-api-reference) - [Documentation](#documentation) - [Additional Resources](#additional-resources) - [Tested On](#tested-on) -- [Credits](#credits) +- [Special Thanks](#special-thanks) ## Features @@ -24,14 +25,18 @@ - Up and running in just three lines of code! - User-friendly interface hosted from the micro-controller itself! - Complete control over animations from delay time, color, brightness -- Completely customizable animations and user interface which is written in just Python/HTML/CSS/JS! -- Use with the user interface or just use the Animations API +- Completely customizable animations and user interface which is written with just Python/HTML/CSS/JS! +- Use with the user interface or programmatically using the Animations API +- Call animations from the network! - Support for optional separate status indicator LED ### Out of the Box Animations: - Rainbow + - Rainbow Chase - Bounce + - Sparkle + - Wipe - Chase - RGB Fade - Alternating Colors @@ -44,6 +49,7 @@ ## Changelog | Release | Changes | Date | | :-----: | :-------------------------------------------------------------------------- | :-------: | +| v2.0 | | 12/31/2020 | | v1.2 | | 3/22/2020 | | v1.1 | | 9/2/2019 | | v1.0 | | 8/12/2019 | @@ -137,6 +143,78 @@ pixels.randomFill(ms=150, color=None) # random fill animation with 150ms delay a #### See the docs below for usage of all the μPixels animations! +----- + +## REST API Reference + +After running `uPixels.startServer()`, the following routes will be available at the address and port set when uPixels was initialized(Default: 0.0.0.0:8000). + +### `GET /` + +#### *Response* + +- Returns uPixels user interface(works best on a mobile browser) +- Add this page to your phone's homescreen using Chrome(Android) or Safari(iOS) for an app-like experience([tutorial](https://www.howtogeek.com/196087/how-to-add-websites-to-the-home-screen-on-any-smartphone-or-tablet/))! +### `POST /execute` + +- Run animations from the Animations API and other methods via a POST request to this route from any device connected on the same network as your microcontroller. +- All animations from the Animations API can be called from here as well as the `setStrip`, `setSegment`, and `clear` methods. +- BEWARE of infinite loop animations. Once you start them, they can't be stopped unless you do a hard reset! + +#### *Parameters* +- This route takes a JSON body with an `action` and `params` to be passed to the `action` +- *Required:* + - `action` - (string) name of function/animation to be run(name must be same as method names in documentation) + - `params` - (object) named params to be passed to the function(if no params, pass an empty object) + - When color is needed, pass a `color` object with `r`, `g`, `b` values, such as + ```JSON + { + ... + "color" : { + "r": 100, + "g": 100, + "b": 100, + } + } + - NOTE: For the `altColors` animation, pass a `firstColor` and `secondColor` object + +*Ex: To run the `rainbow` animation(w/ params), send a JSON body like this:* +```JSON +{ + "action": "rainbow", + "params": { + "ms": 10, + "iterations": 1 + } +} +``` + +*Ex: To run `setStrip`, which takes a color, send a JSON body like this:* +```JSON +{ + "action": "setStrip", + "params": { + "color": { + "r": 255, + "g": 50, + "b": 50 + } + } +} +``` + +*Ex: To run `clear`, which takes no params, send a JSON body like this:* +```JSON +{ + "action": "clear", + "params": {} +} +``` + +#### *Response* +- On success, the response will have a `200` status with no body. +- On error, the response will have a `400` status and will return an error message. Check the MicroPython WebREPL for a more detailed error message! + ## Documentation ### Objects @@ -227,13 +305,14 @@ Serves the UI using the uWeb server on specified address and port ----- -## `uPixels.chase(ms=20, color=None, direction='right')` +## `uPixels.chase(ms=20, color=None, segment_length=5, direction='right')` ###### Description Chase animation going left or right ###### Parameters - ms - (int) delay time in milliseconds. Default: 20 - color - (tuple) RGB color for animation in the format (r, g, b). Default: None(random color) + - segment_length - (int) number of LEDs to be used. Default: 5 - direction - (str) direction of animation; 'left' or 'right'. Default: 'right' ----- @@ -308,7 +387,7 @@ Serves the UI using the uWeb server on specified address and port ----- -## `uPixels.rainbow(ms=20, iterations = 2)` +## `uPixels.rainbow(ms=20, iterations=2)` ###### Description Cycle of colors in rainbow over entire strip @@ -327,6 +406,26 @@ Serves the UI using the uWeb server on specified address and port ----- +## `uPixels.wipe(ms=20, color=None)` + + ###### Description + Wipe animation + ###### Parameters + - ms - (int) delay time in milliseconds. Default: 20 + - color - (tuple) RGB color for animation in the format (r, g, b). Default: None(random color) + +----- + +## `uPixels.sparkle(ms=10, color=None)` + + ###### Description + Sparkle animation + ###### Parameters + - ms - (int) delay time in milliseconds. Default: 10 + - color - (tuple) RGB color for animation in the format (r, g, b). Default: None(random color) + +----- + ## `uPixels.clear()` ###### Description @@ -336,13 +435,22 @@ Serves the UI using the uWeb server on specified address and port ## Helper Methods +## `uPixels.setStrip(color)` + + ###### Description + Set entire strip to a color + ###### Parameters + - color - (tuple) RGB color in the format (r, g, b). + +----- + ## `uPixels.setSegment(segment_of_leds, color)` ###### Description Set specified segments of LEDS to a color ###### Parameters - segment_of_leds - (list) positions of each individual LED to be set(Ex: `[1, 4, 10]` will set LEDS @ index 1, 4, and 10 to the color). - - color - (tuple) RGB color for animation in the format (r, g, b). + - color - (tuple) RGB color in the format (r, g, b). ----- @@ -388,7 +496,7 @@ Serves the UI using the uWeb server on specified address and port - NodeMCU v3 (ESP8266) - WS2812b Individually Addressable RGB LEDs -## Credits +## Special Thanks - MaterializeCSS - Google Material Design Icons - Spectrum.js diff --git a/images/colors-screen.png b/images/colors-screen.png index 5c02cb7..9fdf33c 100644 Binary files a/images/colors-screen.png and b/images/colors-screen.png differ diff --git a/images/screenshot.png b/images/screenshot.png index 183999f..b2ff0f6 100644 Binary files a/images/screenshot.png and b/images/screenshot.png differ diff --git a/uPixels/uPixels.css b/uPixels/uPixels.css index 80c2655..35f5053 100644 --- a/uPixels/uPixels.css +++ b/uPixels/uPixels.css @@ -82,7 +82,7 @@ nav i { } .tabs-content.carousel .carousel-item { - height: 500px !important; + height: 100% !important; } .options { @@ -117,7 +117,7 @@ nav i { } .tabs-content.carousel.carousel-slider { - height: 600px !important; + height: 100% !important; overflow: auto; } @@ -141,4 +141,4 @@ nav i { text-align: center; margin: auto; width: 190px; -} \ No newline at end of file +} diff --git a/uPixels/uPixels.html b/uPixels/uPixels.html index 0c94275..dc9ecf4 100644 --- a/uPixels/uPixels.html +++ b/uPixels/uPixels.html @@ -2,7 +2,8 @@ uPixels - + @@ -16,8 +17,10 @@ - - + + @@ -27,7 +30,8 @@ @@ -40,13 +44,13 @@
Color Picker:

@@ -61,16 +65,20 @@
Brightness: 50%
Delay: 10 ms
- clearclear strip + clearclear strip
@@ -78,16 +86,41 @@
Delay: 10 ms
  • looksRainbow
    +
  • +
  • +
    looksRainbow Chase
    +
  • call_missedBounce
    +
  • +
  • +
    auto_graphSparkle
    + +
  • +
  • +
    read_moreWipe
    +
  • @@ -108,13 +141,15 @@
    Delay: 10 ms

  • - play_arrow + play_arrow
  • tollRGB Fade
  • @@ -122,37 +157,43 @@
    Delay: 10 ms
    Select Second Color:

    - play_arrow + play_arrow
  • blur_onRandom Fill
  • codeFill from Middle
  • compare_arrowsFill from Sides
  • fast_forwardFill Strip
  • star_halfChristmas
  • @@ -160,9 +201,9 @@
    Select Second Color:
    - + - + @@ -201,55 +242,54 @@
    Select Second Color:
    Set strip to color picker selection:
    + play_arrow
    -
    -

    info_outlineAbout

    - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Device Name:{{name}}
    uPixels Version:{{upixels_ver}}
    MicroPython Version:{{mp_ver}}
    IP Address:{{ip}}
    App Hosted at:{{host}}
    Number of LEDS:{{num}}
    +
    +
    +

    info_outlineAbout

    + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Device Name:{{name}}
    uPixels Version:{{upixels_ver}}
    MicroPython Version:{{mp_ver}}
    IP Address:{{ip}}
    App Hosted at:{{host}}
    Number of LEDS:{{num}}
    -
    Credits
    - -

    code by Philip Z (petabite)

    -
    +
    Credits
    + +

    code by Philip Z (petabite)

    + - \ No newline at end of file diff --git a/uPixels/uPixels.js b/uPixels/uPixels.js index 0e53f3c..e400b80 100644 --- a/uPixels/uPixels.js +++ b/uPixels/uPixels.js @@ -1,5 +1,5 @@ var brightnessSlider, delaySlider, startingPositionSlider, segmentLengthSlider -$(document).ready(function() { +$(document).ready(function () { $("#colorpicker").spectrum({ color: "rgb(0, 255, 155)", preferredFormat: 'rgb', @@ -12,14 +12,14 @@ $(document).ready(function() { showButtons: false, showInput: true, containerClassName: 'second-colorpicker', - change: function(color) { + change: function (color) { console.log($(this).spectrum('get').toRgb()); } }); - $('.color-buttons').children().each(function() { + $('.color-buttons').children().each(function () { color_array = $(this).data('color') - $(this).css('background-color', 'rgb('+ color_array[0] + ',' + color_array[1] + ','+ color_array[2] + ')') + $(this).css('background-color', 'rgb(' + color_array[0] + ',' + color_array[1] + ',' + color_array[2] + ')') $(this).click(function () { color_array = $(this).data('color') color = { @@ -27,7 +27,7 @@ $(document).ready(function() { 'g': Math.round(color_array[1] * getBrightness()), 'b': Math.round(color_array[2] * getBrightness()) } - setStripToColor(color) + setStrip(color) }) }) @@ -50,7 +50,7 @@ $(document).ready(function() { }) }); - delaySlider.noUiSlider.on('update', function(delay) { + delaySlider.noUiSlider.on('update', function (delay) { $('#delay-label').text(delay); }) @@ -68,11 +68,11 @@ $(document).ready(function() { }) }); - brightnessSlider.noUiSlider.on('update', function(brightness) { + brightnessSlider.noUiSlider.on('update', function (brightness) { $('#brightness-label').text(brightness); }) - $('#u-logo').on('click touchstart', function() { + $('#u-logo').on('click touchstart', function () { location.reload() }) }); @@ -136,7 +136,10 @@ function execute(action, params = {}) { var xhr = new XMLHttpRequest(); xhr.open("POST", '/execute', true); xhr.setRequestHeader("Content-Type", "application/json"); - xhr.send(JSON.stringify({'action': action, 'params': params})); + xhr.send(JSON.stringify({ + 'action': action, + 'params': params + })); } function rainbow() { @@ -146,6 +149,12 @@ function rainbow() { }) } +function rainbowChase() { + execute('rainbowChase', { + 'ms': getDelaySelection(), + }) +} + function bounce() { execute('bounce', { 'ms': getDelaySelection(), @@ -153,6 +162,20 @@ function bounce() { }) } +function sparkle() { + execute('sparkle', { + 'ms': getDelaySelection(), + 'color': getColorSelection() + }) +} + +function wipe() { + execute('wipe', { + 'ms': getDelaySelection(), + 'color': getColorSelection() + }) +} + function chase() { if ($('.chase #left').is(":checked")) { direction = 'left' @@ -217,14 +240,21 @@ function fillStrip() { function christmas() { execute('altColors', { 'ms': 300, - 'firstColor': {'r': 0,'g': 255,'b': 0}, - 'secondColor': {'r': 255,'g': 13,'b': 13} + 'firstColor': { + 'r': 0, + 'g': 255, + 'b': 0 + }, + 'secondColor': { + 'r': 255, + 'g': 13, + 'b': 13 + } }) } -function setStripToColor(color) { - execute('setSegment', { - "segment_of_leds": Array.from(Array(num_leds).keys()), +function setStrip(color) { + execute('setStrip', { "color": color }) } diff --git a/uPixels/uPixels.py b/uPixels/uPixels.py index d1cae0d..a6eefbd 100644 --- a/uPixels/uPixels.py +++ b/uPixels/uPixels.py @@ -1,8 +1,10 @@ -import machine, uos, network, neopixel, time, urandom, ntptime +import machine, uos, network, neopixel, time, urandom, sys from uWeb import uWeb, loadJSON + class uPixels: - VERSION = '1.2' + VERSION = "2.0" + def __init__(self, pin, num_leds, address="0.0.0.0", port=8000): self.device_name = uos.uname()[0] self.pin = machine.Pin(pin, machine.Pin.OUT) # configure pin for leds @@ -10,17 +12,21 @@ def __init__(self, pin, num_leds, address="0.0.0.0", port=8000): self.address = address self.port = port self.animation_map = { - 'rainbow': self.rainbow, - 'bounce': self.bounce, - 'chase': self.chase, - 'rgbFade': self.rgbFade, - 'altColors': self.altColors, - 'randomFill': self.randomFill, - 'fillFromMiddle': self.fillFromMiddle, - 'fillFromSides': self.fillFromSides, - 'fillStrip': self.fillStrip, - 'setSegment': self.setSegment, - 'clear': self.clear + "rainbow": self.rainbow, + "rainbowChase": self.rainbowChase, + "bounce": self.bounce, + "chase": self.chase, + "rgbFade": self.rgbFade, + "altColors": self.altColors, + "randomFill": self.randomFill, + "fillFromMiddle": self.fillFromMiddle, + "fillFromSides": self.fillFromSides, + "fillStrip": self.fillStrip, + "wipe": self.wipe, + "sparkle": self.sparkle, + "setStrip": self.setStrip, + "setSegment": self.setSegment, + "clear": self.clear, } self.statusLED = 5 self.startupAnimation() @@ -31,39 +37,61 @@ def setDeviceName(self, name): # web server methods def startServer(self): self.server = uWeb(self.address, self.port) - self.server.routes({ - (uWeb.GET, "/"): self.app, - (uWeb.POST, '/execute'): self.execute - }) + self.server.routes( + {(uWeb.GET, "/"): self.app, (uWeb.POST, "/execute"): self.execute} + ) self.toggleServerStatusLED() self.server.start() def app(self): vars = { - 'name' : self.device_name, - 'upixels_ver': self.VERSION, - 'mp_ver': uos.uname()[3], - 'ip': network.WLAN(network.STA_IF).ifconfig()[0], - 'host': network.WLAN(network.STA_IF).ifconfig()[0]+":"+str(self.server.port), - 'num': self.np.n + "name": self.device_name, + "upixels_ver": self.VERSION, + "mp_ver": uos.uname()[3], + "ip": network.WLAN(network.STA_IF).ifconfig()[0], + "host": network.WLAN(network.STA_IF).ifconfig()[0] + + ":" + + str(self.server.port), + "num": self.np.n, } - self.server.render('uPixels.html', variables=vars) + self.server.render("uPixels.html", layout=False, variables=vars) def execute(self): - query = loadJSON(self.server.request_body) - action = query["action"] - params = query["params"] - if 'color' in params.keys(): - if params['color'] != None: - params['color'] = (params['color']['r'], params['color']['g'], params['color']['b']) - if 'firstColor' in params.keys(): - if params['firstColor'] != None: - params['firstColor'] = (params['firstColor']['r'], params['firstColor']['g'], params['firstColor']['b']) - if 'secondColor' in params.keys(): - if params['secondColor'] != None: - params['secondColor'] = (params['secondColor']['r'], params['secondColor']['g'], params['secondColor']['b']) - print("passing ", params) - self.animation_map[action](**params) # call the animcation method + try: + query = loadJSON(self.server.request_body) + action = query["action"] + params = query["params"] + if action not in self.animation_map.keys(): + self.server.sendStatus(self.server.BAD_REQUEST) + self.server.sendBody(b"%s is not a valid action!" % (action)) + return + if "color" in params.keys(): + if params["color"] != None: + params["color"] = ( + params["color"]["r"], + params["color"]["g"], + params["color"]["b"], + ) + if "firstColor" in params.keys(): + if params["firstColor"] != None: + params["firstColor"] = ( + params["firstColor"]["r"], + params["firstColor"]["g"], + params["firstColor"]["b"], + ) + if "secondColor" in params.keys(): + if params["secondColor"] != None: + params["secondColor"] = ( + params["secondColor"]["r"], + params["secondColor"]["g"], + params["secondColor"]["b"], + ) + self.server.sendStatus(self.server.OK) + self.animation_map[action](**params) # call the animation method + except Exception as e: + self.server.sendStatus(self.server.BAD_REQUEST) + self.server.sendBody(b"An error occurred: %s!" % (str(e))) + sys.print_exception(e) def setStatusLED(self, pin): self.statusLED = pin @@ -75,22 +103,28 @@ def toggleServerStatusLED(self, status=1): # animation methods def startupAnimation(self): - self.chase(color=(0, 255, 155), direction='right') - self.chase(color=(0, 255, 155), direction='left') + self.chase(ms=5, color=(0, 255, 155), direction="right") + self.chase(ms=5, color=(0, 255, 155), direction="left") self.clear() - def chase(self, ms=20, color=None, direction='right'): + def chase(self, ms=20, color=None, segment_length=5, direction="right"): if color == None: color = self.randColor() - if direction == 'right': - led_iter = range(self.np.n) + if direction == "right": + led_iter = range(self.np.n - segment_length - 2) else: - led_iter = range(self.np.n - 1, -1, -1) + led_iter = range(self.np.n - segment_length - 2, -1, -1) for i in led_iter: - self.np[i] = color + for j in range(segment_length): + self.np[i + j] = color self.np.write() time.sleep_ms(ms) - self.np[i] = (0,0,0) + if direction == "right": + clear_iter = range(i, i + segment_length + 1) + else: + clear_iter = range(i + segment_length + 1, i, -1) + for i in clear_iter: + self.np[i] = (0, 0, 0) def fillStrip(self, ms=25, color=None): if color == None: @@ -101,8 +135,8 @@ def fillStrip(self, ms=25, color=None): self.np[i] = color self.np.write() time.sleep_ms(ms) - if i != count-1: - self.np[i] = (0,0,0) + if i != count - 1: + self.np[i] = (0, 0, 0) count -= 1 def fillFromMiddle(self, ms=40, color=None): @@ -133,11 +167,12 @@ def fillFromSides(self, ms=40, color=None): counter += 1 def randomFill(self, ms=150, color=None): - random_positions = [] - while len(random_positions) < self.np.n: - random_pos = self.randInt(0, self.np.n) - if random_pos not in random_positions: - random_positions.append(random_pos) + random_positions = list(range(self.np.n)) + for position in random_positions: + rand_i = self.randInt(0, self.np.n) + temp = position + position = random_positions[rand_i] + random_positions[rand_i] = temp for position in random_positions: if color == None: self.np[position] = self.randColor() @@ -147,7 +182,6 @@ def randomFill(self, ms=150, color=None): time.sleep_ms(ms) def altColors(self, ms=125, firstColor=None, secondColor=None): - ntptime.settime() if firstColor == None: color = self.randColor() if secondColor == None: @@ -174,52 +208,79 @@ def altColors(self, ms=125, firstColor=None, secondColor=None): def bounce(self, ms=20, color=False): while True: if color == False: - self.chase(ms, self.randColor(), 'right') - self.chase(ms, self.randColor(), 'left') + self.chase(ms=ms, color=self.randColor(), direction="right") + self.chase(ms=ms, color=self.randColor(), direction="left") else: - self.chase(ms, color, 'right') - self.chase(ms, color, 'left') + self.chase(ms=ms, color=color, direction="right") + self.chase(ms=ms, color=color, direction="left") def rgbFade(self, ms=20): for channel in range(3): for v in range(256): if channel == 0: - self.setSegment(list(range(self.np.n)), (v,0,0)) + self.setStrip((v, 0, 0)) if channel == 1: - self.setSegment(list(range(self.np.n)), (0,v,0)) + self.setStrip((0, v, 0)) if channel == 2: - self.setSegment(list(range(self.np.n)), (0,0,v)) + self.setStrip((0, 0, v)) time.sleep_ms(ms) - for v in range(255,-1,-1): + for v in range(255, -1, -1): if channel == 0: - self.setSegment(list(range(self.np.n)), (v,0,0)) + self.setStrip((v, 0, 0)) if channel == 1: - self.setSegment(list(range(self.np.n)), (0,v,0)) + self.setStrip((0, v, 0)) if channel == 2: - self.setSegment(list(range(self.np.n)), (0,0,v)) + self.setStrip((0, 0, v)) time.sleep_ms(ms) - def rainbow(self, ms=20, iterations = 2): - for j in range(256*iterations): + def rainbow(self, ms=20, iterations=2): + for j in range(256 * iterations): for i in range(self.np.n): self.np[i] = self.wheel(((i * 256 // self.np.n) + j) & 255) self.np.write() time.sleep_ms(ms) def rainbowChase(self, ms=50): - for j in range(256): - for q in range(3): - for i in range(0, self.np.n, 3): - self.np[i+q] = self.wheel((i+j) % 255) + for i in range(5): + for j in range(256): + for q in range(3): + for i in range(0, self.np.n, 3): + self.np[i + q] = self.wheel((i + j) % 255) + self.np.write() + time.sleep_ms(ms) + for i in range(0, self.np.n, 3): + self.np[i + q] = (0, 0, 0) + + def wipe(self, ms=20, color=None): + if color == None: + color = self.randColor() + while True: + for i in range(self.np.n): + self.np[i] = color + self.np.write() + time.sleep_ms(ms) + for i in range(self.np.n): + self.np[i] = (0, 0, 0) self.np.write() time.sleep_ms(ms) - for i in range(0, self.np.n, 3): - self.np[i+q] = (0, 0, 0) + + def sparkle(self, ms=10, color=None): + if color == None: + color = self.randColor() + while True: + i = self.randInt(0, self.np.n) + self.np[i] = color + self.np.write() + time.sleep_ms(ms) + self.np[i] = (0, 0, 0) def clear(self): - self.setSegment(list(range(self.np.n)), (0,0,0)) + self.setStrip((0, 0, 0)) # helper methods + def setStrip(self, color): + self.setSegment(list(range(self.np.n)), color) + def setSegment(self, segment_of_leds, color): for led in segment_of_leds: self.np[led] = color @@ -233,7 +294,7 @@ def randInt(self, lower, upper): return upper - 1 def randColor(self): - return (self.randInt(0,256),self.randInt(0,256),self.randInt(0,256)) + return (self.randInt(0, 256), self.randInt(0, 256), self.randInt(0, 256)) def wheel(self, pos): if pos < 85: