Ed Nisley's Blog: Shop notes, electronics, firmware, machinery, 3D printing, laser cuttery, and curiosities. Contents: 100% human thinking, 0% AI slop.
So I can find it again, the way to change the sudo timeout for a particular user (that would be me) involves adding a line to the /etc/sudoers file using sudo visudo, thusly:
Defaults: ed timestamp_timeout=90
# blank line to make the underscore visible
Note the colon! Should you add the timeout to the global Defaults env_reset line, then everybody gets a monster timeout, which may not be what you want.
You can change the default editor (nano in Ubuntu) thusly:
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.]
Squirting it with circuit cooler brought it back to life, albeit briefly, so it’s a real thermal failure. OK, after I get smacked upside the head twice, I can recognize a problem when I see it.
I removed the top cover and jammed a themocouple into the screw hole in the middle of the pillar:
Mood Light – thermocouple location
A folded tissue weighted down with random desktop junk kept the breeze out of the interior:
Mood Light – PWM 128 temperature measurement
If the middle of the column hits 50 °C, what’s it like inside the 5050 packages with all those LEDs blazing away? Looks like I’ve been cooking those poor knockoff Neopixels to death.
The temperature is 50 °C with the LEDs running at maximum PWM = 128. Reducing the maximum PWM to 64 reduces the core to 30 °C and that dead blue LED springs back to life.
Figuring each LED package dissipate 250-ish mW at full throttle, that’s 120 mW at PWM 128 / 60 mW at PWM 64. The set of 12 packages dissipates 1.4 W / 750 mW, so, in a 22 °C room, the thermal coefficient is up around 10 to 20 °C/W, which is somewhere between bad and awful. Running the LEDs at full throttle obviously isn’t an option and even half-throttle really doesn’t work.
So, OK, mounting LED strips on a clever 3D printed plastic column with zero air circulation isn’t nearly as smart an idea as I thought: barely more than a watt burns right through the redline.
The Neopixel specs have nothing to say about the thermal coefficient from the LED junctions to the package leads, but cooling the copper conductors in the flex PCB can’t possibly hurt.
No, I do not want to CNC machine an aluminum pillar with little tabs on the platter for better heatsinking. It would be an interesting design project, though.
The tab supporting the strut with the center slides for the lower drawers in our Whirlpool refrigerator broke of its own accord. This is a problem of long standing, somewhat exacerbated by the fact that lifting the strut will break the tab without much effort at all, but this time the drawers pulled the strut downward hard enough to not only break the tab, but also tear the small tabs that align the bracket right out of the frame.
While pondering the problem, I glued the broken chunk back into the frame:
Refrigerator Drawer Strut – clamping front plate
We agreed that, after nigh onto two decades, it would be OK to swap the position of the two drawers, so as to let the strut use the undamaged part of the frame seen below. Presumably, we’ll eventually get used to having the apples on the right and the veggies on the left.
But it was obvious Something Serious Had To Be Done about the tab.
The tab should align like this inside the frame:
Refrigerator Drawer Strut Tab – alignment
The rightmost part of the tab rests atop a U-shaped metal bar that also supports and stiffens the entire front of the frame, but cantilevering the weight of both drawers on that extended tab overpowered my last attempt at making a glue joint. Soooo, I decided to build a (wait for it …) 3D printed part that screws firmly to the front of the strut.
The first step involved introducing the strut to Mr Belt Sander to strip the wreckage of the OEM tab from the front end (visible through the opening) and smooth things out, then measuring the remainder. The locating flange inside the frame almost defeated me, but eventually I found a tool that fit inside the strut opening and around the flange:
Refrigerator Drawer – measuring flange
Which produced a sketch of the key dimensions:
Refrigerator Drawer Strut – Dimension Doodles
Which became an extruded polygon with a few holes punched in the side:
Refrigerator Shelf Strut Tab – solid model
Building it standing up wraps the plastic threads around the entire tab and stacks the layers along the length of the tab. Doing it lying down in the obvious hump-up orientation would put the layers parallel to the bottom surface, where they can pull apart under load.
The key innovation here involves being willing to assemble the tab to the strut in situ, without insisting it fit through the frame opening and be more-or-less easily removable. That let me bulk up the tab to match the end of the strut, fill the entire frame opening with plastic, and get enough bulk for a pair of 4-40 screws that, being loaded in shear, should withstand the weight of all those fruits & veggies in the drawers.
The screws simply thread into the holes in the tab, without benefit of tapping. The OpenSCAD code now includes a pair of nut traps, but I’m hoping they won’t be needed.
The new tab really does fill the space available:
Refrigerator Drawer Strut – new tab in place
The OpenSCAD code now moves the notch half a millimeter further away from the strut to center it over the ridge. What’s not obvious is how the frame slants toward the tab over the U-bar: the tab just barely clears and probably should have a tapered nose. You may add that if you like.
The U-shaped bar constrains the tab pretty firmly and supports the end, which should now be plump enough to withstand the forces involved. The screws sit horizontally with the frame installed and can’t pull out, which is why I think they can get along without nut traps.
It’s built in cyan PETG with three perimeter threads and 40% 3D Honeycomb fill, making it essentially a solid block of plastic; it’ll be interesting to see what fails next.
The OpenSCAD source code, which I hammered out in a white-hot fury:
// Refrigerator Shelf Strut Tab
// Ed Nisley KE4ZNU December 2015
//- Extrusion parameters must match reality!
ThreadThick = 0.25;
ThreadWidth = 0.40;
HoleWindage = 0.2;
Protrusion = 0.1; // make holes end cleanly
inch = 25.4;
function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
//----------------------
// Dimensions
TabSize = [20.0,12.0,35.0]; // length from bracket, height, width along front
SlotSize = [3.0,7.0];
SlotX = 7.0;
TabProfile = [
[0,0],
[12,0], [12,7.0],
[TabSize[0],7.0], [TabSize[0],TabSize[1]],
[SlotX + SlotSize[0]/2,TabSize[1]],
[SlotX + SlotSize[0]/2,5.0], [SlotX - SlotSize[0]/2,5.0],
[SlotX - SlotSize[0]/2,TabSize[1]],
[0,TabSize[1]]
];
ScrewY = 7.0;
ScrewOC = 25.0;
ScrewOD = 2.5;
NutOD = 6.6; // across flats
NutThick = 2.5;
//----------------------
// Useful routines
module PolyCyl(Dia,Height,ForceSides=0) { // based on nophead's polyholes
Sides = (ForceSides != 0) ? ForceSides : (ceil(Dia) + 2);
FixDia = Dia / cos(180/Sides);
cylinder(r=(FixDia + HoleWindage)/2,
h=Height,
$fn=Sides);
}
//----------------------
// Build it
difference() {
linear_extrude(height=TabSize[2],convexity=4)
polygon(points=TabProfile);
for (i=[-1,1]) {
translate([-Protrusion,ScrewY,i*ScrewOC/2 + TabSize[2]/2])
rotate([0,90,0])
rotate(180/6)
PolyCyl(ScrewOD,SlotX,6);
translate([SlotX - SlotSize[0]/2 - NutThick - Protrusion,ScrewY,i*ScrewOC/2 + TabSize[2]/2])
rotate([0,90,0])
rotate(180/6)
PolyCyl(NutOD,NutThick + SlotSize[0],6);
}
}
Maybe that’ll last until we finally scrap out the refrigerator…
The Power Wheels Racer rules limit the motor to 1440 W, a tidy 60 A at 24 V. Let’s call it 70 A, which lines up neatly with the second major division up from the bottom: the orange current line hits 70 A with torque = 2.6 N·m.
Draw a vertical line at that point and read off all the other parameters from the scales on the left.
The motor will produce 2.6 N·m at just shy of 4500 RPM; call it 4400 RPM.
The SqWr Racer has 9:40 chain-drive gearing, so the rear wheels turn at:
990 RPM = 4400 RPM x (9/40)
With 13 inch diameter wheels, the racer moves at:
38 mph = 990 RPM x (π x 13 inch) x (60 min/hr) x (1 mile / 63.36x103 inch)
Which is scary fast if you ask me. A higher ratio may be in order.
At that speed the motor delivers: 1.6 HP = 1180 W = 2.6 N·m x 4400 RPM x 2π rad/rev / (60 s/min)
… to the shaft and, minus mechanical losses, to the tires.
If the racer doesn’t require that much power to roll at breakneck speed, it’ll go even faster, until the motor’s (falling) power output matches the (rising) mechanical load at some higher speed with correspondingly lower current.
With a current of 70 A and a winding resistance of 0.089 Ω (let’s say 0.10 Ω), the motor dissipates 490 W. That’s probably too much for long-term running, even with a 70% (= 1150 / (1150 + 490)) efficiency.
The mandated Littelfuse 60 A fuse has a bit under 1 mΩ of cold resistance and will dissipate 3.6 W at 60 A. The specs say it will blow within 6 minutes at rated current.
The resistance of the wiring / connectors / switches / whatever should be on that same order. Figuring the racer needs 2 m of stranded copper wire, that calls for 2 AWG or larger (0.5 mΩ/m). Right now, the racer uses 8 AWG (2 mΩ/m) and might have 4 mΩ total resistance, although I think it has less than 2 m of wire. Empirically, the motor conductors get really hot at 40 A for about ten seconds, but that’s with a severely defunct motor.
If the conductors + connectors between the battery and the motor introduce, say, 10 mΩ of resistance, they’ll dissipate 36 W at 60 A. That scales linearly with resistance, so a high-resistance connection will incinerate itself.
Using a PWM controller to reduce the speed will reduce the available horsepower, so the racer will accelerate slowly. With the torque limited to 2.6 N·m, the horsepower will vary linearly with the PWM duty cycle: nearly zero for small PWM, up to 1.5 HP for large PWM at 60 A, then upward as the RPM increases with decreasing load. Yeah, you get more torque when you need it least.
I could make a case for a three-speed transmission in addition to higher gear ratio, although that seems overly complex.
A less beefy motor will be in order and The Mighty Thor suggests a torque converter as a low-budget transmission. Sounds good to me; I should learn more about electric traction motors…
The Power Wheels Racer taking shape at SquidWrench let out The Big Stink at the Mini Maker Faire a few weeks ago, so I brought some test equipment to the regular Weekly Doing and helped with the autopsy.
The PWM motor controller purports to do 60 A at up to 50 V, but removing the cover showed it wasn’t going to do any more controlling:
Motor Controller – smoked housing
That smudge came from a rank of detonated MOSFETs:
Motor Controller – exploded MOSFET
Other MOSFETs had unsoldered themselves:
Motor Controller – unsoldered MOSFETs
Explosively:
Motor Controller – solder ejecta
I brought along an ancient Sears starter-motor ammeter to measure the motor current:
Sears 244-2145 Starter Ammeter – front
The magnetic field around the wire directly drives the meter movement, with two guides for the 75 A and 400 A ranges, and none of that newfangled Hall effect nonsense to contend with:
Sears 244-2145 Starter Ammeter – wire guides
Yeah, that says FEB 79; I’ve been collecting tools for quite a while…
I slapped the motor connectors directly on the battery terminals, holding them with small locking pliers after discovering that the wires got way too hot, way too fast. A snippet of retroreflective tape on the motor sprocket and a laser tach gave us the speed:
12 V: 1600 RPM @ 40 A
24 V: 2400 RPM @ > 100 A
The AmpFlow E30-400 motor data sheet confirmed that those numbers were grossly wrong. Unloaded, it should spin at 5700 RPM at 24 V while drawing 3.2 A (thus, 2800 RPM at 12 V & 1.6 A).
Diassembling the motor showed it hadn’t escaped the carnage:
Motor – charred windings
Those windings should be the usual amber enamel-over-copper, not charred black. The excessive current and reduced speed suggests many shorted turns inside the rotor.
Protip: never disassemble a working DC motor, because you’ll demagnetize the stator. The motor should still run when you put it back together, but the reduced magnetic field will wreck the performance.
As nearly as we could tell, one of the motor wires shorted to the frame when it got pinched under the seat; that’s an easy mistake to make and shows why compulsive wire neatness pays off big time. Shorting the controller output blew the transistors and, after raising the seat to look underneath, the motor would cook itself without generating much torque while you figure out what happened.
As far as I’m concerned, if you’ve never blown up anything that severely, you’re not building interesting stuff and definitely not trying hard enough.
The next iteration should work better!
Thanks to Dragorn of Kismet for stepping into the stench with phone camera in hand…
The Epson R380 started making an odd thwapping noise, which turned out to be the ink tubes from the Continuous Ink Supply System slapping the overly complex interior of the printer. They seemed a bit loose, but it took some searching before I found the top of the clamp that holds them in place:
Epson R380 CISS hose clamp – broken
It’s the white rectangle nestled in front of the ink cartridges, where it fits perfectly into a convenient slot and looks like it grew there.
I briefly considered 3D printing a replacement, but came to my senses:
Epson R380 CISS hose clamp – fixed
That should be good until the silicone rubber tubes finally break after a bazillion flex cycles…