Posted on Leave a comment

The Retro Pi Nixie Clock

TLDR: Tubes that Light up Numbers, High Voltage, Russian Parts, Raspberry Pi! What Fun!

Nixie tubes are about the most compelling way to display numbers ever invented. They have light up numbers with a friendly orange glow. The Raspberry Pi is this cool little computer that can do so many things with so little effort. Why not put them together and make a retro clock that gets its time and date from the internet?

A word of warning before getting too much further into this. This project uses 200V-400V which can be lethal under the right circumstances. Construction of the high voltage circuits should be undertaken only with them powered off and off long enough for the high voltage capacitors to discharge.

If you have not heard of Nixie tubes, they are first type of numerical display from the dawn of the digital era. Invented by Burroughs Corporation, they were standard equipment in many types of measurement devices and industrial controls from the late 1950s to the late 1960s. The LED 7 segment display was introduced in the 1960s and its acceptance brought an end to the use of Nixie tubes in the west. However, they lived on for many years in the Soviet Union and Eastern Bloc countries. The Nixie tubes available for sale today are old stock as they have not been made for decades.

Nixie tubes are built with vacuum tube technology. Inside the glass envelope are 10 cathodes (the – terminals) in the shape of the numbers 0-9 plus a piece of fine wire mesh that forms the anode (the + terminal). To light up a number, connect the anode to the + of a power supply through a current limiting resistor and the number you want to light to the – of the power supply. Sounds just like what you do to light up a LED, right? It would, except for the part about 200V to light the thing. Actually, the Nixie tube is a cousin to the neon lamp and works just like one of those.

Won’t 200V bake the Pi?

To operate the 200V Nixies, it is a good idea to keep the 200V far away from the Pi. The internal circuitry of the Raspberry Pi operates at 5V and below. The General-Purpose Inputs and Outputs (GPIOs) on the Pi can only handle 3V. What to do? Time for another peek back in history.

Remember that the Nixie tube was invented in the late 1950s. This was well before integrated circuits were invented and not too long after transistors were introduced. At the time, digital logic was made with individual transistors (see the article in Nuts and Volts about making a clock with individual transistors), vacuum tubes or mechanical devices (yes, really!). The first popular family of integrated circuit logic chips, known as the SN7400 series, was introduced by Texas Instruments in October 1964 and later second sourced by many companies. These parts became wildly successful because they implemented hundreds of logic functions from simple gates to complex arithmetic functions. They also had a part, the SN7441, which could take a 4-bit representation of a decimal number, so called Binary Coded Decimal (BCD), and convert that to 1 of 10 outputs that can drive a Nixie tube directly.

Ah ha, you might say, problem solved! Almost. When the Nixies died out in the west, so did the SN7441. There is some dusty old stock available occasionally on EBAY and NTE has a part, also old stock, the NTE7441. Now back to the Soviet bloc. They could not get the SN7441 with the cold war and all, so they made their own. From Russia with love comes a virtually identical part, the K155ID1 (К155ИД1). This part is still available from Bulgaria on EBAY, surplus from the Cold War.

Interfacing to The Pi

The SN7441 requires 4 inputs to light up the Nixie tube with the appropriate number. We have 4 of these, so 16 digital outputs will be required. An easy way to get these outputs is with a PCF8574 I2C parallel port expander chip. The PCF8574 is to the I2C world as the shark is to the ocean world. Simple minded, fast, and easy to understand. It can only do one thing: read or write an 8-bit register connected to 8 pins. There are 8 possible addresses for these chips, selected by 3 address pins, starting at hex 38. The two chips in the system use address (hex) 38 and 39. The two of these will give the 16 outputs we need. In addition, it will remove the 200V for the Nixies one more step from contact with the Pi.

On the Pi, the I2C bus is called the SM bus. The clock program is written in Python because python is easy to use and understand. Also, Python has a good module to support SM bus.

You might ask why not use the GPIOs? There are more than enough pins to allocate the 16 needed to drive the SN7441s. If we were to use the GPIOs, it would require quite a bit of code to set them up and put the data out on the pins in the right format. By comparison, it only requires 4 lines of code to setup the SM bus and one line of code to write 8 bits (two digits) to one of the PCF8574s.

