Sun May 19 00:38:13 PDT 2024

pocket co2 meter before it was cool

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

The controller is a Seeeduino Lotus, which is essentially an Arduino Uno with Grove 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 Sensirion SCD30 non-dispersive infrared CO2 sensor. The box is a Hammond 1591 in IR Red, purchased from Vetco Electronics, which as far as I can tell is the last remaining electronic components store in the Seattle area and probably all of Washington state.

After getting the measurement and updating the displays, it uploads the results to a simple application server using thirty lines of Python that call Flask.py 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 Plotly.js.

(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.)

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.


Posted by Samuel Bierwagen | Permanent link | File under: nerdery

Sat Jan 2 00:24:00 PDT 2021

sawbot

(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)

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

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 polyfuse in seconds) and unlubricated plastic-on-steel bushings.

The robot is basically just a hacksaw blade (clamped between two 1x2x1x35 c-channels) mounted to a crank. The linear part uses a linear motion kit with all four wide slide trucks mounted, and greased with Super Lube 21030. The rotational bit uses a turntable bearing 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.

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 Pongbot 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!

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!

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.

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())

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. FeedShaft won't do anything outrageously stupid if you give it a negative number, but it'll quite happily run the shaft feed sled into the far end if you give it a large positive number.

We could fix the problem by changing FeedShaft to clamp input values to 1.875-6.0, but input validation should be done at the point where the user enters the input:

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)

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 false afforance that tricks the user into thinking they can keep going up or down, so let's fix that too.

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")

Note the implemtation detail here: the length check in ChooseLength is <= 5.875, while the equivalent value in UpdateLCD 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 off-by-one error.

A slightly different problem arises. On the first update, the LCD prints 1.875 - 10.125 but when you press up once, it prints 2.0 - 10.0, 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:

def ZeroPad(float):
    string = str(float)
    if len(string) == 3:
        return string + "00"
    elif len(string) == 4:
        return string + "0"
    else:
        return string

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 four characters in it, instead of five, because I was counting the digits. I wrote my function as a string of elifs, and became immensely confused when it returned None instead of a zero-padded number. (A function returns None by default if you don't override it by passing something to the return keyword.)

And that's basically it! You can check out the full program here.


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

②: "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 (12.0 - length) is doing in the lcd.write_top call. ^


Posted by Samuel Bierwagen | Permanent link | File under: nerdery

Fri Jan 1 00:24:00 PDT 2021

pongbot, a robot that plays pong against itself

(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)

For VEX Robotics World Championship 2015, like last year, Robot Mesh needed some kind of booth demo.

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

We wanted to do something with pneumatics, and something with the Pixy camera, since computer vision is both interesting and interestingly difficult. How about... a robot that plays pong against itself?

Let's go over the hardware first.

We used 2906s for the frame because they're cheap and rigid. A robot brain with one fried H-bridge 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.

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 dramatic reduction in motor lifespan, 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 high speed configuration. Unloaded, this should have moved the paddle at ((1.5 in. diameter * pi) * (160 RPM / 60)) = 12.56 inches per second. (31.91 cm/s)

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 turbo gears.

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.

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 PixyMon open so you can check to make sure that the Pixy actually sees what you think it sees.

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, or 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.

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.

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 TCP Global TC-20. 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 dry firing them. (This was mitigated by only opening the solenoid very briefly, see below.)

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.

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

Why, at Western Fluid Components, 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.

Late in the project we chopped up a battery extension cable 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 can do weird things to computers.)

Rubber bands were strung across the corners, to keep the ball from getting stuck where the paddle couldn't get at it. (A literal corner case.) 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 1x25 steel bar and bent the ends in a vice so that the ball would be nudged away from the paddle during travel.

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

paddle1_kick = vex.DigitalOutput(8)

paddle1_kick.on()

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

def FirePaddle(device):
    device.on()
    sys.sleep(.05)  # Avoid dry firing by only opening the solenoid for a brief time[1]
    device.off()

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, even in real, professional software. In the programming industry, the injunction against repeating yourself is called Don't Repeat Yourself.

To avoid Repeating Myself, I define the name of a new function using def, name it FirePaddle, and specify that it takes one argument, device. (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.)

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 paddle1_kick:

FirePaddle(paddle1_kick)

Causing FirePaddle to execute:

paddle1_kick.on()
sys.sleep(.05)
paddle1_kick.off()

Isn't that neat?

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

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

(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.)

This stack of if 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.[2] 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.

The while loop runs forever, reading one byte at a time off the serial port. If the byte it reads is FRAME_SYNC[0], 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 breaks out of the infinite loop.

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)

This one's a little arcane.

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 bitwise (one bit at a time) ORed with the LSB. A fabulous ASCII art diagram:

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

There!

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

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)

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

def __init__(self):
    self.older = 0.0
    self.old = 0.0
    self.new = 0.0

This defines a special class method, like the on method of the vex.DigitalOutput class earlier. We give it the special method name __init__, which is executed when the class is instantiated. (A class constructor.) We pass another magic word, self, which is replaced with the variable name you chose when you instantiated the class, here predicted_loc_ball.[3]

