<?xml version="1.0" encoding="utf-8"?>
        <?xml-stylesheet type="text/css" href="http://bbot.org/blog/styles/feed.css"?>
<rss version="2.0"
 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
 xmlns:dc="http://purl.org/dc/elements/1.1/"
 xmlns:admin="http://webns.net/mvcb/"
 xmlns:atom="http://www.w3.org/2005/Atom"
>
<channel>
<title>the bblog</title>
<atom:link href="http://bbot.org/blog/rss.xml" rel="self" type="application/rss+xml" />
<link>http://bbot.org/blog</link>
<description>complaining, nerdery, errata</description>
<dc:language>en-us</dc:language>
<dc:creator>Samuel Bierwagen</dc:creator>
<dc:date>2025-12-20T23:32:15-08:00</dc:date>
<admin:generatorAgent rdf:resource="http://nanoblogger.sourceforge.net" />

<item>
<link>http://bbot.org/blog/archives/2025/12/20/bad_transcript_avatar_fire_and_ash_2025/</link>
<guid isPermaLink="true">http://bbot.org/blog/archives/2025/12/20/bad_transcript_avatar_fire_and_ash_2025/</guid>
<title>Bad Transcript: Avatar: Fire And Ash (2025)</title>
<dc:date>2025-12-20T23:07:47-08:00</dc:date>
<dc:creator>Samuel Bierwagen</dc:creator>
<dc:subject> important</dc:subject>
<description><![CDATA[<p>Hey, remember me? Remember how I used to steal the bit of <a href="https://www.the-editing-room.com/">The Editing Room</a> and write funny little <a href="https://bbot.org/writings/">bad transcripts</a> of movies, like <a href="http://bbot.org/badtranscript-avatar.html">Avatar (2009)?</a>

<p>Well, there's a new Avatar movie out, which means I was compelled to write the first bad transcript in, holy cow, 12 years? <a href="https://bbot.org/badtranscript-avatar3.html">Well, you can check it out here!</a>]]></description>

</item>
<item>
<link>http://bbot.org/blog/archives/2024/05/19/pocket_co2_meter_before_it_was_cool/</link>
<guid isPermaLink="true">http://bbot.org/blog/archives/2024/05/19/pocket_co2_meter_before_it_was_cool/</guid>
<title>pocket co2 meter before it was cool</title>
<dc:date>2024-05-19T00:38:13-08:00</dc:date>
<dc:creator>Samuel Bierwagen</dc:creator>
<dc:subject> nerdery</dc:subject>
<description><![CDATA[<p><a href="https://bbot.org/blog-images/meter1.JPG"><img src="https://bbot.org/blog-images/meter1-thumb.JPG"></a>

<p>This is my CO2 meter. There aren't many like it, since I built it entirely from parts.

<p><a href="https://bbot.org/blog-images/meter2.JPG"><img src="https://bbot.org/blog-images/meter2-thumb.JPG"></a>

<p>The controller is a <a href="https://www.seeedstudio.com/Seeeduino-Lotus-V1-1-ATMega328-Board-with-Grove-Interface.html">Seeeduino Lotus,</a> which is essentially an <a href="https://store-usa.arduino.cc/products/arduino-uno-rev3-smd">Arduino Uno</a> with <a href="https://wiki.seeedstudio.com/Grove_System/">Grove</a> sockets added to the PCB. It talks to the pair of 4 digit LED displays that display temp and CO2 concentration, a wifi module, and the star of the show, the <a href="https://www.seeedstudio.com/Grove-CO2-Temperature-Humidity-Sensor-SCD30-p-2911.html">Sensirion SCD30</a> non-dispersive infrared CO2 sensor. The box is a <a href="https://www.hammfg.com/electronics/small-case/plastic/1591t">Hammond 1591</a> in IR Red, purchased from <a href="https://vetco.net/">Vetco Electronics,</a> which as far as I can tell is the last remaining electronic components store in the Seattle area and probably all of Washington state.

<p>After getting the measurement and updating the displays, it uploads the results to a simple application server using thirty lines of Python that call <a href="https://flask.palletsprojects.com/en/3.0.x/">Flask.py</a> on bbot.org, which writes the results to a sqllite database and provides a database endpoint for a little web page that plots the results using <a href="https://plotly.com/">Plotly.js.</a>

<blockquote class="twitter-tweet"><p lang="en" dir="ltr">I like the little dips in co2 when the AC turns on and starts circulating air, reducing local concentration. Don&#39;t get the rapid drop once everyone leaves, though. Windows and doors closed, no plants... sorption into the hundreds of pounds of cardboard we have lying around? <a href="https://t.co/ydgbuDqT4p">pic.twitter.com/ydgbuDqT4p</a></p>&mdash; Samuel Bierwagen (@ceequof) <a href="https://twitter.com/ceequof/status/1154577934218223616?ref_src=twsrc%5Etfw">July 26, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<p>(That page is now offline. While it's a great demo, a CO2 meter turns out to be an incredibly effective occupancy sensor, and I didn't feel like publishing when I was and was not home for everyone to see.)

<p>It works, but not well. The wifi module is 2.4ghz-only, which is a heavily congested portion of the spectrum. Cramming all the parts into a small box with a few ventilation slots made it look great from a packaging standpoint, but made it terribly slow to respond to CO2 changes and resulted in substantial self-heating (The SCD30 gets its infrared light from a regular incandescent light bulb, which if you set the poll rate any higher than 10 minutes will warm up the whole sensor package) resulting in temperature readings reliably ten degrees above ambient. This resulted in version 2.0, detailed in the next blog post.]]></description>

</item>
<item>
<link>http://bbot.org/blog/archives/2021/01/02/sawbot/</link>
<guid isPermaLink="true">http://bbot.org/blog/archives/2021/01/02/sawbot/</guid>
<title>sawbot</title>
<dc:date>2021-01-02T00:24:00-08:00</dc:date>
<dc:creator>Samuel Bierwagen</dc:creator>
<dc:subject> nerdery</dc:subject>
<description><![CDATA[<p><i>(Programming note: this is a blog post I wrote for my previous employer, now defunct, preserved here. Some of the links won't work, since they point at a domain that no longer exists)</i>

<p><a href="https://bbot.org/blog-images/sawbot.jpg"><img src="https://bbot.org/blog-images/sawbot-thumb.jpg"></a>

<p>This year's Worlds demo is something practical: a robot that cuts shafts for you!

<p>A power hacksaw is testing the limits of the VEX hardware. Power tools need big motors and stiff, precise bearings: VEX 393 motors have a peak power of 35 watts, (At that power level they overheat and trip the internal <a href="https://en.wikipedia.org/wiki/Resettable_fuse">polyfuse</a> in seconds) and unlubricated plastic-on-steel bushings.

<p>The robot is basically just a hacksaw blade (clamped between two <a href="http://www.robotmesh.com/vex-c-channel-1x2x1x35-2-pack">1x2x1x35 c-channels</a>) mounted to a <a href="https://en.wikipedia.org/wiki/Crank_(mechanism)">crank.</a> The linear part uses a <a href="http://www.robotmesh.com/vex-linear-motion-kit">linear motion kit</a> with all four wide slide trucks mounted, and greased with Super Lube 21030. The rotational bit uses a <a href="http://www.robotmesh.com/vex-turntable-bearing-kit">turntable bearing</a> packed with grease. The saw is powered by four 393 motors ganged together with standard gears, which have less friction than the high-strength versions.

<p>In practice you push a shaft through a set of four shaft bar locks until it hits the stop, then tell it what length you want it cut to. The shaft feed moves it to the correct position, a pair of pneumatic cylinders push the blade into the shaft, (using the same air compressor we used in the <a href="http://www.robotmesh.com/blog/pongbot/">Pongbot</a> from last year) and it starts cutting. Once it cuts through, the saw sled trips a limit switch, which retracts the saw, and then the shaft feed ejects the cut shaft. And that's it!

<p>The software half of the robot is uncomplicated, basically just turning motors on and off in response to user input. But we get that user input from the LCD buttons, which means UI code!

<p>UI programming tends to be tricky because it requires a little design, and a lot of input validation. You have to make sure the user can't wiggle the system into a bad state, which requires thinking very hard about how inputs affect everything. Let's do a quick, easy prototype, that also happens to be totally broken.

<pre class="prettyprint">def UpdateLCD(length):
    lcd.write_top(length + " - " + (12.0 - length))
    lcd.write_bottom("UP   START  DOWN")

def ChooseLength():
    length = 1.875
    while True:
        if lcd.button_left():
            length += 0.125
            UpdateLCD(length)
        elif lcd.button_middle():
            return length
        elif lcd.button_right():
            length -= 0.125
            UpdateLCD(length)

def FeedShaft(length):
    ticks = (length - 1.875) * TICKS_PER_INCH
    while feed_encoder.value() < ticks:
        shaft_feed.run(50)
    shaft_feed.off()

FeedShaft(ChooseLength())</pre>

<p>This code works, in the sense that it will faithfully do exactly what the user commands. Unfortunately, you can keep pressing down forever until you get a negative number. <code>FeedShaft</code> won't do anything outrageously stupid if you give it a negative number<a id="ln1" href="#fn1"><sup>①</sup></a>, but it'll quite happily run the shaft feed sled into the far end if you give it a large positive number.

<p>We could fix the problem by changing <code>FeedShaft</code> to <a href="https://en.wikipedia.org/wiki/Clamping_(graphics)">clamp</a> input values to 1.875-6.0<a id="ln2" href="#fn2"><sup>②</sup></a>, but input validation should be done at the point where the user enters the input:

<pre class="prettyprint">while True:
    if lcd.button_left() and length <= 5.875:
        length += 0.125
        UpdateLCD(length)
    elif lcd.button_mid():
        return length
    elif lcd.button_right() and length >= 2:
        length -= 0.125
        UpdateLCD(length)</pre>

<p>This disables the up and down buttons when we're at 1.875 and 6. But it leaves the button labels on the LCD, a <a href="https://en.wikipedia.org/wiki/Affordance">false afforance</a> that tricks the user into thinking they can keep going up or down, so let's fix that too.

<pre class="prettyprint">def UpdateLCD(length):
    lcd.write_top(str(length) + " - " + str((12.0 - length)))
    if length == 1.875:
        lcd.write_bottom("UP   START      ")
    elif length == 6:
        lcd.write_bottom("     START  DOWN")
    else:
        lcd.write_bottom("UP   START  DOWN")</pre>

<p>Note the implemtation detail here: the length check in <code>ChooseLength</code> is <= 5.875, while the equivalent value in <code>UpdateLCD</code> is == 6. If it was 5.875 too, of course, then the up button would have its label removed one increment early, and be invisibly functional, an interesting example of an <a href="https://en.wikipedia.org/wiki/Off-by-one_error">off-by-one error.</a>

<p>A slightly different problem arises. On the first update, the LCD prints <code>1.875 - 10.125</code> but when you press up once, it prints <code>2.0 - 10.0</code>, which is a couple characters shorter. In practice, the numbers thrash around a lot while you hold up or down, which makes it hard to read them. If we pad short strings with zeros, then it will keep the decimal point in the same place while we scroll through sizes. Zero padding is easy enough:

<pre class="prettyprint">def ZeroPad(float):
    string = str(float)
    if len(string) == 3:
        return string + "00"
    elif len(string) == 4:
        return string + "0"
    else:
        return string</pre>

<p>This function takes a number, turns it into a string, then counts how many characters it contains, adding variable numbers of 0's to the end, and returning the concatenated string. (Embarassingly, my first try at this function had another off-by-one-ish bug: I looked at the string "1.875" and concluded it had <i>four</i> characters in it, instead of five, because I was counting the <i>digits.</i> I wrote my function as a string of <code>elif</code>s, and became immensely confused when it returned <code>None</code> instead of a zero-padded number. (A function returns <code>None</code> by default if you don't override it by passing something to the <code>return</code> keyword.)

<p>And that's basically it! <a href="http://www.robotmesh.com/project/4088">You can check out the full program here.</a>

<hr>

<p id="fn1">①: Code inside a <code>while</code> block will only execute if the statement after the keyword evaluates to <code>True</code>. <code>feed_encoder.value()</code> is always 0 at this point in the code, so an expression testing if it is <code>less than</code> a negative number will return <code>False</code>. <a href="#ln1">^</a>

<p id="fn2">②: "Well, what if you wanted a shaft that was 7 inches long?" The robot can only perform one cut, and it gives you both halves of the cut shaft. If you want a 7 inch shaft, then you select 5.0 on the LCD. (It displays the length of both segments on the screen-- that's what <code>(12.0 - length)</code> is doing in the <code>lcd.write_top</code> call. <a href="#ln2">^</a>]]></description>

</item>
<item>
<link>http://bbot.org/blog/archives/2021/01/01/pongbot_a_robot_that_plays_pong_against_itself/</link>
<guid isPermaLink="true">http://bbot.org/blog/archives/2021/01/01/pongbot_a_robot_that_plays_pong_against_itself/</guid>
<title>pongbot, a robot that plays pong against itself</title>
<dc:date>2021-01-01T00:24:00-08:00</dc:date>
<dc:creator>Samuel Bierwagen</dc:creator>
<dc:subject> nerdery</dc:subject>
<description><![CDATA[<p><i>(Programming note: this is a blog post I wrote for my previous employer, now defunct, preserved here. Some of the links won't work, since they point at a domain that no longer exists)</i>

<p>For <a href="http://www.robotevents.com/championship/">VEX Robotics World Championship 2015,</a> like last year, Robot Mesh needed some kind of booth demo.

<p>The requirements are open-ended, but challenging. It's got to be made of (mostly) VEX parts, be novel, and be <i>interesting.</i>

<p>We wanted to do something with pneumatics, and something with the <a href="http://www.cmucam.org/">Pixy camera,</a> since computer vision is both interesting and interestingly difficult. How about... a robot that plays pong against itself?

<p><video loop autoplay controls muted src="https://bbot.org/etc/pong.webm">

<p>Let's go over the hardware first.

<p>We used <a href="http://www.robotmesh.com/vex-c-channel-1x2x1x35-2-pack">2906s</a> for the frame because they're cheap and rigid. A robot brain with one fried <a href="http://en.wikipedia.org/wiki/H_bridge">H-bridge</a> pulled out of the junk pile was used for control, and a joystick with a broken partner port was used for joysticking. Even with heavy use of scrounged parts, final project cost came out to more than five hundred dollars.

<p>The first concern, once we settled on the basic idea, was that paddle traverse speed might not be fast enough. We weren't limited by competition rules, so the motors could have been overvolted, with the corresponding <a href="https://www.pololu.com/docs/0J11/5">dramatic reduction in motor lifespan,</a> but we wanted to see how far we could get with stock components, so the rack gearbox was built with a 36-tooth pinion gear, and the 393 motor was switched to the <a href="http://content.vexrobotics.com/docs/instructions/276-2177-inst-0712.pdf">high speed configuration.</a> Unloaded, this should have moved the paddle at <code>((1.5 in. diameter * pi) * (160 RPM / 60)) =</code> 12.56 inches per second. (31.91 cm/s)

<p>As the paddle had nonzero weight, it ended up moving slower than that, but in practice it ended up being good enough. If you were to build your own Pongbot, you might try using the <a href="http://www.robotmesh.com/vex-393-motor-turbo-gear-set-4-pack">turbo gears.</a>

<!-- <p><img src="{{media url="wysiwyg/pixy.jpg"}}" alt="" /> -->

<p>The Pixy is pretty neat. It does all the heavy processing onboard, then just outputs screen-space coordinates. (It can do color blob detection of 7 different hues, for up to 1000 different objects, though in practice you'd probably run out of sensor resolution and channel bandwidth before then.)  It communicates with the Cortex by serial over the UART port.

<p>Like all computer vision sensors, it is exquisitely sensitive to lighting conditions. If the scene is partially lit by natural light, then a correctly calibrated sensor in the morning will give erratic and strange results in the afternoon. It's handy to keep <a href="http://www.cmucam.org/projects/cmucam5/wiki/Teach_Pixy_an_object">PixyMon</a> open so you can check to make sure that the Pixy actually sees what you think it sees.

<p>Two pitfalls when working with the Pixy: the camera is focused by threading the lens in and out of the camera housing. This thread is fairly loose: if there's any mechanical vibration at all, the lens will gradually drift out of focus. (Oddly enough, this makes signature recognition undependable, which you wouldn't expect) Also, the camera can either output live preview over USB, <i>or</i> data over the UART line. If you're in live preview mode, the camera won't output any data, and your robot will mysteriously stop working.

<p>The robot went together quite quickly, and after about, oh, ten seconds of system testing, it became obvious that the pneumatics were going to need to be directly connected to an air compressor, otherwise we would have to pump it up by hand every five minutes.

<p>It's tough to find air compressors that are both cheap and quiet. Eventually we discovered that the magic keyword here is "airbrush compressor". Any airbrush compressor would work, but the one we used was the <a href="http://www.tcpglobal.com/ABD-TC-20_2.html">TCP Global TC-20</a>. It only had a maximum pressure of 57 psi, but that was fine, since we were running the cylinders almost completely unloaded, and I was worried about damaging the seals by <a href="http://en.wikipedia.org/wiki/Dry_fire">dry firing</a> them. (This was mitigated by only opening the solenoid very briefly, see below.)

<!-- <p><img src="{{media url="wysiwyg/tank.jpg"}}" alt="" /> -->

<p>A problem arose. All our air tool fittings used big chunky high-flow 1/4 inch NPT threads, while the reservoir was drilled and tapped for 1/8 inch NPT. An adapter of that size is kind of a rare beast, with the result that it was unlikely a general hardware store would have them, and we didn't want to wait around another two business days for a little piece of brass to be shipped to us from a warehouse in Ohio. 

<p>Where could we possibly find an obscure pneumatics adapter in Seattle?

<p>Why, at <a href="http://www.westernfluidcomp.com/">Western Fluid Components,</a> just down the street from us, who happened to have a box of them just sitting on the shelf. If you're ever in Kirkland, and need some fluid components, they're your guys.</a>

<p>Late in the project we chopped up a <a href="http://www.robotmesh.com/vex-battery-extension-cable">battery extension cable</a> to run the Pongbot off of a bench power supply, as battery lifespan had become a problem. Unfortunately the PSU had been sized by the time-honored "guessing" method, ("How much current could two little motors draw, anyway?") and was only rated to 3 amps. Well, it turns out that a single motor tries to draw 4.8 amps when stalled, which causes the PSU output voltage to droop alarmingly. This doesn't seem to harm anything, since the Cortex was always tethered to a laptop, making it immune to CPU brownouts. (Low voltage <a href="http://www.atmel.com/Images/doc1051.pdf">can do weird things to computers.</a>)

<!-- <p><img src="{{media url="wysiwyg/corner.jpg"}}" alt="" /> -->

<p>Rubber bands were strung across the corners, to keep the ball from getting stuck where the paddle couldn't get at it. (A literal <a href="http://en.wikipedia.org/wiki/Corner_case">corner case.</a>) Another problem discovered during testing that, due to the square edge on the paddle, it could end up entraining a ball directly next to it, so we chopped up a <a href="http://www.robotmesh.com/vex-bar-1x25?___SID=U">1x25 steel bar</a> and bent the ends in a vice so that the ball would be nudged away from the paddle during travel.

<p>Let's talk about the software. (Code examples edited for clarity)

<pre class="prettyprint lang-py">paddle1_kick = vex.DigitalOutput(8)

paddle1_kick.on()</pre>

<p>This tells the interpreter that port 8 should be defined as a <code>DigitalOutput</code>, and gives it the name <code>paddle1_kick</code>. (To be precise, we instantiate the <code>vex.DigitalOutput</code> class with the object name <code>paddle1_kick</code>.) To turn it on, we do <code>paddle1_kick.on()</code>, (Calling the <code>on</code> method of <code>vex.DigitalOutput</code>.)

<pre class="prettyprint lang-py">def FirePaddle(device):
    device.on()
    sys.sleep(.05)  # Avoid dry firing by only opening the solenoid for a brief time<a href="#fn1" id="ln1">[1]</a>
    device.off()</pre>

<p>One problem is that both sides of the Pongbot have a paddle, which means I need to specify the delay in two places. Copy and paste code is a pain to work with, since every time you want to make one global change, you have to go in and edit the code in multiple places. This results in a surprisingly large number of bugs, <a href="https://medium.com/@Code_Analysis/the-last-line-effect-7b1cb7f0d2a1">even in real, professional software.</a> In the programming industry, the injunction against repeating yourself is called <a href="http://en.wikipedia.org/wiki/Don%27t_repeat_yourself">Don't Repeat Yourself.</a>

<p>To avoid Repeating Myself, I define the name of a new function using <code>def</code>, name it <code>FirePaddle</code>, and specify that it takes one argument, <code>device</code>. (Don't forget to end the line with a colon! The error it returns isn't very helpful for figuring out what you did wrong.)

<p>There is a slight amount of cleverness here: you can pass in the name of a function as an argument to another function. This means you can pass in <code>paddle1_kick</code>:

<pre class="prettyprint lang-py">FirePaddle(paddle1_kick)</pre>

<p>Causing FirePaddle to execute:

<pre class="prettyprint lang-py">paddle1_kick.on()
sys.sleep(.05)
paddle1_kick.off()</pre>

<p>Isn't that neat?

<p>Let's take a look at something a little more complicated.

<pre class="prettyprint lang-py">pixy = vex.Serial(vex.SerialParam.PORT_UART1, 38400)
FRAME_SYNC = (0x55, 0xaa, 0x55, 0xaa)

while True:
    if pixy.read_byte() == FRAME_SYNC[0]:
        if pixy.read_byte() == FRAME_SYNC[1]:
            if pixy.read_byte() == FRAME_SYNC[2]:
                if pixy.read_byte() == FRAME_SYNC[3]:
                    rawdata = pixy.read_bytes_list(40)
                    break</pre>

<p>(During testing, we found that 19200 bits/s was not fast enough for the Pixy to transmit three blocks per frame, but at 56600 bits/s the robot brain wasn't fast enough to keep up with processing, and the buffer filled up and tended to overwrite frames while they were being read off, causing checksum failures. The happy medium is 38400 bits/s.)

<p>This stack of <code>if</code> statements is designed to search through an incoming stream of data for the start of a new frame. The Pixy sync word is 0xaa55, sent least significant byte first.<a href="#fn2" id="ln2">[2]</a> It transmits one sync word between each block (each object in the scene that it has recognized) and two sync words at the start of each new video frame.

<p>The while loop runs forever, reading one byte at a time off the serial port. If the byte it reads is <code> FRAME_SYNC[0]</code>, 0x55, then it reads off another byte and checks to see if it's 0xaa. It proceeds in this fashion until it finds a complete frame sync, at which point it reads off 40 bytes, (Three object blocks) then <code>break</code>s out of the infinite loop.

<pre class="prettyprint lang-py">for x in (0, 14, 28):
    checksum = rawdata[x+0] | (rawdata[x+1] << 8)
    signature = rawdata[x+2] | (rawdata[x+3] << 8)
    x_pos = rawdata[x+4] | (rawdata[x+5] << 8)
    y_pos = rawdata[x+6] | (rawdata[x+7] << 8)
    width = rawdata[x+8] | (rawdata[x+9] << 8)
    height = rawdata[x+10] | (rawdata[x+11] << 8)</pre>

<p>This one's a little arcane.

<p>Each variable inside a block is transmitted as a 16-bit word, but are read off the wire as 8-bit bytes, so we have to reassemble them before packing them into variables. The LSB byte can be handled normally, but the MSB byte is left-shifted 8 bits, then <a href="http://en.wikipedia.org/wiki/Bitwise_operation">bitwise</a> (one bit at a time) ORed with the LSB. A fabulous ASCII art diagram:

<pre class="prettyprint lang-html">Let's say the checksum was decimal 258, which would be hex 0x0102. 
That's transmitted as

1   0x02 00000010
2   0x01 00000001

Left shift byte 2 by 8 bits, then OR them

1   0x02 00000000 00000010
|
2   0x10 00000001 00000000
=
1 0x0102 00000001 00000010</pre>

<p>There!

<p>Now that we have the coordinates of where the ball is right now, we can make a stab at predicting where it <i>will</i> be.

<pre class="prettyprint lang-py">class PredictLocation():
    def __init__(self):
        self.older = 0.0
        self.old = 0.0
        self.new = 0.0
    def __call__(self, location):
        self.older = self.old
        self.old = self.new
        self.new = (((self.old - self.older) + self.old) + location + location) / 3
        self.predicted = ((self.new - self.old) + self.new)
        return self.predicted

predicted_loc_ball = PredictLocation()

predicted_loc_ball(y_pos)</pre>

<p>Here we define a class, instantiate it as <code>predicted_loc_ball</code>, then call it, passing <code>y_pos</code>, which we got from the camera earlier.

<pre class="prettyprint lang-py">def __init__(self):
    self.older = 0.0
    self.old = 0.0
    self.new = 0.0</pre>

<p>This defines a special <i>class method</i>, like the <code>on</code> method of the <code>vex.DigitalOutput</code> class earlier. We give it the <a href="https://docs.python.org/2.5/ref/specialnames.html#specialnames">special method name</a> <code>__init__</code>, which is executed when the class is instantiated. (A class <a href="http://en.wikipedia.org/wiki/Constructor_%28object-oriented_programming%29">constructor.</a>) We pass another magic word, <code>self</code>, which is replaced with the variable name you chose when you instantiated the class, here <code>predicted_loc_ball</code>.<a href="#fn3" id="ln3">[3]</a>

<p>We initialize the variables with 0.0 to force them to use the <a href="http://en.wikipedia.org/wiki/Floating_point">floating point</a> data type. Python will return an integer as the result of dividing two integers, which on the one hand makes logical sense, but on the other hand means that 3 / 2 will return 1, while 3.0 / 2.0 will return 1.5.

<p><code>__call__</code> is another special method name that is called by default without having to specify a method. As for what it actually <i>does:</i>

<pre class="prettyprint lang-py">self.older = self.old
self.old = self.new
self.new = (((self.old - self.older) + self.old) + location + location) / 3
self.predicted = ((self.new - self.old) + self.new)
return self.predicted</pre>

<p>All camera sensors have a fair amount of jitter, so some filtering is required of the raw data. The standard tool used for this in computer vision is the <a href="http://en.wikipedia.org/wiki/Kalman_filter">Kalman filter,</a> (It's like a PID algorithim, only inside out) but all the code found from a lazy Google search required the <a href="http://www.numpy.org/">NumPy</a> library, as Python doesn't have a native array type. Under time pressure, I just threw up my hands and averaged the latest reading with the previous reading. This worked well enough in practice, but introduced half a frame of latency, 10ms, which wasn't super ideal. (The Pixy outtputs 50 frames per second, 50hz. <code>1000ms / 50hz =</code> 20 ms.)

<p>This code makes an effort at predicting the future location of the ball. When called, it takes the difference between the last two readings and uses it to guess at the current location, then does a weighted average of the guess with the sensor reading. It then takes that filtered result and guesses at the future location of the ball.

<p>The paddle control code is greatly simplified by cheating. First, we define a generic <code>Paddle</code> class, since both sides use the same control code, and we don't want to repeat ourselves:

<pre class="prettyprint lang-py">class Paddle():
    def __init__(self, limit_right, limit_left, motor, kick):
        self.limit_right = limit_right
        self.limit_left = limit_left
        self.motor = motor
        self.kick = kick
        self.filtered_loc_paddle = OutputFilter()

paddle1 = Paddle(limit_right_1, limit_left_1, paddle_1, paddle_kick_1)
paddle2 = Paddle(limit_right_2, limit_left_2, paddle_2, paddle_kick_2)</pre>

<p>We pass in the objects of the motors, limit switches, and pneumatic solenoids, (We define the names and port numbers in the interface monitor, which makes troubleshooting a little easier) then instantiate an <code>OutputFilter</code> class. (Like <code>PredictLocation</code>, but it doesn't forward predict. If we forward predicted both the ball and the paddle, then the system would oscillate pretty badly.)

<pre class="prettyprint lang-py">def fire(self):
    self.kick.on()
    sys.sleep(.05)
    self.kick.off()
def __call__(self, predicted_loc_ball, loc_ball_x loc_paddle):
    a = self.filtered_loc_paddle(loc_paddle[1])
    if predicted_loc_ball < (a - 5) and not self.limit_right.is_on():
        self.motor.run(((predicted_loc_ball - a) * 2) - 20)
    elif predicted_loc_ball > (a + 5) and not self.limit_left.is_on():
        self.motor.run(((predicted_loc_ball - a) * 2) + 20)
    else:
        self.motor.off()
    if (abs(loc_paddle[0] - loc_ball_x)) < 10 and (abs(a - predicted_loc_ball)) < 10:
        self.fire()</pre>

<p>Revision 1 only used the Pixy for tracking the ball, and IMEs<a href="#fn4" id="ln4">[4]</a> for tracking the paddles, which had the advantage of much greater resolution and stability, and the disadvantage of having to convert between screen-space coordinates and encoder ticks, as well as a fiddly and tedious calibration procedure every time the camera shifted in the clamp mount. (Though, now that I think of it, calibration could have been an automated step on startup, if I had left the colored tags on the motors...)

<p>Eventually I ran into a showstopping problem with I2C resets clearing the IME tick counter, and just punted and went with Pixy tracking of all three objects. This has the advantage of making the control code incredibly concise: it just tries to move the paddle until it lines up with the ball.

<p>There is a deadband of 10 pixels around the ball, to prevent excessive <a href="http://en.wikipedia.org/wiki/Hunting_oscillation">hunting.</a> (The <code>> (filtered_loc_paddle1 + 5) < (filtered_loc_paddle1 - 5)</code> stuff)

<p>The paddle motor is directly throttled with the offset from the ball, with a minimum power of 20, and a multiplier of 2 to get it to ramp up to 100 more quickly. Values higher than 100 are just clamped to 100 by the <code>Motor.run</code> API. The solenoid fires when the ball enters a 20x20 pixel box around the paddle.

<p>The <code>__call__</code> method takes the x and y coordinates of both the ball and the paddle. The code here isn't as clear or concise as it could be, since to keep loop execution time down I'm only doing forward prediction on the ball's y position, and output filtering for the paddle y position.

<p>And that's pretty much it! <a href="http://www.robotmesh.com/project/948">The code is up on Robot Mesh Python,</a> so you can run it yourself. Check it out!

<!-- <p><img src="{{media url="wysiwyg/pong.gif"}}" alt="" /> -->

<hr>
<p id="fn1"><a href="#ln1">1: ^</a> It is <i>tremendously</i> tempting to rephrase what you're doing in a code comment, especially when the API requires you do something a little awkwardly. On my first pass, I wrote this comment as:

<pre class="prettyprint">sys.sleep(.05)  # Avoid dry firing by only opening the solenoid for 50ms</pre>

<p>Ah, but Don't Repeat Yourself! I had to change the value passed to <code>sys.sleep()</code> several times during testing, which meant I had to keep changing the comment in lockstep. Eventually I realized my error, and made the comment more generic. Never put a number from your program logic in a comment!

<p id="fn2"><a href="#ln2">2: ^</a> This is why we define FRAME_SYNC as (0x55, 0xaa, ...) not (0xaa, 0x55, ...). Keeping track of endianess is one of the joys of programming against the <a href="http://en.wikipedia.org/wiki/Bare_metal">bare metal.</a>

<p id="fn3"><a href="#ln3">3: ^</a> You can examine the internal variables of a class using the same syntax as a method call, of course, since a method is just a variable, like any other. <code>print predicted_loc_ball.old</code> will return <code>0.0</code>. It is considered bad form to do so outside of debugging, however, and if your code depends on fiddling with a class's internal variables, instead of accessing them from its return value, <a href="http://en.wikipedia.org/wiki/Separation_of_concerns">then you are almost certainly committing a design error.</a>

<p id="fn4"><a href="#ln4">4: ^</a> VEX product 276-1321 is actually labeled and titled "Integrated Encoder Module", but the wiki and everyone I've ever talked to have called them IMEs, Integrated Motor Encoders. Even the Inventor's Guide pdf is confused, calling it an encoder module and a motor encoder at different points.
</article>]]></description>

</item>
<item>
<link>http://bbot.org/blog/archives/2018/05/01/google_please_stop_breaking_chromedriver/</link>
<guid isPermaLink="true">http://bbot.org/blog/archives/2018/05/01/google_please_stop_breaking_chromedriver/</guid>
<title>google please stop breaking chromedriver</title>
<dc:date>2018-05-01T23:14:15-08:00</dc:date>
<dc:creator>Samuel Bierwagen</dc:creator>
<dc:subject> Work</dc:subject>
<description><![CDATA[<p>So <a href="https://www.robotmesh.com/blog">at work</a> one of the things I do is write regression tests against <a href="https://www.robotmesh.com/studio">our webapp.</a> One of the many, many ways to do automated testing is with the <a href="https://www.seleniumhq.org/">Selenium browser automation framework.</a> Selenium needs a way to hook into your browser of choice: for chrome, this is <a href="https://sites.google.com/a/chromium.org/chromedriver/">Chromedriver.</a> So far, so good.</p>

<p>My tests fail a lot. This is the point of tests, of course, you make a change, your tests break, you fix them up. (It would be nice to plumb them into a CI pipeline, but since they actuate <a href="/blog-images/cortex-testbot.jpeg">actual, physical machines,</a> that presents certain practical problems.) Ideally, my tests would only break when I break them, but frustratingly often, they start failing because the tooling rotted away when I wasn't looking. In particular, Chromedriver.</p>

<p>In descending order, these would be my preferred outcomes when Chromedriver needs to be updated:</p>

<p>1.) Actually, it would be cool if routine Chrome upgrades didn't screw up Chromedriver. Wouldn't that be great? Each Chromedriver release supports several different versions of Chrome, so obviously it's not an <a href="https://en.wikipedia.org/wiki/Application_binary_interface">ABI</a> thing, so why the short shelf life?</p>

<p>2.) If Chromedriver is stale, how about just updating yourself automatically, rather than making a human do it for you? You're a big program, you can open network sockets yourself. I see there's an <a href="https://www.npmjs.com/package/chromedriver">npm module for Chromedriver,</a> but I'm pretty sure I don't want to be running an npm install on a Windows QA box just to keep a Chrome module up to date.</p>

<p>3.) If Chromedriver is stale, how about you <i>tell me that?</i> An error message along the lines of, maybe, "Chromedriver 2.36 does not support Chrome 67, please download the latest copy of Chromedriver." Would that be so awful?</p>

<p>No, instead Chromedriver picks the last, most exciting option:</p>

<p>4.) Random inscrutable error messages.</p>

<p>Pop quiz, hotshot. When Chromedriver throws the exception <code>selenium.common.exceptions.WebDriverException: Message: unknown error: call function result missing 'value'</code>, what does that mean?

<p><a href="https://stackoverflow.com/questions/49162667/unknown-error-call-function-result-missing-value-for-selenium-send-keys-even">"Update to Chromedriver 2.36"</a></p>

<p>How about <code>SessionNotCreatedError: session not created exception from timeout: Timed out receiving message from renderer: 600.000</code>?

<p><a href="https://stackoverflow.com/questions/33155562/selenium-webdriver-sessionnotcreatederror">Update Chromedriver.</a>

<p><code>org.openqa.selenium.WebDriverException: unknown error: cannot determine loading status from timeout: Timed out receiving message from renderer</code>?

<p><a href="https://stackoverflow.com/questions/34926866/selenium-chromedriver-timed-out-receiving-message-from-renderer-exception">You get the idea.</a> Chromedriver is addicted to throwing <i>wrong, completely spurious exceptions</i> as a matter of routine. According to their site, any given version of Chromedriver is only good <a href="http://chromedriver.chromium.org/downloads">for three releases.</a> That means Chromedriver will go stale and start returning insane, wrong errors after just 4.5 months. I'm not a fan of that behavior! Wish it didn't do that!]]></description>

</item>
<item>
<link>http://bbot.org/blog/archives/2017/12/31/commuter_bike_920_miles_later/</link>
<guid isPermaLink="true">http://bbot.org/blog/archives/2017/12/31/commuter_bike_920_miles_later/</guid>
<title>commuter bike, 920 miles later</title>
<dc:date>2017-12-31T23:18:40-08:00</dc:date>
<dc:creator>Samuel Bierwagen</dc:creator>
<dc:subject> Etc</dc:subject>
<description><![CDATA[<p><a href="https://bbot.org/blog/archives/2016/08/14/putting_together_a_commuter_bike/">Sixteen months ago</a> I said the following:</p>

<blockquote>I glumly regarded my future stretching out ahead of me, a future of cleaning my bike's chain once a week, and decided, screw it, I'm getting a new bike.</blockquote>

<p>I am now cleaning the chain of that replacement bike once a week.</p>

<p>Let me tell you some things I wish I knew about internally geared hubs before I bought my bike.</p>

<p><b>1.)</b> Chains stretch as they wear. You generally throw them away when they hit 0.75%. (0.075" of stretch across 10" of chain) A go/no-go gauge for chain wear, like the <a href="https://www.parktool.com/product/chain-wear-indicator-cc-3-2">Park CC-3.2,</a> is cheap and handy. (You can measure chain with a ruler or tape measure, but it's a hassle.)</p>

<p>Chain stretch is largely invisible on a dérailleur bike, since it has an automatic chain tensioner, (The little arm that swings down) but on a fixed-chainpath bike, like a single speed or internally geared one, you have to do it manually, by sliding the wheel backwards in the rear dropouts. This requires two wrenches, and ten minutes of time. This <b>must</b> be done weekly, or biweekly. If you let enough slack build up in the chain, then it'll bounce off the rear cog when you hit a bump, and you get to experience the joy of handling bike chain with your bare hands. (Unless you want to get chain grunge on your good winter gloves) Chains generally wait until the sun goes down and it starts raining to jump off a cog, as well.</p>

<p><b>2.)</b> IGHs are filled with oil, which, <a href="http://www.koga-signature.com/docs/Maintenance-Manual.pdf">per Shimano,</a> have to be serviced after the first 1000 km, and then every 2 years or 5000 km thereafter. (My Nexus 3 started making noise at 600 miles, right on schedule) A bike shop will charge you $40-50 to do it. Want to do it yourself? The OEM oil costs an eye-popping <a href="http://www.jensonusa.com/Shimano-Internal-Gear-Hub-Oil/">$60 per litre.</a> <a href="https://www.youtube.com/watch?v=tbKs4VFzek0&t=1s">This guy</a> has been using dollar store ATF on his, and it seems to be working fine, but this is not the maintenance-free dream I was promised!</p>

<p><b>3.)</b> This is a <a href="https://www.bikeforums.net/singlespeed-fixed-gear/1038999-questions-about-single-speed-chains-do-they-wear-faster.html">topic of hot debate,</a> but some allege that fixed-chainpath bikes <a href="http://www.63xc.com/gregg/gregchai.htm">eat chains more often.</a> But I've got a Hebie Chainglider on there, which should protect the chain from dirt, and make it last longer, right?</p>

<p>Well, no. The factory original chain lasted just 700 miles before hitting 0.75% on the gauge. I threw the old chain away, along with the chainglider. As far as I could tell, it didn't do anything at all.</p>

<p>So now I've got my bike up on the stand every other week, to tension the chain, and running it through the chain cleaner now that it hangs out unprotected. My original reason to buy the bike is now completely obliterated. Oh well.</p>

<blockquote class="twitter-tweet" data-conversation="none" data-lang="en"><p lang="en" dir="ltr">While I had the front wheel off, I checked on the drum brake, which had been sticking. Plenty of brake pad left, after a thousand miles of wear or so <a href="https://t.co/gEwmv5s3Vg">pic.twitter.com/gEwmv5s3Vg</a></p>&mdash; Samuel Bierwagen (@ceequof) <a href="https://twitter.com/ceequof/status/939580197485010944?ref_src=twsrc%5Etfw">December 9, 2017</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<p>It strikes me that there is a lesson to be learned here about technological systems. As a computer guy, I'm used to slam-dunk upgrades-- this year's Intel chip will be authentically better in every single way than last year's chip, a 2 TB hard drive is better than a 1 TB drive, etc.</p>

<p>This is not true of all things! AC induction motors, for example, basically haven't changed in a century. Mankind quickly figured out the most efficient way to convert electrical power into motion using iron and copper, and there progress stopped. There's no AC motor today that's "better" than a motor made in 1910, no way to get more power without needing more coils, or more voltage. You can shave tenths of a percent off of 98% efficient, but there's no route to 120% efficient.</p>

<p>You see echoes of this all the time in a mature technological system like the bicycle. If there's a better way to do something, it was done decades ago, now all that's left to us in 2017 is choices between tradeoffs. Drum brakes are weatherproof in a way that rim brakes can't be, but heat rejection is terrible, (All the hot parts are sealed inside an aluminum can-- the price paid for being weatherproof!) braking counterforce is applied unevenly to one side of the fork with a single reaction arm, it weighs more, and in the case of my 70mm drum brake, you just plain can't brake as hard.</p>

<p>Disc brakes? You have to use hydraulic force multiplication in order to get the same braking force as rim brakes with a shorter moment arm. Force multiplication means length of travel is <i>divided,</i> so the pads have to be thinner, making them wear out even faster than rim brakes. The rotor has less mass than a wheel rim, so it heats up faster. <a href="http://c1qfxugcgy0.tumblr.com/post/165192049892/heres-one-of-my-favorite-illustrations-of">All is compromise, trading one good for another good.</a></p>

<p>Not to say that the Windsor's totally worthless now. The dynamo hub and lights still work way better than the assortment of battery powered lights I had on the Trek. It has a kickstand, which is unforgivably omitted from all sports bikes for weight reasons. After falling off the bike thanks to ice in the parking lot at work, I swapped the 32mm Panaracer touring tire for an insanely beefy 42-622 <a href="https://www.continental-tires.com/bicycle/tyres/city-trekking-tyres/top-contact-winter2-premium">Continental Top Contact Winter II</a> tire on the front wheel. It mounted up just fine on my 20x622 rim, but has almost no clearance between it and the fender-- if I was going to do it again, I'd get the 37mm version.</p>

<p>You wouldn't expect there to be much of a different between winter tires and all-seasons, since they're both... rubber... but winter rubber compounds are actually <a href="https://jalopnik.com/heres-why-you-need-winter-tires-as-shown-with-a-tricyc-1754259579">quite a lot better on ice.</a> Plus, the contact patch on the 42 mm tire is a hell of a lot bigger, which should help some just by itself.</p>
 
<p>So that's 2017. Check back next year, when I will have talked myself into buying yet another bike for one reason or another.</p>]]></description>

</item>
<item>
<link>http://bbot.org/blog/archives/2016/12/21/commuter_bike_360_miles_later/</link>
<guid isPermaLink="true">http://bbot.org/blog/archives/2016/12/21/commuter_bike_360_miles_later/</guid>
<title>commuter bike, 360 miles later</title>
<dc:date>2016-12-21T22:37:14-08:00</dc:date>
<dc:creator>Samuel Bierwagen</dc:creator>
<dc:subject> Engineering</dc:subject>
<description><![CDATA[<p>So I've had the Oxford for a couple months now, and a few events have occured.</p>

<p>As promised, the drum brake has firmed up, and now feels like a normal brake, rather than the "ice cube on teflon" feel it had when new. The dynamo lights have worked perfectly, no problems at all.</p>

<p>In <a href="https://bbot.org/blog/archives/2016/08/14/putting_together_a_commuter_bike/">the previous post</a> I regarded the shock front fork on the Trek with some suspicion, even though it had given me no trouble over thousands of miles of riding, purely because it was a moving part, and moving parts break. This was because, after riding the bike for ten years, I honestly thought that the shock fork didn't <i>do</i> much.</p>

<p>This illusion was dispelled the very first time I tried hopping off a curb on the Oxford. It hurt! Shock absorbers really do absorb shock! It <i>also</i> doesn't help that the Oxford wears narrow 32C tires. Less rolling resistance, sure, but the point of a commuter bike isn't to go fast, it's to have minimum maintenance cost, along with maximum comfort. (It's also no small consideration that wider tires have better traction, since a commuter bike spends more time braking hard and taking sharp turns than a racing/touring bike.) If I was building another bike, I'd certainly get wider tires, and maybe think about a shock fork.</p>

<p>While we're on that subject, the stock Kenda tires have the puncture resistance of wet tissue paper. In the first month of owning it I got two flats in the rear wheel, one of them due to a glass splinter that was, at most, 2 millimetres long. Replacing the rear tire is especially annoying, since you have to disconnect the hub shifter, take off the chainguard, etc etc.</p>

<p>I switched to <a href="http://www.thebikesmiths.com/panaracer-urban-max-700-x-32c-tire-210000004268/dp/4268">Panaracer Urban Max</a> tires, and haven't had a flat since.</p>

<p><a href="https://bbot.org/blog-images/broken-fender-bracket.jpeg"><img src="https://bbot.org/blog-images/broken-fender-bracket-thumb.jpeg"></a></p>

<p>And then the damn rear fender bracket broke. Without it the rear fender catches on the tire every time you hit a bump, producing a surprisingly loud and alarming clattering noise. No replacement render bracket available from Windsor, no replacement rear fender assembly, either.</p>

<p>Well <i>fine.</i> I'll just make my own!</p>

<p><img src="https://bbot.org/blog-images/layout.png"></p>

<p>Easy!</p>

<p>The first google hit for "lasercut stainless steel" is Lasergist. Their site is excellent, and the prices are cheap. Something they <i>don't</i> go out of their way to tell you is that they're based in Greece, and transit time for international letter mail is something like two weeks. If you're impatiently waiting for a part to arrive so you can fix your primary commuter vehicle, then this is something you might want to know ahead of time!</p>

<p>Once the damn thing got here, I had to put a 90 degree bend in it. You might ask, can you put a bend in 2mm stainless steel with just a vice, a hammer, and some locking pliars?
  
<p><a href="https://bbot.org/blog-images/new-bracket.jpeg"><img src="https://bbot.org/blog-images/new-bracket-thumb.jpeg"></a></p>

<p>Well, eventually.</p>

<p>Now I needed to drill two holes in it so I could rivet it to the fender. Somehow I had forgotten that 302 stainless is <a href="https://en.wikipedia.org/wiki/Machinability#Stainless_steel">legendarily annoying to machine.</a> It's a little harder than mild steel, but it's worst feature is it's very bad at conducting heat. This means when you try to drill through it, the metal right next to the drill bit gets very hot, and heat-hardens. To drill stainless steel you want to use very low bit speeds, and cool the workpiece, either with drilling fluid or water. Smart guy right here did the pilot holes dry, and at the highest bit speed my drill could do. As a reward, I got to spend a full 30 minutes drilling two 5mm holes.</p>

<p><a href="https://bbot.org/blog-images/fixed-fender.jpeg"><img src="https://bbot.org/blog-images/fixed-fender-thumb.jpeg"></a></p>

<p>But now it's fixed, and it's real unlikely it'll break at the same spot.</p>]]></description>

</item>
<item>
<link>http://bbot.org/blog/archives/2016/11/22/fun_and_games_with_3d_printers/</link>
<guid isPermaLink="true">http://bbot.org/blog/archives/2016/11/22/fun_and_games_with_3d_printers/</guid>
<title>fun and games with 3d printers</title>
<dc:date>2016-11-22T23:41:00-08:00</dc:date>
<dc:creator>Samuel Bierwagen</dc:creator>
<dc:subject> Engineering</dc:subject>
<description><![CDATA[<p><a href="https://bbot.org/blog-images/handle.jpeg"><img src="https://bbot.org/blog-images/handle-thumb.jpeg"></a></p>

<p>This is my handle for an <a href="https://en.wikipedia.org/wiki/Anderson_Powerpole">Anderson Powerpole</a> connector. There are many like it, but this one is more expensive than most.</p>

<p><a href="http://www.robotmesh.com/">We</a> recently bought a used forklift, which, like nearly all electric forklifts, has a big lead acid battery connected to the forklift by a beefy 350 amp DC connector. These big connectors are not trivial to insert and remove, and since you generally want to charge a forklift nightly, I found myself spending a lot of time wrestling with it. Anderson makes a connector handle for just this reason, but Grainger wanted <a href="https://www.grainger.com/product/ANDERSON-POWER-PRODUCTS-Handle-3BY29">$20 plus $10 shipping.</a> Outrageous! Are we not men? Do we not have 3D printers with which to produce trivial plastic parts like this? <a href="https://twitter.com/ceequof/status/762143886395400193">I slapped together a simple model</a> <a href="https://bbot.org/etc/handle.slvs">(source)</a> using the parametric CAD program of choice for cheap bastards, <a href="http://solvespace.com/index.pl">Solvespace,</a> and took a quick jaunt downtown to get it printed at <a href="http://www.metrixcreatespace.com/">Metrix Create Space.</a></p>

<p><blockquote class="twitter-tweet" data-conversation="none" data-lang="en"><p lang="en" dir="ltr">Printing... <a href="https://t.co/rd6U2jheBU">pic.twitter.com/rd6U2jheBU</a></p>&mdash; Samuel Bierwagen (@ceequof) <a href="https://twitter.com/ceequof/status/762412996312244224">August 7, 2016</a></blockquote>
  <script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script></p>

<p>This handle was conveniently easy to 3D model, but it's not actually a great candidate to 3D <i>print,</i> since it's so big-- about 200 cubic centimeters. Even cut in half to reduce machine time, it cost me $40 to print.</p>

<p>Like the last one, from a cost-cutting point of view, this project wasn't a success.</p>

<p>I was kinda worried that the half-height version of the handle would break off as soon as I used it, so I threw a fiberglass overwrap on it using a package of <a href="https://fiberfixnow.com/">Fiberfix</a> that was going to expire soon anyway. And it works! Qualified victory!]]></description>

</item>
<item>
<link>http://bbot.org/blog/archives/2016/08/14/putting_together_a_commuter_bike/</link>
<guid isPermaLink="true">http://bbot.org/blog/archives/2016/08/14/putting_together_a_commuter_bike/</guid>
<title>putting together a commuter bike</title>
<dc:date>2016-08-14T23:12:19-08:00</dc:date>
<dc:creator>Samuel Bierwagen</dc:creator>
<dc:subject> Etc</dc:subject>
<description><![CDATA[<p><a href="https://bbot.org/blog-images/windsor-oxford.jpeg"><img src="https://bbot.org/blog-images/windsor-oxford-thumb.jpeg"></a></p>

<p>This is my bike, a <a href="http://www.bikesdirect.com/products/windsor/oxford.htm">Windsor Oxford,</a><sup><a id="ln1" href="#fn1">[1]</a></sup> in Tangerine, serial number L150101593. There are several hundred thousand like it, but this one is mine.</p>

<p>I've been commuting on a Trek 6000 <a href="https://bbot.org/blog/archives/2006/06/02/bicycles/">for the last 10 years or so.</a><sup><a id="ln2" href="#fn2">[2]</a></sup> The Trek is great, but it doesn't really want to be a commuter bike. There's no braze-ons for fenders, so I use clamp on fenders, which pretty reliably slip out and get eaten by the front tire. There's no kickstand, of course. It's got a shock front fork, which has never needed maintenance, but is probably waiting to spring a repair bill on me. All the lights are battery powered, which means on dark mornings, (8 months out of the year in Seattle) I have to turn on three different lights. And each of them have a different startup sequence! <a href="https://bbot.org/blog/archives/2016/03/13/connector_savers_and_usb_devices/">My helmet light</a> is just a single press, but the tail light wants <i>two</i> presses, (one to turn it on, one to switch to blink mode) and the headlamp wants one <i>long</i> press. And they all take different batteries!

<p>And, most importantly, I recently had to shell out $350 to get the entire drivetrain replaced, due to wear. Wear that was dramatically accelerated by the fact that I wasn't cleaning the chain very often. I glumly regarded my future stretching out ahead of me, a future of cleaning my bike's chain once a week, and decided, screw it, I'm getting a new bike.</p>

<p>Local bike shop <a href="http://www.rideyourbike.com/">Aaron's Bicycle Repair</a> frequently puts together <a href="http://www.rideyourbike.com/singlespeed.shtml">Seattle commuter bikes</a>: single-speed or hub-geared bicycles with dynamo lights, full fenders, and a <a href="http://www.hebie.de/en/protection/chainguard/chainglider/350/">Hebie Chainglider</a> covering the chain. (A Chainglider is like a <a href="https://en.wikipedia.org/wiki/Gear_case">chain case,</a> except lighter, and most importantly, not as stupid looking.) The idea sounded great, so I stole it.</p>

<p>I bought the Oxford and had it shipped to work. (I got <a href="http://www.bikesdirect.com/products/windsor/oxford-city-bike-sale.htm">a paint blemish bike</a> on sale for $299) Bikes Direct say the bike comes "90% assembled", which is a great joke. Assembly is not straightforward, or quick. There are no directions. (There is an "assembly instructions" link on the site, which doesn't have instructions for this model) You need a full set of hex keys, and some experience wrenching on bikes. It took me about an hour of messing around to get it together.</p>

<p>I then took it to the shop and spent another $600 on it. (From a cost-cutting point of view, the new bike project was not a roaring success)</p>

<p>The Nexus hub came from the factory largely unlubricated, a frequent complaint online, so I had them grease it. It was also geared for geriatrics or for people interested in scaling cliff faces, so I had them swap in a 19 tooth cog. (That gives me a 44/19 ratio on the middle gear setting, just fine for my mostly level commute) The swept bars it had from the factory were just insanely bad, so I flipped them. Aaron gave me a hard time for that,<sup><a id="ln3" href="#fn3">[3]</a></sup> so I had him put on flat bars.</p>

<p>Then, of course, the dynamo, front light, and rear light, which together added up to a solid $300-- about as much as the bike itself. The dynamo is a <a href="http://www.sturmey-archer.com/en/products/detail/x-fdd">Sturmey Archer X-FDD dynamo/drum brake.</a> As advertised, fresh drum brake pads are not supremely grippy. Supposedly it develops some decent brake power after you put a few hundred miles on it. (I do 30 miles a week)</p>

<p>You don't really want to brake all that hard with it, though. Unlike rim brakes, it transmits all the braking torque through a reaction arm to just one side of your forks, which is how <a href="http://smutpedaller.blogspot.co.uk/2014/01/braking-bad.html">this lunatic</a> pretzeled their fork by doing <a href="https://en.wikipedia.org/wiki/Stoppie">stoppies.</a> I probably won't be trying that with my light, value-engineered front fork made from premium Chinese steel.</p>

<p>Oddly enough, the manual says nothing about replacing the brake pads. Rim brake pads seems to need to be changed every few hundred miles, but assuming the drum brake uses the same pads as in automative drum brakes, they would have a service interval of 50,000 miles, at which point Sturmey Archer seems to advise throwing away the hub and buying a new one.

<p>The lights are the <a href="http://en.bumm.de/produkte/dynamo-scheinwerfer/lumotec-iq-cyo-t.html">Busch & Müller Lumotec IQ Cyo Premium Senso Plus</a> and <a href="http://www.bumm.de/produkte/dynamo-ruecklicht/4d-lite-plus-339as.html">4D-Lite Plus.</a> In the grand tradition of German electronics, you get to cut the wires to length and crimp the connectors on yourself. Despite being a professional electrician for a number of years, and possessing the correct crimping tools, I managed to screw up the crimp on both spade connectors, and just soldered them on. (It also comes with two pieces of heatshrink tubing, making the confident assumption that the customer already owns a heat gun.) The headlight has a 3-position switch somewhat mysteriously labeled "T, S, O". What does the T stand for? Well, <i>tagfahrlicht,</i> obviously. ("Daytime running light") Never thought I'd miss inscrutible Euro internationalized symbol glyphs.</p>

<p>But! Once you start moving the bike, the damn things just <i>turn on,</i> and when the bike stops moving they <i>turn off</i> and now that I've installed them I'm not going to have to touch them again for <i>years,</i> which is mostly worth their incredible expense.</p>

<p>Bikes!

<hr>

<p id="fn1">1: As you might guess from the hyper-Western name, Windsor is a Chinese manufacturer.<a href="#ln1">^</a>
  
<p id="fn2">2: Fun fact, that was my very first blog post, which means this blog is more than ten years old now.<a href="#ln2">^</a>

<p id="fn3">3: In person he sounds uncannily like <a href="https://www.youtube.com/watch?v=LWH5bfpivSU">AvE.</a><a href="#ln3">^</a>]]></description>

</item>
<item>
<link>http://bbot.org/blog/archives/2016/06/09/using_ros_with_the_neato_xv-11_in_the_year_2016/</link>
<guid isPermaLink="true">http://bbot.org/blog/archives/2016/06/09/using_ros_with_the_neato_xv-11_in_the_year_2016/</guid>
<title>using ROS with the Neato XV-11 in the year 2016 </title>
<dc:date>2016-06-09T03:21:45-08:00</dc:date>
<dc:creator>Samuel Bierwagen</dc:creator>
<dc:subject> Engineering</dc:subject>
<description><![CDATA[<p><a href="https://bbot.org/blog-images/xv11.JPG"><img src="https://bbot.org/blog-images/xv11-thumb.JPG"></a>

<p>I've been meaning to do some <a href="http://www.ros.org/">ROS</a> <a href="https://en.wikipedia.org/wiki/Simultaneous_localization_and_mapping">SLAM</a> stuff for a while. There are a number of ways to do this, some more <a href="https://www.willowgarage.com/pages/pr2/order">expensive</a> than others, but one fairly straightforward option is just to use a <a href="https://www.neatorobotics.com/">Neato robot vacuum cleaner,</a> which has a LIDAR sensor and a USB port with a fairly open debugging interface, which lets you get the raw feed off the sensor, and run the motors.

<p>To run the robot, you need a mobile computer, (You <i>can</i> plug a mobile robot into a PC, but it tends to end poorly, for a number of pretty obvious reasons) so I borrowed my friend <a href="http://heysawbones.tumblr.com/">Tish's</a> laptop. It had Windows installed on it, but I had a spare 2.5" SSD with Ubuntu 16.04 installed, so I tried just plugging it in, why not. To my immense surprise, it booted right up and worked fine. Even the screen brightness keys and wifi worked with zero configuration. I've been using Linux since 2003, and the fact that nowadays you can just blindly swap boot drives between computers and have them <i>Just Work</i> is fairly darn incredible. I remember having to hand-edit X11 config files just to use two monitors!

<p>That was my last moment of pleasured surprise, because now I had to deal with ROS, which is a lovely example of the <a href="https://www.jwz.org/doc/cadt.html">CADT model of software development.</a> Most of the documentation I tried to follow was fragmentary or broken, mostly because ROS has gone through at least three different methods of "building a package" in the last 6 years. Adding to the problem was also that I refused to believe what I was reading. All the tutorials showed cloning from git repos and compiling from source. No, man, they're <i>packages!</i> Just show me how to download the precompiled packages! There's a package manager, right? With versioning, and dependency resolution? Right? Well, no. The canonical method of ROS development is downloading source files and compiling them.

<p>Here's what I eventually cobbled together. First, install ROS following the directions on <a href="http://www.ros.org/install/">this page.</a> (At the time of writing, the current stable release of ROS is Indigo) Even if you install ros-indigo-desktop-full it'll skip some stuff you'll need, so also do <code>sudo apt-get install ros-indigo-map-server ros-indigo-amcl ros-indigo-move-base ros-indigo-teleop-twist-keyboard</code><a href="#fn1">[1]</a>

<p>Now, create a new ROS project, set it as the working directory, move into the <code>src/</code> directory, and clone some git repos.

<pre>mkdir neato
cd neato
rosws init
source setup.bash 
cd src
rosws set neato_driver --git https://github.com/mikeferguson/neato_robot
rosws update</pre>

<p>(You may need to run <code>rosws update</code> more than once.)

<p>In that example I show cloning from the original repository, which is unmaintained. <a href="https://github.com/bbot/neato_robot">I had to fork it</a> to get it to work with my robot, (Mike's code assumes the XV-11 <a href="https://github.com/bbot/neato_robot/commit/b9615b75e97c45513a270d8db72c81de1dd143ae">has more motor properties than mine does,</a> oddly, and my LIDAR sensor <a href="https://github.com/bbot/neato_robot/commit/ec7311ba844fdee7210082bf2208d17b2a944ac8">seems to need some time to spin up</a> before returning data) but I didn't put my repo's URL in there since I, uh, haven't actually tested installing <code>neato_robot</code> from it.

<pre>cd ..
catkin_make
source devel/setup.bash</pre>

<p>You would not believe how long it took me to figure out that you have to use the <code>setup.bash</code> in the <code>devel/</code> folder, not the one in the project root. Anyway, if everything went right, you should be able to start up the node that actually talks to the robot. It expects to see it on <code>/dev/ttyUSB0</code><a href="#fn2">[2]</a>

<p>All four of the following commands start four separate programs, and each need their own terminals. You can <a href="http://ss64.com/bash/bg.html">bg</a> them if you want, but they print critical debugging information to the terminal.

<pre>roslaunch neato_node bringup.launch
roslaunch neato_2dnav move_base.launch
rosrun rviz
rosrun teleop_twist_keyboard teleop_twist_keyboard.py</pre>

<p>You can now drive the robot around with the keyboard in the <code>teleop_twist</code> terminal and watch what it thinks the world looks like in <code>rviz</code>.

<p>By default the <code>/map</code> topic will show you the map Mike generated of his office in 2014, which probably won't be helpful for you. You can generate your own map using the instructions on <a href="http://wiki.ros.org/slam_gmapping/Tutorials/MappingFromLoggedData">this page,</a> which, for a change, worked perfectly for me on the very first try. Here what the XV-11 decided the floorplan of my house looks like:

<p><img src="https://bbot.org/blog-images/map.png">

<p>Tah dah!

<hr>

<p id="fn1">[1]: If you don't install these packages, you'll get a cryptic error message along the lines of:

<pre>ERROR: cannot launch node of type [tf/static_transform_publisher]: can't locate node [static_transform_publisher] in package [tf]
ERROR: cannot launch node of type [amcl/amcl]: can't locate node [amcl] in package [amcl]</pre>

<p>Googling this error message will suggest that you need to delete your package and reinstall, which is both harrowing, (When I ran into this problem, I had already spent consecutive hours fighting with environment variable problems) and wrong.

<p id="fn2">[2]: Exasperatingly, Tish's laptop decided to enumerate it as <code>/dev/ttyACM0</code>, so I had to go in and edit <a href="https://github.com/bbot/neato_robot/commit/32258db99ebb208a53538e8a19e5cd452c89584a">all the code</a> that tries to talk to the wrong port. (<i>Super</i> exasperatingly, if you just change the line in <code>neato_driver.py</code> but not the launch file, it will crash out on port initialization, since the launch file passes in the port name as a parameter! That took <i>forever</i> to find. <a href="https://en.wikipedia.org/wiki/Single_source_of_truth">Single point of truth</a> you bastards!)

<p>Anyway, the robot control port is just a regular old tty, you can do <code>screen /dev/ttyUSB0</code> and directly control the robot. There's a bunch of interesting stuff in there, (You can control <i>everything.</i> LCD backlight, individual LEDS, etc) though it seems to invisibly flip between certain interface modes, rather a lot like Cisco IOS.]]></description>

</item>
</channel>
</rss>
