Nov 27, 2022
Want to just connect? — Skip straight to the hardware setup and script sections.

WebTV was a service launched in 1996 that brought this amazing new internet craze right into your living room, and it was a pretty compelling device for its time. You could view newsgroups, join IRC chatrooms, send emails, browse the web, and on later models simultaneously watch TV in Picture-in-Picture mode.

When I was in middle school my brother had one, and he’d pay me to write eBay listings on it. I spent countless hours on that thing chatting, designing flashy HTML 3.2 listings, and listening to its vast library of midi background music.

Honestly, even by today’s standards, WebTV puts most smart TVs’ capabilities to shame. Sure it can’t do streaming video or Netflix, but streaming video barely existed as a novelty at this point, and Netflix wouldn’t begin their transition to online distribution for a whole decade yet.

This launch video provides some context of how mind-blowing the WebTV was for the time, and it only got better. Later models could even display program schedules and control your VCR to record shows like a DVR, which you could then watch later while hanging out in a chatroom. From your couch. In the freaking 90s.

WebTV returns

A few weeks ago I saw a Philips MAT976 WebTV at the thrift store but passed it up figuring it was a brick–the service died long ago, after all. Imagine my delight, then, when I found this YouTube video by Michael MJD showing a project well on its way to restoring the experience.

Naturally I rushed back and bought the unit, then set about connecting it.

Philips MAT976 WebTV box
Paid a whole $12.99 to get my childhood back

Suppose you’ve also just bought a WebTV and want to connect it to one of the custom servers currently out there. How might you go about doing that?


Hardware setup

The first thing you’ll likely need to do when setting up an original WebTV unit is clear its settings. You can do this with a “power off code.” Just have your WebTV plugged in but in a powered-off state, then do one of the following:

  • Keyboard: Press option 3 times, then type 32768
  • Remote: Press option 3 times, then HOME, BACK, ↑, ↓, GO

The lights on your box should give you a fun little show for a few seconds, then all tellyscript, dialing options, TV listings, etc. should be cleared giving you a more or less blank slate to work with.

Buying the right modem

To connect to modern servers you’ll need to go over the internet. To do this you’ll need a Linux-compatible USB voice modem connected to an internet-connected Raspberry Pi. The common wisdom around the community is you should be using something with a Conexant RD02-D400 chipset. Common models of these include the Lenovo RD02-D400 and the Dell NW147. Based on my reading Zoom, StarTech, and US Robotics may have also made RD02-D400 USB modems, but I’m having trouble finding model numbers for those.

It’s worth noting that this modem isn’t connecting to a real phone line. Your WebTV won’t actually be dialing out, and you don’t need phone service. Your Pi is going to trick the WebTV into thinking whatever it dialed was real.

Buying the right Raspberry Pi

I don’t even want to admit how many hours I lost to this project figuring a Pi 2 Model B would be fine, because come on. It’s just using a USB modem. How resource-intensive could that be?

Linux top command showing 104% CPU usage

Get a Pi 4 for this. And hook it up with ethernet. You can try your luck with wifi, but for me that also caused extreme unreliability and inexplicably slow connection speeds when it did work.

Phone line simulator 2022

Lastly, you need to power your phone line. Old phone lines had voltage on them, and your devices are expecting that voltage to be there.

You might see USB modems for DreamPi designed with a line voltage inducer built in, but skip those. The WebTV wants that line voltage cranked. An AC to DC adapter that puts out anywhere from 15-25V DC should work.

To build your phone line, you need to cut a pair 1 wire and splice the positive and negative from your DC supply in with it. Pair 1 is the innermost two pins on your telephone connector. Normally this pair is red and green, but color may not help here since there were multiple standards and some manufacturers also didn’t give a crap. Which wire of pair 1 you splice into makes no difference.

Telephone wiring pinout
Borrowed from allpinouts.org

The DC power supply should go inline with your chosen wire such that signal is coming down one wire, through your power supply, and back out to the same wire. Which way you hook positive and negative also makes no difference.

You may be feeling like this is all very slapdash advice. Just splice inline with either wire in either direction? And nothing’s going to get fried? Yes. I went ahead and tested this with straight-throughs, rollovers, and reversed polarity in each, and you get a nice dial tone in any configuration. Pair 1, pick a wire, splice in, don’t overthink it.

Connect one end of your powered phone line to the WebTV. Connect the other end to your modem. Connect your modem to a Raspberry Pi. Connect the Pi to the internet.

WebTV wired up

Software setup

The dialup and routing methods available as of this writing are a little tricky, but everything’s there if you piece it together. There’s MattMan‘s DreamPi For WebTV V2 Raspberry Pi image which is easy enough to flash over to an SD card and start up, but there are newer routing rules that aren’t in it as of this writing. There’s also notdreamnorpi by samicrusader that isolates the essential USB modem connection components of DreamPi, but it doesn’t come with the routing necessary to make a WebTV function once it completes its dialup connection. Lastly there’s an nftables gist by nitrate92 that contains the necessary routing, but the rules may get dropped on every restart without further configuration.

The simplest solution seems to be getting an old version of notdreamnorpi operational on a pi with the nftables rules from nitrate92’s gist added. I’d recommend this over attempting to add the routes to the DreamPi image, as it was built with Raspbian Stretch and doesn’t include the kernel modules necessary to make nftables function.

If you don’t care how any of this works and just want to start surfing, skip to the end of this section for the lazy bash script.

Installing Raspbian

First you’ll need a base Raspbian image, so grab the Raspberry Pi Imager and get an SD card ready. You’ll want to choose Raspberry Pi OS (other) and then Raspberry Pi OS Lite. I tested on 32-bit Raspbian Buster 2022-09-22. This will give you a fresh OS with no desktop environment. There’s zero point in having a desktop environment for this since it’s just going to sit behind a TV and pretend to be a phone line.

With the image written to your SD card, hook in a monitor and keyboard for first-time setup. Get your locale, hostname, network settings, etc. right and remember to enable SSH if needed. Since it will probably be buried in a nest of TV wires, SSH can be a real blessing.

Retrieving notdreamnorpi

Next you’ll want to get a copy of notdreamnorpi and install your dependencies. You’ll want this old version since the current one is hit-or-miss detecting the modem on Raspbian Buster.

