Configuring the Raspberry Pi as a TARDIS Hardware Controller

The Raspberry Pi at the heart of my smart TARDIS project has gone through a few different roles.

Originally, quite a lot of the automation ran directly on the Pi. As the wider home lab developed, the predictable scheduling and choreography were moved into a central Node-RED instance instead.

That gave me a cleaner architecture:

  • Node-RED decides when scheduled events should happen
  • MQTT carries simple commands
  • the Raspberry Pi handles the actual hardware
  • Python scripts control GPIO, lighting and audio

More recently I have added another layer.

The Raspberry Pi can now also generate its own local TARDIS events, including random temporal disturbances, failed launches, damage effects and reactions to an internet outage.

The result is a hybrid architecture where central automation handles the normal routine, while the physical TARDIS has a small amount of autonomy of its own.


The Current Architecture

The Raspberry Pi is named:

NoobSaibot

For normal scheduled automation the flow looks like this:

Home Assistant / Node-RED
          │
          │ decides what should happen
          ▼
        MQTT
          │
          ▼
      Pi Agent
          │
          ▼
 GPIO / Lighting / Audio
          │
          ▼
        TARDIS

Node-RED remains responsible for predictable events such as:

  • sunrise power-up
  • sunset sequences
  • hourly Cloister Bell
  • scheduled power-down
  • longer choreographed take-off and landing sequences

For example:

Sunset
   │
   ▼
Power Up
   │
   ├── Boom
   ├── Take Off
   ├── In Flight
   └── Landing

The Pi does not need to know about all of the delays involved.

It simply receives each command and executes the corresponding local effect.

That part of the design has not changed.

What has changed is that NoobSaibot now also runs a separate local event engine.

                    NoobSaibot
                        │
          ┌─────────────┴─────────────┐
          │                           │
       Pi Agent                 Local Events
          │                           │
     MQTT commands              Random events
     from Node-RED              Internet watcher
          │                     Warning effects
          └─────────────┬─────────────┘
                        │
                        ▼
                GPIO / Audio
                        │
                        ▼
                      TARDIS

The important distinction is that the local event engine supplements Node-RED rather than replacing it.


Raspberry Pi Details

The main Raspberry Pi scripts are stored under:

/home/Seido/Raiden/Projects/LinuxStuff/Rpi4/Scripts/

The TARDIS sound library is stored under:

/home/Seido/Raiden/Projects/LinuxStuff/Rpi4/Sounds/Tardis/

The central MQTT broker is used by the Pi Agent for Node-RED automation.

The command topic is:

pi/tardis/command

The Pi reports its agent status using:

pi/tardis/status

The new local TARDIS event system does not require Node-RED commands to operate.


1. Python Environment

Modern Raspberry Pi OS and Debian installations can prevent system-wide Python package installation because of PEP 668.

Rather than modifying the operating system Python environment, I created a dedicated virtual environment for the Pi Agent.

From the main scripts directory:

cd /home/Seido/Raiden/Projects/LinuxStuff/Rpi4/Scripts

Create the environment:

python3 -m venv pi-agent-venv

Activate it:

source pi-agent-venv/bin/activate

Install the MQTT library:

pip install paho-mqtt

Then leave the environment:

deactivate

The Pi Agent therefore uses:

/home/Seido/Raiden/Projects/LinuxStuff/Rpi4/Scripts/pi-agent-venv/bin/python

This keeps its dependencies isolated from the operating system.


2. The Pi Agent

The Pi Agent is stored at:

/home/Seido/Raiden/Projects/LinuxStuff/Rpi4/Scripts/pi-agent.py

Its role is deliberately simple.

It subscribes to:

pi/tardis/command

and translates friendly MQTT messages into approved local scripts.

For example:

run:boom
run:hum
run:power_up
run:landing
run:interior_on
run:sign_on

Long-running effects can also be restarted or stopped.

Examples include:

restart:strobe
stop:strobe

restart:interior_breathing
stop:interior_breathing

restart:interior_flicker
stop:interior_flicker

restart:sign_flicker
stop:sign_flicker

This means I do not have Linux commands, GPIO code or long file paths scattered throughout Node-RED.

Node-RED sends a friendly command and the Pi determines what that command is allowed to execute.


3. Mapping Commands to Local Effects

The Pi Agent maps the friendly command names to fixed scripts.

Examples include:

hum          → 2011_hum.py
boom         → Boom.py
strobe       → FastStrobe.py
in_flight    → InFlight.py
landing      → Landing.py
power_up     → PowerUp.py
power_down   → PowerDown.py
takeoff      → StdTakeOff.py
time_winds   → TimeWinds.py

There are also independent lighting controls for:

