Supplier photo of an assembled radio kit with a purple circuit board, telescopic antenna and clear case.
Supplier photo
Temu affiliate offer · Project idea

Build a radio

Soldering required. Check the supplied parts and board revision.

View kit on Temu

Tricky Circuit may earn a commission through these links. Product ideas have not been bench-tested by Tricky Circuit and are not verified lesson parts lists.

One LED, Two Programs: Make GPIO Ownership Explicit

You leave one LED program running, start another in a second terminal, and now neither command stream tells the whole story. The pin can also be reserved by a backend or another service. This experiment establishes a visible ownership rule before allocating the output: cooperating copies of the program must acquire the same local file lock.

The hardware profile is a Raspberry Pi 4 Model B, Raspberry Pi OS Bookworm, Python 3.11 and gpiozero 2.0.1, using its documented LGPIOFactory backend on gpiochip 0. GPIO means general-purpose input/output, a programmable signal pin. A backend is the software layer that sends those requests to the operating system (OS). This is not a Pico program and does not depend on precise waveform timing.

Verification uses real host Unix file locks and child processes plus an injected LED object. The actual GPIO adapter has not been run, and no Pi execution, firmware build, physical voltage or LED-state measurement is claimed. The --mock option imports no gpiozero package and touches no GPIO pin.

One small output is enough

Use one Kingbright WP7113ID red light-emitting diode (LED) and one 1 kΩ resistor rated at least 0.125 W. Connect GPIO17, physical header pin 11, through the resistor to the LED anode; connect its cathode to ground, physical pin 9. The software uses Broadcom (BCM) GPIO numbering, so 17 is a signal name rather than the seventeenth header position.

Wire the selected pin and LED

Raspberry Pi 4B GPIO17 physical pin 11 drives a WP7113ID red LED anode through 1 kΩ, with the LED cathode returning to physical ground pin 9.
Original low-voltage circuit. The resistor bounds the LED current; a software lock does not change the electrical circuit.

For an illustrative 1.9 V forward drop at a nominal 3.3 V output, current is about (3.3−1.9)/1000=1.4 mA. The forward drop depends on current, part and temperature. Ignoring the LED drop gives a conservative nominal bound of 3.3 mA through the resistor, before rail and resistor tolerance. This small load is intentional; an absolute maximum or configurable drive-strength number is not a target current.

Disconnect power before wiring and verify polarity from the exact LED drawing. Use the Pi’s normal power supply. No 5 V header pin, relay, motor or external signal source connects to this output. Pi GPIO is a 3.3 V domain. The lock demonstration does not require connecting the LED until the software-only sequence has been understood.

What an advisory lock promises

An advisory lock is an OS-supported agreement among programs that choose to check it. The first copy opens a stable local file and requests an exclusive nonblocking flock: exclusive means one participating owner, and nonblocking means a competing request returns promptly instead of waiting indefinitely. The second copy reports BUSY and exits before constructing any GPIO device.

Every participating copy must use the same unchanged local file. A different path to a different file creates a different lock. Do not delete the lock file after use or while another copy may be running. The lock belongs to an open file object, identified by its inode, the filesystem’s identity for that object. Deleting and recreating the pathname can leave the old owner holding the old inode while a new owner locks a different one. The host tests demonstrate this limitation in a temporary directory.

The file may remain after normal exit or a crash. That does not make it a stale held lock. Conversely, a process identifier (PID), the number written into the file for diagnosis, is not proof that a lock is still held. The kernel, the OS component managing file descriptors, decides the lock state. A file descriptor is a process’s handle to an open resource.

Use a local filesystem and one account for this experiment. Network filesystem locking, multiple users, replaced paths and other programs that ignore the convention need a separate design. A lock is not a security boundary or a universal reservation of the physical pin. The GPIO backend may independently refuse an already claimed line; neither mechanism makes unrelated software cooperate automatically.

Synthetic ownership and LED command traces: process A holds the lock, process B is refused, A turns off and closes before unlocking, then B acquires the same unchanged file; both processes point to one inode.
Original synthetic ordering diagram. Ownership intervals and LED commands are different facts; the drawing is not a physical voltage or timing measurement.

Complete ownership wrapper and LED adapter