Also note: On line 21 in this build of notdreamnorpi, there’s a reference to /sbin/init –version. That won’t work on Raspbian, and will crash notdreamnorpi. Replace /sbin/init with /usr/bin/systemd/systemd.

The dependencies you’ll actually need in this setup vary a bit from what’s in the readme, so do sudo apt install wvdial python3-psutil python3-serial python3-sh python3-systemd.

Adding and preserving nftables rules

For the webtv.nft gist, save webtv.nft wherever you like on your pi and run sudo nft flush ruleset and sudo nft -f webtv.nft to set the rules.

The only problem here is that all the rules may be gone whenever you reboot the Pi. To keep them around we need to persist them somehow. There are a lot of ways to do this, but the cleanest is probably just to run the above commands in a systemd unit file that also starts notdreamnorpi once the network is up.

Running notdreamnorpi on startup

With everything in place, we need to ensure notdreamnorpi provides a dial tone when the Pi starts up. Then we have a set-and-forget solution that can be reset with a power cycle.

Again there are a lot of ways to do this, but we’ll cover the systemd unit file. Nitrate92 was kind enough to share theirs, and it does the job perfectly while also persisting the nftables rules. I’ve lightly edited it here:

[Unit]
Description=dreampi
After=network.target
Wants=network-online.target

[Service]
Type=simple
User=root
ExecStartPre=/usr/sbin/nft flush ruleset
ExecStartPre=/usr/sbin/nft -f /home/pi/webtv.nft
ExecStart=/usr/bin/python3 /home/pi/notdreamnorpi/dreampi.py
WorkingDir=/home/pi/notdreamnorpi
Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target

Create this file at /etc/systemd/system/dreampi.service.

Ain’t nobody got time for that

I hear you. If you don’t care how any of that works and just want a script that does everything above with the recommended settings, here you go. Just grab a Pi with Raspbian Lite on it, copy this into a .sh file in your home directory, run it, and reboot. Enjoy your dial tone.

#!/bin/bash
# For Raspbian Pi OS 32-bit 2022-09-22.

# prevent root
if [[ $EUID -eq 0 ]]; then
  echo "Script will sudo when it needs to. Just run it as a regular user." 1>&2
  exit 1
fi

echo "Setting up dependencies, notdreamnorpi, WebTV routing rules, and systemd unit file..."
sleep 1

# install dependencies (throw kitchen sink, you likely already have many of these)
sudo apt install -y git ppp net-tools nftables wvdial python3-psutil python3-serial python3-sh python3-systemd

# clone notdreamnorpi - archived version, works well in systemd unit file
wget https://github.com/samicrusader/notdreamnorpi/archive/da8225eb716fa3af9fb3e7d02431033605935f68.zip
unzip da8225eb716fa3af9fb3e7d02431033605935f68.zip
mv notdreamnorpi-da8225eb716fa3af9fb3e7d02431033605935f68 notdreamnorpi
sed -i -e 's/\/sbin\/init/\/usr\/bin\/systemd\/systemd/' notdreamnorpi/dreampi.py # /sbin/init --version doesn't work on raspbian, crashes notdreamnorpi

# download webtv.nft
wget -O webtv.nft https://gist.githubusercontent.com/nitrate92/6f67518c79b769c02e9a12beb0bb87eb/raw/33b33a4a1fffcb86a30f0ef33368228a458032c1/webtv.nft
sudo nft flush ruleset
sudo nft -f webtv.nft

# set up the systemd unit file
unitfile="[Unit]
Description=dreampi
After=network.target
Wants=network-online.target
[Service]
Type=simple
User=root
ExecStartPre=/usr/sbin/nft flush ruleset
ExecStartPre=/usr/sbin/nft -f /home/$(whoami)/webtv.nft
ExecStart=/usr/bin/python3 /home/$(whoami)/notdreamnorpi/dreampi.py
WorkingDir=/home/$(whoami)/notdreamnorpi
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target"
echo "$unitfile" > dreampi.service
sudo mv dreampi.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable dreampi
sudo systemctl start dreampi.service

echo ""
echo "All done. Configured to route to Zefie's minisrv. Recommend rebooting before using."
echo "To change servers any time, edit ServerIP in /home/$(whoami)/webtv.nft then reboot."

If you need to debug this, you can view live logs from the service with journalctl -f -u dreampi. Mine still fails to connect sometimes, but I find it’s usually the WebTV needing a power off code instead of a problem with the Pi.

Now you’re on the World Wide Web

Available servers

While JarHead‘s server from Michael MJD’s video isn’t yet public, there are a couple of other public servers up and running. You can find Zefie’s minisrv by setting the server address to 51.222.164.146 (default in above script), or the HackTV minisrv at 74.76.120.18. These don’t try to offer the original experience, but they will let your original hardware browse the web again just like it used to, and each has its own fun surprises in its custom menus.

Space Jam website on WebTV
Everybody get up, it’s time to slam now

You can also make your own server and do whatever you like with it if that’s more to your taste. Zefie has a great minisrv repo to start with.

Lastly, if you’d like to chat with the WebTV modding community, there’s a Discord server at https://discord.gg/qke279EUa8.

Oct 9, 2022

I’m the type of person who gets excited about rare old tech, so you can imagine my delight when I found a $15 travel bag at the thrift store claiming to contain a laptop printer. Inside was a Seikosha LT-20, a dot matrix model I had never heard of before–and if search is anything to go by, I’m not alone in my ignorance.

Seikosha LT-20

Obligatory Japanese Lesson

First things first, if you immediately thought “That sounds a lot like Seiko, the Japanese watchmaker,” you’re right, it is. My immediate instinct was to assume Seiko used to include “sha” 社 (company) on the end of their name, such that Seikosha effectively meant “Seiko Co.” That’s apparently not the case, though.

According to A Journey In Time: The Remarkable Story of Seiko (2003), the kanji for Seikosha is actually 精工舎, which is a play on “seikou” 精巧 (elaborate; delicate; exquisite–also a homophone for 成功, success) with the second kanji swapped for another “kou” 工 (construction, workmanship) and followed by “sha” 舎 (hut, house). As such, Seikosha means “House of Exquisite Workmanship.” And I must agree, it is a truly exquisite piece of tech!

Lost to Time

The Seikosha LT-20 debuted in 1991 for laptop users who also wanted a printer portable enough to take on business trips. Considering the demographic, you can imagine the market was limited compared to other printers of its time–so limited that I really struggled to find any information about it.