Top beacon
Interior lighting
Police Public Call Box sign

This allows Node-RED to send something simple such as:

run:landing

rather than having to know the full command:

python3 /some/long/path/to/Landing.py

Apart from making the flows cleaner, this also restricts what can be executed remotely.


4. Running the Pi Agent with systemd

The Pi Agent runs as a systemd service so it automatically returns after a reboot.

The service file is:

/etc/systemd/system/pi-agent.service

The important ExecStart entry points at the Python interpreter inside the virtual environment:

ExecStart=/home/Seido/Raiden/Projects/LinuxStuff/Rpi4/Scripts/pi-agent-venv/bin/python /home/Seido/Raiden/Projects/LinuxStuff/Rpi4/Scripts/pi-agent.py

After creating or changing the service:

sudo systemctl daemon-reload

Enable it:

sudo systemctl enable pi-agent

Start it:

sudo systemctl start pi-agent

Check it:

sudo systemctl status pi-agent

And after making changes:

sudo systemctl restart pi-agent

5. Watching MQTT Commands Arrive

The system journal is useful when testing effects from Node-RED.

sudo journalctl -u pi-agent -f

I can then trigger an effect from Node-RED and immediately see whether the Pi received and processed the command.

This is particularly useful with long-running lighting effects where accidentally launching a second copy can produce some interesting results.


6. Direct GPIO Control

The TARDIS lighting is wired directly to the Raspberry Pi GPIO header.

There is no separate microcontroller controlling the model.

This keeps the physical architecture relatively simple:

Raspberry Pi GPIO
      │
      ├── Top beacon
      ├── Interior lighting
      └── Police Box sign

The lighting zones can therefore be controlled independently.

This is useful both for normal operation and for more chaotic effects such as flickering, damage or failed materialisation sequences.


7. Top Beacon GPIO

The top lamp uses physical pin:

35

When using BCM numbering this is:

GPIO 19

This distinction is important because different Python GPIO libraries use different numbering schemes.

For example, the original RPi.GPIO script used BOARD numbering:

GPIO.setmode(GPIO.BOARD)
LedPin = 35

A BCM-based implementation instead uses:

PIN = 19

Both refer to exactly the same physical connection.

The new local event system uses:

BCM GPIO 19
Physical pin 35

for its temporary beacon effects.


8. Top-Light Pulsing and Strobing

The original top-light script used software PWM through RPi.GPIO.

For example:

p = GPIO.PWM(LedPin, 1000)
p.start(0)

Brightness could then be faded using a loop:

for dc in range(0, 101, 5):
    p.ChangeDutyCycle(dc)
    time.sleep(0.05)

This works well enough for a display model, although software PWM is ultimately being timed by a multitasking Linux operating system.

If the Pi becomes busy, very smooth fades can occasionally become slightly uneven.

Using smaller steps helps:

for duty in range(0, 101, 2):
    pwm.ChangeDutyCycle(duty)
    time.sleep(0.02)

For the new local events I also wanted something different from the normal atmospheric pulse.

The beacon can temporarily switch into faster warning patterns during unusual events before returning to its normal state.

For example:

Normal beacon
     │
     ▼
Temporal disturbance detected
     │
     ▼
Rapid / irregular flashing
     │
     ▼
Event finishes
     │
     ▼
Normal behaviour restored

This gives the beacon a useful second purpose without permanently changing the normal TARDIS lighting.


9. Interior and Police Box Sign Effects

The interior and Police Box sign remain independently controllable.

Existing Python scripts provide functions including:

interior_on
interior_off
interior_breathing
interior_flicker

sign_on
sign_off
sign_flicker

Rather than duplicating those GPIO definitions in the new event controller, the local event engine calls the existing scripts.

This means the actual hardware configuration remains in one place.

It also allows an event to combine several effects.

For example:

Time Eddy
   │
   ├── Time Eddy sound
   ├── Interior flicker
   ├── Police Box sign flicker
   └── Irregular top beacon

That produces something much more convincing than simply playing a sound effect.


10. Audio Library

Over time I have built up a fairly sizeable collection of TARDIS sounds.

The library currently includes effects for:

Power up
Power down
Normal take-off
Fast take-off
Failed launch
Landing
Fast landing
Crash landing
Time winds
Time eddies
Damage
Emergency shutdown
Sonic screwdriver
Regeneration
Exterior door
Cloister Bell
TARDIS hum

The files are stored under:

/home/Seido/Raiden/Projects/LinuxStuff/Rpi4/Sounds/Tardis/

This gave me the opportunity to make the model react to more than just the scheduled take-off and landing sequences.


11. The Hourly Cloister Bell

