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.
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.
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:
|SLEEP_TIME||30||Number of ms to sleep between every cycle of the main loop|
|CYCLES_PER_SECOND||1000 / SLEEP_TIME||Number of code loops (cycles) per second|
|SECONDS_PER_MINUTE||60||The number of seconds there are in a minute (now that’s a constant!)|
|EXT_TEMP_UPDATE_SECONDS||15 * SECONDS_PER_MINUTE||How often (in s) to update the external temperature (the API call is a little slow)|
|INT_TEMP_UPDATE_SECONDS||SECONDS_PER_MINUTE||How often (in s) to update the internal temperature|
|BUTTON_IO||10||IO pin on the Raspberry Pi header to which the button is connected|
|PIR_IO||8||IO pin on the Raspberry Pi header to which the PIR sensor is connected|
|BUTTON_PRESS_INIT_VAL||CYCLES_PER_SECOND / 15||How long the button needs to be held down for to register a press – for deboucing (2 cycles)|
|BUTTON_HOLD_INIT_VAL||CYCLES_PER_SECOND * 3||How long the button needs to be held down to register a hold (3 seconds)|
|PIR_DETECT_INIT_VAL||1 * CYCLES_PER_SECOND * SECONDS_PER_MINUTE||How long the PIR detector has to go without detecting motion before putting the Nixie tube display to sleep (1 minute)|
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.
The variables are all initialized and kept track of locally in the Main() function. Here’s the table of these variables:
|Name||Value at Initialization||Description|
|button_press_timer||BUTTON_PRESS_INIT_VAL||How long the button must be held down for checkButton() to register a button press (this is done with the timer reaches zero)|
|button_hold_timer||BUTTON_HOLD_INIT_VAL||How long the button must be held down for checkButton() to register a button hold (puts the clock display to sleep or wakes it)|
|pir_timer||PIR_DETECT_INIT_VAL||How long the PIR sensor must go without detecting motion to put the clock to sleep (also done when the timer reaches zero)|
|current_mode||Mode.TIME||This 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_asleep||False||Whether the clock is asleep (True) or not (False)|
|IO_list||return value of initGPIOs()||The IO list of all the Raspberry Pi IOs connected to the decoders driving the Nixie tubes|
|temp||Current external temperature||The external temperature. This variable gets updated every EXT_TEMP_UPDATE_SECONDS|
|aqi_cat||Current AQI category||The current AQI (Air Quality Index) category, from good to hazardous. This variable also updates every EXT_TEMP_UPDATE_SECONDS|
|aqi_num||Current AQI number||The current AQI number, from 0 to 500. This variable also updates every EXT_TEMP_UPDATE_SECONDS|
|last_weather_time||Now||The last time the external weather was updated|
|int_temp||Current room temperature||The current room temperature|
|last_int_temp_time||Now||The last time the room temperature was updated|
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:
|kelvinToC||temp (in K)||Returns the Celsius equivalent of temp|
|celsiusToF||temp (in °C)||Returns the Fahrenheit equivalent of temp|
|fahrenheitToC||temp (in °F)||Returns the Celsius equivalent of temp|
|getTemperatureAPI||unit (Temp_Units)||Returns the temperature from the OpenWeatherMap API in the unit corresponding to unit|
|getAQIAPI||N/A||Returns the AQI category and number from the AirNow API|
|getCPUTemp||unit||Gets the CPU temperature in the unit corresponding to unit|
|getRoomTemp||unit||Gets 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.
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.