In Beginning design of a cat drinking fountain I wrote

One of our cats likes to drinking from running water (a bathroom sink on a trickle setting), so my wife challenged me to make a drinking fountain for the cat that recirculates water in a water dish. This project will be mainly physical design (3D printing, gluing things together) with a little electronics to control the pump.

I finished the cat fountain this week—the amazing thing is that I only had to print each part once, with the first design working! (Well, that’s almost true—I printed a bunch of 2mm-thick test pieces for the hose clip, to try to get something that would hold firmly to the rim of the bowl, and I aborted one print of the hose clip after a couple of layers, when I realized that one part had not been made level on the bed before slicing.) All the big pieces went together on the first try, and the first fully printed hose clip worked.

Unfortunately, the cats have not show any interest in the fountain yet.

I’ve started using rounded-triangle holes for horizontal bolt holes. The edges of the holes are three circular arcs, with the centers of the circles at the points of an equilateral triangle, and the radius being the side length of the triangle. I make one of the vertices be in the positive z direction. The arched shape is a little easier for the printer to print than the flat top of a circular hole, though for holes this small even circular holes print ok.

I designed in a little clearance between the parts, so that I would not need to sand things to make them fit. The 0.15mm clearance I allowed seems to be about right—the pieces bolted together without problems and the fit seems to be pretty tight.

I drilled the melamine bowl with a 3/8″ Forstner bit, as that seemed to be exactly the right size for the 9.5mm inlet of the pump. Before drilling with the drill press, I covered both sides of the bowl where I was going to drill with transparent tape and taped a piece of MDF below the bowl to support the surface. I got no cracking and the pump outlet fit tightly in the resulting hole. I glued the pump to the bottom of the bowl with FlexEpox, which I also used to glue the two halves of the hose clamp together.

The controller is the design I used for the desk lamps—I happened to have one sitting around unused. I didn’t even change the code, though I’ve been thinking about changing the range of the PWM, and possibly raising the PWM frequency. The pump sometimes makes a quiet, but slightly annoying noise at about 2.2kHz when set to low flow rates. The PWM frequency is nominally 2.344kHz at low output and 9.375kHz at high output (with a 9.6MHz clock), and the 2.2kHz sound is within the ±10% spec for the RC oscillator in the ATtiny13 chip. I may not need as high a precision at low levels for the motor as I do for LEDs, so a simpler program that just uses a 256-step PWM at around 9kHz may be better.

A capacitor across the motor may also help to reduce the voltage fluctuation that causes the noise. If the motor is drawing about 300mA and the power is off for about ¾ of the period (at 2.2kHz), then we’d need about 100µC from the capacitor. If we want the voltage to drop 9V in that time, the average voltage is 4.5V and a 22µF capacitor would be about the right size. Of course, I’d have to go to lower duty cycles, as the average voltage across the motor would be much higher than without the capacitor. Even very short duty cycles may not be low enough, as the capacitor charges very quickly and the slow discharge may leave the pump running at too high a speed—in that case I’d need to make the capacitor smaller.

(Update 2019 Sept 1: a 10µF ceramic capacitor does seem to quiet the 2.2kHz sound while still allowing the motor to be turned down to the point where it stalls. Of course, my tinnitus is loud enough that the fountain may still be making noise, but I just can’t hear it over the tinnitus.)

