Sunday, July 31, 2005

 

Fun with ActiveRecord

In my spare time, I've been playing around with the Ruby On Rails web application framework. It's really a lot of fun. I grew up using embedded SQL and MFC's ODBC wrapper classes to do database access, which were definitely not fun, so The Rails Way of accessing databases seems magical and wonderful.

To give myself a playground in which to explore the Rails framework, I've started a pilot's logbook web application. It has an appearance and functionality similar to that of LogShare, but has a few extra features (like recording of instruction and rental costs). After doing all the typical Rails scaffold-generation-and-tweaking to get a few CRUD screens working, I decided that rather than re-entering all my logbook information manually, I'd rather just import the data from my existing LogShare account. LogShare provides an "Export Your Logbook" feature that dumps all the data into a comma-separated or tab-separated file. So I dumped the data and then started figuring out how to write a utility that uses the Rails ActiveRecord class to insert all this data into my database.

The ActiveRecord class, which provides an object-relational mapping (ORM), can be used independently of the rest of the Rails framework, so it makes sense to use it for a simple database-loading utility. The first thing I did was write a simple test program to verify that I knew how to connect to a database and access its tables. Here's what I wrote:

require 'rubygems'
require_gem 'activerecord'

# Connect to MySQL 'flightlog_development' database
database_spec = {
  :adapter  => 'mysql',
  :host     => 'localhost',
  :database => 'flightlog_development',
  :socket   => '/usr/local/mysql/run/mysql_socket',
  :username => 'kdj',
  :password => 'mypassword'
}
ActiveRecord::Base.establish_connection database_spec

# Mapping for 'airplanes' table
class Airplane < ActiveRecord::Base
end

# Mapping for 'flights' table
class Flight < ActiveRecord::Base
  belongs_to :airplane
end

# Print info for each record in the 'flights' table
flights = Flight.find :all, :order => 'occurred_on'
flights.each { |f|
  puts "#{f.occurred_on}\t#{f.total_hours}\t#{f.airplane.tail_number}\t#{f.route_of_flight}"
}

The MySQL database I'm using for this app is called "flightlog_development". There are a few parameters I have to pass to the establish_connection function to get connected to that database.

My database has a table called "flights" that contains the data for each logged flight, and a table called "airplanes" that contains the data for each airplane I fly. To get object-oriented access to the contents of these tables, all I have to do is declare classes called "Flight" and "Airplane" descended from ActiveRecord. Note that I don't have to specify anything about how these classes and their attributes get mapped to the database tables; the framework automatically figures out the table names from the names of the classes, and will automatically map database columns to instance attributes.

The only thing the framework can't figure out for itself is the relationship between the two tables. The flights table contains a column called "airplane_id" which is a foreign key into the airplanes table's "id" column. All I have to do is add the belongs_to declaration to the Flights class, and the framework handles the relationship automagically. (I could also add "has_many :flights" to the Airplane class so that the framework could handle the relationship in the other direction, but it's not necessary for this application.)

So, with all that stuff declared, it is pretty easy to query the Flights table and show its data. The expression "Flight.find :all, :order => 'occurred_on'" creates a list of Flight instances corresponding to the rows in the table, sorted by date. For each row, I print a few attributes. Note the use of "f.airplane.tail_number", which causes the framework to automatically query the database for the Airplane instance associated with the Flight's airplane_id. I didn't have to write my own SQL join. Cool.

I really like this minimal-coding approach. All you have to do is follow a few naming conventions, and the framework handles all the mapping for you. I know many programmers hate it when there is too much "magic" like this, but I believe that it really can boost one's productivity. (Yes, the Kool-Aid tastes good. I'd like some more, please.)

OK, so I wrote a little program that accessed the database. Now I just needed to figure out how to parse the LogShare data, transform it as necessary, and insert it into my database. Here's what I ended up with:

# This program reads lines from a tab-separated file generated by the
# 'Export Your Logbook' operation from http://www.logshare.com, then
# imports the data into the flightlog database.

require 'rubygems'
require_gem 'activerecord'

# Connect to MySQL 'flightlog_development' database
database_spec = {
  :adapter  => 'mysql',
  :host     => 'localhost',
  :database => 'flightlog_development',
  :socket   => '/usr/local/mysql/run/mysql_socket',
  :username => 'kdj',
  :password => 'mypassword'
}
ActiveRecord::Base.establish_connection database_spec

# Use the database's 'airplane_types' table
class AirplaneType < ActiveRecord::Base
  # Find an airplane type with the given name
  def self.find_by_name(name)
    AirplaneType.find :first, :conditions => [ 'short_name = ?', name ]
  end

  # Create a new airplane type with the given name
  def self.create_new_airplane_type(name)
    airplane_type = AirplaneType.new
    airplane_type.full_name = airplane_type.short_name = airplane_type.abbreviation = name;
    if airplane_type.save
      $stderr.puts "Created new airplane type \"#{name}\""
    end
    find_by_name(name)
  end
end

# Use the database's 'airplanes' table
class Airplane < ActiveRecord::Base
  # Declare relationship between Airplane and AirplaneType
  belongs_to :airplane_type

  # Get the Airplane instance with the specified tail number
  def self.find_by_tail_number(tail_number)
    Airplane.find :first, :conditions => ['tail_number = ?', tail_number]
  end

  # Create a new Airplane instance with the specified tail number and type
  def self.create_new_airplane(tail_number, airplane_type_name)
    airplane = Airplane.new
    airplane.tail_number = tail_number;
    airplane.airplane_type = AirplaneType.find_by_name(airplane_type_name)
    if ! airplane.airplane_type
      airplane.airplane_type = AirplaneType.create_new_airplane_type(airplane_type_name)
    end
    if airplane.save
      $stderr.puts "Created new airplane \"#{tail_number}\" of type \"#{airplane_type_name}\""
    end
    find_by_tail_number(tail_number)
  end
end

# Use the database's 'flights' table
class Flight < ActiveRecord::Base
  # declare relationship between Flight and Airplane
  belongs_to :airplane
end

# Convert MM/DD/YY to YYYY-MM-DD
def import_date_to_sql_date(date)
  date =~ /(..)\/(..)\/(..)/
  "20#{$3}-#{$1}-#{$2}"
end

# For a given input line, extract the data, transform it as necessary,
# and insert it into the database
def import_line(line)
  # fill the params hash with data to be inserted into the database
  params = {}

  # break up the tab-delimited line
  columns = line.split("\t")

  occurred_on                         = columns[0]
  airplane_type_name                  = columns[1]
  tail_number                         = columns[2]
  params[:route_of_flight]            = columns[3]
  params[:total_hours]                = columns[4]
  params[:day_landings]               = columns[5]
  params[:night_landings]             = columns[6]
  params[:actual_instrument_hours]    = columns[7]
  params[:simulated_instrument_hours] = columns[8]
  approaches                          = columns[9]
  simulator_hours                     = columns[10]
  params[:night_hours]                = columns[11]
  params[:cross_country_hours]        = columns[12]
  solo_hours                          = columns[13]
  params[:pilot_in_command_hours]     = columns[14]
  second_in_command_hours             = columns[15]
  params[:dual_received_hours]        = columns[16]
  dual_given_hours                    = columns[17]
  remarks                             = columns[18]

  # find or create the Airplane for the given tail number
  airplane = Airplane.find_by_tail_number(tail_number)
  if ! airplane
    airplane = Airplane.create_new_airplane(tail_number, airplane_type_name)
  end
  params[:airplane] = airplane

  params[:occurred_on] = import_date_to_sql_date(occurred_on)

  # strip opening and closing quotation marks from 'remarks' column
  params[:remarks] = remarks[1..(remarks.size - 3)]

  # logshare.com doesn't record number of takeoffs, so let's assume that
  # our number of takeoffs will equal our number of landings
  params[:day_takeoffs] = params[:day_landings]
  params[:night_takeoffs] = params[:night_landings]

  # create the new record and save it to the database
  if ! Flight.new(params).save
    $stderr.puts "Unable to save flight #{params[:date_occurred]} #{params[:route_of_flight]}"
  end
end


# MAIN PROGRAM STARTS HERE

# skip the first line of input, which contains column headings
gets

# process each remaining line of input
while gets
  import_line $_
end

Obviously, this is a little longer than the previous program, but the database access is not much more complicated. In addition to the flights and airplanes tables I mentioned earlier, there is a table called "airplane_types" that contains the data for each particular type of airplane. For example, there is an airplane_types row for a Piper Warrior, and another row for a Cessna 172. The airplanes table has a column "airplane_type_id" that is a foreign key to airplane_type's "id" column. So I declared a class AirplaneType, and added "belongs_to :airplane_type" to the Airplane class so that the framework would handle the relationship.

For each line read from the imported file, I create a new Flight with the appropriate values and save it to the database. If a particular Flight refers to an airplane that doesn't already exist in the database, then a new Airplane is created, and if that Airplane refers to an airplane type that doesn't already exist, then a new AirplaneType is created. So starting with an empty database, I can run my utility and get all my tables populated.

So there it is: a simple utility that uses ActiveRecord. This Rails stuff is fun. I really wish I could get somebody to pay me to do stuff like this, instead of struggling with coin acceptors.


Friday, July 29, 2005

 

Ground Lesson #13

There was an 800-foot ceiling when I got to the airport, so no flying today. I spent the time with the instructor reviewing aircraft systems, using a software package that gave demos of all the systems in a Piper Warrior III. (I fly the Piper Warrior II, but they are similar enough.)

The software was helpful. Previously, I've only studied simplified idealized drawings of the systems in books, or looked and poked at dirty immobile systems while preflighting the plane, so watching the animated 3D stuff filled in some of the gaps in my understanding. And I forget all this stuff every few months, so review never hurts.

I've now had 52 scheduled lessons, and of those I have had to settle for a ground lesson 13 times due to weather or a TFR. That's exactly one out of four. I hope I don't continue to suffer a 25% scrub rate; as summer winds down I hope things will get better.

Total cost: $114.


Thursday, July 28, 2005

 

Reaching Hidden Folders in Mac OS X

It's surprising that I hadn't stumbled on this problem before, but today I wanted to use the Finder to browse the contents of my /usr/lib directory, and discovered that the Finder didn't want to show me the /usr directory. It's one of those UNIX-y things that most people shouldn't mess with, so Apple doesn't let you see it. When people who know what they're doing want to mess with the contents of such directories, they are generally using the Terminal command interface, not the Finder.

But I did want to see that stuff in the Finder, so I did some Googling to figure out how. Here it is:

  1. From the Finder's Go menu, choose Go to Folder....
  2. Enter "/usr" in the text field, and click the Go button (or press the Enter key).
  3. The /usr directory will now be visible. Drag its icon to the Finder window's sidebar to make it easy to get to in the future.

Of course, you can use the same technique to get to /var, /tmp, /dev and other such directories.

Another tip I found during my searching was that from a standard Open... dialog, you can press the "/" (slash) key to bring up that same Go to Folder... entry field. However, this doesn't work in all applications. The first two I tried were Firefox and Emacs, and the tip doesn't work in either of those apps. It does work in TextEdit.


Wednesday, July 27, 2005

 

Flying Lesson #39

Due to haze, visibility was only three miles today. I couldn't fly solo in that visibility (and I wouldn't want to if I could), but the instructor and I made a brief flight into the practice area to do some steep turns, slow flight, and an engine-out simulation, so that the instructor could judge whether my previous solo practice did me any good.

Things were difficult in the reduced visibility. Without being able to see the horizon, judging bank and pitch angles was impossible without looking at the instruments. Even flying straight and level was tough, because I couldn't see anything over the nose except haze. But it was good experience.

A front is passing through in the next couple of days, so I'm hopeful the weather will be better in the coming week.

Logged for today: 1.2 hours dual in N4363D, with one takeoff and one landing. Cost: $263.


 

The Left-Knee Mystery

Every one of my pairs of jeans has a worn spot on the left knee. I've been puzzled by this for a while. I don't notice myself kneeling down on that knee. I can't think of any activity that causes rubbing against that one spot.

Since I started flying, the toes of my shoes are getting worn, but that is easily explained by the use of rudder pedals. I do kneel on my left knee when doing a preflight on the plane, but I wouldn't expect so much wear on my pants from just doing that.


 

OK, Now We Really Do Have Software Problems

I'm sure readers are on the edges of their seats waiting for more information on my coin acceptors, so here's the latest.

The hardware people did some diagnosis on the failing units, trying to determine why the fuses were blowing on the coin acceptors. It turns out that when the device is powered up, there is a capacitor that initially draws a lot of current. Our one "magic fuse" that doesn't blow is a slow-blow fuse, meaning that the current needs to exceed the fuse's rating for a "long time" (several milliseconds) before it will blow. For some reason, the vendor installed quick-blow fuses in this batch of units, so that initial inrush of high current caused them to blow immediately.

The hardware guys sent their findings to the vendor. The vendor sent us a replacement circuit board that is fuseless. I was asked to test this new board, but unfortunately, the new board has a power connector which is different from the old one, so I can't just plug it in to my machine in place of the old board. So I'm waiting for the hardware guys to give me new wiring.

With the hardware issues off my plate, I finally had time to look into the issues that might be software-related. There were three problems found during testing:

  1. The coin acceptor won't accept any coins until the machine has been on for "a while" (about a minute).
  2. The first coin that is accepted after power-up will not be credited to the customer. (That is, the machine takes the money but doesn't let the customer buy anything.) The second coin and subsequent coins are credited properly.
  3. Even when it is accepting coins, it is not very reliable. A customer often has to insert a valid coin over and over again before the machine will finally take it.

So, it's my job to figure out what's wrong. The code that deals with the coin acceptor was written by another programmer, who is no longer with the company. A few weeks ago, I was assigned to figure out how the stuff works and to fix whatever is wrong with it. I got it basically working, but now I'd need to delve deeper to figure out these uncovered problems.

Reading code written by someone else can be challenging. Programmers write software for two "audiences:" the computer and other programmers. Obviously, the code must make sense to the computer, or it won't work and is therefore useless. However, it is also important to write the code such that other programmers can read it, so that those other programmers can fix bugs or add new features. What makes sense to one programmer can be complete gibberish to another. With complex code, it's necessary to get into the other guy's head to really understand it. It is often easier for a programmer to completely rewrite a program from scratch than it is to figure out how someone else's existing program works.

Fortunately, the guy who initially wrote this stuff wasn't a bad programmer, and it wasn't too hard to figure out how everything worked. I found a solution to issue #1 (no coins accepted for a while) pretty quickly: at startup, the machine is doing a lot of things, and the coin-acceptor initialization code didn't run until a lot of those other things finished. I changed it so that it wouldn't wait for those other things.

Issue #2 (first accepted coin is ignored) took a litle more time. It turns out that after the coin acceptor is powered up, the first query for its status from the machine will result in a different kind of response than the typical response. The machine's software also had a special way of dealing with the first response, and the interaction of these two types of special startup handling caused the first coin-acceptance event to be ignored. The original programmer never noticed this during development because we usually don't power-down the machines very often, and so the special coin-acceptor-power-up behavior wasn't seen very often.

The solution to issue #3 (valid coins not accepted reliably) took some experimentation. My initial guesses were that the problem was due to (a) the machine was not instructing the coin acceptor to accept coins properly, (b) the machine was not querying status fast enough, or (c) the coin acceptor's default security level was too high. I'll explain each of these.

When a coin acceptor is initially turned on, it will reject all coins. The machine must send it a series of commands to enable acceptance of coins. I was able to eliminate possibility (a) by observing the commands being sent to the coin acceptor at startup and verifying that they were correct.

After the coin acceptor is commanded to accept coins, the machine must query the coin acceptor at least once per second to check whether coins have been inserted. If this query does not happen at least once per second, the coin acceptor will assume that the machine is no longer working properly, and will stop accepting any coins. I was able to eliminate possibility (b) by observing the queries, which were reliably happening three or four times per second.

So that left possibility (c). Coin acceptors provide a set of security options that determine how strict they are in determining whether an inserted coin is valid. They can be set at lower security levels, which accept coins readily, but make it more likely that counterfeit coins can slip through, or they can be set at higher security levels, which filter out the counterfeits but also reject valid coins that aren't quite perfect. High security levels can be frustrating for customers, but low security levels get us in trouble when the retailers find counterfeit coins in the machine at the end of the day.

I wrote code to send the "query security level" and "modify security level" messages to the coin acceptor, and experimented with some of the settings. Setting a low security level did indeed make the coin acceptor accept coins more readily, which was good because if that wasn't true, possibility (c) wouldn't have been the problem and I'd have to stay late trying to imagine more possibilities.

But now what should I do? I could have the machine set a particular security level that worked well for me, but that security level might not be appropriate for other machines. I could read the desired security level out of a configuration file, which makes it possible for onsite technicians to tweak things as needed, but techs don't like futzing with configuration files. I could also add some nifty user-interface screens allowing a technician to adjust the security settings by clicking some buttons, but that would take a day or two of work, and this project is already behind schedule.

I decided to not decide. I sent an e-mail to the higher-ups presenting the options, asking them what they wanted. On Thursday, I'll see what the answer is.


Sunday, July 24, 2005

 

Flying Lesson #38: Solo Practice

Today I finally got to fly on my own up into the practice area. This felt more like a "first solo" than those three measly touch-and-goes I did a few weeks ago. Today I decided for myself where I wanted to go, and figured out how to get there. It was a lot easier to concentrate on what I was doing without an instructor talking to me all the time. I felt a lot more like a "real pilot" than I ever have before.

I flew N9127F, the plane that I don't like. But it didn't seem so bad today. Without the instructor sitting next to me, I was free to spread out my knees so that the yoke didn't bump against the tops of my legs when I turned it. The DME (Distance Measuring Equipment) in 27F doesn't work reliably, but I knew where I was at all times, so it didn't matter. It doesn't have a clock on the dashboard, so I had to use my watch. (It actually does have a GPS unit with a clock, but I haven't learned how to use that.)

My mission for today was to practice slow flight, steep turns, and turns around a point. I stayed within sight of GA 400 so that I wouldn't get lost (visibility was only about five miles), and did most of my flying over a familiar area near my apartment. I started with the slow flight and a couple sets of steep turns, then descended down to 1,000 feet above ground level to do turns around a pair of water towers. After doing three sets of left and right turns around the towers, I climbed back to 3,500 feet and did some more slow flight and a lot of steep turns. I think I've got a good sight picture for the steep turns, although I'm looking forward to a day where I can actually see the horizon.

Returning to the airport, visibility was too poor to see the runways from the reporting point, but I was able to follow the highway until I saw the big tank farm that's north of the field, then I pointed in the general direction of the airport. I used the VOR to double-check my course, but didn't want to lean on radio navigation. The runways became visible when I was about four miles out.

Logged today: 1.9 hours solo/PIC in N9127F, 1 takeoff and 1 landing. Cost: $203.


Friday, July 22, 2005

 

Ground Lesson #12: Tower Tour

I went to the airport hoping I'd finally get to make a solo flight in the practice area. The weather looked promising. Unfortunately, I couldn't fly due to an entity even more menacing than the weather: George W. Bush.

The President was in town today to tell everyone about his Social Security plan, so there was a Temporary Flight Restriction in place. When this happens, nobody can fly within a 10-nautical-mile radius of the President except for regularly scheduled commercial flights, emergency aircraft, and law-enforcement aircraft. PDK is within that radius, so the airport was effectively closed.

The instructor called the control tower to see whether we could get a tour. The controllers weren't busy (nobody was flying), so they let us in. We had to present photo ID and get visitor badges, then we were escorted up. There is an elevator that goes part of the way up, then we climbed a few flights of stairs.

The room at the top of the tower was smaller than I expected. There were three controllers on duty. They pointed out various pieces of equipment to us and answered our questions, but without any planes in the air, we really didn't get to see how the tower functioned. We did get to see them record an ATIS broadcast (starting with "Notice to Airmen: There is a Temporary Flight Restriction in place. . ."). One annoying pilot called in every five minutes to ask if there were any updates to the President's plans and whether the TFR might be lifted early. One of the other controllers commented that in a similar situation, she had told the pilot to turn on the TV and watch the local news to keep abreast of the President's position. My instructor took the opportunity to ask the controllers about a few procedural issues, like "When I'm told to 'hold short' over on that taxiway that has no hold-short line, where do you really want me to stop?"

It was nice to meet them, to associate some faces with the voices we always hear. They suggested that we return some day when they were actually doing some work.

We returned to the flight school and did some ground work. We weren't being very productive, so the instructor charged me for only an hour and let me leave early.

I scheduled an airplane for Sunday afternoon, when good weather is expected, so I hope I'll finally get to make that first solo flight away from the airport then. Any US Presidents reading this, please stay away.

Today's cost: $57.


 

Extending Daylight-Saving Time

From http://www.cnn.com/2005/POLITICS/07/22/congress.daylighttime.ap/index.html:

"The beauty of daylight-saving time is that it just makes everyone feel sunnier," said [Rep. Edward] Markey.

Also:

According to some senators, farmers complained that a two-month extension could adversely affect livestock . . .

I wasn't aware that sheep and cattle cared about time zones, but they will just have to move their clocks back the same time the rest of us do.


Wednesday, July 20, 2005

 

Flying Lesson #37: Short and Soft Landings

Today we practiced short- and soft-field landings at an actual short field and an actual soft field. It was a lot more fun than practicing those techniques on a long paved runway.

The instructor had me come in early for today's lesson, so that we'd have time to fly to these airstrips which are both south of Atlanta (my home airport, PDK, is north of Atlanta). We couldn't fly directly to them, as we had to fly around the inner cylinder of the Atlanta Class B airspace. We had to fly at 3,000 feet to avoid the overlying Class B shelf.

Berry Hill Airport (4A0) has a 3000-foot runway, but it has a 995-foot displaced threshold at one end, and a 597-foot displaced threshold at the other end, so the usable landing area is only 1400 feet. It has trees at one end and power lines at the other, so it's a little scary. The runway is only 40 feet wide, which probably doesn't seem narrow to a lot of pilots, but most of my landings have been on a 150-foot-wide runway, so this one seemed pretty tiny. The narrow runway is flanked by tall trees on both sides, reminding everyone who flies there of the Death Star trench from Star Wars. My first two approaches were too high and too fast, so I had to abort those landings. I was too shy of the trees, and was afraid to get too low. The instructor demonstrated a landing for me, and then I did one without any problem. The takeoffs were a little nerve-wracking, with treetops seeming to pass just a few feet beneath the plane, but I know were weren't in any real danger.

Rust Airstrip (3RU) is a 2750-foot-long turf strip. I enjoyed the landings on this strip. The impact with the ground was nice and soft, and then we got to do some off-roading in the plane. We landed once in each runway direction. One of the approaches is over a lake, and the other is over a large wooded area, so the scenery was nice.

When we got back to PDK and parked the plane, we had to pick grass out of the landing gear. The bottoms of the wings behind the wheels were caked with mud, and the tips of the propeller were green. Fortunately, I'm not responsible for cleaning the plane.

Logged today: 2.3 hours dual in N4363D, with 4 takeoffs and 4 landings. Cost: $441.


Monday, July 18, 2005

 

It Still Must Be a Software Problem

At the end of our last episode ("It Must Be a Software Problem"), we had machines with coin acceptors with blown fuses, and a promise from the hardware people to get to the bottom of that problem. When I arrived at work this morning, I found that all three test machines had coin acceptors with lit LEDs. Nobody told me what happened, so I assumed that magical elves had fixed everything while I was away. I wanted to test the machines to verify that everything worked, but I got dragged into a few meetings.

Around 4:30, the project manager stopped by and let me know that the machine up in the QA lab was not accepting coins. He first talked to the hardware guy, who assured him the hardware was in working order. Would I take a look?

I trudge upstairs to the QA lab and talk to the QA guy. He says that coins aren't being accepted. So I put a coin in, and guess what? The machine swallows it up. The coin has been "accepted" and has fallen into the coin bin inside the machine, just like it's supposed to do. So what's the problem? Well, the machine is "accepting" the coins, but it is not crediting the customer with the money. The coin just disappears, and the display still shows "$ 0.00" (actually, it shows "€ 0.00" instead of dollars, but some browsers still can't display the Euro symbol, and I don't want to answer questions about "that funny-looking 'e' thing").

I know that this machine has been through a lot over the past couple of days, so I decide to reboot it. After rebooting, coins still get swallowed without credit. Grumbling, I walk toward the stairs to see if I can get my development machine to exhibit the same behavior, when the QA guy stops me. The machine is now giving credit! We run a few coins through, and they are all working now.

It's nice that it's working, but I need to figure out why it takes a while before the coin acceptor starts working. I go back to my cubicle and fire up my development machine. It is not accepting any coins; they just keep falling out the coin return. I reboot a few times, but still no good.

I go to the third machine. It accepts coins and provides credit the first time I try. It works perfectly.

So now, before I can work on the main problem (figuring out what the QA machine is doing what it's doing), I first have to fix my development machine. I immediately expect a hardware problem, as this machine worked fine before the magical hardware elves "fixed" it, but I have to put on my prosecutor-gathering-evidence hat. Besides, the hardware guys all go home at 4 o'clock, so they won't be around to help me anyway.

The first thing I do is a casual check to verify that all the wires are hooked up. It looks like they are, although some of them go deep into the machine where I can't really see them without disassembly. So I modify my software so that it reports in detail all the communications between the main processor and the coin acceptor. I find that the processor is sending messages out, but is getting no replies back. I look at the coin acceptor, and its little LEDs are blinking when messages go back and forth, so it looks like the coin acceptor thinks it is sending replies back to the processor.

So, either the signal isn't getting back to the processor, or I've done something dumb with the software that is hiding the replies. I rebuild the software, install it on the third machine, and test there. On that machine, I see two-way communications. Conclusion: the software is good; the problem is with the return communications link.

I wanted to avoid disassembling the machine, but now that's the only choice. I take things apart, occasionally running back and forth between my machine and the working machine for comparison. After several minutes, I find the problem. The I/O cable consists of three wires, and two of those wires have come loose from the connector on one end. And unfortunately, there are seven possible holes where these two loose wires could go, so I have to run back and forth a couple of times to see how the other machine is hooked up.

I put everything back together, and now my machine works. So that problem is solved, but I still have the original problem (why doesn't the QA machine work?). I decide to call it a day and look at the remaining problem tomorrow. My final act is to send the hardware guys a friendly e-mail requesting that they provide a more solid cable so that these wires won't get loose again. (I don't expect them to provide one, but I want the issue documented.)

Today I spent about two and a half hours tracking down some loose wires.

People ask me why I never get anything done.

I still don't think it's a software problem.


Sunday, July 17, 2005

 

Flight Training Costs

Many readers of my blog are amazed by, as one put it, the "mind-numbing amount of money" I'm spending on my flight training. I'm afraid to add it all up, but at roughly $300 for each flying lesson and $150 for each ground lesson, my 36 flying lessons and 11 ground lessons have cost me over $12,000 so far. And I've still got a way to go.

(Egads! Over twelve thousand dollars so far!?! I've never estimated it before.)

My father was recently talking to one of his neighbors who is a private pilot. That guy found out about the high costs of training at PDK, and had decided to go through a "cheapee" training program somewhere else. My parents are glad I'm going through a non-cheapee program, and so am I.

Even so, flight training is taking more time and more money than I expected when I started. Every few weeks, I re-evaluate the situation. Should I keep spending my excess income on this, or should I do something else with it? Should I switch to a less-expensive flight school? Is my flight school screwing me?

I keep coming to the conclusion that it's best to stick with what I'm doing. Yes, it's expensive, and I knew that when I started. I knew I was choosing the most expensive option. This school does have some rules and procedures which increase the costs to students, but I'm convinced that safety and quality of instruction are the real reasons for those rules. I wasn't comfortable with the other flight schools I checked out back before I started, so I have no reason to believe that switching schools now would be a good thing. Even if I did, adding an hour to the commute to and from the airport would do more to discourage me than money ever could.

The bottom line is that the money is not really a problem for me, so I'm not going to worry about it. If I thought I could get equivalent-quality training nearby for less money, I'd do it, but if I were to switch to another school now simply to save money, I would never be comfortable with the decision.

Don't take this as criticism of pilots who train elsewhere. For people starting younger, or who have a natural affinity for flying, or who plan to immediately go on to additional training (instrument, commercial, multi-engine, etc.) a faster cheaper private-pilot training course makes a lot of sense. If I have to spend twice as much as everyone else to get what I want out of my training, that's my problem, and nobody else's.

Those of you considering flight training should not use my experience as a guide. Costs vary widely. When I first started investigating, the estimates varied from $6,000 to $15,000. Most estimates were in the $8K-$12K range. Beware of the schools that advertise a small fixed price ("Get a Private Pilot License for only $6995!"). I checked a couple of those schools, and quickly figured out that the real costs would be much higher than the advertised costs. The school I chose was the only one that was honest about the fact that it would cost a lot, and that they were more concerned with making me a good pilot than in just getting me ready for the checkride.

With the benefit of hindsight, I know a few little things I could have done differently to save myself some money:

I'm fortunate in that I make a good living, so minimizing costs was not a high priority. I went with the school that I trusted most after talking with the staff and going on a discovery flight. I'll never know if it really was the best choice, but it's certainly a choice I can live with.


 

Wolfgang Puck Self-Heating Latte

While walking through the grocery store today, I noticed a display of the new Wolfgang Puck Gourmet Rich Espresso Latte. This is a self-heating serving of coffee. I saw these mentioned on Slashdot a few weeks ago. Like many, I think it's obscene to make this much environmental impact just to get a hot cup of coffee, but as a gadget freak I really wanted to try it out. So I bought one (Mother Earth, please forgive me).

The instructions are pretty easy to follow, although I had problems. Step 1 is to turn the can upside-down and peel-off the cover, which was not a problem. Step 2 was to push down the exposed button until "colored water drains into the activation chamber." Well, I pressed down as far as it would go, but didn't see any colored water draining. So I spent a couple of minutes trying to figure out what I'd done wrong, and trying various kitchen tools to press down harder, when I noticed that the can was getting hot. So I guess I did the right thing, but just couldn't verify it.

After waiting about seven minutes, I shook the can well and then opened the pop-top. This was problem #2: shaking a can of hot liquid and then popping open the top causes hot liquid to squirt out onto one's fingers. It wasn't hot enough to cause pain, but I did have to wipe off my hands, the countertop, and the neighboring wall. Performing this operation in a moving car might be a bad idea.

Finally, I got to drink it. It wasn't bad, although it was a little sweet for my taste. The taste reminded me of Yoo-hoo. It stayed hot for the ten minutes or so that I drank it. The can only contains 10 oz. of liquid. With all the magic self-heating stuff in it, the can seemed to weigh about the same empty as it did when full.

My recommendation: Don't buy this. Buy a Thermos instead.


 

Silly Hats

I'll put aside my typical whining to focus on something important: my nieces. That's Hannah (almost 5) on the left, and Sarah (just turned 3) on the right. Behind them is their Uncle Steve (my brother). They are all modeling fabulous hats.


Friday, July 15, 2005

 

Flying Lesson #36

We had another day of thunderstorms forecast, but a call to the weather briefer indicated that we wouldn't have any precipitation for a few hours, so I got to fly.

Today's lesson in the curriculum was really just a lot of review of stuff I've already done. This lesson is supposed to come after a couple of solo flights, so that the instructor can determine whether my practicing is making me better or leading me to develop bad habits, but I haven't been able to make any solo flights in the practice area yet.

The cloud ceiling was at around 3,300 feet, but the instructor found a large hole in the clouds and decided we would fly in there. We considered climbing up above the cloud layer, but the danger is that a hole can close and then a VFR pilot has no way to get down, so we stayed within the hole at around 4,500 feet. We basically just flew back and forth, practicing the various kinds of stalls. Unfortunately, a couple of other pilots found Our Hole, and shared our opinion that it was a good place to practice, so I got some collision-avoidance practice in addition to the cloud-avoidance practice.

After doing that for a while, we descended to do some ground-reference maneuvers. On the way down, the instructor pulled the power. I did a fairly good job of handling the simulated engine-out. When we got down to my landing site, it turned out that the field was a little shorter than I had thought, but it had an uphill slope, so I probably could have landed safely.

After some S-turns over GA 400, we headed back to the airport. Radio traffic was crowded on the way in, as some confused yahoo had just misheard a radio call for another plane and taxied across a runway without permission (this is known as a "runway incursion"), so the controller was busy sorting everything out. I made a harder-than-desired landing on runway 20L, because that runway is narrower than the runway I usually land (20R), and so I thought the plane was higher than it really was. I make this same mistake every time I land on that runway; I hope I'll learn the lesson someday.

The thunderstorms finally hit as I was driving away from the airport.

For today: 1.8 hours dual in N4363D, with 0.3 hours hood time and 1 takeoff and 1 landing. Cost: $331.


Thursday, July 14, 2005

 

It Must Be a Software Problem

"Kris, your coin acceptors are not working. Did you bother to test this stuff at all before releasing it?"

That was, in essence, the first e-mail that greeted me at work today. The background: I am working on the software for a vending machine. Earlier versions of the vending machine only took paper bills, but we are going to be rolling this product out to Europe, so the machine has to be able to take Euro coins. A "coin acceptor" is the device in a vending machine that has the slot where the customer inserts money. The coin acceptor either "accepts" the coin, meaning it drops into a bin inside the machine and the machine bumps up the customer's credit, or the coin acceptor "rejects" the coin, meaning it falls back out the return slot. The CPU in the machine communicates with the coin acceptor via a serial line to tell the acceptor which coins to accept, and to find out when coins have been accepted. We recently released a version of the vending-machine software that makes the coin acceptor work, but the testers couldn't get the coin acceptor to accept any coins.

The coin acceptor does work on the machine I used while developing the software, and I tested the coin-acceptor operation extensively and successfully on that machine. However, the coin acceptors were not working on two recently delivered machines, one of which went to the testers and the other went to another software developer. We discovered this problem on Tuesday, took a quick look at the hardware, and noticed that on my (working) machine, there was an LED on the coin acceptor that lighted up when the power was turned on, and other LEDs that flickered when communications were taking place, whereas on the other (non-working) machines, the LEDs never lit up at all. I concluded that the coin acceptors probably weren't getting power, and asked the hardware people to verify that everything was hooked up correctly.

Problems like this are common when we have prototypes of new hardware, so it doesn't bother me much when the hardware doesn't work. But yesterday, without even looking at the machines, the hardware people replied "It must be a software problem." So that's why the ball was back in my court this morning.

"It must be a software problem" is something we hear a lot from the hardware group. Similarly, we often hear "it must be a client-side software problem" from the server-side software development group. Both groups are usually wrong when they make those claims. That's not because the client-side software developers never make mistakes; it's because all problem reports come to us first, and we check things very thoroughly before pointing the finger in another direction. So, when we get this kind of response, it really pisses us off.

I knew I'd have one of those days were I spend several hours proving to somebody else that it is their stuff that's wrong, not mine. I've gotten very good at gathering evidence and presenting a case; I feel like a criminal prosecutor at times like this. I don't like participating in this adversarial system, but it's the only mechanism that seems to work.

To me, it seems obvious that if the power light doesn't turn on, then the power isn't connected. I don't know of any software I can write that will spontaneously produce a flow of electricity through an external electronic device, so there wasn't much I could do as a software developer. I decided to start by firing off an e-mail describing the problem in simple terms and asking for a reasonable amount of help: "Dear hardware guys, I obviously a clueless ninny, so could you please provide me any technical contact information you have for the coin acceptor vendor? Also, because the power light does not turn on when we turn on the power, could you send somebody over to check the wiring?"

From past experience with the hardware people, I didn't expect any help for a while (they work in another building), so I started taking apart the machines. I figured I would swap the coin acceptor from the "good" machine with the one in the "bad" machine. That should provide a clue as to whether the problem was in the coin acceptor or somewhere else in the machine. So I grabbed my Phillips-head screwdriver, my narrow slot-head screwdriver, my needle-nose pliers, and started unscrewing and unplugging things.

I was pleasantly surprised when a couple of the hardware guys walked in as I was almost finished removing one of the coin acceptors. They hooked up a multimeter to the "bad" machine, and found 24 volts of power across the pins, as expected. They also checked the serial data lines, and found them to be wired correctly as well. They asked me a couple of times whether I was sure I had the right software loaded, and I assured them that the software in the "bad" machine was identical to that in the "good" machine. I pointed out the unlit power light a couple of times, but they didn't seem interested. I sympathized with them—it really did look like everything was hooked up correctly.

So we walked over to the "good" machine, and they did the same checks that they had with the other machine. This was an important step, because after turning the machine on and off a few times, they noticed that the LED turned on and off along with the power. They started accepting the possibility that it might be a hardware problem. They decided that swapping the coin acceptors might be a good idea.

So we did some swapping and testing of various things, and the result was a unanimous conclusion that the "bad" machine had a "bad" coin acceptor. The power light just wouldn't turn on, no matter what they tried. But this raised a new mystery: there was another "bad" machine in the QA lab. One bad coin acceptor might be just bad luck, but two bad coin acceptors out of three seemed unlikely. So we took a long walk upstairs to the QA lab.

The third machine had the same dark power light that the other bad machine did. While they were checking things, I noticed something marked "FUSE" on the coin acceptor's circuit board. I didn't expect this to be the problem, but I asked whether it made sense to check the fuses to see whether they were good.

They checked, and whaddya know, the two bad machines' coin acceptors had blown fuses! So, OK, we just need to replace the fuses. Yeah, it's weird that two fuses would blow, but we'll just have to try and see what happens, right?

A few hours later, they came back with two replacement fuses, taken from some other coin acceptors that were in the warehouse. We replace the blown fuse in a coin acceptor, and turn on the power. The power light goes on. Hooray! Then, the light gets dim, and then goes dark. We check the fuse, and it's blown.

OK, there must be something wrong with this coin acceptor, causing it to blow fuses, right? Well, the thing is that this coin acceptor is the one that was originally the "good" coin acceptor. We had put its fuse in the "bad" coin acceptor, and that coin acceptor suddenly became a "good" coin acceptor. We've also tried two different machine cabinets, so we're pretty sure there's nothing wrong with the wiring in the cabinets. So we seem to have one magic fuse that makes any coin acceptor work, and three blown fuses.

The hardware guy puzzled over this for a while, said "Oh, what the hell?" and then put the second replacement fuse into a coin acceptor and turned it on. Again, we see light for a couple of seconds, and then darkness. So that's one good fuse and four blown fuses.

All of the fuses are rated for the same current. Did the vendor get a batch of bad fuses? Is the one "good" fuse too tolerant? Are we going to get these things fixed before our shipment deadline?

Nobody knows the answer yet, but I'm pretty sure it's not a software problem.


 

a lovely animation

jwz has an entertaining animated GIF in one of his postings: http://www.livejournal.com/users/jwz/511267.html

It reminds me of Don Hertzfeld's work. See http://www.bitterfilms.com/ for examples. I recommend "Rejected."


Wednesday, July 13, 2005

 

Ground Lesson #11

Thunderstorms again today. I've concluded that summer is a bad time to try to fly in Georgia. Mornings are usually foggy with low ceilings, and we have thunderstorms every afternoon. On the days when the weather is flyable, temperatures are in the high 90's with high humidity, so the plane doesn't fly well and the pilot loses a couple quarts of water to perspiration. I guess the bright side to this is that the weather will be getting nicer when I finish my training.

The instructor and I spent the time reviewing regulations, with focus on those related to airworthiness and equipment requirements. We went over some weird scenarios, like "If I were to cut off the tail of my plane, is there a regulation that says I'm not allowed to fly it?" Or "What if the stall-warning horn is not working. Are you allowed to fly, and if so, is there anything special you need to do to comply with regulations?" Or "Does any regulation require a pilot to perform a pre-flight inspection of the aircraft?"

Analyzing the Federal Aviation Regulations to answer these questions reminded me of playing wargames with my friends as a teenager. We spent more time trying to interpret the complicated and contractory rules than actually playing the games.

Cost: $131


Tuesday, July 12, 2005

 

Fixing Ruby on Mac OS X Tiger

Agile Web Development with Rails has a tip for using Ruby on Tiger: run the script from this page: http://tech.rufy.com/entry/46.

The tip seemed to work fine for me.


 

Thoughts Over Drinks

Some thoughts expressed by co-workers and myself over drinks (I won't identify who said what):

"I really don't want to go to work tomorrow."

"It's not a bad job, if all you want is a paycheck."

"I don't like this Heineken stuff. We should just do shots."

"Most people don't do anything, but they keep their jobs for some unknown reason."

"I really don't care about our products anymore. No matter what you do, somebody's gonna fuck it up."

"Fuck [our employer]. I hope more programmers quit. Fuck 'em."

"If I only had three months to live, I'd like to have some breasts in my face." (There was unanimous agreement on this statement.)

Most of these sentiments were expressed before the second round.


Sunday, July 10, 2005

 

Sarah's Third Birthday

My niece Sarah had her third birthday today. She seemed to have a really good time, which was a bit of a surprise. My young nieces often hate the process of opening presents in front of a bunch of picture-taking adults, but Sarah was really excited about all her gifts. I guess she has taken to consumerism more quickly than her older sister.

I almost laughed when, at Toys'R'Us, the clerk asked whether I needed a gift receipt. "No, I'm buying a Care Bears jigsaw puzzle and a Playskool barbeque grill for myself" leapt to mind as a possible response, but I kept that thought to myself.


 

Upgrades

This week, I finally made a few upgrades to a couple of the computers I have lying around the apartment. (This will probably not be interesting to anyone; I just wanted to make some notes in the blog.)

I have a three-year-old Sony Vaio PCG-GR300P that sits next to my couch. I use it to surf the web or play around with code when I feel like being mostly horizontal. It had a 1.13-MHz processor, 256 MB RAM, and 40-GB RAM, which were fairly impressive back when I bought it, but are not so good today. It should have been fine for the simple uses I had for it, but it wasn't up to those tasks. The problem was that it was almost constantly paging stuff between RAM and disk. So everything was slow (after clicking the Start button, it would take a few seconds before the menu appeared), and when unplugged from the power outlet the battery would only last about 20 minutes.

The solution: add RAM. It had only 256 MB, and one empty RAM slot, so I bought another 256 MB to bring it up to 512 MB total (the maximum allowed for this laptop). The laptop is now nice and snappy. The increase in performance was definitely worth the $70 I paid for the new RAM.

The other computer in question is a Dell Axim A5 handheld. It still had the original ROM. I bought a ROM upgrade to "Pocket PC 2003" a couple of years ago, but never got around to installing it for fear of losing my data. However, the handheld sat unused for a few months, so the main battery and backup battery both died, losing all my data. I tried restoring the data from a backup, but only got error messages about the backups being invalid. So, with nothing to lose, I installed the upgrade. It took about half an hour. After the upgrade, my wireless card wouldn't work: every time I plugged it in, I got an "Unrecognized card" error message. I installed the drivers I had before, but they were no good. After a while on Dell's support site, I found an updated driver for the Dell TrueMobile 1180 for Pocket PC 2003, and installing that did the trick.

Now I'll need to try to re-install all the applications and data I had before, but I'll leave that for another lazy weekend.


Friday, July 08, 2005

 

Airplane-Crash Journalism

A big pet peeve of pilots is the inaccuracy of reports about plane crashes. It seems that almost every report of a small-plane crash contains ridiculous elements, such as referring to the ill-fated aircraft as a "Cessna Warrior" (Warriors are manufactured by Piper), or as a "twin-engine Cessna 172" (172's have a single engine), or as a "Cirrus aircraft built from a kit" (Cirrus manufactures very expensive airplanes that are not kits). Eyewitness reports are often completely inaccurate, and the journalists who write the stories clearly know nothing about General Aviation.

To make the journalists' job easier, The Lazy Journalists Plane Story page makes it easy to fabricate a ridiculous story about any plane crash. It appears to be focused on lazy Australian journalists, but with a little tweaking, it should work for US air crash stories as well.


 

Flying Lesson #35: Engine-Out Practice

The weather was beautiful today. After several weeks of hazy hot muggy days, today we had over 10 miles visibility, and temperatures below 90. I was lucky to have my lesson today; the past few days have had terrible weather due to TS Cindy, and the next few days will have terrible weather due to Hurricane Dennis.

The instructor suggested that I go out and fly solo in the practice area today for the first time, but I decided I'd rather have some additional practice in one of my weak areas: engine-out simulations. This is something I did poorly in my stage check, and with which I have never felt comfortable, so I wanted to nail down what my problems were and fix them.

So, what does a pilot do if the engine quits? "Crash and die" is not the correct answer, but is unfortunately something that too many pilots do. Engine failures are very rare—most pilots never experience an actual engine-out—but knowing how to handle them is obviously worth studying and practicing. The procedures differ from airplane to airplane, but generally the procedures go something like this:

  1. Trim the airplane for best glide speed. This is the speed at which the airplane will glide the longest distance with no power. In the Piper Warrior, this speed is 73 knots.
  2. Pick a suitable place to land. Ideally, you'd want to lose your engine when an airport runway is right in front of you, but if it doesn't work out that way, you can pick a road, an open field, or any other area that looks landable.
  3. Check the engine instruments and try to get the engine restarted. In the Warrior, the procedure is to switch the fuel selector to a usable tank, check the ignition key position, check the primer knob, put the throttle in start position, turn on the fuel pump, set mixture to full-rich, and turn on carb heat. If the propeller has stopped windmilling, turn the ignition to START. If you have lots of altitude, you might also try each magneto independently or try switching to the other fuel tank.
  4. Communicate the emergency situation. Set the transponder code to 7700, make a Mayday call on emergency frequency 121.5, and turn on the ELT (emergency locator transmitter).
  5. When committed to making a landing, shut off the fuel, the ignition, and the master electrical switch. Make sure all seat belts and shoulder harnesses are fastened securely, and ask passengers to remove glasses and any sharp objects. Open the door so that it won't get stuck shut.
  6. Land the airplane as safely as possible.

The order of the steps is important. You want to establish best glide first, to give yourself as much time and as many landing options as possible. Then you need to figure out where you are going to land if you can't get the engine restarted. Then you want to start troubleshooting the problem with the engine before wasting time talking to people who really can't do much to help you. It's important to have the procedures memorized, as you will not have time to pull out the checklist in an actual emergency.

We've been doing simulated engine-outs throughout my flight instruction, and I have no problems remembering and executing all the steps of the procedures. What I do have a problem with is step 2: Pick a suitable place to land. Most of the sites I choose turn out to be bad ones, and when I do make a good choice, I screw up the approach such that I would undershoot or overshoot the site. In the event of an actual emergency, chances are that I would end up landing in trees.

So today, the instructor just kept cutting the throttle, watching what I did, and telling me what I did wrong or what I should have done instead. My first couple of attempts were terrible, but I did get better, so the advice and practice are paying off. I'm getting a better feel for how far and how fast the plane glides, what a good landing site looks like from a couple thousand feet in the air, and how to set up a good approach without power. With each scenario we practiced, things seemed to happen a little more slowly, and I was able to predict the results better. A little more practice, and I think I'll have it nailed.

In the meantime, my instructor has suggested that during my solo flights in the practice area, I stay close to Georgia SR 400, a long wide highway that runs from the airport up through the practice area. If I have an engine problem, I'll know exactly where to land. Staying close to 400 also has the benefit of making it almost impossible to get lost.

Logged today: 1.2 hours dual in N4363D, with 1 takeoff and 1 landing. Cost: $233.


Wednesday, July 06, 2005

 

Ground Lesson #10

Bad weather today, so we did some hypothetical flight planning, and reviewed weather services.

Cost: $131.


 

The Best of the Worst Cover Songs

Brian Ibbott's Coverville is one of my favorite podcasts. His show usually consists of good covers of good songs. However, in Coverville 101: The Best of the Worst Cover Songs, Brian changes course a bit and deliberately chooses humorously bad covers.

"Lucy in the Sky with Diamonds" performed by William Shatner and "In the Ghetto" performed by Eilert Pilarm (the "Swedish Elvis") are both excruciatingly bad and definitely worth a listen.

If you like really bad renditions of good music, I recommend listening to this particular episode. If you like good music, check out some of the other episodes too.


Monday, July 04, 2005

 

Things to Remember for the Checkride

Based on events during my stage check and later comments about it, here are some things I really need to remember for subsequent stage checks and the final checkride:


Saturday, July 02, 2005

 

Premature Optimization in Web Apps

This is a quote I want to save from an ASP.NET vs. Ruby on Rails comparison:

RoR encourages validation of business rules in the model, where it can be re-used effectively (read: where it belongs), whereas ASP.NET's validation controls seem to encourage developers to validate the rules in the UI. Surely, a case can be made for UI validation to avoid server roundtrips and workload, but I don't buy into it. My servers are running at 5% CPU utilization, and my developers are running at 105% utilization.

Friday, July 01, 2005

 

Flying Lesson #34: Stage Check

I had my stage check with the chief flight instructor today. I did a few embarrassingly stupid things, but the bottom line is that I "passed" and I will be allowed to fly solos in the practice area. The chief instructor gave me two pages of handwritten notes about things I need to improve. My regular instructor and I will be reviewing my weak areas in subsequent lessons.

So now it is on to cross-countries, night flying, and some more advanced flying skills. The next stage check will be just before my first solo cross-country.

I also had a lesson scheduled with my regular flight instructor, but the stage check ran an hour over, and by the time my regular instructor and I looked at the weather, it appeared that thunderstorms would be rolling in too soon for us to get any flying done, so instead we just stayed on the ground and started planning for a cross-country flight to Chattanooga.

Costs for today: $433 for the stage check with the chief flight instructor; $57 for the ground time with my regular instructor.


This page is powered by Blogger. Isn't yours?