All posts Software

Hardware with Python: Raspberry Pi and Arduino Notes

A year after learning Python I built an earthquake early-warning prototype with a Raspberry Pi and an Arduino. Instead of the flat line I expected on screen, I got a jittering cloud of numbers. What that project taught me about GPIO, serial lines, sampling, filtering and power.

DT
Demir Taşdemir Mobile App & Web Developer
— min read

When I picked up a Raspberry Pi for the first time in 2019 I had a year of Python behind me, and I assumed anything that worked on screen would also work on the desk. That lasted until I wired up the accelerometer and wrote the first loop. What I expected was a flat line from a sensor sitting perfectly still; what I got was a cloud of numbers that would not stop jittering. This post comes from that TÜBİTAK earthquake early-warning project I never finished, and from the notes I kept afterwards while working with hardware.

The world on screen versus the world on the desk

In software, when you read a variable you get back exactly what you wrote. In hardware you do not read a value, you measure it. And every measurement carries error. Reading that in a book is one thing; running print(sensor_oku()) and watching 512, 509, 514, 511 scroll past on screen is something else entirely.

What I did on that first day was embarrassingly simple: if the value went above a certain number, I declared "there is an earthquake." It fired when I banged my fist on the desk, so I thought I was done. Then I left the system running overnight and it triggered itself dozens of times. The real project started that night.

GPIO: the simplest interface, and the one that fooled me most

GPIO pins are digital: either high or low. The trap is that a pin connected to nothing does not read "low." A floating pin picks up the noise in the air and reads at random. The fix is not in software, it is in the circuit: a pull-up or pull-down resistor that ties the pin to a known level. On the Raspberry Pi these are built into the chip and you enable them with a single line — but if you enable them without understanding what they do, you run into inverted logic and lose hours: on a button with a pull-up, pressed is 0, not 1.

The second trap is buttons. A mechanical contact opens and closes several times within milliseconds as it settles. On the software side, a single press shows up as ten interrupts. This is called "debounce" and the fix is simple: ignore new triggers for a short window after the last valid one.

Do not skip the voltage difference

The Raspberry Pi's GPIO pins run at 3.3 V; most Arduino boards run at 5 V. Wire an Arduino output straight into a Pi pin and you may well burn that pin out. You need a level shifter, or at the very least a voltage divider. And then there is common ground: if you do not tie the two boards' GND together, the levels on the data line mean nothing.

Getting two boards to talk: the serial line

In the end I split the architecture in two. The Arduino read the sensor on a fixed period and pushed it down the line; the Raspberry Pi collected that data, processed it and sent it to the network. The reason: there is a Linux running on the Pi, and Linux is not a real-time operating system. A file write, or a service waking up in the background, can delay your loop by tens of milliseconds. On the Arduino there is nothing but your loop. I gave the timing to the simple board and the decision-making to the powerful one.

When I first set up the serial line I sent the data as comma-separated lines and fed them straight into split(",") and then int(). The first crash came five minutes later: the moment you open the port you drop into the middle of the stream and end up with half a line. The second problem is that a single bit corrupted on a long cable silently changes the number. These days I treat every incoming line as suspect:

def satir_coz(ham):
    # Expected format: "S,zaman_ms,x,y,z,saglama"
    parca = ham.strip().split(",")
    if len(parca) != 6 or parca[0] != "S":
        return None
    try:
        zaman = int(parca[1])
        x, y, z = (int(p) for p in parca[2:5])
        saglama = int(parca[5])
    except ValueError:
        return None
    if (zaman + x + y + z) % 256 != saglama:
        return None
    return zaman, x, y, z

This eight-bit sum check is not a serious error-correction scheme, but it keeps a corrupted line out of the data set. I learned one more thing here: if the Python side falls behind on reading, the operating system's buffer fills up and data either goes missing or arrives late. That is exactly why making a network request inside the read loop is a bad idea.

Sampling: where I made the most mistakes

Saying "I'll sample at 100 Hz" and then putting time.sleep(0.01) at the end of the loop was the biggest mistake I made back then. That line does not mean "every 10 ms," it means "wait another 10 ms after you finish your work." The processing time gets added to the period, and over hours a serious drift builds up. The right approach is to compute not the duration to sleep but the moment to wake up:

import time

PERIYOT = 0.01                 # 100 Hz
hedef = time.monotonic()
kacan_toplam = 0

