A 3‑axis geophone seismometer project for Raspberry Pi.
Reads data from an Arduino‑based digitizer over RS-422, processes it in real‑time, and provides:
- SDS-compliant MiniSEED archiving (via ObsPy)
- StationXML generation with full GD-4.5 PAZ instrument response
- Live waveform streaming via WebSocket to a web frontend
- Earthquake detection using a STA/LTA algorithm with immediate file marking and notifications
- Push notifications via Telegram (and other Apprise-compatible services)
The system is built around five concurrently running threads, making it efficient and responsive even on a Raspberry Pi.
- RPI-SEISM
- Continuous data acquisition from a 3‑channel (EHZ, EHN, EHE) geophone at 100 Hz
- MCU settings handshake – sends ADC gain and sample rate to the Arduino on startup and verifies the echo before streaming begins
- Robust RS-422 communication with automatic heartbeat to keep the Arduino streaming
- SDS-compliant MiniSEED archive – writes to a standard SeisComp Data Structure directory tree, with automatic midnight splitting so each sample lands in the correct day file
- StationXML generation – builds a fully calibrated
station.xmlwith GD-4.5 PAZ response stages; automatically manages instrument response epochs when hardware settings change - STA/LTA trigger – detects earthquakes on the vertical channel and triggers an early archive flush after 5 minutes, then resumes the normal schedule
- WebSocket live feed – serves decimated waveform data (1 second updates) to connected clients
- Push notifications – dedicated
NotifierSenderthread sends an immediate Telegram (or any Apprise-compatible service) alert on detection, then collects 60 s of post-event data and attaches an interactive HTML waveform plot - Modular design – each component runs in its own thread, communicating via thread‑safe queues
- Configurable via YAML – station metadata, channel mapping, ADC settings, sampling rate, decimation factor, and more
- Raspberry Pi (any model with GPIO, tested on RPi 3/4)
- Arduino‑based digitizer (code provided in separate repository)
- Sampling at up to 100 Hz, 3 channels
- Communicates over RS-422 at 250 000 baud
- Receives a settings frame on startup, echoes it back for verification
- Expects a heartbeat pulse every 500 ms to continue streaming
- MAX485 or equivalent RS-422 transceiver connected to the Pi's UART and a GPIO pin (e.g., GPIO5) for direction control
- GD-4.5 geophone (3‑component, 4.5 Hz natural frequency) with appropriate pre‑amplifier and shielded cables
📌 Arduino firmware: rpi-seism-reader – handles ADC reading, packet framing, RS-422 transmission, and settings acknowledgement.
- Python 3.7+ (managed with UV)
- ObsPy – MiniSEED I/O, decimation, and StationXML generation
- pyserial – serial communication
- websockets – WebSocket server
- numpy – data handling
- PyYAML – YAML configuration loading
- Pydantic – settings validation
- Apprise – multi-platform push notifications
- Plotly – interactive HTML waveform charts attached to notifications
- pandas – buffer-to-DataFrame conversion for chart generation
UV is a fast Python package installer and resolver.
If you don't have it yet, install it:
curl -LsSf https://astral.sh/uv/install.sh | shThen clone this repository and install dependencies:
git clone https://github.com/ch3p4ll3/rpi-seism.git
cd rpi-seism
uv sync # install dependenciesAll system settings are defined in data/config.yml. If the file is not present, one will be created automatically with the default configuration on first run.
channels:
- adc_channel: 0
name: EHZ
orientation: vertical
sensitivity: 28.8
- adc_channel: 1
name: EHN
orientation: north
sensitivity: 28.8
- adc_channel: 2
name: EHE
orientation: east
sensitivity: 28.8
decimation_factor: 4
jobs_settings:
notifiers:
- enabled: true
url: tgram://{bot_token}/{chat_id}/
reader:
baudrate: 250000
port: /dev/ttyUSB0
trigger:
lta_sec: 10.0
sta_sec: 0.5
thr_off: 1.5
thr_on: 3.5
trigger_channel: EHZ
writer:
write_interval_sec: 1800.0
mcu:
adc_gain: 6
adc_sample_rate: 11
sampling_rate: 100
vref: 2.5
start_date: '2026-03-18T08:23:36.479789Z'
station:
elevation: 0.0
latitude: 0.0
location_code: '00'
longitude: 0.0
network: XX
station: RPI3
| Key | Description |
|---|---|
start_date |
ISO-8601 timestamp marking when this instrument configuration took effect. Used as the channel epoch start in station.xml. Must be updated whenever hardware settings change. |
station.network / station.station |
SEED network and station identifiers |
station.latitude/longitude/elevation |
Geographic coordinates written into station.xml |
decimation_factor |
Downsampling factor applied by the WebSocket sender (e.g., 4 → 25 Hz output) |
mcu.sampling_rate |
Must match the Arduino's output rate (100 Hz) |
mcu.adc_gain |
ADS1256 programmable gain amplifier setting |
mcu.adc_sample_rate |
ADS1256 data rate register value |
mcu.vref |
ADS1256 VREF, default 2.5V |
channels |
List of channels with SEED names, ADC indices, sensitivity, and physical orientations |
notifiers |
Apprise-compatible notification URLs (Telegram, Slack, etc.) |
Start the application with:
uv run python -m src.mainOn startup the system will:
- Validate configuration and generate
station.xmlif needed (or update epochs if settings changed) - Send ADC settings to the Arduino over RS-422 and wait for echo confirmation
- Start all four threads
Stop with Ctrl+C. On shutdown, any buffered data is flushed to disk.
A companion web interface is available to display live waveforms and event notifications:
📁 rpi-seism-frontend – Angular‑based dashboard that connects to the WebSocket endpoint.
- Responsibility: Sole owner of the serial port and the RS-422 direction-control GPIO.
- Startup handshake: Before entering the main loop, it serialises the current
MCUSettingsinto a binary frame and transmits it to the Arduino over RS-422. It then waits up to 10 seconds for the Arduino to echo back an identical frame (identified by the0xCC 0xDDheader). If the echo is absent or mismatched, aMCUNoResponseexception is raised and the application stops. - Operation:
- Sends a heartbeat byte (
0x01) everyheartbeat_interval(default 0.5 s) to keep the Arduino streaming. Before sending, it sets the MAX485 to transmit mode, then immediately back to receive. - Reads incoming bytes into a ring buffer, searches for the packet header (
0xAA 0xBB), and validates the checksum (XOR of all payload bytes). - Upon a valid packet, unpacks three 32‑bit signed integers (one per channel) from the
Samplestruct. - Formats the decoded data as
{"timestamp": time.time(), "measurements": [{"channel": ch_obj, "value": val}, ...]}and places it into every downstream queue.
- Sends a heartbeat byte (
- Why a thread? It must continuously poll the serial port without blocking other tasks, and the heartbeat timing must be precise.
- Responsibility: Buffer incoming samples and write them to a SeisComp Data Structure (SDS) archive.
- SDS layout: Files are written to
OUTPUT_DIR/YEAR/NET/STA/CHAN.D/NET.STA.LOC.CHAN.D.YEAR.DAYand are appended to (not overwritten) on subsequent write cycles. If the buffer spans midnight, it is automatically split so each slice lands in the correct day file. - Operation:
- Maintains a per‑channel list of raw
int32values and the start time of the current batch. - Normally, writes and clears the buffer every
write_interval_sec(default 1800 s = 30 min). - When the
earthquake_eventis set by the trigger, it schedules the next flush to happen in 5 minutes (event_write_delay_sec), ensuring that event waveforms are persisted promptly without waiting for the normal interval. If multiple triggers occur during the countdown, the timer resets. - On final shutdown, any remaining buffered data is flushed.
- Maintains a per‑channel list of raw
- Why a thread? Writing to disk can be I/O-bound; buffering lets the writer operate independently from the high-rate data stream.
- Responsibility: Detect seismic events using ObsPy's recursive STA/LTA algorithm on the vertical channel (
EHZ). - Operation:
- Listens for packets and extracts the value for the trigger channel.
- Appends each new sample to a rolling
dequebuffer sized at2 × LTA window(default:2 × 10 s × 100 Hz = 2000 samples). The oversized buffer ensures the algorithm has a stable long-term baseline before producing meaningful ratios. - Once the buffer has accumulated at least
nltasamples, it callsobspy.signal.trigger.recursive_sta_lta()on the full buffer array. The last element of the returned characteristic function array is taken as the current STA/LTA ratio. - Uses a dual-threshold (hysteresis) scheme to prevent chattering:
- Rising edge (
ratio > thr_on, default 3.5, and not already triggered): sets the sharedearthquake_event, logs the detection, and dispatches a push notification via Apprise. - Falling edge (
ratio < thr_off, default 1.5, and currently triggered): clears the event.
- Rising edge (
- Why a thread? Processing runs for every sample and must not be blocked by the I/O-bound writer or WebSocket sender.
- Responsibility: Provide a live data feed to web clients with decimated waveforms.
- Operation:
- Runs an asyncio event loop that hosts a WebSocket server.
- Maintains a sliding-window buffer (size =
window_seconds * sampling_rate) per channel. - Every
step_seconds(e.g., 1 s), it takes the current window for each channel, creates an ObsPy Trace, and applies decimation (with anti‑alias filtering) usingtrace.decimate(decimation_factor). - Extracts only the newly added decimated samples and broadcasts them as JSON:
{ "channel": "EHZ", "timestamp": "2025-03-23T12:34:56.789Z", "fs": 25, "data": [123, 125, ...] } - Manages client connections, sending updates only to currently active clients.
- Why a thread? It uses asyncio, which runs in its own thread to avoid interfering with the other synchronous threads.
- Responsibility: Send rich push notifications when a seismic event is detected, including an attached interactive waveform plot.
- Operation:
- Maintains a rolling
dequebuffer sized at2 × 60 s × sampling_rate(default 12 000 samples per channel) — enough to hold 60 s before and 60 s after the trigger moment. - Continuously consumes packets from its queue and appends them to the buffer; this ensures the pre-event context is already available the moment a trigger fires.
- When
earthquake_eventis set and at least 30 s have passed since the last notification (cooldown), it immediately dispatches an alert via Apprise:⚠️ Earthquake Alert — Significant seismic activity detected! - It then enters
_handle_event(), which waits until the buffer accumulates a furtherpoints_per_windowsamples (≈ 60 s of post-event data), or until shutdown is requested. - Once the 120 s window is complete,
_generate_plotly_graph()flattens the buffer into a pandasDataFrame, builds a multi-subplot Plotly figure (one row per channel, shared X-axis), and serialises it as a self-contained HTML file. _send_notification()writes the HTML to a temporary file and passes it as an Apprise attachment — a workaround for Apprise's incomplete in-memory stream support.
- Maintains a rolling
- Why a thread? Waiting for 60 s of post-event data is a long blocking operation. Running it in its own thread prevents it from starving the trigger, writer, or WebSocket threads.
On startup, the application calls ensure_station_xml() to maintain a calibrated station.xml alongside the SDS archive. This file encodes the full GD-4.5 instrument response so that recorded waveforms can be properly deconvolved by analysis tools like ObsPy or SeisComp.
The response chain consists of two stages:
- PAZ stage – standard GD-4.5 poles and zeros in Laplace (rad/s) representation, with stage gain equal to the per-channel
sensitivity(V·s/m). - ADC gain stage – converts volts to digital counts, computed as
(adc_gain × 2²³) / vref.
Epoch management prevents accidental corruption of the archive's provenance:
| Scenario | Behaviour |
|---|---|
First run – no station.xml |
File is generated; a JSON sidecar (.sha256) is written to track the settings fingerprint and start_date. |
| Settings unchanged | Nothing happens. |
Settings changed, start_date unchanged |
Application refuses to start. You must update start_date to the date/time of the hardware change. |
Settings changed, start_date updated |
Open channel epochs are automatically closed (their end_date is set) and new epochs are appended. The sidecar is updated. |
⚠️ Never deletestation.xmlor the.sha256sidecar. Both files should be kept in version control alongside the data archive.
+-------------+
| Arduino |
| (100 Hz) |
+------+------+
| RS-422 (250 kbaud)
v
+-------------------------------------------------+
| Reader Thread |
| - Settings handshake on startup |
| - Reads serial, verifies checksum |
| - Sends heartbeat every 500 ms |
| - Distributes packets to all queues |
+--------+----------+------------+----------------+
| | | |
v v v v
+--------+ +----------+ +------+ +----------+
|mseed_q | |trigger_q | | ws_q | |notify_q |
+--------+ +----------+ +------+ +----------+
| | | |
v v v v
+----------+ +----------+ +----------+ +------------------+
|MSeedWriter| |Trigger | |WebSocket | | NotifierSender |
|- Buffers | |Processor | |Sender | | - Rolling 120s |
|- SDS | |- Recursive| |- Sliding | | buffer |
|- Midnight | | STA/LTA | | window | | - Immediate text |
| split | |- Sets | |- Decimates| | alert |
|- Early | | event on | |- JSON | | - 60s post-event |
| flush | | trigger | | broadcast| | data wait |
+----------+ +-----+----+ +----------+ | - Plotly HTML |
^ | | attachment |
| earthquake_event +------------------+
+--------------------+
The TriggerProcessor uses ObsPy's recursive_sta_lta function, which is numerically efficient and well-suited to continuous single-sample updates. The algorithm parameters are currently defined as class attributes in trigger_processor.py:
| Parameter | Default | Description |
|---|---|---|
sta_sec |
0.5 s |
Short-term average window length |
lta_sec |
10.0 s |
Long-term average window length |
thr_on |
3.5 |
STA/LTA ratio above which an event is declared |
thr_off |
1.5 |
STA/LTA ratio below which the event is cleared |
trigger_channel |
"EHZ" |
SEED channel name used for detection |
The rolling data buffer is sized at 2 × nlta samples to ensure a stable LTA baseline before ratios are considered meaningful. No trigger decision is made until the buffer is at least nlta samples deep.
A typical starting point for a quiet site is the default configuration above. Noisy environments may require a higher thr_on (e.g., 5.0–8.0) or a shorter sta_sec. These will be moved to YAML configuration in a future release.
- MCU no response on startup: Verify the serial port, baud rate (250 000), and that the Arduino firmware supports the settings handshake (
0xCC 0xDDecho). Check the wiring of the MAX485 DE/RE pin. - No data in MiniSEED files: Check that packets arrive with header
0xAA 0xBBand a valid XOR checksum. Enable debug logging in the Reader. StationXMLEpochErroron startup: You changed ADC settings or channel sensitivity without updatingstart_date. Setstart_dateinconfig.ymlto the date/time of the hardware change and restart.- GPIO errors: If running on a non‑Raspberry Pi (or without GPIO), the code automatically falls back to a mock pin factory. For real deployment, ensure
gpiozerois installed and the correct GPIO pin number is set in the config. - WebSocket not connecting: Verify the port (default 8765) is not blocked by a firewall and that the frontend points to the correct IP.
- Earthquake not detected: Tune the STA/LTA thresholds. The current implementation may need adjustment for your site's noise level and target magnitude range.
- UV not found: Follow the UV installation guide.
Contributions are welcome! Please open an issue or pull request for any improvements, bug fixes, or documentation updates.
GNU General Public License v3.0
- Inspired by the Raspberry Shake project
- STA/LTA algorithm based on common seismic processing practices
- Built with ObsPy – a great toolkit for seismology