Complete with fancy travel bag!

I found pricing in a short review in issue 1 of a Bosnian magazine called Micro Computer World. The LT-20 cost $499 on release, or $549 with an optional Ni-Cd battery included. Adding a battery charger cost $46.99, and a spare battery cost $77.99. A Seikosha protective bag (which I’m delighted mine came with) cost $74.99. The only other reference I could find to anything ever being published about the LT-20 is a review in PC/Computing Volume 4, Issue 5 (May 1991), but I’ve been unable to locate a digitized copy of this issue. I may need to retrieve that from a library’s archives.

I was also unable to locate a digitized version of the original owner’s and reference manuals, but luckily this printer came with paper copies! I’ve (poorly) digitized them here for posterity. If you have equipment to properly digitize these, please reach out and I’ll happily send them your way.

Seikosha LT-20 Owner's Manual
PDF, 12.4MB
Seikosha LT-20 Reference Manual
PDF, 14.9MB

Seikosha Drivers

You might imagine the driver situation on a piece of hardware like this is hopeless, but it was astoundingly easy to get running. According to page 34 in the owner’s manual it just uses the IBM Proprinter X24 driver. That’s conveniently on Disk 6 of a Windows 3.11 for Workgroups install. After adding an IBM Proprinter X24 to my system the LT-20 lit right up and started printing.

Replacing the Ribbon

Predictably after 31 years the ribbon was almost bone dry, but it still made recognizable prints. Unlike the drivers, though, finding a ribbon for an LT-20 is basically impossible.

The old, dry ribbon as stored in the cartridge

I reached out to Lanie Hurwitz from Ribbons Unlimited to see what my options would be, and he was extremely helpful in finding a generic ribbon I could at least use to remanufacture the existing ribbon cartridge. While Ribbons Unlimited didn’t have any continuous ribbons in the 5/16″ width the LT-20 uses, they did have rolls of 5/16″ black in the correct material. All I needed to do was bond it into a continuous loop and load it into the housing.

Fresh ribbon loaded

Bonding ink-soaked nylon to itself is a bit of a tall order, but is doable! I tried a number of options like melting it with a laser at low power, pressing it with a chisel-tip soldering iron, and even looking around for household chemicals I could repurpose to melt the nylon to itself. I may have stolen my girlfriend’s lip balm to see how much phenol was in it, and may have been trying to concentrate acetic acid in the garage from some vinegar… Ultimately I ended up going with a very boring solution: Elmer’s Craft Bond glue and extensive use of a heat gun. It’s not the best way to bond nylon (there are actual products for this, I’m just cheap and impatient), but it does create a flexible, strong-enough bond for this purpose.

Print comparison

Return of the Seikosha LT-20

With the drivers in order and a brand new ribbon threaded into the cartridge, the Seikosha LT-20 is now printing beautifully again after 31 years! It’s really rewarding to have your ear drums once again burst by that piercing dot matrix screech. Amazingly, the rollers in this unit still work great and pick up the paper every time.

Jul 3, 2022
Looking for the code? — Skip straight to the GitHub repo.
Cat in front of monitor

Our modern, technological world is full of distractions. For this reason, when I really want to engage with a book but still take notes, I like to use a very old computer. It still needs to save to my NAS, have good word processing and printing functions, and provide font support and an IME for writing in Japanese but nothing else. It should be very fast at these tasks, but modern gaming and internet connectivity should be as impractical as possible. I find the sweet spot for this set of requirements to be a generously-clocked 486 running Windows 3.11, Microsoft Office 4.3, and Pacific Software Publishing‘s KanjiWORD.

None of us can remain fully focused, though, and building a period piece like this brings with it a whole world of fascinating distractions to explore while setting it up. My favorite may be the Turbo button.

Turbo power

PCs of this era were, believe it or not, blazing fast. So absurdly fast it was a problem.

If you had amassed a software library through the “IBM PC Compatible” ’80s, a lot of it was likely targeted at running on an Intel 8088 or 8086 processor or one of their numerous clones at 4.77MHz or 8MHz. This became an issue when compatible, but much quicker 286, 386, and 486 processors became the norm. You could still run your old programs, but if any human i/o was timed using clock cycles, it would now run with uncontrollable speed. In games, your character might zip across the screen faster than you could let off the arrow key; in word processors, your cursor might shoot to the end of the document when you were just trying to go down a few lines. It was too much of a good thing.

They weren’t kidding.

To combat this, you could temporarily engage Turbo mode by shorting a jumper on the motherboard, usually attached to a physical switch on the front of the computer. This would under-clock your fancy, light speed processor down to the old, slower rate so that your programs could run how they used to.

Some manufacturers would wire this up the other way around so that Turbo meant you were going faster, but Turbo was often, counter-intuitively, a speed limiter. LGR has a great video about this.

Misinformation? Maybe! (Update Oct. 8, 2022) — VWestlife recently released a video debunking the idea that “turbo mode slows the computer down” where he reviews Intel documentation and demonstrates that on systems with a Turbo LED to indicate the mode, ON should mean higher clock speed, and OFF should mean lower, regardless of the Turbo button wiring. I only have this system to test with, but it does work that way, so I may be spreading a myth here!

Stupid case mods, retro edition

My first experience case modding was chopping a side window with a hack saw and shoving cold cathodes from FrozenCPU.com into a beige 98 machine while chugging Bawls, but this degeneracy has an even longer and more storied legacy.

If you understood what a Turbo button did, then you already knew what speeds the clock was toggling between: Native and 808x. But what if you wanted it to look cool when you smashed that Turbo button and engaged the Turbo brakes? What if all you really wanted out of life were lights and bleep-bloops? Then you needed a clock speed display.

Clint from LGR’s famous wood grain PC at 66MHz

It’s worth noting that clock speed displays never had any idea what the actual clock speed was–they didn’t measure anything. The numbers were set with jumpers on the back of the display and they would just toggle back and forth with the state of the Turbo button. They were purely eye candy.

I love eye candy.

Give me the shiny

Until recently, my 486 has been in a newer ATX case waiting for a forever home. When I finally found the right case, I couldn’t tell from the listing photos if it had a clock speed display or not. It seemed to have a spot for one, but the front panel wasn’t hooked up and it was all behind a smoked lens, so I couldn’t tell for sure. Unfortunately it didn’t have one–just a spot where one could go. Hats off to Jefferson College, who bought this unit in March 1994 from Skywalker AV Supply, for not wasting their money on nonsense… but now it’s time to fix that.