while calisiyor:
    oku_ve_isle()
    hedef += PERIYOT
    kalan = hedef - time.monotonic()
    if kalan > 0:
        time.sleep(kalan)
    else:
        # We fell behind: count the missed periods, do not grow the backlog
        kacan = int(-kalan // PERIYOT) + 1
        hedef += kacan * PERIYOT
        kacan_toplam += kacan

Choosing time.monotonic() was deliberate too: the system clock can be pulled backwards by NTP, a monotonic clock cannot. Counting the missed periods turned out to be a separate win; when that counter climbed, I knew the problem was in my loop and not in the sensor.

There is a clear rule for picking the sampling rate as well: at least twice the highest frequency you care about (Nyquist). Go below it and the high-frequency component does not disappear, it comes back as a fake low-frequency signal. For a seismic signal the band of interest is roughly a few Hz to twenty Hz, so 100 Hz was a comfortable choice.

Where the noise comes from

Before declaring "the sensor is broken," I learned to separate out the sources of noise:

  • Quantization: the Arduino Uno's analog-to-digital converter is 10-bit. With a 5 V reference, one step works out to about 4.9 mV. You cannot see a change finer than that; the last digit flickering is normal.
  • Power supply: whenever a motor or relay ran off the same line, the measurement jumped. Switching noise rides into the measurement through the supply.
  • Cable: a long, unshielded cable behaves like an antenna. Moving the sensor next to the board was more effective than most software filters.
  • Mechanical: a door slamming is a real acceleration. That is not noise, it is a signal I did not want — and it is the hardest one to tell apart.
  • Drift: the zero point shifts as the temperature changes. A threshold I calibrated in the morning was wrong by the evening.

Filtering and the price of latency

I used two filters back to back. A median filter cleans up the occasional spike, and an exponential moving average (EMA) smooths out the high-frequency jitter:

from collections import deque

class Filtre:
    def __init__(self, pencere=5, alfa=0.2):
        self.tampon = deque(maxlen=pencere)
        self.deger = None
        self.alfa = alfa

    def ekle(self, ham):
        self.tampon.append(ham)
        sirali = sorted(self.tampon)
        medyan = sirali[len(sirali) // 2]
        if self.deger is None:
            self.deger = medyan
        else:
            self.deger = self.alfa * medyan + (1 - self.alfa) * self.deger
        return self.deger

The real lesson here was not the filter itself but its price: every filter adds latency. The wider the window, the nicer the line looks — and the later you see the event. In a system trying to give early warning, that eats directly into its reason for existing. A graph that looks good and a graph that works are not the same thing.

I eventually dropped the fixed threshold as well. The classic approach in seismology is to take the ratio of a short window's average to a long window's average (STA/LTA). That way you are not saying "if it goes above this value" but "if it rises sharply relative to the environment's own noise." If the baseline drifts, the threshold drifts with it. I learned this after I had already abandoned the project, and I still wish I had read about it earlier.

Power: the thing a software developer never thinks about

The system restarted itself several days in a row, and I spent days hunting for a bug in the code. The problem was the power adapter. With insufficient current the board resets when the voltage drops; worst of all, if it happens to be writing to the SD card at that moment, the file system can get corrupted. Taking the Raspberry Pi's low-voltage warning seriously was the most expensive lesson I learned that week.

For any system whose power can be cut

Instead of one big file, I split the recording into hourly chunks and flushed the buffer to disk after every write. An outage now costs me at most the last line rather than all the data. I use the same logic on the mobile side today: the app can be killed at any moment, so critical state belongs on disk.

What I learned from hardware and carried into software

The project was left unfinished; I am not hiding that. But what I took away from it is still present in the mobile apps I write today:

  1. Every input is noisy. The sensor is, the user is, and so is the JSON coming off the network. I do not let incoming data in without validating it.
  2. Time is measured, not assumed. sleep is not a guarantee, it is a request.
  3. Separate the work. Time-sensitive work and slow work should not sit in the same loop. Separating the serial read from the network request taught me the same thing as separating the UI thread from background work.
  4. There is always a physical limit. In hardware it is power and current; on a phone it is battery, memory and mobile data. In both cases you ignore the limit first and hit it later.

The biggest benefit of working with hardware, for a software developer, is this: it reminds you that code is not abstract. You write an if on screen and an LED on the desk either lights up or it does not. When it does not, you cannot make excuses. My advice to someone starting out is to buy a cheap board after learning Python and try to read a sensor. The parts that are not in the book start right there.

  • Python
  • Raspberry Pi
  • Arduino
  • Hardware
  • Embedded Systems
  • Sensors
Share: LinkedIn X WhatsApp
DT

Demir Taşdemir

Mobile App & Web Developer

I have been building software since 2018. I have shipped 11 apps on the App Store and Google Play; right now I am working on 6 mobile apps, 1 e-commerce platform and 1 desktop game.

Have an idea on the hardware side?

I enjoy building systems that collect sensor data, process it and carry it over to the mobile side. If you want to talk through a project you have in mind, drop me a line.