Last active
September 18, 2025 14:56
-
-
Save Staars/d96600eb8cd67d0dbb0adf5b4011595e to your computer and use it in GitHub Desktop.
Matrix tests
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # === Setup === | |
| leds = Leds(8 * 32, gpio.pin(gpio.WS2812, 0)) | |
| buf = leds.pixels_buffer() | |
| m = Matrix(buf, 32, 8, 3, true) # serpentine = true | |
| # === Helper: show and pause === | |
| def show_pause(ms) | |
| leds.show() | |
| tasmota.delay(ms) | |
| end | |
| # === 1. Fill background === | |
| m.clear(0x101010) # dim grey background | |
| show_pause(500) | |
| # === 2. Draw a perfect blue diagonal === | |
| for i: 0..7 | |
| m.set(i, i, 0x0000FF) # blue | |
| end | |
| show_pause(1000) | |
| # === 3. Moving vertical bar === | |
| for x: 0..31 | |
| # Clear previous bar | |
| m.clear(0x101010) | |
| # Redraw diagonal | |
| for i: 0..7 | |
| m.set(i, i, 0x0000FF) | |
| end | |
| # Draw vertical bar in yellow | |
| for y: 0..7 | |
| m.set(x, y, 0xFFFF00) | |
| end | |
| show_pause(50) | |
| end | |
| # === 4. Scroll test === | |
| # Scroll right | |
| for s: 1..5 | |
| m.scroll(3, 1) # dir=3 (right), steps=1 | |
| show_pause(100) | |
| end | |
| # Scroll down | |
| for s: 1..3 | |
| m.scroll(2, 1) # dir=2 (down), steps=1 | |
| show_pause(100) | |
| end | |
| # Scroll left | |
| for s: 1..5 | |
| m.scroll(1, 1) # dir=1 (left), steps=1 | |
| show_pause(100) | |
| end | |
| # Scroll up | |
| for s: 1..3 | |
| m.scroll(0, 1) # dir=0 (up), steps=1 | |
| show_pause(100) | |
| end | |
| # === End pattern === | |
| m.clear(0x000000) # clear to black | |
| leds.show() | |
| ######################################### | |
| #- | |
| - LED driver for Ulanzi clock written in Berry | |
| - Matrix rain animation using Matrix + blit() | |
| - Uses packed integer hex colours | |
| -# | |
| #@ solidify:MATRIX_ANIM | |
| class MATRIX_ANIM | |
| var strip | |
| var matrix | |
| var drop | |
| var positions | |
| var wait | |
| def init() | |
| import crypto | |
| # Create LED strip and wrap in Matrix (serpentine handled in C++) | |
| self.strip = Leds(32 * 8, gpio.pin(gpio.WS2812, 32)) | |
| var bpp = self.strip.pixel_size() | |
| var buf = self.strip.pixels_buffer() | |
| self.matrix = Matrix(buf, 32, 8, bpp, true) | |
| # Create a 1×4 drop pattern (head bright green, tail dimmer) | |
| # Allocate exact size: width × height × bpp | |
| self.drop = Matrix(drop_buf, 1, 4, bpp, false) | |
| self.drop.set(0, 0, 0x001800) # head | |
| self.drop.set(0, 1, 0x001200) # tail 1 | |
| self.drop.set(0, 2, 0x000a00) # tail 2 | |
| self.drop.set(0, 3, 0x000500) # tail 3 | |
| # Initialise drop positions | |
| self.wait = 0 | |
| self.positions = [] | |
| for i: 0..31 | |
| var y = (crypto.random(1)[0] % 10) - 3 | |
| if i > 0 && y == self.positions[i - 1] | |
| y += 3 | |
| end | |
| self.positions.push(y) | |
| end | |
| # Register driver | |
| tasmota.add_driver(self) | |
| end | |
| def deinit() | |
| self.strip.clear() | |
| tasmota.remove_driver(self) | |
| end | |
| def every_50ms() | |
| if self.wait == 0 | |
| self.wait = 4 | |
| return | |
| end | |
| self.wait -= 1 | |
| # Clear the whole matrix to black | |
| self.matrix.clear(0x000000) | |
| # Draw each drop | |
| var x = 0 | |
| for y: self.positions | |
| if y >= 9 | |
| self.positions[x] = -3 | |
| else | |
| # Blit the 1×4 drop into the main matrix at (x, y) | |
| self.matrix.blit(self.drop, x, y) | |
| self.positions[x] = y + 1 | |
| end | |
| x += 1 | |
| end | |
| self.strip.show() | |
| end | |
| end | |
| # Instantiate and return the driver | |
| var anim = MATRIX_ANIM() | |
| ################################################# | |
| import math | |
| #@ solidify:ULANIM | |
| class ULANIM | |
| var matrix, clist, cmatrix | |
| var strip | |
| var animation, current_animation, delay | |
| def init() | |
| # Create LED strip and wrap in Matrix (serpentine handled in C++) | |
| self.strip = Leds(32 * 8, gpio.pin(gpio.WS2812, 32)) | |
| var bpp = self.strip.pixel_size() | |
| var buf = self.strip.pixels_buffer() | |
| self.matrix = Matrix(buf, 32, 8, bpp, true) | |
| self.initCMatrix() | |
| self.loadAnimation() | |
| tasmota.add_driver(self) | |
| end | |
| def deinit() | |
| self.strip.clear() | |
| tasmota.remove_driver(self) | |
| end | |
| def every_50ms() | |
| if self.current_animation == nil | |
| return | |
| end | |
| if self.delay > 0 | |
| self.delay -= 1 | |
| return | |
| end | |
| self.updateCList() | |
| self.draw() | |
| self.nextAnimation() | |
| end | |
| def loadAnimation() | |
| self.animation = [ # color, cycles, delay, mode | |
| [0x0000ff,8,2,4], | |
| [0x0000ff,7,2,5], | |
| [0x00ffff,8,2,4], | |
| [0x00ffff,7,2,5], | |
| [0x00ff00,8,0,4], | |
| [0x00ff00,7,1,5], | |
| [0xffff00,8,0,4], | |
| [0xffff00,7,1,5], | |
| [0xff0000,8,0,4], | |
| [0xff0000,7,1,5], | |
| [0xffffff,8,0,4], | |
| [0xffffff,8,1,5], | |
| ] | |
| self.current_animation = [0,0,0,0] | |
| self.delay = 0 | |
| self.colorFromSeed() | |
| self.clist = self.current_animation[4] | |
| end | |
| def updateCList() | |
| var mode = self.current_animation[3] | |
| if mode == 0 || mode == 2 | |
| var last = self.clist[0] | |
| for i:0..6 | |
| self.clist[i] = self.clist[i+1] | |
| end | |
| self.clist[7] = last | |
| elif mode == 1 || mode == 3 | |
| var first = self.clist[7] | |
| for i:range(6,0,-1) | |
| self.clist[i+1] = self.clist[i] | |
| end | |
| self.clist[0] = first | |
| elif mode == 4 | |
| var _clist = self.current_animation[4] | |
| var c = 7 - self.current_animation[1] % 8 | |
| self.clist = [0,0,0,0,0,0,0,0] | |
| for i:range(7,7-c,-1) | |
| self.clist[i] = _clist[i] | |
| end | |
| elif mode == 5 | |
| var _clist = self.current_animation[4] | |
| var c = self.current_animation[1] % 8 | |
| self.clist = [0,0,0,0,0,0,0,0] | |
| for i:range(0,c) | |
| self.clist[7-i] = _clist[i] | |
| end | |
| end | |
| end | |
| def nextAnimation() | |
| if self.current_animation[1] > 0 | |
| self.current_animation[1] -= 1 | |
| else | |
| if size(self.animation) == 0 | |
| self.current_animation = nil | |
| return | |
| end | |
| self.current_animation = self.animation[0] | |
| self.animation = self.animation[1..] | |
| self.colorFromSeed() | |
| self.clist = self.current_animation[4] | |
| end | |
| self.delay = self.current_animation[2] | |
| end | |
| def colorFromSeed() | |
| var color = self.current_animation[0] | |
| var divider = 8 | |
| var mode = self.current_animation[3] | |
| if mode == 2 || mode == 3 | |
| divider = 9 | |
| end | |
| var r = (((color >> 16) / divider) << 16) & 0xff0000 | |
| var g = ((((color >> 8) & 0xff) / divider) << 8) & 0xff00 | |
| var b = (((color & 0xff) / divider)) | |
| var step = r | g | b | |
| var clist = [color,0,0,0,0,0,0,0] | |
| for i:1..7 | |
| clist[i] = clist[i-1] - step | |
| end | |
| if mode == 2 || mode == 3 | |
| clist = clist[4..7] + clist[4..7].reverse() | |
| end | |
| self.current_animation.push(clist) | |
| self.clist = clist | |
| end | |
| def draw() | |
| var cl = self.clist | |
| for i:0..255 | |
| var slot = self.cmatrix[i] | |
| self.matrix.set(i % 32, i / 32, cl[slot]) | |
| end | |
| self.strip.show() | |
| end | |
| def initCMatrix() | |
| var offset = [0] | |
| self.cmatrix = bytes(-256) | |
| for y:0..7 | |
| for x:0..31 | |
| for o:offset | |
| if x >= o && x < (32 - o) | |
| self.cmatrix[32*y + x] = o | |
| end | |
| end | |
| end | |
| offset.push(offset[-1]+1) | |
| end | |
| end | |
| end | |
| var anim = ULANIM() | |
| ############################# | |
| import math | |
| #@ solidify:PULSE_RINGS | |
| class PULSE_RINGS | |
| var strip, matrix | |
| var frame, bpp | |
| def init() | |
| # Create LED strip and wrap in Matrix (serpentine handled in C++) | |
| self.strip = Leds(32 * 8, gpio.pin(gpio.WS2812, 32)) | |
| self.bpp = self.strip.pixel_size() | |
| var buf = self.strip.pixels_buffer() | |
| self.matrix = Matrix(buf, 32, 8, self.bpp, true) | |
| self.frame = 0 | |
| tasmota.add_driver(self) | |
| end | |
| def deinit() | |
| self.strip.clear() | |
| tasmota.remove_driver(self) | |
| end | |
| def every_50ms() | |
| self.draw() | |
| self.frame += 1 | |
| if self.frame > 14 | |
| self.frame = 0 | |
| end | |
| end | |
| def draw() | |
| self.matrix.clear(0x000000) | |
| # Centre of the panel | |
| var cx = 15.5 | |
| var cy = 3.5 | |
| # Draw rings at radii based on frame | |
| for y:0..7 | |
| for x:0..31 | |
| var dx = x - cx | |
| var dy = y - cy | |
| var dist = math.sqrt(dx*dx + dy*dy) | |
| # Pulse: ring radius grows each frame | |
| var radius = (self.frame % 8) + 1 | |
| # If pixel is near the current radius, colour it | |
| if math.abs(dist - radius) < 0.5 | |
| # Fade colour based on radius | |
| var fade = 255 - (radius * 30) | |
| if fade < 0 | |
| fade = 0 | |
| end | |
| var col = (fade << 16) | (fade << 8) | fade # white fade | |
| self.matrix.set(x, y, col) | |
| end | |
| end | |
| end | |
| self.strip.show() | |
| end | |
| end | |
| var anim = PULSE_RINGS() | |
| ############################################ | |
| import math | |
| #@ solidify:AURORA_DUAL_WAVES | |
| class AURORA_DUAL_WAVES | |
| var strip, matrix | |
| var tick, frame_div | |
| var fast_loop_closure | |
| def init() | |
| # LED strip + Matrix | |
| self.strip = Leds(32 * 8, gpio.pin(gpio.WS2812, 32)) | |
| var bpp = self.strip.pixel_size() | |
| var buf = self.strip.pixels_buffer() | |
| self.matrix = Matrix(buf, 32, 8, bpp, true) | |
| self.tick = 0 | |
| self.frame_div = 1 # update every fast_loop tick | |
| # Register fast loop | |
| self.fast_loop_closure = def () self.fast_loop() end | |
| tasmota.add_fast_loop(self.fast_loop_closure) | |
| end | |
| def deinit() | |
| self.strip.clear() | |
| tasmota.remove_fast_loop(self.fast_loop_closure) | |
| tasmota.remove_driver(self) | |
| end | |
| def fast_loop() | |
| self.tick += 1 | |
| if self.tick % self.frame_div != 0 | |
| return | |
| end | |
| self.draw() | |
| end | |
| # Integer HSV (s=255). h: 0..359, v: 0..255 | |
| def hsvInt(h, v) | |
| var s = 255 | |
| var sector = h / 60 | |
| var f = h % 60 | |
| var p = (v * (255 - s)) / 255 | |
| var sf = (s * f) / 60 | |
| var q = (v * (255 - sf)) / 255 | |
| var s60f = (s * (60 - f)) / 60 | |
| var t = (v * (255 - s60f)) / 255 | |
| var r = 0 | |
| var g = 0 | |
| var b = 0 | |
| if sector == 0 | |
| r = v | |
| g = t | |
| b = p | |
| elif sector == 1 | |
| r = q | |
| g = v | |
| b = p | |
| elif sector == 2 | |
| r = p | |
| g = v | |
| b = t | |
| elif sector == 3 | |
| r = p | |
| g = q | |
| b = v | |
| elif sector == 4 | |
| r = t | |
| g = p | |
| b = v | |
| else | |
| r = v | |
| g = p | |
| b = q | |
| end | |
| return (r << 16) | (g << 8) | b | |
| end | |
| def draw() | |
| var t = self.tick | |
| # Wave parameters | |
| var speed1 = 2.0 | |
| var speed2 = 1.3 | |
| var hue_shift1 = 0 | |
| var hue_shift2 = 180 | |
| for y:0..7 | |
| for x:0..31 | |
| # First wave: horizontal bias | |
| var wave1 = math.sin((x * 0.3) + (t / speed1)) | |
| # Second wave: diagonal bias | |
| var wave2 = math.sin(((x + y) * 0.25) - (t / speed2)) | |
| # Combine waves for brightness modulation | |
| var combined = (wave1 + wave2) / 2.0 | |
| var v = int(128 + combined * 127) | |
| # Hue also shifts with position and time | |
| var hue = int((x * 6 + y * 4 + t) % 360) | |
| # Alternate hue shift for second wave influence | |
| if wave2 > 0 | |
| hue = (hue + hue_shift2) % 360 | |
| else | |
| hue = (hue + hue_shift1) % 360 | |
| end | |
| var col = self.hsvInt(hue, v) | |
| self.matrix.set(x, y, col) | |
| end | |
| end | |
| self.strip.show() | |
| end | |
| end | |
| var anim = AURORA_DUAL_WAVES() | |
| ######################################### | |
| #@ solidify:MULTI_SPRITE_DEMO | |
| class MULTI_SPRITE_DEMO | |
| var strip, matrix | |
| var fast_loop_closure | |
| var tick, frame_div | |
| # Sprite definitions: width, height, pixel data (0 = transparent) | |
| var sprites | |
| var actors # list of [sprite_index, x, y, vx, vy] | |
| def init() | |
| self.strip = Leds(32 * 8, gpio.pin(gpio.WS2812, 32)) | |
| var bpp = self.strip.pixel_size() | |
| var buf = self.strip.pixels_buffer() | |
| self.matrix = Matrix(buf, 32, 8, bpp, true) | |
| # Define sprites | |
| var smiley = [ | |
| 0, 0xFFFF00, 0xFFFF00, 0xFFFF00, 0, | |
| 0xFFFF00, 0, 0x000000, 0, 0xFFFF00, | |
| 0xFFFF00, 0, 0x000000, 0, 0xFFFF00, | |
| 0xFFFF00, 0, 0, 0, 0xFFFF00, | |
| 0, 0xFFFF00, 0xFFFF00, 0xFFFF00, 0 | |
| ] | |
| var heart = [ | |
| 0xFF0000, 0xFF0000, 0, 0xFF0000, 0xFF0000, | |
| 0xFF0000, 0xFF0000, 0xFF0000, 0xFF0000, 0xFF0000, | |
| 0xFF0000, 0xFF0000, 0xFF0000, 0xFF0000, 0xFF0000, | |
| 0, 0xFF0000, 0xFF0000, 0xFF0000, 0, | |
| 0, 0, 0xFF0000, 0, 0 | |
| ] | |
| var star = [ | |
| 0, 0, 0xFFFFFF, 0, 0, | |
| 0, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0, | |
| 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, | |
| 0, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0, | |
| 0, 0, 0xFFFFFF, 0, 0 | |
| ] | |
| self.sprites = [ | |
| [5, 5, smiley], | |
| [5, 5, heart], | |
| [5, 5, star] | |
| ] | |
| # Actors: [sprite_index, x, y, vx, vy] | |
| self.actors = [ | |
| [0, 0, 0, 1, 1], | |
| [1, 10, 2, -1, 1], | |
| [2, 20, 5, 1, -1] | |
| ] | |
| self.tick = 0 | |
| self.frame_div = 10 # update every 2 fast_loop ticks | |
| # Register fast loop | |
| self.fast_loop_closure = def () self.fast_loop() end | |
| tasmota.add_fast_loop(self.fast_loop_closure) | |
| end | |
| def deinit() | |
| self.strip.clear() | |
| tasmota.remove_fast_loop(self.fast_loop_closure) | |
| tasmota.remove_driver(self) | |
| end | |
| def fast_loop() | |
| self.tick += 1 | |
| if self.tick % self.frame_div != 0 | |
| return | |
| end | |
| self.update() | |
| self.draw() | |
| end | |
| def update() | |
| for a: self.actors | |
| a[1] += a[3] # x += vx | |
| a[2] += a[4] # y += vy | |
| var sprite_w = self.sprites[a[0]][0] | |
| var sprite_h = self.sprites[a[0]][1] | |
| # Bounce horizontally | |
| if a[1] <= 0 || a[1] + sprite_w >= 32 | |
| a[3] = -a[3] | |
| a[1] += a[3] | |
| end | |
| # Bounce vertically | |
| if a[2] <= 0 || a[2] + sprite_h >= 8 | |
| a[4] = -a[4] | |
| a[2] += a[4] | |
| end | |
| end | |
| end | |
| def draw() | |
| self.matrix.clear(0x000000) | |
| for a: self.actors | |
| self.blitSprite(a[0], a[1], a[2]) | |
| end | |
| self.strip.show() | |
| end | |
| def blitSprite(index, x, y) | |
| var sprite_w = self.sprites[index][0] | |
| var sprite_h = self.sprites[index][1] | |
| var data = self.sprites[index][2] | |
| for sy:0..sprite_h-1 | |
| for sx:0..sprite_w-1 | |
| var col = data[sy * sprite_w + sx] | |
| if col != 0 | |
| var px = x + sx | |
| var py = y + sy | |
| if px >= 0 && px < 32 && py >= 0 && py < 8 | |
| self.matrix.set(px, py, col) | |
| end | |
| end | |
| end | |
| end | |
| end | |
| end | |
| var anim = MULTI_SPRITE_DEMO() | |
| ############################### | |
| import math | |
| #@ solidify:PSYCHEDELIC_SCROLL_LAYERED | |
| class PSYCHEDELIC_SCROLL_LAYERED | |
| var strip, matrix | |
| var fast_loop_closure | |
| var tick, frame_div | |
| var offset_x, offset_y | |
| var dir_x, dir_y | |
| var change_dir_tick | |
| var zoom_phase_x, zoom_phase_y | |
| def init() | |
| self.strip = Leds(32 * 8, gpio.pin(gpio.WS2812, 32)) | |
| var bpp = self.strip.pixel_size() | |
| var buf = self.strip.pixels_buffer() | |
| self.matrix = Matrix(buf, 32, 8, bpp, true) | |
| self.tick = 0 | |
| self.frame_div = 3 # slower updates | |
| self.offset_x = 0.0 | |
| self.offset_y = 0.0 | |
| self.dir_x = 0.2 | |
| self.dir_y = 0.0 | |
| self.change_dir_tick = 0 | |
| self.zoom_phase_x = 0.0 | |
| self.zoom_phase_y = math.pi / 2 | |
| self.fast_loop_closure = def () self.fast_loop() end | |
| tasmota.add_fast_loop(self.fast_loop_closure) | |
| end | |
| def deinit() | |
| self.strip.clear() | |
| tasmota.remove_fast_loop(self.fast_loop_closure) | |
| tasmota.remove_driver(self) | |
| end | |
| def fast_loop() | |
| self.tick += 1 | |
| if self.tick % self.frame_div != 0 | |
| return | |
| end | |
| # Occasionally change scroll direction | |
| if self.tick - self.change_dir_tick > 150 | |
| import crypto | |
| self.dir_x = ((crypto.random(1)[0] % 3) - 1) * 0.3 | |
| self.dir_y = ((crypto.random(1)[0] % 3) - 1) * 0.3 | |
| if self.dir_x == 0 && self.dir_y == 0 | |
| self.dir_x = 0.3 | |
| end | |
| self.change_dir_tick = self.tick | |
| end | |
| self.offset_x += self.dir_x | |
| self.offset_y += self.dir_y | |
| # Advance zoom phases | |
| self.zoom_phase_x += 0.04 | |
| self.zoom_phase_y += 0.05 | |
| if self.zoom_phase_x > math.pi * 2 | |
| self.zoom_phase_x -= math.pi * 2 | |
| end | |
| if self.zoom_phase_y > math.pi * 2 | |
| self.zoom_phase_y -= math.pi * 2 | |
| end | |
| self.draw() | |
| end | |
| def hsvInt(h, v) | |
| var s = 255 | |
| var sector = h / 60 | |
| var f = h % 60 | |
| var p = (v * (255 - s)) / 255 | |
| var sf = (s * f) / 60 | |
| var q = (v * (255 - sf)) / 255 | |
| var s60f = (s * (60 - f)) / 60 | |
| var t = (v * (255 - s60f)) / 255 | |
| var r = 0 | |
| var g = 0 | |
| var b = 0 | |
| if sector == 0 | |
| r = v | |
| g = t | |
| b = p | |
| elif sector == 1 | |
| r = q | |
| g = v | |
| b = p | |
| elif sector == 2 | |
| r = p | |
| g = v | |
| b = t | |
| elif sector == 3 | |
| r = p | |
| g = q | |
| b = v | |
| elif sector == 4 | |
| r = t | |
| g = p | |
| b = v | |
| else | |
| r = v | |
| g = p | |
| b = q | |
| end | |
| return (r << 16) | (g << 8) | b | |
| end | |
| def draw() | |
| var t = self.tick | |
| var zoom_x = 1.0 + 0.25 * math.sin(self.zoom_phase_x) | |
| var zoom_y = 1.0 + 0.25 * math.sin(self.zoom_phase_y) | |
| for y:0..7 | |
| for x:0..31 | |
| var zx = (x - 16) * zoom_x + 16 | |
| var zy = (y - 4) * zoom_y + 4 | |
| # First plasma layer | |
| var v1a = math.sin((zx + self.offset_x) / 6.0 + t / 40.0) | |
| var v1b = math.sin((zy + self.offset_y) / 6.0 - t / 55.0) | |
| var v1c = math.sin(((zx + self.offset_x) + (zy + self.offset_y)) / 6.0 + t / 70.0) | |
| var hue1 = ((v1a + v1b + v1c) / 3.0 + 1) * 180 | |
| # Second plasma layer (different scale/speed/offset) | |
| var v2a = math.sin((zx * 1.3 + self.offset_x * 0.7) / 5.0 - t / 60.0) | |
| var v2b = math.sin((zy * 1.3 + self.offset_y * 0.7) / 5.0 + t / 45.0) | |
| var v2c = math.sin(((zx * 1.3 + self.offset_x * 0.7) - (zy * 1.3 + self.offset_y * 0.7)) / 5.0 - t / 80.0) | |
| var hue2 = ((v2a + v2b + v2c) / 3.0 + 1) * 180 + 120 # offset hue | |
| # Blend hues by averaging | |
| var hue = int(((hue1 + hue2) / 2.0) % 360) | |
| var col = self.hsvInt(hue, 255) | |
| self.matrix.set(x, y, col) | |
| end | |
| end | |
| self.strip.show() | |
| end | |
| end | |
| var anim = PSYCHEDELIC_SCROLL_LAYERED() | |
| ################################ | |
| import math | |
| #@ solidify:MOIRE_DRIFT | |
| class MOIRE_DRIFT | |
| var strip, matrix | |
| var fast_loop_closure | |
| var tick, frame_div | |
| var offset1_x, offset1_y | |
| var offset2_x, offset2_y | |
| def init() | |
| self.strip = Leds(32 * 8, gpio.pin(gpio.WS2812, 32)) | |
| var bpp = self.strip.pixel_size() | |
| var buf = self.strip.pixels_buffer() | |
| self.matrix = Matrix(buf, 32, 8, bpp, true) | |
| self.tick = 0 | |
| self.frame_div = 1 # update every fast_loop tick | |
| self.offset1_x = 0.0 | |
| self.offset1_y = 0.0 | |
| self.offset2_x = 0.0 | |
| self.offset2_y = 0.0 | |
| self.fast_loop_closure = def () self.fast_loop() end | |
| tasmota.add_fast_loop(self.fast_loop_closure) | |
| end | |
| def deinit() | |
| self.strip.clear() | |
| tasmota.remove_fast_loop(self.fast_loop_closure) | |
| tasmota.remove_driver(self) | |
| end | |
| def fast_loop() | |
| self.tick += 1 | |
| if self.tick % self.frame_div != 0 | |
| return | |
| end | |
| # Move pattern offsets at slightly different speeds | |
| self.offset1_x += 0.07 | |
| self.offset1_y += 0.04 | |
| self.offset2_x += 0.05 | |
| self.offset2_y += 0.06 | |
| self.draw() | |
| end | |
| def hsvInt(h, v) | |
| var s = 255 | |
| var sector = h / 60 | |
| var f = h % 60 | |
| var p = (v * (255 - s)) / 255 | |
| var sf = (s * f) / 60 | |
| var q = (v * (255 - sf)) / 255 | |
| var s60f = (s * (60 - f)) / 60 | |
| var t = (v * (255 - s60f)) / 255 | |
| var r = 0 | |
| var g = 0 | |
| var b = 0 | |
| if sector == 0 | |
| r = v | |
| g = t | |
| b = p | |
| elif sector == 1 | |
| r = q | |
| g = v | |
| b = p | |
| elif sector == 2 | |
| r = p | |
| g = v | |
| b = t | |
| elif sector == 3 | |
| r = p | |
| g = q | |
| b = v | |
| elif sector == 4 | |
| r = t | |
| g = p | |
| b = v | |
| else | |
| r = v | |
| g = p | |
| b = q | |
| end | |
| return (r << 16) | (g << 8) | b | |
| end | |
| def draw() | |
| var t = self.tick | |
| for y:0..7 | |
| for x:0..31 | |
| # First grid pattern | |
| var g1 = math.sin((x + self.offset1_x) / 2.5) + math.sin((y + self.offset1_y) / 2.5) | |
| # Second grid pattern (different scale) | |
| var g2 = math.sin((x + self.offset2_x) / 3.0) + math.sin((y + self.offset2_y) / 3.0) | |
| # Combine patterns to get moiré interference | |
| var combined = (g1 + g2) / 4.0 # normalize to ~-1..1 | |
| var hue = int(((combined + 1.0) * 180 + t) % 360) | |
| var v = 200 | |
| var col = self.hsvInt(hue, v) | |
| self.matrix.set(x, y, col) | |
| end | |
| end | |
| self.strip.show() | |
| end | |
| end | |
| var anim = MOIRE_DRIFT() | |
| ##################################### | |
| import math | |
| #@ solidify:SPECTRUM_ANALYZER_RANDOM | |
| class SPECTRUM_ANALYZER_RANDOM | |
| var strip, matrix | |
| var fast_loop_closure | |
| var tick, frame_div | |
| # Layout | |
| var cols, rows | |
| var bands, band_w | |
| # Signal model | |
| var level # current bar heights (0..rows) | |
| var target # target heights (signal input proxy) | |
| var peak # peak-hold per band (0..rows) | |
| var phase # per-band phase for sine driver | |
| var speed # per-band phase speed | |
| var jitter # per-band small noise accumulator | |
| # Tuning | |
| var rise, fall, peak_fall | |
| var jitter_amt, jitter_every | |
| # PRNG seed | |
| var rng_seed | |
| def init() | |
| # LED strip + Matrix | |
| self.cols = 32 | |
| self.rows = 8 | |
| self.strip = Leds(self.cols * self.rows, gpio.pin(gpio.WS2812, 32)) | |
| var bpp = self.strip.pixel_size() | |
| var buf = self.strip.pixels_buffer() | |
| self.matrix = Matrix(buf, self.cols, self.rows, bpp, true) | |
| # Analyzer layout | |
| self.bands = 8 | |
| self.band_w = self.cols / self.bands # 4 | |
| # Arrays | |
| self.level = [] | |
| self.target = [] | |
| self.peak = [] | |
| self.phase = [] | |
| self.speed = [] | |
| self.jitter = [] | |
| # Dynamics tuning | |
| self.rise = 0.35 | |
| self.fall = 0.15 | |
| self.peak_fall = 0.30 | |
| self.jitter_amt = 0.20 | |
| self.jitter_every = 6 | |
| # Seed arrays | |
| self.rng_seed = 123456789 | |
| for i:0..self.bands-1 | |
| self.level.push(0.0) | |
| self.target.push(0.0) | |
| self.peak.push(0.0) | |
| var base_phase = 2.0 * math.pi * (i / self.bands) | |
| self.phase.push(base_phase) | |
| var sp = 0.08 + 0.02 * i | |
| self.speed.push(sp) | |
| self.jitter.push(0.0) | |
| end | |
| # Timing | |
| self.tick = 0 | |
| self.frame_div = 1 | |
| # Register fast loop | |
| self.fast_loop_closure = def () self.fast_loop() end | |
| tasmota.add_fast_loop(self.fast_loop_closure) | |
| end | |
| def deinit() | |
| self.strip.clear() | |
| tasmota.remove_fast_loop(self.fast_loop_closure) | |
| tasmota.remove_driver(self) | |
| end | |
| def fast_loop() | |
| self.tick += 1 | |
| if self.tick % self.frame_div != 0 | |
| return | |
| end | |
| self.updateSignal() | |
| self.updateBars() | |
| self.draw() | |
| end | |
| def rand01() | |
| self.rng_seed = (1103515245 * self.rng_seed + 12345) % 2147483647 | |
| return self.rng_seed / 2147483647.0 | |
| end | |
| def updateSignal() | |
| var t = self.tick | |
| for i:0..self.bands-1 | |
| self.phase[i] += self.speed[i] | |
| var base = (math.sin(self.phase[i]) + 1.0) * 0.5 | |
| var tilt = 1.0 - (i / (self.bands * 1.5)) | |
| if t % self.jitter_every == 0 | |
| self.jitter[i] = (self.rand01() - 0.5) * 2.0 * self.jitter_amt | |
| end | |
| var val = base * tilt + self.jitter[i] | |
| if val < 0.0 val = 0.0 end | |
| if val > 1.0 val = 1.0 end | |
| var shaped = math.pow(val, 0.85) | |
| self.target[i] = shaped * self.rows | |
| end | |
| end | |
| def updateBars() | |
| for i:0..self.bands-1 | |
| var cur = self.level[i] | |
| var tar = self.target[i] | |
| if tar > cur | |
| cur = cur + self.rise * (tar - cur) | |
| else | |
| cur = cur - self.fall * (cur - tar) | |
| end | |
| if cur < 0.0 cur = 0.0 end | |
| if cur > self.rows cur = self.rows end | |
| self.level[i] = cur | |
| if cur > self.peak[i] | |
| self.peak[i] = cur | |
| else | |
| self.peak[i] = self.peak[i] - self.peak_fall | |
| if self.peak[i] < 0.0 | |
| self.peak[i] = 0.0 | |
| end | |
| end | |
| end | |
| end | |
| def hsvInt(h, v) | |
| var s = 255 | |
| var sector = h / 60 | |
| var f = h % 60 | |
| var p = (v * (255 - s)) / 255 | |
| var sf = (s * f) / 60 | |
| var q = (v * (255 - sf)) / 255 | |
| var s60f = (s * (60 - f)) / 60 | |
| var t = (v * (255 - s60f)) / 255 | |
| var r = 0 | |
| var g = 0 | |
| var b = 0 | |
| if sector == 0 | |
| r = v | |
| g = t | |
| b = p | |
| elif sector == 1 | |
| r = q | |
| g = v | |
| b = p | |
| elif sector == 2 | |
| r = p | |
| g = v | |
| b = t | |
| elif sector == 3 | |
| r = p | |
| g = q | |
| b = v | |
| elif sector == 4 | |
| r = t | |
| g = p | |
| b = v | |
| else | |
| r = v | |
| g = p | |
| b = q | |
| end | |
| return (r << 16) | (g << 8) | b | |
| end | |
| def barColor(y, h) | |
| var ratio = 0.0 | |
| if h > 0.0 | |
| ratio = y / (self.rows - 1.0) | |
| end | |
| var hue = int(120 - 120 * ratio) | |
| var v = 180 + int(75 * (1.0 - ratio)) | |
| if v > 255 v = 255 end | |
| if v < 0 v = 0 end | |
| return self.hsvInt(hue, v) | |
| end | |
| def draw() | |
| self.matrix.clear(0x000000) | |
| for i:0..self.bands-1 | |
| var h = self.level[i] | |
| var p = self.peak[i] | |
| var x0 = i * self.band_w | |
| var x1 = x0 + self.band_w - 1 | |
| for y:0..self.rows-1 | |
| var y_pix = self.rows - 1 - y | |
| if y < int(h + 0.5) | |
| var col = self.barColor(y, h) | |
| for x:x0..x1 | |
| self.matrix.set(x, y_pix, col) | |
| end | |
| end | |
| end | |
| var peak_y = int(p) | |
| if peak_y > 0 | |
| var py = self.rows - 1 - peak_y | |
| var peak_col = 0xFFFFFF | |
| for x:x0..x1 | |
| self.matrix.set(x, py, peak_col) | |
| end | |
| end | |
| end | |
| self.strip.show() | |
| end | |
| end | |
| var anim = SPECTRUM_ANALYZER_RANDOM() | |
| ######################### | |
| import math | |
| #@ solidify:KALEIDOSCOPE_SPIN | |
| class KALEIDOSCOPE_SPIN | |
| var strip, matrix | |
| var fast_loop_closure | |
| var tick, frame_div | |
| var cx, cy # center coordinates | |
| var wedges # number of mirrored segments | |
| var spin_speed | |
| def init() | |
| self.strip = Leds(32 * 8, gpio.pin(gpio.WS2812, 32)) | |
| var bpp = self.strip.pixel_size() | |
| var buf = self.strip.pixels_buffer() | |
| self.matrix = Matrix(buf, 32, 8, bpp, true) | |
| self.cx = (32 - 1) / 2.0 | |
| self.cy = (8 - 1) / 2.0 | |
| self.wedges = 6 # number of symmetrical slices | |
| self.spin_speed = 0.02 # radians per frame | |
| self.tick = 0 | |
| self.frame_div = 1 | |
| self.fast_loop_closure = def () self.fast_loop() end | |
| tasmota.add_fast_loop(self.fast_loop_closure) | |
| end | |
| def deinit() | |
| self.strip.clear() | |
| tasmota.remove_fast_loop(self.fast_loop_closure) | |
| tasmota.remove_driver(self) | |
| end | |
| def fast_loop() | |
| self.tick += 1 | |
| if self.tick % self.frame_div != 0 | |
| return | |
| end | |
| self.draw() | |
| end | |
| def hsvInt(h, v) | |
| var s = 255 | |
| var sector = h / 60 | |
| var f = h % 60 | |
| var p = (v * (255 - s)) / 255 | |
| var sf = (s * f) / 60 | |
| var q = (v * (255 - sf)) / 255 | |
| var s60f = (s * (60 - f)) / 60 | |
| var t = (v * (255 - s60f)) / 255 | |
| var r = 0 | |
| var g = 0 | |
| var b = 0 | |
| if sector == 0 | |
| r = v | |
| g = t | |
| b = p | |
| elif sector == 1 | |
| r = q | |
| g = v | |
| b = p | |
| elif sector == 2 | |
| r = p | |
| g = v | |
| b = t | |
| elif sector == 3 | |
| r = p | |
| g = q | |
| b = v | |
| elif sector == 4 | |
| r = t | |
| g = p | |
| b = v | |
| else | |
| r = v | |
| g = p | |
| b = q | |
| end | |
| return (r << 16) | (g << 8) | b | |
| end | |
| def draw() | |
| var angle_offset = self.tick * self.spin_speed | |
| for y:0..7 | |
| for x:0..31 | |
| # Translate to center | |
| var dx = x - self.cx | |
| var dy = y - self.cy | |
| # Polar coordinates | |
| var r = math.sqrt(dx*dx + dy*dy) | |
| var ang = math.atan2(dy, dx) + angle_offset | |
| # Wrap angle into one wedge | |
| var wedge_angle = (2.0 * math.pi) / self.wedges | |
| ang = ang % wedge_angle | |
| if ang < 0 | |
| ang += wedge_angle | |
| end | |
| # Map wedge angle back to 0..1 | |
| var norm_ang = ang / wedge_angle | |
| var norm_r = r / (math.sqrt(self.cx*self.cx + self.cy*self.cy)) | |
| # Colour mapping: hue from angle, brightness from radius | |
| var hue = int((norm_ang * 360) % 360) | |
| var val = int(255 * (1.0 - norm_r)) | |
| if val < 0 val = 0 end | |
| if val > 255 val = 255 end | |
| var col = self.hsvInt(hue, val) | |
| self.matrix.set(x, y, col) | |
| end | |
| end | |
| self.strip.show() | |
| end | |
| end | |
| var anim = KALEIDOSCOPE_SPIN() | |
| ------------------------------------------------------------------------------- | |
| Notes | |
| - Coordinates are 0-based; (0,0) is top-left. | |
| - For RGBW, the white channel is in the least significant byte. | |
| - Always call strip.show() after drawing to update the LEDs. | |
| - serpentine=true mirrors every odd row automatically in get/set/store logic. | |
| - blit() supports mono→colour with optional tint; other bpp conversions are rejected. | |
| def l(name) | |
| var cl = webclient() | |
| cl.begin(f"http://192.168.0.108:8000/{name}") | |
| var r = cl.GET() | |
| print(r) | |
| cl.write_file(f"/{name}") | |
| cl.close() | |
| end | |
| files = [ | |
| "AlertScreen.be", | |
| "autoexec.be", | |
| "BaseScreen.be", | |
| "BasicScreen.be", | |
| "BatteryScreen.be", | |
| "cal.bin", | |
| "CalendarScreen.be", | |
| "caution.bin", | |
| "fonts.be", | |
| "ImgScreen.be", | |
| "MatrixController.be", | |
| "NetScreen.be", | |
| "red_eye.bin", | |
| "ScreenManager.be", | |
| "StartScreen.be", | |
| "Tasmota.bin", | |
| "TsensScreen.be", | |
| "util.be", | |
| "weather.bin", | |
| "WeatherScreen.be" | |
| ] | |
| for f:files | |
| l(f) | |
| Matrix (Berry Binding) | |
| The Matrix class provides fast, low‑level access to an LED matrix buffer from Berry scripts. | |
| It is designed for performance‑critical operations such as pixel manipulation, scrolling, flipping, and blitting, while leaving higher‑level drawing to Berry or other modules. | |
| Constructor: | |
| Matrix(buf:bytes, width:int, height:int, bpp:int[, serpentine:bool]) | |
| Matrix(width:int, height:int, bpp:int[, serpentine:bool]) | |
| - buf: existing byte buffer to wrap (no allocation, external = true) | |
| - width, height: matrix dimensions in pixels | |
| - bpp: bytes per pixel (1, 3, or 4 supported) | |
| - serpentine: optional, true if rows alternate direction in physical layout | |
| Methods: | |
| clear([val:int]) | |
| Fast fill of the entire buffer with a single byte value. | |
| - val: optional, 0–255. Defaults to 0 (black). | |
| - Ignores serpentine mapping — writes raw bytes directly. | |
| Example: | |
| m.clear() # black | |
| m.clear(255) # white (mono) or max channel value | |
| get(x:int, y:int) -> int|list | |
| Returns the pixel value at (x, y). | |
| - bpp == 1: returns an integer 0–255 | |
| - bpp == 3 or 4: returns a packed integer (RGB or RGBA) | |
| - other bpp: returns a list of channel values | |
| set(x:int, y:int, val:int|list[, bri:int]) | |
| Sets the pixel at (x, y) to the given value. | |
| - val: packed integer or list of channel values | |
| - bri: optional brightness scaling (0–255, default 255) | |
| blit(src:Matrix, dx:int, dy:int[, bri:int][, tint:list]) | |
| Copies pixels from src into this matrix at offset (dx, dy). | |
| - Fast path if same bpp, no brightness change, no tint | |
| - Supports mono→color conversion with optional tint | |
| scroll(direction:int[, src:Matrix]) | |
| Scrolls the matrix in the given direction: | |
| - 0 = up, 1 = left, 2 = down, 3 = right | |
| - If src is provided, fills the vacated row/column from it | |
| - If src is omitted, wraps from the opposite edge of the same matrix | |
| flip(axis:str) | |
| Flips the matrix in place. | |
| - "h" = horizontal flip | |
| - "v" = vertical flip | |
| Notes: | |
| - All coordinates are logical (x, y); serpentine mapping is handled internally by load()/store() | |
| - clear() is the fastest way to reset the matrix — it uses a single memset over the buffer | |
| - For drawing shapes, use Berry loops with set() or higher‑level APIs in your Leds module | |
| Matrix C++ Driver (Native Berry Binding) | |
| Purpose: | |
| Provides a high‑performance native backend for the Berry Matrix class, enabling direct manipulation of LED matrix frame buffers with minimal overhead. | |
| Implements memory‑safe allocation/wrapping, pixel access, and optimized bulk operations. | |
| Core Structure: | |
| struct MatrixCore | |
| - data: pointer to raw pixel buffer (uint8_t*) | |
| - width, height: matrix dimensions in pixels | |
| - bpp: bytes per pixel (1, 3, or 4 supported) | |
| - serpentine: bool, true if physical wiring alternates row direction | |
| - external: bool, true if wrapping an external buffer (no free on deinit) | |
| - index(x, y): returns byte offset for logical coordinates | |
| - load(x, y, out): reads pixel at logical (x, y) into out[] | |
| - store(x, y, in): writes pixel at logical (x, y) from in[] | |
| - bytes_size(): total buffer size in bytes | |
| - blit(src, dx, dy): fast copy from src to this buffer with bounds checking | |
| Helper Functions: | |
| - self_core(vm): fetches native MatrixCore* from Berry object | |
| - in_bounds(mc, x, y): coordinate bounds check | |
| - read_tint_or_default(vm, idx, dest_bpp, tint[]): parses tint list or sets default | |
| - apply_brightness(px[], bpp, bri): scales pixel channels by brightness factor | |
| Berry Entry Points: | |
| be_matrix_init(buf|w, h, bpp[, serpentine]) | |
| - Wraps existing bytes or allocates new buffer | |
| - Validates dimensions, bpp, and buffer size | |
| - Sets .p member to native MatrixCore* | |
| be_matrix_deinit() | |
| - Frees buffer if owned (external == false) | |
| - Deletes MatrixCore and clears .p | |
| be_matrix_get(x, y) | |
| - Returns pixel at (x, y) as int or list depending on bpp | |
| be_matrix_set(x, y, val[, bri]) | |
| - Sets pixel at (x, y) from int or list, with optional brightness scaling | |
| be_matrix_clear([val]) | |
| - Fast memset() over entire buffer to byte value (default 0) | |
| be_matrix_blit(src, dx, dy[, bri][, tint]) | |
| - Fast path: same bpp, bri=255, no tint → direct MatrixCore::blit() | |
| - Slow path: per‑pixel copy with brightness/tint or mono→color conversion | |
| be_matrix_flip(axis) | |
| - Horizontal ("h") or vertical ("v") in‑place flip | |
| be_matrix_scroll(direction[, src]) | |
| - Scrolls in direction (0=up, 1=left, 2=down, 3=right) | |
| - If src provided: fill vacated row/col from src | |
| - If src omitted: wrap from opposite edge of same matrix | |
| Performance Notes: | |
| - All heavy operations (clear, blit, scroll, flip) are implemented in C++ for speed | |
| - Serpentine mapping handled in load()/store(), transparent to higher‑level code | |
| - Memory safety checks prevent buffer overruns | |
| - Designed for minimal Berry VM overhead in hot paths |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment