Saturday, October 25, 2008

Apple sucks at XML

OK, I've officially had it with Apple. Steve Jobs may have style down cold, but his programmers were smoking something fierce when they designed the XML format for their so-called Property list. Don't let that Wikipedia page fool you on the apparent simplicity of the format. Take a look at one of Apple's own samples. Still not convinced? How about a real-world use-case: the emoticon definition file for an Adium theme, a portion of which is reproduced below:
<plist version="1.0">
<dict>
<key>AdiumSetVersion</key>
<integer>1</integer>
<key>Emoticons</key>
<dict>
<key>amazing.png</key>
<dict>
<key>Equivalents</key>
<array>
<string>=-o</string>
<string>=-O</string>
<string>:-o</string>
<string>:-O</string>
</array>
<key>Name</key>
<string>Surprised</string>
</dict>
<key>anger.png</key>
<dict>
<key>Equivalents</key>
<array>
<string>&gt;:o</string>
<string>:-@</string>
<string>:@</string>
<string>X(</string>
</array>
<key>Name</key>
<string>Angry</string>
</dict>
<key>bad_egg.png</key>
<dict>
<key>Equivalents</key>
<array>
<string>&gt;-[</string>
<string>&gt;-(</string>
</array>
<key>Name</key>
<string>Nervous</string>
</dict>
(...snip...)
</dict>
</dict>
</plist>
Do you see what the problem is? For those of you playing at home, here's a hint: how would you write an XPath expression to obtain the "equivalents" of a given image file?

Yes, it's not impossible to grab a value for a given key, but did they have to make it so hard when XML can express the same idea in a much easier format? Or, rather, did they have to be so lazy when writing the code that serializes these property lists to/from XML?

In any case, if you ever have the need to process an XML file created by an Apple program, the following stylesheet will (likely) help restore your sanity. Simply pre-process the XML with my stylesheet and then your XML code or stylesheet will be much easier to write (and read!):
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">

<xsl:output method="xml" encoding="utf-8" indent="yes" />

<xsl:template match="* | @* | node()">
<xsl:copy>
<!-- if the previous sibling is a 'key' element -->
<xsl:if test="name(preceding-sibling::*[position()=1]) = 'key'">
<xsl:attribute name="key">
<xsl:value-of select="preceding-sibling::key[position()=1]/text()" />
</xsl:attribute>
</xsl:if>
<xsl:apply-templates select="* | @* | node()" />
</xsl:copy>
</xsl:template>

<xsl:template match="key" />

</xsl:stylesheet>
For an example, let's take another look at the sample XML I showed earlier and compare that with the XML sexiness that is generated by applying my stylesheet against it (some spacing was added to the "after" version to better illustrate how they compare to each other):
BeforeAfter
<plist version="1.0">
<dict>
<key>AdiumSetVersion</key>
<integer>1</integer>
<key>Emoticons</key>
<dict>
<key>amazing.png</key>
<dict>
<key>Equivalents</key>
<array>
<string>=-o</string>
<string>=-O</string>
<string>:-o</string>
<string>:-O</string>
</array>
<key>Name</key>
<string>Surprised</string>
</dict>
<key>anger.png</key>
<dict>
<key>Equivalents</key>
<array>
<string>&gt;:o</string>
<string>:-@</string>
<string>:@</string>
<string>X(</string>
</array>
<key>Name</key>
<string>Angry</string>
</dict>
<key>bad_egg.png</key>
<dict>
<key>Equivalents</key>
<array>
<string>&gt;-[</string>
<string>&gt;-(</string>
</array>
<key>Name</key>
<string>Nervous</string>
</dict>
(...snip...)
</dict>
</dict>
</plist>
<plist version="1.0">
<dict>

<integer key="AdiumSetVersion">1</integer>

<dict key="Emoticons">

<dict key="amazing.png">

<array key="Equivalents">
<string>=-o</string>
<string>=-O</string>
<string>:-o</string>
<string>:-O</string>
</array>

<string key="Name">Surprised</string>
</dict>

<dict key="anger.png">

<array key="Equivalents">
<string>&gt;:o</string>
<string>:-@</string>
<string>:@</string>
<string>X(</string>
</array>

<string key="Name">Angry</string>
</dict>

<dict key="bad_egg">

<array key="Equivalents">
<string>&gt;-[</string>
<string>&gt;-(</string>
</array>

<string key="Name">Nervous</string>
</dict>
(...snip...)
</dict>
</dict>
</plist>

...isn't that a sight for sore eyes? You're welcome.

Wednesday, September 03, 2008

The world in Verdana

OK, by now everybody and their cat has heard about Google Chrome. The general buzz on Slashdot and blogs seems to be "it's not Firefox" and, more specifically, a general lament of the chicken and egg problem surrounding the release of a new platform when there isn't any software (in this case "plug-ins" or "add-ons") to run on it. Well, OK, there are some Chrome plug-ins available, but Chrome extensions are currently NOT supported. On the other hand, the source code is out there and given that it has the "Google brand", it won't be hard to find motivated geeks hacking some neat software to [eventually] bring Chrome on par with Firefox, with said geeks' hidden agenda of being noticed by Google and be offered a nice googlejob where they get to sit on their googlechair, etc.

It's technically not such a bad chicken-and-egg situation, with the massive and thorough testing Google [claims to] have performed. Plus, the browser is more than a fine replacement for users of Internet Explorer or plain, out-of-the-box Firefox, thus making all us geeks appear crazy when non-techies ask us why we're not using Google Chrome, when we use everything else Google throws at us.

Anyway, I didn't set out to write yet another review, but to post solutions to problems I encountered:

Installing Chrome as a non-administrator


I hit issue 119 ( Install Fails on W2K8 with low-rights user ) when I tried to install on my computer as a non-administrator and got the following:



Thankfully, the work-around by stephen.oakman in comment 6 worked and I found the elusive chrome_installer.exe in a folder matching the pattern C:\Documents and Settings\[user]\Local Settings\Application Data\Google\Update\Download\[guid]\chrome_installer.exe and was able to install it successfully.


Setting the minimum font size


I also had the same accessibility lament about minimum font sizes and font family overrides. For example, here is the Getting Started page in Firefox 2, with Verdana @ 14pt bliss:



...and this is what Chrome gave me with the same page:



OK, yes, the Firefox version looks weird with the title text not lining up with the logo, etc. but all the content is perfectly legible, which is more important to me. Well, more legible than the fonts picked by the web developer.

Thankfully, I have solved the first half the problem with a few quick searches in the source code and now my C:\Documents and Settings\[user]\Local Settings\Application Data\Google\Chrome\User Data\Default\Preferences file contains this little gem:


"webkit": {
"webprefs": {
"cursive_font_family": "Verdana",
"default_fixed_font_size": 14,
"default_font_size": 14,
"fantasy_font_family": "Verdana",
"fixed_font_family": "Courier New",
"minimum_font_size": 14,
"minimum_logical_font_size": 14,
"sansserif_font_family": "Verdana",
"serif_font_family": "Verdana"
}
}



...which gives me a slight improvement in readability in Chrome:



You can find out what the names of the [other] supported hidden preferences are by peeking into chrome/common/pref_names.cc and cross-referencing with WebContents::GetWebkitPrefs() in chrome/browser/web_contents.cc. In particular, you'll find (as of this writing) that the other half of my problem is already identified in a comment:



// User CSS is currently disabled because it crashes chrome. See
// webkit/glue/webpreferences.h for more details.



...with the more details being:



// TODO(tc): User style sheets will not work in chrome because it tries to
// load the style sheet using a request without a frame.
bool user_style_sheet_enabled;
GURL user_style_sheet_location;



D'oh. Maybe this will inspire someone else to fix that part of the code or otherwise provide the elusive "let me choose my own damn fonts" setting that I rely on for keeping my sight and posture in good shape.

Friday, August 08, 2008

Effort vs. Results

I remember having a conversation with my father when I was a kid about his company's employees. I was shocked to discover they were being paid by the hour. I remember following up with a question along the lines of "Wouldn't that encourage them to take lots of time to do their work?" He assured me that, although it was a possibility, it didn't happen very often. (and presumably he explained that people who did that could lose their job, so it was to their long-term advantage to not slack off)

Fast forward a few years later, when I'm in high-school and a student asks the teacher - after she explained a homework assignment - if any marks would be given for effort. I couldn't help but laugh out loud, thinking he was pulling the teacher's leg. He didn't join me in laughing or smiling (and probably shot me a dirty look). Uh oh. He was serious!?!?

Maybe I found that proposition silly because I figured there was no way for it to be reliably measured: it would have to be self-reported. How hard would it be to say "I spent 100 hours on this" when handing it in? And if I had completed the assignment in 10 hours and produced equivalent results to his, would that mean he would get more marks than I got because he spent more time?? Worse yet, even if it wasn't self-reported, how would it be measured??? And what about the difference between "brain time" and "body time"?

Nowadays, I understand why people are [usually] compensated by how much their skills are in demand and that it is possible to be 10 times better than someone else at what you do. Maybe I also did as a kid? That might explain my reaction in both instances. Could it also explain my drive for correctness? My passion for getting things done and done right?? Now here's a doozy for you: would this knowledge at a younger age have helped other students??? Or maybe I'm just being arrogant and that I should just shut up for being the nerd that didn't have any trouble with his assignments????

Discuss.

P.S.: Please go easy on me as I have been working on this blog post since November and it's only now that I have been able to finish it.

Saturday, July 26, 2008

I got in a "jam"...

Round 1A of Google's Code Jam programming contest (in which I participated) ended about an hour-and-a-half ago (there are three sub-rounds of Round 1 and contestants can compete in two of them to try to move on to Round 2).

A failure (on my part) to pay close attention to the requirements of the first problem meant I implemented an algorithm with a complexity of O(n! * n!), which means 25 401 600 iterations for n = 7 (sort of reasonable) but 1 625 702 400 iterations for n = 8, something my laptop wouldn't be able to finish in any reasonable amount of time. It's much worse when you consider that n was expected to go as high as 800! Once I read that, it occurred to me that I had been going at it entirely the wrong way... About an hour-and-a-half of going the wrong way, which involved implementing (and debugging) a nice "permute the items of this list" method.

You see, there was a trick to the problem. Once I realized this, I replaced the double permutation loop with two calls to Sort() and a single O(n) loop. D'oh! My solution to the small input was judged as correct, so I proceeded to the large input. It ran just as fast and so I submitted its output, too. Well, again, I screwed up with the requirements and it turns out my math was overflowing left, right and center and thus, when the contest ended, I got a measly 5 points (out of a possible 15 for that problem and out of a possible 100 for all 3 problems!), which means I ranked 2363 out of 2394. (you don't find out if your submission to the large version is correct until the end of the contest - I also only attempted the first problem)

Oh, well... I might try again in Round 1C (Sunday at 05:00 local time!), but in the meantime I thought I'd publish some of the source code that came out of this. I used Visual Studio 2008, which meant I could try out the neat features of the C# that came out with .NET 3.5, such as:
  • the var keyword
  • extension methods
  • LINQ

OK, so I didn't need to use LINQ, nor did I use extension methods until after the contest, but here's my touched-up Permutations iterator method, generalized to any IList<T> instance:
public static class Extension {
public static IEnumerable<IList<T>> Permutations<T> ( this IList<T> input ) {
int numElements = input.Count;
var slotOffsets = new int[numElements];
var slotBusy = new bool[numElements];
bool allDone = false;
while ( !allDone ) {
#region Set slotBusy flags to false
for ( int i = 0; i < numElements; i++ ) {
slotBusy[i] = false;
}
#endregion

IList<T> permutation = new List<T> ( numElements );
int lastSelected = -1;
for ( int i = 0; i < numElements; i++ ) {
for ( int j = 0; j < numElements; j++ ) {
int selectedSlot = ( lastSelected + 1 + j + slotOffsets[i] ) % numElements;
if ( !slotBusy[selectedSlot] ) {
slotBusy[selectedSlot] = true;
permutation.Add ( input[selectedSlot] );
lastSelected = selectedSlot;
break;
}
}
}
yield return permutation;

#region Update offsets
for ( int i = 0; i < numElements; i++ ) {
slotOffsets[i]++;
if ( slotOffsets[i] < ( numElements - i ) ) {
break;
}
else {
if ( i == numElements - 1 ) {
allDone = true;
}
else {
slotOffsets[i] = 0;
}
}
}
#endregion
}
}
}

...which you can use as follows (notice how it's magically a method on any IList<> implementation?):
IList<int> inputList = new List<int> ( new int[] { 1, 2, 3 } );
foreach ( List<int> permutation in inputList.Permutations ( ) ) {
StringBuilder sb = new StringBuilder ( );
sb.Append ( "[" );
bool isFirst = true;
foreach ( var item in permutation ) {
if ( !isFirst ) {
sb.Append ( ", " );
}
else {
isFirst = false;
}
sb.Append ( item );
}
sb.Append ( "]" );
Console.WriteLine ( sb.ToString() );
}

...and it should produce the following output:
[1, 2, 3]
[2, 3, 1]
[3, 1, 2]
[1, 3, 2]
[2, 1, 3]
[3, 2, 1]

I just wish I had prepared this method before the contest, although I think some actual practice in solving these kinds of problems would have helped me more. Maybe next time... :)

Tuesday, June 10, 2008

Announcing the web-screen-saver project

I finally got around to posting a little tidbit of source code I had lying around on my computer as the new open-source project web-screen-saver (really lame name, I know). Not only that, but the premise seems a bit lame, too. Who would want a screen-saver that runs in their web browser?

It turns out I do! I have been learning about aviation weather and the best way to stay fresh is to practice every day, so I took a really old Toshiba laptop (Pentium MMX @ 166 MHz, 16 MB RAM and a 2.1 GB HD), got it running again (which involved soldering frankensteining an equally-as-old power supply from a Futjitsu laptop, not to mention putting the disassembled laptop back together from memory) and set it on top of my refrigerator. This way, whenever I'm preparing meals, washing dishes, feeding the cat, etc. in the kitchen, I can take a minute or so to browse the latest weather.

Next up was figuring out how to get various semi-frequently-updated images to show up on the screen on a cycle. Various options popped up, such as:


  1. a cron job that would trigger wget on a few URLs with one of:

    1. a companion program that would convert the HTML pages to images and rescale pure images appropriately and a corresponding client (any of many file-system-based screen-savers)

    2. a static page that would cycle the pre-downloaded pages or images using a bit of JavaScript

    3. a number of static pages that serve up one pre-downloaded page or image for a little bit of time before redirecting to the next



  2. a static page that would cycle the live pages or images using a bit of JavaScript

  3. a number of static pages that serve up one live page or image for a little bit of time before redirecting to the next



Some of the above options don't make that much sense until you consider that aviation weather is the kind of information that's sensitive to updates (or the lack thereof) and so it is of the utmost importance to always have the latest version of, say, a TAF or a GFA cloud & weather map. With some web browsers reportedly overzealously caching pages or images coupled with sometimes unreliable internet connections, I was trying to explore the space of options so that I could potentially detect the worse case and do something about it.

I ended up deciding to see whether the current browsers that I use/care about indeed [still] suffered from that problem, since it would eliminate an entire class of problems/solutions. Turns out that improper browser caching no longer appears to be a problem, although this could also be due to proper configuration programming at the server end. That, and the prospect of having to write client-server code that scanned files coming from remote computers sounded more stupid every second I continued to think about it since, you know, web browsers do that sort of thing already!

A few evenings of light programming (i.e. tweaking a few lines of code here and there during a few boring moments of TV watching) and I've got what I think is "version 1.0"-worthy. It's running right now on the trusty laptop in Internet Explorer 6[1] in full-screen mode (hide the status bar, hit F11, then right-click on the toolbars to select auto-hide), although with this much RAM it just swaps and thrashes like hell for a minute or two on every reload. I also need to reboot it every so often because the USB wireless adapter's driver is as stable as the U.S. economy. *cough*

Download


Download version 1.0: awesome-web-screen-saver-1.0.html.
View source of version 1.0: index.html
Live demo (latest version): Aviation Weather Electronic Summary Of Major Events

How to use


Once everything loads, all but the first page/image will be "hidden" (it's really a clever lame container resizing trick). After 2 minutes, the next page/image will be shown, etc. until about 33 minutes at which point the whole thing will reload. You can, of course, scroll up and down, but you may want to use the left and right arrow keys, since they are programmed to jump back and forward (respectively) at page/image boundaries.

Roadmap for future versions



  • JavaScript code to live in its own file

  • META-based refresh to be adjusted based on the number of "pages"

  • better handling of unavailable content (i.e. keep retrying - with suitable back-off - iframes or images that returned anything other than HTTP 200)

  • use Google App Engine for the following features:

    • configurable content profiles (i.e. pick your own frequently-changing content)

    • content caching (i.e. Google's servers can withstand more hits and will probably replicate content to various globally-distributed data centers for higher availability)

    • content download scheduling (i.e. while you have both of the previous two features, you might as well configure content with an expected lifetime or scheduled release, so that it is only pulled once, per publishing, from its source)

    • stats collection

    • general geekiness





Notes


[1] This will probably cost me a few geek points, but the laptop is still running the Windows 98 copy it came with - I couldn't get any GNU/Linux distro to boot/install and I have better things to do with my time!

Thursday, May 08, 2008

"I must use this power only to annoy"

I clicked on an Amazon link to a product the other day and it lead me to its product page. Everything there is fine, except instead of a price, there was a link that read "Click here to see the price". There was also another link that read (Why don't we show the price?) which contained the following text:


Why Don't We Show the Price?

Manufacturers sometimes ask that retailers not display a price if it drops below a certain amount. The "click here to see price" message indicates that the price of the item is so low that the manufacturer requested that it not be advertised (that is, displayed). In a brick-and-mortar store, you would probably have to ask a salesperson what the price of the product is. At Amazon, by clicking on "click here to see price" you are essentially asking to see the price, at which point we show it to you.


Ummm... WTF? If I didn't see a price in a brick-and-mortar store, I wouldn't buy the item! You forcing me to click just to replicate that have-to-ask experience is forcing me to replicate my wouldn't-buy-the-item reaction. I hope you're happy about your lost sale.

Tuesday, April 29, 2008

Hi-tech alumni

What appeared to be a joke article at first glance (Cravath sounds like the french word for a tie) turned out to be a very insightful post by Alex on the topic of quitting and more generally about the habits of a certain class of software developers. I couldn't agree more! In fact, I have found myself proud to announce to my peers that I have worked at Microsoft and Macadamian; that I'm an alumnus or a graduate of sorts from their respective schools [of thought], that I embody some/most of their best practices and serve as a kind of unofficial ambassador.

I can relate to the "bus factor" concept and can even suggest a tangible metric: fantastic developers (who are thus likely to quit) are usually top contributors to corporate wikis, since they are constantly externalizing their knowledge for future colleagues/replacements. This, ironically, increases their value since they can spend more of their [precious] time solving new problems instead of being constantly disturbed for knowledge and acting as a "walking wiki" (which Alex calls "unskilled people"). It's not hard to see how true synergy is achieved by having all documentation at everybody's fingertips. This is what Bill Gates called a Digital Nervous System.

The next logical step to documenting is automating. So even if you feel your "bus factor" is low because everything is documented and your team spends a lot/most of their time solving new problems (which is, by itself, an excellent start) documented AND automated processes are the true mark of excellent talent.

So, in the spirit of the topic, I will quit my job (and there's nothing wrong with that), but I just haven't decided when. And if I start a company, I will include a link to this article in the corporate wiki.

P.S.: I'm posting this to my blog from Google Docs. I had this idea that it would be great to be able to do so and, lo and behold, there it was, under Share/Publish as web page...

Update: Ok, it posted, but without a title and the HTML was definitely not clean, but it's a start. I probably just need to tweak a few styles.

Friday, February 29, 2008

PalmPilot Jr

Having received my XO's developer key, I immediately upgraded the operating system to build number "Joyride 1638", using one of those "USB-based memory sticks". The large PDA/small laptop now appears to switch to some form of low-power mode whenever the lid is closed, when in "Reader mode" and in the middle of yum-based downloads. A tad too aggressive, if you ask me, but that's life when installing unstable software. It may have had something to do with having the Reader activity opened at the same time, but in any case, I don't think suspending is necessary when the laptop is plugged in. Maybe it would be OK after a much longer timeout, say 10 minutes.

Other than that, the new power management features (coming from build 653 -- which I think had absolutely none, except maybe for the "blank screen" screensaver -- these features are totally new) are really spiffy, and allowed me to go from a full charge this morning and still be going after a ride on the bus, followed by a full day of work (where it spent most of that time closed up). Hmmm... Maybe I didn't explain that correctly, so I'll switch to rough pseudo-code:


laptop.powerMode = PowerMode.OnBattery;
laptop.radio = RadioMode.Off;
me.use ( laptop, 30 /* minutes */ );
laptop.powerMode = PowerMode.Suspend;
Thread.sleep ( 1000 * 60 * 60 * 8 /* 8 hours in milliseconds */ );
laptop.powerMode = PowerMode.OnBattery;
me.use ( laptop, 30 /* minutes */ );
// because I didn't have to shutdown and boot up the laptop again
me.mood = Mood.Happy;


...there. I swear those "I write code" t-shirts were invented for me.


Anyway, there are still a few quirks, but the software is definitely getting there, which brings me to the whole point of this post: the XO should [eventually] be sold around the world. I'm thinking it could be ready and in all the stores where laptops and/or educational toys are normally sold (such as Walmart, Toys 'R' Us, ThinkGeek, Amazon.com, Future Shop, Best Buy, Fry's Electronics, Radio Shack, etc.) by next Christmas.

This form of distribution could expand their user base in a way that's not too dissimilar to the G1G1 program, minus the logistics nightmares associated with selling and shipping 80 000 units individually. This way, they ship say, 250 000 units in chunks of 10 or 20 thousand to a few retailers with established distribution channels and the foundation can then focus their efforts on what they do best (designing and building the laptop, marketing to governments of poor countries, etc.), instead of trying to also be a mass-distributor.

Of course, this assumes that the sale of XOs at retail would bring enough revenue to not only make this affordable to the foundation, but also to make money. I don't think they could pitch it for $400 (as in the G1G1 campaign) with a straight face (some Acer laptops are available now for $600), but they could probably do it for less than $200. At that price, they are starting to compete with other educational toys, as well as traditional PDAs that also come with a keyboard, word processing software, a web browser and an extended battery life.

I'm totally serious about this: it could be the source of volume they have been after to bring the price of the units down, not to mention the associated network effects. Think of it this way: as a parent, would you rather get your child an expensive, fragile computer made for adults or an inexpensive, rugged computer made for kids? ("hand-me-down clunkers" and sources of free tech support notwithstanding) How many people missed out due to the G1G1 campaign's short lifespan or high barrier of entry (i.e. you must create a PayPal account - even though the error message only said your credit card was not valid - and then have it authorized for large amounts - which means having it linked to a bank account - on top of the amateur-looking website and the uncertainty of shipping dates, etc.)?

Let's hope they read my blog and credit me after the associated success boom from implementing my master plan. You're welcome. :)

Tuesday, January 29, 2008

Blogging resumes (and other news)

After far too long of not blogging, I have arranged the circumstances to make it more likely I would blog through the use of an innovative concept I like to call "Blogging on the bus". Such a thing is now possible since the arrival of my late Christmas/early birthday present: an XO laptop (and about an hour of travel by bus to/from work every weekday).

I participated in the G1G1 (give one, get one) campaign and received the small laptop/large PDA device shortly two months after ordering it. Apart from being too small to allow me to touchtype, I really like it. It should be at least faster to write with than with my crusty old Palm III and also easier to read RSS feeds, etc. than same. I'm not sure I have figured out how to have it sleep and wake up (suspend/resume) really fast like my Palm did (thus never having to boot up/shutdown), but the OLPC wiki is nice and should have the answer.

UPDATE: The software that shipped with my unit does not support this yet:
How do I put an XO laptop to sleep?

The sleep feature is not enabled in software provided on XOs shipped starting December 2007 in the Give1Get1 program. A software upgrade early in 2008 will support suspend/resume sleep features, for much improved battery life.




Also worthy of noting is the successful completion of the courses part of my master's program at Carleton University. I now have to pick a decent topic and a supervisor to get started on this "other half". I'm hoping to add neat features to T.O.D.D. (a.k.a. testoriented), such as automatic testing, thus effectively putting me out of a day job. (ha ha)


Lastly, I would like to thank everybody who left words of thanks for my posting about Hacking the Microsoft Natural keyboard 4000, redux, as well as whoever drove all that traffic to my blog. I think we now have enough evidence to say that Microsoft definitely shipped the wrong mapping for that slider. I wonder if I should also start selling a little "Scroll" sticker (or, really, just a piece of electrical tape - such as this patch kit) alongside a floppy disk containing an automated and friendly version of the software I had posted, calling it an "upgrade kit" of some sort.