One of my favourite existing automations is probably one of the simplest.

On the hour, the TARDIS plays the Cloister Bell.

In Doctor Who the Cloister Bell normally means that something has gone catastrophically wrong.

Mine has a slightly less dramatic job.

I use it almost like the chime of a grandfather clock.

Every hour
    │
    ▼
Cloister Bell

It gives the model a little presence in the room without requiring someone to interact with it.

I considered reusing the Cloister Bell as the new network fault alarm, but decided against it.

Once a sound has become the TARDIS equivalent of a grandfather-clock chime, having it suddenly mean that the broadband has fallen over would make the two behaviours difficult to distinguish.

So the hourly bell remains exactly as it was.


12. Adding a Local TARDIS Event Engine

The next evolution of the project was to make the model occasionally do something unexpected.

Rather than adding another large collection of Node-RED flows, I created a small local event system on NoobSaibot.

It is installed under:

/opt/tardis-events/

The main components are:

config.env
internet-watch.sh
random-event.py
tardis-event.sh

The local controller can access the existing sound and lighting scripts while also driving GPIO 19 directly for temporary beacon patterns.

Importantly, this event system is separate from the existing Node-RED command structure.

I did not want to break working automation simply to add some personality to the model.


13. Random TARDIS Events

A systemd timer periodically runs:

random-event.py

The random generator then decides whether anything should happen.

Most of the time the answer is deliberately:

Nothing.

That is important.

If the TARDIS starts crashing, regenerating and screaming every twenty minutes it stops being interesting surprisingly quickly.

The current event pool includes:

Time Eddy
Sonic activity
Gravity anomaly
TARDIS damage
Failed launch
Crash landing
Regeneration

Different probabilities can be assigned to each effect.

Relatively harmless events can happen occasionally while something such as regeneration is made extremely rare.

The intention is that eventually I forget the random system is even there.

Then one evening the TARDIS suddenly decides it has been hit by something.


14. Manual Event Testing

Any of the local events can be triggered manually.

For example:

/opt/tardis-events/tardis-event.sh time_eddy

Other available tests include:

/opt/tardis-events/tardis-event.sh sonic
/opt/tardis-events/tardis-event.sh gravity
/opt/tardis-events/tardis-event.sh damage
/opt/tardis-events/tardis-event.sh failed_launch
/opt/tardis-events/tardis-event.sh crashland

And, if the TARDIS has apparently had enough of its current incarnation:

/opt/tardis-events/tardis-event.sh regeneration

There is also a basic test:

/opt/tardis-events/tardis-event.sh test

This provides a quick way to confirm that the local event controller and GPIO are working.


15. Preventing Events From Colliding

One problem with adding random effects to an already automated model is that several things could theoretically happen at once.

For example:

15:00
 │
 ├── Node-RED plays hourly Cloister Bell
 │
 └── Local random controller decides to crash-land

That is probably a little too much TARDIS.

The local event system therefore uses event locking so another local event cannot start while one is already running.

Random events are also intentionally spread away from exact hourly boundaries to reduce the chance of them interfering with the existing Cloister Bell automation.

The aim is to add unpredictability without turning the system into chaos.

At least, not uncontrolled chaos.


16. Internet Outage Detection

The other useful addition is something that isn’t random at all.

NoobSaibot now monitors internet connectivity using:

internet-watch.sh

The watcher runs continuously as a systemd service.

Its current status can be checked with:

systemctl status tardis-internet-watch.service

The watcher does not immediately declare an emergency after one failed check.

Instead, several consecutive failures are required before connectivity is considered down.

This prevents the TARDIS from having a nervous breakdown because of a single lost packet.

Once an outage is confirmed, the model can react using its own lighting and sound effects.

For example:

Internet available
       │
       ▼
     Normal
       │
       ▼
Repeated connectivity failures
       │
       ▼
Internet considered DOWN
       │
       ├── Warning sound
       └── Faster beacon pattern

When connectivity returns:

Internet restored
       │
       ▼
Recovery sound
       │
       ▼
Normal lighting behaviour

It is completely unnecessary.

Which is exactly why I like it.


17. Running the Internet Watcher Automatically

The internet watcher is installed as:

tardis-internet-watch.service

It is enabled at boot using systemd.

Its status can be checked with:

systemctl status tardis-internet-watch.service

Logs can be watched live with:

journalctl -u tardis-internet-watch.service -f

A successful check appears in the log as something similar to:

Internet confirmed up

Because it runs locally, internet fault detection continues even if Node-RED itself is unavailable.


18. Running Random Events Automatically

The random event generator uses a systemd timer:

tardis-random-event.timer

Check when it will next run with:

systemctl list-timers tardis-random-event.timer

