Browse Source

LED strip refactor (#3158)

`ws2812` buffer extracted to new `pixbuf` module.

* The new pixbuf module has more functionality than the `ws2812`-specific buffer it replaces.
* This is work in progress towards https://github.com/nodemcu/nodemcu-firmware/issues/2916
* The LED driver modules `ws2812`, `ws2801`, `apa102`, and `tm1829` have sprouted `pixbuf` support.
* `NTest` tests for `pixbuf` now exist.

While here, document the ws2812 UART-based overlapping with mainline
execution.  Fixes https://github.com/nodemcu/nodemcu-firmware/issues/3140

Co-authored-by: Gregor Hartmann <HHHartmann@users.noreply.github.com>
Nathaniel Wesley Filardo 3 years ago
parent
commit
85df6b588d

+ 1 - 0
app/include/user_modules.h

@@ -45,6 +45,7 @@
 //#define LUA_USE_MODULES_PCM
 //#define LUA_USE_MODULES_PERF
 //#define LUA_USE_MODULES_PIPE
+//#define LUA_USE_MODULES_PIXBUF
 //#define LUA_USE_MODULES_PWM
 //#define LUA_USE_MODULES_PWM2
 //#define LUA_USE_MODULES_RFSWITCH

+ 22 - 3
app/modules/apa102.c

@@ -5,6 +5,8 @@
 #include "platform.h"
 #include "user_interface.h"
 
+#include "pixbuf.h"
+
 
 #define NOP asm volatile(" nop \n\t")
 
@@ -79,9 +81,26 @@ static int apa102_write(lua_State* L) {
   MOD_CHECK_ID(gpio, clock_pin);
   uint32_t alt_clock_pin = pin_num[clock_pin];
 
-  size_t buf_len;
-  const char *buf = luaL_checklstring(L, 3, &buf_len);
-  uint32_t nbr_frames = buf_len / 4;
+  const char *buf;
+  uint32_t nbr_frames;
+
+  switch(lua_type(L, 3)) {
+  case LUA_TSTRING: {
+    size_t buf_len;
+    buf = luaL_checklstring(L, 3, &buf_len);
+    nbr_frames = buf_len / 4;
+    break;
+   }
+  case LUA_TUSERDATA: {
+    pixbuf *buffer = pixbuf_from_lua_arg(L, 3);
+    luaL_argcheck(L, buffer->nchan == 4, 3, "Pixbuf not 4-channel");
+    buf = (const char *)buffer->values;
+    nbr_frames = buffer->npix;
+    break;
+   }
+  default:
+    return luaL_argerror(L, 3, "String or pixbuf expected");
+  }
 
   if (nbr_frames > 100000) {
     return luaL_error(L, "The supplied buffer is too long, and might cause the callback watchdog to bark.");

+ 723 - 0
app/modules/pixbuf.c

@@ -0,0 +1,723 @@
+#include "module.h"
+#include "lauxlib.h"
+
+#include <string.h>
+
+#include "pixbuf.h"
+#define PIXBUF_METATABLE "pixbuf.buf"
+
+#ifndef MIN
+#define MIN(a,b) ((a) < (b) ? (a) : (b))
+#endif
+#ifndef MAX
+#define MAX(a,b) ((a) > (b) ? (a) : (b))
+#endif
+
+pixbuf *pixbuf_from_lua_arg(lua_State *L, int arg) {
+  return luaL_checkudata(L, arg, PIXBUF_METATABLE);
+}
+
+pixbuf *pixbuf_opt_from_lua_arg(lua_State *L, int arg) {
+  return luaL_testudata(L, arg, PIXBUF_METATABLE);
+}
+
+static ssize_t posrelat(ssize_t pos, size_t len) {
+  /* relative string position: negative means back from end */
+  if (pos < 0)
+    pos += (ssize_t)len + 1;
+  return MIN(MAX(pos, 1), len);
+}
+
+const size_t pixbuf_channels(pixbuf *p) {
+  return p->nchan;
+}
+
+const size_t pixbuf_size(pixbuf *p) {
+  return p->npix * p->nchan;
+}
+
+/*
+ * Construct a pixbuf newuserdata using C arguments.
+ *
+ * Allocates, so may throw!  Leaves new buffer at the top of the Lua stack
+ * and returns a C pointer.
+ */
+static pixbuf *pixbuf_new(lua_State *L, size_t leds, size_t chans) {
+  // Allocate memory
+
+  // A crude hack of an overflow check, but unlikely to be reached in practice
+  if ((leds > 8192) || (chans > 32)) {
+    luaL_error(L, "pixbuf size limits exeeded");
+    return NULL; // UNREACHED
+  }
+
+  size_t size = sizeof(pixbuf) + leds * chans;
+
+  pixbuf *buffer = (pixbuf*)lua_newuserdata(L, size);
+
+  // Associate its metatable
+  luaL_getmetatable(L, PIXBUF_METATABLE);
+  lua_setmetatable(L, -2);
+
+  // Save led strip size
+  *(size_t *)&buffer->npix = leds;
+  *(size_t *)&buffer->nchan = chans;
+
+  memset(buffer->values, 0, leds * chans);
+
+  return buffer;
+}
+
+// Handle a buffer where we can store led values
+int pixbuf_new_lua(lua_State *L) {
+  const int leds  = luaL_checkint(L, 1);
+  const int chans = luaL_checkint(L, 2);
+
+  luaL_argcheck(L, leds > 0, 1, "should be a positive integer");
+  luaL_argcheck(L, chans > 0, 2, "should be a positive integer");
+
+  pixbuf_new(L, leds, chans);
+  return 1;
+}
+
+static int pixbuf_concat_lua(lua_State *L) {
+  pixbuf *lhs = pixbuf_from_lua_arg(L, 1);
+  pixbuf *rhs = pixbuf_from_lua_arg(L, 2);
+
+  luaL_argcheck(L, lhs->nchan == rhs->nchan, 1,
+                "can only concatenate buffers with same channel count");
+
+  size_t osize = lhs->npix + rhs->npix;
+  if (lhs->npix > osize) {
+    return luaL_error(L, "size sum overflow");
+  }
+
+  pixbuf *buffer = pixbuf_new(L, osize, lhs->nchan);
+
+  memcpy(buffer->values, lhs->values, pixbuf_size(lhs));
+  memcpy(buffer->values + pixbuf_size(lhs), rhs->values, pixbuf_size(rhs));
+
+  return 1;
+}
+
+static int pixbuf_channels_lua(lua_State *L) {
+  pixbuf *buffer = pixbuf_from_lua_arg(L, 1);
+  lua_pushinteger(L, buffer->nchan);
+  return 1;
+}
+
+static int pixbuf_dump_lua(lua_State *L) {
+  pixbuf *buffer = pixbuf_from_lua_arg(L, 1);
+  lua_pushlstring(L, (char*)buffer->values, pixbuf_size(buffer));
+  return 1;
+}
+
+static int pixbuf_eq_lua(lua_State *L) {
+  bool res;
+
+  pixbuf *lhs = pixbuf_from_lua_arg(L, 1);
+  pixbuf *rhs = pixbuf_from_lua_arg(L, 2);
+
+  if (lhs->npix != rhs->npix) {
+    res = false;
+  } else if (lhs->nchan != rhs->nchan) {
+    res = false;
+  } else {
+    res = true;
+    for(size_t i = 0; i < pixbuf_size(lhs); i++) {
+      if(lhs->values[i] != rhs->values[i]) {
+        res = false;
+        break;
+      }
+    }
+  }
+
+  lua_pushboolean(L, res);
+  return 1;
+}
+
+static int pixbuf_fade_lua(lua_State *L) {
+  pixbuf *buffer = pixbuf_from_lua_arg(L, 1);
+  const int fade = luaL_checkinteger(L, 2);
+  unsigned direction = luaL_optinteger( L, 3, PIXBUF_FADE_OUT );
+
+  luaL_argcheck(L, fade > 0, 2, "fade value should be a strictly positive int");
+
+  uint8_t *p = &buffer->values[0];
+  for (size_t i = 0; i < pixbuf_size(buffer); i++)
+  {
+    if (direction == PIXBUF_FADE_OUT)
+    {
+      *p++ /= fade;
+    }
+    else
+    {
+      // as fade in can result in value overflow, an int is used to perform the check afterwards
+      int val = *p * fade;
+      *p++ = MIN(255, val);
+    }
+  }
+
+  return 0;
+}
+
+/* Fade an Ixxx-type strip by just manipulating the I bytes */
+static int pixbuf_fadeI_lua(lua_State *L) {
+  pixbuf *buffer = pixbuf_from_lua_arg(L, 1);
+  const int fade = luaL_checkinteger(L, 2);
+  unsigned direction = luaL_optinteger( L, 3, PIXBUF_FADE_OUT );
+
+  luaL_argcheck(L, fade > 0, 2, "fade value should be a strictly positive int");
+
+  uint8_t *p = &buffer->values[0];
+  for (size_t i = 0; i < buffer->npix; i++, p+=buffer->nchan) {
+    if (direction == PIXBUF_FADE_OUT) {
+      *p /= fade;
+    } else {
+      int val = *p * fade;
+      *p++ = MIN(255, val);
+    }
+  }
+
+  return 0;
+}
+
+static int pixbuf_fill_lua(lua_State *L) {
+  pixbuf *buffer = pixbuf_from_lua_arg(L, 1);
+
+  if (buffer->npix == 0) {
+    goto out;
+  }
+
+  if (lua_gettop(L) != (1 + buffer->nchan)) {
+    return luaL_argerror(L, 1, "need as many values as colors per pixel");
+  }
+
+  /* Fill the first pixel from the Lua stack */
+  for (size_t i = 0; i < buffer->nchan; i++) {
+    buffer->values[i] = luaL_checkinteger(L, 2+i);
+  }
+
+  /* Fill the rest of the pixels from the first */
+  for (size_t i = 1; i < buffer->npix; i++) {
+    memcpy(&buffer->values[i * buffer->nchan], buffer->values, buffer->nchan);
+  }
+
+out:
+  lua_settop(L, 1);
+  return 1;
+}
+
+static int pixbuf_get_lua(lua_State *L) {
+  pixbuf *buffer = pixbuf_from_lua_arg(L, 1);
+  const int led = luaL_checkinteger(L, 2) - 1;
+  size_t channels = buffer->nchan;
+
+  luaL_argcheck(L, led >= 0 && led < buffer->npix, 2, "index out of range");
+
+  uint8_t tmp[channels];
+  memcpy(tmp, &buffer->values[channels*led], channels);
+
+  for (size_t i = 0; i < channels; i++)
+  {
+    lua_pushinteger(L, tmp[i]);
+  }
+
+  return channels;
+}
+
+/* :map(f, buf1, ilo, ihi, [buf2, ilo2]) */
+static int pixbuf_map_lua(lua_State *L) {
+  pixbuf *outbuf = pixbuf_from_lua_arg(L, 1);
+  /* f at index 2 */
+
+  pixbuf *buffer1 = pixbuf_opt_from_lua_arg(L, 3);
+  if (!buffer1)
+    buffer1 = outbuf;
+
+  const int ilo = posrelat(luaL_optinteger(L, 4, 1), buffer1->npix) - 1;
+  const int ihi = posrelat(luaL_optinteger(L, 5, buffer1->npix), buffer1->npix) - 1;
+
+  luaL_argcheck(L, ihi > ilo, 3, "Buffer limits out of order");
+
+  size_t npix = ihi - ilo + 1;
+
+  luaL_argcheck(L, npix == outbuf->npix, 1, "Output buffer wrong size");
+
+  pixbuf *buffer2 = pixbuf_opt_from_lua_arg(L, 6);
+  const int ilo2 = buffer2 ? posrelat(luaL_optinteger(L, 7, 1), buffer2->npix) - 1 : 0;
+
+  if (buffer2) {
+    luaL_argcheck(L, ilo2 + npix <= buffer2->npix, 6, "Second buffer too short");
+  }
+
+  for (size_t p = 0; p < npix; p++) {
+    lua_pushvalue(L, 2);
+    for (size_t c = 0; c < buffer1->nchan; c++) {
+      lua_pushinteger(L, buffer1->values[(ilo + p) * buffer1->nchan + c]);
+    }
+    if (buffer2) {
+      for (size_t c = 0; c < buffer2->nchan; c++) {
+        lua_pushinteger(L, buffer2->values[(ilo2 + p) * buffer2->nchan + c]);
+      }
+    }
+    lua_call(L, buffer1->nchan + (buffer2 ? buffer2->nchan : 0), outbuf->nchan);
+    for (size_t c = 0; c < outbuf->nchan; c++) {
+      outbuf->values[(p + 1) * outbuf->nchan - c - 1] = luaL_checkinteger(L, -1);
+      lua_pop(L, 1);
+    }
+  }
+
+  lua_settop(L, 1);
+  return 1;
+}
+
+struct mix_source {
+  int factor;
+  const uint8_t *values;
+};
+
+static uint32_t pixbuf_mix_clamp(int32_t v) {
+  if (v <   0) { return   0; }
+  if (v > 255) { return 255; }
+  return v;
+}
+
+/* This one can sum straightforwardly, channel by channel */
+static void pixbuf_mix_raw(pixbuf *out, size_t n_src, struct mix_source* src) {
+  size_t cells = pixbuf_size(out);
+
+  for (size_t c = 0; c < cells; c++) {
+    int32_t val = 0;
+    for (size_t s = 0; s < n_src; s++) {
+      val += (int32_t)src[s].values[c] * src[s].factor;
+    }
+
+    val += 128; // rounding instead of floor
+    val /= 256; // do not use implemetation dependant right shift
+
+    out->values[c] = (uint8_t)pixbuf_mix_clamp(val);
+  }
+}
+
+/* Mix intensity-mediated three-color pixbufs.
+ *
+ * XXX This is untested in real hardware; do they actually behave like this?
+ */
+static void pixbuf_mix_i3(pixbuf *out, size_t ibits, size_t n_src,
+    struct mix_source* src) {
+  for(size_t p = 0; p < out->npix; p++) {
+    int32_t sums[3] = { 0, 0, 0 };
+
+    for (size_t s = 0; s < n_src; s++) {
+      for (size_t c = 0; c < 3; c++) {
+        sums[c] += (int32_t)src[s].values[4*p+c+1] // color channel
+                   * src[s].values[4*p]            // global intensity
+                   * src[s].factor;                // user factor
+
+      }
+    }
+
+    uint32_t pmaxc = 0;
+    for (size_t c = 0; c < 3; c++) {
+      pmaxc = sums[c] > pmaxc ? sums[c] : pmaxc;
+    }
+
+    size_t maxgi;
+    if (pmaxc == 0) {
+      /* Zero value */
+      memset(&out->values[4*p], 0, 4);
+      return;
+    } else if (pmaxc <= (1 << 16)) {
+      /* Minimum global factor */
+      maxgi = 1;
+    } else if (pmaxc >= ((1 << ibits) - 1) << 16) {
+      /* Maximum global factor */
+      maxgi = (1 << ibits) - 1;
+    } else {
+      maxgi = (pmaxc >> 16) + 1;
+    }
+
+    // printf("mixi3: %x %x %x -> %x, %zx\n", sums[0], sums[1], sums[2], pmaxc, maxgi);
+
+    out->values[4*p] = maxgi;
+    for (size_t c = 0; c < 3; c++) {
+      out->values[4*p+c+1] = pixbuf_mix_clamp((sums[c] + 256 * maxgi - 127) / (256 * maxgi));
+    }
+  }
+}
+
+// buffer:mix(factor1, buffer1, ..)
+// factor is 256 for 100%
+// uses saturating arithmetic (one buffer at a time)
+static int pixbuf_mix_core(lua_State *L, size_t ibits) {
+  pixbuf *buffer = pixbuf_from_lua_arg(L, 1);
+  pixbuf *src_buffer;
+
+  int pos = 2;
+  size_t n_sources = (lua_gettop(L) - 1) / 2;
+  struct mix_source sources[n_sources];
+
+  if (n_sources == 0) {
+    lua_settop(L, 1);
+    return 1;
+  }
+
+  for (size_t src = 0; src < n_sources; src++, pos += 2) {
+    int factor = luaL_checkinteger(L, pos);
+    src_buffer = pixbuf_from_lua_arg(L, pos + 1);
+
+    luaL_argcheck(L, src_buffer->npix == buffer->npix &&
+                     src_buffer->nchan == buffer->nchan,
+                     pos + 1, "buffer not same size or shape");
+
+    sources[src].factor = factor;
+    sources[src].values = src_buffer->values;
+  }
+
+  if (ibits != 0) {
+    luaL_argcheck(L, src_buffer->nchan == 4, 2, "Requires 4 channel pixbuf");
+    pixbuf_mix_i3(buffer, ibits, n_sources, sources);
+  } else {
+    pixbuf_mix_raw(buffer, n_sources, sources);
+  }
+
+  lua_settop(L, 1);
+  return 1;
+}
+
+static int pixbuf_mix_lua(lua_State *L) {
+  return pixbuf_mix_core(L, 0);
+}
+
+static int pixbuf_mix4I5_lua(lua_State *L) {
+  return pixbuf_mix_core(L, 5);
+}
+
+
+// Returns the total of all channels
+static int pixbuf_power_lua(lua_State *L) {
+  pixbuf *buffer = pixbuf_from_lua_arg(L, 1);
+
+  int total = 0;
+  size_t p = 0;
+  for (size_t i = 0; i < buffer->npix; i++) {
+    for (size_t j = 0; j < buffer->nchan; j++, p++) {
+      total += buffer->values[p];
+    }
+  }
+
+  lua_pushinteger(L, total);
+  return 1;
+}
+
+// Returns the total of all channels, intensity-style
+static int pixbuf_powerI_lua(lua_State *L) {
+  pixbuf *buffer = pixbuf_from_lua_arg(L, 1);
+
+  int total = 0;
+  size_t p = 0;
+  for (size_t i = 0; i < buffer->npix; i++) {
+    int inten = buffer->values[p++];
+    for (size_t j = 0; j < buffer->nchan - 1; j++, p++) {
+      total += inten * buffer->values[p];
+    }
+  }
+
+  lua_pushinteger(L, total);
+  return 1;
+}
+
+static int pixbuf_replace_lua(lua_State *L) {
+  pixbuf *buffer = pixbuf_from_lua_arg(L, 1);
+  ptrdiff_t start = posrelat(luaL_optinteger(L, 3, 1), buffer->npix);
+  size_t channels = buffer->nchan;
+
+  uint8_t *src;
+  size_t srcLen;
+
+  if (lua_type(L, 2) == LUA_TSTRING) {
+    size_t length;
+    src = (uint8_t *) lua_tolstring(L, 2, &length);
+    srcLen = length / channels;
+  } else {
+    pixbuf *rhs = pixbuf_from_lua_arg(L, 2);
+    luaL_argcheck(L, rhs->nchan == buffer->nchan, 2, "buffers have different channels");
+    src = rhs->values;
+    srcLen = rhs->npix;
+  }
+
+  luaL_argcheck(L, srcLen + start - 1 <= buffer->npix, 2, "does not fit into destination");
+
+  memcpy(buffer->values + (start - 1) * channels, src, srcLen * channels);
+
+  return 0;
+}
+
+static int pixbuf_set_lua(lua_State *L) {
+
+  pixbuf *buffer = pixbuf_from_lua_arg(L, 1);
+  const int led = luaL_checkinteger(L, 2) - 1;
+  const size_t channels = buffer->nchan;
+
+  luaL_argcheck(L, led >= 0 && led < buffer->npix, 2, "index out of range");
+
+  int type = lua_type(L, 3);
+  if(type == LUA_TTABLE)
+  {
+    for (size_t i = 0; i < channels; i++)
+    {
+      lua_rawgeti(L, 3, i+1);
+      buffer->values[channels*led+i] = lua_tointeger(L, -1);
+      lua_pop(L, 1);
+    }
+  }
+  else if(type == LUA_TSTRING)
+  {
+    size_t len;
+    const char *buf = lua_tolstring(L, 3, &len);
+
+    // Overflow check
+    if( channels*led + len > channels*buffer->npix ) {
+      return luaL_error(L, "string size will exceed strip length");
+    }
+    if ( len % channels != 0 ) {
+      return luaL_error(L, "string does not contain whole LEDs");
+    }
+
+    memcpy(&buffer->values[channels*led], buf, len);
+  }
+  else
+  {
+    luaL_argcheck(L, lua_gettop(L) <= 2 + channels, 2 + channels,
+        "extra values given");
+
+    for (size_t i = 0; i < channels; i++)
+    {
+      buffer->values[channels*led+i] = luaL_checkinteger(L, 3+i);
+    }
+  }
+
+  lua_settop(L, 1);
+  return 1;
+}
+
+static void pixbuf_shift_circular(pixbuf *buffer, struct pixbuf_shift_params *sp) {
+  /* Move a buffer of pixels per iteration; loop repeatedly if needed */
+  uint8_t tmpbuf[32];
+  uint8_t *v = buffer->values;
+  size_t shiftRemaining = sp->shift;
+  size_t cursor = sp->offset;
+
+  do {
+    size_t shiftNow = MIN(shiftRemaining, sizeof tmpbuf);
+
+    if (sp->shiftLeft) {
+      memcpy(tmpbuf, &v[cursor], shiftNow);
+      memmove(&v[cursor], &v[cursor+shiftNow], sp->window - shiftNow);
+      memcpy(&v[cursor+sp->window-shiftNow], tmpbuf, shiftNow);
+    } else {
+      memcpy(tmpbuf, &v[cursor+sp->window-shiftNow], shiftNow);
+      memmove(&v[cursor+shiftNow], &v[cursor], sp->window - shiftNow);
+      memcpy(&v[cursor], tmpbuf, shiftNow);
+    }
+
+    cursor += shiftNow;
+    shiftRemaining -= shiftNow;
+  } while(shiftRemaining > 0);
+}
+
+static void pixbuf_shift_logical(pixbuf *buffer, struct pixbuf_shift_params *sp) {
+  /* Logical shifts don't require a temporary buffer, so we just move bytes */
+  uint8_t *v = buffer->values;
+
+  if (sp->shiftLeft) {
+    memmove(&v[sp->offset], &v[sp->offset+sp->shift], sp->window - sp->shift);
+    bzero(&v[sp->offset+sp->window-sp->shift], sp->shift);
+  } else {
+    memmove(&v[sp->offset+sp->shift], &v[sp->offset], sp->window - sp->shift);
+    bzero(&v[sp->offset], sp->shift);
+  }
+}
+
+void pixbuf_shift(pixbuf *b, struct pixbuf_shift_params *sp) {
+#if 0
+  printf("Pixbuf %p shifting %s %s by %zd from %zd with window %zd\n",
+         b,
+	 sp->shiftLeft ? "left" : "right",
+	 sp->type == PIXBUF_SHIFT_LOGICAL ? "logically" : "circularly",
+	 sp->shift, sp->offset, sp->window);
+#endif
+
+  switch(sp->type) {
+  case PIXBUF_SHIFT_LOGICAL: return pixbuf_shift_logical(b, sp);
+  case PIXBUF_SHIFT_CIRCULAR: return pixbuf_shift_circular(b, sp);
+  }
+}
+
+int pixbuf_shift_lua(lua_State *L) {
+  struct pixbuf_shift_params sp;
+
+  pixbuf *buffer = pixbuf_from_lua_arg(L, 1);
+  const int shift_shift = luaL_checkinteger(L, 2) * buffer->nchan;
+  const unsigned shift_type = luaL_optinteger(L, 3, PIXBUF_SHIFT_LOGICAL);
+  const int pos_start = posrelat(luaL_optinteger(L, 4, 1), buffer->npix);
+  const int pos_end = posrelat(luaL_optinteger(L, 5, -1), buffer->npix);
+
+  if (shift_shift < 0) {
+    sp.shiftLeft = true;
+    sp.shift = -shift_shift;
+  } else {
+    sp.shiftLeft = false;
+    sp.shift = shift_shift;
+  }
+
+  switch(shift_type) {
+  case PIXBUF_SHIFT_LOGICAL:
+  case PIXBUF_SHIFT_CIRCULAR:
+    sp.type = shift_type;
+    break;
+  default:
+    return luaL_argerror(L, 3, "invalid shift type");
+  }
+
+  if (pos_start < 1) {
+    return luaL_argerror(L, 4, "start position must be >= 1");
+  }
+
+  if (pos_end < pos_start) {
+    return luaL_argerror(L, 5, "end position must be >= start");
+  }
+
+  sp.offset = (pos_start - 1) * buffer->nchan;
+  sp.window = (pos_end - pos_start + 1) * buffer->nchan;
+
+  if (sp.shift > pixbuf_size(buffer)) {
+    return luaL_argerror(L, 2, "shifting more elements than buffer size");
+  }
+
+  if (sp.shift > sp.window) {
+    return luaL_argerror(L, 2, "shifting more than sliced window");
+  }
+
+  pixbuf_shift(buffer, &sp);
+
+  return 0;
+}
+
+/* XXX for backwards-compat with ws2812_effects; deprecated and should be removed */
+void pixbuf_prepare_shift(pixbuf *buffer, struct pixbuf_shift_params *sp,
+    int shift, enum pixbuf_shift type, int start, int end)
+{
+  start = posrelat(start, buffer->npix);
+  end = posrelat(end, buffer->npix);
+
+  lua_assert((end > start) && (start > 0) && (end < buffer->npix));
+
+  sp->type   = type;
+  sp->offset = (start - 1) * buffer->nchan;
+  sp->window = (end - start + 1) * buffer->nchan;
+
+  if (shift < 0) {
+    sp->shiftLeft = true;
+    sp->shift = -shift * buffer->nchan;
+  } else {
+    sp->shiftLeft = false;
+    sp->shift = shift * buffer->nchan;
+  }
+}
+
+static int pixbuf_size_lua(lua_State *L) {
+  pixbuf *buffer = pixbuf_from_lua_arg(L, 1);
+  lua_pushinteger(L, buffer->npix);
+  return 1;
+}
+
+static int pixbuf_sub_lua(lua_State *L) {
+  pixbuf *lhs = pixbuf_from_lua_arg(L, 1);
+  size_t l = lhs->npix;
+  ssize_t start = posrelat(luaL_checkinteger(L, 2), l);
+  ssize_t end = posrelat(luaL_optinteger(L, 3, -1), l);
+  if (start <= end) {
+    pixbuf *result = pixbuf_new(L, end - start + 1, lhs->nchan);
+    memcpy(result->values, lhs->values + lhs->nchan * (start - 1),
+           lhs->nchan * (end - start + 1));
+    return 1;
+  } else {
+    pixbuf_new(L, 0, lhs->nchan);
+    return 1;
+  }
+}
+
+static int pixbuf_tostring_lua(lua_State *L) {
+  pixbuf *buffer = pixbuf_from_lua_arg(L, 1);
+
+  luaL_Buffer result;
+  luaL_buffinit(L, &result);
+
+  luaL_addchar(&result, '[');
+  int p = 0;
+  for (size_t i = 0; i < buffer->npix; i++) {
+    if (i > 0) {
+      luaL_addchar(&result, ',');
+    }
+    luaL_addchar(&result, '(');
+    for (size_t j = 0; j < buffer->nchan; j++, p++) {
+      if (j > 0) {
+        luaL_addchar(&result, ',');
+      }
+      char numbuf[5];
+      sprintf(numbuf, "%d", buffer->values[p]);
+      luaL_addstring(&result, numbuf);
+    }
+    luaL_addchar(&result, ')');
+  }
+
+  luaL_addchar(&result, ']');
+  luaL_pushresult(&result);
+
+  return 1;
+}
+
+LROT_BEGIN(pixbuf_map, NULL, LROT_MASK_INDEX | LROT_MASK_EQ)
+  LROT_TABENTRY ( __index, pixbuf_map )
+  LROT_FUNCENTRY( __eq, pixbuf_eq_lua )
+
+  LROT_FUNCENTRY( __concat, pixbuf_concat_lua )
+  LROT_FUNCENTRY( __tostring, pixbuf_tostring_lua )
+
+  LROT_FUNCENTRY( channels, pixbuf_channels_lua )
+  LROT_FUNCENTRY( dump, pixbuf_dump_lua )
+  LROT_FUNCENTRY( fade, pixbuf_fade_lua )
+  LROT_FUNCENTRY( fadeI, pixbuf_fadeI_lua )
+  LROT_FUNCENTRY( fill, pixbuf_fill_lua )
+  LROT_FUNCENTRY( get, pixbuf_get_lua )
+  LROT_FUNCENTRY( replace, pixbuf_replace_lua )
+  LROT_FUNCENTRY( map, pixbuf_map_lua )
+  LROT_FUNCENTRY( mix, pixbuf_mix_lua )
+  LROT_FUNCENTRY( mix4I5, pixbuf_mix4I5_lua )
+  LROT_FUNCENTRY( power, pixbuf_power_lua )
+  LROT_FUNCENTRY( powerI, pixbuf_powerI_lua )
+  LROT_FUNCENTRY( set, pixbuf_set_lua )
+  LROT_FUNCENTRY( shift, pixbuf_shift_lua )
+  LROT_FUNCENTRY( size, pixbuf_size_lua )
+  LROT_FUNCENTRY( sub, pixbuf_sub_lua )
+LROT_END(pixbuf_map, NULL, LROT_MASK_INDEX | LROT_MASK_EQ)
+
+LROT_BEGIN(pixbuf, NULL, 0)
+  LROT_NUMENTRY( FADE_IN, PIXBUF_FADE_IN )
+  LROT_NUMENTRY( FADE_OUT, PIXBUF_FADE_OUT )
+
+  LROT_NUMENTRY( SHIFT_CIRCULAR, PIXBUF_SHIFT_CIRCULAR )
+  LROT_NUMENTRY( SHIFT_LOGICAL, PIXBUF_SHIFT_LOGICAL )
+
+  LROT_FUNCENTRY( newBuffer, pixbuf_new_lua )
+LROT_END(pixbuf, NULL, 0)
+
+int luaopen_pixbuf(lua_State *L) {
+  luaL_rometatable(L, PIXBUF_METATABLE, LROT_TABLEREF(pixbuf_map));
+  lua_pushrotable(L, LROT_TABLEREF(pixbuf));
+  return 1;
+}
+
+NODEMCU_MODULE(PIXBUF, "pixbuf", pixbuf, luaopen_pixbuf);

+ 50 - 0
app/modules/pixbuf.h

@@ -0,0 +1,50 @@
+#ifndef APP_MODULES_PIXBUF_H_
+#define APP_MODULES_PIXBUF_H_
+
+typedef struct pixbuf {
+  const size_t npix;
+  const size_t nchan;
+
+  /* Flexible Array Member; true size is npix * pixbuf_channels_for(type) */
+  uint8_t values[];
+} pixbuf;
+
+enum pixbuf_fade {
+  PIXBUF_FADE_IN,
+  PIXBUF_FADE_OUT
+};
+
+enum pixbuf_shift {
+  PIXBUF_SHIFT_LOGICAL,
+  PIXBUF_SHIFT_CIRCULAR
+};
+
+pixbuf *pixbuf_from_lua_arg(lua_State *, int);
+const size_t pixbuf_size(pixbuf *);
+
+// Exported for backwards compat with ws2812 module
+int pixbuf_new_lua(lua_State *);
+
+/*
+ * WS2812_EFFECTS does pixbuf manipulation directly in C, which isn't the
+ * intended use case, but for backwards compat, we export just what it needs.
+ * Move this struct to pixbuf.c and mark these exports static instead once
+ * WS2812_EFFECTS is no more.
+ */
+struct pixbuf_shift_params {
+  enum pixbuf_shift type;
+    // 0 <= offset <= buffer length
+  size_t offset;
+    // 0 <= window + offset <= buffer length
+  size_t window;
+    // 0 <= shift <= window_size
+  size_t shift;
+  bool   shiftLeft;
+};
+void pixbuf_shift(pixbuf *, struct pixbuf_shift_params *);
+void pixbuf_prepare_shift(pixbuf *, struct pixbuf_shift_params *,
+    int val, enum pixbuf_shift, int start, int end);
+const size_t pixbuf_channels(pixbuf *);
+/* end WS2812_EFFECTS exports */
+
+#endif

+ 29 - 29
app/modules/tm1829.c

@@ -5,6 +5,8 @@
 #include <string.h>
 #include "user_interface.h"
 
+#include "pixbuf.h"
+
 static inline uint32_t _getCycleCount(void) {
   uint32_t cycles;
   __asm__ __volatile__("rsr %0,ccount":"=a" (cycles));
@@ -13,8 +15,9 @@ static inline uint32_t _getCycleCount(void) {
 
 // This algorithm reads the cpu clock cycles to calculate the correct
 // pulse widths. It works in both 80 and 160 MHz mode.
-static void ICACHE_RAM_ATTR tm1829_write_to_pin(uint8_t pin, uint8_t *pixels, uint32_t length) {
-  uint8_t *p, *end;
+static void ICACHE_RAM_ATTR tm1829_write_to_pin(uint8_t pin, const uint8_t *pixels, size_t length) {
+  const uint8_t *p, *end;
+  uint8_t phasergb = 0;
 
   p   =  pixels;
   end =  p + length;
@@ -28,6 +31,13 @@ static void ICACHE_RAM_ATTR tm1829_write_to_pin(uint8_t pin, uint8_t *pixels, ui
     register int i;
 
     register uint8_t pixel = *p++;
+    if ((phasergb == 0) && (pixel == 0xFF)) {
+      // clamp initial byte value to avoid constant-current shenanigans.  Yuck!
+      pixel = 0xFE;
+    }
+    if (++phasergb == 3) {
+      phasergb = 0;
+    }
 
     ets_intr_lock();
 
@@ -55,35 +65,27 @@ static void ICACHE_RAM_ATTR tm1829_write_to_pin(uint8_t pin, uint8_t *pixels, ui
 }
 
 // Lua: tm1829.write(pin, "string")
-// Byte triples in the string are interpreted as R G B values and sent to the hardware as G R B.
-// WARNING: this function scrambles the input buffer :
-//    a = string.char(255,0,128)
-//    tm1829.write(3,a)
-//    =a.byte()
-//    (0,255,128)
+// Byte triples in the string are interpreted as GRB values.
 static int ICACHE_FLASH_ATTR tm1829_write(lua_State* L)
 {
   const uint8_t pin = luaL_checkinteger(L, 1);
+  const uint8_t *pixels;
   size_t length;
-  const char *rgb = luaL_checklstring(L, 2, &length);
-
-  // dont modify lua-internal lstring - make a copy instead
-  char *buffer = (char *)malloc(length);
 
-  // Ignore incomplete Byte triples at the end of buffer
-  length -= length % 3;
-
-  // Copy payload and make sure first byte is < 0xFF (triggers
-  // constant current command, instead of PWM duty command)
-  size_t i;
-  for (i = 0; i < length; i += 3) {
-    buffer[i]     = rgb[i];
-    buffer[i + 1] = rgb[i + 1];
-    buffer[i + 2] = rgb[i + 2];
-
-    // Check for first byte
-    if (buffer[i] == 0xff)
-        buffer[i] = 0xfe;
+  switch(lua_type(L, 3)) {
+  case LUA_TSTRING: {
+    pixels = luaL_checklstring(L, 2, &length);
+    break;
+   }
+  case LUA_TUSERDATA: {
+    pixbuf *buffer = pixbuf_from_lua_arg(L, 2);
+    luaL_argcheck(L, pixbuf_channels(buffer) == 3, 2, "Bad pixbuf format");
+    pixels = buffer->values;
+    length = 3 * buffer->npix;
+    break;
+   }
+  default:
+    return luaL_argerror(L, 2, "String or pixbuf expected");
   }
 
   // Initialize the output pin and wait a bit
@@ -91,12 +93,10 @@ static int ICACHE_FLASH_ATTR tm1829_write(lua_State* L)
   platform_gpio_write(pin, 1);
 
   // Send the buffer
-  tm1829_write_to_pin(pin_num[pin], (uint8_t*) buffer, length);
+  tm1829_write_to_pin(pin_num[pin], pixels, length);
 
   os_delay_us(500); // reset time
 
-  free(buffer);
-
   return 0;
 }
 

+ 19 - 2
app/modules/ws2801.c

@@ -5,6 +5,8 @@
 #include <string.h>
 #include "osapi.h"
 
+#include "pixbuf.h"
+
 /**
  * Code is based on https://github.com/CHERTS/esp8266-devkit/blob/master/Espressif/examples/EspLightNode/user/ws2801.c
  * and provides a similar api as the ws2812 module.
@@ -110,13 +112,28 @@ static int ICACHE_FLASH_ATTR ws2801_init_lua(lua_State* L) {
  */
 static int ICACHE_FLASH_ATTR ws2801_writergb(lua_State* L) {
     size_t length;
-    const char *buffer = luaL_checklstring(L, 1, &length);
+    const uint8_t *values;
+
+    switch(lua_type(L,1)) {
+    case LUA_TSTRING:
+      values = (const uint8_t*) luaL_checklstring(L, 1, &length);
+      break;
+    case LUA_TUSERDATA: {
+      pixbuf *buffer = pixbuf_from_lua_arg(L, 1);
+      luaL_argcheck(L, buffer->nchan == 3, 1, "Pixbuf not 3-channel");
+      values = buffer->values;
+      length = pixbuf_size(buffer);
+      break;
+    }
+    default:
+      return luaL_argerror(L, 1, "pixbuf or string expected");
+    }
 
     os_delay_us(10);
 
     ets_intr_lock();
 
-    ws2801_strip(buffer, length);
+    ws2801_strip(values, length);
 
     ets_intr_unlock();
 

+ 15 - 472
app/modules/ws2812.c

@@ -9,13 +9,11 @@
 #include "driver/uart.h"
 #include "osapi.h"
 
-#include "ws2812.h"
+#include "pixbuf.h"
 
-#define CANARY_VALUE 0x32383132
 #define MODE_SINGLE  0
 #define MODE_DUAL    1
 
-
 // Init UART1 to be able to stream WS2812 data to GPIO2 pin
 // If DUAL mode is selected, init UART0 to stream to TXD0 as well
 // You HAVE to redirect LUA's output somewhere else
@@ -125,14 +123,14 @@ static int ws2812_write(lua_State* L) {
   }
   else if (type == LUA_TUSERDATA)
   {
-    ws2812_buffer * buffer = (ws2812_buffer*)luaL_checkudata(L, 1, "ws2812.buffer");
-
+    pixbuf *buffer = pixbuf_from_lua_arg(L, 1);
+    luaL_argcheck(L, pixbuf_channels(buffer) == 3, 1, "Bad pixbuf format");
     buffer1 = buffer->values;
-    length1 = buffer->colorsPerLed*buffer->size;
+    length1 = pixbuf_size(buffer);
   }
   else
   {
-    luaL_argerror(L, 1, "ws2812.buffer or string expected");
+    luaL_argerror(L, 1, "pixbuf or string expected");
   }
 
   // Second optionnal parameter
@@ -148,14 +146,14 @@ static int ws2812_write(lua_State* L) {
   }
   else if (type == LUA_TUSERDATA)
   {
-    ws2812_buffer * buffer = (ws2812_buffer*)luaL_checkudata(L, 2, "ws2812.buffer");
-
+    pixbuf *buffer = pixbuf_from_lua_arg(L, 2);
+    luaL_argcheck(L, pixbuf_channels(buffer) == 3, 2, "Bad pixbuf format");
     buffer2 = buffer->values;
-    length2 = buffer->colorsPerLed*buffer->size;
+    length2 = pixbuf_size(buffer);
   }
   else
   {
-    luaL_argerror(L, 2, "ws2812.buffer or string expected");
+    luaL_argerror(L, 2, "pixbuf or string expected");
   }
 
   // Send the buffers
@@ -164,475 +162,20 @@ static int ws2812_write(lua_State* L) {
   return 0;
 }
 
-static ptrdiff_t posrelat(ptrdiff_t pos, size_t len) {
-  /* relative string position: negative means back from end */
-  if (pos < 0) pos += (ptrdiff_t)len + 1;
-  return MIN(MAX(pos, 1), len);
-}
-
-static ws2812_buffer *allocate_buffer(lua_State *L, int leds, int colorsPerLed) {
-  // Allocate memory
-  size_t size = sizeof(ws2812_buffer) + colorsPerLed*leds;
-  ws2812_buffer * buffer = (ws2812_buffer*)lua_newuserdata(L, size);
-
-  // Associate its metatable
-  luaL_getmetatable(L, "ws2812.buffer");
-  lua_setmetatable(L, -2);
-
-  // Save led strip size
-  buffer->size = leds;
-  buffer->colorsPerLed = colorsPerLed;
-
-  return buffer;
-}
-
-
-// Handle a buffer where we can store led values
-static int ws2812_new_buffer(lua_State *L) {
-  const int leds = luaL_checkint(L, 1);
-  const int colorsPerLed = luaL_checkint(L, 2);
-
-  luaL_argcheck(L, leds > 0, 1, "should be a positive integer");
-  luaL_argcheck(L, colorsPerLed > 0, 2, "should be a positive integer");
-
-  ws2812_buffer * buffer = allocate_buffer(L, leds, colorsPerLed);
-
-  memset(buffer->values, 0, colorsPerLed * leds);
-
-  return 1;
-}
-
-
-int ws2812_buffer_fill(ws2812_buffer * buffer, int * colors) {
-
-  // Grab colors
-  int i, j;
-
-  // Fill buffer
-  uint8_t * p = &buffer->values[0];
-  for(i = 0; i < buffer->size; i++)
-  {
-    for (j = 0; j < buffer->colorsPerLed; j++)
-    {
-      *p++ = colors[j];
-    }
-  }
-
-  return 0;
-}
-
-static int ws2812_buffer_fill_lua(lua_State* L) {
-  ws2812_buffer * buffer = (ws2812_buffer*)luaL_checkudata(L, 1, "ws2812.buffer");
-
-  // Grab colors
-  int i;
-  int * colors = luaM_malloc(L, buffer->colorsPerLed * sizeof(int));
-
-  for (i = 0; i < buffer->colorsPerLed; i++)
-  {
-    colors[i] = luaL_checkinteger(L, 2+i);
-  }
-
-  ws2812_buffer_fill(buffer, colors);
-
-  // Free memory
-  luaM_free(L, colors);
-
-  return 0;
-}
-
-void ws2812_buffer_fade(ws2812_buffer * buffer, int fade, unsigned direction) {
-  uint8_t * p = &buffer->values[0];
-  int val = 0;
-  int i;
-  for (i = 0; i < buffer->size * buffer->colorsPerLed; i++)
-  {
-    if (direction == FADE_OUT)
-    {
-      *p++ /= fade;
-    }
-    else
-    {
-      // as fade in can result in value overflow, an int is used to perform the check afterwards
-      val = *p * fade;
-      if (val > 255) val = 255;
-      *p++ = val;
-    }
-  }
-}
-
-static int ws2812_buffer_fade_lua(lua_State* L) {
-  ws2812_buffer * buffer = (ws2812_buffer*)luaL_checkudata(L, 1, "ws2812.buffer");
-  const int fade = luaL_checkinteger(L, 2);
-  unsigned direction = luaL_optinteger( L, 3, FADE_OUT );
-
-  luaL_argcheck(L, fade > 0, 2, "fade value should be a strict positive number");
-
-  ws2812_buffer_fade(buffer, fade, direction);
-
-  return 0; 
-}
-
-int ws2812_buffer_shift(lua_State* L, ws2812_buffer * buffer, int shiftValue, unsigned shift_type, int pos_start, int pos_end){
-  
-  ws2812_buffer_shift_prepare* prepare = ws2812_buffer_get_shift_prepare(L, buffer, shiftValue, shift_type, pos_start, pos_end);
-  ws2812_buffer_shift_prepared(prepare);
-  // Free memory
-  luaM_freemem(L, prepare, sizeof(ws2812_buffer_shift_prepare) + prepare->shift_len);
-
-  return 0;
-}
-
-ws2812_buffer_shift_prepare* ws2812_buffer_get_shift_prepare(lua_State* L, ws2812_buffer * buffer, int shiftValue, unsigned shift_type, int pos_start, int pos_end){
-
-  ptrdiff_t start = posrelat(pos_start, buffer->size);
-  ptrdiff_t end = posrelat(pos_end, buffer->size);
-
-  start--;
-  int size = end - start;
-  size_t offset = start * buffer->colorsPerLed;
-
-  luaL_argcheck(L, shiftValue >= 0-size && shiftValue <= size, 2, "shifting more elements than buffer size");
-
-  int shift = shiftValue >= 0 ? shiftValue : -shiftValue;
-
-  size_t shift_len, remaining_len;
-  // calculate length of shift section and remaining section
-  shift_len = shift*buffer->colorsPerLed;
-  remaining_len = (size-shift)*buffer->colorsPerLed;
-
-  ws2812_buffer_shift_prepare* prepare = luaM_malloc(L, sizeof(ws2812_buffer_shift_prepare) + shift_len);
-  prepare->offset = offset;
-  prepare->tmp_pixels = (uint8_t*)(prepare+1);
-  prepare->shiftValue = shiftValue;
-  prepare->shift_len = shift_len;
-  prepare->remaining_len = remaining_len;
-  prepare->shift_type = shift_type;
-  prepare->buffer = buffer;
-
-  return prepare;
-}
-
-void ws2812_buffer_shift_prepared(ws2812_buffer_shift_prepare* prepare) {
-
-  // check if we want to shift at all
-  if (prepare->shift_len == 0 || (prepare->shift_len + prepare->remaining_len) <= 0)
-  {
-    return;
-  }
-
-  if (prepare->shiftValue > 0)
-  {
-    // Store the values which are moved out of the array (last n pixels)
-    memcpy(prepare->tmp_pixels, &prepare->buffer->values[prepare->offset + prepare->remaining_len], prepare->shift_len);
-    // Move pixels to end
-    os_memmove(&prepare->buffer->values[prepare->offset + prepare->shift_len], &prepare->buffer->values[prepare->offset], prepare->remaining_len);
-    // Fill beginning with temp data
-    if (prepare->shift_type == SHIFT_LOGICAL)
-    {
-      memset(&prepare->buffer->values[prepare->offset], 0, prepare->shift_len);
-    }
-    else
-    {
-      memcpy(&prepare->buffer->values[prepare->offset], prepare->tmp_pixels, prepare->shift_len);
-    }
-  }
-  else
-  {
-    // Store the values which are moved out of the array (last n pixels)
-    memcpy(prepare->tmp_pixels, &prepare->buffer->values[prepare->offset], prepare->shift_len);
-    // Move pixels to end
-    os_memmove(&prepare->buffer->values[prepare->offset], &prepare->buffer->values[prepare->offset + prepare->shift_len], prepare->remaining_len);
-    // Fill beginning with temp data
-    if (prepare->shift_type == SHIFT_LOGICAL)
-    {
-      memset(&prepare->buffer->values[prepare->offset + prepare->remaining_len], 0, prepare->shift_len);
-    }
-    else
-    {
-      memcpy(&prepare->buffer->values[prepare->offset + prepare->remaining_len], prepare->tmp_pixels, prepare->shift_len);
-    }
-  }
-}
-
-static int ws2812_buffer_shift_lua(lua_State* L) {
-
-  ws2812_buffer * buffer = (ws2812_buffer*)luaL_checkudata(L, 1, "ws2812.buffer");
-  const int shiftValue = luaL_checkinteger(L, 2);
-  const unsigned shift_type = luaL_optinteger( L, 3, SHIFT_LOGICAL );
-
-  const int pos_start = luaL_optinteger(L, 4, 1);
-  const int pos_end = luaL_optinteger(L, 5, -1);
-
-
-  ws2812_buffer_shift(L, buffer, shiftValue, shift_type, pos_start, pos_end);
-  return 0;
-}
-
-static int ws2812_buffer_dump(lua_State* L) {
-  ws2812_buffer * buffer = (ws2812_buffer*)luaL_checkudata(L, 1, "ws2812.buffer");
-
-  lua_pushlstring(L, buffer->values, buffer->size * buffer->colorsPerLed);
-
-  return 1;
-}
-
-static int ws2812_buffer_replace(lua_State* L) {
-  ws2812_buffer * buffer = (ws2812_buffer*)luaL_checkudata(L, 1, "ws2812.buffer");
-  ptrdiff_t start = posrelat(luaL_optinteger(L, 3, 1), buffer->size);
-
-  uint8_t *src;
-  size_t srcLen;
-
-  if (lua_type(L, 2) == LUA_TSTRING) {
-    size_t length;
-
-    src = (uint8_t *) lua_tolstring(L, 2, &length);
-    srcLen = length / buffer->colorsPerLed;
-  } else {
-    ws2812_buffer * rhs = (ws2812_buffer*)luaL_checkudata(L, 2, "ws2812.buffer");
-    src = rhs->values;
-    srcLen = rhs->size;
-    luaL_argcheck(L, rhs->colorsPerLed == buffer->colorsPerLed, 2, "Buffers have different colors");
-  }
-
-  luaL_argcheck(L, srcLen + start - 1 <= buffer->size, 2, "Does not fit into destination");
-
-  memcpy(buffer->values + (start - 1) * buffer->colorsPerLed, src, srcLen * buffer->colorsPerLed);
-
-  return 0;
-}
-
-// buffer:mix(factor1, buffer1, ..)
-// factor is 256 for 100%
-// uses saturating arithmetic (one buffer at a time)
-static int ws2812_buffer_mix(lua_State* L) {
-  ws2812_buffer * buffer = (ws2812_buffer*)luaL_checkudata(L, 1, "ws2812.buffer");
-
-  int pos = 2;
-  size_t cells = buffer->size * buffer->colorsPerLed;
-
-  int n_sources = (lua_gettop(L) - 1) / 2;
-
-  struct {
-    int factor;
-    const uint8_t *values;
-  } source[n_sources];
-
-  int src;
-  for (src = 0; src < n_sources; src++, pos += 2) {
-    int factor = luaL_checkinteger(L, pos);
-    ws2812_buffer *src_buffer = (ws2812_buffer*) luaL_checkudata(L, pos + 1, "ws2812.buffer");
-
-    luaL_argcheck(L, src_buffer->size == buffer->size && src_buffer->colorsPerLed == buffer->colorsPerLed, pos + 1, "Buffer not same shape");
-
-    source[src].factor = factor;
-    source[src].values = src_buffer->values;
-  }
-
-  size_t i;
-  for (i = 0; i < cells; i++) {
-    int32_t val = 0;
-    for (src = 0; src < n_sources; src++) {
-      val += (int32_t)(source[src].values[i] * source[src].factor);
-    }
-
-	  val += 128;	// rounding istead of floor
-    val /= 256; // do not use implemetation dependant right shift
-
-    if (val < 0) {
-      val = 0;
-    } else if (val > 255) {
-      val = 255;
-    }
-    buffer->values[i] = (uint8_t)val;
-  }
-
-  return 0;
-}
-
-// Returns the total of all channels
-static int ws2812_buffer_power(lua_State* L) {
-  ws2812_buffer * buffer = (ws2812_buffer*)luaL_checkudata(L, 1, "ws2812.buffer");
-
-  size_t cells = buffer->size * buffer->colorsPerLed;
-
-  size_t i;
-  int total = 0;
-  for (i = 0; i < cells; i++) {
-    total += buffer->values[i];
-  }
-
-  lua_pushinteger(L, total);
-
-  return 1;
-}
-
-static int ws2812_buffer_get(lua_State* L) {
-  ws2812_buffer * buffer = (ws2812_buffer*)luaL_checkudata(L, 1, "ws2812.buffer");
-  const int led = luaL_checkinteger(L, 2) - 1;
-
-  luaL_argcheck(L, led >= 0 && led < buffer->size, 2, "index out of range");
-
-  int i;
-  for (i = 0; i < buffer->colorsPerLed; i++)
-  {
-    lua_pushinteger(L, buffer->values[buffer->colorsPerLed*led+i]);
-  }
-
-  return buffer->colorsPerLed;
-}
-
-static int ws2812_buffer_set(lua_State* L) {
-  ws2812_buffer * buffer = (ws2812_buffer*)luaL_checkudata(L, 1, "ws2812.buffer");
-  const int led = luaL_checkinteger(L, 2) - 1;
-
-  luaL_argcheck(L, led >= 0 && led < buffer->size, 2, "index out of range");
-
-  int type = lua_type(L, 3);
-  if(type == LUA_TTABLE)
-  {
-    int i;
-    for (i = 0; i < buffer->colorsPerLed; i++)
-    {
-      // Get value and push it on stack
-      lua_rawgeti(L, 3, i+1);
-
-      // Convert it as int and store them in buffer
-      buffer->values[buffer->colorsPerLed*led+i] = lua_tointeger(L, -1);
-    }
-
-    // Clean up the stack
-    lua_pop(L, buffer->colorsPerLed);
-  }
-  else if(type == LUA_TSTRING)
-  {
-    size_t len;
-    const char * buf = lua_tolstring(L, 3, &len);
-
-    // Overflow check
-    if( buffer->colorsPerLed*led + len > buffer->colorsPerLed*buffer->size )
-    {
-      return luaL_error(L, "string size will exceed strip length");
-    }
-
-    memcpy(&buffer->values[buffer->colorsPerLed*led], buf, len);
-  }
-  else
-  {
-    int i;
-    for (i = 0; i < buffer->colorsPerLed; i++)
-    {
-      buffer->values[buffer->colorsPerLed*led+i] = luaL_checkinteger(L, 3+i);
-    }
-  }
-
-  return 0;
-}
-
-static int ws2812_buffer_size(lua_State* L) {
-  ws2812_buffer * buffer = (ws2812_buffer*)luaL_checkudata(L, 1, "ws2812.buffer");
-
-  lua_pushinteger(L, buffer->size);
-
-  return 1;
-}
-
-static int ws2812_buffer_sub(lua_State* L) {
-  ws2812_buffer * lhs = (ws2812_buffer*)luaL_checkudata(L, 1, "ws2812.buffer");
-  size_t l = lhs->size;
-  ptrdiff_t start = posrelat(luaL_checkinteger(L, 2), l);
-  ptrdiff_t end = posrelat(luaL_optinteger(L, 3, -1), l);
-  if (start <= end) {
-    ws2812_buffer *result = allocate_buffer(L, end - start + 1, lhs->colorsPerLed);
-    memcpy(result->values, lhs->values + lhs->colorsPerLed * (start - 1), lhs->colorsPerLed * (end - start + 1));
-  } else {
-    ws2812_buffer *result = allocate_buffer(L, 0, lhs->colorsPerLed);
-  }
-  return 1;
-}
-
-static int ws2812_buffer_concat(lua_State* L) {
-  ws2812_buffer * lhs = (ws2812_buffer*)luaL_checkudata(L, 1, "ws2812.buffer");
-  ws2812_buffer * rhs = (ws2812_buffer*)luaL_checkudata(L, 2, "ws2812.buffer");
-
-  luaL_argcheck(L, lhs->colorsPerLed == rhs->colorsPerLed, 1, "Can only concatenate buffers with same colors");
-
-  int colorsPerLed = lhs->colorsPerLed;
-  int leds = lhs->size + rhs->size;
-
-  ws2812_buffer * buffer = allocate_buffer(L, leds, colorsPerLed);
-
-  memcpy(buffer->values, lhs->values, lhs->colorsPerLed * lhs->size);
-  memcpy(buffer->values + lhs->colorsPerLed * lhs->size, rhs->values, rhs->colorsPerLed * rhs->size);
-
-  return 1;
-}
-
-static int ws2812_buffer_tostring(lua_State* L) {
-  ws2812_buffer * buffer = (ws2812_buffer*)luaL_checkudata(L, 1, "ws2812.buffer");
-
-  luaL_Buffer result;
-  luaL_buffinit(L, &result);
-
-  luaL_addchar(&result, '[');
-  int i;
-  int p = 0;
-  for (i = 0; i < buffer->size; i++) {
-    int j;
-    if (i > 0) {
-      luaL_addchar(&result, ',');
-    }
-    luaL_addchar(&result, '(');
-    for (j = 0; j < buffer->colorsPerLed; j++, p++) {
-      if (j > 0) {
-        luaL_addchar(&result, ',');
-      }
-      char numbuf[5];
-      sprintf(numbuf, "%d", buffer->values[p]);
-      luaL_addstring(&result, numbuf);
-    }
-    luaL_addchar(&result, ')');
-  }
-
-  luaL_addchar(&result, ']');
-  luaL_pushresult(&result);
-
-  return 1;
-}
-
-LROT_BEGIN(ws2812_buffer_map, NULL, LROT_MASK_INDEX)
-  LROT_FUNCENTRY( __concat, ws2812_buffer_concat )
-  LROT_TABENTRY(  __index, ws2812_buffer_map )
-  LROT_FUNCENTRY( __tostring, ws2812_buffer_tostring )
-  LROT_FUNCENTRY( dump, ws2812_buffer_dump )
-  LROT_FUNCENTRY( fade, ws2812_buffer_fade_lua)
-  LROT_FUNCENTRY( fill, ws2812_buffer_fill_lua )
-  LROT_FUNCENTRY( get, ws2812_buffer_get )
-  LROT_FUNCENTRY( replace, ws2812_buffer_replace )
-  LROT_FUNCENTRY( mix, ws2812_buffer_mix )
-  LROT_FUNCENTRY( power, ws2812_buffer_power )
-  LROT_FUNCENTRY( set, ws2812_buffer_set )
-  LROT_FUNCENTRY( shift, ws2812_buffer_shift_lua )
-  LROT_FUNCENTRY( size, ws2812_buffer_size )
-  LROT_FUNCENTRY( sub, ws2812_buffer_sub )
-LROT_END(ws2812_buffer_map, NULL, LROT_MASK_INDEX)
-
 LROT_BEGIN(ws2812, NULL, 0)
   LROT_FUNCENTRY( init, ws2812_init )
-  LROT_FUNCENTRY( newBuffer, ws2812_new_buffer )
+  LROT_FUNCENTRY( newBuffer, pixbuf_new_lua ) // backwards compatibility
   LROT_FUNCENTRY( write, ws2812_write )
-  LROT_NUMENTRY( FADE_IN, FADE_IN )
-  LROT_NUMENTRY( FADE_OUT, FADE_OUT )
+  LROT_NUMENTRY( FADE_IN, PIXBUF_FADE_IN )    // BC
+  LROT_NUMENTRY( FADE_OUT, PIXBUF_FADE_OUT )  // BC
   LROT_NUMENTRY( MODE_SINGLE, MODE_SINGLE )
   LROT_NUMENTRY( MODE_DUAL, MODE_DUAL )
-  LROT_NUMENTRY( SHIFT_LOGICAL, SHIFT_LOGICAL )
-  LROT_NUMENTRY( SHIFT_CIRCULAR, SHIFT_CIRCULAR )
+  LROT_NUMENTRY( SHIFT_LOGICAL, PIXBUF_SHIFT_LOGICAL ) // BC
+  LROT_NUMENTRY( SHIFT_CIRCULAR, PIXBUF_SHIFT_CIRCULAR ) // BC
 LROT_END(ws2812, NULL, 0)
 
-int luaopen_ws2812(lua_State *L) {
+static int luaopen_ws2812(lua_State *L) {
   // TODO: Make sure that the GPIO system is initialized
-  luaL_rometatable(L, "ws2812.buffer", LROT_TABLEREF(ws2812_buffer_map));
   return 0;
 }
 

+ 0 - 55
app/modules/ws2812.h

@@ -1,55 +0,0 @@
-#ifndef APP_MODULES_WS2812_H_
-#define APP_MODULES_WS2812_H_
-
-#include "module.h"
-#include "lauxlib.h"
-#include "lmem.h"
-#include "platform.h"
-#include <stdlib.h>
-#include <math.h>
-#include <string.h>
-#include "user_interface.h"
-#include "driver/uart.h"
-#include "osapi.h"
-
-#define FADE_IN  1
-#define FADE_OUT 0
-#define SHIFT_LOGICAL  0
-#define SHIFT_CIRCULAR 1
-
-#ifndef MIN
-#define MIN(a,b) ((a) < (b) ? (a) : (b))
-#endif
-#ifndef MAX
-#define MAX(a,b) ((a) > (b) ? (a) : (b))
-#endif
-
-typedef struct {
-  int size;
-  uint8_t colorsPerLed;
-  uint8_t values[0];
-} ws2812_buffer;
-
-typedef struct {
-  size_t offset;
-  uint8_t* tmp_pixels;
-  int shiftValue;
-  size_t shift_len;
-  size_t remaining_len;
-  unsigned shift_type;
-  ws2812_buffer* buffer;
-} ws2812_buffer_shift_prepare;
-
-
-void ICACHE_RAM_ATTR ws2812_write_data(const uint8_t *pixels, uint32_t length, const uint8_t *pixels2, uint32_t length2);
-// To shift the lua_State is needed for error message and memory allocation.
-// We also need the shift operation inside a timer callback, where we cannot access the lua_State,
-// so This is split up in prepare and the actual call, which can be called multiple times with the same prepare object.
-// After being done just luaM_free on the prepare object.
-void ws2812_buffer_shift_prepared(ws2812_buffer_shift_prepare* prepare);
-ws2812_buffer_shift_prepare* ws2812_buffer_get_shift_prepare(lua_State* L, ws2812_buffer * buffer, int shiftValue, unsigned shift_type, int pos_start, int pos_end);
-
-int ws2812_buffer_fill(ws2812_buffer * buffer, int * colors);
-void ws2812_buffer_fade(ws2812_buffer * buffer, int fade, unsigned direction);
-
-#endif /* APP_MODULES_WS2812_H_ */

+ 94 - 108
app/modules/ws2812_effects.c

@@ -10,7 +10,7 @@
 #include "osapi.h"
 #include "pm/swtimer.h"
 
-#include "ws2812.h"
+#include "pixbuf.h"
 #include "color_utils.h"
 
 #define CANARY_VALUE 0x32372132
@@ -43,9 +43,8 @@
 #define IDX_B 2
 #define IDX_W 3
 
-
 typedef struct {
-  ws2812_buffer *buffer;
+  pixbuf *buffer;
   int buffer_ref;
   uint32_t mode_delay;
   uint32_t counter_mode_call;
@@ -58,10 +57,9 @@ typedef struct {
   uint8_t effect_type;
   uint8_t color[4];
   int effect_int_param1;
-  ws2812_buffer_shift_prepare* prepare;
-} ws2812_effects;
-
 
+  struct pixbuf_shift_params shift;
+} ws2812_effects;
 
 enum ws2812_effects_type {
   WS2812_EFFECT_STATIC,
@@ -91,40 +89,33 @@ static ws2812_effects *state;
 // UTILITY METHODS
 //-----------------
 
+// XXX Not exported because this module is its sole non-Lua consumer and we
+// should be going away soon!  Deprecated, 'n all that.
+extern void ICACHE_RAM_ATTR ws2812_write_data(
+  const uint8_t *pixels, uint32_t length,
+  const uint8_t *pixels2, uint32_t length2);
 
-static int ws2812_write(ws2812_buffer* buffer) {
-  size_t length1, length2;
-  const char *buffer1, *buffer2;
-
-  buffer1 = buffer->values;
-  length1 = buffer->colorsPerLed*buffer->size;
-
-  buffer2 = 0;
-  length2 = 0;
-
-  // Send the buffers
-  ws2812_write_data(buffer1, length1, buffer2, length2);
-
+static int ws2812_effects_write(pixbuf* buffer) {
+  ws2812_write_data(buffer->values, pixbuf_size(buffer), 0, 0);
   return 0;
 }
 
+// :opens_boxes -1
+static void ws2812_set_pixel(int pixel, uint32_t color) {
+  pixbuf * buffer = state->buffer;
 
-static int ws2812_set_pixel(int pixel, uint32_t color) {
-  ws2812_buffer * buffer = state->buffer;
   uint8_t g = ((color & 0x00FF0000) >> 16);
   uint8_t r = ((color & 0x0000FF00) >> 8);
   uint8_t b = (color & 0x000000FF);
-  uint8_t w = buffer->colorsPerLed  == 4 ? ((color & 0xFF000000) >> 24) : 0;
+  uint8_t w = pixbuf_channels(buffer) == 4 ? ((color & 0xFF000000) >> 24) : 0;
 
-  int offset = pixel * buffer->colorsPerLed;
+  int offset = pixel * pixbuf_channels(buffer);
   buffer->values[offset+IDX_R] = r;
   buffer->values[offset+IDX_G] = g;
   buffer->values[offset+IDX_B] = b;
-  if (buffer->colorsPerLed == 4) {
+  if (pixbuf_channels(buffer) == 4) {
     buffer->values[offset+IDX_W] = w;
   }
-
-  return 0;
 }
 
 
@@ -160,16 +151,14 @@ static int ws2812_effects_init(lua_State *L) {
   platform_print_deprecation_note("ws2812_effects",
     "soon; please see https://github.com/nodemcu/nodemcu-firmware/issues/3122");
 
-  ws2812_buffer * buffer = (ws2812_buffer*)luaL_checkudata(L, 1, "ws2812.buffer");
-  luaL_argcheck(L, buffer != NULL, 1, "no valid buffer provided");
+  pixbuf * buffer = pixbuf_from_lua_arg(L, 1);
+
   // get rid of old state
   if (state != NULL) {
-    if (state->prepare) {
-      luaM_free(L, state->prepare);
-    }
     luaL_unref(L, LUA_REGISTRYINDEX, state->buffer_ref);
     free((void *) state);
   }
+
   // Allocate memory and set all to zero
   state = (ws2812_effects *) calloc(1,sizeof(ws2812_effects));
   // initialize
@@ -245,11 +234,10 @@ static int ws2812_effects_set_brightness(lua_State* L) {
   return 0;
 }
 
-
-
+// :opens_boxes -1
 static void ws2812_effects_fill_buffer(uint8_t r, uint8_t g, uint8_t b, uint8_t w) {
 
-  ws2812_buffer * buffer = state->buffer;
+  pixbuf * buffer = state->buffer;
 
   uint8_t bright_g = g * state->brightness / BRIGHTNESS_MAX;
   uint8_t bright_r = r * state->brightness / BRIGHTNESS_MAX;
@@ -259,11 +247,11 @@ static void ws2812_effects_fill_buffer(uint8_t r, uint8_t g, uint8_t b, uint8_t
   // Fill buffer
   int i;
   uint8_t * p = &buffer->values[0];
-  for(i = 0; i < buffer->size; i++) {
+  for(i = 0; i < buffer->npix; i++) {
     *p++ = bright_g;
     *p++ = bright_r;
     *p++ = bright_b;
-    if (buffer->colorsPerLed == 4) {
+    if (pixbuf_channels(buffer) == 4) {
       *p++ = bright_w;
     }
   }
@@ -308,8 +296,8 @@ static int ws2812_effects_mode_blink() {
   }
   else { 
     // off
-    ws2812_buffer * buffer = state->buffer;
-    memset(&buffer->values[0], 0, buffer->size * buffer->colorsPerLed);
+    pixbuf * buffer = state->buffer;
+    memset(&buffer->values[0], 0, pixbuf_size(buffer));
   }
   return 0;
 }
@@ -318,10 +306,10 @@ static int ws2812_effects_mode_blink() {
 
 static int ws2812_effects_gradient(const char *gradient_spec, size_t length1) {
 
-  ws2812_buffer * buffer = state->buffer;
+  pixbuf * buffer = state->buffer;
 
-  int segments = (length1 / buffer->colorsPerLed) - 1;
-  int segmentSize = buffer->size / segments;
+  int segments = (length1 / pixbuf_channels(buffer)) - 1;
+  int segmentSize = buffer->npix / segments;
 
   uint8_t g1, r1, b1, g2, r2, b2;
   int i,j,k;
@@ -330,7 +318,7 @@ static int ws2812_effects_gradient(const char *gradient_spec, size_t length1) {
   r2 = *gradient_spec++;
   b2 = *gradient_spec++;
   // skip non-rgb components
-  for (j = 3; j < buffer->colorsPerLed; j++)
+  for (j = 3; j < pixbuf_channels(buffer); j++)
   {
     *gradient_spec++;
   }
@@ -353,7 +341,7 @@ static int ws2812_effects_gradient(const char *gradient_spec, size_t length1) {
     g2 = *gradient_spec++;
     r2 = *gradient_spec++;
     b2 = *gradient_spec++;
-    for (j = 3; j < buffer->colorsPerLed; j++)
+    for (j = 3; j < pixbuf_channels(buffer); j++)
     {
       *gradient_spec++;
     }
@@ -371,7 +359,7 @@ static int ws2812_effects_gradient(const char *gradient_spec, size_t length1) {
     int numPixels = segmentSize;
     // make sure we fill the strip correctly in case of rounding errors
     if (k == segments - 1) {
-      numPixels = buffer->size - (segmentSize * (segments - 1));
+      numPixels = buffer->npix - (segmentSize * (segments - 1));
     }
 
     int steps = numPixels - 1;
@@ -391,7 +379,7 @@ static int ws2812_effects_gradient(const char *gradient_spec, size_t length1) {
       *p++ = ((grb & 0x0000FF00) >> 8)  * state->brightness / BRIGHTNESS_MAX;
       *p++ = (grb & 0x000000FF) * state->brightness / BRIGHTNESS_MAX;
 
-      for (j = 3; j < buffer->colorsPerLed; j++) {
+      for (j = 3; j < pixbuf_channels(buffer); j++) {
         *p++ = 0;
       }
     }
@@ -404,10 +392,10 @@ static int ws2812_effects_gradient(const char *gradient_spec, size_t length1) {
 
 static int ws2812_effects_gradient_rgb(const char *buffer1, size_t length1) {
 
-  ws2812_buffer * buffer = state->buffer;
+  pixbuf * buffer = state->buffer;
 
-  int segments = (length1 / buffer->colorsPerLed) - 1;
-  int segmentSize = buffer->size / segments;
+  int segments = (length1 / pixbuf_channels(buffer)) - 1;
+  int segmentSize = buffer->npix / segments;
 
   uint8_t g1, r1, b1, g2, r2, b2;
   int i,j,k;
@@ -416,7 +404,7 @@ static int ws2812_effects_gradient_rgb(const char *buffer1, size_t length1) {
   r2 = *buffer1++;
   b2 = *buffer1++;
   // skip non-rgb components
-  for (j = 3; j < buffer->colorsPerLed; j++)
+  for (j = 3; j < pixbuf_channels(buffer); j++)
   {
     *buffer1++;
   }
@@ -432,7 +420,7 @@ static int ws2812_effects_gradient_rgb(const char *buffer1, size_t length1) {
     r2 = *buffer1++;
     b2 = *buffer1++;
 
-    for (j = 3; j < buffer->colorsPerLed; j++) {
+    for (j = 3; j < pixbuf_channels(buffer); j++) {
       *buffer1++;
     }
 
@@ -440,7 +428,7 @@ static int ws2812_effects_gradient_rgb(const char *buffer1, size_t length1) {
     int numPixels = segmentSize;
     // make sure we fill the strip correctly in case of rounding errors
     if (k == segments - 1) {
-      numPixels = buffer->size - (segmentSize * (segments - 1));
+      numPixels = buffer->npix - (segmentSize * (segments - 1));
     }
 
     int steps = numPixels - 1;
@@ -449,7 +437,7 @@ static int ws2812_effects_gradient_rgb(const char *buffer1, size_t length1) {
       *p++ = (g1 + ((g2-g1) * i / steps)) * state->brightness / BRIGHTNESS_MAX;
       *p++ = (r1 + ((r2-r1) * i / steps)) * state->brightness / BRIGHTNESS_MAX;
       *p++ = (b1 + ((b2-b1) * i / steps)) * state->brightness / BRIGHTNESS_MAX;
-      for (j = 3; j < buffer->colorsPerLed; j++)
+      for (j = 3; j < pixbuf_channels(buffer); j++)
       {
         *p++ = 0;
       }
@@ -466,7 +454,7 @@ static int ws2812_effects_gradient_rgb(const char *buffer1, size_t length1) {
 */
 static int ws2812_effects_mode_random_color() {
   state->mode_color_index = get_random_wheel_index(state->mode_color_index);
-  ws2812_buffer * buffer = state->buffer;
+  pixbuf * buffer = state->buffer;
 
   uint32_t color = color_wheel(state->mode_color_index);
   uint8_t r = ((color & 0x00FF0000) >> 16) * state->brightness / BRIGHTNESS_MAX;
@@ -476,11 +464,11 @@ static int ws2812_effects_mode_random_color() {
   // Fill buffer
   int i,j;
   uint8_t * p = &buffer->values[0];
-  for(i = 0; i < buffer->size; i++) {
+  for(i = 0; i < buffer->npix; i++) {
     *p++ = g;
     *p++ = r;
     *p++ = b;
-    for (j = 3; j < buffer->colorsPerLed; j++)
+    for (j = 3; j < pixbuf_channels(buffer); j++)
     {
       *p++ = 0;
     }
@@ -493,7 +481,7 @@ static int ws2812_effects_mode_random_color() {
 */
 static int ws2812_effects_mode_rainbow() {
 
-  ws2812_buffer * buffer = state->buffer;
+  pixbuf * buffer = state->buffer;
 
   uint32_t color = color_wheel(state->counter_mode_step);
   uint8_t r = (color & 0x00FF0000) >> 16;
@@ -503,11 +491,11 @@ static int ws2812_effects_mode_rainbow() {
   // Fill buffer
   int i,j;
   uint8_t * p = &buffer->values[0];
-  for(i = 0; i < buffer->size; i++) {
+  for(i = 0; i < buffer->npix; i++) {
     *p++ = g * state->brightness / BRIGHTNESS_MAX;
     *p++ = r * state->brightness / BRIGHTNESS_MAX;
     *p++ = b * state->brightness / BRIGHTNESS_MAX;
-    for (j = 3; j < buffer->colorsPerLed; j++)
+    for (j = 3; j < pixbuf_channels(buffer); j++)
     {
       *p++ = 0;
     }
@@ -523,12 +511,12 @@ static int ws2812_effects_mode_rainbow() {
 */
 static int ws2812_effects_mode_rainbow_cycle(int repeat_count) {
 
-  ws2812_buffer * buffer = state->buffer;
+  pixbuf * buffer = state->buffer;
 
   int i,j;
   uint8_t * p = &buffer->values[0];
-  for(i = 0; i < buffer->size; i++) {
-    uint16_t wheel_index = (i * 360 / buffer->size * repeat_count) % 360;
+  for(i = 0; i < buffer->npix; i++) {
+    uint16_t wheel_index = (i * 360 / buffer->npix * repeat_count) % 360;
     uint32_t color = color_wheel(wheel_index);
     uint8_t r = ((color & 0x00FF0000) >> 16) * state->brightness / BRIGHTNESS_MAX;
     uint8_t g = ((color & 0x0000FF00) >>  8) * state->brightness / BRIGHTNESS_MAX;
@@ -536,7 +524,7 @@ static int ws2812_effects_mode_rainbow_cycle(int repeat_count) {
     *p++ = g;
     *p++ = r;
     *p++ = b;
-    for (j = 3; j < buffer->colorsPerLed; j++)
+    for (j = 3; j < pixbuf_channels(buffer); j++)
     {
       *p++ = 0;
     }
@@ -552,7 +540,7 @@ static int ws2812_effects_mode_rainbow_cycle(int repeat_count) {
 */
 static int ws2812_effects_mode_flicker_int(uint8_t max_flicker) {
 
-  ws2812_buffer * buffer = state->buffer;
+  pixbuf * buffer = state->buffer;
 
   uint8_t p_g = state->color[0];
   uint8_t p_r = state->color[1];
@@ -561,7 +549,7 @@ static int ws2812_effects_mode_flicker_int(uint8_t max_flicker) {
   // Fill buffer
   int i,j;
   uint8_t * p = &buffer->values[0];
-  for(i = 0; i < buffer->size; i++) {
+  for(i = 0; i < buffer->npix; i++) {
     int flicker = rand() % (max_flicker > 0 ? max_flicker : 1);
     int r1 = p_r-flicker;
     int g1 = p_g-flicker;
@@ -572,7 +560,7 @@ static int ws2812_effects_mode_flicker_int(uint8_t max_flicker) {
     *p++ = g1 * state->brightness / BRIGHTNESS_MAX;
     *p++ = r1 * state->brightness / BRIGHTNESS_MAX;
     *p++ = b1 * state->brightness / BRIGHTNESS_MAX;
-    for (j = 3; j < buffer->colorsPerLed; j++) {
+    for (j = 3; j < pixbuf_channels(buffer); j++) {
       *p++ = 0;
     }
   }
@@ -584,7 +572,7 @@ static int ws2812_effects_mode_flicker_int(uint8_t max_flicker) {
 * Halloween effect
 */
 static int ws2812_effects_mode_halloween() {
-  ws2812_buffer * buffer = state->buffer;
+  pixbuf * buffer = state->buffer;
 
   int g1 = 50 * state->brightness / BRIGHTNESS_MAX;
   int r1 = 255 * state->brightness / BRIGHTNESS_MAX;
@@ -598,11 +586,11 @@ static int ws2812_effects_mode_halloween() {
   // Fill buffer
   int i,j;
   uint8_t * p = &buffer->values[0];
-  for(i = 0; i < buffer->size; i++) {
+  for(i = 0; i < buffer->npix; i++) {
     *p++ = (i % 4 < 2) ? g1 : g2;
     *p++ = (i % 4 < 2) ? r1 : r2;
     *p++ = (i % 4 < 2) ? b1 : b2;
-    for (j = 3; j < buffer->colorsPerLed; j++)
+    for (j = 3; j < pixbuf_channels(buffer); j++)
     {
       *p++ = 0;
     }
@@ -614,7 +602,7 @@ static int ws2812_effects_mode_halloween() {
 
 
 static int ws2812_effects_mode_circus_combustus() {
-  ws2812_buffer * buffer = state->buffer;
+  pixbuf * buffer = state->buffer;
 
   int g1 = 0 * state->brightness / BRIGHTNESS_MAX;
   int r1 = 255 * state->brightness / BRIGHTNESS_MAX;
@@ -627,7 +615,7 @@ static int ws2812_effects_mode_circus_combustus() {
   // Fill buffer
   int i,j;
   uint8_t * p = &buffer->values[0];
-  for(i = 0; i < buffer->size; i++) {
+  for(i = 0; i < buffer->npix; i++) {
     if (i % 6 < 2) {
       *p++ = g1;
       *p++ = r1;
@@ -643,7 +631,7 @@ static int ws2812_effects_mode_circus_combustus() {
       *p++ = 0;
       *p++ = 0;
     }
-    for (j = 3; j < buffer->colorsPerLed; j++)
+    for (j = 3; j < pixbuf_channels(buffer); j++)
     {
       *p++ = 0;
     }
@@ -660,35 +648,37 @@ static int ws2812_effects_mode_circus_combustus() {
 */
 static int ws2812_effects_mode_larson_scanner() {
 
-  ws2812_buffer * buffer = state->buffer;
+  pixbuf * buffer = state->buffer;
   int led_index = 0;
 
-  ws2812_buffer_fade(buffer, 2, FADE_OUT);
+  for(int i=0; i < pixbuf_size(buffer); i++) {
+    buffer->values[i] = buffer->values[i] >> 2;
+  }
 
   uint16_t pos = 0;
 
-  if(state->counter_mode_step < buffer->size) {
+  if(state->counter_mode_step < buffer->npix) {
     pos = state->counter_mode_step;
   } else {
-    pos = (buffer->size * 2) - state->counter_mode_step - 2;
+    pos = (buffer->npix * 2) - state->counter_mode_step - 2;
   }
-  pos = pos * buffer->colorsPerLed;
+  pos = pos * pixbuf_channels(buffer);
   buffer->values[pos + 1] = state->color[1];
   buffer->values[pos] = state->color[0];
   buffer->values[pos + 2] = state->color[2];
 
-  state->counter_mode_step = (state->counter_mode_step + 1) % ((buffer->size * 2) - 2);
+  state->counter_mode_step = (state->counter_mode_step + 1) % ((buffer->npix * 2) - 2);
 }
 
 
 
 static int ws2812_effects_mode_color_wipe() {
 
-  ws2812_buffer * buffer = state->buffer;
+  pixbuf * buffer = state->buffer;
 
-  int led_index = (state->counter_mode_step % buffer->size) * buffer->colorsPerLed;
+  int led_index = (state->counter_mode_step % buffer->npix) * pixbuf_channels(buffer);
 
-  if (state->counter_mode_step >= buffer->size)
+  if (state->counter_mode_step >= buffer->npix)
   {
     buffer->values[led_index] = 0;
     buffer->values[led_index + 1] = 0;
@@ -703,30 +693,30 @@ static int ws2812_effects_mode_color_wipe() {
     buffer->values[led_index + 1] = px_r;
     buffer->values[led_index + 2] = px_b;
   }
-  state->counter_mode_step = (state->counter_mode_step + 1) % (buffer->size * 2);
+  state->counter_mode_step = (state->counter_mode_step + 1) % (buffer->npix * 2);
 }
 
 static int ws2812_effects_mode_random_dot(uint8_t dots) {
 
-  ws2812_buffer * buffer = state->buffer;
+  pixbuf * buffer = state->buffer;
 
   // fade out
-  for(int i=0; i < buffer->size * buffer->colorsPerLed; i++) {
+  for(int i=0; i < pixbuf_size(buffer); i++) {
     buffer->values[i] = buffer->values[i] >> 1;
   }
 
   for(int i=0; i < dots; i++) {
     // pick random pixel
-    int led_index  = rand() % buffer->size;
+    int led_index  = rand() % buffer->npix;
 
     uint32_t color = (state->color[0] << 16) | (state->color[1] << 8) | state->color[2];
-    if (buffer->colorsPerLed == 4) {
+    if (pixbuf_channels(buffer) == 4) {
       color = color | (state->color[3] << 24);
     }
     ws2812_set_pixel(led_index, color);
   }
 
-  state->counter_mode_step = (state->counter_mode_step + 1) % ((buffer->size * 2) - 2);
+  state->counter_mode_step = (state->counter_mode_step + 1) % ((buffer->npix * 2) - 2);
 }
 
 
@@ -767,6 +757,11 @@ static uint32_t ws2812_effects_mode_delay()
   return delay;
 }
 
+static void ws2812_effects_do_shift(void)
+{
+  pixbuf_shift(state->buffer, &state->shift);
+  ws2812_effects_write(state->buffer);
+}
 
 /**
 * run loop for the effects.
@@ -784,7 +779,7 @@ static void ws2812_effects_loop(void* p)
   else if (state->effect_type == WS2812_EFFECT_RAINBOW_CYCLE)
   {
     // the rainbow cycle effect can be achieved by shifting the buffer
-    ws2812_buffer_shift_prepared(state->prepare);
+    ws2812_effects_do_shift();
   }
   else if (state->effect_type == WS2812_EFFECT_FLICKER)
   {
@@ -816,11 +811,11 @@ static void ws2812_effects_loop(void* p)
   }
   else if (state->effect_type == WS2812_EFFECT_HALLOWEEN)
   {
-    ws2812_buffer_shift_prepared(state->prepare);
+    ws2812_effects_do_shift();
   }
   else if (state->effect_type == WS2812_EFFECT_CIRCUS_COMBUSTUS)
   {
-    ws2812_buffer_shift_prepared(state->prepare);
+    ws2812_effects_do_shift();
   }
   else if (state->effect_type == WS2812_EFFECT_LARSON_SCANNER)
   {
@@ -828,7 +823,7 @@ static void ws2812_effects_loop(void* p)
   }
   else if (state->effect_type == WS2812_EFFECT_CYCLE)
   {
-    ws2812_buffer_shift_prepared(state->prepare);
+    ws2812_effects_do_shift();
   }
   else if (state->effect_type == WS2812_EFFECT_COLOR_WIPE)
   {
@@ -845,7 +840,7 @@ static void ws2812_effects_loop(void* p)
   // call count
   state->counter_mode_call = (state->counter_mode_call + 1) % UINT32_MAX;
   // write the buffer
-  ws2812_write(state->buffer);
+  ws2812_effects_write(state->buffer);
   // set the timer
   if (state->running == 1 && state->mode_delay >= 10)
   if (state->running == 1 && state->mode_delay >= 10)
@@ -855,15 +850,6 @@ static void ws2812_effects_loop(void* p)
   }
 }
 
-void prepare_shift(lua_State* L, ws2812_buffer * buffer, int shiftValue, unsigned shift_type, int pos_start, int pos_end){
-  // deinit old effect
-  if (state->prepare) {
-    luaM_free(L, state->prepare);
-  }
-
-  state->prepare = ws2812_buffer_get_shift_prepare(L, buffer, shiftValue, shift_type, pos_start, pos_end);
-}
-
 /**
 * Set the active effect mode
 */
@@ -910,13 +896,13 @@ static int ws2812_effects_set_mode(lua_State* L) {
         size_t length1;
         const char *buffer1 = lua_tolstring(L, 2, &length1);
 
-        if ((length1 / state->buffer->colorsPerLed < 2) || (length1 % state->buffer->colorsPerLed != 0))
+        if ((length1 / pixbuf_channels(state->buffer) < 2) || (length1 % pixbuf_channels(state->buffer) != 0))
         {
           luaL_argerror(L, 2, "must be at least two colors and same size as buffer colors");
         }
 
         ws2812_effects_gradient(buffer1, length1);
-        ws2812_write(state->buffer);
+        ws2812_effects_write(state->buffer);
       }
       else
       {
@@ -930,13 +916,13 @@ static int ws2812_effects_set_mode(lua_State* L) {
         size_t length1;
         const char *buffer1 = lua_tolstring(L, 2, &length1);
 
-        if ((length1 / state->buffer->colorsPerLed < 2) || (length1 % state->buffer->colorsPerLed != 0))
+        if ((length1 / pixbuf_channels(state->buffer) < 2) || (length1 % pixbuf_channels(state->buffer) != 0))
         {
           luaL_argerror(L, 2, "must be at least two colors and same size as buffer colors");
         }
 
         ws2812_effects_gradient_rgb(buffer1, length1);
-        ws2812_write(state->buffer);
+        ws2812_effects_write(state->buffer);
       }
       else
       {
@@ -952,7 +938,7 @@ static int ws2812_effects_set_mode(lua_State* L) {
       break;
     case WS2812_EFFECT_RAINBOW_CYCLE:
       ws2812_effects_mode_rainbow_cycle(effect_param != EFFECT_PARAM_INVALID ? effect_param : 1);
-      prepare_shift(L, state->buffer, 1, SHIFT_CIRCULAR, 1, -1);
+      pixbuf_prepare_shift(state->buffer, &state->shift, 1, PIXBUF_SHIFT_CIRCULAR, 1, -1);
       break;
     case WS2812_EFFECT_FLICKER:
       state->effect_int_param1 = effect_param;
@@ -967,11 +953,11 @@ static int ws2812_effects_set_mode(lua_State* L) {
       break;
     case WS2812_EFFECT_HALLOWEEN:
       ws2812_effects_mode_halloween();
-      prepare_shift(L, state->buffer, 1, SHIFT_CIRCULAR, 1, -1);
+      pixbuf_prepare_shift(state->buffer, &state->shift, 1, PIXBUF_SHIFT_CIRCULAR, 1, -1);
       break;
     case WS2812_EFFECT_CIRCUS_COMBUSTUS:
       ws2812_effects_mode_circus_combustus();
-      prepare_shift(L, state->buffer, 1, SHIFT_CIRCULAR, 1, -1);
+      pixbuf_prepare_shift(state->buffer, &state->shift, 1, PIXBUF_SHIFT_CIRCULAR, 1, -1);
       break;
     case WS2812_EFFECT_LARSON_SCANNER:
       ws2812_effects_mode_larson_scanner();
@@ -980,7 +966,7 @@ static int ws2812_effects_set_mode(lua_State* L) {
       if (effect_param != EFFECT_PARAM_INVALID) {
         state->effect_int_param1 = effect_param;
       }
-      prepare_shift(L, state->buffer, state->effect_int_param1, SHIFT_CIRCULAR, 1, -1);
+      pixbuf_prepare_shift(state->buffer, &state->shift, state->effect_int_param1, PIXBUF_SHIFT_CIRCULAR, 1, -1);
       break;
     case WS2812_EFFECT_COLOR_WIPE:
       // fill buffer with black. r,g,b,w = 0

+ 4 - 1
docs/modules/apa102.md

@@ -19,7 +19,10 @@ Send ABGR data in 8 bits to a APA102 chain.
 - `data_pin` any GPIO pin 0, 1, 2, ...
 - `clock_pin` any GPIO pin 0, 1, 2, ...
 - `string` payload to be sent to one or more APA102 LEDs.
-  It should be composed from a ABGR quadruplet per element.
+
+  It may be a [pixbuf](pixbuf) with four channels or a string,
+  composed from a ABGR quadruplet per element:
+
     - `A1` the first pixel's Intensity channel (0-31)
     - `B1` the first pixel's Blue channel (0-255)<br />
     - `G1` the first pixel's Green channel (0-255)

+ 342 - 0
docs/modules/pixbuf.md

@@ -0,0 +1,342 @@
+# Pixel Buffer (pixbuf) Module
+
+| Since  | Origin / Contributor  | Maintainer  | Source  |
+| :----- | :-------------------- | :---------- | :------ |
+| 2020-?? | [nwf](https://github.com/nwf) | nwf | [pixbuf.c](../../app/modules/pixbuf.c) |
+
+The pixbuf library offers C-array byte objects and convenient utility functions
+for maintaining small frame buffers, usually for use with LED arrays, as
+supported by, e.g., ws2812.
+
+## pixbuf.newBuffer()
+Allocate a new memory buffer to store LED values.
+
+#### Syntax
+`pixbuf.newBuffer(numberOfLeds, numberOfChannels)`
+
+#### Parameters
+ - `numberOfLeds` length of the LED strip (in pixels)
+ - `numberOfChannels` the channel count (bytes per pixel)
+
+#### Returns
+`pixbuf.buffer` object
+
+## pixbuf.buffer:get()
+Return the value at the given position, in native strip color order
+
+#### Syntax
+`buffer:get(index)`
+
+#### Parameters
+ - `index` position in the buffer (1 for first LED)
+
+#### Returns
+`(color)`
+
+#### Example
+```lua
+buffer = pixbuf.newBuffer(32, 4)
+print(buffer:get(1))
+0	0	0	0
+```
+
+## pixbuf.buffer:set()
+Set the value at the given position, in native strip color order
+
+#### Syntax
+`buffer:set(index, color)`
+
+#### Parameters
+ - `index` position in the buffer (1 for the first LED)
+ - `color` payload of the color
+
+Payload could be:
+- `number, number, ...`, passing as many colors as required by the array type
+- `table` should contain one value per color required by the array type
+- `string` with a natural multiple of the colors required by the array type
+
+`string` inputs may be used to set multiple consecutive pixels!
+
+#### Returns
+The buffer
+
+#### Example
+```lua
+buffer = pixbuf.newBuffer(32, 3)
+buffer:set(1, 255, 0, 0) -- set the first LED green for a GRB strip
+```
+
+```lua
+buffer = pixbuf.newBuffer(32, 4)
+buffer:set(1, {255, 0, 0, 255}) -- set the first LED white and red for a RGBW strip
+```
+
+```lua
+-- set the first LED green for a RGB strip and exploit the return value
+buffer = pixbuf.newBuffer(32, 3):set(1, string.char(0, 255, 0))
+```
+
+## pixbuf.buffer:size()
+Return the size of the buffer in number of LEDs
+
+#### Syntax
+`buffer:size()`
+
+#### Parameters
+none
+
+#### Returns
+`int`
+
+## pixbuf.buffer:channels()
+Return the buffer's channel count
+
+#### Syntax
+`buffer:channels()`
+
+#### Parameters
+none
+
+#### Returns
+`int`
+
+## pixbuf.buffer:fill()
+Fill the buffer with the given color.
+The number of given bytes must match the channel count of the buffer.
+
+#### Syntax
+`buffer:fill(color)`
+
+#### Parameters
+ - `color` bytes for each channel
+
+#### Returns
+The buffer
+
+#### Example
+```lua
+buffer:fill(0, 0, 0) -- fill the buffer with black for a RGB strip
+```
+
+## pixbuf.buffer:dump()
+Returns the contents of the buffer (the pixel values) as a string. This can then be saved to a file or sent over a network and may be fed back to [`pixbuf.buffer:set()`](#pixbufbufferset).
+
+#### Syntax
+`buffer:dump()`
+
+#### Returns
+A string containing the pixel values.
+
+#### Example
+```lua
+local s = buffer:dump()
+```
+
+## pixbuf.buffer:replace()
+Inserts a string (or a pixbuf) into another buffer with an offset.
+The buffer must be of the same type or an error will be thrown.
+
+#### Syntax
+`buffer:replace(source[, offset])`
+
+#### Parameters
+ - `source` the pixel values to be set into the buffer. This is either a string or a pixbuf.
+ - `offset` the offset where the source is to be placed in the buffer. Default is 1. Negative values can be used.
+
+#### Returns
+`nil`
+
+#### Example
+```lua
+buffer:replace(anotherbuffer:dump()) -- copy one buffer into another via a string
+buffer:replace(anotherbuffer) -- copy one buffer into another
+newbuffer = buffer.sub(1)     -- make a copy of a buffer into a new buffer
+```
+
+## pixbuf.buffer:mix()
+This is a general method that loads data into a buffer that is a linear combination of data from other buffers. It can be used to copy a buffer or,
+more usefully, do a cross fade. The pixel values are computed as integers and then range limited to [0, 255]. This means that negative
+factors work as expected, and that the order of combining buffers does not matter.
+
+#### Syntax
+`buffer:mix(factor1, buffer1, ...)`
+
+#### Parameters
+ - `factor1` This is the factor that the contents of `buffer1` are multiplied by. This factor is scaled by a factor of 256. Thus `factor1` value of 256 is a factor of 1.0.
+ - `buffer1` This is the source buffer. It must be of the same shape as the destination buffer.
+
+There can be any number of factor/buffer pairs.
+
+#### Returns
+The output buffer.
+
+#### Example
+```lua
+-- loads buffer with a crossfade between buffer1 and buffer2
+buffer:mix(256 - crossmix, buffer1, crossmix, buffer2)
+
+-- multiplies all values in buffer by 0.75
+-- This can be used in place of buffer:fade
+buffer:mix(192, buffer)
+```
+
+## pixbuf.buffer:mix4I5()
+Like [`pixbuf.buffer:mix()`](#pixbufbuffermix) but treats the first channel as
+a scaling, 5-bit intensity value.  The buffers must all have four channels.
+This is mostly useful for APA102 LEDs.
+
+## pixbuf.buffer:power()
+Computes the total energy requirement for the buffer. This is merely the total sum of all the pixel values (which assumes that each color in each
+pixel consumes the same amount of power). A real WS2812 (or WS2811) has three constant current drivers of 20mA -- one for each of R, G and B. The
+pulse width modulation will cause the *average* current to scale linearly with pixel value.
+
+#### Syntax
+`buffer:power()`
+
+#### Returns
+An integer which is the sum of all the pixel values.
+
+#### Example
+```lua
+-- Dim the buffer to no more than the PSU can provide
+local psu_current_ma = 1000
+local led_current_ma = 20
+local led_sum = psu_current_ma * 255 / led_current_ma
+
+local p = buffer:power()
+if p > led_sum then
+  buffer:mix(256 * led_sum / p, buffer) -- power is now limited
+end
+```
+
+## pixbuf.buffer:powerI()
+Like [`pixbuf.buffer:power()`](#pixbufbufferpower) but treats the first channel as
+a scaling intensity value.
+
+## pixbuf.buffer:fade()
+Fade in or out. Defaults to out. Multiply or divide each byte of each led with/by the given value. Useful for a fading effect.
+
+#### Syntax
+`buffer:fade(value [, direction])`
+
+#### Parameters
+ - `value` value by which to divide or multiply each byte
+ - `direction` pixbuf.FADE\_IN or pixbuf.FADE\_OUT. Defaults to pixbuf.FADE\_OUT
+
+#### Returns
+`nil`
+
+#### Example
+```lua
+buffer:fade(2)
+buffer:fade(2, pixbuf.FADE_IN)
+```
+
+## pixbuf.buffer:fadeI()
+Like [`pixbuf.buffer:fade()`](#pixbufbufferfade) but treats the first channel as
+a scaling intensity value.  This is mostly useful for APA102 LEDs.
+
+## pixbuf.buffer:shift()
+Shift the content of (a piece of) the buffer in positive or negative direction. This allows simple animation effects. A slice of the buffer can be specified by using the
+standard start and end offset Lua notation. Negative values count backwards from the end of the buffer.
+
+#### Syntax
+`buffer:shift(value [, mode[, i[, j]]])`
+
+#### Parameters
+ - `value` number of pixels by which to rotate the buffer. Positive values rotate forwards, negative values backwards.
+ - `mode` is the shift mode to use. Can be one of `pixbuf.SHIFT_LOGICAL` or `pixbuf.SHIFT_CIRCULAR`. In case of SHIFT\_LOGICAL, the freed pixels are set to 0 (off). In case of SHIFT\_CIRCULAR, the buffer is treated like a ring buffer, inserting the pixels falling out on one end again on the other end. Defaults to SHIFT\_LOGICAL.
+ - `i` is the first offset in the buffer to be affected. Negative values are permitted and count backwards from the end. Default is 1.
+ - `j` is the last offset in the buffer to be affected. Negative values are permitted and count backwards from the end. Default is -1.
+
+#### Returns
+`nil`
+
+#### Example
+```lua
+buffer:shift(3)
+```
+
+## pixbuf.buffer:sub()
+This implements the extraction function like `string.sub`. The indexes are in leds and all the same rules apply.
+
+#### Syntax
+`buffer1:sub(i[, j])`
+
+#### Parameters
+ - `i` This is the start of the extracted data. Negative values can be used.
+ - `j` this is the end of the extracted data. Negative values can be used. The default is -1.
+
+#### Returns
+A buffer containing the extracted piece.
+
+#### Example
+```
+b = buffer:sub(1,10)
+```
+
+## pixbuf.buffer:__concat()
+This implements the `..` operator to concatenate two buffers. They must have the same number of colors per led.
+
+#### Syntax
+`buffer1 .. buffer2`
+
+#### Parameters
+ - `buffer1` this is the start of the resulting buffer
+ - `buffer2` this is the end of the resulting buffer
+
+#### Returns
+The concatenated buffer.
+
+#### Example
+```
+ws2812.write(buffer1 .. buffer2)
+```
+## pixbuf.buffer:map()
+Map a function across each pixel of one, or zip a function along two,
+pixbuf(s), storing into the buffer on which it is called.
+
+#### Syntax
+`buffer0:map(f, [buffer1], [start1], [end1], [buffer2, [start2]])`
+
+#### Parameters
+ - `f` This is the mapping function; it is applied for each pixel to all channels of `buffer1` and
+   all channels of `buffer2`, if given.  It must return a value for each channel of the output
+   buffer, `buffer0`.
+ - `buffer1` The first source buffer.  Defaults to `buffer0`.
+ - `start1` This is the start of the mapped range of `buffer1`. Negative values can be used and will be interpreted as before the end of `buffer1`.  The default is 1.
+ - `end1` this is the end of the mapped range. Negative values can be used. The default is -1 (i.e., the end of `buffer1`).
+ - `buffer2` is a second buffer, for zip operations
+ - `start2` This is the start of the mapped range within `buffer2`.  Negative values can be used and will be interpreted as before the end of `buffer2`.  The default is 1.
+
+`buffer0` must have sufficient room to recieve all pixels from `start1` to
+`end1` (which is true of the defaults, when `buffer1` is `buffer0` and `start1`
+is 1 and `end1` is -1).  `buffer2`, if given, must have sufficient pixels after
+`start2`.
+
+#### Returns
+`buffer0`
+
+#### Examples
+
+Change channel order within a single buffer:
+```Lua
+buffer:map(function(r,g,b) return g,r,b end)
+```
+
+Change channel order for a subset of pixels:
+```Lua
+buffer:map(function(r,g,b) return g,r,b end, nil, 2, 5)
+```
+
+Extract one channel for a subset of pixels:
+```Lua
+outbuf = pixbuf.create(11, 1)
+outbuf:map(function(r,g,b) return b end, inbuf, 10, 20)
+```
+
+Concatenate channels per pixel, possibly with different offsets in buffers:
+```Lua
+outbuf:map(function(...) return ... end, inbuf1, inbuf2)
+outbuf:map(function(...) return ... end, inbuf1, 5, 10, inbuf2, 3)
+```

+ 3 - 1
docs/modules/tm1829.md

@@ -15,7 +15,9 @@ Send data to a led strip using native chip format.
 `tm1829.write(string)`
 
 #### Parameters
-- `string` payload to be sent to one or more TM1829 leds.
+- `string` payload to be sent to one or more TM1829 leds.  It is either
+  a 3-channel [pixbuf](pixbuf) (e.g., `pixbuf.TYPE_RGB`) or a string of
+  raw byte values to be sent.
 
 #### Returns
 `nil`

+ 29 - 266
docs/modules/ws2812.md

@@ -31,8 +31,20 @@ In `ws2812.MODE_DUAL` mode you will be able to handle two strips in parallel but
 `nil`
 
 ## ws2812.write()
-Send data to one or two led strip using its native format which is generally Green,Red,Blue for RGB strips
-and Green,Red,Blue,White for RGBW strips.
+Send data to one or two led strip using its native format, which is generally
+Green, Red, Blue for RGB strips and Green, Red, Blue, White for RGBW strips.
+(However, ws2812 drivers have been observed wired up in other orders.)
+
+Because this function uses the hardware UART(s), it is able to return and allow
+Lua to resume execution up to 300 microseconds before the data has finished
+being sent.  If you wish to perform actions synchronous with the end of the
+data transmission, [`tmr.delay()`](../tmr#tmr.delay()) for 300 microseconds.
+
+Separately, because this function returns early, back-to-back invocations may
+not leave enough time for the strip to latch, and so may appear to the ws2812
+drivers to be simply writes to a longer LED strip.  Please ensure that you have
+more than 350 microseconds between the return of `ws2812.write()` to your Lua
+and the next invocation thereof.
 
 #### Syntax
 `ws2812.write(data1, [data2])`
@@ -44,7 +56,7 @@ and Green,Red,Blue,White for RGBW strips.
 Payload type could be:
 - `nil` nothing is done
 - `string` representing bytes to send
-- `ws2812.buffer` see [Buffer module](#buffer-module)
+- a [pixbuf](pixbuf) object containing the bytes to send.  The pixbuf's type is not checked!
 
 #### Returns
 `nil`
@@ -70,280 +82,31 @@ ws2812.init(ws2812.MODE_DUAL)
 ws2812.write(nil, string.char(0, 255, 0, 0, 255, 0)) -- turn the two first RGB leds to red on the second strip, do nothing on the first
 ```
 
-# Buffer module
+# Pixbuf support
 For more advanced animations, it is useful to keep a "framebuffer" of the strip,
 interact with it and flush it to the strip.
 
-For this purpose, the ws2812 library offers a read/write buffer. This buffer has a `__tostring` method so that it can be printed. This is useful for debugging.
+For this purpose, the [pixbuf](pixbuf) library offers a read/write buffer and
+convenient functions for pixel value manipulation.
+
+For backwards-compatibility, `pixbuf.newBuffer()` is aliased as
+`ws2812.newBuffer`, but this will be removed in the next nodemcu-firmware
+release.
 
 #### Example
 Led chaser with a RGBW strip
+
 ```lua
 ws2812.init()
-local i, buffer = 0, ws2812.newBuffer(300, 4); buffer:fill(0, 0, 0, 0); tmr.create():alarm(50, 1, function()
+
+i, buffer = 0, pixbuf.newBuffer(300, 4)
+
+buffer:fill(0, 0, 0, 0)
+
+tmr.create():alarm(50, 1, function()
   i = i + 1
   buffer:fade(2)
   buffer:set(i % buffer:size() + 1, 0, 0, 0, 255)
   ws2812.write(buffer)
 end)
 ```
-
-## ws2812.newBuffer()
-Allocate a new memory buffer to store led values.
-
-#### Syntax
-`ws2812.newBuffer(numberOfLeds, bytesPerLed)`
-
-#### Parameters
- - `numberOfLeds` length of the led strip
- - `bytesPerLed` 3 for RGB strips and 4 for RGBW strips
-
-#### Returns
-`ws2812.buffer`
-
-## ws2812.buffer:get()
-Return the value at the given position
-
-#### Syntax
-`buffer:get(index)`
-
-#### Parameters
- - `index` position in the buffer (1 for first led)
-
-#### Returns
-`(color)`
-
-#### Example
-```lua
-buffer = ws2812.newBuffer(32, 4)
-print(buffer:get(1))
-0	0	0	0
-```
-
-## ws2812.buffer:set()
-Set the value at the given position
-
-#### Syntax
-`buffer:set(index, color)`
-
-#### Parameters
- - `index` position in the buffer (1 for the first led)
- - `color` payload of the color
-
-Payload could be:
-- `number, number, ...` you should pass as many arguments as `bytesPerLed`
-- `table` should contains `bytesPerLed` numbers
-- `string` should contains `bytesPerLed` bytes
-
-#### Returns
-`nil`
-
-#### Example
-```lua
-buffer = ws2812.newBuffer(32, 3)
-buffer:set(1, 255, 0, 0) -- set the first led green for a RGB strip
-```
-
-```lua
-buffer = ws2812.newBuffer(32, 4)
-buffer:set(1, {0, 0, 0, 255}) -- set the first led white for a RGBW strip
-```
-
-```lua
-buffer = ws2812.newBuffer(32, 3)
-buffer:set(1, string.char(255, 0, 0)) -- set the first led green for a RGB strip
-```
-
-## ws2812.buffer:size()
-Return the size of the buffer in number of leds
-
-#### Syntax
-`buffer:size()`
-
-#### Parameters
-none
-
-#### Returns
-`int`
-
-## ws2812.buffer:fill()
-Fill the buffer with the given color.
-The number of given bytes must match the number of bytesPerLed of the buffer
-
-#### Syntax
-`buffer:fill(color)`
-
-#### Parameters
- - `color` bytes of the color, you should pass as many arguments as `bytesPerLed`
-
-#### Returns
-`nil`
-
-#### Example
-```lua
-buffer:fill(0, 0, 0) -- fill the buffer with black for a RGB strip
-```
-
-## ws2812.buffer:dump()
-Returns the contents of the buffer (the pixel values) as a string. This can then be saved to a file or sent over a network.
-
-#### Syntax
-`buffer:dump()`
-
-#### Returns
-A string containing the pixel values.
-
-#### Example
-```lua
-local s = buffer:dump()
-```
-
-## ws2812.buffer:replace()
-Inserts a string (or a buffer) into another buffer with an offset.
-The buffer must have the same number of colors per led or an error will be thrown.
-
-#### Syntax
-`buffer:replace(source[, offset])`
-
-#### Parameters
- - `source` the pixel values to be set into the buffer. This is either a string or a buffer.
- - `offset` the offset where the source is to be placed in the buffer. Default is 1. Negative values can be used.
-
-#### Returns
-`nil`
-
-#### Example
-```lua
-buffer:replace(anotherbuffer:dump()) -- copy one buffer into another via a string
-buffer:replace(anotherbuffer) -- copy one buffer into another
-newbuffer = buffer.sub(1)     -- make a copy of a buffer into a new buffer
-```
-
-## ws2812.buffer:mix()
-This is a general method that loads data into a buffer that is a linear combination of data from other buffers. It can be used to copy a buffer or,
-more usefully, do a cross fade. The pixel values are computed as integers and then range limited to [0, 255]. This means that negative
-factors work as expected, and that the order of combining buffers does not matter.
-
-#### Syntax
-`buffer:mix(factor1, buffer1, ...)`
-
-#### Parameters
- - `factor1` This is the factor that the contents of `buffer1` are multiplied by. This factor is scaled by a factor of 256. Thus `factor1` value of 256 is a factor of 1.0.
- - `buffer1` This is the source buffer. It must be of the same shape as the destination buffer.
-
-There can be any number of factor/buffer pairs.
-
-#### Returns
-`nil`
-
-#### Example
-```lua
--- loads buffer with a crossfade between buffer1 and buffer2
-buffer:mix(256 - crossmix, buffer1, crossmix, buffer2)
-
--- multiplies all values in buffer by 0.75
--- This can be used in place of buffer:fade
-buffer:mix(192, buffer)
-```
-
-## ws2812.buffer:power()
-Computes the total energy requirement for the buffer. This is merely the total sum of all the pixel values (which assumes that each color in each
-pixel consumes the same amount of power). A real WS2812 (or WS2811) has three constant current drivers of 20mA -- one for each of R, G and B. The
-pulse width modulation will cause the *average* current to scale linearly with pixel value.
-
-#### Syntax
-`buffer:power()`
-
-#### Returns
-An integer which is the sum of all the pixel values.
-
-#### Example
-```lua
--- Dim the buffer to no more than the PSU can provide
-local psu_current_ma = 1000
-local led_current_ma = 20
-local led_sum = psu_current_ma * 255 / led_current_ma
-
-local p = buffer:power()
-if p > led_sum then
-  buffer:mix(256 * led_sum / p, buffer) -- power is now limited
-end
-```
-
-## ws2812.buffer:fade()
-Fade in or out. Defaults to out. Multiply or divide each byte of each led with/by the given value. Useful for a fading effect.
-
-#### Syntax
-`buffer:fade(value [, direction])`
-
-#### Parameters
- - `value` value by which to divide or multiply each byte
- - `direction` ws2812.FADE\_IN or ws2812.FADE\_OUT. Defaults to ws2812.FADE\_OUT
-
-#### Returns
-`nil`
-
-#### Example
-```lua
-buffer:fade(2)
-buffer:fade(2, ws2812.FADE_IN)
-```
-## ws2812.buffer:shift()
-Shift the content of (a piece of) the buffer in positive or negative direction. This allows simple animation effects. A slice of the buffer can be specified by using the
-standard start and end offset Lua notation. Negative values count backwards from the end of the buffer.
-
-#### Syntax
-`buffer:shift(value [, mode[, i[, j]]])`
-
-#### Parameters
- - `value` number of pixels by which to rotate the buffer. Positive values rotate forwards, negative values backwards.
- - `mode` is the shift mode to use. Can be one of `ws2812.SHIFT_LOGICAL` or `ws2812.SHIFT_CIRCULAR`. In case of SHIFT\_LOGICAL, the freed pixels are set to 0 (off). In case of SHIFT\_CIRCULAR, the buffer is treated like a ring buffer, inserting the pixels falling out on one end again on the other end. Defaults to SHIFT\_LOGICAL.
- - `i` is the first offset in the buffer to be affected. Negative values are permitted and count backwards from the end. Default is 1.
- - `j` is the last offset in the buffer to be affected. Negative values are permitted and count backwards from the end. Default is -1.
-
-#### Returns
-`nil`
-
-#### Example
-```lua
-buffer:shift(3)
-```
-
-## ws2812.buffer:sub()
-This implements the extraction function like `string.sub`. The indexes are in leds and all the same rules apply.
-
-#### Syntax
-`buffer1:sub(i[, j])`
-
-#### Parameters
- - `i` This is the start of the extracted data. Negative values can be used.
- - `j` this is the end of the extracted data. Negative values can be used. The default is -1.
-
-#### Returns
-A buffer containing the extracted piece.
-
-#### Example
-```
-b = buffer:sub(1,10)
-```
-
-## ws2812.buffer:__concat()
-This implements the `..` operator to concatenate two buffers. They must have the same number of colors per led.
-
-#### Syntax
-`buffer1 .. buffer2`
-
-#### Parameters
- - `buffer1` this is the start of the resulting buffer
- - `buffer2` this is the end of the resulting buffer
-
-#### Returns
-The concatenated buffer.
-
-#### Example
-```
-ws2812.write(buffer1 .. buffer2)
-```
-
-
-

+ 288 - 0
tests/NTest_pixbuf.lua

@@ -0,0 +1,288 @@
+local N = ...
+N = (N or require "NTest")("pixbuf")
+
+local function initBuffer(buf, ...)
+  for i,v in ipairs({...}) do
+    buf:set(i, v, v*2, v*3, v*4)
+  end
+  return buf
+end
+
+N.test('initialize a buffer', function()
+    local buffer = pixbuf.newBuffer(9, 3)
+    nok(buffer == nil)
+    ok(eq(buffer:size(), 9), "check size")
+    ok(eq(buffer:dump(), string.char(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0)), "initialize with 0")
+
+    fail(function() pixbuf.newBuffer(9, -1) end, "should be a positive integer")
+    fail(function() pixbuf.newBuffer(0, 3) end, "should be a positive integer")
+    fail(function() pixbuf.newBuffer(-1, 3) end, "should be a positive integer")
+end)
+
+N.test('have correct size', function()
+    local buffer = pixbuf.newBuffer(9, 3)
+    ok(eq(buffer:size(), 9), "check size")
+    buffer = pixbuf.newBuffer(9, 4)
+    ok(eq(buffer:size(), 9), "check size")
+end)
+
+N.test('fill a buffer with one color', function()
+    local buffer = pixbuf.newBuffer(3, 3)
+    buffer:fill(1,222,55)
+    ok(eq(buffer:dump(), string.char(1,222,55,1,222,55,1,222,55)), "RGB")
+    buffer = pixbuf.newBuffer(3, 4)
+    buffer:fill(1,222,55,77)
+    ok(eq(buffer:dump(), string.char(1,222,55,77,1,222,55,77,1,222,55,77)), "RGBW")
+end)
+
+N.test('replace correctly', function()
+    local buffer = pixbuf.newBuffer(5, 3)
+    buffer:replace(string.char(3,255,165,33,0,244,12,87,255))
+    ok(eq(buffer:dump(), string.char(3,255,165,33,0,244,12,87,255,0,0,0,0,0,0)), "RGBW")
+
+    buffer = pixbuf.newBuffer(5, 3)
+    buffer:replace(string.char(3,255,165,33,0,244,12,87,255), 2)
+    ok(eq(buffer:dump(), string.char(0,0,0,3,255,165,33,0,244,12,87,255,0,0,0)), "RGBW")
+
+    buffer = pixbuf.newBuffer(5, 3)
+    buffer:replace(string.char(3,255,165,33,0,244,12,87,255), -5)
+    ok(eq(buffer:dump(), string.char(3,255,165,33,0,244,12,87,255,0,0,0,0,0,0)), "RGBW")
+
+    fail(function() buffer:replace(string.char(3,255,165,33,0,244,12,87,255), 4) end,
+         "does not fit into destination")
+end)
+
+N.test('replace correctly issue #2921', function()
+    local buffer = pixbuf.newBuffer(5, 3)
+    buffer:replace(string.char(3,255,165,33,0,244,12,87,255), -7)
+    ok(eq(buffer:dump(), string.char(3,255,165,33,0,244,12,87,255,0,0,0,0,0,0)), "RGBW")
+end)
+
+N.test('get/set correctly', function()
+    local buffer = pixbuf.newBuffer(3, 4)
+    buffer:fill(1,222,55,13)
+    ok(eq({buffer:get(2)},{1,222,55,13}))
+    buffer:set(2, 4,53,99,0)
+    ok(eq({buffer:get(1)},{1,222,55,13}))
+    ok(eq({buffer:get(2)},{4,53,99,0}))
+    ok(eq(buffer:dump(), string.char(1,222,55,13,4,53,99,0,1,222,55,13)), "RGBW")
+
+    fail(function() buffer:get(0) end,         "index out of range")
+    fail(function() buffer:get(4) end,         "index out of range")
+    fail(function() buffer:set(0,1,2,3,4) end, "index out of range")
+    fail(function() buffer:set(4,1,2,3,4) end, "index out of range")
+    fail(function() buffer:set(2,1,2,3) end, "number expected, got no value")
+    fail(function() buffer:set(2,1,2,3,4,5) end, "extra values given")
+end)
+
+N.test('get/set multiple with string', function()
+    -- verify that :set does indeed return its input
+    local buffer = pixbuf.newBuffer(4, 3):set(1,"ABCDEF")
+    buffer:set(3,"LMNOPQ")
+    ok(eq(buffer:dump(), "ABCDEFLMNOPQ"))
+
+    fail(function() buffer:set(4,"AAAAAA") end, "string size will exceed strip length")
+    fail(function() buffer:set(2,"AAAAA") end, "string does not contain whole LEDs")
+end)
+
+N.test('fade correctly', function()
+    local buffer = pixbuf.newBuffer(1, 3)
+    buffer:fill(1,222,55)
+    buffer:fade(2)
+    ok(buffer:dump() == string.char(0,111,27), "RGB")
+    buffer:fill(1,222,55)
+    buffer:fade(3, pixbuf.FADE_OUT)
+    ok(buffer:dump() == string.char(0,math.floor(222/3),math.floor(55/3)), "RGB")
+    buffer:fill(1,222,55)
+    buffer:fade(3, pixbuf.FADE_IN)
+    ok(buffer:dump() == string.char(3,255,165), "RGB")
+    buffer = pixbuf.newBuffer(1, 4)
+    buffer:fill(1,222,55, 77)
+    buffer:fade(2, pixbuf.FADE_OUT)
+    ok(eq(buffer:dump(), string.char(0,111,27,38)), "RGBW")
+end)
+
+N.test('mix correctly issue #1736', function()
+    local buffer1 = pixbuf.newBuffer(1, 3)
+    local buffer2 = pixbuf.newBuffer(1, 3)
+    buffer1:fill(10,22,54)
+    buffer2:fill(10,27,55)
+    buffer1:mix(256/8*7,buffer1,256/8,buffer2)
+    ok(eq({buffer1:get(1)}, {10,23,54}))
+end)
+
+N.test('mix saturation correctly ', function()
+    local buffer1 = pixbuf.newBuffer(1, 3)
+    local buffer2 = pixbuf.newBuffer(1, 3)
+
+    buffer1:fill(10,22,54)
+    buffer2:fill(10,27,55)
+    buffer1:mix(256/2,buffer1,-256,buffer2)
+    ok(eq({buffer1:get(1)}, {0,0,0}))
+
+    buffer1:fill(10,22,54)
+    buffer2:fill(10,27,55)
+    buffer1:mix(25600,buffer1,256/8,buffer2)
+    ok(eq({buffer1:get(1)}, {255,255,255}))
+
+    buffer1:fill(10,22,54)
+    buffer2:fill(10,27,55)
+    buffer1:mix(-257,buffer1,255,buffer2)
+    ok(eq({buffer1:get(1)}, {0,5,1}))
+end)
+
+N.test('power', function()
+    local buffer = pixbuf.newBuffer(2, 4)
+    buffer:fill(10,22,54,234)
+    ok(eq(buffer:power(), 2*(10+22+54+234)))
+end)
+
+N.test('shift LOGICAL', function()
+    local buffer1 = pixbuf.newBuffer(4, 4)
+    local buffer2 = pixbuf.newBuffer(4, 4)
+
+    initBuffer(buffer1,7,8,9,12)
+    initBuffer(buffer2,0,0,7,8)
+    ok(buffer1 ~= buffer2, "disequality pre shift")
+    buffer1:shift(2)
+    ok(buffer1 == buffer2, "shift right")
+
+    initBuffer(buffer1,7,8,9,12)
+    initBuffer(buffer2,9,12,0,0)
+    buffer1:shift(-2)
+    ok(buffer1 == buffer2, "shift left")
+
+    initBuffer(buffer1,7,8,9,12)
+    initBuffer(buffer2,7,0,8,12)
+    buffer1:shift(1, nil, 2,3)
+    ok(buffer1 == buffer2, "shift middle right")
+
+    initBuffer(buffer1,7,8,9,12)
+    initBuffer(buffer2,7,9,0,12)
+    buffer1:shift(-1, nil, 2,3)
+    ok(buffer1 == buffer2, "shift middle left")
+
+    -- bounds checks, handle gracefully as string:sub does
+    initBuffer(buffer1,7,8,9,12)
+    initBuffer(buffer2,8,9,12,0)
+    buffer1:shift(-1, pixbuf.SHIFT_LOGICAL, 0,5)
+    ok(buffer1 == buffer2, "shift left out of bound")
+
+    initBuffer(buffer1,7,8,9,12)
+    initBuffer(buffer2,0,7,8,9)
+    buffer1:shift(1, pixbuf.SHIFT_LOGICAL, 0,5)
+    ok(buffer1 == buffer2, "shift right out of bound")
+
+end)
+
+N.test('shift LOGICAL issue #2946', function()
+    local buffer1 = pixbuf.newBuffer(4, 4)
+    local buffer2 = pixbuf.newBuffer(4, 4)
+
+    initBuffer(buffer1,7,8,9,12)
+    initBuffer(buffer2,0,0,0,0)
+    buffer1:shift(4)
+    ok(buffer1 == buffer2, "shift all right")
+
+    initBuffer(buffer1,7,8,9,12)
+    initBuffer(buffer2,0,0,0,0)
+    buffer1:shift(-4)
+    ok(buffer1 == buffer2, "shift all left")
+
+    fail(function() buffer1:shift(10) end, "shifting more elements than buffer size")
+    fail(function() buffer1:shift(-6) end, "shifting more elements than buffer size")
+end)
+
+N.test('shift CIRCULAR', function()
+    local buffer1 = pixbuf.newBuffer(4, 4)
+    local buffer2 = pixbuf.newBuffer(4, 4)
+
+    initBuffer(buffer1,7,8,9,12)
+    initBuffer(buffer2,9,12,7,8)
+    buffer1:shift(2, pixbuf.SHIFT_CIRCULAR)
+    ok(buffer1 == buffer2, "shift right")
+
+    initBuffer(buffer1,7,8,9,12)
+    initBuffer(buffer2,9,12,7,8)
+    buffer1:shift(-2, pixbuf.SHIFT_CIRCULAR)
+    ok(buffer1 == buffer2, "shift left")
+
+    initBuffer(buffer1,7,8,9,12)
+    initBuffer(buffer2,7,9,8,12)
+    buffer1:shift(1, pixbuf.SHIFT_CIRCULAR, 2,3)
+    ok(buffer1 == buffer2, "shift middle right")
+
+    initBuffer(buffer1,7,8,9,12)
+    initBuffer(buffer2,7,9,8,12)
+    buffer1:shift(-1, pixbuf.SHIFT_CIRCULAR, 2,3)
+    ok(buffer1 == buffer2, "shift middle left")
+
+    -- bounds checks, handle gracefully as string:sub does
+    initBuffer(buffer1,7,8,9,12)
+    initBuffer(buffer2,8,9,12,7)
+    buffer1:shift(-1, pixbuf.SHIFT_CIRCULAR, 0,5)
+    ok(buffer1 == buffer2, "shift left out of bound")
+
+    initBuffer(buffer1,7,8,9,12)
+    initBuffer(buffer2,12,7,8,9)
+    buffer1:shift(1, pixbuf.SHIFT_CIRCULAR, 0,5)
+    ok(buffer1 == buffer2, "shift right out of bound")
+
+    initBuffer(buffer1,7,8,9,12)
+    initBuffer(buffer2,12,7,8,9)
+    buffer1:shift(1, pixbuf.SHIFT_CIRCULAR, -12,12)
+    ok(buffer1 == buffer2, "shift right way out of bound")
+
+end)
+
+N.test('sub', function()
+    local buffer1 = pixbuf.newBuffer(4, 4)
+    initBuffer(buffer1,7,8,9,12)
+    buffer1 = buffer1:sub(4,3)
+    ok(eq(buffer1:size(), 0), "sub empty")
+
+    local buffer2 = pixbuf.newBuffer(2, 4)
+    buffer1 = pixbuf.newBuffer(4, 4)
+    initBuffer(buffer1,7,8,9,12)
+    initBuffer(buffer2,9,12)
+    buffer1 = buffer1:sub(3,4)
+    ok(buffer1 == buffer2, "sub")
+
+    buffer1 = pixbuf.newBuffer(4, 4)
+    buffer2 = pixbuf.newBuffer(4, 4)
+    initBuffer(buffer1,7,8,9,12)
+    initBuffer(buffer2,7,8,9,12)
+    buffer1 = buffer1:sub(-12,33)
+    ok(buffer1 == buffer2, "out of bounds")
+end)
+
+N.test('map', function()
+    local buffer1 = pixbuf.newBuffer(4, 4)
+    buffer1:fill(65,66,67,68)
+
+    buffer1:map(function(a,b,c,d) return b,a,c,d end)
+    ok(eq("BACDBACDBACDBACD", buffer1:dump()), "swizzle")
+
+    local buffer2 = pixbuf.newBuffer(4, 1)
+    buffer2:map(function(b,a,c,d) return c end, buffer1) -- luacheck: ignore
+    ok(eq("CCCC", buffer2:dump()), "projection")
+
+    local buffer3 = pixbuf.newBuffer(4, 3)
+    buffer3:map(function(b,a,c,d) return a,b,d end, buffer1) -- luacheck: ignore
+    ok(eq("ABDABDABDABD", buffer3:dump()), "projection 2")
+
+    buffer1:fill(70,71,72,73)
+    buffer1:map(function(c,a,b,d) return a,b,c,d end, buffer2, nil, nil, buffer3)
+    ok(eq("ABCDABCDABCDABCD", buffer1:dump()), "zip")
+
+    buffer1 = pixbuf.newBuffer(2, 4)
+    buffer1:fill(70,71,72,73)
+    buffer2:set(1,"ABCD")
+    buffer3:set(1,"EFGHIJKLM")
+    buffer1:map(function(c,a,b,d) return a,b,c,d end, buffer2, 1, 2, buffer3, 2)
+    ok(eq("HIAJKLBM", buffer1:dump()), "partial zip")
+end)
+
+--[[
+pixbuf.buffer:__concat()
+--]]

+ 2 - 2
tests/NTest_ws2812.lua

@@ -58,7 +58,7 @@ N.test('replace correctly', function()
   buffer:replace(string.char(3,255,165,33,0,244,12,87,255), -5)
   ok(eq(buffer:dump(), string.char(3,255,165,33,0,244,12,87,255,0,0,0,0,0,0)), "RGBW")
 
-  fail(function() buffer:replace(string.char(3,255,165,33,0,244,12,87,255), 4) end, "Does not fit into destination")
+  fail(function() buffer:replace(string.char(3,255,165,33,0,244,12,87,255), 4) end, "does not fit into destination")
 end)
 
 N.test('replace correctly issue #2921', function()
@@ -91,7 +91,7 @@ N.test('fade correctly', function()
   ok(buffer:dump() == string.char(0,111,27), "RGB")
   buffer:fill(1,222,55)
   buffer:fade(3, ws2812.FADE_OUT)
-  ok(buffer:dump() == string.char(0,222/3,55/3), "RGB")
+  ok(buffer:dump() == string.char(0,math.floor(222/3),math.floor(55/3)), "RGB")
   buffer:fill(1,222,55)
   buffer:fade(3, ws2812.FADE_IN)
   ok(buffer:dump() == string.char(3,255,165), "RGB")

+ 11 - 0
tools/luacheck_config.lua

@@ -492,6 +492,17 @@ stds.nodemcu_libs = {
         create = empty
       }
     },
+    pixbuf = {
+      fields = {
+        FADE_IN = empty,
+        FADE_OUT = empty,
+        SHIFT_CIRCULAR = empty,
+        SHIFT_LOGICAL = empty,
+        init = empty,
+        newBuffer = empty,
+        write = empty
+      }
+    },
     pwm = {
       fields = {
         close = empty,