eBay listing photo of 486 case
The empty spot got my hopes up.

You may have guessed that finding an obscure mod like this from 30 years ago is practically impossible, but this also presents opportunities. We now have three decades of technological progress with which to make our mods even stupider.

With that in mind, what if we could display not just a number, but a cool logo? Maybe even a tiny screen saver?

The hardware

Arduino with Molex power connector
Use red and black for 5V from a molex connector.

I had a 0.96″ SSD1306 panel and an Arduino Mega sitting around and didn’t feel like waiting on other parts, so that’s what I used to build this mod. It’s almost a perfect size to fit where a 2-digit clock speed display would have gone.

The wiring is dead simple:

  • 5V and GND from computer power supply to power the Arduino, which you can just steal from any molex cable. Hack up a spare case fan or something. (pinout)
  • Four jumper wires from Arduino to SSD1306 display (5V, GND, SDA, and SCK–check your specific Arduino for which pins are SDA and SCK)
  • One pull-down resistor between Arduino GND and a digital input of your choosing
  • One jumper wire from said digital input to the positive lead of the LED above the case’s Turbo button (measure with a voltmeter if it’s not obvious which lead is which)
Arduino with Turbo Button wiring attached
Yellow goes to Turbo LED positive. Note pull-down resistor to GND.

Connecting in this way allows you to view the current Turbo status without even bothering to look at the motherboard’s jumper polarity or trying to do some tiny, neat solder connection on your original jumper wires. If Turbo is engaged, it’s going to send voltage to your Turbo LED, and this big, easily-reversible soldering target can set your digital input HIGH. If Turbo’s disengaged, the pull-down resistor will set the input LOW. No need to get too fancy; these weren’t technically fancy to begin with. Just do your soldering with some haste so you don’t damage the LED or anything, and make sure your Arduino is either insulated somehow or mounted up off the case.

Photo of wiring installed in case
Neat enough I suppose!

In retrospect, a custom four-ended jumper cable with one female end, two male ends, and one male end with an inline pull-down resistor, paired with one straight-through female to male jumper would accomplish this same thing with no soldering on original components whatsoever so long as the pins are the same size as what’s on your motherboard. Maybe next time.

The software

To drive the display you can use Adafruit’s SSD1306 library and GFX library. All the hard work is already done with this display so there’s no point reinventing the wheel. You’ll need two bitmaps representing what you want to show when the Turbo is engaged or disengaged, and you can make them in GIMP, Paint, or whatever. Jasper van Loenen’s image2cpp will happily convert your monochrome 128×64 bitmap into an array that the Arduino can read and display with the drawBitmap function in the Adafruit GFX library. For the SSD1306, use the “Horizontal, 1 bit per pixel” draw mode.

Arduino with OLED showing a Cyrix logo

For a little extra fun I mashed Sinoia’s oled-starfield into my program so that ten seconds after pressing the Turbo button, the screen switches to the classic Starfield Simulation screensaver until you press it again. OLED burn-in isn’t really a significant concern if you don’t use the machine that much (and it’s not like these tiny OLEDs are expensive anyway), but mine idles a lot and I would like to get at least another few decades out of it with as little maintenance as possible.

Animated gif of Starfield Simulation screen saver

Here’s the code I’m using, which you can also view on GitHub. I recommend adapting this to your tastes, or just stealing relevant bits and pieces. I’ve also included a separate display centering sketch in the GitHub repo that you can load while you’re actually attaching the display into the case to ensure it’s centered and level.

/**************************************************************************
OLED Turbo Button
Turbo button display replacement for IBM PC Compatibles
Using Adafruit SSD1306 B/W OLED panel at 128x64px
Fits into your case where a Turbo display normally would

Shows an icon for your processor (or 8088 if Turbo activated) on startup,
and whenever you toggle the Turbo button. Changes to a star field
screensaver after 10 seconds.

Requires a jumper with a pulldown resistor from the positive
lead on the case's Turbo LED to the digital input of your
choice (default 2) on the Arduino.

Create a logo for your processor using image2cpp
128x64 in Paint, monochrome bitmap format
Draw mode Horizontal, 1 bit per pixel
http://javl.github.io/image2cpp/
 **************************************************************************/

#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels

// Declaration for an SSD1306 display connected to I2C (SDA, SCL pins)
// The pins for I2C are defined by the Wire-library. 
// On an arduino UNO:       A4(SDA), A5(SCL)
// On an arduino MEGA 2560: 20(SDA), 21(SCL)
// On an arduino LEONARDO:   2(SDA),  3(SCL), ...
#define OLED_RESET     4 // Reset pin # (or -1 if sharing Arduino reset pin)
#define SCREEN_ADDRESS 0x3C ///< See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);


////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////// SETTINGS
////////////////////////////////////////////////////////////////

// Solder a male jumper wire with a pulldown resistor to ground from the positive lead of the Turbo LED.
// When Turbo light is on, this Arduino pin will be HIGH. When it is off, the pin will be pulled LOW.
int turboPin = 2;

// How long to show the icon when the Turbo button is toggled (approximation in seconds)
int showIconFor = 10;


////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////// PROCESSOR ICONS
////////////////////////////////////////////////////////////////