We don’t give up on the GPIOs completely though, we do use them to drive the 2 neon lamps for the colon when time is shown and 3 neon lamps when the dash is shown for date. The respective GPIO lines drive MMBTA42 high voltage transistors which handle the 200V nicely. It takes almost as much code to light up 2 groups of neon lamp punctuation as it takes to run 2 digits of Nixies.

The Pi uses the Linux operating system and has WIFI capability. Linux will access the internet and get the time and date for you as part of its normal function. The time and date are simple to get to from Python.

So, there you have your interface from 2 retro 200V Nixie tubes to the Raspberry Pi: two SN7441 ICs and a PCF8574. The next hill to climb: making suitable power supplies.

With a Few Cheap Tricks You Get a Power Supply

An AC adapter is a convenient way to get a step-down transformer and line cord all in one. This power supply uses a 12.6V AC center tapped transformer. By bringing the 12V center tapped AC into the box we can do a couple tricks.

The first trick is to use a two-diode full wave rectifier (D1 and D2) for the Pi 5V power supply. It produces about 8V which is filtered by C1 and regulated down to 5V for the Pi by a simple 5V 3A voltage regulator chip, the venerable LM323.

For the second trick, a 12.6V to 120/240V stepdown transformer is operated in reverse to produce the high voltage for the Nixies and the neon lamps. A transformer is just as happy to step up or step down as long as the proper voltage is applied to the windings. In this case, applying 12.6V to the 12.6V winding will yield 120/240V. The two 120V windings are connected in series to produce 240V which is rectified by BR101 and filtered by C102 to make about 330V.

The third Trick is a TL432 used as a voltage regulator. The TL432 is a slick little part. In a 3-pin transistor package is a micro-power op-amp, 2.5V precision voltage reference and an output transistor. It can be used as a programmable Zener diode, comparator, shunt regulator or as here, a voltage regulator. But look ma! There is no power supply pin!

The pins on the TL432 go by the usual names for a Zener diode: cathode and anode. In addition, the pin to set the voltage is called the reference. If it is put in a circuit with the anode grounded and a series resistor from the cathode to a supply voltage, the cathode will be at near the supply voltage until the reference pin goes above 2.5V. When that happens, the cathode will go to 2.5V. The op-amp gets its power from the grounded anode and the 2.5V (or more) from the cathode.

Normally, the TL432 is used for low voltages. The part itself only rated at 36V. So, the next trick is to make it work at 330V. That can be done by powering it from the handy Pi 5V power supply and teaming it up with our friend the MPSA42 transistor, the surface mount version of which, the MMBTA42, was used on the display board to drive the neon lamps. The MPSA42 is an NPN transistor and it has a cousin, the MPSA92, a PNP device.

When the power is first turned on, the output capacitor will not have any voltage on it. That means that the voltage on the output of the voltage divider made up of XXX will not have any voltage on it and the reference pin on the TL432 will not have any voltage on it. Since this pin is below 2.5V, the cathode will have 5V on it. Q101 will be turned on with the 5V coming from the cathode of the TL432. The resistor R109 and Zener diode Z1 bias the emitter of Q101 so that it can turn off when the TL432 is in regulation. When Q101 turns on, the voltage on its collector will fall which will reduce the voltage on the base of Q103 turning on Q103 and Q104. This will then charge the output capacitor through R108. R108 is there to keep Q104 from blowing up as it charges the output capacitor during power up.

As the voltage on the output capacitor starts to rise, the voltage divider output will start to rise. When the voltage on the reference pin of the TL432 approaches 2.5V, the cathode voltage will begin to come down. This causes Q101 to reduce its collector voltage which in turn makes Q103 and Q104 conduct less. As all of this happens, the output comes into regulation.

This power supply can be easily adapted to other voltages by simply changing the voltage divider to output 2.5V when the output is at the voltage you want.

Finding Parts is an Easter Egg Hunt that Spans from Your Closet to Around the Globe

Just like an Easter Egg hunt, there are some items that are easy to find and some that take more effort. Most of the parts in this project can be found at the usual gang of suspects: Digikey, Mouser, Newark and so on. The Nixie tubes can be found on EBAY. If the B5092 Nixie is not available, the circuit can be used with the only modification being the pinout of the Nixie. There is a vendor in Bulgaria that sells on EBAY who has some similar Russian Nixies and the Russian version SN7441. The 12-pin socket is also available on EBAY from a vendor in China.

