Why I Started Using This Tool

After getting one LED blinking from C with trigger and brightness, the obvious next question was "okay, but what about all four?" My first pass at the answer was exactly what you'd expect from a first pass: four macros, eight fopen() calls typed out by hand, and a blink loop that copy-pasted the same four lines twice. It worked, but every time I looked at it I knew that adding a fifth LED — or even just fixing one bug — meant editing the same logic in four places instead of one. So I sat down and refactored it: one array of paths, one setup loop, one array of open file handles, one cleanup loop. Same hardware, same sysfs files, dramatically less code to maintain.

A quick note on the toolchain: this is plain C (it compiles equally cleanly as C++, if you'd rather build it that way), written and cross-compiled inside Eclipse CDT as a C/C++ Remote Application project targeting the BeagleBone Black — the same Eclipse project and remote-debug setup from the previous GDB post, just pointed at this new source file. That matters here because embedded work like this lives at the boundary between "regular C" and "the specific board you're targeting": the code itself is standard stdio.h/unistd.h, but it only does anything useful once it's cross-compiled for the BBB's ARM target and actually run against real /sys/class/leds files on the hardware, not on your Windows dev machine.

What It Does

Here's the plain-English version: every LED under /sys/class/leds still works the same way — write "none" to trigger to take manual control, then flip brightness between 1 and 0 to turn it on and off. The refactor doesn't change any of that. What it changes is how the C code is organized once you're driving more than one LED at a time.

  • One paths[NUM_LEDS] array instead of four #defines — every LED's base path lives in one array, and NUM_LEDS controls how many the rest of the program loops over. Adding a fifth LED means adding one line, not editing every block that used to reference LED1 through LED4.
  • sprintf() builds each file path at runtime — since the LED name now lives in an array instead of a compile-time macro, the old trick of gluing string literals together (LED1 "/trigger") stops working. sprintf(filepath, "%s/trigger", paths[i]) builds the same string, just at runtime instead of compile time.
  • One setup loop replaces four repeated blocks — "open trigger, write none, close, open brightness" used to be typed out four times. Now it's one for loop body that runs once per LED, and FILE *led[NUM_LEDS] stores every open brightness handle by index instead of naming them led1 through led4.
  • The habits from the single-LED version still apply — every LED still needs rewind() before each repeat write to brightness (sysfs only accepts writes at offset 0), and fopen()'s return value is still worth checking before you use it. Refactoring the structure doesn't remove the need for the fundamentals.

The Walkthrough

The overall shape of the program is three phases: set every LED up once, blink some of them in a loop, then close every handle on the way out.

┌───────────────────────────────┐
│ SETUP LOOP (i = 0..NUM_LEDS-1) │
│  sprintf → open trigger        │
│  write "none" → close trigger  │
│  sprintf → open brightness     │
│  store handle in led[i]        │
└───────────────┬─────────────────┘
                 v
┌───────────────────────────────┐
│ BLINK LOOP (10 iterations)      │
│  rewind/write "1"/fflush led[0] │
│  rewind/write "1"/fflush led[1] │
│  sleep(2)                        │
│  rewind/write "0"/fflush led[0] │
│  rewind/write "0"/fflush led[1] │
│  sleep(2)                        │
└───────────────┬─────────────────┘
                 v
┌───────────────────────────────┐
│ CLEANUP LOOP (i = 0..NUM_LEDS-1)│
│  fclose(led[i])                 │
└───────────────────────────────┘

Here's the full program:

#define NUM_LEDS 4

const char *paths[NUM_LEDS] = {
    "/sys/class/leds/beaglebone:green:usr0",
    "/sys/class/leds/beaglebone:green:usr1",
    "/sys/class/leds/beaglebone:green:usr2",
    "/sys/class/leds/beaglebone:green:usr3",
};

FILE *led[NUM_LEDS];
char filepath[128];

for (int i = 0; i < NUM_LEDS; i++) {
    sprintf(filepath, "%s/trigger", paths[i]);
    FILE *trig = fopen(filepath, "w");
    fputs("none", trig);
    fclose(trig);

    sprintf(filepath, "%s/brightness", paths[i]);
    led[i] = fopen(filepath, "w");
}

for (int count = 0; count < 10; count++) {
    rewind(led[0]); fputs("1", led[0]); fflush(led[0]);
    rewind(led[1]); fputs("1", led[1]); fflush(led[1]);
    sleep(2);

    rewind(led[0]); fputs("0", led[0]); fflush(led[0]);
    rewind(led[1]); fputs("0", led[1]); fflush(led[1]);
    sleep(2);
}

for (int i = 0; i < NUM_LEDS; i++) fclose(led[i]);

Here's what tripped me up at first: I assumed once the setup was array-based, everything downstream would automatically scale to NUM_LEDS. It doesn't — the blink loop above still hardcodes led[0] and led[1] by index. The array makes it possible to loop over all four LEDs with for (int i = 0; i < NUM_LEDS; i++), but you still have to actually write that loop; the refactor buys you the structure, not the behavior, for free.

Building and running it on the BBB is the same two-line affair as before. From a terminal, that's a plain gcc build and a sudo run:

gcc -o led_array led_array.c
sudo ./led_array   # root is needed to write trigger/brightness

Inside Eclipse, it's the same end result reached a different way: hit Build on the C/C++ Remote Application project to cross-compile for ARM, then Debug (or Run) to have Eclipse copy the binary over SSH and execute it on the BeagleBone — no manual gcc or scp needed once the project's set up, and you get breakpoints on this exact loop for free if you launch it in Debug mode instead of Run.

My Honest Pros & Cons

✅ What I Love

  • Adding a fifth LED is now a one-line change — one new entry in paths[] and bumping NUM_LEDS, instead of hunting down every block that referenced LED1..LED4 by name.
  • The setup and cleanup logic exists exactly once — if I find a bug in how a trigger gets set, there's one loop to fix, not four near-identical blocks that all need the same fix applied by hand.
  • It's a genuinely useful pattern beyond LEDs — path array + runtime sprintf() + setup loop + handle array + cleanup loop generalizes to any "I have N of the same sysfs device" situation, not just LEDs.

❌ What Could Be Better

  • The blink loop didn't get the same treatment — it still explicitly writes to led[0] and led[1] instead of looping over NUM_LEDS, so blinking all four still means hand-editing the loop body rather than changing a constant.
  • No fopen() NULL-check in the setup loop — the single-LED version checked every fopen() before using it; this refactor dropped that check, so a missing sudo now fails less obviously than it should.
  • sprintf() instead of snprintf() — fine for these short, known paths, but it's a habit worth swapping now rather than after a longer path overflows a 128-byte buffer.

Pricing: Is It Worth It?

There's no dollar cost here — it's the same free sysfs interface the kernel already exposes. The real cost is time: refactoring four copy-pasted blocks into an array and two loops took maybe twenty extra minutes over just leaving the working-but-repetitive version alone.

My take: twenty minutes now to learn sprintf() and array-of-FILE* patterns is cheaper than the ten minutes you'll spend, four separate times, hunting down the same bug in four almost-identical blocks later.

Final Verdict

If you've already got a single LED blinking from C and you're about to copy-paste that logic for a second, third, or fourth device, stop and reach for an array and a loop instead — this refactor is the smallest useful example of exactly that move. Beginners should focus on the sprintf()-builds-the-path-at-runtime idea, since that's the one piece that isn't obvious coming from the compile-time macro version. If you already write C comfortably, the only new idea here is trading four named variables for one indexed array — skim the code, and go finish the blink loop so it actually loops over NUM_LEDS instead of two hardcoded indices.