// TURBO OFF ICON - Swap this array with the icon to show when the turbo button is disabled (default i8088 icon)
const unsigned char turbo_off [] PROGMEM = {
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x3f, 0x01, 0xf8, 0x03, 0xe0, 0x1f, 0x80, 0x7e, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x3f, 0x03, 0xfe, 0x07, 0xf0, 0x3f, 0xe0, 0xff, 0x80, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x3f, 0x03, 0xff, 0x0f, 0xf8, 0x3f, 0xf0, 0xff, 0xc0, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x3f, 0x07, 0x8f, 0x0e, 0x38, 0x78, 0xf1, 0xe3, 0xc0, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x1e, 0x07, 0x07, 0x1c, 0x18, 0x70, 0x71, 0xc1, 0xc0, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x07, 0x1c, 0x1c, 0x70, 0x71, 0xc1, 0xc0, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x3f, 0x07, 0x07, 0x1c, 0x1c, 0x70, 0x71, 0xc1, 0xc0, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x3f, 0x07, 0x07, 0x1c, 0x1c, 0x70, 0x71, 0xc1, 0xc0, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x3f, 0x07, 0x8f, 0x1c, 0x1c, 0x78, 0xf1, 0xe3, 0xc0, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x3f, 0x03, 0xfe, 0x1c, 0x1c, 0x3f, 0xe0, 0xff, 0x80, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x3f, 0x01, 0xfc, 0x1c, 0x1c, 0x1f, 0xc0, 0x7f, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x3f, 0x03, 0xfe, 0x1c, 0x1c, 0x3f, 0xe0, 0xff, 0x80, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x3f, 0x07, 0x8f, 0x1c, 0x1c, 0x78, 0xf1, 0xe3, 0xc0, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x3f, 0x07, 0x07, 0x1c, 0x1c, 0x70, 0x71, 0xc1, 0xc0, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x3f, 0x07, 0x07, 0x1c, 0x1c, 0x70, 0x71, 0xc1, 0xc0, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x3f, 0x07, 0x07, 0x1c, 0x1c, 0x70, 0x71, 0xc1, 0xc0, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x3f, 0x07, 0x07, 0x1c, 0x1c, 0x70, 0x71, 0xc1, 0xc0, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x3f, 0x07, 0x07, 0x1c, 0x1c, 0x70, 0x71, 0xc1, 0xc0, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x3f, 0x07, 0x07, 0x0c, 0x1c, 0x70, 0x71, 0xc1, 0xc0, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x3f, 0x07, 0x8f, 0x0e, 0x38, 0x78, 0xf1, 0xe3, 0xc0, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x3f, 0x03, 0xfe, 0x0f, 0xf8, 0x3f, 0xe0, 0xff, 0x80, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x3f, 0x03, 0xfe, 0x07, 0xf0, 0x3f, 0xe0, 0xff, 0x80, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x3f, 0x00, 0xf8, 0x03, 0xe0, 0x0f, 0x80, 0x3e, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x0f, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0xff, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x3f, 0xff, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x03, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x1f, 0xff, 0xff, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x03, 0xe3, 0xff, 0xff, 0xff, 0xfc, 0x0f, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x03, 0xfc, 0x7f, 0xff, 0xfe, 0x03, 0xff, 0xff, 0xff, 0xb6, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x03, 0xff, 0x9f, 0xff, 0x01, 0xff, 0xff, 0xff, 0xed, 0xb6, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xc0, 0xff, 0xff, 0xff, 0xfb, 0x6d, 0x92, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x01, 0xff, 0xfc, 0x3f, 0xff, 0xff, 0xfe, 0xdb, 0x64, 0x92, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xfd, 0xb6, 0xc9, 0x24, 0x92, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x9f, 0xff, 0xff, 0xff, 0x6d, 0xb6, 0x49, 0x24, 0x92, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x97, 0xff, 0xff, 0xdb, 0x6c, 0x92, 0x49, 0x24, 0x80, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x91, 0xff, 0xf6, 0xdb, 0x64, 0x92, 0x49, 0x20, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x90, 0x7d, 0xb6, 0xc9, 0x24, 0x92, 0x48, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x80, 0x0d, 0xb2, 0x49, 0x24, 0x92, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x92, 0x49, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x92, 0x49, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x92, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x90, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
// TURBO ON ICON - Swap this array with the icon to show when the turbo button is enabled (default Cyrix 486 icon)
const unsigned char turbo_on [] PROGMEM = {
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0xc0, 0x60, 0x00, 0x03, 0x80, 0x01, 0xff, 0x00, 0x00, 0x03, 0xf0, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0xe0, 0xe0, 0x00, 0x07, 0x80, 0x07, 0x83, 0xc0, 0x00, 0x0f, 0x80, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0xf1, 0xe0, 0x00, 0x07, 0x80, 0x0e, 0x01, 0xe0, 0x00, 0x1e, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0xfb, 0xe0, 0x00, 0x0f, 0x80, 0x0c, 0x00, 0xe0, 0x00, 0x3c, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0xff, 0xe0, 0x00, 0x1f, 0x80, 0x1c, 0x00, 0xf0, 0x00, 0x78, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x7f, 0xc0, 0x00, 0x1f, 0x80, 0x1c, 0x00, 0x70, 0x00, 0xf0, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x3f, 0x80, 0x00, 0x3f, 0x80, 0x1c, 0x00, 0x70, 0x01, 0xe0, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x7f, 0xc0, 0x00, 0x3f, 0x80, 0x1c, 0x00, 0x70, 0x03, 0xc0, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0xff, 0xe0, 0x00, 0x77, 0x80, 0x1c, 0x00, 0x70, 0x07, 0x80, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0xfb, 0xe0, 0x00, 0xe7, 0x80, 0x1c, 0x00, 0xe0, 0x07, 0x80, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0xf1, 0xe0, 0x00, 0xe7, 0x80, 0x1e, 0x00, 0xe0, 0x0f, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0xe0, 0xe0, 0x01, 0xc7, 0x80, 0x1f, 0x01, 0xc0, 0x0f, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0xc0, 0x60, 0x03, 0x87, 0x80, 0x0f, 0x83, 0xc0, 0x1f, 0x1f, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x87, 0x80, 0x07, 0xc7, 0x80, 0x1e, 0x7f, 0xe0, 0x00, 0x00, 
	0x00, 0x00, 0x0c, 0x00, 0x00, 0x07, 0x07, 0x80, 0x07, 0xfe, 0x00, 0x1f, 0xf7, 0xf0, 0x00, 0x00, 
	0x00, 0x00, 0x1e, 0x7f, 0xe0, 0x07, 0x07, 0x80, 0x03, 0xfc, 0x00, 0x1f, 0x81, 0xf8, 0x00, 0x00, 
	0x00, 0x00, 0x3f, 0x3f, 0xe0, 0x0e, 0x07, 0x80, 0x01, 0xfe, 0x00, 0x3e, 0x00, 0x78, 0x00, 0x00, 
	0x00, 0x00, 0x3f, 0x3f, 0xe0, 0x1c, 0x07, 0x80, 0x00, 0xff, 0x00, 0x3e, 0x00, 0x7c, 0x00, 0x00, 
	0x00, 0x00, 0x1e, 0x7f, 0xe0, 0x1c, 0x07, 0x80, 0x01, 0xff, 0x80, 0x3c, 0x00, 0x3c, 0x00, 0x00, 
	0x00, 0x00, 0x0c, 0x00, 0x00, 0x38, 0x07, 0x80, 0x03, 0x8f, 0xc0, 0x3c, 0x00, 0x3c, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x07, 0x80, 0x07, 0x07, 0xe0, 0x3c, 0x00, 0x3e, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0xf0, 0x00, 0x70, 0x07, 0x80, 0x0e, 0x03, 0xf0, 0x3c, 0x00, 0x1e, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0xf0, 0x00, 0xe0, 0x07, 0x80, 0x1c, 0x00, 0xf8, 0x3c, 0x00, 0x1e, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x78, 0x00, 0xe0, 0x07, 0x80, 0x3c, 0x00, 0xf8, 0x3c, 0x00, 0x1e, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0xff, 0xe0, 0xff, 0xff, 0xf8, 0x3c, 0x00, 0x7c, 0x1c, 0x00, 0x1e, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0xff, 0xe0, 0xff, 0xff, 0xf8, 0x3c, 0x00, 0x7c, 0x1c, 0x00, 0x1e, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0xff, 0xe0, 0x00, 0x07, 0x80, 0x78, 0x00, 0x3c, 0x1e, 0x00, 0x1e, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0xff, 0xe0, 0x00, 0x07, 0x80, 0x78, 0x00, 0x3c, 0x1e, 0x00, 0x1c, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x80, 0x3c, 0x00, 0x3c, 0x0e, 0x00, 0x1c, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x07, 0x80, 0x3c, 0x00, 0x3c, 0x0f, 0x00, 0x3c, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0xe0, 0x00, 0x00, 0x07, 0x80, 0x3c, 0x00, 0x38, 0x0f, 0x00, 0x38, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0xf8, 0x00, 0x00, 0x07, 0x80, 0x1e, 0x00, 0x70, 0x07, 0x80, 0x78, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x07, 0x80, 0x0f, 0x00, 0xe0, 0x03, 0xc0, 0x70, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0xff, 0xc0, 0x00, 0x07, 0x80, 0x07, 0x81, 0xc0, 0x01, 0xf1, 0xe0, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x3f, 0xf0, 0x00, 0x07, 0x80, 0x03, 0xff, 0x80, 0x00, 0xff, 0x80, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x0f, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x0f, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x7f, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0xff, 0x1e, 0x00, 0x07, 0xff, 0x80, 0x00, 0x00, 0x00, 0x78, 0x3c, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0xfc, 0x0e, 0x00, 0x04, 0x00, 0x7f, 0x1f, 0xc0, 0x01, 0xcc, 0x66, 0x00, 0x00, 
	0x00, 0x00, 0x18, 0xf0, 0x02, 0x00, 0x04, 0x00, 0x3f, 0x3f, 0x80, 0x01, 0x8c, 0xc6, 0x00, 0x00, 
	0x00, 0x00, 0x3c, 0xc3, 0xc0, 0x00, 0x01, 0xf8, 0x3f, 0xbf, 0x00, 0x01, 0x99, 0x86, 0x00, 0x00, 
	0x00, 0x00, 0x7c, 0x03, 0xe0, 0x00, 0x1f, 0xfe, 0xff, 0xfe, 0xf8, 0x01, 0xb1, 0x8e, 0x00, 0x00, 
	0x00, 0x00, 0x78, 0x01, 0xe0, 0x00, 0x11, 0xff, 0x1f, 0xfd, 0xfe, 0x01, 0xe1, 0x8c, 0x00, 0x00, 
	0x00, 0x00, 0x78, 0x01, 0xe0, 0x00, 0x01, 0xff, 0x8f, 0xf9, 0xfe, 0x01, 0xe3, 0x0c, 0x00, 0x00, 
	0x00, 0x00, 0x70, 0x01, 0xe0, 0x01, 0xfd, 0xe7, 0xbf, 0xfb, 0xce, 0x03, 0x73, 0x0c, 0x00, 0x00, 
	0x00, 0x00, 0x70, 0x01, 0xe0, 0x01, 0x01, 0xe3, 0x87, 0xf0, 0x0e, 0x06, 0x33, 0x18, 0x00, 0x00, 
	0x00, 0x00, 0x78, 0x01, 0xe0, 0x01, 0x01, 0xe3, 0x87, 0xf0, 0x1e, 0x0c, 0x33, 0x30, 0x00, 0x00, 
	0x00, 0x00, 0x78, 0x01, 0xe0, 0x00, 0x3f, 0xe3, 0x8f, 0xf0, 0x3c, 0x0c, 0x63, 0x30, 0x00, 0x00, 
	0x00, 0x00, 0x3c, 0x03, 0xc0, 0x00, 0x21, 0xe7, 0x9f, 0xf8, 0x78, 0x07, 0xc3, 0xe0, 0x00, 0x00, 
	0x00, 0x00, 0x3e, 0x07, 0xc0, 0x00, 0x21, 0xff, 0xbf, 0xfc, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x1f, 0xff, 0x80, 0x00, 0x01, 0xff, 0x3f, 0xfd, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x0f, 0xff, 0x00, 0x00, 0x01, 0xfe, 0x7f, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x07, 0xfe, 0x00, 0x0f, 0xff, 0xf8, 0xfe, 0x7e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x01, 0xf8, 0x00, 0x08, 0x00, 0x01, 0xfc, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xf8, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};

// Array of all bitmaps for convenience. (Total bytes used to store images in PROGMEM = 2080)
// If you swap bitmaps above, be sure to swap their names to your new ones in this array
const int epd_bitmap_allArray_LEN = 2;
const unsigned char* epd_bitmap_allArray[2] = {
	turbo_off,
	turbo_on
};


////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////// STAR FIELD SCREENSAVER
//////////////////////////////////////////////////////////////// adapted from https://github.com/sinoia/oled-starfield/
////////////////////////////////////////////////////////////////

const int starCount = 512; // number of stars in the star field
const int maxDepth = 32;   // maximum distance away for a star

// the star field - starCount stars represented as x, y and z co-ordinates
double stars[starCount][3];

int getRandom(int lower, int upper) {
    /* Generate and return a  random number between lower and upper bound */
    return lower + static_cast<int>(rand() % (upper - lower + 1));
}

void drawStars() {
    int origin_x = 128 / 2;
    int origin_y = 64 / 2;

    // Iterate through the stars reducing the z co-ordinate in order to move the
    // star closer.
    for (int i = 0; i < starCount; ++i) {
       stars[i][2] -= 0.76;
       // if the star has moved past the screen (z < 0) reposition it far away
       // with random x and y positions.
       if (stars[i][2] <= 0) {
           stars[i][0] = getRandom(-25, 25);
           stars[i][1] = getRandom(-25, 25);
           stars[i][2] = maxDepth;
       }

       // Convert the 3D coordinates to 2D using perspective projection.
       double k = 128 / stars[i][2];
       int x = static_cast<int>(stars[i][0] * k + origin_x);
       int y = static_cast<int>(stars[i][1] * k + origin_y);

       //  Draw the star (if it is visible in the screen).
       // Distant stars are smaller than closer stars.
       if ((0 <= x and x < 128) 
           and (0 <= y and y < 64)) {
           int size = (1 - stars[i][2] / maxDepth) * 4;
           display.fillRect(x, y, size, size, 1);
       }
    }
}


////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////// MAIN
////////////////////////////////////////////////////////////////

// Begin with an initially impossible Turbo status. This will ensure the correct icon appears when the machine is started as the mode will be forced to switch.
int turboLastStatus = 99;
// Status 0 = Turbo Off, Status 1 = Turbo On
int turboStatus = 0;
// Accumulator of wait cycles to still allow polling for button changes while holding the processor icon on the screen (in cases you toggle in rapid succession)
int waitCycles = 0;

void setup() {
  Serial.begin(9600);

  // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
  if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
    Serial.println(F("SSD1306 allocation failed"));
    for(;;); // Don't proceed, loop forever
  }

  // initialize the turbo pin
  pinMode(turboPin, INPUT);

  // initialize random stars
  for (int i = 0; i < starCount; i++) {
    stars[i][0] = getRandom(-25, 25);
    stars[i][1] = getRandom(-25, 25);
    stars[i][2] = getRandom(0, maxDepth);
  }

  // Clear the buffer
  display.clearDisplay();
}