The case is a 11” x 8” x 4 ½” “stash” box available on Amazon. Buying the box already made saves a lot of time, but a homemade or other suitable box can be used. The clear Plexiglas (acrylic) is a common item that can be purchased at Home Depot. Orange Plexiglass is somewhat more unusual but can be found on Amazon.

If you haven’t thrown out that old PC, dig it out of the closet and pull out the IDE disk drive cable. The clock needs the same 40 pin ribbon cable as the disk drive. Sometimes, these cables have 3 connectors on them. If yours doesn’t, simply press a third one on at the right place for the display board.

Do Judge a Book by Its Cover

Most people have never seen a Nixie tube display and will be wowed by the box that has the amazing orange numbers. Having a nicely made box will impress people as much as the technology inside. A beautiful case will let you bring your clock out of the workshop and into the living room and create a great conversation piece.

If you use the stash box, the only things that need to be done to it are:

  • Route out the display window
  • Put a hole in the back for the AC cord
  • Mount the transformer
  • Attach rubber feet

Routing out the display window can be done with a router or a mill. If you happen to have access to a CNC router at a school, machine shop or maker space, that the best of all. The Nixie Clock in this project was made on a CNC mill. The opening in the box should be made with a 1/8″ lip around the inside for the Plexiglass to sit on. Similarly, the Plexiglass should be routed with a 1/8″ lip on it. When it is done, the Plexiglass should be a press fit in the case.

Holding the Plexiglass while routing it can be problematic. The easiest way to hold it is to first cut a piece that is about 3/4” bigger than needed. Then, tape it to a piece of wood that size. Blue painter’s tape works well for this and it won’t leave any adhesive.

The piece of Plexiglass holding the display and Pi was cut to size on a table saw and the mounting holes for the display boards and Pi were drilled on a drill press. This piece of Plexiglass was secured to the case with strong double-sided tape.

The Software: Don’t Panic!

Solder is the preferred programming language for most people that like to build stuff. But don’t panic if you don’t know how to program. The Raspberry Pi makes it easy.

The Pi will have to be set up and the program in the listing will have to be loaded. To set up the Pi, you will need a mouse, keyboard, power supply and monitor. Again, that old PC you dug out of the closet comes to the rescue. You can use the USB keyboard and mouse from it. Newer TVs have an HDMI port which will connect directly to the HDMI port on the Pi. You can also use a VGA monitor from your old PC with a HDMI to VGA converter. These are available at Walmart, Bestbuy or on Amazon among others. The final part you will need is a 5V 2A AC adapter with a micro USB connector on it. The easiest way to get that is to use a regular USB cube charger and get a USB to micro USB cable at the local drug store if you don’t have one from an old phone.

A Raspberry Pi model 3 B+ was used for this project. A plain model 3 will also work. When you buy it, get a micro SD card with NOOBS (New Out Of the Box Software) loaded on it. The Pis are available from many suppliers on line including Adafruit, Newark and Amazon.

After all the cables are connected, be sure that the Micro SD card is inserted with the contacts facing the circuit board. Apply power to the board and a torrent of messages will display on the screen and a short time later a desktop like a Windows desktop will appear.

The clock gets its time from the internet. The internet needs to be enabled before the clock will have the right time. You probably noticed that there are no buttons to set the time. That’s because the internet time is accurate, and it knows your time zone. Click on the WIFI logo and enter the password to your network. This will enable the WIFI and the WIFI logo will change to reflect the signal strength. The time displayed in the upper right corner of the screen will change once the Pi has synced to the internet time.

To get the Python program that runs the clock, down load it from the Nuts and Volts website www.nutsandvolts.com/programlistings/clock.py. The Pi browser (world icon) is a version of Chrome which works more or less like the version for Windows. After you have located the listing, copy it to the clip board by highlighting the text and pressing control C, just like windows. Next, open the Python programming environment by clicking on the Raspberry icon in the upper left of the screen and selecting programming and Python 3. In the Python environment, click new. Click in the upper left corner of the just opened window and press control + V. Your listing will appear. Press control + shift + s to open the save as window. Name the program “clock” and click save. The program will be saved in the home/pi directory.

Only one last step to do. Let’s make the program auto start when the Pi boots up so that it will not require as screen, keyboard and mouse. To do that, the file that holds the commands that are to be executed after the graphic user interface comes up, needs to be edited to add the command to run the clock program. Open the “terminal” (>_ icon on the top of the screen) and enter the following command (paying attention to what is upper case!):

