Ed Nisley's Blog: Shop notes, electronics, firmware, machinery, 3D printing, laser cuttery, and curiosities. Contents: 100% human thinking, 0% AI slop.
Tag: Improvements
Making the world a better place, one piece at a time
The LED’s aluminum baseplate (perhaps there’s an actual “board” inside the yellow silicone fill) is firmly epoxied to a small heatsink from the Big Box o’ Heatsinks, chosen on the basis of being the right size and not being too battered.
The rather limited specs say the LED supply voltage can range from 9 to 12 V, suggesting a bit of slack, with a maximum dissipation of 3 W, which definitely requires a heatsink.
The First Light test looked promising:
COB LED Desk Lamp – first light
That’s driven from the same 12 VDC 200 mA wall wart that I used for the failed ring light version. Measuring the results shows that the supply now runs at the ragged edge of its current rating, with the output voltage around 10.5 V with plenty of ripple:
COB LED V I 100ma div
The 260 mA current (bottom, trace 1 at 100 mA/div) varies from 200 to 300 mA as the voltage (top, trace 2 at 2 V/div) varies between 10 V and a bit under 11 V. If you believe the RMS values, it’s dissipating 2.7 W and the heatsink runs at a pleasant 105 °F in an ordinary room. The wall wart gets about as warm as you’d expect; it contains an old heavy-iron transformer and rectifier, not a trendy switcher.
The heatsink mount looks nice, in a geeky way:
COB LED Desk Lamp – side detail
The left side must be that long to anchor the gooseneck; I thought about tapering the slab a bit, but, really, it’s OK the way it is. Dabs of epoxy hold the gooseneck and heatsink in place.
The heatsink rests on a small ledge at the bottom of the slab that’s as tall as the COB LED is thick, with a wire channel from the gooseneck socket:
COB LED Heatsink mount – Slic3r
The Hilbert Curve infill on the top produces a textured finish; I’m a sucker for that pattern.
The old lamp base isn’t particularly stylin’, but the new head lights up my desk below the big monitors without any glare:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
The Suction Control slider on the handle of our shiny new Kenmore Progressive vacuum cleaner varies the speed of the howling motor in the base unit, rather than venting more or less air into the pipe. We like that, but it’s all too easy to inadvertently slide the control and never notice it, sooo I marked the default condition:
Although every vacuum cleaner we’ve ever owned has touted its “quiet operation”, we always wear 30 dB ear muffs and it’s sometimes hard to tell the difference between full throttle and not quite so fast…
A recent and rather battered book-on-CD posed more than the usual problems for Asunder, so I finally broke down and fiddled around with cdparanoia and lame. This has obviously been done many times before, but breaking it into two simple steps per CD makes the inevitable errors easier to find and work around.
Invoke cdparanoia thusly to rip an entire CD into separate tracks:
cdparanoia -B -v
The files pop out sporting names like track01.cdda.wav, but they won’t be around long enough for you to develop a deep emotional attachment.
Throw a handful of parameters at lame to convert the WAV files into tagged MP3 files:
d=7
for t in {01..18} ; do lame --preset tape --tt "D${d}:T${t}" --ta "Author Name" --tl "Book title" --tn "${t}/18" --tg "Audio Book" --add-id3v2 track${t}.cdda.wav D${d}-${t}.mp3 ; done
rm track*
There’s surely a way to make a double substitution work in the track sequence, but the syntax, ah, escapes me at the moment.
You might want to not delete the WAV files until you’re happy with the MP3 results.
In any event, that produces a sequence of MP3 files imaginatively named along the lines of D1-01.mp3, which fits neatly into the cramped LCD space available on an MP3 player.
Vacuum cleaner dust brushes, separated by millimeters and decades:
Kenmore vs adapted Electrolux dust brushes
The bulky one on the left came with our new Kenmore Progressive vacuum cleaner. It’s fine for dust on a flat horizontal or vertical surface and totally useless for dust on actual objects. It’s supposed to snap around the handle at the end of the cleaner’s flexy hose, where it helps make the entire assembly too large and too clumsy, or on the end of the “wand”, where it’s at the wrong angle. The bonus outer shell slides around the stubby bristles in the unlikely event they’re too long for the flat surface at hand.
The brush on the right emerged from the Box o’ Electrolux Parts that Came With The House™, must be half a century old, and consists of a cast aluminum lump with various holes milled into it, adorned with luxuriously long and flexible horsehair. Suffice it to say they don’t make ’em like that any more. Heck, they probably don’t make horses with hair like that any more, either.
The short snout fits neatly into the space available inside the ball. The abrupt ledge at the top of the snout, of course, didn’t work well; I rushed the design for a show-n-tell.
The OpenSCAD source code (as a Github gist) bevels that ledge and tweaks the interior air channel a bit:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
With hardware handshaking in full effect, the Chiplotle routine that sends data to the HP 7475A plotter doesn’t need to sleep, because the Linux serial handlers take care of that under the hood. Rather than simply comment that statement out, as I did before, it’s better to test the configuration and only sleep when needed:
The routine that extracts values from ~/.chiplotle/config.py is already included (well, imported) in the distribution’s baseplotter.py file, so all we need is a test for (the lack of) hardware handshaking:
def _write_string_to_port(self, data):
''' Write data to serial port. data is expected to be a string.'''
#assert type(data) is str
if not isinstance(data, basestring):
raise TypeError('string expected.')
data = self._filter_unrecognized_commands(data)
data = self._slice_string_to_buffer_size(data)
for chunk in data:
if not get_config_value('rtscts'):
self._sleep_while_buffer_full( )
self._serial_port.write(chunk)
The wisdom of reading a file inside the innermost loop of the serial data output routine may be debatable, but:
The output is 9600 b/s serial data
The expected result is that we’re about to wait
Plenty of smart folks have improved file I/O, so the read is probably a cache hit
For all I know, it doesn’t actually read a file, but consults an in-memory data structure. Works well enough for me, anyhow.
The configuration file I’ve been using all along looks like this (minus most of the comments):
# -*- coding: utf-8 -*-
serial_port_to_plotter_map = {'/dev/ttyUSB0' : 'HP7475A'}
## Serial connection parameters.
## Set your plotter to match these values, or vice versa..
baudrate = 9600
bytesize = 8
parity = 'N'
stopbits = 1
timeout = 1
xonxoff = 0
rtscts = 1
## Maximum wait time for response from plotter.
## Every time the plotter is queried, Chiplotle will wait for
## a maximum of `maximum_response_wait_time` seconds.
maximum_response_wait_time = 4
## Set to True if you want information (such as warnings)
## displayed on the console. Set to False if you don't.
verbose = True
which amounts to a delay of 5.45 s = 218 step * 25 ms/step. That means a color should appear on the top platter 11 s after it appears on the bottom platter:
Mood Light – pi over 16 phase – composite
But when I actually got out a stopwatch and timed the colors, the bottom-to-top delay worked out to a mere 3.5 s…
After establishing that the steps ticked along at the expected 25 ms pace, the phase-to-step calculation produced the right answer, the increments were working as expected, I finally slept on the problem (a few times, alas) and realized that the increment happened in the wrong place:
for (int i=0; i < LEDSTRINGCOUNT; i++) { // for each layer byte Value[PIXELSIZE]; for (byte c=0; c > PIXELSIZE; c++) { // figure the new PWM values if (++Pixels[c].Step >= Pixels[c].NumSteps) { // ... from incremented step
Pixels[c].Step = 0;
}
Value[c] = StepColor(c,-i*Pixels[c].PlatterPhase);
}
uint32_t UniColor = strip.Color(Value[RED],Value[GREEN],Value[BLUE]);
for (int j=0; j < LEDSTRIPCOUNT; j++) { // fill layer with color
strip.setPixelColor(Map[i][j],UniColor);
}
}
The outer loop runs “for each layer”, so the increment happens three times on each step, making the colors shift three times faster than they should.
Promoting the increments to their own loop solved the problem:
MillisNow = millis();
if ((MillisNow - MillisThen) > UpdateMS) {
digitalWrite(PIN_HEARTBEAT,HIGH);
for (byte c=0; c < PIXELSIZE; c++) { // step to next increment in each color if (++Pixels[c].Step >= Pixels[c].NumSteps) {
Pixels[c].Step = 0;
printf("Cycle %d steps %d at %8ld delta %ld ms\r\n",c,Pixels[c].NumSteps,MillisNow,(MillisNow - MillisThen));
}
}
for (int i=0; i < LEDSTRINGCOUNT; i++) { // for each layer
byte Value[PIXELSIZE];
for (byte c=0; c < PIXELSIZE; c++) { // ... for each color
Value[c] = StepColor(c,-i*Pixels[c].PlatterPhase); // figure new PWM value
// Value[c] = (c == RED && Value[c] == 0) ? Pixels[c].MaxPWM : Value[c]; // flash highlight for tracking
}
uint32_t UniColor = strip.Color(Value[RED],Value[GREEN],Value[BLUE]);
if (false && (i == 0))
printf("L: %d C: %08lx\r\n",i,UniColor);
for (int j=0; j < LEDSTRIPCOUNT; j++) { // fill layer with color
strip.setPixelColor(Map[i][j],UniColor);
}
}
strip.show();
MillisThen = MillisNow;
digitalWrite(PIN_HEARTBEAT,LOW);
}
And then It Just Worked.
Verily, it is written: One careful measurement trumps a thousand expert opinions.
Sheesh…
(The WordPress editor wrecked these code snippets. I’m leaving them broken so WP can maybe fix the problem.) The problem isn’t fixed, but these are OK now… as long as I don’t unleash the “improved” editor on the post, anyway.
Now that the trig argument runs from 0 through 2π and resets for each complete cycle, it’s practical to add a phase that changes the colors on a per-layer basis.
The first trick, filling each layer with a single color, requires a two-dimensional Map array that lists the pixels in the proper order:
// number of LED strips around hub
#define LEDSTRIPCOUNT 4
// number of LEDs per strip
#define LEDSTRINGCOUNT 3
byte Map[LEDSTRINGCOUNT][LEDSTRIPCOUNT] = {{0,5,6,11}, {1,4,7,10}, {2,3,8,9}}; // pixel IDs around platter, bottom to top.
Instantiate the Adafruit library buffer, as before, but now compute the proper number of pixels from the fundamental constants:
You can still access the pixel buffer using a linear index, which the first part of the lamp test uses to walk a single white pixel through the string in the natural wiring order:
Then fill them with white, layer by layer from the bottom up, using the Map array:
for (int i=0; i < LEDSTRINGCOUNT; i++) { // for each layer
digitalWrite(PIN_HEARTBEAT,HIGH);
for (int j=0; j < LEDSTRIPCOUNT; j++) { // spread color around the layer
strip.setPixelColor(Map[i][j],FullWhite);
strip.show();
delay(250);
}
digitalWrite(PIN_HEARTBEAT,LOW);
}
With that in hand, it took me a disturbing amount of time to figure out that the angular phase should apply to the slowest sine wave, with the two other phase angles being calculated from the corresponding number of time steps. That way, the phases correspond to the same fixed time delay in each sinusoid: the phases produce colors that have occurred (or will occur) at a specific time relative to “now”, with the sine function handling argument wrapping without forcing me to recalculate all those pesky indexes.
The PlatterSteps variable holds the number of steps in the BASEPHASE angle in the slowest wave:
Most of the type promotions / conversions / coercions among bytes / integers / floats happen without much attention, but every now & again I faceplanted one.
Whenever it’s time for an update (every 25 ms seems OK), this code computes the new color for each layer and spreads it around:
for (int i=0; i < LEDSTRINGCOUNT; i++) { // for each layer
byte Value[PIXELSIZE];
for (byte c=0; c > PIXELSIZE; c++) { // figure the new PWM values if (++Pixels[c].Step >= Pixels[c].NumSteps) { // ... from incremented step
Pixels[c].Step = 0;
}
Value[c] = StepColor(c,-i*Pixels[c].PlatterPhase);
}
uint32_t UniColor = strip.Color(Value[RED],Value[GREEN],Value[BLUE]);
for (int j=0; j < LEDSTRIPCOUNT; j++) { // fill layer with color
strip.setPixelColor(Map[i][j],UniColor);
}
}
The -i*Pixels[c].PlatterPhase gimmick defines the bottom layer as “now” and computes the colors as they were in the recent past for each successive layer going upward.
With the phase difference boosted to π/4 to make the differences more visible:
Mood Light – pi over 4 phase
You’re seeing three LEDs reflected in the platters, of course.
A phase difference of π/16 seems barely visible in this composite image,but it’s pleasant in person:
Mood Light – pi over 16 phase – composite
The greenish ones come from a slightly different perspective. The purple ones show the progression over the course of a few seconds.
A π/16 = 11.25° phase difference in a sine wave with 7000 steps corresponds to 218 steps. At 25 ms/step, that’s a 5.5 s delay and the top layer duplicates the bottom layer after 11 s.
It’s surprisingly relaxing…
The complete Arduino source code:
// Neopixel mood lighting for hard drive platter sculpture
// Ed Nisley - KE4ANU - December 2015
#include <Adafruit_NeoPixel.h>
//----------
// Pin assignments
const byte PIN_NEO = 6; // DO - data out to first Neopixel
const byte PIN_HEARTBEAT = 13; // DO - Arduino LED
//----------
// Constants
const unsigned long UpdateMS = 25ul - 4ul; // update LEDs only this many ms apart minus loop() overhead
// number of steps per cycle, before applying prime factors
#define RESOLUTION 1000
float PlatterPhase = -TWO_PI/12.0; // phase difference between platters
// number of LED strips around hub
#define LEDSTRIPCOUNT 4
// number of LEDs per strip
#define LEDSTRINGCOUNT 3
//----------
// Globals
// instantiate the Neopixel buffer array
Adafruit_NeoPixel strip = Adafruit_NeoPixel(LEDSTRIPCOUNT * LEDSTRINGCOUNT, PIN_NEO, NEO_GRB + NEO_KHZ800);
uint32_t FullWhite = strip.Color(255,255,255);
uint32_t FullOff = strip.Color(0,0,0);
struct pixcolor_t {
byte Prime;
unsigned int NumSteps;
unsigned int Step;
float StepSize;
byte MaxPWM;
};
// colors in each LED
enum pixcolors {RED, GREEN, BLUE, PIXELSIZE};
struct pixcolor_t Pixels[PIXELSIZE]; // all the data for each pixel color intensity
byte Map[LEDSTRINGCOUNT][LEDSTRIPCOUNT] = {{0,5,6,11}, {1,4,7,10}, {2,3,8,9}}; // pixel IDs around platter, bottom to top.
unsigned long MillisNow;
unsigned long MillisThen;
//-- Figure PWM based on current state
byte StepColor(byte Color, float Phi) {
byte Value;
Value = (Pixels[Color].MaxPWM / 2.0) * (1.0 + sin(Pixels[Color].Step * Pixels[Color].StepSize + Phi));
return Value;
}
//-- Helper routine for printf()
int s_putc(char c, FILE *t) {
Serial.write(c);
}
//------------------
// Set the mood
void setup() {
pinMode(PIN_HEARTBEAT,OUTPUT);
digitalWrite(PIN_HEARTBEAT,LOW); // show we arrived
Serial.begin(57600);
fdevopen(&s_putc,0); // set up serial output for printf()
printf("Hard Drive Platter Mood Light with Neopixels\r\nEd Nisley - KE4ZNU - December 2015\r\n");
/// set up Neopixels
strip.begin();
strip.show();
// lamp test: run a brilliant white dot along the length of the strip
printf("Lamp test: walking white\r\n");
strip.setPixelColor(0,FullWhite);
strip.show();
delay(500);
for (int i=1; i<strip.numPixels(); i++) {
digitalWrite(PIN_HEARTBEAT,HIGH);
strip.setPixelColor(i-1,FullOff);
strip.setPixelColor(i,FullWhite);
strip.show();
digitalWrite(PIN_HEARTBEAT,LOW);
delay(500);
}
strip.setPixelColor(strip.numPixels() - 1,FullOff);
strip.show();
delay(500);
// fill the layers
printf(" ... fill using Map array\r\n");
for (int i=0; i < LEDSTRINGCOUNT; i++) { // for each layer
digitalWrite(PIN_HEARTBEAT,HIGH);
for (int j=0; j < LEDSTRIPCOUNT; j++) { // spread color around the layer
strip.setPixelColor(Map[i][j],FullWhite);
strip.show();
delay(250);
}
digitalWrite(PIN_HEARTBEAT,LOW);
}
// clear to black
printf(" ... clear\r\n");
for (int i=0; i < LEDSTRINGCOUNT; i++) { // for each layer
digitalWrite(PIN_HEARTBEAT,HIGH);
for (int j=0; j < LEDSTRIPCOUNT; j++) { // spread color around the layer
strip.setPixelColor(Map[i][j],FullOff);
strip.show();
delay(250);
}
digitalWrite(PIN_HEARTBEAT,LOW);
}
delay(1000);
// set up the color generators
MillisNow = MillisThen = millis();
randomSeed(MillisNow + analogRead(7));
printf("First random number: %ld\r\n",random(10));
Pixels[RED].Prime = 7;
Pixels[GREEN].Prime = 11;
Pixels[BLUE].Prime = 5;
Pixels[RED].MaxPWM = 64;
Pixels[GREEN].MaxPWM = 64;
Pixels[BLUE].MaxPWM = 64;
for (byte c=0; c < PIXELSIZE; c++) {
Pixels[c].NumSteps = RESOLUTION * (unsigned int) Pixels[c].Prime;
Pixels[c].Step = (true) ? random(Pixels[c].NumSteps) : Pixels[c].NumSteps - 1;
Pixels[c].StepSize = TWO_PI / Pixels[c].NumSteps;
}
printf("Prime scales: (%d,%d,%d)\r\n",Pixels[RED].Prime,Pixels[GREEN].Prime,Pixels[BLUE].Prime);
printf("Initial step: (%d,%d,%d)\r\n",Pixels[RED].Step,Pixels[GREEN].Step,Pixels[BLUE].Step);
printf("Max PWM: (%d,%d,%d)\r\n",Pixels[RED].MaxPWM,Pixels[GREEN].MaxPWM,Pixels[BLUE].MaxPWM);
printf("Platter phase: %d deg\r\n",(int)(360.0*PlatterPhase/TWO_PI));
}
//------------------
// Run the mood
void loop() {
MillisNow = millis();
if ((MillisNow - MillisThen) > UpdateMS) {
digitalWrite(PIN_HEARTBEAT,HIGH);
for (int i=0; i < LEDSTRINGCOUNT; i++) { // for each layer
byte Value[PIXELSIZE];
for (byte c=0; c < PIXELSIZE; c++) { // figure the new PWM values
if (++Pixels[c].Step >= Pixels[c].NumSteps) { // ... from incremented step
Pixels[c].Step = 0;
}
Value[c] = StepColor(c,i*PlatterPhase);
}
uint32_t UniColor = strip.Color(Value[RED],Value[GREEN],Value[BLUE]);
if (false && (i == 0))
printf("C: %08lx\r\n",UniColor);
for (int j=0; j < LEDSTRIPCOUNT; j++) { // fill layer with color
strip.setPixelColor(Map[i][j],UniColor);
}
}
strip.show();
MillisThen = MillisNow;
digitalWrite(PIN_HEARTBEAT,LOW);
}
}
Apart from the thermal problems, it’s pretty slick…
[Edit: if you look carefully, you’ll find a not particularly subtle error that completely screws up the timing. The LEDs looks great and work as described, but the colors run too fast. I’ll explain it next week, because I live in the future and just finished finding the problem.]