We initialize the variables with 0.0 to force them to use the floating point 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.

__call__ is another special method name that is called by default without having to specify a method. As for what it actually does:

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

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 Kalman filter, (It's like a PID algorithim, only inside out) but all the code found from a lazy Google search required the NumPy 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. 1000ms / 50hz = 20 ms.)

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.

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

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)

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 OutputFilter class. (Like PredictLocation, but it doesn't forward predict. If we forward predicted both the ball and the paddle, then the system would oscillate pretty badly.)

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()

Revision 1 only used the Pixy for tracking the ball, and IMEs[4] 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...)

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.

There is a deadband of 10 pixels around the ball, to prevent excessive hunting. (The > (filtered_loc_paddle1 + 5) < (filtered_loc_paddle1 - 5) stuff)

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 Motor.run API. The solenoid fires when the ball enters a 20x20 pixel box around the paddle.

The __call__ 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.

And that's pretty much it! The code is up on Robot Mesh Python, so you can run it yourself. Check it out!


1: ^ It is tremendously 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:

sys.sleep(.05)  # Avoid dry firing by only opening the solenoid for 50ms

Ah, but Don't Repeat Yourself! I had to change the value passed to sys.sleep() 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!

2: ^ 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 bare metal.

3: ^ 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. print predicted_loc_ball.old will return 0.0. 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, then you are almost certainly committing a design error.

4: ^ 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.


Posted by Samuel Bierwagen | Permanent link | File under: nerdery

Tue May 1 23:14:15 PDT 2018

google please stop breaking chromedriver

So at work one of the things I do is write regression tests against our webapp. One of the many, many ways to do automated testing is with the Selenium browser automation framework. Selenium needs a way to hook into your browser of choice: for chrome, this is Chromedriver. So far, so good.

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 actual, physical machines, 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.

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

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 ABI thing, so why the short shelf life?

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 npm module for Chromedriver, 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.

3.) If Chromedriver is stale, how about you tell me that? 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?

No, instead Chromedriver picks the last, most exciting option:

4.) Random inscrutable error messages.

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

"Update to Chromedriver 2.36"

How about SessionNotCreatedError: session not created exception from timeout: Timed out receiving message from renderer: 600.000?

Update Chromedriver.

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

You get the idea. Chromedriver is addicted to throwing wrong, completely spurious exceptions as a matter of routine. According to their site, any given version of Chromedriver is only good for three releases. 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!


Posted by Samuel Bierwagen | Permanent link | File under: Work

Sun Dec 31 23:18:40 PST 2017

commuter bike, 920 miles later

Sixteen months ago I said the following:

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.

I am now cleaning the chain of that replacement bike once a week.

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

1.) 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 Park CC-3.2, is cheap and handy. (You can measure chain with a ruler or tape measure, but it's a hassle.)

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 must 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.

2.) IGHs are filled with oil, which, per Shimano, 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 $60 per litre. This guy 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!

3.) This is a topic of hot debate, but some allege that fixed-chainpath bikes eat chains more often. But I've got a Hebie Chainglider on there, which should protect the chain from dirt, and make it last longer, right?

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.

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.

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.

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.

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.

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 divided, 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. All is compromise, trading one good for another good.

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 Continental Top Contact Winter II 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.

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 quite a lot better on ice. Plus, the contact patch on the 42 mm tire is a hell of a lot bigger, which should help some just by itself.

So that's 2017. Check back next year, when I will have talked myself into buying yet another bike for one reason or another.


Posted by Samuel Bierwagen | Permanent link | File under: Etc

Wed Dec 21 22:37:14 PST 2016

commuter bike, 360 miles later

So I've had the Oxford for a couple months now, and a few events have occured.

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.

In the previous post 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 do much.

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 also 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.

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.

I switched to Panaracer Urban Max tires, and haven't had a flat since.

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.

Well fine. I'll just make my own!

Easy!

The first google hit for "lasercut stainless steel" is Lasergist. Their site is excellent, and the prices are cheap. Something they don't 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!

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?

Well, eventually.

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 legendarily annoying to machine. 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.

But now it's fixed, and it's real unlikely it'll break at the same spot.


Posted by Samuel Bierwagen | Permanent link | File under: Engineering

Tue Nov 22 23:41:00 PST 2016

fun and games with 3d printers

This is my handle for an Anderson Powerpole connector. There are many like it, but this one is more expensive than most.

We 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 $20 plus $10 shipping. Outrageous! Are we not men? Do we not have 3D printers with which to produce trivial plastic parts like this? I slapped together a simple model (source) using the parametric CAD program of choice for cheap bastards, Solvespace, and took a quick jaunt downtown to get it printed at Metrix Create Space.

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

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

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 Fiberfix that was going to expire soon anyway. And it works! Qualified victory!


Posted by Samuel Bierwagen | Permanent link | File under: Engineering

Sun Aug 14 23:12:19 PDT 2016

putting together a commuter bike