void loop() {
  turboStatus = digitalRead(turboPin);

  // if the button status has changed, show the icon for the new status
  if (turboStatus != turboLastStatus) {
    if (turboStatus == 0) {
      turboOff();
    } else if (turboStatus == 1) {
      turboOn();
    }
  }
  turboLastStatus = turboStatus;

  // run up the wait cycles, or swap to screensaver if enough have passed
  if (waitCycles < 1000) {
    delay(showIconFor);
    waitCycles = waitCycles + 1;
  } else {
    display.clearDisplay();
    drawStars();
    display.display();
  }
}

void turboOff() {
  display.clearDisplay();
  display.drawBitmap(0, 0, turbo_off, 128, 64, 1);
  display.display();
  waitCycles = 0;
}

void turboOn() {
  display.clearDisplay();
  display.drawBitmap(0, 0, turbo_on, 128, 64, 1);
  display.display();
  waitCycles = 0;
}

Turbo button bliss

With it all installed, now I can show off that there’s a cool Cyrix Cx486 DX2 80MHz chip in there even if it’s buried in the case where you’ll never see it. In this computer, Turbo is wired backwards so that having it engaged runs at 80MHz and disengaged drops to 4.77MHz. Disengaging the Turbo swaps to a little “i8088” icon depicting the classic chip.