The timer is enabled automatically at boot.

Logs from the event service can be viewed with:

journalctl -u tardis-random-event.service

The important thing to remember is that a timer activation does not necessarily mean the model will perform an effect.

The timer simply gives the random event generator an opportunity to roll the dice.

Most rolls intentionally do nothing.


19. Checking the Local Services

The complete local event setup can be checked with:

systemctl status tardis-internet-watch.service --no-pager

and:

systemctl status tardis-random-event.timer --no-pager

Both are enabled to return automatically after the Raspberry Pi reboots.

This means there is no manual startup process required for the event engine.


20. Graceful Shutdown of Long-Running Effects

One lesson from controlling lighting scripts remotely is that long-running Python processes need to terminate properly.

A script that only handles:

except KeyboardInterrupt:

will not necessarily clean up correctly when stopped by systemd or pkill.

Linux services generally terminate processes using signals such as:

SIGTERM
SIGINT

A better approach is therefore:

import signal

running = True

def stop_signal(signum, frame):
    global running
    running = False

signal.signal(signal.SIGTERM, stop_signal)
signal.signal(signal.SIGINT, stop_signal)

The main loop can then stop naturally and GPIO cleanup can happen inside a finally block.

This is particularly important when one temporary effect needs to stop and hand GPIO control back to another effect.


21. Checking for Duplicate Effects

Long-running effects can also be checked manually.

For example:

pgrep -af FastStrobe.py

A normal result should contain a single process.

If several copies somehow exist:

sudo pkill -f FastStrobe.py

Then check again:

pgrep -af FastStrobe.py

No output means the process is no longer running.

This is useful when experimenting with new sequences because two separate Python processes attempting to PWM the same GPIO pin generally produces something less like Doctor Who and more like a faulty fluorescent tube.


22. Why Keep Node-RED and Local Events Separate?

It would have been possible to create all of the random logic inside Node-RED.

I deliberately chose not to.

Node-RED is excellent for the predictable automation:

Sunrise
Sunset
Hourly events
Scheduled shutdown
Home Assistant integration
Long sequences

The Pi is better placed to handle things directly related to the physical prop:

GPIO
Audio
Temporary lighting effects
Random personality events
Local connectivity monitoring

The result is a fairly clean division.

NODE-RED
│
├── Sunrise
├── Sunset
├── Hourly Cloister Bell
├── 23:00 power-down
└── Choreographed sequences


NOOBSAIBOT
│
├── Pi Agent
├── GPIO
├── Audio
├── Interior effects
├── Police Box sign effects
├── Top beacon
├── Internet monitoring
└── Random TARDIS events

Neither system needs to take over the other’s job.


23. The Raspberry Pi’s Role Today

NoobSaibot is therefore no longer just an MQTT endpoint.

For centrally controlled events it still performs the original hardware-controller role:

Receive MQTT command
        │
        ▼
Translate command
        │
        ▼
Run approved script
        │
        ▼
GPIO / Audio / Lighting

But it now also has a small independent event layer:

Local condition / random roll
            │
            ▼
       Select event
            │
            ▼
 Sound + lighting sequence
            │
            ▼
 Restore normal operation

This leaves the complete architecture looking something like:

                Home Assistant
                      │
                      ▼
                   Node-RED
                      │
               Scheduled logic
                      │
                      ▼
                    MQTT
                      │
                      ▼
             ┌─────────────────┐
             │    NoobSaibot   │
             │  Raspberry Pi 4 │
             └────────┬────────┘
                      │
          ┌───────────┴────────────┐
          │                        │
      Pi Agent                Local Events
          │                        │
   MQTT commands          Random / Internet
          │                        │
          └───────────┬────────────┘
                      │
                      ▼
        ┌─────────────────────────┐
        │ GPIO / Lighting / Audio │
        └────────────┬────────────┘
                     │
                     ▼
                   TARDIS

Final Thoughts

The original goal of moving the main automation away from the Raspberry Pi was to simplify it.

That still makes sense.

Normal timing, smart-home integration and choreography remain centrally managed through Node-RED, while NoobSaibot deals with the physical hardware.

The local event engine does not undo that design.

Instead, it adds a small amount of autonomy where it actually makes sense.

If the internet disappears, the TARDIS can react.

Every so often it can encounter a Time Eddy.

It might apparently take damage.

It might attempt a launch and fail.

Very occasionally, it might even regenerate.

None of these require an enormous automation flow or another central dependency.

The Raspberry Pi simply has enough local intelligence to make the physical model appear a little less predictable.

And that is really the point of the whole project.

It is still a Raspberry Pi connected to some LEDs and a speaker.

It just doesn’t always behave like one.