Sudo nano ~/.config/lxsession/LXDE-pi/autostart

This opens a simple text editor called “nano” and displays the contents of the autostart file. This editor does not use the mouse, everything is done from the keyboard. Go to the end of the file by pressing the down arrow past the last line of text. Add this line at the end:

@sudo /usr/bin/idle3 -r /home/pi/clock.pi

Press control + x to exit. You will be asked if you want to save the changes, type Y. You will be asked to save in the file …..autostart. Press enter. You are done.

Where to go from here

The clock uses very little of the Rasperry Pi’s resources. It can do many things besides run the clock. Some things may need a keyboard/mouse and monitor. There are a variety of wireless keyboards with a mouse pad that will work well. If you have a television that has an HDMI port, you can use that as a monitor.

With a television and keyboard/mouse you can access Youtube, Netflix and Amazon to play videos (some additional software may be required). The Pi comes with a good suite of applications loaded on the NOOBS micro SD card. These include a browser, spread sheet, word processor, data base as well as a host of handy accessories like a backup program.

The Nixie Clock will make a great addition to any living room or den. But be warned, If you set it next to your TV, getting caught up in just one more episode of Star Trek will no longer be an excuse to not do that chore you promised to do at 8:30.

Posted on Leave a comment

Grokking Streams and Fighting Zombies

TLDR: Streams are nothing more than a convenient way of handling callbacks, and they can cause a zombie outbreak if you are not careful, because callbacks ARE a type of reference to the creating object.

The Unnecessarily Long Introduction

Between RxJS clocking in at 15 million weekly (yes, seriously, weekly) downloads and RxDart exploding in popularity, it looks like streams, as a paradigm, are here to stay. But what the hell are streams? Are they a special language syntax? A magical data structures that just somehow “flow” data? Or, are they just a convenient way of handling async callbacks with a ton of functionality (and complexity) built on top of a very simple concept? Let’s take a look.

Initially, I started to learn about streams for a Bluetooth project I was writing in Flutter. In case you have been living under a rock, or simply don’t keep up with all the latest mobile development trends (Inconceivable!), Flutter is this really cool framework that Google “graciously” released a few years ago. In reality Google was tired of people releasing all the cool apps for iOS first, and they decided to make a cross platform toolkit so that at the very least, if they can’t get devs to code for Android first, they would get them to code for both platforms at the same time. Well, what can I say, it worked, they got me.

My current project is an app for a bluetooth enabled battery management system (BMS) for large scale lithium batteries. The app pulls data from the BMS about the state of the battery, and allows the end user to set settings on the BMS. Overall, it is a pretty simple 2-3 screen app with only complex bits residing in the BMS connection negotiation code. We needed to support iOS and Android out of the box, so this app was perfect for Flutter!

Flutter doesn’t support Bluetooth out of the box, but Buffalo Inc, the people who make portable hard drives and a bunch of other stuff, were gracious enough to release Flutter Blue, an open source bluetooth library. The library is based around RxDart, and handles everything via streams. So, I had to roll up my sleeves and finally figure out streams properly.

Finally, Streams!!!

So, what are streams and why do you need them? Well, for that bit of info I’ll let you read or watch any number of explanations from countless sources on the internet. You see, you have to get properly confused and desperate before you’ll become inspired to continue reading this blog post 😉 But, in short, streams are pitched as this magical alternative to callbacks. People analogize streams to data flows or to factory assembly lines. They are neither.

Streams are literally just another way to handle good old callbacks. It took me banging my head on my desk (literally), to understand that little bit of insight. Now I am going to try to convey that insight to you by reinventing the wheel and making our very own stream using nothing more magical than classes and callbacks. My hope is that by showing you what streams look like under the hood you will be able to think about them in a more accurate way, and would avoid the zombie object issues I had to deal with ZombieLand style.

(What, you don’t think your project has zombies? Have you checked? Read on, you may find that you have a zombie infestation as well, and you just can’t see it.)

I know JS is taking over the world, but I’ll take this moment to also introduce you to Dart. Dart is the language used to program Flutter apps in. It’s also a pretty cool little language in it’s own right, and it compiles to vanilla JS, as well as native code! It’s strongly typed, very strongly typed, not like that promiscuous TypeScript, and it has great out of the box support for Streams. If you are unfamiliar with Dart, just read the code examples below while omitting all of the type annotations and it will basically read like JS.