This is my bike, a Windsor Oxford,[1] in Tangerine, serial number L150101593. There are several hundred thousand like it, but this one is mine.

I've been commuting on a Trek 6000 for the last 10 years or so.[2] 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! My helmet light is just a single press, but the tail light wants two presses, (one to turn it on, one to switch to blink mode) and the headlamp wants one long press. And they all take different batteries!

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.

Local bike shop Aaron's Bicycle Repair frequently puts together Seattle commuter bikes: single-speed or hub-geared bicycles with dynamo lights, full fenders, and a Hebie Chainglider covering the chain. (A Chainglider is like a chain case, except lighter, and most importantly, not as stupid looking.) The idea sounded great, so I stole it.

I bought the Oxford and had it shipped to work. (I got a paint blemish bike 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.

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)

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,[3] so I had him put on flat bars.

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 Sturmey Archer X-FDD dynamo/drum brake. 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)

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 this lunatic pretzeled their fork by doing stoppies. I probably won't be trying that with my light, value-engineered front fork made from premium Chinese steel.

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.

The lights are the Busch & Müller Lumotec IQ Cyo Premium Senso Plus and 4D-Lite Plus. 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, tagfahrlicht, obviously. ("Daytime running light") Never thought I'd miss inscrutible Euro internationalized symbol glyphs.

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

Bikes!


1: As you might guess from the hyper-Western name, Windsor is a Chinese manufacturer.^

2: Fun fact, that was my very first blog post, which means this blog is more than ten years old now.^

3: In person he sounds uncannily like AvE.^


Posted by Samuel Bierwagen | Permanent link | File under: Etc

Thu Jun 9 03:21:45 EDT 2016

using ROS with the Neato XV-11 in the year 2016

I've been meaning to do some ROS SLAM stuff for a while. There are a number of ways to do this, some more expensive than others, but one fairly straightforward option is just to use a Neato robot vacuum cleaner, 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.

To run the robot, you need a mobile computer, (You can 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 Tish's 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 Just Work is fairly darn incredible. I remember having to hand-edit X11 config files just to use two monitors!

That was my last moment of pleasured surprise, because now I had to deal with ROS, which is a lovely example of the CADT model of software development. 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 packages! 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.

Here's what I eventually cobbled together. First, install ROS following the directions on this page. (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 sudo apt-get install ros-indigo-map-server ros-indigo-amcl ros-indigo-move-base ros-indigo-teleop-twist-keyboard[1]

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

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

(You may need to run rosws update more than once.)

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

cd ..
catkin_make
source devel/setup.bash

You would not believe how long it took me to figure out that you have to use the setup.bash in the devel/ 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 /dev/ttyUSB0[2]

All four of the following commands start four separate programs, and each need their own terminals. You can bg them if you want, but they print critical debugging information to the terminal.

roslaunch neato_node bringup.launch
roslaunch neato_2dnav move_base.launch
rosrun rviz
rosrun teleop_twist_keyboard teleop_twist_keyboard.py

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

By default the /map 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 this page, 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:

Tah dah!


[1]: If you don't install these packages, you'll get a cryptic error message along the lines of:

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]

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.

[2]: Exasperatingly, Tish's laptop decided to enumerate it as /dev/ttyACM0, so I had to go in and edit all the code that tries to talk to the wrong port. (Super exasperatingly, if you just change the line in neato_driver.py 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 forever to find. Single point of truth you bastards!)

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


Posted by Samuel Bierwagen | Permanent link | File under: Engineering

Sun Jun 5 22:58:11 EDT 2016

sawbot for VEX Worlds 2016

I built another tradeshow demo! This time, it's a robot that cuts shafts to length.


Posted by Samuel Bierwagen | Permanent link | File under: Engineering

Sun Mar 13 23:15:11 EDT 2016

connector savers and USB devices

My bike helmet light, the Light & Motion Vis 360, like basically anything with a battery in it nowadays, charges over USB. Unfortunately, USB sockets aren't invulnerable, and after a couple years of bike commuting, mine loosened up to the point that it wouldn't charge.

Well, I'm a handy fellow, so I opened up the light, got out my fine tip temperature-controlled soldering iron, and promptly lifted a couple tracks. Whoops.

Light & Motion charged me $48.75 for a new logic board and battery, which was reasonable enough, but I really don't want to spend $49 every two years as long as I own this light. Isn't There A Better Way?

There sure is! Connector savers!

A connector saver is an adapter that you put on an expensive piece of equipment, and make all future connections at this interface. This protects your equipment, because when some idiot trashes the connector saver, you replace a relatively inexpensive adapter rather than send your equipment out for repair.

Connector savers are much more common with test equipment and in the aerospace world, (Indeed, the first place I heard about them was in NASA-STD 8739.4) where a single big multi-contact connector can cost thousands of dollars, and be rated for surprisingly few cycles. But hey, nothing stopping me from putting a connector saver on my piddly little bike light.