The application programming interface (API), the callable software interface, combines Python’s Unix flock with gpiozero’s LED and pin-factory objects. The actual adapter explicitly selects gpiozero 2.0.1 and Linux gpiochip 0; a mismatched package, missing backend or insufficient device access produces a fault. Record the installed Python, gpiozero and lgpio versions on the Pi before testing. This article does not claim an installed or exercised Pi environment.

Download the complete program (ZIP; file: tricky-circuit-batch-007/code/pi-gpio-single-owner.py). Its first operation under the lock is construction with an initial LOW output. Cleanup attempts LED off, closes the LED and its factory, then leaves the lock context. The order keeps another cooperating copy from acquiring while normal cleanup is still running.

"""Pi4B / Python3.11 / gpiozero2.0.1. Advisory local GPIO17 ownership.
--mock exercises process locks without importing gpiozero or claiming GPIO access.
"""
import argparse,errno,fcntl,json,math,os,signal,stat,time
from pathlib import Path

class StopFlag:
    """Signal handler only assigns requested; normal code performs timed polling."""
    def __init__(self):self.requested=False
    def is_set(self):return self.requested
    def wait(self,seconds):
        deadline=time.monotonic()+seconds
        while not self.requested:
            remaining=deadline-time.monotonic()
            if remaining<=0:return False
            time.sleep(min(0.05,remaining))
        return True

class BusyError(RuntimeError):pass

class OwnerLock:
    def __init__(self,path):self.path=os.fspath(path);self.fd=None
    def __enter__(self):
        flags=os.O_CREAT|os.O_RDWR|os.O_NOFOLLOW|getattr(os,'O_CLOEXEC',0)
        fd=os.open(self.path,flags,0o600)
        try:
            info=os.fstat(fd)
            if not stat.S_ISREG(info.st_mode) or info.st_uid!=os.getuid():
                raise ValueError('lock must be a regular file owned by this user')
            os.set_inheritable(fd,False)
            try:fcntl.flock(fd,fcntl.LOCK_EX|fcntl.LOCK_NB)
            except OSError as exc:
                if exc.errno in (errno.EACCES,errno.EAGAIN):raise BusyError('GPIO17 busy under this lock path')
                raise
            os.ftruncate(fd,0);os.write(fd,(str(os.getpid())+'\n').encode())
        except BaseException:
            os.close(fd);raise
        self.fd=fd;return self
    def __exit__(self,*exc):
        fd,self.fd=self.fd,None
        try:fcntl.flock(fd,fcntl.LOCK_UN)
        finally:os.close(fd)

class PiLED:
    def __init__(self):
        from importlib.metadata import version
        if version('gpiozero')!='2.0.1':raise RuntimeError('Select gpiozero2.0.1 for this qualified software profile')
        from gpiozero import LED
        from gpiozero.pins.lgpio import LGPIOFactory
        self.factory=LGPIOFactory(chip=0)
        try:self.led=LED(17,active_high=True,initial_value=False,pin_factory=self.factory)
        except BaseException:self.factory.close();raise
    def on(self):self.led.on()
    def off(self):self.led.off()
    def close(self):
        try:self.led.close()
        finally:self.factory.close()

class MockLED:
    def __init__(self):self.state=False
    def on(self):self.state=True
    def off(self):self.state=False
    def close(self):pass

def run(factory,path,stop,period_s=0.5,emit=print,limit=None):
    if isinstance(period_s,bool) or not math.isfinite(period_s) or not .05<=period_s<=10:
        raise ValueError('period_s must be finite and0.05..10')
    with OwnerLock(path):
        led=None
        try:
            led=factory();led.off();emit({'event':'OWNED','pid':os.getpid()})
            count=0;on=False
            while not stop.is_set() and (limit is None or count<limit):
                on=not on
                if on:led.on()
                else:led.off()
                count+=1;stop.wait(period_s)
        finally:
            if led is not None:
                try:led.off()
                finally:led.close()
    emit({'event':'RELEASED','gpio_state_measured':False})

