Nixie Clock Project (Part 6 – Code)

I haven’t posted in a little while – I’ve been putting off soldering down the nixie tubes onto the board (mostly because it’s a little annoying to bend the pins so that the tubes are vertical), so I’ve been working on writing the code for the project. I’m periodically interspersing the tube soldering, so I’ve only got two left! Anyway, I realize nobody wants to just see a bunch of code on a blog post, so bear with me as I try to make the information a little more digestible while preserving the important bits of this part of the project.

Language

The Raspberry Pi will run basically anything you’d like on it as it’s just a Linux box. I decided to code this up in Python for simplicity and because I’ve done plenty of GPIO toggling in it before. However, you may notice that the design is very procedural, as if it were written in C – I guess my mind went to that immediately when I thought of doing an embedded systems project; now that I think about it, it may be more prudent to design the code in a more object-oriented kind of way – by, for instance, creating instances of objects for each nixie tube that hold member variables for their IO lists and states as well as member functions for writing a digit and so forth. I may overhaul the code, but this works for now.

Initialization

I designed the code such that there would be no global variables, although I do have “constants” (Python can’t enforce that they stay constant) that are global. Here’s the constants, divided by category:

NameValueDescription
SLEEP_TIME30Number of ms to sleep between every cycle of the main loop
CYCLES_PER_SECOND1000 / SLEEP_TIMENumber of code loops (cycles) per second
SECONDS_PER_MINUTE60The number of seconds there are in a minute (now that’s a constant!)
EXT_TEMP_UPDATE_SECONDS15 * SECONDS_PER_MINUTEHow often (in s) to update the external temperature (the API call is a little slow)
INT_TEMP_UPDATE_SECONDSSECONDS_PER_MINUTEHow often (in s) to update the internal temperature
Constants for keep track of time
NameValueDescription
BUTTON_IO10IO pin on the Raspberry Pi header to which the button is connected
PIR_IO8IO pin on the Raspberry Pi header to which the PIR sensor is connected
BUTTON_PRESS_INIT_VALCYCLES_PER_SECOND / 15How long the button needs to be held down for to register a press – for deboucing (2 cycles)
BUTTON_HOLD_INIT_VALCYCLES_PER_SECOND * 3How long the button needs to be held down to register a hold (3 seconds)
PIR_DETECT_INIT_VAL1 * CYCLES_PER_SECOND * SECONDS_PER_MINUTEHow long the PIR detector has to go without detecting motion before putting the Nixie tube display to sleep (1 minute)
Constants for IO interfaces

In addition to the above constants, there’s also a few constants required for the API calls (the strings and API keys) in the file nixie_helpers.py, as well the constant ROOM_TEMP_OFFSET, which is the temperature offset in Fahrenheit between the CPU temperature and the room temperature. I calibrated this out with a thermometer and it’s around 34°F at my place.

The sole initialization function is initGPIOs(), which builds and returns a list of lists, where each one of the lists corresponds to the IO pins connected to the four bits driving the nixie tube decoders of that digit. So, for instance, the numbers in dig_H1 = [11, 7, 5, 3] correspond to the four IOs connected to the four binary bits on the decoder driving the tube of the first hour digit. This (and the rest of the connectivity) is illustrated below.

The function then initializes these GPIOs, configures them as outputs, and initializes the input GPIOs corresponding to the button and PIR sensor. The list of nixie tube IOs is returned to the Main() function to be used later for displaying the digits.

Variables

The variables are all initialized and kept track of locally in the Main() function. Here’s the table of these variables:

NameValue at InitializationDescription
button_press_timerBUTTON_PRESS_INIT_VALHow long the button must be held down for checkButton() to register a button press (this is done with the timer reaches zero)
button_hold_timerBUTTON_HOLD_INIT_VALHow long the button must be held down for checkButton() to register a button hold (puts the clock display to sleep or wakes it)
pir_timerPIR_DETECT_INIT_VALHow long the PIR sensor must go without detecting motion to put the clock to sleep (also done when the timer reaches zero)
current_modeMode.TIMEThis is set to an IntEnum type that tracks the current mode – TIME, INT_TEMP (CPU temp), or ALTERNATE. I’m planning on adding an EXT_TEMP to get the external temperature later.
is_asleepFalseWhether the clock is asleep (True) or not (False)
IO_listreturn value of initGPIOs()The IO list of all the Raspberry Pi IOs connected to the decoders driving the Nixie tubes
tempCurrent external temperatureThe external temperature. This variable gets updated every EXT_TEMP_UPDATE_SECONDS
aqi_catCurrent AQI categoryThe current AQI (Air Quality Index) category, from good to hazardous. This variable also updates every EXT_TEMP_UPDATE_SECONDS
aqi_numCurrent AQI numberThe current AQI number, from 0 to 500. This variable also updates every EXT_TEMP_UPDATE_SECONDS
last_weather_timeNowThe last time the external weather was updated
int_tempCurrent room temperatureThe current room temperature
last_int_temp_timeNowThe last time the room temperature was updated
Local variables for this project

Helper Functions

There’s a few short functions in the file aptly named nixie_helpers.py. They’re pretty self-explanatory, so I’ll just summarize them in a table:

NameArgumentDescription
kelvinToCtemp (in K)Returns the Celsius equivalent of temp
celsiusToFtemp (in °C)Returns the Fahrenheit equivalent of temp
fahrenheitToCtemp (in °F)Returns the Celsius equivalent of temp
getTemperatureAPIunit (Temp_Units)Returns the temperature from the OpenWeatherMap API in the unit corresponding to unit
getAQIAPIN/AReturns the AQI category and number from the AirNow API
getCPUTempunitGets the CPU temperature in the unit corresponding to unit
getRoomTempunitGets the room temperature (CPU temperature with an offset) in the unit corresponding to unit

Software Loop Design

I designed this as a polling based system that checks on the status of the IO inputs (the button and the PIR), updates its internal variables with the current time, weather, and CPU temperature, and writes to the IO outputs (the decoders driving the Nixie tube) every so often (so that there are CYCLES_PER_SECOND number of cycles per second). After it has completed its tasks, the loop sleeps for SLEEP_TIME (in milliseconds) and starts again from the beginning.

This is the piece of code that the Raspberry Pi continuously runs every cycle. The first function that is called is the checkButton() function. This function takes in the current values of the timers for pressing and holding the button, and decrements them if the button is pushed; otherwise, the timers are reset if the button is not pushed. The function returns the new timer values as well as booleans for whether the button was pushed or held. The function does not register continuous button presses – that is, the button must be released in between two button presses for it to count as two presses instead of one long button press.

Next, checkPIR() is called. This works in a similar way to the above – it takes in the pir_timer and decrements it if is not detecting motion this cycle; otherwise, it resets it. The function returns the new timer as well as a boolean indicating whether no motion has been detected for the full PIR_DETECT_INIT_VAL time.

After this, getDisplayString() is called – this function takes in current_mode, is_asleep, and a value (time, temperature tuple, or AQI tuple) and uses these parameters to determine what to display on the nixie tubes – whether that’s the time, the temperatures, the AQI, or turning the display off. This function returns a string in the form “DDDDDD”, where every “D” is a digit from 0 to 9, or the character “A” if that particular digit is meant to be turned off.

Finally, updateDisplay() is called with IO_list and the string returned from getDisplayString() as arguments and displays the correct string on the nixie tubes or throws an exception if it gets a malformed string as an input.

After all this, the loop repeats and again begins by polling the inputs.

Full Code

Please also note that, since I forgot to order a push-button, I haven’t tried that part of the code yet! You can find the repository (hardware design and code) on the main project page. Or you can browse the two files directly on the repository by clicking the following links: main.py and nixie_helpers.py.