I wanted a connector saver with a flexible bit in the middle, basically a very short extension cable. This was hard to find. Newegg's cable category has really lousy search, and while Amazon did have what I was looking for, shipping would be annoyingly expensive, and I didn't really want to give the Beast from Lake Union any more business, given that my company directly competes with them in a couple markets.

I finally found USBFirewire's RR-MCB-EXT-05G5, which worked perfectly. Now we'll see how long the connector saver will last!


Posted by Samuel Bierwagen | Permanent link | File under: Engineering

Wed Feb 24 22:28:57 EST 2016

awaken

So!

As you have no doubt not noticed at all, this blog has in fact not updated in two years. I am not dead!

Two years ago my hosting provider went bankrupt suddenly, erasing all user data in the process. I had backups, of course, but inconsiderately neglected to back up my home directory on the server, which happened to house my Nanoblogger installation. (A shell script-based static site generator (basically Jekyll for cavemen) so old that it has not been updated since 2011, for whom development was suspended in 2013, and is currently hosted on Sourceforge, a software host so wildly reviled that the domain is now blocked by uBlock Origin.)

The prospect of rebuilding the data files was so daunting that I put it off until "next month" for 24 months in a row, until I finally got around to it, today. So, hi.

I have been updating my tumblr in the mean time. There are some amusing posts on it, like this one!

I don't want to come off as a solar power hater with these posts. Solar power is our future, it is inevitable, and it is a thing that must be done, because oil sure as heck isn't going to last much longer.

But solar panel roads are never going to happen, not for centuries, at least in the United States. Solar driveways probably will happen, but as a toy of the rich.

Let me tell you why. There is one single figure of merit in a PV system, and that is cost per peak-kilowatt installed. For giant, utility-scale installations, it's around $1000 per KWp, because you can get better price breaks when you buy ten million bucks of solar panels at a time, and permitting costs, which remain roughly constant, make up a smaller proportion of the total price. For large residential installations, $2200 per KWp is a better bet.

The Wikipedia article on PV systems has a very useful table:

image

Installed cost per KWp is on the left, insolation is on the top. "Insolation" is how much light you get per square metre of land, and for solar, it's most useful when expressed as kilowatt-hours per day. (To convert KWh/day to KWh/year, you just multiply by 365.25, of course) Las Vegas gets around 1936KWh/y, so for a $2200KWp installation, your electricity costs a little more than 11 cents per KWh. Seattle gets 1289KWh/y, so you'd pay ~14 cents/KWh.

And here's the big problem, the central issue around which everything in the solar industry revolves: Conventional electricity in Washington state costs about 8.2 cents/KWh. Solar is a solid 80% more expensive. The only places where PV is at grid parity is in deserts.

PV is expensive. The solar industry is on a mad, headlong dash to cut costs as much as possible. They want grid parity, they are seeking it, seeking it, all of their thoughts are bent on it

I've also been writing for my employer:

image

Pongbot at VEX Worlds 2015

For VEX Robotics World Championship 2015, like last year, Robot Mesh needed some kind of booth demo.

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

We wanted to do something with pneumatics, and something with the Pixy camera, since computer vision is both interesting and interestingly difficult. How about... a robot that plays pong against itself?

image

Python for Kiwibot

Normally, to check the state of six touch sensors in a single if expression, you'd have to do something like:

if FR_touch.is_touch() or MR_touch.is_touch() or BR_touch.is_touch() or FL_touch.is_touch() or ML_touch.is_touch() or BL_touch.is_touch():
  doSomething()