Installed display showing an i8088 icon

The cool icons and animations you could use this for are endless. I was very seriously considering having the screensaver be the little Chrome pixel dinosaur jumping over cacti. Or maybe the Netscape Navigator loading animation.

Finished computer on desk

Naturally I had to go with wood grain on the case as an homage to the LGR wood grain PC, which initially got me into this. I highly recommend watching the original build and upgrade series.

Feb 8, 2022

I’m in the process of replacing my voice assistants with a more private, home-based solution that keeps everything inside the local network. I had originally decided on using Rhasspy in a server/satellite configuration to collect voice commands, and Home Assistant to manage turning things on and off. I changed to openHAB after trying and failing for hours to get Home Assistant’s Docker image to listen to and act upon Rhasspy intents, plus running into other annoyances like having no clear way inside the Docker version to do basic things like group lightbulbs in the same lamp post in a way that made sense to me.

image by EFF/Hugh D’Andrade, source/license

A key part of the project is keeping everything local-only, so I didn’t want any accounts on any cloud services, and I wasn’t open to using more than a Docker image for uniformity with my other services. Unfortunately, I found Home Assistant’s Docker image to be a cut-down version of the software that seemed to require constant off-documentation modifications to YAML files to try to achieve the same functionality as running on bare metal or in a VM with a cloud account. I didn’t trust that I’d remember half of the changes I made or why I made them a year from now. openHAB, on the other hand, seemed at home in Docker and didn’t appear to lock any functionality behind creating off-site accounts.

I was still pretty set on using Rhasspy, though, as I was finding it quite friendly and intuitive. Notably, Rhasspy has a Home Assistant setting for intent handling, but none for openHAB, so I knew I’d need to go off the books a bit.

Luckily Rhasspy will emit MQTT events for commands you define, and openHAB can listen for MQTT events just fine. So long as you can capture the data inside those events, there’s nothing stopping you from having openHAB run actions based on that data.

This is where it gets a little tricky, though. How do you catch an MQTT event, read the data inside it, and then act on it in openHAB?

What follows assumes you have working installations of Rhasspy and openHAB, plus some devices added.

Disclaimer

This is almost certainly not the best way to do this, and may just be a flat-out awful way to do it. My total experience with these systems is about one day. I’d love to hear from you if you have advice on more official/correct ways of setting up this integration.

Install MQTT Binding and JSScripting

To get started, go to Settings in openHAB and click Bindings under Add-ons. You’ll need to install the MQTT Binding add-on. Next, go back to Settings and click Automations under Add-ons. Install the JSScripting add-on. Both of these are official add-ons provided by openHAB and pop right into your installation with zero drama or trips back to the command line.

MQTT Binding Add-on page

Create an “MQTT Broker” Thing