To run any of the code examples simply use DartPad and run it directly in the browser (remember, dart compiles to JS, after all).

Normal Dart Streams

So, first, let’s take a look at a very simple example of using the streams built right into Dart. Read through the code quickly and see if you can predict what the output will be.

import 'dart:async';

void main() {
  MyPrettyLittleZombie zombie = MyPrettyLittleZombie();

  Future.delayed(const Duration(seconds: 10), () {
    print("Attempting to garbage collect the zombie by nulling its reference.");
    zombie = null;
  });
}

class MyPrettyLittleZombie {
  MyPrettyLittleZombie() {
    ns = NormalStream();

    sub = ns.stream.listen((counter) {
      print("Zombie said $counter! It's still coming straight for us!");
    });

    Future.delayed(const Duration(seconds: 15), () {
      print("OK, that didn't work. Let's try unsubscribing.");
      sub.cancel();
    });
  }
  NormalStream ns;
  StreamSubscription sub;
}

class NormalStream {
  NormalStream() {
    Future.delayed(const Duration(milliseconds: 1000), counterIncrement);
  }

  StreamController<int> streamController = StreamController<int>.broadcast();
  Stream<int> get stream => streamController.stream;

  // Counter Stuff
  int counter = 0;
  void counterIncrement() {
    if (streamController.hasListener) {
      counter++;
      streamController.sink.add(counter);
      Future.delayed(const Duration(milliseconds: 1000), counterIncrement);
    }
  }
}

Ok, so what do you think? Do you know what the output will look like? Let’s take a look at the output, and then we will go quickly through the code section by section. Here is the output from above:

Zombie said 1! It's still coming straight for us!
Zombie said 2! It's still coming straight for us!
Zombie said 3! It's still coming straight for us!
Zombie said 4! It's still coming straight for us!
Zombie said 5! It's still coming straight for us!
Zombie said 6! It's still coming straight for us!
Zombie said 7! It's still coming straight for us!
Zombie said 8! It's still coming straight for us!
Zombie said 9! It's still coming straight for us!
Attempting to garbage collect the zombie by nulling its reference.
Zombie said 10! It's still coming straight for us!
Zombie said 11! It's still coming straight for us!
Zombie said 12! It's still coming straight for us!
Zombie said 13! It's still coming straight for us!
Zombie said 14! It's still coming straight for us!
OK, that didn't work. Let's try unsubscribing.

Did you get it right? Are you a little surprised that the stream capped on counting even after we set zombie = null? Like JS and many other garbage collected languages, Dart should have double tapped that zombie after we nulled its one and only reference. But was that really the only reference to our zombie? Or, perhaps, just maybe, another reference to our zombie is created when it runs the listen function? Is listen just a really shitty analogy to what we are actually doing? All questions will be answered, but first let’s look closely at the code.

Dart starts running all code from the main function, just like C and many other languages. If you are more familiar with JS, just pretend that everything in main is at the top of the file.

void main() {
  MyPrettyLittleZombie zombie = MyPrettyLittleZombie();

  Future.delayed(const Duration(seconds: 10), () {
    print("Attempting to garbage collect the zombie by nulling its reference.");
    zombie = null;
  });
}

In the above section we instantiate the MyPrettyLittleZombie class into a zombie object. (If this sentence is confusing, go and watch some YouTube videos on object orientated programming. It’s boring, and I am not covering it.)

The next bit is simple, though, through the pure genius of Google engineers, it’s made in the most complex way possible. Basically 10 seconds from when the code runs I want it to do something. Think setTimeout in JS.

The bit that I want it to do is to set the zombie object to null, which should (theoretically) cause it to get garbage collected. But it doesn’t, as you can see in the output. Why? That’s what we aim to find out. Let’s take a look at what our zombie looks like.

class MyPrettyLittleZombie {
  MyPrettyLittleZombie() {
    ns = NormalStream();

    sub = ns.stream.listen((counter) {
      print("Zombie said $counter! It's still comming stright for us!");
    });

    Future.delayed(const Duration(seconds: 15), () {
      print("OK, that didn't work. Let's try unsubscribing.");
      sub.cancel();
    });
  }
  NormalStream ns;
  StreamSubscription sub;
}