This has a number of disadvantages:

  1. We end up having to type is_touch() over and over, which is annoying. (Don't Repeat Yourself, he repeated.)
  2. Very long lines are hard to parse, and therefore impair readability. (Code is hard enough to read already, we don't want to do anything to make it worse.)
  3. If the end of a line hangs off the edge of the screen, then bugs can hide where you won't see them at a casual glance. If we had forgotten the () on the very last is_touch then the if expression would always evaluate as True[1], and we would be very confused when the robot acted like the TouchLEDs were constantly being pressed.
  4. It's ugly and I hate it.

I've also been tweeting, to the immense regret of everyone, everywhere. I am of course hilarious and beloved:

Follow me on all these platforms, or don't, it's up to you!


Posted by Samuel Bierwagen | Permanent link | File under: Game Design

Fri Mar 21 20:58:25 PDT 2014

replacing the battery in a cheap solar charger

So I've got a very cheap solar charger, whose internal li-poly battery has been slowly dying, and is now mostly useless. Popping it open, I discover it's a 800mAh unit.

Why, robotmesh.com, my place of employment, just happens to sell 850mAh li-poly batteries! How convenient.

So let's replace this sucker.

While I've got it open, though, I want to see how much current the little solar panel can actually source. All you need to do this is a single multimeter, though it's more convenient to use two, one bridging the positive and negative rails, to measure voltage, and one inserted in the current flow, to measure amperage. (If you get it backwards, the ammeter will look like a dead short to the voltage source, and either blow a fuse or melt your test leads.)

Experimental apparatus

(Diagram made with Circuitlab, which can apparently do all sorts of fancy simulation stuff, none of which I actually used.)

This looks very neat and clean on paper, and becomes a horrifying tangle of wires when implemented with multimeter leads and alligator clips. (Mostly hidden off-frame)

I set everything up, ready to finally, at last, read off the current...

And it's way off the low end of the scale. Switching to the digital meter, I discover that even in direct sunlight, the charging circuit can only manage a thoroughly unexciting 7mA. Assuming perfect charging efficiency, (which ain't gonna happen) it'd take 121 hours of direct, face-on sunlight to recharge a flat battery.

I wrote a gloomy analysis of a fictional solar charger on the other blog, and even with my worst case assumptions, this real-world solar charger is more than 26 times worse. Time, and sunlight, has not been kind to its panel.

So, now that I know how much it sucks, it probably wasn't worth spending 7 bucks to put a new battery in it, but oh well, let's close it up again.

Today's stars are my old Weller P2K butane soldering iron, here used just for the heat-shrink, a crappy Tenma unregulated soldering iron, a Panavise PCB holder that they apparently don't make anymore, and some Radio Shack "helping hands". A depressing amount of the work that goes into doing electronics stuff is fixturing-- getting things to stay in the right position while you do things to them.

I took six photos here, all of them in varying degrees of out-of-focus.

Someone more professional than I would probably have done a lapped splice here, in which case it's very important to slip on the heatstrink before you solder the joint, but I did a twist splice because I'm lazy and the joint wasn't going to take any mechanical stress anyway.

And we're done! Now I have a charger which should last several more years, at which point I will throw it right into the trash.


Posted by Samuel Bierwagen | Permanent link | File under: important, Engineering

Thu Mar 13 22:41:11 PDT 2014

introducing tinypass.py v1.0

Before I can talk about what I did right, I have to talk about what I did wrong.

I host some files for a friend. They're great big zip files full of art, which he sells for money, so he'd like to put a password on them.

"Easy enough, this is exactly what HTTP Basic authentication is for."

But he'd like to be able to set passwords on files without having to ask me to manually fiddle with nginx config files.

"Well, I'll just whip up a quick forms-based thing for editing nginx config files. How hard could it be?"

(A chill wafts over your skin. Dread shivers up your spine.)

It took POSTed form data (filename, username, password) from a static HTML page, created a hashed password file from that password, appended a location /filename block to a config file, then called /etc/init.d/nginx reload.

And as soon as it was actually used by someone who didn't write it, it blew up.

Oh v0.1, there were so many things wrong with you, how could I possibly count them all?

1.) It had to have permissions to edit nginx config files and reload the server. So I just ran it as root, which meant that I was running a python web server as root, which is an absolute security disaster. I'm listing this first, even though nothing bad actually happened (as far as I can tell) because it was just a complete unforced error. This was the first warning sign that I was doing something dumb, and I completely ignored it.

2.) filename was just a text box, not a dropdown menu or picker, so it was trivially easy to typo a filename, and "set a password" on something that didn't exist. v0.1 had no error checking of any kind, so it couldn't refuse to do that.

3.) HTTP Basic is user-granular, but for this particular use we're doing file-granular permissions. HTTP Basic doesn't handle this very gracefully: if you're already logged in, and try to access a file you don't have permissions for, (say, if you bought several different items, or if you're me, and are trying to troubleshoot your broken fucking login system) then it just hits you with a 405 Authorization Needed error, no login window. Since HTTP Basic doesn't have a log out button, (hint: where would you put it?) you have to restart the browser, or just wait around until the browser expires your login credentials, which is, as you'd guess, implementation-specific.

4.) Remember when I said that it just appended lines to a config file and reloaded the server? v0.1 had no conception of records-- it was a basic CRUD app in theory, but in practice it only created records, it couldn't read, update or delete them. It would quite happily, create two location blocks for the same file.

Nginx will refuse to load a config file that has contradictory options. If you restart it with a bad config file, then it won't start back up, and your web server goes down until you fix it.

A minor decision I made early on really saved my ass here. I heard that using reload instead of restart let nginx wait for clients to finish transferring data, so I used it in the script. Luckily, reload won't take down your sever with a bad config file, it'll just refuse to load it.

So instead of blowing up the server, v0.1 just silently stopped applying changes until the config file was manually fixed.

Now, all these problems have solutions. You could conceivably train the end user to carefully work around the problems, on the theory that your software is great but the user is dumb, but when your tool collapses in a great heap of splinters at the slightest touch, then it's not the fault of the user, it's your fault.

You could also fix each of these bugs, add tests, etc, but the basic architecture of the program is just bad. It's fucked.

Ctrl+a, del.

Let's try again.

A nice screenshot of the tinypass.py github project

tinypass.py v1.0 is designed to replace HTTP Basic with something about as secure, but a little friendlier to use, as well as letting the end user set and change passwords without fatally confusing nginx.

Rather than sending login credentials in the clear over HTTP headers, like HTTP Basic, tinypass.py sends credentials in the clear over cookies, which is much more secure. Best practices here would be hashed passwords and session ID cookies, which would require more work. I didn't feel like doing that work, because...