Next, create a new Thing in openHAB under Settings > Things. Choose “MQTT Binding” as the binding, then “MQTT Broker” as the Thing type. Fill in the field “Broker Hostname/IP”, then click “Show advanced” and fill in “Broker Port” if you’re not running Rhasspy’s MQTT on 1883 or 8883. By default Rhasspy’s Docker image seems to run MQTT over 12183, so it’s likely you’ll need to fill in this setting.

MQTT Broker configuration screen

Next, you’ll need to configure channels for each of your Rhasspy intents. You’ll find these intents in your Sentences screen in Rhasspy.

I have two intents defined so far, one for RGB lights, and one for what I’m calling “binary devices,” which just take ON or OFF commands (and which I just realized I should have called “boolean devices,” but there’s no way I’m going back and redoing these screenshots now). Apologies for the complication here, but it seems my RGB lights don’t respond to ON and OFF, but instead work by setting their color temperature to turn on, and setting their RGB to 0,0,0 to turn off.

Switch to the channels tab of your new Thing to copy these over into openHAB. I’ve added a ChangeStateRGB and ChangeStateBinary channel on the MQTT Broker Thing to catch these intents:

Here’s the channel configuration–it looks basically the same in each. Note the “MQTT Topic” (e.g. hermes/intent/ChangeStateRGB). This is how we listen only to these events we want to hear and nothing else.

At this point, save the Thing. This is done and won’t be edited again unless you add new intents in Rhasspy.

Create an “MQTT Handler” Rule

Next, we need to do something when an event happens on the ChangeStateRGB or ChangeStateBinary channel of the new MQTT Broker Thing. These events will be picked up automatically whenever Rhasspy interprets commands, but right now openHAB is just ignoring them. To start paying attention, go to Settings and click “Rules” under Automations, then add a new rule.

In the When section, add all your channels from the MQTT Broker Thing. Any time MQTT Broker receives an update, we want to activate this rule.

Under the Then section, click Add Action and choose Run Script. Choose the scripting language “ECMAScript 262 Edition 11” (or higher if you’re reading this years from now). openHAB ships with 262 edition 5.1 included, but from what I’ve read you’re missing access to some variables in there and edition 11 is going to be a smoother experience.

At this point, we want to parse the data attached to the MQTT event, then do something with it. Here’s what an MQTT event coming from Rhasspy will look like:

{
  "input": "turn off the TeaRoomFloorLamp",
  "intent": {
    "intentName": "ChangeStateRGB",
    "confidenceScore": 1
  },
  "siteId": "default",
  "id": "5833d3ae-30f5-4790-9e01-c16787a353bd",
  "slots": [
    {
      "entity": "state",
      "value": {
        "kind": "Unknown",
        "value": "off"
      },
      "slotName": "state",
      "rawValue": "off",
      "confidence": 1,
      "range": {
        "start": 5,
        "end": 8,
        "rawStart": 5,
        "rawEnd": 8
      }
    },
    {
      "entity": "RGBLights",
      "value": {
        "kind": "Unknown",
        "value": "TeaRoomFloorLamp"
      },
      "slotName": "name",
      "rawValue": "tea room floor lamp",
      "confidence": 1,
      "range": {
        "start": 13,
        "end": 32,
        "rawStart": 13,
        "rawEnd": 32
      }
    }
  ],
  "sessionId": "5833d3ae-30f5-4790-9e01-c16787a353bd",
  "customData": null,
  "asrTokens": [
    [
      {
        "value": "turn",
        "confidence": 1,
        "rangeStart": 0,
        "rangeEnd": 4,
        "time": null
      },
      {
        "value": "off",
        "confidence": 1,
        "rangeStart": 5,
        "rangeEnd": 8,
        "time": null
      },
      {
        "value": "the",
        "confidence": 1,
        "rangeStart": 9,
        "rangeEnd": 12,
        "time": null
      },
      {
        "value": "TeaRoomFloorLamp",
        "confidence": 1,
        "rangeStart": 13,
        "rangeEnd": 32,
        "time": null
      }
    ]
  ],
  "asrConfidence": null,
  "rawInput": "turn off the tea room floor lamp",
  "wakewordId": null,
  "lang": null
}

This data is going to come through as a message with some junk in front of the JSON, but by coercing it to a string, running a regex match for the outer brackets, then JSON.parse()ing it, we get a clean object (todo: rewrite this in a sturdier, safer manner). At that point we can pull relevant information out like the intent, state, and device.

One thing to note when writing your script is to use var instead of let or const. No idea why, but running the rule back-to-back will cause collisions with variables that have already been defined with let or const, but it seems to run it clean each time with var.

Here’s my script (so far) that parses the MQTT message, then runs openHAB actions based on the intent, device, and state it pulled out of it:

/* Get the data from the MQTT event
 * {
 *   json: Event Object,
 *   intent: String (intent name),
 *   state: String ("ON" | "OFF"),
 *   device: String (device name)
 * }
 */
var data = {};
data.json = JSON.parse((event + "").match(/\{.*\}/)[0]);
data.intent = data.json.intent.intentName;
data.state = data.json.slots.find(e => e.slotName === "state").value.value.toUpperCase();
data.device = data.json.slots.find(e => e.slotName === "name").value.value;


console.log("MQTT rule triggered: " + data.intent + ", " + data.device + " " + data.state);

// Perform actions by MQTT intent
// object "items", function "items.getItem()", and function "<item>.sendCommand()" are provided by openHAB
if (data.intent === "ChangeStateRGB") {
  
  // handle ChangeStateRGB event
  if (data.state === "ON") items.getItem(data.device + "_ColorTemperature").sendCommand("35");
  if (data.state === "OFF") items.getItem(data.device + "_Color").sendCommand("0,0,0");
  
} else if (data.intent === "ChangeStateBinary") {
  
  // handle ChangeStateBinary event
  items.getItem(data.device).sendCommand(data.state);
  
}

If you need some visibility into your data here, you can tail -f /openhab/userdata/logs/openhab.log. Anything you console.log() here will show up in your terminal each time the rule is triggered. Just throw some test data at Rhasspy to repeatedly trigger it.

Next Steps

This is probably workable for most things I’m doing, although next I’ll need to figure out how to turn groups of items and areas on and off and pull data for dimming. Also of note is no feedback is going back out from here, so I won’t be getting responses when commands are executed yet. I’m sure this can all be tacked on now that I have working core functionality, though.

Let me know if this comes in handy for you, and what additional things you end up doing with it!

Fork me on GitHub