Skip to content
Michael Miller edited this page Jan 9, 2017 · 7 revisions

This section will describe how to implement a spiral topography for a concentric series of ring NeoPixels.

Today you can purchase rings of NeoPixels of various counts, with the most popular being 60, 24, 16, 12, and the jewel that is 6 pixel ring with a center pixel. As an example this section will discuss using all of these in one series with the jewel in the center.

How do you physically connect your pixel collection

The first thing to discuss is then how the rings get wired together. Is the first pixel the center pixel/ring? Is the outer ring contain the first pixel? I will discuss using the center as the first. Then you will want to make sure each ring is rotated so that the first pixel on each ring align near the same straight virtual line from the center to an outside edge. I will discuss using a starting line that is vertically straight up; or in the nomenclature of a clock, at 12 o'clock position.

The second thing to discuss is how you want to "address" the pixels. When talking about a Cartesian or matrix layout, you address the by the column (x) and row (y). But with a spiral its more about Ring (distance from center) and then Pixel (count from a standard position). You might even consider thinking of the Pixel as an angle which would then be similar to a polar coordinate system; but for this discussion we will keep it simple to the count along the ring from top center. Of course with this simplified model, the number of pixels per ring changes with the ring distance from center.

With this, each ring and its starting pixel index is listed in the next table. With the total number of pixels being 119.
Ring - Starting Pixel
0 - 0 (the center of the jewel)
1 - 1 (the outer ring of the jewel)
2 - 7 (the first on the 12 count ring)
3 - 19 (the first on the 16 count ring)
4 - 35 (the first on the 24 count ring)
5 - 59 (the first on the 60 count ring)

The Spiral Topology Object

If you examine the topology objects included in the library, they always include a constructor to configure the object and a method that maps the coordinates into the linear strip index that you then pass to the NeoPixelBus. We want to continue to follow this model; but use our addressing concept.
So it will include a constructor that assumes our configuration. You can always make this more general if you want.
It will also include a map method like

uint16_t Map(int16_t ring, int16_t pixel) const

Mapping Implementation

To implement the map method, the ring argument and pixel must be translated into the final index value. The first part is to calculate the start of the ring; this can be accomplished by a lookup table using the data above. Then we just need to add the pixel to offset to the correct one.

const uint16_t Rings[] = {0, 1, 7, 19, 35, 59}; 

uint16_t Map(int16_t ring, int16_t pixel) const {
  uint16_t index = Rings[ring] + pixel;
  return index;
}

This can be improved by checking boundary cases. As it sits if you call it with ring 0 but state pixel #32 which doesn't make sense. It may also return a value that is possibly out of scope.

Below I have added the argument checking code. The ring table has been expanded to include an extra ring valueas as it will provide us with the information to be able to validate the arguments.

const uint16_t Rings[] = {0, 1, 7, 19, 35, 59, 119}; // note, included the start of another ring 

uint16_t Map(uint16_t ring, uint16_t pixel) const {
  if (ring >= (sizeof(Rings)/sizeof(Rings[0]) - 1)) { // minus one as the Rings includes the extra value
    return 0; // invalid ring argument, always return the first one
  }
  if (pixel >= (Rings[ring + 1] - Rings[ring]) { // using the extra value for range testing
    return 0; // invalid pixel argument, always return the first one
  }
  uint16_t index = Rings[ring] + pixel;
  return index;
}

NOTE: The code above (sizeof(Rings)/sizeof(Rings[0]) is a way of having the compiler calculate the count of elements in the Rings array without us hardcoding anything. Often this is exposed as _countof() in many code bases but it was not included in the Arduino headers for some reason.

Making the object usable for different collections of rings

The above code uses a constant array of values directly. We can make this a configuration argument to the constructor of our mapping class and thus reuse the code for different projects. To do this, it and its count of elements must be past to the constructor and stored in member variables.

class NeoSpiralTopology {
public:
    NeoSpiralTopology(const uint16_t* rings, uint8_t ringsCount) :
        _rings(rings),
        _ringsCount(ringsCount) {}

private:
    const uint16_t* _rings;
    const uint8_t _ringsCount;
}

and the code to initialize it would be

const uint16_t Rings[] = {0, 1, 7, 19, 35, 59, 119}; // required, include the start of another ring 

NeoSpiralTopology topo(Rings, sizeof(Rings) / sizeof(Rings[0]));

And the Final NeoSpiralTopology class is

We need to add support for a few handy property access to get the ring count and pixel count on a ring. Also include the standard topology probe method that can return an out of bounds values as the normal map method will always return a valid value. Then merge and modify the code above into one class and you get the following ...

class NeoSpiralTopology {
public:
    NeoSpiralTopology(const uint16_t* rings, uint8_t ringsCount) :
        _rings(rings),
        _ringsCount(ringsCount) {}

    uint16_t Map(uint16_t ring, uint16_t pixel) const {
      if (ring >= getCountOfRings()) { 
        return 0; // invalid ring argument, always return a valid value, the first one
      }
      if (pixel >= getPixelCountAtRing(ring)) { 
        return 0; // invalid pixel argument, always return a valid value, the first one
      }
      return _map(ring, pixel);
    }

    uint16_t MapProbe(uint16_t ring, uint16_t pixel) const
    {
        if (ring >= getCountOfRings() || pixel >= getPixelCountAtRing(ring)) {
            return _rings[_ringsCount - 1]; // total count, the last entry, out of bounds
        }

        return _map(ring, pixel);
    }

    uint16_t getCountOfRings() const {
      return _ringsCount - 1; // minus one as the Rings includes the extra value
    }
    uint16_t getPixelCountAtRing(uint16_t ring) const {
       return Rings[ring + 1] - Rings[ring]; // using the extra value for count calc
    }

private:
    const uint16_t* _rings;
    const uint8_t _ringsCount;

    uint16_t _map(uint16_t ring, uint16_t pixel) {
       return _rings[ring] + pixel;
    }
}
Clone this wiki locally