"Principles of an Indie Game Bottom Feeder"

I don't really make a living selling games. I sell an ethical life.

How could I make a living selling games? Anyone who wants to pay me for my games doesn't have to. It's not like buying a chair, where they'll chase you down and taser you if you grab it and run out of the store. Nobody who wants my game on Windows or Mac has to pay for it to get it. Frankly, most of them don't.

So why do people pay for it? Because they understand a fundamental fact: For these games to exist, someone has to pay. If everyone just takes it, I'll have to get a real job and the supply will shut off. I don't want to get into one of the eternal tedious arguments about "software piracy". I will instead focus on one single, incontrovertible fact: I have a family to feed. If nobody pays for my games, I can't make them.

So what does someone get when they pay for my game? They get the knowledge that they are Part of the Solution and not Part of the Problem. They know that, in this case, they are one of the Good Guys. It is well-earned self-satisfaction, and it is valuable. To know they are doing the right thing, some people will happily pay 20 bucks. This is how I stay in business.

You can't stop piracy. DRM never works. You can't let somebody look at something without also letting them copy it. Cannot be done, impossible, full stop.

So tinypass.py is a speedbump, not an impassible wall. Since there are no confidential login credentials at risk, I don't go to any great lengths to keep them secure.

So hey! That's it. Check it out, I guess, just as long as you don't look at the commit history.


Posted by Samuel Bierwagen | Permanent link | File under: important, Linux

Mon Oct 28 22:48:43 PDT 2013

programmatically advertising mobile bandwidth cost: a proposal

You know what would be cool? If your phone knew how much bandwidth from each carrier cost, and could switch between them on the fly, depending on which one was cheapest, like a multi-SIM phone that didn't suck.[1]

You know what would be cool? If your phone could roam between a cell tower and an arbitrary wifi AP, like a parallel-universe version of UMA that also doesn't suck.

You know what would be cool? If wifi APs could programmatically advertise bandwidth cost too, so anyone could compete with AT&T just by nailing a linksys router to a wall.[2]

You know what would be cool? If your DSL modem[3] could advertise bandwidth cost too, just like your wifi AP and your cell tower. You'd have to pay common carrier costs for the last mile of cable to your house, no way around that, but as soon as your bits make it to the first point of presence, you'd have you choice of long-distance IP transit providers, just like the last time we broke up a telecom monopoly.[4]

All these ideas seem very simple and obvious. Have they been proposed before?


1: Bandwidth is a utility service, like electricity, or water. Any profit a utility monopoly makes is extracted from the productive economy, a tax on real industries. AT&T made $3.8 billion last quarter.

2: The implementation details of this one are going to be tricky. With a major carrier, you can just tally up all the kilobytes used and bill the user at the end of the month, but with wifi, it's entirely possible a person will walk into a starbucks, watch a video on youtube, then walk out, never to return. How is that billing system going to work? Are you going to have to manually provide billing information before connecting? That would be terrible.

3: Well, "transceiver".

4: Honestly, I anticipate a lot less benefit from this one. Transit is already a very competitive commodity market, with razor-thin margins. The biggest problem with consumer internet has always been that last mile, and the associate incumbent telcos, who have no useful competition, and therefore lots of monopoly profit.

You'd almost have to mandate embedded cell phone radios in terrestrial internet transceivers in order to guarantee last-mile diversity... wait, shit, that's a great idea!


Posted by Samuel Bierwagen | Permanent link | File under: important, Engineering

Sat Aug 31 02:22:57 PDT 2013

Review: "Apocalypse Codex" by Charles Stross (2012)

My tweet about the Apocalypse Codex

Actually, I want to expand on that.

Apoc. Cod. is a book that either needed another rewrite or a more aggressive editor, which is odd, for an author's 20th book.

Its sins are numerous. Stross has picked up an unfortunate habit of repeating himself-- SCORPION STARE is explained several times, and at many points where characters explain what's going on to other characters, instead of eliding the details, he'll actually spend a couple pages on the conversation. These recaps would be useful in a longer, more complex novel, but Apoc. Cod.'s tight structure and fast pacing work against it, (And its 336 page length) making the frequent reiterations of the plot more annoying than useful. (Plus, I powered through the whole thing in 5 hours during a car ride, which helps to keep events fresh in your mind.)

The book does have some fairly good moments, to the point where the usual in-car soundtrack of classic rock FM radio became grating, and I wished for something gloomy, sepulchral.[1] The despond is punctuated by some unfortunate attempts at soapboxing. One of the characters, much like the author, is an atheist, and by God he's gonna let you know about it.

This is ill-advised. Evangelical Christianity is best criticized by repeating their ludicrous bullshit with a straight face. (Did you know that Pat Robertson has a long list of divine revelations?) The Quiverfull ideology mentioned in the book is a real thing that actually exists. With all this rich material, having a in-universe character actually say "These people are super dumb" is redundant, bordering on jejune. We get it, dude. You don't need to have zombie missionaries smashing in the doors to get the point across.