Our zombie, as most zombies, is a pretty simple little creature, it only has 2 properties, one real function, and one safety measure (you know, just in case our garbage collector fails to double tap the bastard). At the top, we assign ns to a new instance of NormalStream. NormalStream is just an implementation of a stream using Dart’s built in Stream classes. Upon instantiation, NormalStream starts broadcasting an incrementing integer, pretty simple.

After instantiating NormalStream we “subscribe” to updates from our instance of NormalStream by using the listen function for the stream. The analogy is that as soon as we hear the stream emit (send out) a new value, we will run some code on that value, in our case just print it to the console. Pretty simple. The sub variable stores a reference to our subscription. It’s like a receipt, you’ll need it to unsubscribe.

Finally, we add a safety that will unsubscribe our zombie from NormalStream updates after 15 seconds by using the sub object we talked about above.

Pretty simple stuff, no? Now let’s look at our final bit of code, the NormalStream class.

class NormalStream {
  NormalStream() {
    Future.delayed(const Duration(milliseconds: 1000), counterIncrement);
  }

  StreamController<int> streamController = StreamController<int>.broadcast();
  Stream<int> get stream => streamController.stream;

  // Counter Stuff
  int counter = 0;
  void counterIncrement() {
    if (streamController.hasListener) {
      counter++;
      streamController.sink.add(counter);
      Future.delayed(const Duration(milliseconds: 1000), counterIncrement);
    }
  }
}

If you are familiar with streams, nothing here should be very surprising. Basically, we setup a StreamController and a Stream, and then add some logic to emit the incrementing integers once a second. The StreamController handles all of the additional helper logic around the Stream object.

To send a new value to the stream we run the streamController.sink.add(counter) function with counter as the integer we want to emit. Why is it called a sink? Because developers have a penchant for having really terrible analogies for simple concepts. In any case, by adding a new value to the sink, that value will be emitted on the stream.

Finally, the counterIncrement function calls itself over and over again. Yes, I know, it’s tempting to sound smart by calling this recursion, but recursion it is not.

So that’s it! We looked at all the code. Do you now know why our zombie refuses to die after we null its reference? No? Ok, let’s keep going. In the hopes of giving you the same “aha” moment I had earlier this month, let me now rebuild the same code with our own “Stream” class, instead.

Reinventing the Wheel, or rather, the Stream

To understand a bit better what’s happening under the hood of a Stream object let’s make our own, very simple, implementation. Yes, we will have Streams without any libraries or built in classes. In fact, you could easily implement the code below using vanilla JS (again, dart is compiled to vanilla JS). So, first, like before, let’s take a look at the code.

import 'dart:async';

void main() {
  MyPrettyLittleZombie zombie = MyPrettyLittleZombie();

  Future.delayed(const Duration(seconds: 10), () {
    print("Attempting to garbage collect the zombie by nulling its reference.");
    zombie = null;
  });
}

class MyPrettyLittleZombie {
  MyPrettyLittleZombie() {
    ns = ReinventedStream();

    sub = ns.listen((counter) {
      print("Zombie said $counter! It's still coming straight for us!");
      return;
    });

    Future.delayed(const Duration(seconds: 15), () {
      print("OK, that didn't work. Let's try unsubscribing.");
      sub.cancel();
    });
  }
  ReinventedStream ns;
  ReinventedStreamSubscription sub;
}

class ReinventedStream {
  ReinventedStream() {
    Future.delayed(const Duration(milliseconds: 1000), counterIncrement);
  }

  // Stream Sub and Unsub Methods and Vars
  Map<int, void Function(int)> subCallBacks = Map<int, void Function(int)>();
  int subCounter = 0;

  ReinventedStreamSubscription listen(void Function(int) callback(int data)) {
    final ReinventedStreamSubscription sub = ReinventedStreamSubscription(subCounter, this);
    subCallBacks[subCounter] = callback;
    subCounter++;
    return sub;
  }

  void unlisten(int sub) {
    subCallBacks.remove(sub);
  }

  // Stream Sink Methods and Vars
  int last;
  void add(int newData) {
    last = newData;
    subCallBacks.forEach((index, callBack) {
      callBack(newData);
    });
  }

  // Counter Stuff
  int counter = 0;
  void counterIncrement() {
    if (subCallBacks.isNotEmpty) {
      counter++;
      add(counter);
      Future.delayed(const Duration(milliseconds: 1000), counterIncrement);
    }
  }
}

class ReinventedStreamSubscription {
  ReinventedStreamSubscription(this.pos, this.stream);
  final int pos;
  ReinventedStream stream;

