Ed Nisley's Blog: Shop notes, electronics, firmware, machinery, 3D printing, laser cuttery, and curiosities. Contents: 100% human thinking, 0% AI slop.
I dropped that lens cap and the sheet-metal disk popped out; evidently the acrylic caulk doesn’t really count as an adhesive. Cleaned out the residue, ran a thin layer of urethane adhesive around the rim, and applied some clamps:
Re-clamping the cover
Cleaned out the inevitable urethane bubbles that emerge from even the most minute opening and it’s all good.
A few months after shaking off the previous fruit fly infestation, the worm compost bin has succumbed to another species of fruit fly that’s probably Drosophila melanogaster: much larger, breeds faster, and seems far more tenacious. Even though they’re completely innocuous, Something Must Be Done, but alas there are no insecticides suitable for a worm bin that produces vegetable garden compost. That reduces the situation to the Siege of Stalingrad: cut off their supplies and let them fight it out.
It seems that fruit flies and their progeny die slightly faster than worms; after three or six weeks without feeding, the flies will should be history and the worms will be eating the dead. Temperatures in the Basement Laboratory Vermiculture Wing will remain in the 60 °F range for the next month or two, so the fly egg-to-adult time will be longer than the usual eight days and this may not work as well as we’d like.
Assuming that succeeds, however, we’ll be freezing all the kitchen scraps that go into the bin to kill off the fruit fly eggs that arrive here from around the world. There seems no way to get fruits without fruit fly eggs, even with non-organic produce. Organic stuff, well, it’s worse than that.
I conjured up a Fruit Fly Escape trap that should I hope will lure flies out of the bin to their death, while keeping the worms inside.This won’t help much with the current extreme infestation, but may help dry the bin’s upper layer and, when we get the population knocked down, should exterminate the more adventurous survivors. Obviously, we’re breeding for stay-at-home fruit flies and, given their rapid-prototyping life cycle, they may evolve into tiny couch potatoes.
Anyhow.
Flies like heat and light, while worms vastly prefer cool and dark, so the general idea is to drill a hole in the bin lid, fit a long tube over it, put an LED ring light at the base, and run a flypaper spiral up the tube to a vent cap near the top. The first picture gives an overview, although it’s tough to see the vertical tube against the clutter: it’s clear with two red spirals, having started life as some weird-ass holiday decoration for the previous owners of our house.
Anyhow, the more interesting plastic bits look like this:
Fly Escape – solid model
The top ring is the vent cap, with a hole in the middle for a string supporting the sticky tape strip. The middle ring holds three sections of LED strip light that dissipate about 2 W from a 12 V wall wart; that’s enough heat around the tube to produce a slight upward draft. The riser tube at the bottom has an angled rim that compensates for the bin lid angle and holds the long tube vertical. The ring around the riser has a matching angle.
They fit into the lid thusly:
Fly Escape – Riser trial fit
Two beads of hot-melt glue, top and bottom, hold them in place and make an air- / worm- / fly-tight seal.
The inner tube holds the fly paper container and has a slight inward taper toward the top to wedge it in place:
Fly Escape – solid model – bottom
A similar view from inside the actual lid:
Fly Escape – Riser trial fit – bottom
That was the first pass at the dimensions; the tube walls didn’t quite join because I forgot to force the number of polygonal sides to be equal. It’s deliberately thin to make the walls springy, but everything must be Just Right to get both no fill and no space between the two perimeter threads.
The riser and LED ring, combined with festive spiral stripes along the tube and some silicone tape sealing the tubes together, produce a cheery nuclear glow that’s enhanced by the victims mired in the adjacent flypaper strips. A third strip runs up the middle of the tube:
Fly Escape in action
The vent cap on the top of the tube has a small hole in the middle to hold the string supporting the flypaper spiral exactly in the middle of the tube. This view is upside-down from the mounted orientation :
Fly Escape – Vent Cap
The alert reader will notice a red top plug in place of the vent cap in the first picture. This whole project happened over the course of a frantic afternoon, evening, and morning, with progressive product improvements along the way. For example, it turns out that some flies went pedestrian and walked up the inside of the tube, so there’s now a circle of screening inside that nice vented cap.
Having a 3D printer to hammer out custom plastic widgetry on a short schedule = win.
The OpenSCAD source code:
// Worm bin fly escape
// Ed Nisley KE4ZNU - March 2012
Layout = "Show"; // Build.. Show Riser Ring Cap
//- Extrusion parameters - must match reality!
ThreadThick = 0.25;
ThreadWidth = 2.0 * ThreadThick;
HoleFinagle = 0.3;
HoleFudge = 1.00;
function HoleAdjust(Diameter) = HoleFudge*Diameter + HoleFinagle;
Protrusion = 0.1; // make holes end cleanly
//-- Dimensions
RiserID = 47.0; // ID = transparent riser tube OD
RiserOD = 51.0; // OD = hole in lid (matches hole saw OD)
RiserHeight = 50.0; // wall height from lid
RiserSides = 4*8; // for consistency & symmetry
RiserBaseHeight = IntegerMultiple(5.0,ThreadThick); // stop ring height
RiserBaseID = RiserID - 2*1.0; // stop ring ID
LipOD = 59.0; // OD of lip mounted on lid around tube
LipAngle = 3.0; // angle for lip to make tube vertical
LipMinThick = IntegerMultiple(3.0,ThreadThick); // min lip thickness
LipAngleThick = LipOD*tan(LipAngle); // angled section thickness
LipThick = LipMinThick + LipAngleThick; // total lip thickness
RingClearance = 0.5; // space between ring and tube
TrapID = 23.0; // sticky tape container OD
TrapIDTaper = 2.0; // taper to hold container in place
TrapHeight = 45.0; // ... height
TrapWallThickness = 2*ThreadWidth;
TrapSides = 4*4;
TrapFlanges = 3; // number of support flanges
TrapFlangeThick = IntegerMultiple(3.5,ThreadWidth);
LEDThick = 2.5; // LED strip thickness
LEDWidth = 11.0; // ... width
LEDWireOD = 3.0; // power cable dia
LightID = RiserID + 2*LEDThick; // ID of LED collar
LightOD = LightID + 2*4*ThreadWidth; // ... OD
LightFlangeThick = IntegerMultiple(2.0,ThreadThick);
CapID = RiserID;
CapRingID = CapID - 2*1.5;
CapOD = CapID + 2*4*ThreadWidth;
CapBaseHeight = RiserBaseHeight;
CapHeight = 10.0 + CapBaseHeight;
CapSides = RiserSides;
CapFlanges = 3;
CapFlangeThick = TrapFlangeThick;
CapGuideID = 3.0;
CapGuideOD = CapGuideID + 6*ThreadWidth;
//-- Sticky tape container holder
module TrapMount() {
ODBot = TrapID + 2*TrapWallThickness;
ODTop = TrapID - TrapIDTaper + 2*TrapWallThickness;
difference() {
union() {
cylinder(r1=ODBot/2,r2=ODTop/2,h=TrapHeight,$fn=TrapSides);
for (i=[0:TrapFlanges-1])
rotate(i*(360/TrapFlanges) + 90) // align leg with thick side
translate([RiserOD/4,0,RiserBaseHeight/2])
cube([(RiserOD/2 - 4*Protrusion),TrapFlangeThick,RiserBaseHeight],center=true);
}
translate([0,0,-Protrusion])
cylinder(r1=HoleAdjust(TrapID)/2,
r2=HoleAdjust(TrapID - TrapIDTaper)/2,
h=(TrapHeight + 2*Protrusion),
$fn=TrapSides);
}
}
//-- Riser tube
module RiserTube() {
TotalHeight = RiserHeight + RiserBaseHeight;
difference() {
cylinder(r=RiserOD/2,h=TotalHeight,$fn=RiserSides);
translate([0,0,RiserBaseHeight])
PolyCyl(RiserID,TotalHeight,RiserSides);
translate([0,0,-Protrusion])
cylinder(r=RiserBaseID/2,h=TotalHeight,$fn=RiserSides);
}
}
//-- Angled lip around ring
// aligned with flat side downward at Z=0
module LipRing(Clearance = 0.0) {
difference() {
cylinder(r=LipOD/2,h=LipThick);
translate([0,0,-Protrusion])
cylinder(r=(RiserOD/2 + Clearance),
h=(LipThick + 2*Protrusion),
$fn=RiserSides);
rotate([LipAngle,0,0])
translate([-LipOD,-LipOD,(LipMinThick + LipOD/2*tan(LipAngle))])
cube([2*LipOD,2*LipOD,LipAngleThick],center=false);
}
}
//-- Collar to hold LED strip light
module LEDCollar() {
difference() {
PolyCyl(LightOD,(LEDWidth + LightFlangeThick));
translate([0,0,LightFlangeThick])
PolyCyl(LightID,(LEDWidth + Protrusion));
translate([0,0,-Protrusion])
PolyCyl(RiserID,(LightFlangeThick + 2*Protrusion));
translate([0,0,(LightFlangeThick + LEDWidth/2)])
rotate([0,90,90])
PolyCyl(LEDWireOD,LightOD);
}
}
//-- Cap to hold trap string and vent the tube
module VentCap() {
union() {
difference() {
cylinder(r=CapOD/2,h=CapHeight,$fn=CapSides);
translate([0,0,-Protrusion])
cylinder(r=CapRingID/2,h=(CapHeight +2*Protrusion),$fn=CapSides);
translate([0,0,CapBaseHeight])
cylinder(r=CapID/2,h=CapHeight,$fn=CapSides);
}
difference() {
union() {
for (i=[0:TrapFlanges-1])
rotate(i*(360/CapFlanges))
translate([CapOD/4,0,CapBaseHeight/2])
cube([(CapOD/2 - 4*Protrusion),CapFlangeThick,CapBaseHeight],center=true);
cylinder(r=CapGuideOD,h=CapBaseHeight);
}
translate([0,0,-Protrusion])
PolyCyl(CapGuideID,CapHeight);
}
}
}
//-- Handy routines
function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
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 + HoleFinagle)/2,h=Height,$fn=Sides);
}
//-- Put peg grid on build surface
module ShowPegGrid(Space = 10.0,Size = 1.0) {
Range = floor(50 / Space);
for (x=[-Range:Range])
for (y=[-Range:Range])
translate([x*Space,y*Space,Size/2])
%cube(Size,center=true);
for (z=[1:10])
translate([0,0,z*Space])
%cube(Size,center=true);
}
//- Build it
ShowPegGrid();
if (Layout == "Ring")
LipRing();
if (Layout == "Riser")
RiserTube();
if (Layout == "Cap")
VentCap();
if (Layout == "Show") {
color("SkyBlue") {
TrapMount();
RiserTube();
LipRing();
}
color("Salmon")
translate([0,0,2*LipThick])
rotate([180,0,0])
LipRing(RingClearance);
color("Chocolate")
translate([0,0,(1.25*RiserHeight)])
LEDCollar();
color("Sienna")
translate([0,0,2*RiserHeight])
rotate([180,0,0])
VentCap();
}
if (Layout == "Build1") {
TrapMount();
RiserTube();
LipRing();
}
if (Layout == "Build2") {
LipRing(RingClearance);
}
if (Layout == "Build3") {
LEDCollar();
}
if (Layout == "Build4") {
VentCap();
}
So the Epson R380’s magenta printhead has clogged and cleaning it doesn’t have any effect. I figured I’d pop the printhead out, rinse off the crud, and see if that improved the situation. Turns out, you can’t get there from here…
The first step is removing the printer side panels, which involves sliding a steel strip into the not-really-vent slots along the side to release the catches as described there. This picture shows what’s going on inside:
R380 side panel locking tab release
You must hit that slot in the catch with the strip, so the strip must be no wider than 15 mm = 5/8 inch and tapering the end would certainly help. After I removed the panels, I broke those latch tabs off; the panel has locating tabs that align the edges, so the latch tabs just keep you out.
In any rational printer, accessing the printhead for cleaning would be trivially easy. Epson has a different attitude: KEEP OUT!
My original idea was to release the rod upon which the ink tank carrier slides, then pull the whole thing out, but it turns out the rod is also a shaft that transmits rotary motion from one side of the printer to the other, plus a mechanism to raise and lower the printhead over the cleaning station (and, perhaps, the DVD carrier that I’ve never used). A vast assortment of gears, clips, encoder wheels, and doodads affixed to each end convinced me not to go that route right now.
The left side includes an impossibly delicate rotary encoder disk blocking the end of the shaft:
R380 left side mechanism
Prying the spring out of the shaft notch allows it to slide to the right until another spring clip slams up against the inside of the frame on the right side. That clip may be pry-able, but it’s carefully arranged so as to be maximally inconvenient to reach.
R380 right side interior
The ring holding the gear in place must be removable, somehow or another, even without an obvious hole or tab:
R380 right side mechanism
With that encoder wheel blocking the left end of the rod, I gave up.
Then I tried to dismantle enough of the ink tank carrier to release the printhead. The first step removed the tank carrier’s two side panels, both of which use pull-out clips to prevent them from sliding. A view of the removed panels shows the tabs:
R380 Ink Tank Carrier side panels latches
The outside panel requires jamming a small screwdriver behind that tab at an awkward angle, then the panel slides downward:
R380 Ink Tank Carrier – right side cover
You can release the inside panel with a fingernail near the top of the (unmarked, but obvious) tab outlined in white on the far right side, then slide upward:
R380 Ink Tank carrier – interior
The magenta circles mark three screws that secure the printhead plate to the carrier, but it won’t do you any good. The two rear screws require a narrow-shaft Philips #1 driver and you cannot get the screws out through the holes; I managed to get them back in place, but don’t loosen them until you figure out how to remove the assembly holding the electrical contacts for the ink tanks.
That assembly, marked by the six color panels, slides vertically into the rear wall of the carrier and seems to have a latch on the rear wall of the tank carrier. Of course, you can’t access the latch without dismantling the damn printer.
So I put everything back together again and the printer works no worse than it did before. I’m considering connecting a syringe with length of tubing to the magenta inlet port, then forcing a toxic mix of water, alcohol, and detergent through the printhead:
Although the economic argument for producing custom cookie cutters may not be persuasive, the fact that you (well, I) can produce custom widgets certainly is. Most of the things I build and repair don’t require great mechanical strength or finicky dimensional precision, so a DIY 3D printer is exactly the right hammer for the job.
The Peltier assembly looked like this while I was epoxying everything together with JB Weld:
Peltier module – epoxy curing
The aluminum-case resistor held the heatsink at 105 °F to encourage the epoxy to cure in a finite amount of time.
The 40 mm square block is a squared-up piece of 1/2 inch aluminum plate (manual CNC on the Sherline, nothing fancy) with a pair of 6-32 tapped holes for the screws that will hold TO-220 transistors or the yet-to-be-built TO-92 adapter. The CPU heatsink got a pair of symmetric holes for the posts holding it to the acrylic base, but other than that it’s perfectly stock.
MOSFET thermal block – drilling
Then epoxy the thermistor brick to the middle of the block between the two screws, stick on some obligatory Kapton tape to prevent embarrassing short circuits, and add a foam collar around the Peltier module to insulate the block from the heatsink:
MOSFET thermal block
A square foam shako covers everything, held down with a random chunk o’ weighty stuff, to insulate the whole affair from the world at large.
As I feared, P control can’t push the platform into the deadband all by itself at high temperatures, so I rewrote the loop the way it should have been all along:
PWM=1 beyond a limit well beyond the deadband, set integral=0 to avoid windup
Proportional + integral control inside that limit
Not worrying about relay chatter
Holding PWM=1 until the PI loop kicks in ensures that the P control won’t lose traction along the way, but full throttle must give way to PI control outside the deadband to avoid a massive overshoot. Relay chatter could be a problem around room temperature where the heating/cooling threshold falls within the deadband, but that won’t shouldn’t be a problem in this application.
Without much tuning, the results looked like this:
PI-Loop-Temps
Each temperature plateau lasts 3 minutes, the steps are 10 °C, starting at 30 °C and going upward to 50 °C, then downward to 0 °C, and upward to 20 °C. These are screenshots from OpenOffice Calc, so the resolution isn’t all that great.
Two internal variables show what’s going on:
PI-Loop-ErrDrive
The blue trace is the temperature error (actual – setpoint: negative = too cold = more heat needed), the purple trace is the signed PWM drive (-1.0 = full heat, +1.0 = full cool) summed from the P and I terms.
Overlaying all the plateaus with their starting edges aligned on the left, then zooming in on the interesting part, shows the detailed timing:
PI-Loop-ErrDrive-Overlay
These X axis units are in samples = calls to the PI function, which happened about every 100 ms, which is roughly what the main loop will require for the MOSFET measurements.
The Peltier module just barely reaches 0 °C with a 14 °C ambient: the drive exceeds +1.0 (output PWM = 255) as the temperature gradually stabilized at 0 °C with the module at full throttle; it’s dissipating 15 W to pump the temperature down. The heatsink reached 20 °C, with a simple foam hat surrounding the Peltier module and aluminum MOSFET mount. Any power dissipation from a MOSFET would add heat inside the insulation, but a bit more attention to detail should make 0 °C workable.
On the high end, it looks like the module might barely reach 60 °C.
Increasing the power supply voltage to increase the Peltier current would extend the temperature range, although a concerted stack probe didn’t produce anything like an 8 V 5A supply in the Basement Laboratory Parts Warehouse. If one turns up I’ll give it a go.
There’s a bit of overshoot that might get tuned away by fiddling with the P gain or squelching the integral windup beyond the deadband. The temperature changes will be the most time-consuming part of the MOSFET measurement routine no matter what, so it probably doesn’t make much difference: just stall 45 s to get past most of the transient overshoot, then sample the temperature until it enters the deadband if it hasn’t already gotten there. Reducing the initial overshoot wouldn’t improve the overall time by much, anyway, as it’d just increase the time to enter the deadband. Given that the initial change takes maybe 30 seconds at full throttle, what’s the point?
The PI loop Arduino source code, with some cruft left over from the last attempt, and some tweaks left to do:
#define T_LIMIT 3.0 // delta for full PWM=1 action
#define T_ACCEPT 1.5 // delta for good data (must be > deadband)
#define T_DEADBAND 1.0 // delta for integral-only control
#define T_PGAIN (1.0 / T_LIMIT) // proportional control gain: PWM/degree
#define T_IGAIN 0.001 // integral control gain: PWM/degree*sample
#define sign(x) ((x>0.0)-(x<0.0)) // adapted from old Utility.h library
//-- Temperature control
// returns true for temperature within deadband
int SetPeltier(float TNow, float TSet) {
float TErr, TErrMag;
int TSign;
float PelDrive;
int EnableHeat,OldEnableHeat;
static float Integral;
int TZone;
int PWM;
int PWMSigned;
TErr = TNow - TSet; // what is the temperature error
TErrMag = abs(TErr); // ... magnitude
TSign = sign(TErr); // ... direction
if (TErrMag >= T_LIMIT) // beyond outer limit
TZone = 3;
else if (TErrMag >= T_DEADBAND) // beyond deadband
TZone = 2;
else if (TErrMag >= T_DEADBAND/2) // within deadband
TZone = 1;
else // pretty close to spot on
TZone = 0;
switch (TZone) {
case 3: // beyond outer limit
PelDrive = TSign; // drive hard: -1 heat +1 cool
Integral = 0.0; // no integration this far out
break;
case 2: // beyond deadband
case 1: // within deadband
case 0: // inner deadband
PelDrive = T_PGAIN*TErr + T_IGAIN*Integral; // use PI control
Integral += TErr; // integrate the offset
break;
default: // huh? should not happen...
PelDrive = 0.0;
break;
}
EnableHeat = (PelDrive > 0.0) ? LOW : HIGH; // need cooling or heating?
OldEnableHeat = digitalRead(PIN_ENABLE_HEAT); // where is the relay now?
if (OldEnableHeat != EnableHeat) { // change from heating to cooling?
analogWrite(PIN_SET_IPELTIER,0); // disable PWM to flip relay
digitalWrite(PIN_ENABLE_HEAT,EnableHeat);
delay(15); // relay operation + bounce
}
PWM = constrain(((abs(PelDrive) * AO_PEL_SCALE) + AO_PEL_OFFSET),0.0,255.0);
analogWrite(PIN_SET_IPELTIER,PWM);
if (true) {
PWMSigned = (EnableHeat == HIGH) ? -PWM : PWM;
Serial.print(TSet,1);
Serial.print("\t");
Serial.print(TNow,1);
Serial.print("\t");
Serial.print(TZone,DEC);
Serial.print("\t");
Serial.print(TErr);
Serial.print("\t");
Serial.print(Integral,3);
Serial.print("\t");
Serial.print(PelDrive,3);
Serial.print("\t");
Serial.print(PWMSigned,DEC);
Serial.print("\t");
Serial.print(NowTime - StartTime);
Serial.println();
}
return (TZone <= 1);