(Update 2019 Sept 9: the pump was still whining, even with the capacitor, so yesterday I did the right thing and reprogrammed the ATTiny13 in the controller to use a PWM of ~37.5kHz, instead of 2.2kHz. The cats may still be able to hear it, as the cat range of hearing is about 48Hz–85kHz [https://doi.org/10.1016/0378-5955(85)90100-5], but ** I** can’t hear it.)

The fountain takes about 2–3W (measured at the AC input to the power supply), so I’ve not looked into adding a power switch or a motion detector to turn the pump on and off.

As usual, I designed everything using OpenSCAD.

### nozzle1.scad

// barbed nozzle(s) for pump // Kevin Karplus // 2019 August 12 // Creative Commons Attribution-ShareAlike (CC BY-SA 3.0) module nozzle(ID=3, OD=6, final_OD=undef, length=undef, num_barbs=undef) { $fa=5; $fs=0.1; barb_diam= 1.15*OD; barb_length= OD/3; thickness=(OD-ID)/2; assert(num_barbs!=undef || length!=undef); barb_count = num_barbs!=undef? num_barbs: max(1, min(4, length/barb_length -2)); real_length = length!=undef? length: (barb_count+1)*barb_length; real_final_OD = final_OD==undef? OD: final_OD; flare_from = (barb_count+1)*barb_length; assert(flare_from < real_length || OD==real_final_OD); barbs = [for (i=[1:barb_count]) each [[barb_diam/2, i*barb_length], [OD/2, i*barb_length]] ]; profile = concat( [[ID/2,0], [OD/2,0]], barbs, [ [OD/2, flare_from], [real_final_OD/2,real_length], [real_final_OD/2-thickness,real_length], [ID/2,flare_from], [ID/2,0] ]); echo(profile=profile); rotate_extrude() { polygon(profile); } } intersection() { color("red") nozzle(length=20, num_barbs=3, final_OD=10); color("blue",0.4) translate([0,-12,0]) rotate([30,0,0]) cube(36,center=true); }

### hose_clip_v6.scad

// cat dish hose clip // // Kevin Karplus 2019 Aug 25 // Creative Commons Attribution-ShareAlike (CC BY-SA 3.0) // // v1 uses linear_extrude for bowl rim, has wrong rim profile // v2 uses rotate_extrude for bowl rim, rim profile angled wrong outside, too wide // v3 has taller clip. Rim profile is better, but still not right. // v4 tweaked the rim profile, but still a little loose // v5 tweaked the rim profile some more, but apparently in the wrong direction // v6 seems to have an ok rim profile, though clipping it on the rim opens the // fork a bit, so that the rim is only grasped near the top. use <BOSL2/std.scad> include <BOSL2/paths.scad> include <BOSL2/rounding.scad> pi=3.14159265358979; $fa=0.3; $fs=0.1; bowl_diam = 254; // bowl diameter at rim // rim profile inside = [ [0,0], [-2,0], [-3,-0.3], [-4,-1], [-5.1,-2], [-6.6,-4], [-7.2,-5], [-10,-10], [-16,-20], [-22,-30]]; L2 = reverse([for (pt=inside) pt+[0,3.4]]); outside = [ [0,0], [-1.9,-0.9], [-2.8,-1.4], [-4.7, -2.8], [-5.5,-3.5],[-6.5,-4.6],[-10,-9.9], [-13.6,-15], [-18.1,-21.6], [-22.3,-27.8], [-26,-33.6] ]; L4 = simplify2d_path(concat(outside,[outside[len(outside)-1]+[1,-3]], reverse(offset(outside,5)), [[5,-5], [5,9]], reverse(offset(L2,5)), [L2[0]+[-3,-1]], L2 , [[0,0]] )); module rotate_extrude_at_origin(r=bowl_diam, arc_l=12) // like linear_extrude but curving the extrusion by // rotating about x=-r, to get an arc length of arc_l for point at origin { angle = arc_l/r*180/pi; rotate([-90,0,0]) translate([-r,0,0])rotate_extrude(angle=-angle) translate([r,0]) children(); } module hose_clip(clip_thick=12) { r= bowl_diam/2; angle = clip_thick/r *180/pi; difference() { color("blue")rotate_extrude_at_origin(r=r, arc_l=clip_thick) { polygon(L4); translate([0,-5])square([5,30]); } translate([-r,0,0]) rotate([0,-angle/2,0]) translate([r,0,0]) { translate([0,20,0]) rotate([0,90,0]) cylinder(d=9, h=11, center=true); translate([0,25,0]) cube([11,10,7.5], center=true); } } } module split(r=bowl_diam/2, arc_l=clip_thick, move_to=[15,0]) // split the children at the a plane through x=-r, with angle top_angle/2 // to the xy plane, rotating so that top_angle is on xy plane, and // moving it over to move_to { // below cut plane intersection() { children(); rotate_extrude_at_origin(r=r, arc_l=arc_l/2) { translate([-r+0.001,-2*r]) square( [4*r,4*r]); } } // above cut plane top_angle = arc_l/r *180/pi; translate(point3d(move_to)) rotate([0,180,0]) // rotate so top surface is now on bottom translate([0,0,-r*sin(top_angle/2)]) // top is xy plane rotate([0,top_angle/2,0]) // top is horizontal intersection() { color("red") translate([-r,0,0]) rotate([0,top_angle/2,0]) translate([r,0,0]) children(); rotate_extrude_at_origin(r=r, arc_l=arc_l/2) { translate([-r+0.001,-2*r]) square( [4*r,4*r]); } } } // test rim shape // hose_clip(2); clip_thick=16; split(arc_l=clip_thick) hose_clip(clip_thick);

### cat_dish_base_v1.scad

// Base for running-water cat bowl // // Kevin Karplus 2019 Aug 24 // Creative Commons Attribution-ShareAlike (CC BY-SA 3.0) // First some measurements and model for the pump pump_d=26; pump_h=25.7; outlet_d=6; wire_h = 15; module pump() { $fa=4; $fs=0.1; cylinder(d=pump_d, h=pump_h-4); cylinder(d=21, h=pump_h); cylinder(d=9.5, h=35.7); translate([0,-3.7-6.88/2,pump_h-outlet_d/2]) rotate([0,90,0]) cylinder(d=outlet_d, h=10+pump_d/2); rotate([0,0,135+8.5]) translate([10,0,-wire_h]) cylinder(d=1,h=wire_h); rotate([0,0,135-8.5]) translate([10,0,-wire_h]) cylinder(d=1,h=wire_h); } // how much space should be left between matching surfaces? clearance = 0.15; module nth_circle(diam=127, thickness=18, arc=120) // draw a part of a circular arc (no more than 180 degrees) // with outer diameter diam and inner diam diam-2*thickness { $fa=1; intersection() { difference() { circle(d=diam); circle(d=diam-2*thickness); } scale=diam*2; polygon(scale*[[0,0], [1,0], [cos(arc/2), sin(arc/2)], [cos(arc), sin(arc)] ]); } } module overhanging_circle(arc=120, diam=127, outer=10, inner=8, overlap_degrees=20) // Two circular arcs offset from one another, with thickness "outer" // for the outer arc and "inner" for the inner arc. // The outer arc starts on the x-axis, the inner one at "overlap_degrees". // { color("red") nth_circle(arc=arc, diam=diam, thickness=outer-clearance/2); mid_diam = diam-2*outer-clearance; color("green")rotate([0,0,overlap_degrees]) nth_circle(arc=arc, mid_diam, thickness=inner); color("purple")rotate([0,0,overlap_degrees]) nth_circle(arc=arc-overlap_degrees, diam, thickness=inner+outer); echo ("max_dist = ", 0.5*norm([diam,0] - diam*[cos(arc),sin(arc)]), "or", 0.5*norm([diam,0] - mid_diam*[cos(arc+overlap_degrees),sin(arc+overlap_degrees)]) ); } module rounded_triangle(side=1) // intersection of three circles, centers at corners of equilateral triangle. // On corner is on x axis. { intersection() { translate([side/sqrt(3),0]) circle(r=side,$fn=60); translate([-side/(2*sqrt(3)), side/2]) circle(r=side, $fn=60); translate([-side/(2*sqrt(3)), -side/2]) circle(r=side, $fn=60); } } module rounded_beam(side=1, length=3, center=false) // rounded_triangle beam from (0,0,0) to (0,0,length). // (0,0,-length/2) to (0,0,length/2) if center is set. // Corners of beam as for rounded_triangle. { linear_extrude(height=length, center=center) rounded_triangle(side=side); } module screw_hole(side=3.5, cap_side=6, cap_depth=4, length=18) // rounded_triangle screw hole, for horizontal screw holes // Top of screw_hole at x=0, screw extends length+cap_depth along -x axis. // Hole is oriented so that point of arch in +z direction. // Default sizes are appropriate for M3 screws. { rotate([0,-90,0]) // put beam along negative x-axis { rounded_beam(side=cap_side, length=cap_depth); rounded_beam(side=side, length=length+cap_depth); } } module drilled_overhang(arc=90, diam=127, outer=10, inner=8, overlap_degrees=10, height = 40, hole_d=3.5, cap_diam=6, cap_depth=4) // make a part of the circular wall, with screw holes to line up sections { assert (outer> cap_depth+3, "Outer wall thick enough for screw hole"); clearance_angle = atan(2*clearance/diam); difference() { linear_extrude(height) overhanging_circle(arc=arc-clearance_angle, diam=diam, outer=outer, inner=inner, overlap_degrees=overlap_degrees); for (angle= [overlap_degrees/2, arc+overlap_degrees/2]) for (z= [height/3, 2*height/3]) { translate([0,0,z]) // move beam up rotate([0,0,angle]) // put beam in correct orientation translate([diam/2+0.001,0,0]) // move out to circle screw_hole(side=hole_d, cap_side=cap_diam, cap_depth=cap_depth, length=outer+inner+0.1); } } } module controller_board() // cut this module out of a drilled overhang to make room for the // controller board { $fa=2; $fs=0.1; hole_centers=60; width=36; mid_length=34; length= mid_length+ width; height=15; translate([0,0,-height]) linear_extrude(2*height) polygon([ [length/2,0], //point [mid_length/2,width/2], [-mid_length/2,width/2], // mid-length side [-length/2, width/4], [-length/2,0], [-length/2, -width/4],// spread point (for wires) [-mid_length/2,-width/2], [mid_length/2,-width/2] ]); cylinder(d=8, h=32); translate([hole_centers/2,0,22]) rotate([90,-90,0]) screw_hole(side=3.5, cap_side=6, cap_depth=4, length=18); translate([-hole_centers/2,0,22]) rotate([90,-90,0]) screw_hole(side=3.5, cap_side=6, cap_depth=4, length=18); } module controller_panel(arc=110) // minor bug: the top edge has a cantilevered part that barely works--- // the first layer of the cantilevered part does not bond to the // subsequent layers. // Possible fixes include removing the cantilevered part (cutting // the top back to just the front face, as was done on the power panel) // or tapering the cantilever so that it has a 45-degree slope to it. { difference() { overhang=10; drilled_overhang(arc=arc,overlap_degrees=overhang); rotate([0,0,overhang/2+arc/2-90]) { translate([0,35,20]) rotate([-90,0,0]) // make up be +y direction controller_board(); translate([0,35+35+22,20]) cube(70,center=true); } } } module notch_panel(arc=80,height=40, diam=127) // minor bug: the notch could be just a little tighter to // grasp the 1/4" ID vinyl tubing, rather than having it resting loose in // the notch. { hose_d=10; difference() { drilled_overhang(diam=diam,arc=arc,overlap_degrees=10); translate([0,0,height-hose_d]) rotate(arc/2) { translate([0,-hose_d/2,0]) cube([diam+1,hose_d,hose_d+1]); rotate([0,90,0]) cylinder(d=hose_d,h=diam+1, $fs=0.1); } } } module pump_panel(arc=90,height=40, diam=127) // Bug: There is nothing in this version to keep the pump from sliding off // the platform if the base rotates—a rim is needed around the pump. // Minor bug: the platform is a little too high, as there was not enough // clearance for the wall of the hose between the outlet of the pump and the // bottom of the bowl. { offset=diam/2-33; platform_h = height-pump_h; scaled_offset = offset/ (pump_d/2); alpha=acos(1/scaled_offset); tangent1 = [cos(180+alpha),sin(180+alpha)]; tangent2 = [cos(180-alpha),sin(180-alpha)]; union() { color("blue") drilled_overhang(arc=arc,overlap_degrees=10, height=height); rotate(arc/2) translate([offset,0]) difference() { pump_angle=135; // rotation to align outlet with notch union() { cylinder(h=platform_h, d=pump_d, $fs=0.1, $fa=1); color("cyan") linear_extrude(platform_h) polygon( [[0,0], 10*[cos(135+pump_angle), sin(135+pump_angle)], // tangent1*pump_d/2, tangent1*pump_d+[offset,0], [pump_d,0], tangent2*pump_d+[offset,0], tangent2*pump_d/2 ]); } // notch for pump wires rotate([0,0,135+pump_angle]) translate([10+1.8,0,0]) cylinder(d=7.2,h=3*platform_h, center=true, $fa=1, $fs=0.1); // ghost of pump to check alignment---not part of model % translate([0,0,platform_h]) rotate([0,0,pump_angle]) pump(); } } } module power_panel(arc=80,diam=127,height=40) // minor bug: the hole for the socket for barrel adapter is // slightly too small. // The socket can be screwed into the hole, but not slid in. // Slight sanding with a riffler was enough to fix the problem. { outer_face = diam/2-1; wall_t = 5; cut_w = 16; difference() { overhang=10; drilled_overhang(diam=diam,arc=arc,overlap_degrees=overhang); rotate(arc/2+overhang/2) { translate([0,0,height/2]) rotate([0,90,0]) // drop into +x direction cylinder(d=7.7, h=diam/2+1, $fs=0.1); translate([outer_face,-35,0]) cube(70); // outer face translate([outer_face-wall_t-cut_w, -cut_w/2, 0]) // inner face cube([cut_w,cut_w,height+0.001]); } } } // comment out all but one panel for making STL files pump_panel(); color("red") rotate([0,0,90])notch_panel(); color("green") rotate([0,0,170]) controller_panel(); color("purple") rotate([0,0,280]) power_panel();