  void cancel() {
    if (stream != null) {
      stream.unlisten(pos);
      stream = null;
    }
  }
}

Because this post is already getting ridiculously long, I’ll skip copy and pasting the console output here, but take my word for it that it looks identical to the output from the normal stream implementation above. Or, if you are not the trusting type, just run it in DartPad.

The first bit of this code is essentially identical to what we looked at above. In the MyPrettyLittleZombie class we use ns.listen instead of ns.stream.listen, but that’s only because I was too lazy to split my Stream Implementation into 15 different classes. But generally, the main function and MyPrettyLittleZombie class are identical. The difference are in the ReinventedStream and ReinventedStreamSubscription classes. So let’s take a look at those. What do these magical streams actually look like under the hood?

First, let’s take a look at the ReinventedStream class. I’ll skip over periodic number emitting logic, as it’s identical to the normal implementation above, and will instead concentrate on the “stream” bits.

Map<int, void Function(int)> subCallBacks = Map<int, void Function(int)>();
int subCounter = 0;

Here we set up 2 properties. The subCallBacks property is a Map that has an integer key index and stores a void Function(int) callback. If we were making a proper generic Stream object we would have used a generic type, of course, but for this example a hard coded int type will be sufficient.

The other property, subCounter, is used to generate unique index values for the subCallBacks map. (BTW, a Map is similar to named array, HashMap, or a Named List, in other languages. Every language, well, almost every language, has one, and every language has to invent a new name for the same damn concept.)

ReinventedStreamSubscription listen(void Function(int) callback(int data)) {
  final ReinventedStreamSubscription sub = ReinventedStreamSubscription(subCounter, this);
  subCallBacks[subCounter] = callback;
  subCounter++;
  return sub;
}

void unlisten(int sub) {
  subCallBacks.remove(sub);
}

In this bit of code we define our listen and unlisten functions. Again, terrible names! They should be called addCallBack and removeCallBack, but I am using the nomenclature of the magical streams here.

The listen function stores the call back passed to it from the outside, in our case, from our zombie object, into the subCallBacks map. The unlisten function removes the callback at a given index. Again, pretty simple so far. The next bit is where the “magic” happens.

int last;
void add(int newData) {
  last = newData;
  subCallBacks.forEach((index, callBack) {
      callBack(newData);
  });
}

Yep, you made it! Now you should be having that “aha” moment I was talking about! This is the function that “sinks” or adds new values to the stream. Upon adding a new value it calls all of the callbacks one at a time, and sends to them the new value.

As you may know, callbacks run in the context they are created in, NOT the context they are called in. The latter would make them pretty useless in anything other than globally scoped programs. So, to properly run the callback that we set up inside the zombie object, the instance of the ReinventedStream has to maintain a reference to the zombie object! And, of course, since we instantiated the ReinventedStream inside the zombie object, they are now cross linked, and will not get garbage collected until we delete the callback stored in the subCallBacks map of the ReinventedStream instance by unsubscribing from the stream.

As an aside, this is an implementation of a broadcast stream. A regular stream can only have one listener. A broadcast stream can have many listeners, but you can’t guarantee which listener’s callback will run first. You can see how you can easily use this code to create your own stream library with priority settings, where you can assign some callback to run first, and then have groups of callbacks. For instance, group A can run after the first callback, group B next, and so on. I can think of a few UI applications where that would be very useful, but I have digressed.

Finally, let’s just take a quick look at the ReinventedStreamSubscription class, just for completeness, after all, we have figured out our object zombification issue now, no?

class ReinventedStreamSubscription {
  ReinventedStreamSubscription(this.pos, this.stream);
  final int pos;
  ReinventedStream stream;

  void cancel() {
    if (stream != null) {
      stream.unlisten(pos);
      stream = null;
    }
  }
}

This class is instantiated by the listen function of the ReinventedStream class and returned to the subscriber so that the subscriber could cancel the subscription. It really should be called a subscription receipt. The code is pretty self-explanatory, the cancel function reaches out to the instance of its creator and runs unlisten on a given callback, as identified by its integer index.

Conclusion

Wow, you are still reading? Well, I am tired of typing, so let’s keep this short. Streams are awesome, but you MUST remember to unsubscribe from all the streams before disposing of an object or you will suffer from a zombie uprising of an unstoppable magnitude (or just a memory leak). Happy hunting!