Another problem is the hero, a computational demonologist and former IT schlub.

There's an authorial voice that's peculiar to nerds in general, and science fiction fans in particular. It shines through clearly in print in books like Fallen Angels (which contains paid-for cameos by big name fans) and the execrable Troper Tales of tvtropes, which were so bad that they've been quarantined on another site. It's distinctive as it is annoying.

When he's not actually holding a gun, Bob talks like a slashdot commenter. This is,

A.) Top notch characterization, and spot on accurate.

B.) Super, super irritating.

I found myself skimming early conversations just to avoid reading what the main character actually said, which is unhelpful for following the plot. This is another example of Stross' mania for absolute factual accuracy, which can occasionally get in the way of the story. (He emphasizes several times that the life of a spy is boring, and not at all like a Bond flick, which is troublesome when you're pretty much writing a bond flick.)

The book's okay. I guess.


1: While writing this, I discovered that Catacombs is actually a one man band, run by a fellow named Xathagorra Mlandroth. Xathagorra Mlandroth! Gosh I love funeral metal.


Posted by Samuel Bierwagen | Permanent link | File under: important, nerdery

Wed May 29 00:28:23 PDT 2013

keeping my ass warm

So I was reading the owner's manual for the 2013 Nissan Leaf, (as you do) because I was wondering if it said anything about overuse of rapid charging.

Every time I bike to the grocery store, I see a Leaf connected to the 440V DC fast charger, (Google Kirkland is five minutes away.) which can put an 80% charge on a car in 30 minutes.

But batteries suck. Charging a battery in 30 minutes in precisely analogous to discharging it in 30 minutes-- something big lithium-ion batteries don't like. Sure enough, page 43:

Batteries don't like to get too hot or too cold, to be discharged too hard, too deeply, or left discharged for too long. You might then conclude that the best choice is to just leave the car in your driveway-- where the battery will then just quietly decay on its own. Batteries suck.

Also, page 187:

This makes a lot of sense-- heating pads are orders of magnitude more efficient at warming humans than using air as the transfer fluid. In an internal-combustion vehicle, you get hot air for free, since a heat engine has to dump a lot of heat to the outside environment in order to extract useful work from it, you might as well pass that waste heat stream through the cabin, like a not-terribly-efficient cogeneration setup.

But in an electric vehicle, every watt-hour of power comes from the battery pack, and each watt-hour is dear indeed. Heat is no longer free.

Bold prediction: Heated seats will be standard equipment in all electric cars.


Posted by Samuel Bierwagen | Permanent link | File under: Engineering

Thu May 23 02:50:48 PDT 2013

Bad Transcript: Star Trek Into Darkness (2013)

I wrote a Bad Transcript for the new Star Trek movie. It's pretty good, you should read it.

The transcript, that is. The movie isn't good.


Posted by Samuel Bierwagen | Permanent link | File under: important, Meta

Fri Jan 4 21:03:31 EST 2013

building dtwenty.org

767 days ago, I commented on a HN submission about a random number generator:

3.) Providing random numbers as an advertisement for your fine line of hardware random number generators. Here it doesn't matter how much money you make [providing the numbers], you just want people to buy the hardware that made them. Oddly enough, none of the random number services (and there are quite a few) do this, for some inexplicable reason. There's not even an argument-from-proprietary technology, since HRNGs are supposed to generate perfectly random noise, and there's no way an attacker could stage a replay attack.

I left it there, because I was lazy. But last month, notorious badass Maciej Cegłowski created The Pinboard Co-Prosperity Cloud.

What is it?

The Pinboard Co-Prosperity Cloud is a startup self-incubator. Six successful applicants will receive a modest amount of funding and as much publicity as I can provide for their sustainable and useful business idea.

Is this a joke?

It is not a joke.

What are the requirements?

You must have a good idea that you are capable of building, a willingness to build it, and a plan for making it mildly profitable.

How much funding will I get?

Each successful applicant will receive $37. This will cover the cost of six months of hosting at prgmr.com and a productivity-enhancing hot beverage.

So I entered. Ha ha why not?

The more I thought about it though, the more I realized that I wasn't getting the joke. The idea was trivially simple. I already had a web server. I didn't need all that mad cash. I could just... build it.

So I did. It's right here. (EDIT 2013/3/22: I let the domain name lapse, and moved the content to bbot.org)

It was amazingly easy, even though this was pretty much my first major (har) piece of programming. I had never used python, javascript, or jquery before.

Web programming in the year 2012 has the smooth, well polished feel of something that has had the sharp edges worn off by the passage of thousands of other people. Getting nginx to talk to the WSGI server was a snap. Installing bottle.py was easy. JQuery was no problem.

Any time I had a problem, googling the error message would return a helpful, relevant page, explaining how my "build it as fast as possible, while learning as little as possible" design methodology had screwed me over again.

At the time, of course, it seemed a vast edifice of impossible complexity, but in retrospect it was painless. "It's easy to do if you know how to do it", maybe.