def main(argv=None):
    p=argparse.ArgumentParser(description=__doc__)
    p.add_argument('--lock',default=str(Path.home()/'.tricky-circuit-gpio17.lock'))
    p.add_argument('--mock',action='store_true')
    p.add_argument('--cycles',type=int,default=None)
    a=p.parse_args(argv)
    if a.cycles is not None and a.cycles<1:p.error('--cycles must be positive')
    stop=StopFlag()
    def request_stop(*_):stop.requested=True
    for number in (signal.SIGINT,signal.SIGTERM):signal.signal(number,request_stop)
    try:
        run(MockLED if a.mock else PiLED,Path(a.lock).expanduser(),stop,
            emit=lambda row:print(json.dumps(dict(row,mock=a.mock)),flush=True),limit=a.cycles)
    except BusyError as exc:print(json.dumps({'event':'BUSY','error':str(exc)}),flush=True);return 3
    except (OSError,ValueError,RuntimeError,ImportError) as exc:
        print(json.dumps({'event':'FAULT','error':str(exc),'gpio_state_measured':False}),flush=True);return 2
    return 0
if __name__=='__main__':raise SystemExit(main())

SIGINT is the interrupt signal normally sent by Ctrl+C; SIGTERM is a normal termination request. The signal handlers only assign a plain stop flag. Ordinary execution polls that flag between sleep chunks of at most 50 ms, then performs cleanup. OS scheduling can add delay; this is not a hard response bound. The handler acquires no Python condition lock, avoiding a deadlock if a signal interrupts a wait that already holds such a lock. SIGKILL is an uncatchable forced termination. The OS releases that process’s open-file lock when it dies, but Python cannot then promise LED off or backend cleanup. A subsequent owner starts from its own explicit initial LOW; that is a new command, not evidence about the interval before it started.

Output, console, backend and lock-file errors are surfaced. A cleanup off error still attempts close, and the context still closes its lock descriptor. Successful software cleanup is not a voltage measurement. The blink wait is nominally half a second and subject to OS scheduling; use a different experiment for precise pulse generation.

Two-terminal experiment

  1. In a Unix environment, run python3 pi-gpio-single-owner.py --mock. Keep it running. In a second terminal run the same command with the same default lock path. It should report BUSY and return exit status 3; no GPIO object is allocated in that second copy.
  2. Stop the first normally with Ctrl+C. The file remains. Start the second again and observe that it can acquire. Compare the printed PID with the advisory-lock outcome rather than treating file existence as ownership.
  3. On the specified Pi profile, check the actual user has access to gpiochip 0 and verify wiring while unpowered. Run without --mock only after that check. Confirm the LED indication, then perform the same two-terminal ownership sequence.
  4. Record normal termination and any cleanup faults. Reproduce uncatchable process death only with --mock or the host suite: its test proves lock release, not a physical LED state. Do not infer hardware safety from a subprocess exit code.
  5. Use the blank observation sheet (ZIP; file: tricky-circuit-batch-007/code/pi-gpio-single-owner-observations-blank.csv) to record the exact path, PID, backend, return status and separately observed LED behavior.
Challenge: can I fix BUSY by deleting the lock file?

No. A live owner may still hold the old file object, and recreating the pathname can permit another owner of a new inode. Stop and inspect the participating processes. A remaining unlocked file is normal. Keep one stable local lock identity instead of using deletion as an unlock command.

Primary references and scope

Review the electronics lessons before extending the circuit. This project adds process ownership and cleanup boundaries, rather than duplicating the site’s bounded logger or Pi health-flag article. The actual host suite covers competing processes, normal termination, forced death, constructor/output/console faults, invalid periods and the inode-replacement counterexample.

Download the reader files (ZIP): the complete program, blank observation sheet and original editable SVG/PNG diagrams for pi-gpio-single-owner are inside tricky-circuit-batch-007/. The inline program above is complete; the download is supplementary.

📡Broadcast the signal — amplify the connection.

Leave a Reply

Temu affiliate offers

Find your next electronics project

Explore these do-it-yourself project kits. Check the selected option, included parts and instructions on each offer.

Tricky Circuit may earn a commission if you buy through these Temu affiliate links. These are product ideas; they have not been bench-tested by Tricky Circuit and are not verified lesson parts lists.

Supplier photo of an assembled infinity mirror light cube illuminated on a dark background.

Supplier product photo

Build an infinity light cube

Listing specifies 5-volt direct current. Soldering required; battery and glue not included.

View kit on Temu

Supplier photo of a breadboard, jumper wires and assorted electronic components in a plastic storage case.

Supplier product photo

Try breadboard experiments

Breadboard, wires and assorted parts. Arduino controller board not included.

View kit on Temu

Supplier photo of an assembled blue surface-mount soldering practice board with a circular ring of small lights.

Supplier product photo

Solder a chasing-light board

Surface-mount soldering practice. Battery and battery holder not included.

View kit on Temu