Posted 24 August 2020, 1402 days into the Covid-19 Lockdown
My autonomous wall-following robot Wall-E2 is now smart enough to reliably follow walls and connect to a charging station, at least in my office ‘sandbox’ testing area, as shown in the following video
However, as can be seen toward the end of the video, Wall-E2 had some trouble and almost got stuck making the third 90 degree turn. Apparently the current thin 90mm wheels just don’t provide enough traction on carpet.
So, I decided to see what I could do about re-wheeling Wall-E2. After some research I found there are now plenty of larger diameter wheels for robots out there, but I couldn’t seem to find a set that would fit Wall-E2 and still allow me to keep the current set of wheel guards. I needed the same (or maybe slightly larger) diameter for ‘road’ clearance, but something less than about 20 mm thick to fit within the current wheel guard dimensions. Then it occurred to me while reading the specs for one of the wheels (ABS for the wheel, and TPU for the tire) that I already had two 3D printers standing around waiting for something to do, and I had a plentiful supply of ABS (or in my case, PETG) and TPU filaments – why not build my own? After all, how hard could it be? As you might guess, that question started what now feels like a 10-year slog through ‘3D printed wheel hell’
I wanted to create a spoked wheel with a hub that would accept a 3mm flatted motor shaft, and I wanted to fit this wheel with a simple TPU treaded tire. The wheel would have small ‘guard rail’ rims that would keep the tire from sliding off.
It started innocently enough with a search through Thingiverse, where I found several SCAD scripts for ‘parameterized’ wheels. Great – just what the doctor ordered! Well, except that the scripts, which may have worked fine for the authors, didn’t do what I wanted. and as soon as I tried to adjust them to fit my design specs, I discovered they were incomplete, buggy, or both.
I had wanted to learn a bit more about SCAD anyway and this seemed like a good project to do that with, so I persevered, and eventually came up with a SCAD design that I liked.
I started with bioconcave’s ‘Highly Modular Wheel_v1.0.scad’ file from Thingiverse, and (after what seemed like years trying to understand what was going on) was able to extract modular pieces into my own ‘FlatTireWheel’ scad script, as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 |
/* The wheel is defined as having a hub, a rim, and a tire. Wheel dimensions are overall diameter and width, i.e. a 100mm x 30mm wheel will be a cylindrical shape with an overall diameter of 100mm and a height (width) of 30mm. The rim will be a hollow cylinder with ID = overall diam - tire thickness - rim thickness, and OD = ID + rim thickness. The cylindrical area between the center of the wheel and the ID of the rim may be solid or spoked, and there may or may not be a hub. */ $fn=150; wheelDiameter = 90; //overall diameter of the wheel, including rim & tire wheelWidth = 15; //overall width (height) of the wheel, including guardrails rimThickness = 5; //rim thickness (part of overall tire diameter) tireThickness = 5; //tire thickness (part of overall tire diameter) guardrailThickness = 2; //doesn't add to overall tire diameter guardrailWidth = 1; //included in overall tire width spokeThickness = 9; numberOfSpokes = 3; spokeEccentricity = 1.5; //how elliptical do the spokes look //derived values wheelMinusRimDiameter = wheelDiameter - rimThickness; rimOD = wheelDiameter - tireThickness; rimID = rimOD - rimThickness; // The hub includeHub = true; // Set to false to remove the hub and only include the shaft diameter hole. hubDiameter = 15; // The diameter of the hub portion of the wheel hubHeight = 18; // The total height of the hub hubZOffset = -wheelWidth/2; // The Z position of the hub, negative numbers from the surface of the wheel innerCircleDiameter = 0; // The diameter of the solid inner circle under the hub, or zero for none. baseFilletRadius = 2; // The radius of the fillet (rounded part) between the hub and wheel. topFilletRadius = 2; // The radius of the fillet (rounded part) at the top of the hub. chamferOnly = false; // Set to true to use chamfers (straight 45-degree angles) instead of fillets. concavity = [0,0]; //hardware shaftDiameter = 4.5; // The diameter of the motor shaft shaftFlatDiameter = 3.5 ; // The diameter of the motor shaft at the flat, or shaftDiameter for no flat. setScrewCount = 1; // The number of set screws/nuts to render, spaced evenly around the shaft setScrewDiameter = 3; // The diameter of the set screw. 3 is the default for an M3 screw. setScrewTrap = [5.4, 2.3]; // Size [indiameter, thickness] of set screw nut. The depth is set automatically. setScrewNutDiameter = 5.4; // The "diameter" of the captive nut, from flat to flat (the "in-diameter") setScrewNutThickness = 2.3; // The thickness of the captive nut setScrewNutOffset = 0; // The distance to offset the nut from the center of the material. -/+ = in/out servoHoleDiameter = 0; // The diameter of servo arm hounting holes, or zero if no holes servoHoleDistance1 = 25; // Distance across servo horn from hole to hole (0 to ignore) servoHoleDistance2 = 21; // Distance across servo horn from hole to hole, rotated 90 degrees (0 to ignore) servoArmRotation = 45; // The total rotation of all servo holes servoNutTrap = [4,1.6]; // Size [indiameter, depth] of servo arm captive nut, or 0 (any) for none. outerNutTrap = [12.5,0]; // Size [indiameter, depth] of a captive nut, or 0 (any) for none. wheel_with_tire(); if ( includeHub ) { translate([0,0, hubHeight/2 + wheelWidth/2 + hubZOffset - concavity[0]]) hub(hubHeight, hubDiameter, shaftDiameter, shaftFlatDiameter, setScrewCount, setScrewTrap, setScrewDiameter, setScrewNutOffset, hubZOffset, baseFilletRadius, topFilletRadius, chamferOnly); } ///////////////////////////////////////////////////////////////////////////// // Modules... ///////////////////////////////////////////////////////////////////////////// // The hub (the part that holds the wheel onto the motor module hub( height, diameter, shaftDiameter, shaftFlatDiameter, nuts, nutSize, setScrewDiameter, setScrewNutOffset=0, hubZOffset=0, baseFilletRadius=0, topFilletRadius=0, chamferOnly=false) { hubWidth=(diameter-shaftDiameter)/2; union() { difference() { // Main hub shape union() { difference() { union() { cylinder( h=height, r=diameter/2, center=true ); // First chamfer the base... translate([0,0,hubZOffset]) rotate_extrude() translate([diameter/2,-(height/2)-hubZOffset,0]) polygon(points=[[0,0],[0,baseFilletRadius],[baseFilletRadius,0]]); } // Chamfer the top... rotate_extrude() translate([diameter/2,height/2,0]) polygon(points=[[0.5,0.5],[-topFilletRadius-0.5,0.5],[0.5, -topFilletRadius-0.5]]); // Carve the bottom fillet from the chamfer if ( !chamferOnly ) { translate([0,0,hubZOffset]) rotate_extrude() { translate([(diameter/2)+baseFilletRadius, -(height-(2*baseFilletRadius))/2-hubZOffset,0]) { circle(r=baseFilletRadius); } } } } // Add the fillet back on top of the top chamfer if (!chamferOnly) { rotate_extrude() { translate([ (diameter/2)-topFilletRadius, (height-(2*topFilletRadius))/2, 0]) circle(r=topFilletRadius); } } } // Remove the bore echo(str("shaftDiameter = ", shaftDiameter)); difference() { cylinder( h=height+1, r=shaftDiameter/2, center=true ); translate([(shaftDiameter-shaftFlatDiameter+1)/2 + (shaftDiameter/2) - (shaftDiameter - shaftFlatDiameter),0,0]) cube( [shaftDiameter-shaftFlatDiameter+1,shaftDiameter,height+2], center=true ); } // Remove the captive nut //08/22/20 gfp bugfix: chg translate() z param to zero so it follows hub offsets //08/22/20 gfp bugfix: repl 'boreDiameter' with 'shaftDiameter' to reduce confusion for( i=[0:nuts-1] ) { rotate([ 0,0, (360/nuts)*i ]) translate([shaftDiameter/2+(diameter-shaftDiameter)/4 +setScrewNutOffset, 0, 0]) { rotate([0,-90,0]) { captiveNut( nutSize, setScrewDiameter, depth=height/2+1, holeLengthTop=hubWidth/2+setScrewNutOffset +(shaftDiameter-shaftFlatDiameter), holeLengthBottom=hubWidth+baseFilletRadius-setScrewNutOffset); } } } } } } module wheel_with_tire() { difference() { union() { cylinder(wheelWidth, rimOD/2, rimOD/2, $fn=150); cylinder(guardrailWidth, guardrailThickness + rimOD/2, guardrailThickness + rimOD/2, $fn=150); translate(v=[0,0,wheelWidth-guardrailWidth]) { cylinder(guardrailWidth, guardrailThickness + rimOD/2, guardrailThickness + rimOD/2, $fn=150); } } translate(v=[0,0,-2]) { cylinder(wheelWidth+4, rimID/2, rimID/2, $fn=150); } //08/22/20 remove setscrew access hole //extend the hub shaft capture screw hole out past rim, so that //it will pierce the rim if the hub sits low enough on the rim if ( includeHub ) { nutSize=setScrewTrap; translate([0,0, hubHeight/2 + wheelWidth/2 + hubZOffset]) hubHoleCutout(hubHeight, hubDiameter, shaftDiameter, shaftFlatDiameter, setScrewCount, setScrewTrap, setScrewDiameter, setScrewNutOffset, hubZOffset, baseFilletRadius, topFilletRadius, chamferOnly); } } if(numberOfSpokes > 0) { difference() { for (step = [0:numberOfSpokes-1]) { rotate( [0, 0, step*(360/numberOfSpokes)] ) ellipticalSpoke(wheelWidth, wheelMinusRimDiameter, spokeEccentricity); } // Remove the motor shaft and setscrew nut trap cutouts if necessary if(includeHub) { union() { //flatted motor shaft cutout difference() { cylinder( h=(wheelWidth+hubHeight) + 2, r=shaftDiameter/2, center=true ); translate([(shaftDiameter-shaftFlatDiameter+1)/2 + (shaftDiameter/2) - (shaftDiameter - shaftFlatDiameter),0,0]) cube( [shaftDiameter-shaftFlatDiameter+1,shaftDiameter,10*hubHeight+2], center=true ); } //setscrew nut trap cutout //08/22/20 gfp bugfix: chg translate() z param to zero so it follows hub offsets //08/22/20 gfp bugfix: repl 'boreDiameter' with 'shaftDiameter' to reduce confusion // for( i=[0:nuts-1] ) { hubWidth=(hubDiameter-shaftDiameter)/2; // rotate([ 0,0, (360/nuts)*i ]) // translate([shaftDiameter/2+(hubDiameter-shaftDiameter)/4 +setScrewNutOffset, // 0, 0]) translate([shaftDiameter/2+(hubDiameter-shaftDiameter)/4 +setScrewNutOffset,0, hubHeight/2 + wheelWidth/2 + hubZOffset]) { rotate([0,-90,0]) { echo(str("captiveNut(", setScrewTrap, ",",setScrewDiameter, ",",hubHeight/2+1, ",",hubWidth/2+setScrewNutOffset +(shaftDiameter-shaftFlatDiameter), ",",hubWidth+baseFilletRadius-setScrewNutOffset,") call in spoke")); captiveNut( setScrewTrap, setScrewDiameter, depth=hubHeight/2+1, holeLengthTop=hubWidth/2+setScrewNutOffset +(shaftDiameter-shaftFlatDiameter), holeLengthBottom=hubWidth+baseFilletRadius-setScrewNutOffset); } } } } } //08/22/20 remove setscrew access hole //extend the hub shaft capture screw hole out past rim, so that //it will pierce the rim if the hub sits low enough on the rim if ( includeHub ) { hubWidth=(wheelDiameter-shaftDiameter)/2; nutSize=setScrewTrap; translate([0,0, hubHeight/2 + wheelWidth/2 + hubZOffset]) hubHoleCutout(hubHeight, hubDiameter, shaftDiameter, shaftFlatDiameter, setScrewCount, setScrewTrap, setScrewDiameter, setScrewNutOffset, hubZOffset, baseFilletRadius, topFilletRadius, chamferOnly); } } } } module hubHoleCutout( height, diameter, boreDiameter, shaftFlatDiameter, nuts, nutSize, setScrewDiameter, setScrewNutOffset=0, hubZOffset=0, baseFilletRadius=0, topFilletRadius=0, chamferOnly=false) { hubWidth=(diameter-boreDiameter)/2; union() { // Remove the captive nut for( i=[0:nuts-1] ) { rotate([ 0,0, (360/nuts)*i ]) // translate([boreDiameter/2+(diameter-boreDiameter)/4 +setScrewNutOffset, // 0, height/2 - (height+hubZOffset)/2]) translate([boreDiameter/2+(diameter-boreDiameter)/4 +setScrewNutOffset, 0,0]) { rotate([0,-90,0]) { echo(str("before call to captiveNutHoleExtension(",nutSize, ", ",setScrewDiameter, ", ",depth=height/2+1, ", ",hubWidth/2+setScrewNutOffset +(boreDiameter-shaftFlatDiameter), ", ",hubWidth+baseFilletRadius-setScrewNutOffset)); //* captiveNut( nutSize, setScrewDiameter, // depth=height/2+1, holeLengthTop=hubWidth/2+setScrewNutOffset // +(boreDiameter-shaftFlatDiameter), // holeLengthBottom=hubWidth+baseFilletRadius-setScrewNutOffset); captiveNutHoleExtension( nutSize, setScrewDiameter, depth=height/2+1, holeLengthTop=hubWidth/2+setScrewNutOffset +(boreDiameter-shaftFlatDiameter), holeLengthBottom=hubWidth+baseFilletRadius-setScrewNutOffset); } } } // } } } module ellipticalSpoke(width, diameter, eccentricity) { translate([0,0,width/2]) intersection() { cylinder(h=width, r=wheelMinusRimDiameter/2, center = true); translate([spokeEccentricity*wheelMinusRimDiameter/4, 0, 0]) scale([spokeEccentricity,1,1]) { difference() { cylinder(h = width, r = wheelMinusRimDiameter/4, center = true); //translation and width addition to make sure of good punch-out translate([spokeThickness,0,0]) cylinder(h = width+4, r = wheelMinusRimDiameter/4, center = true); } } } } //circleSpokes( d, wheelWidth, spokeThickness, proportion, numberOfSpokes ); // //// Circles pattern spokes //module circleSpokes( wheelDiameter, wheelWidth, spokeThickness, proportion, numberofSpokes ) { // echo( "Circles Style..." ); // intersection() { //# cylinder( h=wheelWidth, r=wheelDiameter/2, center = true ); // // for (step = [0:numberofSpokes-1]) { // rotate( [0, 0, step*(360/numberofSpokes)] ) // circleSpoke( wheelDiameter, wheelWidth, spokeThickness, proportion ); // } // } // //} //module circleSpoke( wheelDiameter, wheelWidth, spokeThickness, proportion ) { //// render() // echo(str("spokeThickness = ",spokeThickness)); // let( ox=(wheelDiameter/2)*proportion[0], oy=(wheelDiameter/2)*proportion[1] ) // let( ix=ox-(spokeThickness*2), iy=oy-(spokeThickness*2) ) // { // translate ( [-ox/2, 0, 0] ) // { // difference() // { // scale([proportion[0],proportion[1],1]) //# cylinder( r=wheelDiameter/4, h=wheelWidth, center=true); // scale([(ix/ox)*proportion[0],(iy/oy)*proportion[1],1]) // cylinder( r=wheelDiameter/5, h=wheelWidth +1, center=true); // } // } // } //} // This is the captive nut module I use in several of my designs. module captiveNut( nutSize, setScrewHoleDiameter=3, depth=10, holeLengthTop=5, holeLengthBottom=5 ) { render() union() { nut( nutSize ); if ( depth > 0 ) translate([depth/2,0,0]) cube( [depth, nutSize[0], nutSize[1]], center=true ); translate([0,0,-(nutSize[1]/2)-holeLengthBottom]) cylinder(r=setScrewHoleDiameter/2, h=nutSize[1]+holeLengthTop+holeLengthBottom, $fn=15); } } // nutSize = [inDiameter,thickness] module nut( nutSize ) { side = nutSize[0] * tan( 180/6 ); if ( nutSize[0] * nutSize[1] != 0 ) { for ( i = [0 : 2] ) { rotate( i*120, [0, 0, 1]) cube( [side, nutSize[0], nutSize[1]], center=true ); } } } // This extends the captive nut hole so through the wheel rim for tool access module captiveNutHoleExtension( nutSize, setScrewHoleDiameter=3, depth=10, holeLengthTop=5, holeLengthBottom=5 ) { translate([0,0,-wheelDiameter/2]) cylinder(r=1+setScrewDiameter/2, h=wheelDiameter/2, $fn=15); } |
Here’s a screenshot of a completed 86mm wheel with elliptical spokes and a hub compatible with a 3mm flatted shaft. When a TPU tire is added to the rim, the assembly should be about 90mm in diameter.
One of the many issues I had with the original code is it assumed the hub would sit on top of the spokes, and therefore there was no need to worry about whether or not the setscrew arrangement would be blocked by the spokes and/or rim. Since I wanted a wheel that was mostly tread, I wanted to ‘sink’ the hub into the spokes as shown above. In order to make this work, I needed to extend the setscrew access hole through the spoke assembly and through the rim. In the finished design, the hub assembly can be moved freely up and down in the center of the wheel, and the hole extensions will follow. If the hub setscrew hole isn’t blocked by the spokes and/or rim, then the extensions don’t do anything; otherwise they extend the setscrew hole as shown above.
Here’s a photo of separate wheel/tire pieces, and a completed wheel/tire combination on Wall-E2
2 September 2020 Update:
After running some sandbox tests with my new wheels, I discovered that the new tires didn’t have much more traction than the old ones. However, now that I ‘had the technology’, it was a fairly simple task to design and print new tires to fit onto the existing new rims. Rather than do the tire design in SCAD, I found it much much easier to do this in TinkerCad. Here’s a couple of screenshots showing the TCad design.
03 September 2020 Update:
The increased traction provided by the new tires have caused a new problem; on a hard surface the rotation during a ‘spin turn’ (one side’s motors going forward, the other going in reverse) is too fast, causing the robot to slide well past the target heading. Not so much of a problem on carpet, but how would the robot know which surface is in play at the moment. After some thought, I decided to try and modulate the turn rate, in deg/sec, as a proxy for the surface type. So, in ‘SpinTurn()’ I put in some code to monitor the turn rate and adjust the motor speeds upward or downward to try and keep the turn rate at a reasonable level.
Here’s a video of a recent run utilizing the new ‘SpinTurn’ rate modulation algorithm
And the data from the three ‘spin turn’ executions, one on hard surface, and two on carpet.
As can be seen in the above video and plots, the motor speeds used on the hard surface turn is much lower than the speeds used during the carpet turns, as would be expected. This is a much nicer result than the ‘fire and forget’ algorithm used before. Moreover, the carpet turns are much more positive now thanks to the more aggressive tread on the new tires – yay!