The only difficulty I faced was the hardware random number generator. The numbers had to come from it, since that was the whole point of the site; but my server was a virtual machine on the east coast, and my HRNG was sitting on my desk.

The "money" solution would be to buy a rackmount server, plug the widget into it, then slot it into a colo, but I didn't have money, and instead I had to be creative.

I couldn't just run the web server locally, since my ISP blocks port 80. Enter the ugly hack: I plugged the entropykey into a spare laptop, ran the application server on that, then ran a SSH tunnel to my web server, which communicates with the front end via JSON. It works, at the cost of an extra 150ms of latency per roll.

There's room to improve, of course. You could probably list off a dozen features dtwenty.org needs without pausing to draw breath, (starting with "make it less ugly") but, the ideal of the minimum viable product shines bright.

The second biggest problem after integrating the HRNG was the ad copy that makes up most of the page. It was originally twice as long-- ruthless editing has reduced to it merely "too long" from "far, far too long." This too could use improvement.

But! It's done and it works! Programming is fun.


Posted by Samuel Bierwagen | Permanent link | File under: important, Linux

Sun Dec 9 23:49:30 EST 2012

"How would I get started" and the problem of truth

I've been meaning to write about Hacker News again, but have held back, since it's a pretty boring subject outside of HN's rather shallow pool of users. But recent events have forced my hand.

"How would I get started?"

Last night on Hacker News, someone asked a simple question with a complicated answer: “I want to build a cable company. How would I get started?”

I’m really disappointed in the universally pessimistic and generally unhelpful answers this question received. Some people pitched some interesting ideas and helpful analysis, but most of the replies reinforced the notion that Hacker News readers are predominantly male know-it-alls and on the average, a bunch of snarky dicks.

Lots of emotional content here, but not much meaning. The attitude behind these two paragraphs becomes clearer if we look at some other quotes:

"Black Swan Farming"

In startups, the big winners are big to a degree that violates our expectations about variation. I don't know whether these expectations are innate or learned, but whatever the cause, we are just not prepared for the 1000x variation in outcomes that one finds in startup investing.

That yields all sorts of strange consequences. For example, in purely financial terms, there is probably at most one company in each YC batch that will have a significant effect on our returns, and the rest are just a cost of doing business. [1] I haven't really assimilated that fact, partly because it's so counterintuitive, and partly because we're not doing this just for financial reasons; YC would be a pretty lonely place if we only had one company per batch. And yet it's true.

To succeed in a domain that violates your intuitions, you need to be able to turn them off the way a pilot does when flying through clouds. [2] You need to do what you know intellectually to be right, even though it feels wrong.

It's a constant battle for us. It's hard to make ourselves take enough risks. When you interview a startup and think "they seem likely to succeed," it's hard not to fund them. And yet, financially at least, there is only one kind of success: they're either going to be one of the really big winners or not, and if not it doesn't matter whether you fund them, because even if they succeed the effect on your returns will be insignificant. In the same day of interviews you might meet some smart 19 year olds who aren't even sure what they want to work on. Their chances of succeeding seem small. But again, it's not their chances of succeeding that matter but their chances of succeeding really big. The probability that any group will succeed really big is microscopically small, but the probability that those 19 year olds will might be higher than that of the other, safer group.

The probability that a startup will make it big is not simply a constant fraction of the probability that they will succeed at all. If it were, you could fund everyone who seemed likely to succeed at all, and you'd get that fraction of big hits. Unfortunately picking winners is harder than that. You have to ignore the elephant in front of you, the likelihood they'll succeed, and focus instead on the separate and almost invisibly intangible question of whether they'll succeed really big.

"Why I now, unfortunately, hate Hacker News"

raffi 114 days ago | link

Most companies fail. It's a safe bet to predict failure. It's pretty lame to celebrate that failure from the sidelines.

Vision is not "how is this guaranteed to fail?" but how could it possibly succeed despite the odds?

A core tenet of hacker ethics, the zeroeth law perhaps, is being right, having correct perceptions regarding the universe. A map that matches the territory.

Under this ethical system, the above statement makes less than no sense. The most likely outcome is failure... but you shouldn't predict failure?

However, the way startup financing is currently organized, a VC fund can shrug off a dozen miserable failures to chase the one Google or Intel.

The purpose of Hacker News is to advertise Y Combinator startups, such as 9gag. The purpose is not to act as a prediction market. In fact, since one of the major routes of of startup profitability is being purchased by another company, accurate predictions of value are contrary to Y Combinator's interests. Y Combinator wants valuations as high as possible.

Someone starting a new cable company in 2012 is very likely to fail. This is the correct prediction: it is the outcome with the highest probability.

But a new cable company which somehow isn't immediately crushed, would have an enormous customer base, and could potentially make billions and billions of dollars.

To someone steeped in the Bay Area Startup lottery culture, this isn't an insanely stupid idea at all. It's almost a safe bet. With the force of millions of dollars behind you, being right is irrelevant.


Posted by Samuel Bierwagen | Permanent link | File under: important, nerdery